4646
4747use std:: collections:: { HashMap , HashSet } ;
4848use std:: path:: Path ;
49+ use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
4950use std:: sync:: { Arc , OnceLock } ;
5051
5152use 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.
579585fn 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.
22932299static 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`]).
22972304struct 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+
23022321impl 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+
23592401impl 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