Repository navigation
Speed up project-patch and purl checks (#1607) - #1701
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Every scan checks the project for its own patches (Yarn patch: locators, patch-package scripts) before touching a package, and hosted scans check again under the write lock. On a large Yarn Berry lock that check split every line of yarn.lock to find the few resolution lines that carry a patch: locator, and it recompiled the two patch-package regexes each time. Now it jumps straight to the "@patch:" occurrences and compiles the regexes once, only for a project whose scripts run patch-package. What it finds is unchanged. On the yarn-berry/rescan bench fixture this cuts about 20M instructions (~2%) from each scan. Refs #1607 Assisted-by: Claude Code:claude-opus-5-5
Scans key every crawled package's purl several times, and each key first checks whether the purl is already canonical. That check looked for '?', '#' and '%' by decoding every character. Scanning bytes gives the same answer (all three are ASCII, and no multi-byte UTF-8 character contains an ASCII byte) for a fraction of the cost: about 10M fewer instructions per yarn-berry/rescan bench run. Refs #1607 Assisted-by: Claude Code:claude-opus-5-5
The helper that finds yarn.lock lines holding a patch: locator now moves past each line it returns, so a needle that is empty or holds a newline can't keep matching the same spot. A test pins the documented line-ending rules instead of only comparing against str::lines. Refs #1607 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit d9b8faa. Configure here.
|
Ready for review (burn-down agent). Head Generated by Claude Code |
|
Labeled Ready for review (burn-down agent).
Generated by Claude Code |
Final review briefWhat it does: It speeds up two checks that run on every scan. The yarn.lock patch scan now jumps straight to the lines that contain Risk: low. It's a local refactor on two pure functions. Look here:
Verified: I read the full diff and the loop body around the call site. It stays within its scope, has no debug leftovers and leaves Changes I made: none. Open questions: none for this PR. Whether to do a binary-size pass for #1607 is a maintainer call (noted on the issue). Auto-merge is armed, so approving sends it straight to the merge queue. Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Refs #1607
This speeds up two spots on every scan's path, the project-patch check and the purl key check, and cuts
yarn-berry/rescaninstructions by 2.7%. Most of #1607's leftover wall time and peak RSS turns out to come from the binary's growth, not from a hotspot (see What's left). So this PR usesRefs, notFixes.Root cause
Callgrind of
yarn-berry/rescan, basecc938aa6vs main3114a28: 1,047M → 1,103M instructions (+56M). The largest new cost is the project-patch check (UserPatchIndex::discover, ~15.5M per call). It didn't exist atcc938aa6, and it runs twice per hosted scan: once inrun_scanand once more under the write lock inrun_redirect_selected_untracked. Inside it:resolution:lines with a@patch:locatorcanonical_base_purl's "already canonical" fast path (#1693) also checked for?/#/%with a per-char decode. It runs ~25k times per scan, about 700 instructions each.Changes
user_patches.rs: a newlines_containing(text, "@patch:")searches for the needle and widens each hit to its line, instead of walking every line. Its output matchesstr::lines().filter(..): a\rbefore\nis dropped, a final bare\ris kept, and each line comes back once even with several hits. The loop body is unchanged, and it already required@patch:in the value, so it sees the same lines.user_patches.rs: the patch-package--patch-dirandcdregexes areLazyLockstatics. They compile once, on first use, only when a script mentions patch-package.purl_key.rs:is_canonical_basescans bytes for?/#/%. All three are ASCII, and no multi-byte UTF-8 sequence contains an ASCII byte, so the answer is the same.The second project-patch check under the write lock is deliberate: it re-reads declarations so a patch added during API selection isn't lost. I kept it. It is now cheap.
Evidence
Scan
--jsonoutput on the bench fixture is identical before and after, apart fromdurationMs.Callgrind,
yarn-berry/rescan: base 1,047M, main 1,103M, this PR 1,073M.socket-patch-bench compare, 25 interleaved pairs (+10 confirm), 4 vCPU sandbox:3114a28→ this PRyarn-berry/rescan3114a28→ this PRyarn-berry/hostedcc938aa6→ mainyarn-berry/rescancc938aa6→ this PRyarn-berry/rescanThe wall gain against main is within noise. The instruction cut is real but small next to the remaining gap.
What's left (why #1607 stays open)
The remaining gap is not a hotspot:
strace -c: openat, statx and read are within 1–2% of base. Only 4 more threads.HashMapinserts and rehashes (+15k inserts) and sip hashing, from features added sincecc938aa6.A deliberate binary-size pass, or re-baselining the bench, is a maintainer call. I've noted it on #1607.
Tests
patch::user_patches::tests::lines_containing_matches_a_filtered_lines_walk: equivalence withlines().filter(..)over edge cases (CRLF, bare\r, several hits per line, no trailing newline, non-ASCII), plus the documented contract pinned with literal expected valuespatch::user_patches::tests::yarn_lock_patches_are_found_among_many_unpatched_entries: a CRLF lock with 200 plain entries, where the user patches at the start and end are found and a builtin compat patch is notutils::purl_key::tests::canonical_fast_path_matches_the_rebuild: now also covers non-ASCII names, with and without%and?Local runs:
cargo fmt --all -- --check: cleancargo clippy --workspace --all-features -- -D warnings: cleancargo test --workspace --all-features --no-fail-fast: 14,873 passed, 11 failed. All 11 are environmental, and none touches the changed code:e2e_bun_lockb::binary_shared_bundled_record_hosted_pin_is_managed, which needspatches-api.socket.devand this sandbox has no network route to itNo npm/pypi/gem wrapper changes: the wrappers only dispatch to the binary.
/code-review high
No correctness findings. Fixed:
lines_containingcould stall on an empty or multi-line needle. It now moves past each line it returns, with adebug_asserton the needle.lines. It now also pins literal expected values.Not changed:
lines().filter": that is the slow path this PR removes. The helper is 15 lines and has its own equivalence test.trim()is still Unicode-aware": it only looks at the two ends of the string, which doesn't show up in the profilehttps://claude.ai/code/session_01CCk3YDBWYBfufBX3DuwRTs
Generated by Claude Code