Repository navigation
perf(scan): keep the js thread free while scanning - #213
Open
theanmolsharma wants to merge 4 commits into
Open
theanmolsharma wants to merge 4 commits into
theanmolsharma wants to merge 4 commits into
Conversation
Each 50-block window went to Rust in one synchronous JSI call, which holds the JS thread for its whole run: taps, timers and JS-driven animation all wait on it. On the simulator a 27,578-transaction signet window held it for 286ms, and phones are several times slower. Windows now go in slices of 1,000 transactions with a macrotask between them, so the longest hold is 5-37ms on the same simulator, for about the same total time. A cancel between slices throws instead of returning what was found so far: the caller marks the whole window scanned once the batch resolves, so a partial result would skip the rest of the window for good. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every packet of a response was appended to one growing Buffer, and a chunked body was re-decoded from its start on every packet: work quadratic in the response's size, on the JS thread, for each window a scan fetches over Tor (over a gigabyte copied for a 2MB body). The headers are buffered until their terminator, and the body is kept as the packets that carried it and joined once at the end. A chunked body is decoded as it arrives. Everything else behaves as it did: the size cap, a terminal chunk completing without trailers, a malformed or truncated body never completing (so the socket closing rejects it), and close-delimited bodies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
theanmolsharma
force-pushed
the
theanmolsharma/scan-js-thread
branch
from
October 19, 2026 07:10
8373c50 to
d1f44f1
Compare
theanmolsharma
added this pull request to stack #215
October 19, 2026 08:22
A yield costs up to a frame on iOS: RCTTiming runs timers under 18ms on the next display frame. So yielding after every 1,000-transaction slice left a fast device idle about as long as it scanned. Slices now run back to back until they've held the JS thread for 30ms, then yield. A slow device still yields after every slice, since one takes that long on its own. A cancel is checked between every two slices, yield or not. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third of four stacked PRs, on top of #212. Taps on the Sync screen, Pause especially, could freeze during a scan because the scan held the JS thread.
What changes
Smaller Rust calls. Each 50-block window went to Rust in one synchronous JSI call (
spScanTransactions), which holds the JS thread for its whole run.Measured on the iOS simulator against signet:
Phones are several times slower, and mainnet windows are bigger.
Windows now go in slices of 1,000 transactions. Once slices have held the thread for 30 ms, the scan yields with a macrotask (
setTimeout(0)); a microtask or thesetImmediateshim would run before touches do.The longest hold on the same simulator dropped to 5–37 ms.
Why yield only every 30 ms: a yield isn't free on iOS.
RCTTimingruns any timer under 18 ms on the next display frame, so each yield idles for up to a frame. Yielding after every slice made a fast device sit idle about as long as it scanned. Android runs zero-delay timers immediately. A slow device still yields after every slice, because one slice takes that long on its own.A cancel between slices throws
SCAN_CANCELLEDrather than returning what was found so far. The caller marks the whole window scanned once the batch resolves, so a partial result would skip the rest of the window for good.Linear Tor buffering.
socks5Fetchappended every packet to one growing Buffer and re-decoded the whole chunked body on every packet. That is quadratic, on the JS thread: over a gigabyte copied for a 2 MB body.Runtime version
JS only, so it ships over the air. Moving the scan off the JS thread entirely needs an async JSI call on a worker thread, which means a native change and a new binary. That's a follow-up.
Testing
rustTransactionProcessor.test.ts:sp-scan-lifecycle: a cancel in the middle of a window leaveslastScannedBlockunchanged.socks5Fetch.test.ts:Content-Length: 0;[]on cancel each fails tests.Stack
🤖 Generated with Claude Code