Skip to content

Commit cfeafb0

Browse files
mikolalysenkoclaude
andcommitted
perf(vendor): index a pnpm lock only once the same lines are probed again
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>
1 parent e369bf4 commit cfeafb0

1 file changed

Lines changed: 117 additions & 12 deletions

File tree

‎crates/socket-patch-core/src/vendor/pnpm_lock.rs‎

Lines changed: 117 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
4747
use std::collections::{HashMap, HashSet};
4848
use std::path::Path;
49+
use std::sync::atomic::{AtomicUsize, Ordering};
4950
use std::sync::{Arc, OnceLock};
5051

5152
use serde_json::Value;
@@ -494,6 +495,7 @@ pub(super) fn preflight_package(
494495
Err(detail) => return Err(Box::new(refused("vendor_override_conflict", detail))),
495496
};
496497
let effective_key = disposition.effective_key(override_key).to_string();
498+
project.lines.note_probe();
497499
if let Err(detail) = check_lock_override(&project.lines, name, version, &effective_key) {
498500
return Err(Box::new(refused("vendor_override_conflict", detail)));
499501
}
@@ -567,15 +569,19 @@ pub async fn pnpm_entry_in_use(entry: &VendorEntry, project_root: &Path) -> Opti
567569
}
568570
// Every `packages:`/`snapshots:` block key resolving into
569571
// `.socket/vendor/npm/<uuid>/`, collected once per lock bytes (see
570-
// [`LockIndex`]); [`pnpm_entry_in_use_scan`] is the per-call scan it
571-
// answers for.
572+
// [`LockIndex`]) once these bytes are probed again; the first probe
573+
// runs [`pnpm_entry_in_use_scan`], the per-call scan it answers for.
572574
let doc = LOCK_MEMO.parse_infallible(text.as_bytes(), || LockDoc::new(split_lines(&text)));
573-
Some(doc.index().vendored_npm_uuids.contains(&entry.uuid))
575+
doc.note_probe();
576+
Some(match doc.index() {
577+
Some(index) => index.vendored_npm_uuids.contains(&entry.uuid),
578+
None => pnpm_entry_in_use_scan(&entry.uuid, &doc.lines),
579+
})
574580
}
575581

