From 76e1a88b9c2eae1de0145ff077f038b57e68c3a8 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 5 Jul 2026 21:27:06 -0700 Subject: Chunk the silent-payment catch-up scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An imported mainnet wallet's first scan pulled the indexer's whole window (~336k tweaks, 25MB) and posted it to the worker as ONE message: it blew the 30s worker timeout, fell back to the inline main-thread loop (minutes of jank) while the worker kept grinding the orphaned request — the runaway CPU/memory users saw. The catch-up now processes 100-block chunks: each worker call is seconds, the scan watermark checkpoints after every chunk (an interrupted catch-up resumes instead of restarting from zero), buffers free as it goes, and the UI yields between chunks. Verified against the live indexer: full catch-up in ~5min of background work, main heap bounded (~80MB peak), no timeouts. Co-Authored-By: Claude Fable 5 --- src/features/sp-wallet.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) (limited to 'src/features') diff --git a/src/features/sp-wallet.js b/src/features/sp-wallet.js index c2d1cdb..e72532c 100644 --- a/src/features/sp-wallet.js +++ b/src/features/sp-wallet.js @@ -212,18 +212,34 @@ export function installSpWallet(wallet) { const tip = res.tip || 0; const have = new Set(this.spUtxos.map((u) => `${u.txid}:${u.vout}`)); const found = []; - const hits = await this._spHitBlocks(res.blocks || []); - for (const block of res.blocks || []) { - if (!hits.has(block.height)) continue; - const blk = await (await fetch(`${indexer}/block/${block.height}`)).json(); - for (const f of await this._spScanItems(blk.items || [])) { - const id = `${f.txid}:${f.vout}`; - if (have.has(id)) continue; - have.add(id); - found.push(f); + 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); + const hits = await this._spHitBlocks(slice); + for (const block of slice) { + if (!hits.has(block.height)) continue; + const blk = await (await fetch(`${indexer}/block/${block.height}`)).json(); + for (const f of await this._spScanItems(blk.items || [])) { + const id = `${f.txid}:${f.vout}`; + if (have.has(id)) continue; + have.add(id); + found.push(f); + } } + const maxH = slice[slice.length - 1].height; + if (maxH > (this.lastSpScan || 0)) { this.lastSpScan = maxH; this.saveCache(); } // checkpoint + await new Promise((r) => setTimeout(r, 0)); // let the UI breathe } - if (tip > this.lastSpScan) this.lastSpScan = tip; + if (tip > (this.lastSpScan || 0)) this.lastSpScan = tip; // Verify newly-found outputs are unspent + get confirmation status. for (const f of found) { const address = this._spAddress(f.xonly); -- cgit v1.2.3