aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-30 23:47:03 -0700
committerAdam Soltys <[email protected]>2026-07-30 23:47:03 -0700
commitc264f12ded66f60ab137d05a6e2449f17c98ad68 (patch)
treefdb1e79bb1983fce89a60a2c434d48d42980e930 /src/features
parent6713f8d4e55db8d5344684b08b062cc9129ef6b0 (diff)
downloadcoinosv3-c264f12ded66f60ab137d05a6e2449f17c98ad68.tar.gz
coinosv3-c264f12ded66f60ab137d05a6e2449f17c98ad68.zip
Fix silent-payment catch-up pegging the CPU (both threads)
A wallet a few thousand blocks behind gets the whole scan window from the indexer at once (~1M tweaks over ~3900 mainnet blocks). The catch-up loop chunked by 100 BLOCKS, but the cost is one EC point-multiply per TWEAK and mainnet blocks are wildly uneven (up to 2565 tweaks each) — so a 100-block chunk could be ~25k tweaks, seconds of work that blew the worker's 30s timeout. On timeout the caller ran the SAME chunk inline on the main thread while the orphaned worker kept grinding it, pegging both threads and freezing the visible tab for the whole ~20min catch-up. Two fixes: - Chunk by tweak count (budget 2000, always ≥1 block) instead of block count, so every worker call stays sub-second regardless of block density. Verified against live mainnet data: max chunk drops from ~25k tweaks to ~2.8k (~850ms, <3% of the timeout). - On a worker-call timeout, TERMINATE the worker instead of leaving it grinding the orphaned request behind the caller's inline fallback — that double-grind was the both-threads-pegged symptom. _ensureSpWorker respawns a fresh worker on the next call. Diagnosed via CDP CPU profiling of the live tab (95% self-time in _spHitBlocks -> _spCandidate -> secp256k1 wNAF/precompute). Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat (limited to 'src/features')
-rw-r--r--src/features/sp-wallet.js41
1 files changed, 29 insertions, 12 deletions
diff --git a/src/features/sp-wallet.js b/src/features/sp-wallet.js
index 116f2e0..2ac3e70 100644
--- a/src/features/sp-wallet.js
+++ b/src/features/sp-wallet.js
@@ -103,7 +103,19 @@ export function installSpWallet(wallet) {
const id = ++this._spReqId;
this._spPending.set(id, { res, rej });
this._spWorker.postMessage({ id, op, ...data });
- setTimeout(() => { if (this._spPending.has(id)) { this._spPending.delete(id); rej(new Error('sp worker timeout')); } }, 30000);
+ setTimeout(() => {
+ if (!this._spPending.has(id)) return;
+ // Timed out. TERMINATE the worker rather than let it keep grinding the
+ // orphaned request in the background — otherwise the caller's inline
+ // fallback runs the SAME work on the main thread and both threads peg.
+ // _ensureSpWorker() respawns a fresh worker (and re-sends keys) on the
+ // next call, so this only costs us the current chunk. With the tweak
+ // budget above, calls stay sub-second and this should never fire.
+ try { this._spWorker && this._spWorker.terminate(); } catch {}
+ this._spWorker = null;
+ for (const p of this._spPending.values()) p.rej(new Error('sp worker timeout'));
+ this._spPending.clear();
+ }, 30000);
});
},
@@ -222,17 +234,22 @@ export function installSpWallet(wallet) {
const have = new Set(this.spUtxos.map((u) => `${u.txid}:${u.vout}`));
const found = [];
const blocks = res.blocks || [];
- // Process in chunks. An imported wallet's first catch-up can be the
- // indexer's whole window (hundreds of thousands of tweaks, ~25MB) —
- // sent as ONE worker message it blew the 30s worker timeout, fell
- // back to the inline loop (janking the UI for minutes while the
- // worker kept grinding the orphaned request), and held the giant
- // buffers live the whole time. Chunks keep each call sub-second, let
- // the scan watermark advance as blocks complete (interruptions
- // resume instead of restarting), and free memory as they go.
- const CHUNK = 100;
- for (let c = 0; c < blocks.length; c += CHUNK) {
- const slice = blocks.slice(c, c + CHUNK);
+ // Process in chunks bounded by TWEAK count, not block count. The cost is
+ // one EC point-multiply per tweak, and mainnet blocks are wildly uneven
+ // (a few to 2500+ tweaks each). A fixed 100-block chunk could be ~25k
+ // tweaks — seconds of work that blew the 30s worker timeout, which then
+ // dropped the same chunk onto the main thread while the orphaned worker
+ // kept grinding it (both threads pegged). Budgeting by tweaks keeps every
+ // worker call sub-second regardless of block density; the watermark still
+ // advances per chunk so interruptions resume instead of restarting.
+ const TWEAK_BUDGET = 2000;
+ for (let c = 0; c < blocks.length; ) {
+ // grow the slice until it holds ~TWEAK_BUDGET tweaks (always ≥1 block,
+ // so a single dense block still goes as its own chunk).
+ let end = c, tw = 0;
+ do { tw += (blocks[end].tweaks || []).length; end++; } while (end < blocks.length && tw < TWEAK_BUDGET);
+ const slice = blocks.slice(c, end);
+ c = end;
const hits = await this._spHitBlocks(slice);
for (const block of slice) {
if (!hits.has(block.height)) continue;