Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters - #257
Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters#257Mikola Lysenko (mikolalysenko) wants to merge 205 commits into
Conversation
`ordered_concurrent` / `map_ordered_concurrent` wrap `stream::iter(..).map(f).buffered(limit)`: at most `limit` requests in flight, results yielded in input order, nothing started until polled. The serial patch-API loops can adopt it and fold results exactly as before. `API_CONCURRENCY` (8) and `PROXY_API_CONCURRENCY` (4) carry the per-client caps. futures-util was already in the lock; it is now a direct dependency of core and the CLI. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
New subprocess suite for the three patch-API loops `scan` drives (batch POSTs, per-package detail GETs, hosted record views). Every mock answers later requests first, so an implementation that folds in completion order, or lets a discarded response leak in, changes the output: - batch: a 401 on the first chunk sends that chunk and all later ones to the proxy with one auth request and one warning; a 401 on chunk 3 of 6 folds 0-2 from the auth API and replays 3-5 on the proxy; per batch 500 warnings print in chunk order; the all-failed error carries the last chunk's error. - details: partial-failure warnings print in package order and the whole human preview equals a zero-latency run; the all-failed error names the last package. - hosted wet run: record_fetch_failed warnings keep confirmed order and stdout, lockfile and ledger equal a zero-latency run. The suite passes against the current serial loops (checked with the baseline binary) and is the oracle for making them concurrent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`fetch_patch_details` awaited one `by-package` GET per package with patches (74 on depscan, ~10 s of serial round trips). The queries now run through `ordered_concurrent` (8 in flight, 4 on the public proxy) and are consumed in `packages` order, so `results`, `failures`, the warn-after loop and the all-failed rule see exactly what the serial loop produced. `ApiClient::uses_public_proxy` picks the cap. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The batch loop POSTed one chunk at a time (56 chunks on depscan). Chunks now run through `ordered_concurrent` and are consumed strictly in chunk order, so per-batch warnings, `batch_error_count`, `last_batch_error` and the paid-access flag fold as before. The authenticated-to-proxy downgrade keeps the serial loop's exact sequence: the first chunk goes alone (a stale token still costs the auth API one request), and at the first consumed chunk k whose error is a fallback candidate — any index — the window is dropped, responses for chunks past k are discarded unfolded, the same warning prints, chunk k is retried on the proxy and the rest continue there (4 in flight). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A wet hosted run fetched `patches/view/{uuid}` for every confirmed
redirect one at a time (74 on depscan, ~9 s). The views now run through
`ordered_concurrent` and are consumed in `confirmed` order, so `records`
(newest wins) and the `record_fetch_failed` warnings are unchanged. The
ledger re-fetch on idempotent re-runs is deliberately kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Scan awaited each telemetry POST inline (150-300 ms typical, up to the 5 s budget on a bad network) before carrying on. Its three events now go through `spawn_patch_scanned` / `spawn_patch_scan_failed`: the event is built and its endpoint resolved where it fires (same body, timestamp, env reads and "Sending telemetry" debug line), and only the POST runs in a background task. `scan::run` awaits `PendingTelemetry::flush` before returning, so every event is still delivered, or given up on within the same 2 s connect / 5 s request budget, before the process exits. The inline trackers and every other command are unchanged. Tests: core unit tests pin that a background send posts the same bytes and headers as an inline one and that flush waits for it; telemetry_e2e pins that each scan terminal (success, empty crawl, all batches failed) delivers its one event and stays alive until the slow endpoint answers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The background send was only awaited after run_scan returned, so a process killed after the event fired but before that flush lost it: `scan | head` / `scan | true` dies of SIGPIPE on its first result write (main restores SIG_DFL), and a Ctrl-C at a confirm prompt or a CI SIGTERM had the same effect. The inline send it replaced had always landed before any output. `PendingTelemetry::flush` now drains (`&mut self`), and scan flushes at the first output point after each event fires: right after the send on the empty-crawl and all-batches-failed terminals (they print at once), at the start of the human section (before the table, prompts and every human exit), before the plain `--json` envelope, and inside `discover_selected` right after the detail fetches (before its error line and whatever the `--apply`, hosted and vendored `--json` arms print next). The send still overlaps the by-package detail fetches on those arms; the flush at the end of `run` stays as the exit backstop. Under `--debug` this also puts the human path's "Telemetry sent" line back ahead of the per-package detail warnings, as in the inline order. Tests: telemetry_e2e runs each JSON terminal with stdout closed before the child writes and requires the event delivered (red on the previous commit: SIGPIPE, 0 events); a core unit test pins that flush drains and that sends started after it join the next flush. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`track_patch_scan_failed` and `spawn_patch_scan_failed` each spelled out
the `{"fallback_to_proxy": ...}` literal; build it in one place, as
`patch_scanned_metadata` already is for the success event, so the inline
and background paths cannot drift. The inline trackers stay: they are
public API of the published core crate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
No production caller used `map_ordered_concurrent`: every API loop consumes `ordered_concurrent` directly. Move it into the tests module so it no longer ships as unused public API. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… + ordered merge `crawl_all` and the workspace roots walk made one `spawn_blocking` round trip per readdir, stat and package.json read, strictly in sequence. Both now run as one blocking-pool task: directory I/O is gathered in parallel (rayon, already in the dependency graph via qbsdiff) into per-root event trees that record the sequential visit order, and a single-threaded merge replays them so the order-dependent `seen` dedup and the store entries' `identity_seen` decisions see exactly the state the old walk saw — same packages, same paths, same order. Two probes are answered from listings the walk reads anyway, only where that is provably the same answer: - the roots walk skips the `is_dir(child/node_modules)` stat when the child's complete listing holds nothing that could alias `node_modules` on a case-insensitive filesystem (a listed dir still stats: a readable-but-unsearchable parent lists kinds while stats fail); - a store entry's `node_modules` existence probe is the readdir the scan needs next; a dir that does not open falls back to the stat. FIFO-safe package.json reads (read_regular_to_string_sync), the NESTED_STORE depth/dir caps (kept sequential: the budget order decides survivors), symlink-not-traversed rules and lossy-vs-raw name joins are unchanged. The previous async implementation is kept verbatim as a #[cfg(test)] oracle; a randomized fixture test (flat/nested/legacy stores, scoped, live/dangling/store symlinks, duplicate identities, aliases, broken/BOM/FIFO/dir package.json, unreadable and unsearchable dirs, node_modules case variants) plus a kitchen-sink tree assert identical roots, crawl output, find_by_purls results and store enumeration. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ly listed names The resolver opened `<nm>/<target>/package.json` for every pending target in every visited node_modules — targets × dirs failed opens, each its own spawn_blocking hop — then listed the same dir again for the descent. Both passes now run as one blocking-pool task: each dequeued dir is listed once, a target is probed there only when the listing could hold its first path component, the surviving probes run in parallel and fold back in target order, and the same listing drives the descent (whose per-entry stats also run in parallel, appended in listing order). The name filter is a strict superset: it only engages for a complete, all-ASCII listing and matches ASCII-case-insensitively (APFS/NTFS), and components a filesystem can resolve to a differently spelled entry (non-ASCII, `~` 8.3 aliases, trailing dot/space) are always probed. BFS root-first order, every-copy collection, the name+version identity check, the pass-2 fallback and the store-entry name filter are unchanged. `.pnpm` entry names are still filtered after the `node_modules` stat, not before: an entry without one is a nested host whose synthesized children can match, so the stat decides the result. The oracle equivalence suite (now also covering case-variant package and scope dirs) asserts identical find_by_purls maps on every generated root. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`crawl_all_ecosystems` awaited each crawler in turn, and the crawlers that block (maven's walkdir walk + POM reads, `gem env`, the python site-packages probe, `composer global config home`) did so inline on the async task. The crawlers are independent — none prints, none mutates shared state — so they are now joined, with every blocking walk or subprocess moved onto the blocking pool, and their results are consumed in the fixed Npm, Pypi, Cargo, Gem, Golang, Maven, Composer, Nuget, Deno order, so packages and counts are exactly the serial run's. The joined futures are heap-allocated from a non-async constructor so the caller's poll frame does not grow by their combined size (Windows main-stack budget). `gem env gemdir` and `gem env gempath` run concurrently but are still two calls consumed gemdir-then-gempath (no single-call merge: platform path separators). A polyglot `--global-prefix` test pins the joined output against the serial sequence. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The previous commit fanned each visited dir's probes and descent stats out to the rayon pool separately, one injection per dir: on a deep pnpm tree the per-dir handoff latency outweighed the parallelism, and `apply --dry-run` on a large monorepo ran slower than the async walk. A visit's reads depend only on the dir and the fixed target list, never on what earlier dirs resolved, so the walk now proceeds level by level (exactly the FIFO queue's order: everything a dir enqueues lands behind the rest of its level). Each level's visits — listing, filtered probes, nested-dir discovery with the virtual store's entries returned whole — are gathered in one parallel pass, then the order-dependent part (folding matches into the result, the unmatched-name store filter, next-level order) is replayed sequentially in queue order. Output is unchanged; the oracle equivalence suite still covers it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ix targets Symlinks, FIFOs and permission stripping are generated on Unix only, so the fields that record them are never read elsewhere. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…'s use block Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The parallel roots walk recursed once per directory level on rayon and blocking-pool threads (2 MiB stacks), where the old async walk recursed through boxed futures on the 8 MiB main thread. A deep enough directory chain (reachable under Linux's 4096-byte PATH_MAX, and deeper on Windows long paths) aborted the scan with a stack overflow the old walk survived. Read the tree one level at a time, each level's dirs in parallel, record each dir's child range, then emit with an explicit stack in the same depth-first order. Stack use no longer grows with depth; a new test runs a 400-deep chain on 256 KiB walk threads (the recursive walk overflowed there). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… budget Two properties of the old sequential async walk did not survive the move to parallel sync walks on rayon's global pool: - Stack: the recursive node_modules gather ran on 2 MiB worker threads instead of the 8 MiB main thread. The npm walks now run on a dedicated walk pool whose threads get the main thread's 8 MiB. - Descriptors: every walker treats a failed read_dir/open, EMFILE included, as an absent dir, and the old crawl held one descriptor at a time with the nine crawlers run back to back. With one walk thread per CPU plus concurrent crawlers, depscan lost packages silently below `ulimit -n 24` (5349 of 5520 at 20) where the old crawl was intact down to 14. Under a soft RLIMIT_NOFILE below 128 the walk pool now gets one thread and the crawlers run one at a time (the old descriptor profile); above it the pool is capped at half of what is left after a 64-descriptor reserve. depscan now matches the baseline byte-for-byte at every limit from 16 to 256. New tests: pool sizing, a 4 MiB frame fitting on a walk thread, and an e2e scan under `ulimit -n 16` that must match the ample-limit JSON (the one-thread-per-CPU pool lost most of that tree there). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hosted pnpm rewriter re-parsed every lock (entries, the early shrinkwrap sniff, the residual gate) and rebuilt the whole lock string once per dep: O(deps x lock) work that cost ~430 ms of critical-path CPU on depscan's 2 MB lock with 74 redirected deps. Each lock is now parsed and key-indexed once; a dep's instances are found by binary search, the residual gate judges each instance on its post-splice body, and committed splices are applied in one pass at the end. A later dep that hits an already-spliced entry (a duplicate name@version override) folds the pending splices in and re-indexes first, so it re-reads the rewritten text exactly as before, and the vendored-marker scan runs over the post-splice text the same way. Output bytes, the FileEdit list (order and original fragments), warnings and refusals are unchanged: the previous implementation is kept as a test oracle and compared on a depscan-sized synthetic lock set, on 300 randomized mixes of every lock flavor, and on duplicate-override and peer-suffixed multi-instance cases. depscan wet run: pnpm-lock.yaml and redirect-state.json byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rently Deciding which pypi deps need hosted wheel metadata ran a full `rewrite_python_lock` (parse, a second parse for the source-scope check on script locks, mutate, serialize) per dep per lock, only to test the result for `Some`. The rewrite's refusal and not-applicable checks now live in one planning step that `rewrite_python_lock` and a new `PythonLockProbe` share: the probe parses each lock once and answers exactly `matches!(rewrite_python_lock(..), Ok(Some(_)))` per dep, and the rewrite no longer re-parses the lock for the scope check. The qualifying wheels are then downloaded through an ordered `buffered(8)` stream and folded in dep order, so `python_metadata`, the withheld artifacts and the `python_metadata_unavailable` skips are unchanged. The stream is inlined here (futures-util added with the same workspace spec as the scan-concurrency branch); it moves onto the shared ordered-concurrency helper once that lands. New tests: a probe/rewrite equivalence sweep over every lock shape and outcome, and a hosted scan whose slow first failure must still be reported before a fast second one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… lock The npm package-lock rewriter re-derived every `packages` entry's identity (the `node_modules/` key split plus the `name`/`version` lookups) for every dep, and the classic yarn.lock rewriter re-split every block's key patterns for every dep: O(deps x entries) work that dominated both rewriters' CPU. Each identity is now computed once per lock. npm entries keep theirs by map position (a rewrite only touches `resolved`/`integrity`, never a key, `name` or `version`); a yarn block's key and sole real package are recomputed whenever this run rewrites that block, so later deps still see its current text. Output bytes, FileEdits and warnings are unchanged: both previous implementations are kept as test oracles and compared on 400 randomized locks each (aliases, links, bundled copies, workspaces, v1/v2 dependency trees, alias-only and fork-substitution yarn keys, CRLF and mixed line endings, duplicate overrides). Rewrite-phase CPU on the lockfile-only benches: npm-socket 103 -> 61 ms, yarn-strapi 49 -> 32 ms (whole-process medians). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The indexed rewriter judges residuals inline, so the boundary test over `pnpm_unrewritten_instances` now covers only the test-only reference. Feed the same boundary locks through `rewrite_registry_redirect`: hosted, longer-version, scoped and snapshot keys never count, v6 nested-paren and v5 `_` instances are repointed, and only the unparseable instance is named in the refusal. The helper's doc comment now says what it is. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each in-flight wheel download buffers the whole wheel under its own body timeout and retry budget, so memory and link sharing scale with the limit; 4 keeps the overlapped round trips while halving that. The comment records what concurrency changes that output cannot see (status line names the awaited dep, debug lines interleave, Retry-After pauses one fetch). The order test now also records request arrivals and fails if `bbb` is not requested before `aaa`'s delayed response is due, so a regression to serial fetching is caught. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scan time is dominated by API round trips, so live timings are noisy and unrepeatable. scripts/perf/replay.py stands in for api.socket.dev, patch.socket.dev and the public proxy: `record` forwards and stores every response, `replay` serves only from the store with a fixed or recorded per-request latency (plus optional per-connection latency) and reports request counts per endpoint, max in-flight, connections and network span. Batch POSTs replay per purl, so a build that changes chunking or order still gets identical answers. The listener skips HTTPServer's getfqdn(), which stalls ~35 s under the macOS sandbox. scripts/perf/bench.sh drives it: `record`, `replay`, and `ab`, which runs BASE and NEW interleaved against one store and fails unless every run's stdout sha256 and exit code match the first BASE run. Stores hold real API responses (possibly paid-patch data), so bench.sh refuses a store path inside the repository. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Syntax-checks replay.py (py_compile) and bench.sh (bash -n), then drives the harness against a local upstream stub: record-then-replay with batch re-assembly across chunks and orders, miss/unknown-purl accounting, --fill, a 502 (never stored) for an unreachable upstream, per-request latency with max in-flight, the getfqdn-free bind, the in-repo store refusal, and an end-to-end `bench.sh ab` pass and sha-mismatch failure with fake CLI binaries. Picked up by the existing `unittest discover -s scripts/tests` CI step. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…current helper The inline stream::iter().buffered() from the wheel-metadata fan-out predates utils::concurrent landing; route it through ordered_concurrent with the same limit (4) and the same in-order fold. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… too 0ef24902 flushed the scan event before the first stdout write after it fires, but stderr raises SIGPIPE just as well (main restores SIG_DFL). Two stderr writers could run in that window with the send still in flight: the lenient redirect-ledger load's "Warning: <corrupt ledger>" (non-hosted JSON and human paths, before discover_selected or the human flush) and, on the report-only JSON arm, the GC and VEX build ahead of the envelope. The inline send it replaced was always delivered first. The ledger load is inlined at its scan call site so the send is flushed right before its warning (only when it warns, so the overlap with the detail fetches is kept), and the JSON arm flushes before the GC/VEX step instead of just before the envelope. The --apply arm's warnings already follow discover_selected's flush. Test: telemetry_e2e runs a scan over a malformed redirect ledger with stderr closed and requires the event delivered (red before: SIGPIPE, 0 events). It uses a well-shaped token so the token-shape warning does not kill the child before the event fires. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scan now sends patch_scanned / patch_scan_failed through the spawn_* variants, which leaves the inline trackers without an in-tree caller. They stay: socket-patch-core is published to crates.io, removing a pub fn is a breaking change there, and every other event keeps its inline tracker. The doc comments now say so, so a later cleanup does not read them as leftovers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
batch_fallback_mid_run_replays_from_the_failing_chunk checked the folded uuids and the proxied tail, but not that chunks 4-5 were ever sent to the authenticated API. A serial loop (or a window of 1) would never request them and still pass. Assert all 6 authenticated requests: chunk 0 alone, then the whole 1..6 window in flight, so the discard path really runs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On the public proxy scan runs up to PROXY_API_CONCURRENCY (4) batch windows at once. Each window's search_patches_batch degrades to the legacy per-package GETs (10 at a time) when /patch/batch rejects the chunk: a 400 from one exotic purl such as pkg:jsr, or an old proxy with no batch route. So a polyglot project on the proxy could put 4 x 10 by-package GETs in flight where the serial loop peaked at 10. That path swallows per-purl errors as "no patches", so extra load that saturates the proxy could change which packages come back. The client now holds a semaphore of PROXY_BATCH_PATH_CONCURRENCY (10) slots, shared by clones. Every proxy /patch/batch POST and every legacy per-package GET takes a slot, so all concurrent batch calls on one client stay within the old peak. A single call never waits: its groups of 10 fit the cap exactly as before. The authenticated API is untouched. Test: four concurrent batch calls of 10 purls each, all rejected with 400, keep at most 10 by-package GETs in flight and still reach 10 (red without the slots: 40). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…can't spawn When the walk pool could not be built (the OS refusing threads: a tight RLIMIT_NPROC or cgroup pids.max, or a huge RAYON_NUM_THREADS), run_walk ran the walk on the calling thread and the first parallel iterator then tried to build rayon's global pool, which needs the same refused threads and panics (exit 101) where the sequential walk succeeded. - Retry the pool build with half the threads on each failure, down to 1. - Route every parallel gather through walk_pool::par_map, which maps sequentially (in order) on a thread outside any rayon pool, so the no-pool fallback never reaches the global pool. - RAYON_NUM_THREADS can lower the walk thread count but no longer raise it past available_parallelism. Tests: halving build, par_map's sequential/ordered contract, run_walk's no-pool path, and the randomized oracle comparison with the pool off. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Under the group commit the purl is no longer failed closed: the whole commit fails and nothing is written. Rename it and pin that the failed commit leaves no journal behind. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and group commit CHANGELOG and CLI_CONTRACT: the cargo deferral's verifiable-crate scope and the file-artifact hash check; the gem gate's wet-only scope, text and precedence; the per-record version-2 edit format and its safety under an older binary; the barrier for in-place rebuilds (dropping the stale "commit granularity is unchanged"); and the journal's recorded originals, set-aside outcomes, symlink refusal, fail-closed replay, kept-journal commit failure and the post-commit go-patches removal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A journal that is unreadable, or names a path outside the lockfiles and ledgers or behind a symbolic link, was reported as "a file it covers changed since". Give it its own outcome and warning. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
| case | b4 | b5 | approved item |
|---|---|---|---|
| maven hosted dry | 8.36 G (3.02 s) | 3.31 G (2.77 s) | MVN-1 |
| maven hosted wet | 8.42 G | 3.51 G | MVN-1 |
maven scan --mode hosted --dry-run -e npm |
7.00 G (0.69 s) | 0.04 G (0.11 s) | MVN-4 |
| maven vendored fresh | 32.4 G (38.6 s), RSS 1,070 MB | 7.6 G (31.1 s), RSS 279 MB | MVN-1 + X11 |
| maven vendored re-run | 10.2 G | 5.1 G | MVN-1 |
| nuget fx-scale vendored fresh | 15.2 G, RSS 266 MB | 8.6 G, RSS 105 MB | X11 + V2 |
| nuget fx-real vendored fresh | 2.76 G | 2.12 G | X11 + V2 |
| pylock (12 patches) vendored fresh | 17.1 G (3.9 s), RSS 500 MB | 13.6 G (2.3 s), RSS 409 MB | X11 + V2 |
| cargo zed vendored fresh | 49.6 G (40.0 s), 492 requests | 38.9 G (34.1 s), 456 requests | X1b |
| cargo zed vendored re-run | 26.5 G (5.2 s), 162 requests | 26.6 G (4.3 s), 126 requests | X1b |
cargo zed vendor --offline re-run |
exit 1 (17.9 G) | exit 0 (26.9 G: the 36 in-sync checks now run instead of refusing early) | X1b |
| uv-big vendored re-run | 3.04 G (1.60 s), 28 requests | 0.89 G (0.73 s), 17 requests | X1b |
uv-big vendor --offline re-run |
exit 1 | exit 0 | X1b |
| depscan vendored fresh | 14.9 G (9.7 s) | 13.4 G (8.8 s) | V2 / X12 |
| depscan vendored re-run | 11.6 G | 12.1 G (same 131 requests, same output; within this fixture's spread) | — |
state.json size:
| fixture | b4 (v1) | b5 |
|---|---|---|
| maven | 29,843,313 | 15,065,386 (v2) |
| pylock | 43,896,582 | 21,964,210 (v2) |
| nuget fx-scale | 7,221,990 | 3,873,321 (v2) |
| nuget fx-real | 743,209 | 449,784 (v2) |
| cargo, go, gem, composer, poetry, uv, depscan pnpm | — | unchanged, byte-identical v1 |
Every observed difference, and the approved item it maps to
54 steps × 2 binaries × 2 reps. The checks: canonical stdout, stderr, full project and home tree digest, file list, and request/grant counts.
- cargo-v fresh/re-run (X1b). b4 emits 36
vendor_fetched_missingand makes 36 more registry GETs. Dropping those events, stdout is identical, and so are the tree and ledger. - uv-v re-run (X1b). 11
vendor_fetched_missingand 11 GETs are gone. Otherwise identical. - cargo-v / uv-v
--offlinere-run (X1b).- cargo:
partialFailure, 36package_not_installed+ 74already_vendoredbecomessuccess, 110already_vendored. - uv: 11 + 1 becomes 12
already_vendored. - The tree is identical in both.
- cargo:
- ds-cargo-enpm
scan -e npm(MVN-4).lockfileOnlyPackagesgoes 6920 → 5347; nothing else changes. With--prunethe output is byte-identical (6920). - maven-v, nuget-v, nuget-scale-v, pylock-v, every step (X11).
.socket/vendor/state.jsonis the only differing file. Decoding the v2 ops and maskingvendoredAt, it equals b4's ledger in all 24 comparisons. Every lock, manifest, config and artifact is byte-identical. - MVN-1, V2, X12. No output difference on any run, and no
.commit-journal*remains in any tree.
Byte-identical on both binaries (stdout, stderr, tree, file list, requests):
- Hosted, dry and wet: depscan (real tree, plain and
-e npm), cargo, maven, nuget, composer, uv, poetry, go, gem. - Vendored, fresh / re-run / offline: depscan, composer, poetry, go, gem.
- uv vendored fresh.
Nothing differs that is not listed above.
🤖 Generated with Claude Code
Edited after review: added the vendor_commit_failed error, the event-order change, and the two updated tests under item 6.
The durability barrier re-opened every deferred artifact by path with
File::open (read-only) and called sync_all on it. On Windows
FlushFileBuffers needs a handle with write access, so every vendored run
failed the barrier with "Access is denied. (os error 5)": the group
commit refused ("could not commit the vendored lockfile, manifest and
ledger edits") and the lock release warned "could not sync the vendored
artifacts". A write re-open is no answer either: it is refused for a
read-only artifact.
Off Unix, stage_and_rename now fsyncs every stage through its own
writable handle before the rename -- what the durable writer (and main)
always did there -- and the barrier syncs no files (directories were
already Unix-only). Artifacts are still recorded, so the barrier and its
failpoint run at the same points on every platform. Unix keeps the
deferred plain-fsync barrier unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
vendor_crash_safety_e2e and vendor_group_commit_e2e crash the real binary
through SOCKET_PATCH_FAILPOINT, which was compiled in under
debug_assertions only; the test-release job (cargo test --profile
ci-release) runs them against an optimized binary where the failpoint is
a no-op, so the "crash" run completed with status success and the suites
failed ("the failpoint must crash the run").
A new socket-patch-core `failpoints` feature compiles them into builds
without debug assertions as well. The CLI enables it from its
[dev-dependencies] only, so resolver 2 adds it to `cargo test` builds
(debug or release) and never to `cargo build --release` / `cargo
install` -- the shipped binary still has no environment-triggered exit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sinks CodeQL's rust/cleartext-logging flags any value named *uuid* reaching a log/panic sink. The prefetch-plan cross-checks interpolated the patch uuid into their (unreachable) debug_assert! messages, and debug_log's refactor for deferred lines moved its eprintln to a new line, which re-raised main's existing alert as a new one. The assert messages now name the plan they fell out of step with, and debug_log keeps main's direct eprintln with the deferral split into defer_debug_line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Masked on Windows until now by the barrier's os error 5: * vendor_ledger_schema_e2e replays the base binary's checked-in legacy ledgers and compares byte for byte; the Windows checkout (autocrlf) turned their LF into CRLF. Check tests/fixtures/legacy-ledgers out exactly as committed (-text), like the redirect/pnpm golden fixtures. * go_crawler's randomized oracle equivalence ran a fixed 48 seeds, which on Windows (case-insensitive names fold together, modes ignored) yielded 92/80 modules against the unchanged >100/>100 non-vacuity bar. It now keeps adding seeds until the bar is met (cap 480). * registry_fetch's parallel-zip test compared tree_of (backslash paths, mode 0 off Unix) with an oracle that used '/' and Unix modes. tree_of now reports '/'-separated paths and the oracle expects mode 0 where the disk holds none. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
write_zip_entries promises byte-identical archives for the same content, but zip 8 stamps the central directory's "version made by" host as DOS on a Windows build (Unix elsewhere). So a wheel/jar/nupkg rebuilt on Windows hashed differently from the one a macOS/Linux run recorded, and told readers to ignore the unix modes (the preserved exec bit) it carries. vendor_ledger_schema_e2e caught it on the Windows test job: the pypi-requirements wheel hash differed from the base binary's. Pin System::Unix in write_zip_entries (every product zip rebuild goes through it; berry_zip writes its own fixed headers) and in the ecosystem fixtures' zip builder, whose nupkg hashes the checked-in legacy-ledger fixtures carry. A unit test pins the host byte and the modes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CI fixes for the 17 failing checks on 5916139I pulled the logs for every failing check. They come down to four root causes, and fixing the first one exposed three more Windows-only failures that it had been hiding. Commits pushed as fast-forwards: 1. Windows:
|
|
BugBot review |
|
Claude (@claude) review |
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 f9fc676. Configure here.
|
External validation (depscan cross-OS Maven matrix): depscan's harness ran its full 25-job Linux/Windows/macOS Maven matrix against this PR's head It also confirms that this PR fixes #275, the pre-existing Windows zip-host bug. All 55 vendored local-rebuild cells now produce identical jar digests on Linux, Windows and macOS. On 🤖 Generated with Claude Code |
reuse::prior_entry loaded the whole vendor ledger once per npm/pypi package: a full re-parse outside a group commit, a deep clone of the captured ledger inside one. On a monorepo whose ledger runs to megabytes that was the vendored re-run's single largest cost. Read it through load_state_shared instead (still reads the bytes every time, re-parses when they moved, answers from the group's captured value) and clone only the matching entry. The pre-change body stays as a test oracle. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ying it per save Inside a group commit every per-package save_state captured a deep copy of the whole ledger (O(P^2) over a run). The vendor loop now holds its ledger as an Arc and saves through save_state_shared, which drops the group's reference to the value it captured last time, edits in place and re-captures it, all under the overlay lock. The committed bytes are the same ledger rendered once at the commit; outside a group the save is the old write. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pnpm backend re-read, re-split and re-scanned pnpm-lock.yaml once per patched npm package. The split now lives behind a ParseMemo keyed on the bytes read (the read itself is kept), re-seeded with the lock the backend writes, and its lazily built section index answers the pre-flight probes (lock_has_target_package, check_rewritable_refs), the in-use probe and the packages/snapshots edits' block lookups while the lines are unedited; the first edit takes a private copy and the scans take over. The scans stay as the fallback and as the oracles of a generated-lock equivalence test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Profiled on the polyglot re-run (unstripped build): per crate the backend re-parsed the root Cargo.toml, rebuilt Cargo.lock's package list for each of several probes and re-scanned every manifest for Socket registry pins. Memoize each per input bytes: the pre-flight's manifest parse, the locked package list, and a one-pass extraction of every crate's registry pin. All reads are kept; the per-call versions stay as test oracles. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…onment A scan that crawls and then vendors ran the python probe and the site query (two process spawns) twice with identical inputs. Keep the last answer keyed on the whole process environment and working directory, so any change to either re-runs it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er walk The vendored cargo backend's residue check walks the workspace members once per patched crate, and once a project is vendored every [patch.crates-io] path copy is a path dependency: the walk re-parsed every vendored crate's manifest with toml_edit for every crate, O(P^2) parses (profiled as the largest cost of the polyglot cargo in-sync re-run, on the blocking pool the loop waits on). Keep the facts the walk needs per manifest bytes; every manifest is still read on every walk. The per-call walk stays as a test oracle. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… check reads The member walk returns every vendored crate's path copy, so the residue check reads one manifest per vendored crate; eight slots thrashed and the pins were re-extracted for every manifest on every crate. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The DC-6 memo kept a failed interpreter query's None, so a transient spawn failure during the crawl (EAGAIN/EMFILE, an interpreter exiting non-zero once) also blanked the vendor phase's ask, where the unmemoized build ran the query again. Store only Some; a failure re-runs on the next call exactly as before. Keep the unmemoized query as run_site_query, the oracle the memo is checked against. Replace the unserialized stability test, which raced the #[serial] PYENV_ROOT test and compared the memo against itself, with a #[serial] test that asks under two PYTHONUSERBASE values and requires the memoized answer to equal the oracle each time (a constant memo key fails it). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
edit_value releases the overlay's reference to the caller's ledger before editing it in place. Had the edit (or make_mut's copy) unwound, the key stayed missing, and a commit that still ran would write the run's lock edits beside the pre-run ledger. Re-capture the caller's value before resuming the unwind. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The member walk and the residue check read their manifests in a fixed order, so a set larger than the slots evicts every slot before its next use: each read then pays the parse plus the slot scan and bytes copy, slightly more than no memo, not the same. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…gain A fresh vendored run rewrites pnpm-lock.yaml after every package, so each lock the memo is re-seeded with is probed by exactly one package. The section index was built for that one probe and dropped, which cost more than the scans it answered for: depscan vendored fresh went from 13.3G to 15.8G instructions. The index is now built on a lock's second package probe (a pre-flight or an in-use check); the first is answered by the scans, whose answers the equivalence test already holds equal. An idempotent re-run and the download plan's batch pre-flight probe one unchanged lock once per package and still index it once. A new test pins that one probe leaves the index unbuilt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
WP-P1: vendored CPU memos (11 commits, f9fc676..cfeafb0)
Every item is meant to preserve behavior. Three adversarial reviewers checked the WP: 5 findings fixed, 1 deferred and documented (512-slot memo FIFO eviction). Before → after (previous PR head f9fc676 vs cfeafb0, release builds, replay proxy)Polyglot monorepo (~158k files, 8 ecosystems):
The 0 ms vfresh/vrerun rows were re-measured on cfeafb0. The other rows are from e369bf4 (cfeafb0 without the V-2 fix). The fix only touches the pnpm vendored path, so those rows are not expected to move, and the 100 ms vfresh row is, if anything, pessimistic. depscan (2 interleaved reps each, 0 ms):
The first hosted rep is cold for both binaries (12.4G then 10.4G). Warm, hosted is flat. Vendored fresh is now 21% cheaper, where the previous draft of this WP regressed it by 18%. It costs about +8 MiB peak RSS, because the memo holds the 2 MB lock's bytes and lines. Byte-equalityPolyglot gate at 0 ms on vfresh/vrerun with cfeafb0, and on hdry/hwet with e369bf4. Canonical stdout, stderr, exit codes, request counts by kind and the full work-tree digest are identical between the two binaries. The digest covers 250-274k entries: every file hash with timestamps masked, plus symlinks and dirs. For depscan, the canonical stdout (hosted and vendored) is identical, and so are pnpm-lock.yaml, pnpm-workspace.yaml, package.json, the vendor ledger and every vendored artifact and marker (timestamps masked). Canonical stdout sorts arrays, because the order of the per-package vendored warnings follows fetch completion and already varies from run to run on f9fc676. Tests
🤖 Generated with Claude Code |
Hosted and vendored scans spent most of their time in two places:
This PR makes the network phases concurrent and the crawlers and rewriters single-pass. It also adds lazy vendored sources, in-memory repacking and per-run parse memos, and ends with a small set of approved behavior changes.
Up to the approved batch, stdout, stderr, lockfiles, ledgers and exit codes are byte-identical to
main. The approved changes are listed below and detailed in this comment.Results
"Before" is
mainwithout this PR and "after" is this branch's head. Both use a replay proxy (recorded API responses served locally) at 100 ms/request, 2 interleaved reps, medians. Instructions retired are in parentheses.The "before" binary predates #251–#254. #254 made cargo vendoring heavier, so the cargo rows understate the gain. Measured against the pre-batch head of this branch, cargo vendored fresh drops 49.6 → 38.9 G.
scan --mode hosted --dry-run(live API)¹¹ Measured on the first slice of this PR and not re-run since.
main's 71 grants / 344 requests.What changed
Network (all ecosystems)
utils/concurrent.rsis an ordered-concurrency helper (buffered, neverbuffer_unordered). Results, warnings and errors are folded in input order.GET /patches/view, download views and vex record fetches run concurrently: 8 in flight on the API, 4 on the public proxy.SOCKET_API_CONCURRENCY:1restores strictly serial requests. A tight descriptor limit falls back to serial automatically.Crawl
gem envand composer home are asked once per process.Rewriters and vendoring
[[package]]). The old rewriters are kept as oracles.Tooling:
scripts/perf/holds the record/replay benchmark harness.Behavior changes (approved)
Details and measurements are in the batch comment.
{snapshot, ops}edits of the record's ownoriginal. Version 1 ledgers are still read and revert byte for byte. Other ledgers stay byte-identical v1.state.jsonshrinks: maven 29.8 → 15.1 MB, pylock 43.9 → 22.0 MB, nuget 7.2 → 3.9 MB.--offline/ network-less re-run now succeeds (already_vendored, exit 0), and re-runs no longer reportvendor_fetched_missing.-e <eco>crawl scope (MVN-4): without--prune/--sync, only the named ecosystems are crawled.lockfileOnlyPackagescounts only them (depscan+zed-e npm: 6920 → 5347).~/.m2path (MVN-1): canonical POMs are not opened once their top-level group directory is confirmed. A hand-placed POM that disagrees with its path reports the path's coordinates..socket/vendor/.commit-journal.json. A crash leaves the pre-run wiring, or is rolled forward by the next locked command. A failed commit write now fails the run with the new top-level errorvendor_commit_failed(exit 1), replacing the per-purlvendor_state_write_failed/redirect_ledger_write_failedevents; locks and ledgers stay pre-run, and artifacts written in the loop remain until the next run.vendor_stale_artifact_removedevents now follow the per-package events.Resolved: exact plan, 71 grants
The earlier prefetch planned download grants before each backend's pre-flight checks ran, so depscan sent 74 grant POSTs where
mainsends 71. The plan now calls each backend's own pre-flight gates, so fresh depscan vendoring sends exactly 71 grants and 344 requests, with 0 replay misses. Parity tests pin each npm flavor's plan against its loop, refusal for refusal.Other observable differences
--debug, the[socket-patch debug] GET/POST <url>lines for concurrent requests interleave. Normal warnings keep serial order, and tests pin that.Test plan
cargo clippy --workspace --all-targets -- -D warningsis clean.cargo test --workspace --no-fail-fast: 9,218 passed, 0 failed, 136 ignored. No existing test was ignored or deleted. All existing test edits pin an approved behavior change: the two ledger-write-failure tests incovgap_commands_vendor.rsnow expect the newvendor_commit_failedenvelope error instead of a per-purlfailedevent (V2), and the redirect one no longer asserts that the artifact is absent, since it is written before the commit fails. It asserts instead that nothing is committed or rewired and no journal remains. The v1 legacy-ledger fixture for cargo was regenerated with the pre-batch binary to matchmain's Wire vendored cargo patches through Cargo.toml and tag the copy #254 cargo wiring. Every other change before the approved-behavior batch kept all existing assertions unchanged.SOCKET_API_CONCURRENCY=1and tight fds;The commits are the original reviewed commits, replayed onto current
main(#251–#254). Where a conflict or an adaptation tomainwas needed, it is folded into the commit that needed it, and each batch comment lists them.🤖 Generated with Claude Code
Note
High Risk
Changes vendoring crash semantics, ledger schema, offline/idempotent vendor behavior, Maven discovery coordinates, and concurrent API failure modes—areas that affect committed project state and patch correctness.
Overview
This PR speeds up hosted and vendored workflows by running patch-API traffic concurrently (default 8 authenticated / 4 on the public proxy) via
ordered_concurrent, withSOCKET_API_CONCURRENCY(1–32) to throttle when a proxy or WAF limits in-flight requests. The same pacing applies tovex/scan --vexrecord fetches and hosted wheel-metadata downloads;hold_back_debugkeeps per-request--debuglines in the same order as the old serial loop.Beyond networking, it lands several v5.0 contract changes documented in CHANGELOG / CLI_CONTRACT: deferred pristine fetch so already-vendored / service-backed cargo re-runs can succeed offline without upfront registry pulls;
scan -ecrawls only selected ecosystems (except prune/sync); Maven derives coordinates from canonical~/.m2paths when the repo root is confirmed; vendor ledger v2 stores large whole-file wiring as snapshot+ops edits; one group commit per vendored run through.socket/vendor/.commit-journal.json(new top-levelvendor_commit_failedon commit I/O failure); and batched fsync for artifacts before durable lockfile/ledger writes.Smaller fixes include
vexapi_auth_fallbackquoting the first refusal in pending order (not completion order), batched gem/Python stale-install probes usingjudge_installed_record, andconfirm_waitsso scan knows when a hosted confirm prompt will block.Reviewed by Cursor Bugbot for commit f9fc676. Configure here.