Repository navigation
perf(store): find chunk cutoffs and code fences by binary search - #1103
Open
mggarofalo wants to merge 1 commit into
Open
mggarofalo wants to merge 1 commit into
mggarofalo wants to merge 1 commit into
Conversation
findBestCutoff walked every break point from the start of the document for each chunk, and isInsideCodeFence scanned every fence for each break point in the window. Both lists are sorted, and findCodeFences never returns overlapping regions, so each lookup can jump straight to the one place that matters. Chunk boundaries do not change. The new tests compare both functions with a linear walk on documents that put fences and break points where an off-by-one would show, and bound how many elements each lookup reads. Fixes tobi#1102 Co-Authored-By: Claude Opus 5.5 <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.
Fixes #1102.
This makes chunking fast on documents with many break points and code fences, and chunk boundaries stay the same. A 1 MiB document of nothing but fence lines goes from about 4.4 s to about 0.1 s. A 10 MiB one goes from not finishing in 300 s to about 1.7 s. Ordinary documents gain little in absolute terms, because they were not slow to begin with.
The change
Both lookups now use binary search, through one small helper:
findBestCutoffjumps to the first break point in its window. It used to walk every break point from the start of the document for each chunk.isInsideCodeFencelooks only at the last fence that starts before the position. It used to scan every fence.The second one relies on fences being sorted and not overlapping.
findCodeFencesguarantees that: each region ends no later than the next\n+```match starts, and regions are appended in order. The differential test below checks this on every input instead of assuming it. Neither function is exported from the package entry point, and the two doc comments now state what they require. Open PR #1074 callsisInsideCodeFencewithfindCodeFencesoutput, so it stays compatible.Chunks do not change
A change in chunk boundaries would quietly alter what gets embedded, so I tested this against the old build directly. A throwaway script loads the build of 86d2365 and the build of this branch in one process and compares their output.
The inputs were:
chunkDocumentAsyncwith both strategies.findBestCutoffcallsisInsideCodeFencecalls, at every position up to 20,000To check that the script can fail, I ran it against 5 deliberately broken builds: an inclusive fence end, an inclusive fence start, a dropped window-edge break point, a skipped first break point, and an ignored first fence. It reported differences on all 5, between about 15,000 and 260,000 each.
I did not run the token-based path with a real tokenizer, because this work used no models. That path calls the same
chunkDocumentandchunkDocumentAsynccompared above.Before and after
Times are the median of 5 runs, with the fastest and slowest in brackets, unless noted. Each run used a fresh process, with builds interleaved. They cover the break point scan, the fence scan and chunking together, at default sizes.
```lines```lines```lines```linesThe candidate loop in
structuredSearch, with 40 candidates of 256 KiB and 3 runs each, goes from 5.3 s to 0.32 s on fence lines and from 103 ms to 67 ms on prose with short code blocks. The timings come from Node 24.18 on Windows 11 with a Ryzen 9 9900X, on a machine busy with other jobs. Issue #1102 has the full table and a reproduction script.The scan is now the larger cost, and this PR does not change it. At 10 MiB of nothing but newlines, it takes about 5 s and 1.5 GiB of memory.
Tests
test/store.test.tsgains 8 tests:Results, on the same commit, for
mainand this branch:maintscfor the build and the whole project, Linuxbun test, LinuxThe failing tests are the same on
mainand this branch in all 3 runs. I did not look into their causes, though I ran withQMD_FORCE_CPU=1and no GPU, which may explain theQMD_LLAMA_GPUones. The Linux runs used WSL with Node 24.18 and Bun 1.4.4, which differ from the versions in your CI. I did not run on macOS or on Node 22 or 23, and tests that need models were skipped.I ran the code checks on the commit before the changelog entry, which is the only later change.
Not included
Maplimit inscanBreakPoints. It throws above 2^24 newlines, which the 10 MB file limit keeps indexing from reaching.#496 proposed the same binary searches, and its author closed it without a review. This PR adds the measurements at the sizes where the problem shows, the differential check, and tests.
Differential script
Usage:
QMD_REPO=<checkout> node differential.mjs <old store.js> <new store.js> <fuzz iterations> [seed]An AI agent (Claude Code) wrote this change, ran these measurements and wrote this description on behalf of the account owner.
🤖 Generated with Claude Code