Repository navigation
perf(js): indexing a JavaScript or TypeScript project is faster - #519
Merged
Merged
Conversation
…l require call
The CommonJS require pass re-parsed every JavaScript and TypeScript file
whose text contains "require" to walk its calls and scopes. Most such files
have no call it can read: `typeof require`, `required: true`, prose in
comments, `__webpack_require__`, package names like `require('jquery')`.
The pass now checks the text first: `require`, an optional `?.`, `(`, empty
argument slots, then a quoted module path, skipping whitespace and comments
the way the grammar does. Anything the check cannot read (type arguments,
escapes, non-ASCII spacing, HTML-like comments) still goes to the tree walk.
The check holds only for a tree without syntax errors: error recovery turns
`require@('./x')` into a call. The kernel already knows whether its parse
of the file had errors, so it now reports that in a spare byte of the
result header (bit 0 set means a clean parse; an older kernel leaves it 0,
which keeps the walk). The check applies only when that bit is set and the
kernel parsed the file's own text rather than a Flow-blanked copy.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…monjs-require-scan
This was referenced Oct 18, 2026
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.
Indexing pretix is 4.9% faster, a median 10.17 s down to 9.67 s with its parse phase 16% shorter, and indexing supabase uses 6.7% less CPU (full index on Node 24, 3 alternating runs per build, identical graphs).
The chart shows pretix. Its "no clear change" line is 3.2% of the baseline, the largest spread between two identical builds of this change on any pretix row. supabase is in the table below, where each row is judged against its own corpus's spread.
What changed
The fork finds file dependencies from CommonJS calls such as
require('./x')by walking a tree-sitter tree, so that a shadowedrequire(a parameter, a local, an import namedrequire) and string or comment text are left out. The kernel's extraction tree is not available on the JavaScript side, socommonJsRequireRefsparsed every JavaScript or TypeScript file that contains the wordrequirea second time. Few of those files have such a call: 4 of 112 in pretix, 27 of 1,126 in supabase, 17 of 4,499 in n8n. The rest saytypeof require,required:,__webpack_require__orrequire('lodash').Now (
src/extraction/commonjs-requires.ts):mayRequireModulePathreads the text at eachrequire: an optional?.,(, any empty argument slots, then a quoted literal whose text is a relative path, a#import or a path with a slash, which is what the walk accepts. It skips whitespace and//and/* */comments the way the grammar does. Anything it cannot read counts as a possible call and the walk runs as before: non-ASCII or control characters between tokens,<!--/-->, an unclosed comment, TypeScript type arguments, an escape,${in a template, a line break in a quoted string.require@('./x'). The kernel already computeshas_error()on its tree;codegraph-kernel/src/buffers.rsnow writes it into byte 1 of the result metadata, which was zero padding. An older kernel leaves the byte 0, read as "unknown", so the buffer layout stays at version 3.src/extraction/kernel/index.tsreads it, and also requires that the kernel parsed the same text (a Flow file is blanked before parsing).commonJsRequireRefsskips the second parse only when the kernel reports a clean parse and the text has no possible call. The kernel path and the parse worker's raw path pass the flag; the remaining caller passes nothing and keeps the old behavior.Results
Full index (
codegraph init), before =fork/consolidatedd161ef74, after = this change on that base (the rebase onto #516, #517 and #518 adds commits that touch none of these files). Medians of 3 runs, builds alternating within each round, with a third build that is an identical copy of this change. A change counts when the medians differ by more than that corpus's floor (largest minus smallest of the six runs of the two identical builds) and the ranges do not overlap; otherwise "no clear change". All runs: Node 24.22.0,systemd-run --user --scope -p MemoryMax=16G, admitted only on a quiet host, rejected when other processes used more than 2 cores."Parse phase" is the indexer's own
parse-looptiming (CODEGRAPH_SYNTH_TIMINGS). supabase's wall time moved by about half its floor: its third round ran on a quieter host for every build (all four builds were 1-2 s faster than in rounds one and two), which widened the spread.Where the time went: CPU profiles of one extra run per build and corpus (V8
--cpu-profon every thread; these runs are not among the timing samples) put the require scan (commonJsRequireRefs, summed over threads) at 1.14 s before and 0.28 s after on pretix, and 2.12 s before and 0.11 s after on supabase. Of the 0.28 s left on pretix, 0.20 s is the second parse of files that still need it.Graphs are identical: on every accepted run, the after build and its identical copy produce the same graph hash as the before build on the same corpus (pretix
bc3469fcdcfc, supabasef41125aee6df). 24 runs accepted, 1 rejected and re-run (an upstream pretix run where other processes used 2.4 cores).Against upstream (
e8ec7fd8, same runs): pretix's index time goes from 21% above upstream's (8.39 s) to 15% above it. supabase's index time stays within the spread of upstream's (19.98 s), and its CPU time goes from 3.7% below upstream's (77.82 s) to 10.2% below it.The text check was also compared file by file against the old walk through the real kernel flag, on every JavaScript or TypeScript file containing
requirein ten projects (pretix, n8n, supabase, nocodb, trezor-suite, fastify-demo, nest, discourse, koel, halo): no file's references changed. A generated-source fuzz of 1,000,000 cases (3,579 of them parse cleanly and yield a reference) changed no output.Checks
On the final head
f6509ce6(rebased onto0e6f7e30):npm run build:kernel(clippy included) andnpm run build: exit 0.UPDATE_GOLDEN=1 npx vitest run __tests__/kernel-golden-dumps.test.ts: exit 0, 8 passed,.dumpdiff empty.npx tsc --noEmit,npm run check:agent-docs: exit 0.npx vitest run: exit 0, 747 files, 9,268 passed, 40 skipped (includesreadme-sync, 6 passed, and the new test).On the code of
3d0d8b42(the same change before the rebase):cargo test --release --lib buffers: exit 0, 2 passed.__tests__/commonjs-require-prefilter.test.ts: 5 passed on Node and on Bun 1.4.3; on the based161ef74, 3 of the 5 fail. It checks that a clean file that only mentionsrequireis not parsed again; thatrequire /* c */ ?.(...),import x = require(...),require<T>(...)andrequire(, './d')still are; that a file with a syntax error keeps the walk; a property test over 6,000 generated sources (output equals the walk's on every clean one); and the kernel flag itself. With the empty-slot skip removed, the property test fails.README rows checked
Not covered
src/extraction/tree-sitter.ts), used when the native walker does not handle a file or is switched off withCODEGRAPH_KERNEL=0, still parses again.Summary by cubic
Speeds up indexing projects that mix in JavaScript or TypeScript by skipping the redundant second parse of files that mention "require" without a
require('./x')call. A full index of pretix drops from a median 10.17 s to 9.67 s (parse phase 2.48 s → 2.07 s); supabase uses 6.7% less CPU. Graphs are identical.What changed
typeof require,required:,__webpack_require__, or prose. A text check now runs first, and the second parse happens only when the text could hold arequirecall with a module path argument, or when the file has a syntax error.require@('./x')into a call. The kernel now reports its parse status in a spare metadata byte; an older kernel leaves it 0, which reads as "unknown" and keeps the old behavior, so the buffer layout stays at version 3.CODEGRAPH_KERNEL=0).Written for commit 506e4a0. Summary will update on new commits.