576-
/// The pre-index [`pnpm_entry_in_use`] body over already-split lines — the
577-
/// equivalence oracle for the indexed answer.
578-
#[cfg(test)]
582+
/// The pre-index [`pnpm_entry_in_use`] body over already-split lines: the
583+
/// answer for a lock probed once, and the equivalence oracle for the
584+
/// indexed answer.
579585
fn pnpm_entry_in_use_scan(uuid: &str, lines: &[String]) -> bool {
580586
for section in ["packages", "snapshots"] {
581587
let Some((start, end)) = section_bounds(lines, section) else {
@@ -2292,23 +2298,50 @@ fn matching_blocks<L: EditLines>(
22922298
/// is split afresh. The backend re-seeds the slot with the lock it wrote.
22932299
static LOCK_MEMO: ParseMemo<LockDoc> = ParseMemo::new();
22942300

2295-
/// One lock's lines plus, built on first use, their [`LockIndex`] — a pure
2296-
/// function of the lines, so of the bytes the memo keys on.
2301+
/// One lock's lines plus their [`LockIndex`] — a pure function of the
2302+
/// lines, so of the bytes the memo keys on — built only once the same lines
2303+
/// are probed a second time ([`INDEX_AFTER_PROBES`]).
22972304
struct LockDoc {
22982305
lines: Vec<String>,
2306+
/// Package probes against these lines so far ([`Self::note_probe`]).
2307+
probes: AtomicUsize,
22992308
index: OnceLock<LockIndex>,
23002309
}
23012310

2311+
/// How many package probes the same lock lines must see before their
2312+
/// [`LockIndex`] is built. Building it costs several whole-lock scans, so
2313+
/// it pays only when the lines are asked about again: an idempotent re-run
2314+
/// (and the download plan's batch pre-flight) probes one unchanged lock
2315+
/// once per package, while a fresh run rewrites the lock after every
2316+
/// package, so each lock it re-seeds is probed exactly once — and answering
2317+
/// that one probe with the scans is cheaper than indexing lines nobody will
2318+
/// query again.
2319+
const INDEX_AFTER_PROBES: usize = 2;
2320+
23022321
impl LockDoc {
23032322
fn new(lines: Vec<String>) -> Self {
23042323
Self {
23052324
lines,
2325+
probes: AtomicUsize::new(0),
23062326
index: OnceLock::new(),
23072327
}
23082328
}
23092329

2310-
fn index(&self) -> &LockIndex {
2311-
self.index.get_or_init(|| LockIndex::build(&self.lines))
2330+
/// Count one package's probe of these lines (one pre-flight, one in-use
2331+
/// check), however many lookups it goes on to make.
2332+
fn note_probe(&self) {
2333+
self.probes.fetch_add(1, Ordering::Relaxed);
2334+
}
2335+
2336+
/// The index, once these lines have seen [`INDEX_AFTER_PROBES`] probes;
2337+
/// `None` before that, and every caller then runs the scan it answers
2338+
/// for (the answers are equal either way).
2339+
fn index(&self) -> Option<&LockIndex> {
2340+
if let Some(index) = self.index.get() {
2341+
return Some(index);
2342+
}
2343+
(self.probes.load(Ordering::Relaxed) >= INDEX_AFTER_PROBES)
2344+
.then(|| self.index.get_or_init(|| LockIndex::build(&self.lines)))
23122345
}
23132346
}
23142347

@@ -2356,6 +2389,15 @@ impl EditLines for Vec<String> {
23562389
}
23572390
}
23582391

2392+
impl LockLines {
2393+
/// [`LockDoc::note_probe`] while the lines are the memoized split.
2394+
fn note_probe(&self) {
2395+
if let LockLines::Shared(doc) = self {
2396+
doc.note_probe();
2397+
}
2398+
}
2399+
}
2400+
23592401
impl EditLines for LockLines {
23602402
fn read(&self) -> &Vec<String> {
23612403
match self {
@@ -2376,7 +2418,7 @@ impl EditLines for LockLines {
23762418

23772419
fn index(&self) -> Option<&LockIndex> {
23782420
match self {
2379-
LockLines::Shared(doc) => Some(doc.index()),
2421+
LockLines::Shared(doc) => doc.index(),
23802422
LockLines::Owned(_) => None,
23812423
}
23822424
}
@@ -8493,8 +8535,15 @@ snapshots:
84938535
/// One lock edit over `L` lines, as `vendor_pnpm`'s edit array holds it.
84948536
type Edit<L> = fn(&mut L, &EditCtx<'_>, &mut Vec<WiringRecord>) -> Result<bool, String>;
84958537

8538+
/// The memoized split of `lines`, already probed often enough that its
8539+
/// index answers (so the indexed paths are the ones exercised).
84968540
fn shared(lines: &[String]) -> LockLines {
8497-
LockLines::Shared(Arc::new(LockDoc::new(lines.to_vec())))
8541+
let doc = LockDoc::new(lines.to_vec());
8542+
for _ in 0..INDEX_AFTER_PROBES {
8543+
doc.note_probe();
8544+
}
8545+
assert!(doc.index().is_some());
8546+
LockLines::Shared(Arc::new(doc))
84988547
}
84998548

85008549
/// V-2: every indexed answer equals the scan it replaces — section
@@ -8623,6 +8672,62 @@ snapshots:
86238672
);
86248673
}
86258674

8675+
/// V-2: a lock probed by ONE package is answered by the scans and never
8676+
/// indexed — the fresh-run shape, where every package re-seeds the memo
8677+
/// with the lock it wrote and the next package probes it exactly once —
8678+
/// and the second package probing the same lines (an idempotent re-run)
8679+
/// builds the index once for every later probe.
8680+
#[test]
8681+
fn lock_index_is_built_only_when_the_same_lines_are_probed_again() {
8682+
let project = || PnpmProject {
8683+
pkg_bytes: b"{}".to_vec(),
8684+
pkg: serde_json::json!({}),
8685+
lines: LockLines::Shared(Arc::new(LockDoc::new(split_lines(P1_BEFORE_LOCK)))),
8686+
ws_text: None,
8687+
};
8688+
let doc_of = |p: &PnpmProject| match &p.lines {
8689+
LockLines::Shared(doc) => Arc::clone(doc),
8690+
LockLines::Owned(_) => unreachable!(),
8691+
};
8692+
8693+
// One package: the pre-flight passes on the scans alone.
8694+
let fresh = project();
8695+
let doc = doc_of(&fresh);
8696+
let key = preflight_package(&fresh, "left-pad", "1.3.0", "left-pad@1.3.0")
8697+
.map_err(|_| ())
8698+
.unwrap();
8699+
assert_eq!(key, "left-pad@1.3.0");
8700+
assert!(fresh.lines.index().is_none());
8701+
assert!(
8702+
doc.index.get().is_none(),
8703+
"one probe must not build the index"
8704+
);
8705+
// Its edits (which take the private copy) leave it unbuilt too.
8706+
let ctx = EditCtx {
8707+
name: "left-pad",
8708+
version: "1.3.0",
8709+
rel_tgz: ".socket/vendor/npm/u/left-pad-1.3.0.tgz",
8710+
spec: "file:.socket/vendor/npm/u/left-pad-1.3.0.tgz",
8711+
integrity: "sha512-x",
8712+
override_key: "left-pad@1.3.0",
8713+
};
8714+
let mut lines = fresh.lines;
8715+
let mut wiring = Vec::new();
8716+
assert_eq!(edit_overrides(&mut lines, &ctx, &mut wiring), Ok(true));
8717+
assert!(doc.index.get().is_none());
8718+
8719+
// A second package probing the same lines builds it, once.
8720+
let rerun = project();
8721+
let doc = doc_of(&rerun);
8722+
for _ in 0..3 {
8723+
preflight_package(&rerun, "left-pad", "1.3.0", "left-pad@1.3.0")
8724+
.map_err(|_| ())
8725+
.unwrap();
8726+
}
8727+
assert!(doc.index.get().is_some());
8728+
assert_eq!(doc.probes.load(Ordering::Relaxed), 3);
8729+
}
8730+
86268731
/// V-2: the memoized split is keyed on the bytes read, so a lock edited
86278732
/// between two packages is re-split — the second read sees the edit.
86288733
#[tokio::test]

0 commit comments

Comments
 (0)