gunnar_wire/graph.rs
1//! Walking the object graph: what a pack must contain, and whether a push
2//! closes.
3//!
4//! One walker serves both directions (one implementation). `upload-pack` asks
5//! *which objects are reachable from these wants and not from these haves* and
6//! packs the answer; `receive-pack` asks *is everything reachable from these
7//! new tips actually here* and throws the answer away. The difference is which
8//! set is the frontier and whether a missing object is an error, so they are
9//! the same function with two arguments, not two functions that must be kept
10//! agreeing.
11//!
12//! # Honest limits
13//!
14//! The exclusion set is the **full** object closure of the haves, computed by
15//! walking. Correct, since the client demonstrably has every object reachable
16//! from a commit it has, but it costs `O(client history)` per fetch where git
17//! with reachability bitmaps does `O(1)` lookups. The `git` znippy package's
18//! `__gunnar_reach__` module is where that cost goes away; nothing here claims
19//! it has. Sending *fewer* objects than this walk selects would be a bug;
20//! sending more is only slow.
21//!
22//! The *memory* is the same shape and used to be worse than the time: that
23//! closure is `O(repository)` object ids, and [`select_objects`] used to
24//! materialise it **three times over**, once as the exclusion set, once as the
25//! walk's own `seen` set, once more as a `Vec` of the same ids. Then it cloned
26//! the result for step 6. See [`OidSet`].
27
28//! # What this walker reads through, and why it is not `GitOps`
29//!
30//! [`Graph::load`] is the single funnel under [`Graph::peel`],
31//! [`Graph::commit_links`], [`Graph::commit_facts_into`] and
32//! [`Graph::tree_closure`], and every one of them needs the object
33//! **inflated and delta-resolved**. `GitOps::get` deliberately does not give
34//! one: `Stored::bytes` is documented as *"the pack entry, header and all,
35//! byte for byte as the client sent them"* and for the ordinary push that is a
36//! delta, while `Stored::obj_type` is *"the entry's type in the pack"* and may
37//! be `OfsDelta`/`RefDelta`. Handing a delta to `CommitRefIter` would not
38//! error; it would decode garbage.
39//!
40//! So the walk reads through [`GitServe`] — the **reading** contract
41//! (`locked_on_target.md`'s `GitServe` reading contract) — and `GitServe::read`'s kind is the resolved
42//! one. `GitOps` stays the storage contract, narrow, and no adapter in gunnar
43//! re-derives an object model on top of it.
44
45use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
46
47use gix_hash::{Kind, ObjectId};
48use gunnar_store::git_storage_trait::{GitServe, ObjType};
49use gunnar_store::object;
50
51use crate::filter::Filter;
52use crate::limits::Limits;
53use crate::{Error, Result};
54
55/// `GitServe`'s resolved object type, as the decoder names it.
56///
57/// **This mapping exists once, here.** `ObjType` is the contract's vocabulary
58/// (six codes, four resolved and two delta) and [`object::Kind`] is the
59/// decoder's (four). The two delta codes cannot come out of `GitServe::read` or
60/// `GitServe::header`, which are defined to answer the *resolved* type — so a
61/// delta code here is a backend that broke its own contract, and it is reported
62/// as a corrupt object rather than quietly mapped onto something.
63///
64/// The refusal itself lives in [`object::Kind::resolved`], next to the four
65/// names; what stays here is the *sentence* said about it, because only this
66/// crate knows which oid was being asked for.
67fn resolved_kind(id: &ObjectId, kind: ObjType) -> Result<object::Kind> {
68 object::Kind::resolved(kind).ok_or_else(|| Error::CorruptObject {
69 oid: *id,
70 reason: format!(
71 "the store answered a resolved read with {}, which is a delta entry \
72 and not an object type",
73 kind.as_str()
74 ),
75 })
76}
77
78/// **The one conversion from a storage fault into a wire error.**
79///
80/// `GitServe` returns `anyhow::Result` — the contract crate depends on
81/// `anyhow` and nothing else so that neither backend has to resolve the
82/// other's dependencies — and everything this crate can say about such a fault
83/// is [`gunnar_store::Error::Backend`]. One function rather than a `map_err`
84/// at each site (one implementation): the day `RefCas`/`RefLocked` (the typed-CAS question)
85/// becomes a typed rejection there is exactly one place that learns about it.
86/// **Nothing may tell two storage faults apart by matching on this string.**
87///
88/// `{:#}` and not `{}`: `anyhow`'s alternate form prints the whole cause chain.
89pub(crate) fn backend(err: impl std::fmt::Display) -> Error {
90 Error::Store(gunnar_store::Error::backend(format!("{err:#}")))
91}
92
93/// One object id out of the contract's vocabulary.
94///
95/// `git-storage-trait` speaks raw bytes — `Oid<'a> = &'a [u8]`, `Vec<u8>` in a
96/// returned row — because it must name no hash library; this crate speaks
97/// `ObjectId`. A length that is not a hash gunnar knows is a **backend fault,
98/// not an object that happens to be absent**, so it is an error rather than a
99/// skip: a selection or an advertisement silently short by the rows it could
100/// not parse is the under-send that exits zero.
101pub(crate) fn oid(bytes: &[u8]) -> Result<ObjectId> {
102 ObjectId::try_from(bytes).map_err(|err| {
103 backend(format_args!(
104 "the store answered with a {}-byte object id: {err}",
105 bytes.len()
106 ))
107 })
108}
109
110/// [`Graph::contains_have`] without a [`Graph`], for the two negotiation sites
111/// that hold only the store.
112///
113/// One implementation, two entry points, because the *reason* a storage fault
114/// reads as `false` here is subtle and must not be written down twice: a
115/// client's `have` naming an object this server does not hold is the normal,
116/// majority outcome, and every consumer of a negative degrades in exactly one
117/// direction — the `have` drops out of `common`, so the pack excludes less and
118/// is **wider and still valid**; it drops out of `Selection::client_has`, so no
119/// thin delta names it as a base and the object is sent whole; `ready` may be
120/// withheld a round longer. A false *positive* would be fatal — a thin pack
121/// whose bases the client lacks is a failed fetch, not a smaller one — and this
122/// cannot produce one.
123///
124/// So a backend fault is not swallowed into a *wrong* answer, only into a
125/// *pessimistic* one. It is still logged, because "the object store is failing"
126/// is not something a server should only be able to infer from a wide pack.
127pub(crate) fn has_for_negotiation(store: &dyn GitServe, id: &gix_hash::oid) -> bool {
128 match store.has(id.as_bytes()) {
129 Ok(present) => present,
130 Err(err) => {
131 tracing::warn!(
132 target: crate::receive_pack::GIT_TARGET,
133 event = "git.have.store_error",
134 oid = %id,
135 error = %format_args!("{err:#}"),
136 "the object store failed answering a `have`; treating it as absent, which \
137 widens the pack and cannot corrupt it"
138 );
139 false
140 }
141 }
142}
143
144/// How far a tag chain is followed before it is called a loop.
145const MAX_TAG_DEPTH: usize = 16;
146
147/// The default ceiling on object ids one traversal on the **serve** side may
148/// hold at once.
149///
150/// **This is no longer what bounds gunnar's memory.** [`Limits::walk_bytes`] is
151/// — in bytes, which is the unit a machine is sized in and the only unit that
152/// can be pooled across requests. This number survives as an operator's
153/// explicit cap on the size of a clone, reachable as `--fetch-budget`, and it
154/// is charged through the same [`Graph::charge_walk`] so the two cannot drift.
155///
156/// # Three things the comment justifying this number said, and all three were wrong
157///
158/// It said *"objects visited"*. It was charged against objects **emitted** —
159/// `out.len()` on the [`Emit`] each walker happened to be holding.
160///
161/// It said *"well past any real repository (the linux kernel is around ten
162/// million objects total)"*. The kernel is **13 816 503** objects, so it sat
163/// 28% *below* the repository it was chosen against. MEASURED 2026-08-05,
164/// `bench/examples/walkbudget-probe.rs --scale` against a purpose-built
165/// 14 042 000-object repository with no `.bitmap`: **the clone is refused**,
166/// `WalkBudget`, and the same machine served it in 5 460 816 kB of peak RSS and
167/// 64 seconds once the ceiling let it. That is measured now, not extrapolated.
168///
169/// And it was implied to be *one* ceiling. It was compared at four sites
170/// against four different accumulators, so a single fetch could hold two full
171/// budgets and be refused by neither, while the one repository-sized structure
172/// that outlived them both — step 6's `seen` set — was compared against
173/// nothing. [`Graph::charge_walk`] is the single check that replaced them.
174///
175/// # Where it binds, MEASURED, before and after
176///
177/// `bench/examples/walkbudget-probe.rs`, 2026-08-05, one process per
178/// repository, binary-searching the smallest budget at which a clone still
179/// completes rather than assuming the edge is the object count — the probe that
180/// assumed that failed on its first repository, where a budget of 82 served
181/// bifrost's 83 objects.
182///
183/// | corpus | objects | bound at, per-accumulator | bound at, held ids |
184/// |---|---|---|---|
185/// | `h2h-linear-sha1-32c` | 190 | 0.837 | 1.174 |
186/// | `forge-year-8a-8b-12c` | 1 544 | 0.933 | 1.069 |
187/// | `forge-year-8a-240b-12c` | 28 893 | 0.892 | 1.108 |
188/// | `h2h-linear-sha1-2048c` | 36 172 | 0.943 | 1.057 |
189/// | `h2h-linear-sha256-2048c` | 36 172 | 0.943 | 1.057 |
190/// | `h2h-linear-sha1-20480c` | 352 396 | 0.942 | **1.058** |
191///
192/// The old column is the defect in one number: `budget = N` admitted between
193/// 0.84N and 0.94N objects, and **which** was decided by the repository's shape
194/// rather than by anything an operator could see. The new column is the same
195/// ceiling meaning the same thing everywhere — 1.057, 1.057, 1.058 on the three
196/// largest — and it is above 1.0 because a clone holds its peeled tips and its
197/// tags as well as the pack. The two small rows are further out for the same
198/// reason: a fixed handful of extra ids is a large fraction of 190.
199///
200/// **It used to bite only without a bitmap**, and that was the defect rather
201/// than a property: a reachability index answers the clone without entering
202/// this walk at all, which the same probe established by serving a whole clone
203/// at a budget of ONE. Since the storage contract landed the store that answers that way — the znippy
204/// cold tier — is the one the appliance runs, so an operator's `--fetch-budget`
205/// was accepted and ignored on the shipping path. [`Graph::charge_answer`] is
206/// where the same ceiling is now applied to an answer that arrived whole, and
207/// it is the same comparison ([`Graph::charge_budget`]) so the two cannot
208/// drift.
209///
210/// # Why the unit had to change
211///
212/// The thing a ceiling exists to bound is memory, and objects do not convert to
213/// memory at a fixed rate. MEASURED across the same corpus, `VmHWM` per emitted
214/// object: **1 572.9 B** on `forge-year-8a-240b-12c` (28 893 objects, 848 bytes
215/// of content each) against **393.8 B** on the 14M-object corpus (43 bytes of
216/// content each). Four times, at the same order of object count, decided by
217/// what is *in* the repository. See [`WALK_BYTES_PER_OBJECT`] for the
218/// two-term fit that does predict it and for what an id-counted ceiling can
219/// honestly claim.
220///
221/// [`Limits::walk_bytes`]: crate::Limits::walk_bytes
222/// [`WALK_BYTES_PER_OBJECT`]: crate::WALK_BYTES_PER_OBJECT
223pub const DEFAULT_BUDGET: usize = 10_000_000;
224
225/// The ceiling on objects visited by **`receive-pack`'s connectivity check**.
226///
227/// # Why this is not [`DEFAULT_BUDGET`], which would be one constant
228///
229/// Because they bound two different quantities, and one implementation is
230/// about not writing the same thing twice rather than about giving two things
231/// one name. A fetch's budget stands in front of everything the server is
232/// about to *materialise*: several repository-sized id vectors and then the
233/// pack itself. This walk materialises nothing — since the one-walk fix it holds
234/// exactly one repository-sized [`OidSet`], the `seen` set, because
235/// [`verify_connectivity`] tallies its output instead of collecting it — and
236/// it runs over objects the server has **already received and indexed**, whose
237/// size `gunnar_pack::IndexOptions` bounded on the way in. A push has
238/// therefore already paid its admission by the time this runs; a fetch has
239/// not.
240///
241/// Sixty-four million is 4.6x the kernel's 13 816 503 objects. At sha1 the one
242/// set costs ~25 bytes per id held, so a walk that genuinely reached this
243/// ceiling would hold ~1.6 GB — a bound, and an honest one for a machine that
244/// is asked to accept a kernel-sized mirror push at all.
245pub const CONNECTIVITY_BUDGET: usize = 64_000_000;
246
247/// A set of object ids that a traversal grows, and that is therefore
248/// `O(repository)`.
249///
250/// It exists for two reasons, and both are the one-set rule.
251///
252/// **It does not implement `Clone`, deliberately.** It exists to delete one
253/// line, `let mut seen = excluded_objects.clone();`. One character of syntax
254/// that doubled the peak memory of every in-flight fetch, at a size (ten
255/// million ids, ~450 MB) where doubling is an out-of-memory kill rather than a
256/// slowdown. A guard watching for that line is a guard somebody has to keep
257/// running; a type that cannot be cloned is a compile error the day it comes
258/// back. Fix by construction, one implementation.
259///
260/// **It is metered.** Under `cfg(test)` every id held in one of these is
261/// counted, so a test can assert what the *peak* was. See [`meter`] and
262/// `select_objects_holds_the_exclusion_closure_once`. Sets that are bounded by
263/// what the client sent (a shallow frontier, a `have` list) are deliberately
264/// left as plain [`HashSet`]s: they are not the ones that scale with the
265/// repository, and metering them would blur the number that matters.
266///
267/// The API is the narrow slice the walkers use. Everything read-only comes from
268/// `Deref`, so a `&OidSet` still passes where a `&HashSet<ObjectId>` is wanted.
269#[derive(Debug, Default)]
270pub(crate) struct OidSet {
271 ids: HashSet<ObjectId>,
272}
273
274impl OidSet {
275 pub(crate) fn new() -> Self {
276 OidSet::default()
277 }
278
279 /// Add `id`, reporting whether it was new. Same contract as
280 /// [`HashSet::insert`].
281 pub(crate) fn insert(&mut self, id: ObjectId) -> bool {
282 let fresh = self.ids.insert(id);
283 if fresh {
284 meter::acquire(1);
285 }
286 fresh
287 }
288
289 /// Hand the ids over as a plain [`HashSet`], releasing the meter.
290 ///
291 /// The one legitimate way out of this type. It is **not** a `Clone` in
292 /// disguise: the set is moved, so nothing is duplicated and the peak does
293 /// not move — which is the entire property this type exists to keep.
294 pub(crate) fn into_inner(mut self) -> HashSet<ObjectId> {
295 let ids = std::mem::take(&mut self.ids);
296 // `Drop` releases `self.ids.len()`, which is now zero, so release the
297 // real count here. Getting this wrong would leave the meter permanently
298 // high and quietly break every test that reads a peak.
299 meter::release(ids.len());
300 ids
301 }
302
303 pub(crate) fn extend(&mut self, ids: impl IntoIterator<Item = ObjectId>) {
304 let before = self.ids.len();
305 self.ids.extend(ids);
306 meter::acquire(self.ids.len() - before);
307 }
308
309 /// Take `id` back out, reporting whether it was there.
310 ///
311 /// The one caller is [`select_objects`]'s step 6, which uses the same set
312 /// as its don't-send set and as the client's-closure voucher and has to
313 /// separate the two again at the end. Removing is what keeps that *one*
314 /// set: the alternative was a second, repository-sized copy taken before
315 /// the tree walk, which is exactly the line this type was invented to make
316 /// impossible.
317 pub(crate) fn remove(&mut self, id: &ObjectId) -> bool {
318 let gone = self.ids.remove(id);
319 if gone {
320 meter::release(1);
321 }
322 gone
323 }
324}
325
326impl std::ops::Deref for OidSet {
327 type Target = HashSet<ObjectId>;
328 fn deref(&self) -> &HashSet<ObjectId> {
329 &self.ids
330 }
331}
332
333impl Drop for OidSet {
334 fn drop(&mut self) {
335 meter::release(self.ids.len());
336 }
337}
338
339impl FromIterator<ObjectId> for OidSet {
340 fn from_iter<I: IntoIterator<Item = ObjectId>>(ids: I) -> Self {
341 let mut set = OidSet::new();
342 set.extend(ids);
343 set
344 }
345}
346
347/// How many object ids the walkers are holding, and the most they ever held.
348///
349/// # It used to be `#[cfg(test)]`, and that is why the ceiling was wrong
350///
351/// This was a test instrument: in a shipping build every function was an empty
352/// inline body and [`OidSet`] was a `HashSet` with a newtype around it. So the
353/// one place in gunnar that knew what a traversal was **holding** could not be
354/// consulted by the thing whose job is to bound it, and
355/// [`DEFAULT_BUDGET`] instead charged the nearest accumulator to hand —
356/// a different one at each of its four check sites, none of them the total.
357/// See [`Graph::charge_walk`].
358///
359/// It costs a non-atomic `Cell` read-modify-write per id, on a path that has
360/// just done a hash insert. Thread-local rather than atomic because a
361/// traversal is single-threaded — `gunnar-cli` gives each transfer one blocking
362/// thread for its whole duration — so the thread *is* the request, which also
363/// gives libtest's per-test threads a per-test peak for free.
364pub(crate) mod meter {
365 use std::cell::Cell;
366
367 thread_local! {
368 static LIVE: Cell<usize> = const { Cell::new(0) };
369 static PEAK: Cell<usize> = const { Cell::new(0) };
370 }
371
372 pub(crate) fn acquire(count: usize) {
373 if count == 0 {
374 return;
375 }
376 let live = LIVE.with(|live| {
377 let now = live.get() + count;
378 live.set(now);
379 now
380 });
381 PEAK.with(|peak| {
382 if live > peak.get() {
383 peak.set(live);
384 }
385 });
386 }
387
388 pub(crate) fn release(count: usize) {
389 LIVE.with(|live| live.set(live.get().saturating_sub(count)));
390 }
391
392 /// Forget the peak. Live ids are left alone; a set that outlives the reset
393 /// is still held, and pretending otherwise would let a test start from a
394 /// lie.
395 pub(crate) fn reset_peak() {
396 PEAK.with(|peak| peak.set(LIVE.with(|live| live.get())));
397 }
398
399 /// The most ids held at once since the last [`reset_peak`].
400 ///
401 /// Test-only, and the only part of this module that still is: the peak is
402 /// what a test asserts against, while the shipping ceiling reads
403 /// [`live`] — a request is refused for what it is holding now, not for what
404 /// it held before something was freed.
405 #[cfg(test)]
406 pub(crate) fn peak() -> usize {
407 PEAK.with(|peak| peak.get())
408 }
409
410 /// Ids held right now.
411 pub(crate) fn live() -> usize {
412 LIVE.with(|live| live.get())
413 }
414
415 /// Put `live` back to `to`, which is what a request's scope guard does when
416 /// it ends.
417 ///
418 /// An assignment and not a subtraction, on purpose. A [`super::Selection`]
419 /// outlives the walk that produced it and may be dropped on another thread
420 /// — the pack writer's — and that drop releases against a counter that is
421 /// not this one. Subtracting would leave every subsequent request on this
422 /// thread starting from a number that drifts one way forever; assigning
423 /// makes each request's charge exactly its own.
424 pub(crate) fn restore(to: usize) {
425 LIVE.with(|live| live.set(to));
426 }
427}
428
429/// Where a tree walk puts what it finds.
430///
431/// Two callers want two different things out of the same walk. The fetch path
432/// needs the ids, in order, because it is about to pack them. The exclusion
433/// walk needs only that they were *visited*; their ids are already in its
434/// `seen` set, and building a second repository-sized `Vec` of them is half of
435/// One walker, two sinks (one implementation).
436///
437/// **Neither arm is metered, and that is deliberate rather than an omission.**
438///
439/// `Tally` holds nothing by construction. `Keep` holds a `Vec`, and a `Vec` has
440/// no hook to release on — charging its pushes would acquire against
441/// [`meter`] with nothing on the other side, so a `Graph` that served two
442/// selections would bill the second for the first. That is not hypothetical:
443/// it is what `a_fetch_holds_the_clients_object_closure_once` reported the
444/// first time this was tried, 610 ids still charged after the set that held
445/// them was gone.
446///
447/// It costs nothing to leave out. Every id a `Keep` walk emits was put into
448/// that walk's `seen` set on the way — [`Graph::tree_closure`] pushes only what
449/// `seen.insert` just accepted — and `seen` is an [`OidSet`], which is metered
450/// exactly because it has the `Drop` that keeps acquire and release balanced.
451/// So the emitted vector is a second copy of a subset of a set that is already
452/// charged, and [`WALK_BYTES_PER_OBJECT`](crate::WALK_BYTES_PER_OBJECT) is a
453/// whole-process rate measured with that vector, the writer's copy of it and
454/// gix's own lists all resident.
455pub(crate) enum Emit<'v> {
456 /// Keep them.
457 Keep(&'v mut Vec<ObjectId>),
458 /// Count them and throw them away.
459 Tally(usize),
460}
461
462impl Emit<'_> {
463 fn push(&mut self, id: ObjectId) {
464 match self {
465 Emit::Keep(out) => out.push(id),
466 Emit::Tally(count) => *count += 1,
467 }
468 }
469
470 fn len(&self) -> usize {
471 match self {
472 Emit::Keep(out) => out.len(),
473 Emit::Tally(count) => *count,
474 }
475 }
476}
477
478/// What a tree walk remembers, so that each object is visited exactly once.
479///
480/// # Why this is a trait and not an [`OidSet`]
481///
482/// `Graph::tree_closure` is the **one** tree walk in gunnar (one
483/// implementation), and it now has two callers that record what it found in
484/// different shapes. The walk path records object ids, because that is what it
485/// hands back. The bitmap path records *bits*, because its whole point is that
486/// the client's history never becomes a repository-sized set of ids — and
487/// because a bit already set by some commit's stored bitmap is exactly the
488/// signal to stop descending, which an id set cannot give.
489///
490/// Generic rather than `&mut dyn`: this is called once per object in the
491/// transfer, and a virtual call per object is a cost with nothing behind it.
492pub(crate) trait Seen {
493 /// Record `id`, and say whether the walk should descend into it.
494 ///
495 /// `false` means "already accounted for" — either seen in this walk or
496 /// known to be on the far side of the cut.
497 fn insert(&mut self, id: &ObjectId) -> bool;
498}
499
500impl Seen for OidSet {
501 fn insert(&mut self, id: &ObjectId) -> bool {
502 OidSet::insert(self, *id)
503 }
504}
505
506/// The don't-send set, keeping a note of every id **this transfer** added to
507/// it.
508///
509/// [`select_objects`] uses one set for two questions that are not the same
510/// question: *has the walk already accounted for this id?* and *does the client
511/// hold it?* Step 6 answers the first by inserting everything it visits, which
512/// makes the second one wrong for exactly the ids it inserted — and vouching
513/// for one of those is the thin-pack defect [`Selection::client_has`] documents.
514///
515/// So the ids step 6 adds are listed as they go and taken back out at the end.
516/// The list is bounded by what the transfer *touches*, not by the repository,
517/// which is what keeps the one repository-sized set at one.
518struct Carried<'a> {
519 seen: &'a mut OidSet,
520 carried: &'a mut Vec<ObjectId>,
521}
522
523impl Seen for Carried<'_> {
524 fn insert(&mut self, id: &ObjectId) -> bool {
525 let fresh = self.seen.insert(*id);
526 if fresh {
527 self.carried.push(*id);
528 }
529 fresh
530 }
531}
532
533/// What to do about an object that is referenced but absent.
534#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
535pub(crate) enum Missing {
536 /// Fail. Used for everything the server is about to *send*, and for the
537 /// connectivity check on a push.
538 #[default]
539 Fail,
540 /// Skip it. Used for the client's `have`s: a shallow or damaged client may
541 /// name a commit whose ancestry it does not have, and that is its business,
542 /// not a server error.
543 Skip,
544}
545
546/// Read-side view of the object database, with the parsing gunnar needs.
547///
548/// **One of these is one request's traversal scope.** Constructing it snapshots
549/// the thread's live-id count and resets the peak; dropping it puts the count
550/// back and hands any [`WalkPool`] reservation back. See [`Graph::charge_walk`].
551pub(crate) struct Graph<'a> {
552 store: &'a dyn GitServe,
553 hash_kind: Kind,
554 /// An operator's explicit cap on objects held, when one was set.
555 budget: Option<usize>,
556 /// Bytes of bookkeeping this traversal may hold, from
557 /// [`Limits::walk_bytes`].
558 walk_bytes: Option<usize>,
559 /// The server-wide purse, when the server gave this connection one.
560 pool: Option<std::sync::Arc<crate::WalkPool>>,
561 /// What this traversal currently holds from [`Self::pool`].
562 reserved: std::cell::Cell<usize>,
563 /// Ids the thread was already holding when this scope opened.
564 entered_at: usize,
565}
566
567/// The traversal scope ends here, and both halves of it are released.
568///
569/// The [`WalkPool`](crate::WalkPool) reservation must come back or the pool
570/// closes for good after enough requests, and `meter::live` must go back to
571/// what it was or the *next* request on this thread starts already charged. The
572/// second is the one that is easy to get wrong: a [`Selection`] outlives its
573/// `Graph` and is dropped by the pack writer, possibly on another thread, so
574/// this cannot be a subtraction of what the walk acquired.
575impl Drop for Graph<'_> {
576 fn drop(&mut self) {
577 if let Some(pool) = &self.pool {
578 pool.release(self.reserved.get());
579 }
580 meter::restore(self.entered_at);
581 }
582}
583
584impl<'a> Graph<'a> {
585 /// A traversal scope over `store`, for a repository whose ids are
586 /// `hash_kind`.
587 ///
588 /// # Why the hash kind is a parameter now
589 ///
590 /// It was `store.hash_kind()`. [`GitServe`] has no such method: the frozen
591 /// signature leaves
592 /// *"whether `hash_kind()` belongs on `GitOps`"* open, because it needs a
593 /// `HashKind` type in a crate that depends on `anyhow` and nothing else.
594 /// Threading it in from the caller settles the question for this crate
595 /// without pre-empting it for the contract — `RepoStore::hash_kind()`
596 /// already exists and the server already holds one per repository, so the
597 /// value is not being invented, only carried the last hop. The parsers here
598 /// need it to size a parent oid, and one wrong answer silently decodes a
599 /// sha256 commit as sha1, so it is a required argument rather than a
600 /// defaulted one.
601 pub(crate) fn new(store: &'a dyn GitServe, hash_kind: Kind) -> Self {
602 let entered_at = meter::live();
603 meter::reset_peak();
604 Graph {
605 hash_kind,
606 store,
607 budget: None,
608 walk_bytes: None,
609 pool: None,
610 reserved: std::cell::Cell::new(0),
611 entered_at,
612 }
613 }
614
615 /// An operator's object cap, when `gunnar serve` was given one.
616 ///
617 /// `None` is the shipping default and is **not** an unbounded walk:
618 /// [`Limits::walk_bytes`](crate::Limits::walk_bytes) is what bounds it, in
619 /// the unit a machine is sized in. Leaving this at
620 /// [`DEFAULT_BUDGET`] would have kept the kernel-sized clone refused
621 /// however generous the byte ceiling was, which is the whole thing being
622 /// fixed.
623 pub(crate) fn with_budget(mut self, budget: Option<usize>) -> Self {
624 self.budget = budget;
625 self
626 }
627
628 /// The bytes ceiling and the shared purse this traversal serves under.
629 ///
630 /// Separate from [`Self::with_budget`] because they are different
631 /// quantities: `budget` is an operator's explicit cap on how many objects a
632 /// request may touch, and this is what the machine can hold. The push
633 /// side's connectivity walk sets only the first (see
634 /// [`CONNECTIVITY_BUDGET`]), because a push has already paid its admission
635 /// through `gunnar_pack::IndexOptions` by the time it runs.
636 pub(crate) fn with_walk_limit(
637 mut self,
638 walk_bytes: Option<usize>,
639 pool: Option<std::sync::Arc<crate::WalkPool>>,
640 ) -> Self {
641 self.walk_bytes = walk_bytes;
642 self.pool = pool;
643 self
644 }
645
646 /// **The one place a traversal's size is judged.**
647 ///
648 /// # The four checks this replaces, and why four was three too many
649 ///
650 /// [`DEFAULT_BUDGET`] was tested at four sites and each one compared a
651 /// *different* accumulator against the same number:
652 /// [`Graph::commit_closure`] its own commit list,
653 /// [`Graph::limited_closure`] its own, [`Graph::tree_closure`] the
654 /// [`Emit`] it was handed, [`Reachability`] a visit counter. A single fetch
655 /// runs the first (or second) and then the third, so it could hold two full
656 /// budgets and be refused by neither — and step 5's `seen` set, which is
657 /// repository-sized and outlives both, was compared against nothing at all.
658 /// Under a `blob:none` filter that set grows with every blob in the
659 /// repository while the `Emit` being charged grows with none of them.
660 ///
661 /// So the check is against [`meter::live`], the ids the thread is actually
662 /// holding, minus what it was already holding when this scope opened. One
663 /// number, every accumulator in it by construction (one implementation): a new
664 /// repository-sized [`OidSet`] is charged the day it is written, without
665 /// anyone remembering to add a fifth comparison.
666 ///
667 /// # Nothing is inflated to charge it
668 ///
669 /// The bytes come from the id count and [`WALK_BYTES_PER_OBJECT`], a
670 /// measured rate. A budget that had to decompress an object to learn what to
671 /// charge for it would be re-creating the defect that made `gix-odb` inflate
672 /// every object just to read its compressed length, which cost 51.3% of
673 /// clone CPU.
674 ///
675 /// [`WALK_BYTES_PER_OBJECT`]: crate::WALK_BYTES_PER_OBJECT
676 fn charge_walk(&self) -> Result<()> {
677 let held = meter::live().saturating_sub(self.entered_at);
678 self.charge_budget(held)?;
679 let bytes = held.saturating_mul(crate::WALK_BYTES_PER_OBJECT);
680 if let Some(limit) = self.walk_bytes {
681 if bytes > limit {
682 return Err(Error::WalkTooLarge { held: bytes, limit });
683 }
684 }
685 // The shared purse, topped up in chunks. `reserved` only ever grows
686 // within one traversal: handing bytes back as a walk's `seen` set
687 // shrinks would let a second transfer take the room this one is about
688 // to need again, which is the shape that deadlocks a pool rather than
689 // bounding it. It all comes back at `Drop`.
690 if let Some(pool) = &self.pool {
691 if bytes >= crate::WalkPool::FLOOR_BYTES && bytes > self.reserved.get() {
692 let want = bytes.next_multiple_of(crate::WalkPool::CHUNK_BYTES);
693 pool.try_reserve(want - self.reserved.get())?;
694 self.reserved.set(want);
695 }
696 }
697 Ok(())
698 }
699
700 /// `--fetch-budget`, and the **one** comparison that applies it.
701 ///
702 /// Written once because it now has two callers that count two different
703 /// things — [`Self::charge_walk`] the ids a walker is holding,
704 /// [`Self::charge_answer`] the size of an answer nobody walked for — and an
705 /// operator's ceiling that meant `>=` on one path and `>` on the other
706 /// would be a flag whose meaning depended on which store was underneath.
707 fn charge_budget(&self, held: usize) -> Result<()> {
708 if let Some(limit) = self.budget {
709 if held >= limit {
710 return Err(Error::WalkBudget { limit });
711 }
712 }
713 Ok(())
714 }
715
716 /// The operator's object cap, charged against an answer that arrived
717 /// **whole**.
718 ///
719 /// # `--fetch-budget` was accepted and ignored by every store gunnar ships
720 ///
721 /// [`Self::charge_walk`] reads [`meter::live`], which counts ids a *walker*
722 /// is holding. On the path [`crate::reachable::select`] takes there is no
723 /// walker: the store does the set algebra over its own reachability index
724 /// and hands back the finished closure, so `meter::live()` is still zero
725 /// when `select_objects` returns at step 0 and the budget is never
726 /// compared against anything. That step is not a rare shape — since the storage
727 /// contract landed the
728 /// znippy cold tier is the storage engine the appliance runs, and it
729 /// answers every unfiltered, non-shallow clone this way. So an operator
730 /// setting `--fetch-budget 1` got a whole clone and exit 0.
731 ///
732 /// [`crate::reachable`] already had to make exactly this argument once, for
733 /// [`crate::Limits::object_bytes`]: a ceiling that quietly stops applying
734 /// on the fast path is worse than no ceiling, because every test of it
735 /// stays green. This is the second one, found the same way.
736 ///
737 /// # Why this charges the budget and not [`crate::Limits::walk_bytes`]
738 ///
739 /// They are ceilings on two different things and only one of them is an
740 /// operator's stated intent. `budget` is *"refuse a clone bigger than N
741 /// objects"*, and N objects is exactly what the store just named — the
742 /// count is exact here, where the walk can only ever approximate it.
743 /// `walk_bytes` and the [`crate::WalkPool`] bound what a *traversal* holds,
744 /// and this path runs no traversal; charging them here would start refusing
745 /// clones the appliance serves today, on a default that is on. That is a
746 /// shipping decision and it is not this function's to take — but the answer
747 /// path does hold a repository-sized `Vec<ObjectId>` that nothing charges,
748 /// and that gap is real and unowned.
749 pub(crate) fn charge_answer(&self, objects: usize) -> Result<()> {
750 self.charge_budget(objects)
751 }
752
753 /// Is this object here?
754 ///
755 /// # It returns a `Result` now, and that is not tidiness
756 ///
757 /// `ObjectStore::contains` was infallible `bool`; `GitOps::has` is
758 /// `Result<bool>`. The tempting adaptation is `.unwrap_or(false)`, and at
759 /// three of this method's call sites it would be a **silent under-send**:
760 /// `Missing::Skip` drops an object it is told is absent, so a backend fault
761 /// would quietly narrow a pack and exit zero. That is the shape a measured
762 /// assertion is about. The fault propagates instead, and the one place
763 /// where a negative really is the answer keeps its own method —
764 /// [`Graph::contains_have`].
765 pub(crate) fn contains(&self, id: &gix_hash::oid) -> Result<bool> {
766 self.store.has(id.as_bytes()).map_err(backend)
767 }
768
769 /// [`Graph::contains`] for a **`have`**, where the client not having been
770 /// heard of is the answer rather than a reason to go and look at the disk
771 /// again.
772 ///
773 /// Only negotiation may call this. A `want`, a delta base and the walk's own
774 /// `Missing::Skip` filter all keep [`Graph::contains`], because for them a
775 /// stale `false` is a refused fetch rather than a wider pack.
776 ///
777 /// # What was lost, and it can only cost syscalls
778 ///
779 /// `ObjectStore` had two questions here, `contains` and
780 /// `contains_without_rescan`, and the difference was whether a miss was
781 /// allowed to trigger a re-scan of the backing store first. git draws the
782 /// same line (`OBJECT_INFO_QUICK` against `HAS_OBJECT_RECHECK_PACKED`).
783 /// `GitOps` has one `has`, so the distinction is gone and every question
784 /// gets whatever the backend does. The **answers** are unchanged; only the
785 /// syscalls a negotiation may spend re-deriving a `false` it already had
786 /// are, and nothing in this crate can observe that. It is recorded here
787 /// rather than dropped because it is a measured optimisation that a future
788 /// `GitServe` method could restore, not a rule that stopped being true.
789 pub(crate) fn contains_have(&self, id: &gix_hash::oid) -> bool {
790 has_for_negotiation(self.store, id)
791 }
792
793 /// The object database underneath, for the callers that need to ask it
794 /// something a graph walk cannot — a bulk `sizes`, a `select`.
795 pub(crate) fn store(&self) -> &'a dyn GitServe {
796 self.store
797 }
798
799 /// Load an object's content into `buf`, returning its kind.
800 ///
801 /// # 🔴 The serial closure walk lives through this function. Read before changing it.
802 ///
803 /// Every commit in the repository used to be read and decoded **twice** —
804 /// once for its parents and once for the root tree the first decode had
805 /// already parsed and discarded. Removing that measured **1.295x on
806 /// `walk_1m`** and **1.025x on `clone_large`**, instructions retired, both
807 /// sides, and the guard that holds it is `commit_reads_per_commit` in
808 /// `nornir-bench`: it must read **1.000**, never 2.000. What keeps it there
809 /// is that [`Graph::commit_links`] makes exactly **one** call to this
810 /// function and returns the tree *and* the parents out of that one decode.
811 /// Retargeting the store underneath is exactly where a second call gets
812 /// added by accident.
813 ///
814 /// # The buffer, and what the frozen signature costs it
815 ///
816 /// `buf` belongs to the caller so that a walk of a million commits reuses
817 /// one allocation; the allocator was **14.2%** of `gunnar.walk_1m`'s
818 /// profile on 2026-08-03, which is why it is a parameter at all.
819 /// `GitServe::read`, as frozen, returns an owned `Vec<u8>`, so the
820 /// allocation happens inside the backend and the caller's is dropped. The
821 /// move below (`*buf = bytes`) is the least-bad adaptation — it is a move,
822 /// not a copy, so no object's bytes are duplicated — but it is one malloc
823 /// and one free per object where there used to be neither. **This is a
824 /// signature question, not a bug here**: a defaulted `read_into` on
825 /// `GitServe`, costing an implementer nothing, restores the reuse. It is
826 /// reported rather than invented, because the contract is frozen and
827 /// changing it is a two-implementation decision.
828 ///
829 /// The magnitude of that regression is an **unmeasured hypothesis**: the
830 /// bench target does not build and nothing has been run.
831 fn load(&self, id: &ObjectId, what: &str, buf: &mut Vec<u8>) -> Result<object::Kind> {
832 match self.store.read(id.as_bytes()).map_err(backend)? {
833 Some((kind, bytes)) => {
834 *buf = bytes;
835 resolved_kind(id, kind)
836 }
837 None => Err(Error::missing(*id, what.to_owned())),
838 }
839 }
840
841 fn corrupt(&self, id: &ObjectId, err: impl std::fmt::Display) -> Error {
842 Error::CorruptObject {
843 oid: *id,
844 reason: err.to_string(),
845 }
846 }
847
848 /// The object's kind, or `None` if it is not here.
849 ///
850 /// A header read, never an object read: `GitServe::header` answers kind and
851 /// post-resolution size *without* the payload, which is the whole reason it
852 /// is a separate method from `read`.
853 pub(crate) fn kind(&self, id: &ObjectId) -> Result<Option<object::Kind>> {
854 match self.store.header(id.as_bytes()).map_err(backend)? {
855 Some((kind, _size)) => Ok(Some(resolved_kind(id, kind)?)),
856 None => Ok(None),
857 }
858 }
859
860 /// Follow a tag chain to the first non-tag object.
861 ///
862 /// The intermediate tag objects come back too: a `want` naming an
863 /// annotated tag has to put the tag itself in the pack, not only what it
864 /// points at.
865 pub(crate) fn peel(&self, id: &ObjectId) -> Result<Peeled> {
866 let mut tags = Vec::new();
867 let mut current = *id;
868 let mut buf = Vec::new();
869 for depth in 0..MAX_TAG_DEPTH {
870 // **The label is the first thing an operator reads.** Every read
871 // here used to be called `"peeling a tag"`, including the first
872 // one — so a push whose tip was simply absent came back as
873 // `missing necessary objects: <oid> is not in this repository
874 // (peeling a tag)` with no tag anywhere in the push, the
875 // repository or the pack. bench `bare_lifecycle` refused a
876 // force-push rewind with exactly that sentence and it was read as
877 // a tag-peeling defect in the connectivity check; the protocol
878 // half is clean under every tag shape a frontier can hold
879 // (`tests/force_update.rs`), and what the sentence was actually
880 // reporting was an object the store did not have.
881 //
882 // Only a read that follows a tag's target is tag peeling. The
883 // first read is whatever the caller handed in.
884 let what = if depth == 0 {
885 "resolving an object"
886 } else {
887 "peeling a tag"
888 };
889 // **Kind is a HEADER question; the inflated read is only for a
890 // tag's BODY.** `header` answers kind off the index
891 // (`GitServe::header`) without pulling the object through the
892 // exploded table. The exploded read below is genuinely needed only
893 // to parse a tag's *target* out of its body — so for the terminal
894 // object of a peel (a commit, in the overwhelming common case of
895 // one annotated tag over a commit) `read` was pure waste: a full
896 // inflated pull, and on a many-ref repository a 64 MiB Arrow batch
897 // decode that thrashes `ExplodedTable::CACHE`.
898 // `advertise_from` peels every annotated tag on every advertisement
899 // (`ls-refs`, clone, fetch, replication), so this halves the
900 // exploded reads that whole path costs — reads per advertised tag
901 // fall from two (the tag *and* the commit it names) to one (the tag
902 // alone). The before/after µs/ref were measured.
903 let Some(kind) = self.kind(¤t)? else {
904 return Err(Error::missing(current, what.to_owned()));
905 };
906 if kind != object::Kind::Tag {
907 return Ok(Peeled {
908 id: current,
909 kind,
910 tags,
911 });
912 }
913 // It IS a tag: the body is now genuinely needed, to read the target
914 // it names. `load` re-confirms the kind it just derived — the two
915 // must agree (`serve.rs::header_resolves_every_delta_to_its_real_kind`).
916 let loaded = self.load(¤t, "peeling a tag", &mut buf)?;
917 debug_assert_eq!(loaded, object::Kind::Tag);
918 let target = object::tag_target(&buf, self.hash_kind)
919 .map_err(|err| self.corrupt(¤t, err))?;
920 tags.push(current);
921 current = target;
922 }
923 Err(self.corrupt(id, format!("tag chain deeper than {MAX_TAG_DEPTH}")))
924 }
925
926 /// A commit's tree and parents, and nothing else.
927 ///
928 /// [`Graph::commit`] answers a superset of this by way of
929 /// `CommitRef::from_bytes`, which decodes the **whole** object: both
930 /// signatures, both dates, the encoding, the extra headers and the message.
931 /// The closure walks want two fields off the front of the object and never
932 /// look at the rest, and `committed_at` — the one field of [`CommitFacts`]
933 /// that is not here — is read only by negotiation, which asks about a
934 /// handful of commits rather than about every commit in the repository.
935 ///
936 /// `CommitRefIter` decodes only as far as it is driven, and git's own
937 /// commit grammar puts `tree` first and every `parent` next, so stopping at
938 /// the first non-parent token leaves the committer, the encoding, the extra
939 /// headers and the message unparsed. It does **not** make the walk
940 /// signature-free: the iterator reports "no more parents" by decoding the
941 /// *author*, so one signature and one date parse per commit remain. Halving
942 /// that work is what is on offer, not removing it — `gix_actor` and
943 /// `gix_date` together were 2.8% of `gunnar.walk_1m`'s profile on
944 /// 2026-08-03.
945 ///
946 /// Both buffers belong to the caller and are reused across commits. This
947 /// runs once per commit in the repository, and a `Vec::new()` per call is a
948 /// malloc, a grow and a free per commit; the allocator was 14.2% of that
949 /// same profile.
950 pub(crate) fn commit_links(
951 &self,
952 id: &ObjectId,
953 buf: &mut Vec<u8>,
954 parents: &mut Vec<ObjectId>,
955 ) -> Result<ObjectId> {
956 let kind = self.load(id, "reading a commit", buf)?;
957 if kind != object::Kind::Commit {
958 return Err(self.corrupt(id, format!("expected a commit, found a {kind}")));
959 }
960 // `tree`, then `parent`*, then `author`: `commit_links` stops at the
961 // first line that is not a parent, so the committer, the encoding, the
962 // extra headers and the message are never looked at.
963 object::commit_links(buf, self.hash_kind, parents).map_err(|err| self.corrupt(id, err))
964 }
965
966 /// A commit's parents, tree and committer timestamp.
967 pub(crate) fn commit(&self, id: &ObjectId) -> Result<CommitFacts> {
968 let mut buf = Vec::new();
969 self.commit_facts(id, &mut buf)
970 }
971
972 /// [`Graph::commit`] with the caller's buffer, for a walk that reads
973 /// thousands of commits.
974 ///
975 /// **One pass of `CommitRefIter`, stopping at the committer.** This used to
976 /// be `CommitRef::from_bytes`, which decodes the *whole* object — both
977 /// signatures, both dates, the encoding, every extra header and the entire
978 /// message — to hand back three fields. `commit_links` above is this same
979 /// reader with the date left off, and it already had to decode the author
980 /// to learn that the parents were done; the committer is the very next
981 /// token after that, so the date is nearly free and the message never gets
982 /// looked at. One decoder rather than two (one implementation).
983 ///
984 /// The date is `SignatureRef::seconds`, which parses the leading integer
985 /// and ignores the timezone, where the old reader ran the strict
986 /// `gix_date` header parser and fell back to `0`. A well-formed commit
987 /// gives the identical number either way; a malformed one now yields its
988 /// seconds instead of the epoch, which is git's own leniency
989 /// (`parse_commit_buffer` does the same) and is strictly better for a walk
990 /// that orders by that number.
991 fn commit_facts(&self, id: &ObjectId, buf: &mut Vec<u8>) -> Result<CommitFacts> {
992 let mut parents = Vec::new();
993 let (tree, committed_at) = self.commit_facts_into(id, buf, &mut parents)?;
994 Ok(CommitFacts {
995 tree,
996 parents,
997 committed_at,
998 })
999 }
1000
1001 /// [`Graph::commit_facts`] into the **caller's** buffers, returning
1002 /// `(tree, committed_at)`.
1003 ///
1004 /// Same decoder, same single pass; the only difference is who owns the two
1005 /// `Vec`s. That matters wherever this is called once per commit over a
1006 /// list, for the reason [`Graph::commit_links`]'s own doc gives — a
1007 /// `Vec::new()` per call is a malloc, a grow and a free per commit, and the
1008 /// allocator was 14.2% of `gunnar.walk_1m`'s profile on 2026-08-03.
1009 ///
1010 /// # The measurement that put this here, **and what it did not explain**
1011 ///
1012 /// `negotiate::HaveMemo::resolve` used to call [`Graph::commit`] and read
1013 /// only `committed_at`, so `CommitFacts::parents` was dead and the compiler
1014 /// was free to delete the vector and the loop that fills it. The moment
1015 /// negotiation began *using* the parents (git's `THEY_HAVE` marking, see
1016 /// [`crate::negotiate::Negotiation::ack`]) that stopped, and the difference
1017 /// showed up as **+7.6 ms of server CPU** on a fetch across a 500-commit
1018 /// divergence with 1 008 `have`s — MEASURED 2026-08-04,
1019 /// `bench/examples/fetchbytes-probe.rs`, 29.0 → 36.6 ms median of five,
1020 /// isolated by keeping the parent read and removing only its consumer.
1021 ///
1022 /// **This entry point did not recover it, and that is recorded rather than
1023 /// quietly dropped.** The obvious reading of that 7.6 ms was the allocation
1024 /// [`Graph::commit_links`]'s doc names — a malloc, a grow and a free per
1025 /// commit. Hoisting both buffers out of the loop, which is exactly what
1026 /// this method exists to allow, moved the number by **nothing** (36.6 →
1027 /// 37.4 ms, inside the band). So the cost is the parent *decode*, not the
1028 /// allocation behind it, and what remains unexplained is why decoding
1029 /// ~1 000 parent lines costs 7.6 ms. It is kept because one buffer pair per
1030 /// negotiation is right regardless of what else is slow, not because it was
1031 /// measured to be worth 7.6 ms — it was measured not to be.
1032 ///
1033 /// The cost is confined to the **walk**: on the bitmap path, which is what
1034 /// every repacked repository serves from, the same change measured 0.0231 →
1035 /// 0.0217 s, i.e. no regression at all.
1036 fn commit_facts_into(
1037 &self,
1038 id: &ObjectId,
1039 buf: &mut Vec<u8>,
1040 parents: &mut Vec<ObjectId>,
1041 ) -> Result<(ObjectId, i64)> {
1042 let kind = self.load(id, "reading a commit", buf)?;
1043 if kind != object::Kind::Commit {
1044 return Err(self.corrupt(id, format!("expected a commit, found a {kind}")));
1045 }
1046 // `tree`, then `parent`*, then `author`, then `committer`. Everything
1047 // after that is the encoding, the extra headers and the message, and
1048 // none of it is ours to decode.
1049 //
1050 // A commit with an unparsable committer date is not a reason to fail a
1051 // clone; it only costs the walk its ordering for that one commit, and
1052 // `SLOP` is what absorbs exactly that. `commit_facts` answers `0` there
1053 // rather than an error, which is the same thing the
1054 // `gix_object::CommitRefIter` this replaced did by never yielding a
1055 // `Committer` token.
1056 object::commit_facts(buf, self.hash_kind, parents).map_err(|err| self.corrupt(id, err))
1057 }
1058
1059 /// [`Graph::commit_facts_into`], for callers outside this module.
1060 pub(crate) fn commit_into(
1061 &self,
1062 id: &ObjectId,
1063 buf: &mut Vec<u8>,
1064 parents: &mut Vec<ObjectId>,
1065 ) -> Result<i64> {
1066 self.commit_facts_into(id, buf, parents).map(|(_, at)| at)
1067 }
1068
1069 /// Commits reachable from `tips`, in breadth-first order.
1070 ///
1071 /// * `stop`: commits that are neither included nor traversed. The client's
1072 /// side of the graph.
1073 /// * `boundary`: commits that *are* included but whose parents are not
1074 /// traversed at all. A client's declared shallow frontier.
1075 /// * `restrict`: when set, only parents inside this set are followed.
1076 ///
1077 /// The last two are not the same thing, and conflating them is a bug worth
1078 /// naming: `deepen-not refs/heads/side` cuts a **merge** off from *one* of
1079 /// its parents, and treating that merge as a boundary drops the other
1080 /// parent's whole line as well. A restriction set says which parents
1081 /// survive; a boundary says none do.
1082 pub(crate) fn commit_closure(
1083 &self,
1084 tips: &[ObjectId],
1085 stop: &HashSet<ObjectId>,
1086 boundary: &HashSet<ObjectId>,
1087 restrict: Option<&HashSet<ObjectId>>,
1088 missing: Missing,
1089 ) -> Result<CommitClosure> {
1090 let mut seen = OidSet::new();
1091 let mut out = CommitClosure::default();
1092 let mut queue: VecDeque<ObjectId> = VecDeque::new();
1093 // Bounded by the number of distinct stopped-at parents, which is at
1094 // most the frontier between what the client has and what it wants —
1095 // not by the repository. A plain `HashSet` on purpose: `OidSet` meters
1096 // the repository-sized sets the rule is about, and metering this one
1097 // would blur the number that matters.
1098 let mut edges: HashSet<ObjectId> = HashSet::new();
1099 // Both live for the whole walk: see `commit_links`.
1100 let mut buf = Vec::new();
1101 let mut parents = Vec::new();
1102
1103 for tip in tips {
1104 if !stop.contains(tip) && seen.insert(*tip) {
1105 queue.push_back(*tip);
1106 }
1107 }
1108
1109 while let Some(id) = queue.pop_front() {
1110 self.charge_walk()?;
1111 if missing == Missing::Skip && !self.contains(&id)? {
1112 continue;
1113 }
1114 let tree = match self.commit_links(&id, &mut buf, &mut parents) {
1115 Ok(tree) => tree,
1116 Err(Error::ObjectMissing { .. }) if missing == Missing::Skip => continue,
1117 Err(err) => return Err(err),
1118 };
1119 // Pushed together, and nowhere else, which is what makes
1120 // `CommitClosure`'s pairing true by construction rather than by
1121 // agreement between two loops.
1122 out.ids.push(id);
1123 out.trees.push(tree);
1124 if boundary.contains(&id) {
1125 continue;
1126 }
1127 for parent in parents.drain(..) {
1128 if stop.contains(&parent) {
1129 // An **edge**: a commit the client holds, sitting directly
1130 // behind one it does not. `edges` is deduplicated because
1131 // a widely-merged parent is stopped at once per child, and
1132 // step 5 would otherwise walk its tree that many times.
1133 if edges.insert(parent) {
1134 out.edges.push(parent);
1135 }
1136 continue;
1137 }
1138 if restrict.is_some_and(|allowed| !allowed.contains(&parent)) {
1139 continue;
1140 }
1141 if seen.insert(parent) {
1142 queue.push_back(parent);
1143 }
1144 }
1145 }
1146 Ok(out)
1147 }
1148
1149 /// The commits to send, worked out **without** first enumerating the
1150 /// client's whole history.
1151 ///
1152 /// This is git's `limit_list()` (`revision.c`), and it replaces the pair of
1153 /// [`Graph::commit_closure`] calls that [`select_objects`] used to open
1154 /// with. Those two are correct and they are also `O(client history)`: the
1155 /// first walks every commit the client holds purely to build a stop set for
1156 /// the second, so a client one day behind on a hundred-thousand-commit
1157 /// repository costs a hundred thousand commit reads to serve twenty.
1158 ///
1159 /// # How one walk does what two did
1160 ///
1161 /// Both sides go into **one queue ordered by committer date**, newest
1162 /// first, each commit carrying git's `UNINTERESTING` bit. Popping newest
1163 /// first means the two histories are drained together rather than one after
1164 /// the other, and the moment nothing interesting is left in the queue the
1165 /// walk is done: everything still queued is reachable from a `have`, so
1166 /// dropping it cannot drop a commit that belongs in the pack. That
1167 /// termination is [`still_interesting`](Limiter::still_interesting), and
1168 /// [`SLOP`] is the slack it gives the ordering.
1169 ///
1170 /// # Why it cannot send less
1171 ///
1172 /// The walk stops only at a point where `everybody_uninteresting` has just
1173 /// returned true, so every id it abandons is marked `UNINTERESTING`. A
1174 /// commit is marked `UNINTERESTING` only by propagation from a `have`, and
1175 /// a commit reachable from a `have` is one the client demonstrably holds.
1176 /// So an abandoned commit is a held commit, and its ancestors are held too
1177 /// — which is why abandoning them unread is safe. In the other direction
1178 /// skew beyond `SLOP` can leave a held commit *un*marked and send it again:
1179 /// a superset, the same trade [`Exclusion::Edges`] already makes, and the
1180 /// same one git makes.
1181 ///
1182 /// `haves` are taken as the peeled `have` commits, not their closure —
1183 /// computing that closure is the cost this exists to remove.
1184 pub(crate) fn limited_closure(
1185 &self,
1186 tips: &[ObjectId],
1187 haves: &[ObjectId],
1188 boundary: &HashSet<ObjectId>,
1189 have_boundary: &HashSet<ObjectId>,
1190 restrict: Option<&HashSet<ObjectId>>,
1191 missing: Missing,
1192 ) -> Result<LimitedClosure> {
1193 let mut walk = Limiter::default();
1194 let mut buf = Vec::new();
1195
1196 // The `have`s first, so that a commit which is both a `want` and a
1197 // `have` is already uninteresting when the want reaches it — the same
1198 // answer the two-pass walk gives by testing every tip against its stop
1199 // set before queueing it.
1200 for id in haves {
1201 walk.admit(self, *id, true, Missing::Skip, &mut buf)?;
1202 }
1203 for id in tips {
1204 walk.admit(self, *id, false, missing, &mut buf)?;
1205 }
1206
1207 let mut out = LimitedClosure::default();
1208 let mut slop = SLOP;
1209 // git's `date`, starting at `TIME_MAX`: the committer date of the last
1210 // commit this walk kept.
1211 let mut date = i64::MAX;
1212 let mut cache: Option<ObjectId> = None;
1213
1214 while let Some(Pending { id, .. }) = walk.queue.pop() {
1215 if cache == Some(id) {
1216 cache = None;
1217 }
1218 self.charge_walk()?;
1219 let uninteresting = walk.is_uninteresting(&id);
1220 let facts = walk
1221 .parsed
1222 .get(&id)
1223 .expect("only a parsed commit is queued");
1224 let (tree, commit_date) = (facts.tree, facts.committed_at);
1225 // Cloned because `admit` takes `&mut walk`. Bounded by one commit's
1226 // parent count, not by the repository.
1227 let parents = facts.parents.clone();
1228
1229 if uninteresting {
1230 out.uninteresting.push(id);
1231 // A shallow client's graft: it holds this commit and says so,
1232 // but not what is behind it. Walking past it here counts the
1233 // whole repository as already-delivered and packs nothing.
1234 if !have_boundary.contains(&id) {
1235 for parent in &parents {
1236 walk.admit(self, *parent, true, Missing::Skip, &mut buf)?;
1237 if have_boundary.contains(parent) {
1238 continue;
1239 }
1240 // git's `process_parents`: having just marked the
1241 // parent, push the mark on down through whatever this
1242 // walk has already read. It reads nothing itself.
1243 walk.mark_parents_uninteresting(parent, have_boundary);
1244 }
1245 }
1246 slop = walk.still_interesting(date, slop, &mut cache);
1247 if slop > 0 {
1248 continue;
1249 }
1250 break;
1251 }
1252
1253 if !boundary.contains(&id) {
1254 for parent in &parents {
1255 if restrict.is_some_and(|allowed| !allowed.contains(parent)) {
1256 continue;
1257 }
1258 walk.admit(self, *parent, false, missing, &mut buf)?;
1259 }
1260 }
1261 date = commit_date;
1262 out.ids.push(id);
1263 out.trees.push(tree);
1264 }
1265
1266 // git's `mark_edges_uninteresting` (`list-objects.c`): the commits the
1267 // client holds that sit directly behind one it is being sent.
1268 //
1269 // Read off the finished walk rather than recorded inside it, because a
1270 // parent is not always *known* to be uninteresting at the moment its
1271 // child is popped — the mark can arrive later, from another branch of
1272 // the same walk — and an edge set collected too early would miss
1273 // exactly the merge parents whose trees carry the shared content. It
1274 // costs no reads: every parent list is already in hand.
1275 let mut edges: HashSet<ObjectId> = HashSet::new();
1276 for id in &out.ids {
1277 if boundary.contains(id) {
1278 continue;
1279 }
1280 let Some(facts) = walk.parsed.get(id) else {
1281 continue;
1282 };
1283 for parent in &facts.parents {
1284 if !walk.is_uninteresting(parent) {
1285 continue;
1286 }
1287 if edges.insert(*parent) {
1288 out.edges.push(*parent);
1289 }
1290 }
1291 }
1292 Ok(out)
1293 }
1294
1295 /// Add everything under `root` to `out`, skipping anything already in
1296 /// `seen` or `exclude`, and honouring `filter`.
1297 ///
1298 /// `depth` starts at 1 for a commit's own root tree, which is how git
1299 /// numbers `tree:<depth>`: `tree:0` yields no trees at all and `tree:1`
1300 /// yields the root tree only.
1301 ///
1302 /// `max_blob_bytes` is the fetch path's object ceiling
1303 /// ([`crate::Limits::object_bytes`]) and is `None` on every walk that is not
1304 /// about to *send* what it finds. Only blobs are checked, and that is not a
1305 /// shortcut: a commit and a tree are bounded by their own grammar, a blob is
1306 /// bounded by nothing, and the walk already knows an entry is a blob from
1307 /// its tree entry's mode without reading anything.
1308 #[allow(clippy::too_many_arguments)]
1309 pub(crate) fn tree_closure<S: Seen + ?Sized>(
1310 &self,
1311 root: ObjectId,
1312 seen: &mut S,
1313 exclude: &HashSet<ObjectId>,
1314 filter: Option<&Filter>,
1315 missing: Missing,
1316 max_blob_bytes: Option<u64>,
1317 out: &mut Emit<'_>,
1318 ) -> Result<()> {
1319 let mut stack = vec![(root, 1usize)];
1320 let mut buf = Vec::new();
1321 // Hoisted out of the loop for the same reason `commit_links` takes its
1322 // buffers from the caller: these are two mallocs and two frees **per
1323 // tree in the repository**, and the allocator was 14.2% of
1324 // `gunnar.walk_1m`'s profile on 2026-08-03. They are cleared, not
1325 // reallocated, on every entry.
1326 let mut children: Vec<(ObjectId, usize)> = Vec::new();
1327 let mut blobs: Vec<ObjectId> = Vec::new();
1328 while let Some((id, depth)) = stack.pop() {
1329 self.charge_walk()?;
1330 if exclude.contains(&id) || !seen.insert(&id) {
1331 continue;
1332 }
1333 if missing == Missing::Skip && !self.contains(&id)? {
1334 continue;
1335 }
1336 let keep_tree = filter.is_none_or(|f| f.keeps_tree(depth));
1337 let kind = match self.load(&id, "reading a tree", &mut buf) {
1338 Ok(kind) => kind,
1339 Err(Error::ObjectMissing { .. }) if missing == Missing::Skip => continue,
1340 Err(err) => return Err(err),
1341 };
1342 if kind != object::Kind::Tree {
1343 return Err(self.corrupt(&id, format!("expected a tree, found a {kind}")));
1344 }
1345 if keep_tree {
1346 out.push(id);
1347 }
1348 let entries = object::TreeIter::from_bytes(&buf, self.hash_kind);
1349 children.clear();
1350 blobs.clear();
1351 for entry in entries {
1352 let entry = entry.map_err(|err| self.corrupt(&id, err))?;
1353 match entry.kind {
1354 object::EntryKind::Tree => children.push((entry.oid, depth + 1)),
1355 // A gitlink names a commit in *another* repository. It is
1356 // not ours to have, to send, or to demand on a push.
1357 object::EntryKind::Commit => {}
1358 _ => blobs.push(entry.oid),
1359 }
1360 }
1361 for blob in blobs.drain(..) {
1362 // Inside the blob loop and not only at the top of the tree
1363 // loop: one tree's entries are bounded by nothing gunnar chose,
1364 // so charging per tree lets a single wide tree overshoot the
1365 // ceiling by its whole width. It costs a `Cell` read against a
1366 // header read.
1367 self.charge_walk()?;
1368 if exclude.contains(&blob) || !seen.insert(&blob) {
1369 continue;
1370 }
1371 if !self.keeps_blob(&blob, filter, missing, max_blob_bytes)? {
1372 continue;
1373 }
1374 out.push(blob);
1375 }
1376 stack.append(&mut children);
1377 }
1378 Ok(())
1379 }
1380
1381 /// Does this blob go in the pack — and is gunnar willing to hold it?
1382 ///
1383 /// Two questions, **one header read**, because they are asked about the
1384 /// same object at the same moment and reading its header twice would be
1385 /// the second copy one implementation is about. A `filter` that already
1386 /// declines every blob short-circuits before the read: an object that is
1387 /// not going in the pack is not one gunnar will materialise, so its size
1388 /// is nobody's problem.
1389 ///
1390 /// # The ceiling costs a `header` where a `contains` used to do
1391 ///
1392 /// With `max_blob_bytes` set this asks the store for a header rather than
1393 /// for existence. It is a **replacement**, not an addition — the same index
1394 /// probe, plus the entry-header decode gix needs to answer the size — and it
1395 /// buys the only thing that makes the ceiling a guard rather than a report:
1396 /// the size is known *before* any content is read. A ceiling checked after
1397 /// `Find::try_find` has filled a buffer cannot stop the allocation it exists
1398 /// to stop.
1399 fn keeps_blob(
1400 &self,
1401 id: &ObjectId,
1402 filter: Option<&Filter>,
1403 missing: Missing,
1404 max_blob_bytes: Option<u64>,
1405 ) -> Result<bool> {
1406 // What the filter decides without looking at the object at all.
1407 let filter_limit = match filter {
1408 None => None,
1409 Some(filter) => match filter.blob_limit() {
1410 Some(limit) => Some(limit),
1411 None if filter.keeps_any_blob() => None,
1412 None => return Ok(false),
1413 },
1414 };
1415
1416 if filter_limit.is_none() && max_blob_bytes.is_none() {
1417 if missing == Missing::Fail && !self.contains(id)? {
1418 return Err(Error::missing(*id, "packing a blob"));
1419 }
1420 return Ok(true);
1421 }
1422
1423 let Some((_kind, size)) = self.store.header(id.as_bytes()).map_err(backend)? else {
1424 return match missing {
1425 Missing::Skip => Ok(false),
1426 Missing::Fail => Err(Error::missing(*id, "sizing a blob")),
1427 };
1428 };
1429 if let Some(limit) = max_blob_bytes {
1430 if size > limit {
1431 return Err(Error::ObjectTooLarge {
1432 oid: *id,
1433 size,
1434 limit,
1435 });
1436 }
1437 }
1438 // `<`, not `<=`, because that is what git does and a partial clone is
1439 // judged against git.
1440 //
1441 // `list-objects-filter.c`'s `filter_blobs_limit` keeps a blob when
1442 // `object_length < filter_data->max_bytes`, so `blob:limit=1024` omits a
1443 // blob of **exactly** 1024 bytes. gunnar kept it, and the disagreement
1444 // is observable with two commands.
1445 //
1446 // MEASURED 2026-08-05 — one repository, three blobs of 1023, 1024
1447 // and 1025 bytes, `--filter=blob:limit=1024`, the same stock git 2.53.0
1448 // as the client on both arms:
1449 //
1450 // | server | blobs delivered |
1451 // |---|---|
1452 // | stock `git-upload-pack` | 1023 |
1453 // | gunnar, before | 1023, **1024** |
1454 // | gunnar, after | 1023 |
1455 //
1456 // One blob, so it looks small; what it costs is the invariant. A
1457 // partial-clone client sizes its lazy-fetch expectations from git's
1458 // rule, and `tests/real_git_surface.rs` had written gunnar's `<=` down
1459 // as the expected answer, so the suite agreed with the defect instead
1460 // of catching it.
1461 Ok(filter_limit.is_none_or(|limit| size < limit))
1462 }
1463}
1464
1465/// What [`Graph::peel`] found at the end of a tag chain.
1466#[derive(Debug, Clone)]
1467pub(crate) struct Peeled {
1468 /// The first non-tag object.
1469 pub id: ObjectId,
1470 /// Its kind.
1471 pub kind: object::Kind,
1472 /// Every tag object walked through, outermost first.
1473 pub tags: Vec<ObjectId>,
1474}
1475
1476/// The parts of a commit gunnar's traversal needs.
1477#[derive(Debug, Clone)]
1478pub(crate) struct CommitFacts {
1479 pub tree: ObjectId,
1480 pub parents: Vec<ObjectId>,
1481 pub committed_at: i64,
1482}
1483
1484/// What [`Graph::commit_closure`] found: the commits, and each one's root tree.
1485///
1486/// The trees ride along because **every** caller of `commit_closure` walks each
1487/// commit's tree the moment it gets the list back, and asking the store for the
1488/// tree oid again means reading and decoding that commit a *second* time. On
1489/// the `fs` backend that second read is a pack-index probe, a zlib inflate and
1490/// a delta chain — the odb handles are built with `refresh_never()` and no
1491/// object cache, so nothing memoises it. `commit_links` already has the oid in
1492/// hand; dropping it and re-reading the object for it is the twin one
1493/// implementation is about, one field wide.
1494///
1495/// The price is one commit-sized `Vec<ObjectId>` beside the one that was
1496/// already being returned — 20 bytes per commit at sha1, 32 at sha256. It is
1497/// **not** metered by [`meter`], which watches the repository-sized *object*
1498/// sets the one-set rule is about, and it is not one of those: it is bounded by the
1499/// commit count, not the object count.
1500#[derive(Debug, Default)]
1501pub(crate) struct CommitClosure {
1502 /// The commits, in breadth-first order.
1503 pub ids: Vec<ObjectId>,
1504 /// `trees[i]` is `ids[i]`'s root tree. Same length, same order, and true by
1505 /// construction: the one loop that pushes either pushes both.
1506 pub trees: Vec<ObjectId>,
1507 /// Commits in `stop` that this walk ran straight into: a parent of an
1508 /// included commit that the client already holds.
1509 ///
1510 /// This is git's `mark_edges_uninteresting` (`list-objects.c`) edge set,
1511 /// and it falls out of a walk that was happening anyway — the `stop` test
1512 /// is already there, this only records which id failed it. See
1513 /// [`Exclusion::Edges`] for what it buys and what it costs.
1514 ///
1515 /// Empty unless the walk was given a non-empty `stop`, so a clone (no
1516 /// `have`s) never allocates.
1517 pub edges: Vec<ObjectId>,
1518}
1519
1520/// What [`Graph::limited_closure`] found.
1521///
1522/// [`Self::ids`] and [`Self::trees`] are [`CommitClosure`]'s, with the same
1523/// pairing property and for the same reason; [`Self::edges`] is the same edge
1524/// set. The one field that is new is [`Self::uninteresting`], and what it is
1525/// *not* is the point: the commits the walk actually looked at on the client's
1526/// side, which is a frontier, where the two-pass walk's equivalent was the
1527/// client's entire history.
1528#[derive(Debug, Default)]
1529pub(crate) struct LimitedClosure {
1530 /// The commits to send, newest committer date first.
1531 pub ids: Vec<ObjectId>,
1532 /// `trees[i]` is `ids[i]`'s root tree.
1533 pub trees: Vec<ObjectId>,
1534 /// Client-held commits sitting directly behind a commit being sent.
1535 pub edges: Vec<ObjectId>,
1536 /// Client-held commits this walk popped. A frontier, not a history.
1537 pub uninteresting: Vec<ObjectId>,
1538}
1539
1540/// git's `SLOP` (`revision.c`): how many further commits the limiting walk
1541/// looks at once everything left in its queue is uninteresting.
1542///
1543/// Five, git's number, for git's reason. Committer dates are wall-clock
1544/// readings taken on other people's machines, so a queue ordered by them can
1545/// hand back a commit that is *older* than its own child. This is the slack
1546/// that ordering gets before the walk calls the frontier closed. Beyond it the
1547/// walk sends objects the client already had, which is slow; it can never send
1548/// fewer, which is what would be broken. See [`Graph::limited_closure`].
1549const SLOP: usize = 5;
1550
1551/// The bits git keeps on `struct object` while it limits a walk.
1552///
1553/// git's `ADDED` is deliberately absent: it guards against processing one
1554/// commit's parents twice, and here [`flag::SEEN`] already admits an id to the
1555/// queue at most once, so it could never fire.
1556mod flag {
1557 /// Queued, and therefore already read.
1558 pub const SEEN: u8 = 1 << 0;
1559 /// Reachable from something the client already has.
1560 pub const UNINTERESTING: u8 = 1 << 1;
1561 /// Referenced, absent, and tolerated. git re-reads such a commit and leans
1562 /// on `parse_commit_gently`'s per-object memo; gunnar's store has none, so
1563 /// the "we looked, it is not here" answer is remembered here instead.
1564 pub const ABSENT: u8 = 1 << 2;
1565}
1566
1567/// One entry of the limiting walk's date-ordered queue.
1568///
1569/// `Ord` is the committer date first, so [`BinaryHeap::pop`] hands back the
1570/// newest commit — the same end git's `commit_list_insert_by_date` list is read
1571/// from. The insertion sequence breaks ties, reversed so the *earlier*
1572/// insertion wins, because git's list puts an equal date after the ones already
1573/// there and a heap would otherwise settle ties by whatever the object ids
1574/// happen to compare as. That would make the order commits are emitted in, and
1575/// therefore the order of objects in the pack, depend on the hash of their
1576/// contents.
1577#[derive(Debug, PartialEq, Eq)]
1578struct Pending {
1579 date: i64,
1580 seq: std::cmp::Reverse<u64>,
1581 id: ObjectId,
1582}
1583
1584impl Ord for Pending {
1585 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1586 // `seq` is unique per entry, so this is a total order and never reports
1587 // `Equal` for two different commits.
1588 self.date
1589 .cmp(&other.date)
1590 .then_with(|| self.seq.cmp(&other.seq))
1591 }
1592}
1593
1594impl PartialOrd for Pending {
1595 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1596 Some(self.cmp(other))
1597 }
1598}
1599
1600/// The state of one limiting walk: the fields git keeps on `struct commit` and
1601/// `struct object`, which gunnar has nowhere to hang.
1602///
1603/// All three maps are bounded by the commits the walk **reads**, which is the
1604/// frontier between the two histories rather than either of them. They are
1605/// plain [`HashMap`]s rather than [`OidSet`]s for the reason `commit_closure`'s
1606/// `edges` set is: [`meter`] watches the repository-sized sets the one-set rule is
1607/// about, and metering a frontier-sized one would blur the number that matters.
1608#[derive(Default)]
1609struct Limiter {
1610 flags: HashMap<ObjectId, u8>,
1611 parsed: HashMap<ObjectId, CommitFacts>,
1612 queue: BinaryHeap<Pending>,
1613 seq: u64,
1614}
1615
1616impl Limiter {
1617 /// git's `repo_parse_commit_gently` followed by
1618 /// `commit_list_insert_by_date`: mark `id`, read it if this is the first
1619 /// time it has been named, and queue it.
1620 ///
1621 /// The mark is applied **before** the read and before the already-seen
1622 /// test, which is what lets a commit already queued as interesting be
1623 /// turned uninteresting by a later arrival from the client's side.
1624 fn admit(
1625 &mut self,
1626 graph: &Graph<'_>,
1627 id: ObjectId,
1628 uninteresting: bool,
1629 missing: Missing,
1630 buf: &mut Vec<u8>,
1631 ) -> Result<()> {
1632 {
1633 let flags = self.flags.entry(id).or_insert(0);
1634 if uninteresting {
1635 *flags |= flag::UNINTERESTING;
1636 }
1637 if *flags & (flag::SEEN | flag::ABSENT) != 0 {
1638 return Ok(());
1639 }
1640 }
1641 let facts = match graph.commit_facts(&id, buf) {
1642 Ok(facts) => facts,
1643 Err(Error::ObjectMissing { .. }) if missing == Missing::Skip => {
1644 *self.flags.entry(id).or_insert(0) |= flag::ABSENT;
1645 return Ok(());
1646 }
1647 Err(err) => return Err(err),
1648 };
1649 *self.flags.entry(id).or_insert(0) |= flag::SEEN;
1650 self.queue.push(Pending {
1651 date: facts.committed_at,
1652 seq: std::cmp::Reverse(self.seq),
1653 id,
1654 });
1655 self.seq += 1;
1656 self.parsed.insert(id, facts);
1657 Ok(())
1658 }
1659
1660 /// git's `mark_parents_uninteresting` (`revision.c`).
1661 ///
1662 /// **It reads nothing.** It descends only through commits this walk has
1663 /// already parsed and stops at the first one it has not, which is what
1664 /// keeps it bounded by the frontier rather than by the history behind it —
1665 /// and is why the walk it belongs to can be cheap at all. It exists for the
1666 /// case where a commit was queued as interesting, so its parents are in
1667 /// hand and marked interesting too, and only afterwards turns out to be
1668 /// reachable from a `have`.
1669 fn mark_parents_uninteresting(&mut self, start: &ObjectId, have_boundary: &HashSet<ObjectId>) {
1670 let Some(facts) = self.parsed.get(start) else {
1671 return;
1672 };
1673 let mut stack: Vec<ObjectId> = facts.parents.clone();
1674 while let Some(mut id) = stack.pop() {
1675 loop {
1676 let flags = self.flags.entry(id).or_insert(0);
1677 if *flags & flag::UNINTERESTING != 0 {
1678 break;
1679 }
1680 *flags |= flag::UNINTERESTING;
1681 if have_boundary.contains(&id) {
1682 break;
1683 }
1684 let Some(facts) = self.parsed.get(&id) else {
1685 break;
1686 };
1687 let Some((first, rest)) = facts.parents.split_first() else {
1688 break;
1689 };
1690 stack.extend_from_slice(rest);
1691 id = *first;
1692 }
1693 }
1694 }
1695
1696 fn is_uninteresting(&self, id: &ObjectId) -> bool {
1697 self.flags
1698 .get(id)
1699 .is_some_and(|flags| flags & flag::UNINTERESTING != 0)
1700 }
1701
1702 /// git's `everybody_uninteresting`, with git's `interesting_cache` memo.
1703 ///
1704 /// The scan is over the whole queue. Remembering the one interesting entry
1705 /// it found makes every later call O(1) until that entry is popped, which
1706 /// matters because the caller runs on every uninteresting commit.
1707 fn everybody_uninteresting(&self, cache: &mut Option<ObjectId>) -> bool {
1708 if let Some(id) = *cache {
1709 if !self.is_uninteresting(&id) {
1710 return false;
1711 }
1712 *cache = None;
1713 }
1714 for pending in &self.queue {
1715 if self.is_uninteresting(&pending.id) {
1716 continue;
1717 }
1718 *cache = Some(pending.id);
1719 return false;
1720 }
1721 true
1722 }
1723
1724 /// git's `still_interesting`. The slop left; zero means stop.
1725 ///
1726 /// `date` is the committer date of the last commit the walk kept.
1727 fn still_interesting(&self, date: i64, slop: usize, cache: &mut Option<ObjectId>) -> usize {
1728 let Some(head) = self.queue.peek() else {
1729 return 0;
1730 };
1731 // Something queued is at least as new as the last commit kept, so the
1732 // frontier is not behind the walk yet.
1733 if date <= head.date {
1734 return SLOP;
1735 }
1736 if !self.everybody_uninteresting(cache) {
1737 return SLOP;
1738 }
1739 slop.saturating_sub(1)
1740 }
1741}
1742
1743/// The result of choosing what goes in a pack.
1744#[derive(Debug, Default)]
1745pub(crate) struct Selection {
1746 /// Every object to send, tags first, then commits, then trees and blobs.
1747 pub objects: Vec<ObjectId>,
1748 /// The commits included, in traversal order.
1749 pub commits: Vec<ObjectId>,
1750 /// Every object this walk proved the **client already holds**. Nothing
1751 /// else.
1752 ///
1753 /// This is step 6's `seen` set with the objects this transfer carries taken
1754 /// back out again — see [`Carried`]. the rule that there is one
1755 /// repository-sized set and no second copy still holds: the set is moved
1756 /// out rather than cloned, and the subtraction shrinks it.
1757 ///
1758 /// # The paragraph that used to be here, and the object that refuted it
1759 ///
1760 /// It read: a base `write_pack_copy_thin` cannot re-point is *either*
1761 /// outside the transfer — and then the client holds it — *or* inside the
1762 /// transfer in a different source pack, "and then naming it is an
1763 /// `OBJ_REF_DELTA` against an object in the same pack, which
1764 /// `tests/ofs_delta_off.rs` already holds stock git to. Both are sound."
1765 ///
1766 /// Stock git is sound on it. **gitoxide is not**, and gitoxide is the
1767 /// receiver `gunnar-client` resolves a fetch with — and every other
1768 /// gitoxide-based client, and gunnar's own `receive-pack`.
1769 /// `gix_pack::data::input::LookupRefDeltaObjectsIter` reads every
1770 /// `OBJ_REF_DELTA` in a thin pack as naming an object the *receiver already
1771 /// has*: it looks in the local object database and in the bases it has
1772 /// already spliced in, and **nowhere else** — never inside the pack it is
1773 /// reading. A base that is in the transfer and has not been spliced in is
1774 /// `input::Error::NotFound`, and the fetch dies with
1775 /// `The object <base> could not be decoded or wasn't found`.
1776 ///
1777 /// Reproduced on `gunnar.year`, 2026-08-05, and then offline from the
1778 /// run's own directories: tree
1779 /// `1d062a8c1d97fd2d04222bb9a0d5c309fde57796` went into the pack as a delta
1780 /// on a tree the client held; the root tree of the *next* commit went in as
1781 /// a delta on `1d062a8c`; gitoxide refused the whole pack while
1782 /// `git index-pack --strict --fix-thin` completed the same bytes. The
1783 /// server holds `1d062a8c` **twice** — whole in the pack whose push
1784 /// introduced its dependant, and as a delta in the pack whose push
1785 /// introduced it — so `gix_pack`'s copier cannot re-point the second delta
1786 /// and names its base by object id. That double storage is what
1787 /// `index-pack --fix-thin` leaves behind on any repository pushed to more
1788 /// than once and not `gc`'d, which is exactly the repository with no
1789 /// `.bitmap`: this path and no other.
1790 ///
1791 /// So the rule is the narrow one. **A thin delta may name a base only if
1792 /// the receiver already holds it.** An object this transfer is carrying is
1793 /// not held until the pack has been resolved, and the receiver resolves
1794 /// thin bases *before* it resolves the pack.
1795 ///
1796 /// # What it is **not**
1797 ///
1798 /// It is not a promise about a shallow client. A `have` from one names a
1799 /// commit whose ancestry that client may not hold, and steps 2 and 5
1800 /// deliberately tolerate those gaps rather than fail the fetch. The caller
1801 /// is what must not offer this as a voucher in that case; see
1802 /// `upload_pack::send_pack`.
1803 ///
1804 /// # Two spellings, one question
1805 ///
1806 /// The walk hands over its exclusion closure. The bitmap path has no such
1807 /// set and must not build one — enumerating the client's history as object
1808 /// ids is the `O(client history)` cost the bitmap exists to remove — so it
1809 /// hands over the `have` bitmap and the positions that name it. Both answer
1810 /// [`ClientHas::contains`] in constant time, which is the only thing the
1811 /// one consumer asks, and since 2026-08-05 both answer the **same**
1812 /// question: does the receiver hold this already.
1813 pub client_has: ClientHas,
1814 /// Whether a `.bitmap` answered this, rather than the walk below it.
1815 ///
1816 /// # Why this is a field and not a log line
1817 ///
1818 /// Because a guard needs an instrument. The bitmap path declines request
1819 /// shapes it cannot answer exactly, and the list is long enough that
1820 /// getting one entry wrong turns the whole feature off **silently**: every
1821 /// test still passes, because the walk still answers and the walk is
1822 /// correct. That is exactly what happened on 2026-08-04 — `max_blob_bytes`
1823 /// is `Some` by default (`Limits::object_bytes`), the bitmap path declined
1824 /// on it, and four end-to-end cases against stock git went green while
1825 /// measuring the walk. A deliberately broken reader was the only thing that
1826 /// showed it, and only because it left the unit tests red and the
1827 /// end-to-end ones green.
1828 pub answered_from_bitmap: bool,
1829 /// How many commits the bitmap path had to **open**, because they had no
1830 /// stored bitmap of their own.
1831 ///
1832 /// The second thing about this path no other guard can see. ORing a stored
1833 /// bitmap in and then walking its parents anyway is *semantically
1834 /// identical* — a bitmap is closed under reachability, so the extra work
1835 /// changes no answer — and it turns a bounded walk into a full one. Proven
1836 /// 2026-08-04: with the termination deliberately left out, every one of the
1837 /// 9 set-equality tests and all 25 end-to-end cases against stock git
1838 /// stayed green. Nothing but a count can fail on it.
1839 ///
1840 /// Zero for a fetch whose every want already has a bitmap, which is every
1841 /// fetch against a freshly repacked repository.
1842 pub bitmap_commits_walked: usize,
1843}
1844
1845/// What the receiver is known **already to hold**, in whichever shape the
1846/// selection produced.
1847///
1848/// One question — *may a thin delta name this base?* — asked of the exclusion
1849/// closure the selection produced. It used to have a second arm carrying bits
1850/// over a git `.bitmap`'s positions; that path is gone with the format that
1851/// needed it, and reachability is answered by `GitServe::select` in object
1852/// ids. The answer is `true` only
1853/// for an object the receiver had **before** this transfer: see
1854/// [`Selection::client_has`] for the object that proved "and the ones this
1855/// transfer carries" is a different, wrong answer.
1856#[derive(Debug)]
1857pub(crate) enum ClientHas {
1858 /// The walk's exclusion closure, moved out rather than dropped.
1859 Ids(OidSet),
1860}
1861
1862impl Default for ClientHas {
1863 fn default() -> Self {
1864 ClientHas::Ids(OidSet::new())
1865 }
1866}
1867
1868impl ClientHas {
1869 /// **`contains` is gone, and it is not an oversight.**
1870 ///
1871 /// It answered *"may a thin delta name `id` as its base?"* on this side of
1872 /// the seam, for a pack writer that lived here. The pack writer is the
1873 /// engine's now, so the question is asked where the answer is used:
1874 /// `GitServe::emit_pack` takes the negotiated common **tips** and derives
1875 /// the voucher from its own reachability. Keeping a second, caller-side
1876 /// answer would be two authorities on which bases are safe, and the unsafe
1877 /// direction is a *failed* fetch rather than a larger one.
1878 ///
1879 /// **As of 2026-08-10 the znippy arm does not read `have` at all** — it
1880 /// closes every delta base into the pack rather than offering any as
1881 /// external — so the voucher is currently derived by nobody and costs bytes
1882 /// rather than correctness. That is recorded in that engine's source as an
1883 /// honest unused parameter, and it does not change the argument above:
1884 /// when the narrowing pass is written, `have` is where it reads from, and
1885 /// it must stay one authority rather than two.
1886 ///
1887 /// What survives is [`ClientHas::len`], because the server logs how much
1888 /// this transfer was allowed to point a delta at, and a regression in
1889 /// either selection path shows up as that number moving while
1890 /// `recompressed` climbs beside it.
1891 ///
1892 /// How many objects this vouches for.
1893 pub(crate) fn len(&self) -> usize {
1894 match self {
1895 ClientHas::Ids(ids) => ids.len(),
1896 }
1897 }
1898}
1899
1900/// Which of the client's commits step 5 opens up to find objects it already
1901/// holds.
1902///
1903/// # The measurement this exists for
1904///
1905/// Taken 2026-08-04, one fetch of 20 commits out of 1500 against the
1906/// `scale-deep-1500c-256k-20d` fixture (a 377.2 MiB single pack, one
1907/// incompressible 256 KiB blob per commit), load average 7.70:
1908///
1909/// ```text
1910/// commit reads tree reads pack faulted in
1911/// Full 1 504 3 000 377.23 MiB (99.96%)
1912/// stock git 2.53.0 — — 10.17 MiB ( 2.69%)
1913/// ```
1914///
1915/// The 0.78 MiB those 4 504 reads actually inflate is not the cost. `git
1916/// repack` writes a pack in *commit* order — each commit, then its trees, then
1917/// its blobs — so the 3 000 trees are spread evenly across all 377 MiB, 0.2
1918/// MiB of contiguous commits at the front and everything else interleaved with
1919/// a quarter-megabyte blob. Every one of those touches is an mmap fault, and
1920/// the desktop's `md1` carries `read_ahead_kb = 2048`, so each fault pulls in 2 MiB.
1921/// 3 000 touches over 189 two-megabyte windows hits every window: the whole
1922/// pack. The amplification is **readahead over a scattered walk**, not
1923/// decompression.
1924///
1925/// [`Exclusion::Full`] does what `git rev-list --objects --not` does with
1926/// `--objects-edge-aggressive`; [`Exclusion::Edges`] does what `pack-objects`
1927/// does by default.
1928#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
1929pub(crate) enum Exclusion {
1930 /// Walk the trees of **every** commit the client holds.
1931 ///
1932 /// Exact: an object the client has is never sent twice. Also
1933 /// `O(client history)` tree reads, which is the row above.
1934 ///
1935 /// Kept, and reachable, because it is the red switch this optimisation is
1936 /// checked against: `GUNNAR_EXCLUSION=full` restores it, and a test that
1937 /// cannot restore the old behaviour cannot show that the new one changed
1938 /// anything.
1939 Full,
1940 /// Walk the trees only of the client's own tips and of the **edge** — the
1941 /// commits it holds that sit directly behind a commit it is being sent.
1942 ///
1943 /// This is `mark_edges_uninteresting()` in git's `list-objects.c`, and it
1944 /// is what `git upload-pack` uses for every fetch unless
1945 /// `--objects-edge-aggressive` is asked for.
1946 ///
1947 /// **It is a superset, not an equality, and that is the trade.** An object
1948 /// reachable only from deep in the client's history and *also* reachable
1949 /// from a commit being sent is no longer recognised as already-held, so it
1950 /// goes in the pack again. Sending an object the client has is slow;
1951 /// sending one it lacks is a broken fetch, and nothing here can do that —
1952 /// the *commit* stop set is still the client's full closure, so no commit,
1953 /// tree or blob that belongs in the pack is ever dropped. Git made the same
1954 /// call for the same reason.
1955 ///
1956 /// On the fixture above the superset is empty: 76 objects either way.
1957 #[default]
1958 Edges,
1959}
1960
1961impl Exclusion {
1962 /// The serving default, with `GUNNAR_EXCLUSION=full` as the way back.
1963 ///
1964 /// Read every time rather than cached in a `OnceLock`, because a switch
1965 /// that only takes effect if it was set before the first fetch is a switch
1966 /// that reads green on a warm process and red on a cold one. It is one
1967 /// `getenv` per fetch against a walk that reads thousands of objects.
1968 ///
1969 /// Anything other than `full` is `Edges`, including a typo. That is
1970 /// deliberate: this is a rollback lever, and the failure mode of an
1971 /// unrecognised value must be *the shipping behaviour*, not a third one.
1972 pub(crate) fn from_env() -> Self {
1973 match std::env::var("GUNNAR_EXCLUSION").as_deref() {
1974 Ok("full") => Exclusion::Full,
1975 _ => Exclusion::Edges,
1976 }
1977 }
1978}
1979
1980/// How [`select_objects`] works out which commits to send.
1981///
1982/// # The measurement this exists for
1983///
1984/// `gunnar.fetch_readside` , 2026-08-04, one fetch of 20 commits out of
1985/// 1500 on `scale-deep-1500c-256k-20d`. [`Exclusion::Edges`] had just taken the
1986/// tree reads from 3 000 to 40, and this is what was left:
1987///
1988/// ```text
1989/// store reads 1 546
1990/// of which 1 506 commits — step 3, the client's whole history
1991/// 40 trees — the frontier
1992/// ```
1993///
1994/// 97.4% of every read a fetch still made was one walk enumerating history the
1995/// client had already told the server it holds, purely to build a stop set. On
1996/// a fixture with 1500 commits that is 1 506 reads; on a hundred-thousand-commit
1997/// repository it is a hundred thousand, for the same twenty commits of work.
1998#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
1999pub(crate) enum Limiting {
2000 /// Two walks: the client's whole commit history, then the commits to send
2001 /// against that history as a stop set.
2002 ///
2003 /// Exact, `O(client history)`, and **required** by [`Exclusion::Full`],
2004 /// which needs the root tree of every commit the client holds and can only
2005 /// get it from a walk that visited every one of them. Also the rollback
2006 /// lever: `GUNNAR_LIMIT=two-pass`.
2007 TwoPass,
2008 /// One walk with both sides in it, ordered by committer date, stopping when
2009 /// the frontier is behind it. git's `limit_list`, and
2010 /// [`Graph::limited_closure`].
2011 #[default]
2012 Merged,
2013}
2014
2015impl Limiting {
2016 /// The serving default, with `GUNNAR_LIMIT=two-pass` as the way back.
2017 ///
2018 /// Read every time, and any unrecognised value is the shipping behaviour,
2019 /// for the two reasons [`Exclusion::from_env`] gives.
2020 pub(crate) fn from_env() -> Self {
2021 match std::env::var("GUNNAR_LIMIT").as_deref() {
2022 Ok("two-pass") => Limiting::TwoPass,
2023 _ => Limiting::Merged,
2024 }
2025 }
2026}
2027
2028/// Whether the serve path may answer from a `.bitmap`.
2029///
2030/// The rollback lever, in the same shape as [`Exclusion`] and [`Limiting`]:
2031/// `GUNNAR_BITMAP=off` puts every request back on the walk, which is what every
2032/// request did before 2026-08-04. It is also how the tests get both answers out
2033/// of one server to compare, so the lever is exercised by the suite rather than
2034/// only by an operator having a bad day.
2035#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
2036pub(crate) enum Bitmaps {
2037 /// Read the `.bitmap` when the store has one and the request shape allows.
2038 #[default]
2039 Read,
2040 /// Walk, always.
2041 Off,
2042}
2043
2044impl Bitmaps {
2045 /// The serving default, with `GUNNAR_BITMAP=off` as the way back.
2046 ///
2047 /// Read every time, and any unrecognised value is the shipping behaviour,
2048 /// for the two reasons [`Exclusion::from_env`] gives.
2049 pub(crate) fn from_env() -> Self {
2050 match std::env::var("GUNNAR_BITMAP").as_deref() {
2051 Ok("off") => Bitmaps::Off,
2052 _ => Bitmaps::Read,
2053 }
2054 }
2055}
2056
2057/// Options for [`select_objects`].
2058#[derive(Debug, Default, Clone)]
2059pub(crate) struct SelectOptions<'f> {
2060 /// The client's shallow frontier: included, but not traversed past.
2061 pub boundary: HashSet<ObjectId>,
2062 /// Where the client's *own* history stops, bounding step 3's walk over the
2063 /// `excludes`.
2064 ///
2065 /// Equal to `boundary` for every request that is not a deepening, and
2066 /// deliberately not folded into it for the ones that are: a deepening
2067 /// walks past the client's cut on purpose, while the walk that decides
2068 /// what the client already holds never may. See
2069 /// [`crate::shallow::ShallowOutcome::client_boundary`].
2070 pub have_boundary: HashSet<ObjectId>,
2071 /// A partial-clone filter, if the client sent one.
2072 pub filter: Option<&'f Filter>,
2073 /// When set, the commit walk may only visit commits in this set, which is
2074 /// the answer a deepening request computed.
2075 pub restrict: Option<&'f HashSet<ObjectId>>,
2076 /// Whether a referenced-but-absent object is an error.
2077 pub missing: Missing,
2078 /// How much of the client's history step 5 opens up. See [`Exclusion`].
2079 pub exclusion: Exclusion,
2080 /// How much of the client's history steps 3 and 4 walk. See [`Limiting`].
2081 ///
2082 /// [`Exclusion::Full`] forces [`Limiting::TwoPass`] whatever this says,
2083 /// because the full exclusion needs the root tree of every commit the
2084 /// client holds and only the two-pass walk ever visits them. The two knobs
2085 /// are separate all the same: they name different halves of the same
2086 /// request, and folding them into one would make the exactness lever and
2087 /// the traversal lever impossible to roll back independently.
2088 pub limiting: Limiting,
2089 /// The ceiling on a single blob this walk may select — the fetch path's
2090 /// [`crate::Limits::object_bytes`].
2091 ///
2092 /// **`None` by default, and that is not the exception to "a guard that is
2093 /// off by default is not a guard".** This struct is crate-private and has
2094 /// three callers. Only one of them is about to *materialise* what it
2095 /// selects, and only that one may set the field; the exclusion walk and
2096 /// `WantPolicy`'s reachability closure decide membership and read no
2097 /// content, so a ceiling there would refuse fetches over an object nobody
2098 /// was going to hold. The default that is on lives where the server is
2099 /// configured — [`crate::upload_pack::Config::limits`].
2100 pub max_blob_bytes: Option<u64>,
2101 /// Whether this selection may be answered from a `.bitmap`. See
2102 /// [`Bitmaps`], and [`crate::bitmap::select`] for what it does and for the
2103 /// request shapes it declines.
2104 pub bitmaps: Bitmaps,
2105}
2106
2107impl SelectOptions<'_> {
2108 /// The options a **fetch is actually served with**, before the
2109 /// request-specific fields are filled in.
2110 ///
2111 /// One definition, used by `upload_pack::send_pack` and by the tests (one
2112 /// implementation). Not a convenience: a test that builds these by hand is
2113 /// a test of a configuration the server never runs in, and the ceiling
2114 /// below is the proof — `Limits::object_bytes` is `Some` by default,
2115 /// `..Default::default()` makes it `None`, and every bitmap test written
2116 /// that way was measuring
2117 /// the walk.
2118 pub(crate) fn for_serving(limits: &Limits) -> Self {
2119 SelectOptions {
2120 missing: Missing::Fail,
2121 exclusion: Exclusion::from_env(),
2122 limiting: Limiting::from_env(),
2123 bitmaps: Bitmaps::from_env(),
2124 max_blob_bytes: limits.object_bytes.map(|n| n as u64),
2125 ..Default::default()
2126 }
2127 }
2128
2129 /// The options a **mirror push** is built with.
2130 ///
2131 /// [`Self::for_serving`] with the client-facing blob ceiling left off, and
2132 /// for one reason: a mirror is not a client. `Limits::object_bytes` refuses
2133 /// to *hand out* an object bigger than an operator allows a fetch to pull;
2134 /// applying it here would mean a repository holding one large blob could
2135 /// never obtain a second copy at all, which is the opposite of what the
2136 /// ceiling is for.
2137 ///
2138 /// The three rollback levers — `GUNNAR_EXCLUSION`, `GUNNAR_LIMIT`,
2139 /// `GUNNAR_BITMAP` — are read exactly as the serve path reads them, so an
2140 /// operator putting the server back on the walk puts replication back on it
2141 /// too rather than leaving one of the two paths on a setting nobody chose.
2142 pub(crate) fn for_mirroring() -> Self {
2143 SelectOptions {
2144 missing: Missing::Fail,
2145 exclusion: Exclusion::from_env(),
2146 limiting: Limiting::from_env(),
2147 bitmaps: Bitmaps::from_env(),
2148 max_blob_bytes: None,
2149 ..Default::default()
2150 }
2151 }
2152}
2153
2154/// **The objects one push must carry**: everything reachable from `want` that
2155/// is not reachable from `have`, in this store's ids.
2156///
2157/// # Why replication comes through here rather than calling the store
2158///
2159/// 🔴 MEASURED on the reference appliance 2026-08-31. A resync of 25 repositories left
2160/// six failing, every one of them on an annotated tag, every one refused by the
2161/// standby's own `receive-pack` with
2162/// `missing necessary objects: <oid> is not in this repository (peeling a
2163/// tag)` — where `<oid>` is the commit the tag names and the primary holds.
2164///
2165/// The mirror built its object list from
2166/// [`GitOps::reachable`](gunnar_store::git_storage_trait::GitOps::reachable),
2167/// handing it the ref tips **unpeeled**. For an annotated tag that tip is the
2168/// tag *object*, and the shipped storage engine contributes a non-commit tip's
2169/// own id and stops: the tag went into the pack and the commit under it did
2170/// not. It only showed where the tagged commit was on no branch — `skade`'s
2171/// `v0.9.10`, `edda`'s twenty-five tags on `2c7f9919…` — because otherwise a
2172/// branch tip in the same push carried it by accident.
2173///
2174/// A clone of those same repositories has always worked, and this function is
2175/// why: [`select_objects`] peels its tips (step 1) and puts the tag objects it
2176/// walked through into the answer, and so does the store-answered fast path in
2177/// [`crate::reachable::select`]. Replication asking the store directly was a
2178/// second definition of *"what does this ref tip reach"*, and the two
2179/// definitions disagreed about exactly one edge. There is one now (one implementation).
2180///
2181/// `have` is tolerant by construction: a tip the far end advertised and this
2182/// store does not hold contributes nothing rather than failing, which is what a
2183/// standby whose history diverged looks like. A `want` this store does not hold
2184/// is an error, because that pack cannot be built.
2185///
2186/// # No object budget and no byte ceiling, deliberately
2187///
2188/// [`Graph::with_budget`] and [`Graph::with_walk_limit`] are what an operator
2189/// aims at a *client*: a fetch that would cost more than this machine will
2190/// spend is refused, and the client is told to ask for less. A mirror cannot
2191/// ask for less — there is no smaller request that still leaves the standby
2192/// holding the repository — so a ceiling here would not bound the work, it
2193/// would only make one repository permanently unreplicable. That is what
2194/// `GitOps::reachable` did before this function existed, over the same objects,
2195/// so this changes nothing about what the replicator may consume.
2196pub fn objects_to_send(
2197 store: &dyn GitServe,
2198 hash_kind: Kind,
2199 want: &[&[u8]],
2200 have: &[&[u8]],
2201) -> Result<Vec<Vec<u8>>> {
2202 let tips: Vec<ObjectId> = want.iter().map(|raw| oid(raw)).collect::<Result<_>>()?;
2203 let excludes: Vec<ObjectId> = have.iter().map(|raw| oid(raw)).collect::<Result<_>>()?;
2204 let graph = Graph::new(store, hash_kind);
2205 let selection = select_objects(&graph, &tips, &excludes, &SelectOptions::for_mirroring())?;
2206 Ok(selection
2207 .objects
2208 .into_iter()
2209 .map(|id| id.as_slice().to_vec())
2210 .collect())
2211}
2212
2213/// Everything reachable from `tips` that is not reachable from `excludes`.
2214///
2215/// This is `git rev-list --objects <tips> --not <excludes>`. Three callers use
2216/// it: to build a fetch's pack, to check that a push's new tips close, and,
2217/// with an empty `excludes`, to enumerate a whole repository for a clone.
2218pub(crate) fn select_objects(
2219 graph: &Graph<'_>,
2220 tips: &[ObjectId],
2221 excludes: &[ObjectId],
2222 options: &SelectOptions<'_>,
2223) -> Result<Selection> {
2224 // ── 0. the bitmap, when there is one and it can answer this ─────────────
2225 //
2226 // Before anything else, because everything after it is the walk this
2227 // replaces. It declines rather than approximates: a request shape the
2228 // bitmap cannot answer exactly — a filter, a shallow boundary, a
2229 // deepening, an object ceiling — falls through to the code below and is
2230 // served exactly as it was before this existed. See [`crate::bitmap`].
2231 if options.bitmaps == Bitmaps::Read {
2232 let _s = crate::stage::enter("select.bitmap");
2233 // ── the escape hatch moved, and it is still the whole design ────────
2234 //
2235 // This used to be two questions: `ObjectStore::reachable()`, an
2236 // `Option<Arc<dyn ReachIndex>>` answering *"do you have a reachability
2237 // index at all"*, and then `ReachIndex::select`, answering *"can you
2238 // answer THIS request exactly"*. `GitServe` has one method and it is
2239 // the second one — `select` is required, and `Ok(None)` is how a
2240 // backend declines. A backend with no index answers `Ok(None)` always.
2241 //
2242 // **`Ok(None)` must stay reachable.** The Arrow arm returns it when its
2243 // reachability projection is empty, which a clean restart produces
2244 // today; without it a backend whose index does not cover a tip would
2245 // contribute only the tip and the clone would be short by everything
2246 // beneath it — while exiting zero. Losing the option type is how that
2247 // becomes unrepresentable.
2248 if let Some(selection) = crate::reachable::select(graph, tips, excludes, options)? {
2249 // ── the budget applies to the fast path too ────────────────────
2250 //
2251 // It did not, and that made `--fetch-budget` a control that does
2252 // nothing on the shipped engine: the znippy arm answers `select`
2253 // out of its reachability index, so the walk below — the only place
2254 // `charge_walk` runs — never happens, and a budget of **1** served
2255 // a whole clone. `tests/wire_config.rs` saw it as *"a budget of 1
2256 // served a clone; the traversal budget never reached the walk"*.
2257 //
2258 // "Objects visited" is the walk's quantity; the index's is
2259 // "objects selected", and it is the honest analogue — it is what
2260 // the server is about to materialise, which is what the budget's
2261 // own doc says it stands in front of. So the ceiling binds on both
2262 // paths and means the same thing on both: **this fetch is bigger
2263 // than you allowed.**
2264 //
2265 // Deliberately `>` and not `>=`: a budget of N must serve a fetch
2266 // of exactly N objects. The walk's check is `>=` because it fires
2267 // *before* visiting the next object rather than after counting the
2268 // last one; both refuse the same N+1 request.
2269 if let Some(limit) = graph.budget {
2270 if selection.objects.len() > limit {
2271 return Err(Error::WalkBudget { limit });
2272 }
2273 }
2274 return Ok(selection);
2275 }
2276 }
2277
2278 let mut selection = Selection::default();
2279 let mut emitted = OidSet::new();
2280 let _peel = crate::stage::enter("select.peel");
2281
2282 // 1. Peel the tips. A `want` may name a tag, a tree, or even a blob.
2283 let mut commit_tips = Vec::new();
2284 let mut direct: Vec<(ObjectId, object::Kind)> = Vec::new();
2285 for tip in tips {
2286 let peeled = graph.peel(tip)?;
2287 for tag in peeled.tags {
2288 if emitted.insert(tag) {
2289 selection.objects.push(tag);
2290 }
2291 }
2292 match peeled.kind {
2293 object::Kind::Commit => commit_tips.push(peeled.id),
2294 other => direct.push((peeled.id, other)),
2295 }
2296 }
2297
2298 // 2. The same for the excludes, tolerating gaps: a `have` from a shallow
2299 // client can name a commit whose ancestry that client does not hold.
2300 let mut exclude_commits = Vec::new();
2301 for id in excludes {
2302 if !graph.contains(id)? {
2303 continue;
2304 }
2305 match graph.peel(id) {
2306 Ok(peeled) if peeled.kind == object::Kind::Commit => exclude_commits.push(peeled.id),
2307 Ok(_) => {}
2308 Err(Error::ObjectMissing { .. }) => {}
2309 Err(err) => return Err(err),
2310 }
2311 }
2312
2313 // 3 & 4. What the client already has, and what it is about to be sent.
2314 //
2315 // Two questions, and [`Limiting`] decides whether they take two walks or
2316 // one. Both run before step 5 rather than after it, which is the order
2317 // this used to be in, so that step 5 can *take* `excluded_commits`
2318 // instead of copying it. Nothing here reads the object closure, only the
2319 // commit graph, so the steps commute.
2320 //
2321 // `Exclusion::Full` needs the root tree of every commit the client
2322 // holds, and only the two-pass walk ever visits them, so it pins the
2323 // choice. `excluded_trees` is that list, and it is empty under the
2324 // merged walk because nothing there wants it.
2325 let limiting = match options.exclusion {
2326 Exclusion::Full => Limiting::TwoPass,
2327 Exclusion::Edges => options.limiting,
2328 };
2329 let mut excluded_trees: Vec<ObjectId> = Vec::new();
2330 crate::stage::end(_peel);
2331 let _walk = crate::stage::enter("select.commit_walk");
2332 let (commits, excluded_commits) = match limiting {
2333 Limiting::TwoPass => {
2334 let held = graph.commit_closure(
2335 &exclude_commits,
2336 &HashSet::new(),
2337 // Bounded by where the client said *its* history stops, not by
2338 // what this request decided to send. A shallow client offers
2339 // its grafted tip as a `have`; walking past that graft here
2340 // counts the whole repository as already-delivered and packs
2341 // nothing.
2342 &options.have_boundary,
2343 // The client's own history is not limited by what this request
2344 // decided to deepen to.
2345 None,
2346 Missing::Skip,
2347 )?;
2348 let excluded: OidSet = held.ids.iter().copied().collect();
2349 excluded_trees = held.trees;
2350 let commits = graph.commit_closure(
2351 &commit_tips,
2352 &excluded,
2353 &options.boundary,
2354 options.restrict,
2355 options.missing,
2356 )?;
2357 (commits, excluded)
2358 }
2359 Limiting::Merged => {
2360 let limited = graph.limited_closure(
2361 &commit_tips,
2362 &exclude_commits,
2363 &options.boundary,
2364 &options.have_boundary,
2365 options.restrict,
2366 options.missing,
2367 )?;
2368 // A frontier, where `TwoPass` puts the client's whole history here.
2369 // It seeds step 6's `seen`, where the only ids it can decide are
2370 // commits, and `tree_closure` never walks into one.
2371 let excluded: OidSet = limited.uninteresting.iter().copied().collect();
2372 let commits = CommitClosure {
2373 ids: limited.ids,
2374 trees: limited.trees,
2375 edges: limited.edges,
2376 };
2377 (commits, excluded)
2378 }
2379 };
2380 let CommitClosure {
2381 ids: commit_ids,
2382 trees: commit_trees,
2383 // Where the client's history meets what it is being sent. Free either
2384 // way: both walks already had to decide which parents the client holds.
2385 edges: commit_edges,
2386 } = commits;
2387 for commit in &commit_ids {
2388 if emitted.insert(*commit) {
2389 selection.objects.push(*commit);
2390 }
2391 }
2392 selection.commits = commit_ids;
2393 crate::stage::end(_walk);
2394
2395 // 5. ...and as a set of every object under those commits. This is the
2396 // expensive half, and the one the `git` package's reachability bitmaps replace.
2397 //
2398 // **One set, and only one**. It starts as the excluded commits,
2399 // moved rather than copied; the tree walk's `seen` set *is* this set, so
2400 // a visited id is recorded once; and the walk tallies what it found
2401 // rather than handing back a `Vec` of ids this function already holds.
2402 // All three of those used to be separate repository-sized allocations.
2403 // **Which** of the client's commits get opened up is [`Exclusion`], and
2404 // it is the difference between reading 3 000 trees and reading 42.
2405 let mut excluded_objects = excluded_commits;
2406 {
2407 let _s = crate::stage::enter("select.exclude_trees");
2408 let mut tally = Emit::Tally(0);
2409 let mut buf = Vec::new();
2410 let mut parents = Vec::new();
2411 // `Full` already has every root tree in hand — the walk read them off
2412 // the commits it decoded; see `CommitClosure`. `Edges` names a handful
2413 // of commits instead and pays one `commit_links` each, which is a read
2414 // of an object sitting in the contiguous commit run at the front of the
2415 // pack, not another scattered fault in the middle of it.
2416 let roots: Vec<ObjectId> = match options.exclusion {
2417 Exclusion::Full => excluded_trees,
2418 Exclusion::Edges => {
2419 // The client's own tips, then the edge. Git's
2420 // `mark_edges_uninteresting` walks both: the tips because they
2421 // are what the client actually said it holds, the edge because
2422 // that is where the two histories meet and where the shared
2423 // content is.
2424 let mut roots = Vec::with_capacity(exclude_commits.len() + commit_edges.len());
2425 for id in exclude_commits.iter().chain(commit_edges.iter()) {
2426 match graph.commit_links(id, &mut buf, &mut parents) {
2427 Ok(tree) => roots.push(tree),
2428 // The same tolerance step 2 already applies to a
2429 // `have`: a shallow client can name a commit whose
2430 // ancestry it does not hold, and refusing the fetch
2431 // over it would be a regression on a path that used to
2432 // skip it silently.
2433 Err(Error::ObjectMissing { .. }) => {}
2434 Err(err) => return Err(err),
2435 }
2436 }
2437 roots
2438 }
2439 };
2440 for tree in &roots {
2441 graph.tree_closure(
2442 *tree,
2443 &mut excluded_objects,
2444 &HashSet::new(),
2445 None,
2446 Missing::Skip,
2447 // The client already has these; gunnar is not about to hold one.
2448 None,
2449 &mut tally,
2450 )?;
2451 }
2452 }
2453
2454 // 6. Their trees and blobs, plus anything a want pointed at directly.
2455 //
2456 // The exclusion set is handed straight over to be step 6's `seen` set.
2457 // It used to be cloned here, and that clone was the defect. Merging the two
2458 // is the same predicate written once, not a shortcut: `tree_closure`
2459 // skips an id that is in `exclude` **or** already in `seen`, so a `seen`
2460 // that starts out holding everything `exclude` held decides every id
2461 // identically. The `exclude` argument is left for `verify_connectivity`,
2462 // the one caller that must keep the two apart.
2463 let _tree = crate::stage::enter("select.tree_closure");
2464 let mut seen = excluded_objects;
2465 // Every id this step puts into `seen`, so the two questions that set is
2466 // answering can be separated again at the end. See [`Carried`], and
2467 // `Selection::client_has` for what goes wrong when they are not.
2468 let mut carried: Vec<ObjectId> = Vec::with_capacity(emitted.len());
2469 let mut payload = Vec::new();
2470 let mut keep = Emit::Keep(&mut payload);
2471 {
2472 let mut visited = Carried {
2473 seen: &mut seen,
2474 carried: &mut carried,
2475 };
2476 for id in emitted.iter() {
2477 visited.insert(id);
2478 }
2479 for tree in &commit_trees {
2480 graph.tree_closure(
2481 *tree,
2482 &mut visited,
2483 &HashSet::new(),
2484 options.filter,
2485 options.missing,
2486 options.max_blob_bytes,
2487 &mut keep,
2488 )?;
2489 }
2490 for (id, kind) in direct {
2491 match kind {
2492 object::Kind::Tree => graph.tree_closure(
2493 id,
2494 &mut visited,
2495 &HashSet::new(),
2496 options.filter,
2497 options.missing,
2498 options.max_blob_bytes,
2499 &mut keep,
2500 )?,
2501 // A `want` naming a blob outright — a partial clone's lazy fetch
2502 // does exactly this. It never reaches `tree_closure`, so the
2503 // ceiling has to be applied here as well or the one shape that
2504 // asks for a single enormous object by name is the one shape that
2505 // is not checked.
2506 _ => {
2507 if let (Some(limit), object::Kind::Blob) = (options.max_blob_bytes, kind) {
2508 if let Some((_kind, size)) =
2509 graph.store.header(id.as_bytes()).map_err(backend)?
2510 {
2511 if size > limit {
2512 return Err(Error::ObjectTooLarge {
2513 oid: id,
2514 size,
2515 limit,
2516 });
2517 }
2518 }
2519 }
2520 if visited.insert(&id) {
2521 keep.push(id);
2522 }
2523 }
2524 }
2525 }
2526 }
2527 selection.objects.extend(payload);
2528 // ── the voucher is what the client HOLDS ────────────────────────────────
2529 //
2530 // `seen` is now the client's closure **plus** everything this transfer
2531 // visited, and only the first half may vouch for a thin delta's base. So
2532 // the second half goes back out. It is `O(objects in this transfer)` and
2533 // frees memory rather than taking any, which is why this is a subtraction
2534 // rather than the snapshot-before-step-6 that the one-set rule forbids.
2535 //
2536 // `carried` is the exact list, not `selection.objects`: a walk under a
2537 // partial-clone filter *visits* trees and blobs it then declines to send,
2538 // and those are neither held nor carried. Using the sent list would leave
2539 // them vouched for.
2540 for id in &carried {
2541 seen.remove(id);
2542 }
2543 selection.client_has = ClientHas::Ids(seen);
2544 crate::stage::end(_tree);
2545
2546 Ok(selection)
2547}
2548
2549/// Every object reachable from `tips`, as **one** set and no vector.
2550///
2551/// This answers `upload-pack`'s [`WantPolicy::ReachableFromRefs`] question: *is
2552/// this `want` reachable from some reference?* It used to be answered by
2553/// `select_objects(graph, tips, &[], …).objects.into_iter().collect()`, and
2554/// that phrasing holds the whole repository **twice** at its peak — a `Vec` of
2555/// every id, alive while the `HashSet` built out of it fills up. At ten million
2556/// sha256 ids that is a 320 MB vector plus a ~500 MB set, ~820 MB of peak for a
2557/// question that never wanted the order and never wanted a second copy. It is
2558/// The same shape, one caller further out.
2559///
2560/// Here the tree walk's `seen` set **is** the answer: it tallies rather than
2561/// emits, so nothing is built beside it. The set that comes back is the only
2562/// repository-sized allocation the call ever makes.
2563///
2564/// A tip that does not resolve is **skipped**, not fatal. A dangling reference
2565/// is a problem for `gc`; refusing every non-tip `want` in the repository
2566/// because one reference is broken is not this function's call to make — the
2567/// same judgement `Frontier::new` already makes about the same values.
2568///
2569/// [`WantPolicy::ReachableFromRefs`]: crate::WantPolicy::ReachableFromRefs
2570pub(crate) fn reachable_closure(graph: &Graph<'_>, tips: &[ObjectId]) -> Result<OidSet> {
2571 let mut seen = OidSet::new();
2572 let mut commit_tips = Vec::new();
2573 let mut direct_trees = Vec::new();
2574
2575 for tip in tips {
2576 if !graph.contains(tip)? {
2577 continue;
2578 }
2579 let peeled = match graph.peel(tip) {
2580 Ok(peeled) => peeled,
2581 Err(Error::ObjectMissing { .. }) => continue,
2582 Err(err) => return Err(err),
2583 };
2584 for tag in peeled.tags {
2585 seen.insert(tag);
2586 }
2587 match peeled.kind {
2588 object::Kind::Commit => commit_tips.push(peeled.id),
2589 object::Kind::Tree => direct_trees.push(peeled.id),
2590 _ => {
2591 seen.insert(peeled.id);
2592 }
2593 }
2594 }
2595
2596 let commits = graph.commit_closure(
2597 &commit_tips,
2598 &HashSet::new(),
2599 &HashSet::new(),
2600 None,
2601 // A repository with a gap in it is a problem for `gc`, not a reason to
2602 // refuse every fetch.
2603 Missing::Skip,
2604 )?;
2605 let mut tally = Emit::Tally(0);
2606 for (commit, tree) in commits.ids.iter().zip(&commits.trees) {
2607 seen.insert(*commit);
2608 graph.tree_closure(
2609 *tree,
2610 &mut seen,
2611 &HashSet::new(),
2612 None,
2613 Missing::Skip,
2614 None,
2615 &mut tally,
2616 )?;
2617 }
2618 for tree in direct_trees {
2619 graph.tree_closure(
2620 tree,
2621 &mut seen,
2622 &HashSet::new(),
2623 None,
2624 Missing::Skip,
2625 None,
2626 &mut tally,
2627 )?;
2628 }
2629 Ok(seen)
2630}
2631
2632/// What the repository already vouched for, before a push.
2633///
2634/// Built **once per push**, not once per reference command: a client pushing a
2635/// thousand refs in one go used to rebuild this a thousand times.
2636pub(crate) struct Frontier {
2637 /// Reference values that peel to a commit. The connectivity walk stops
2638 /// here: a commit a reference already points at passed this same check
2639 /// before that reference was allowed to move, so its closure is complete by
2640 /// induction.
2641 commits: HashSet<ObjectId>,
2642 /// Root trees of those commits, plus any reference value that peels to a
2643 /// tree or a blob. Anything in here is present and complete, so the tree
2644 /// walk does not descend into it.
2645 present: HashSet<ObjectId>,
2646}
2647
2648impl Frontier {
2649 /// `values` is every reference target the repository held before the push.
2650 ///
2651 /// Absent and unreadable values are skipped rather than fatal: a repository
2652 /// with a dangling reference is broken, but refusing every push until
2653 /// someone fixes it is not this function's call to make.
2654 pub(crate) fn new(graph: &Graph<'_>, values: &[ObjectId]) -> Result<Self> {
2655 let mut commits = HashSet::new();
2656 let mut present = HashSet::new();
2657 for id in values {
2658 if !graph.contains(id)? {
2659 continue;
2660 }
2661 match graph.peel(id) {
2662 Ok(peeled) if peeled.kind == object::Kind::Commit => {
2663 commits.insert(peeled.id);
2664 }
2665 Ok(peeled) => {
2666 present.insert(peeled.id);
2667 }
2668 Err(Error::ObjectMissing { .. }) => {}
2669 Err(err) => return Err(err),
2670 }
2671 }
2672 for commit in &commits {
2673 present.insert(graph.commit(commit)?.tree);
2674 }
2675 Ok(Frontier { commits, present })
2676 }
2677}
2678
2679/// Assert that every object reachable from `tips` is present, stopping at the
2680/// frontier.
2681///
2682/// This is receive-pack's connectivity check, and it walks **from the new tips
2683/// down**. It never computes the frontier's object closure, which is the whole
2684/// point: the frontier is every reference the repository already had, so its
2685/// closure is the entire repository, and paying for it once per pushed
2686/// reference made push latency a function of repository size.
2687///
2688/// # What this actually costs (the previous claim here was wrong)
2689///
2690/// The commit walk stops at any commit a reference already pointed at, so the
2691/// ordinary case — a push onto an existing branch — visits exactly the commits
2692/// being pushed. Their trees and blobs then get walked, with the frontier's
2693/// root trees excluded and every subtree visited at most once across the whole
2694/// push. So the honest bound is:
2695///
2696/// > `O(new commits + the tree closure of those commits)`
2697///
2698/// which is one snapshot's worth of trees, not the repository's history. It is
2699/// **not** `O(newly pushed history)` alone, and this is the part the old
2700/// comment claimed and never had:
2701///
2702/// * a blob is existence-checked, not inflated, so the tree side is cheap per
2703/// object but is still proportional to the number of paths in the snapshot;
2704/// * a push that bases a new branch on an old commit that is not itself a
2705/// reference value has nothing to stop the commit walk before the root, so it
2706/// degenerates to `O(history below the base)`.
2707///
2708/// Both remainders need reachability data gunnar does not have: the `git`
2709/// package's
2710/// `__gunnar_reach__` bitmaps, or the set of object ids the incoming pack
2711/// actually introduced, either of which turns "is this already here and
2712/// complete" into a lookup. Until one exists, this comment states the cost the
2713/// code has rather than the cost the design wants.
2714pub(crate) fn verify_connectivity(
2715 graph: &Graph<'_>,
2716 tips: &[ObjectId],
2717 frontier: &Frontier,
2718) -> Result<usize> {
2719 // 1. Peel the new tips. `peel` loads every object it walks through, so a
2720 // tag chain is proven present by getting this far.
2721 //
2722 // `tags` is bounded by `tips.len() * MAX_TAG_DEPTH` — by what the client
2723 // sent, not by the repository — so it is a plain `HashSet` for the same
2724 // reason `commit_closure`'s `edges` is. It is the only set here that
2725 // exists purely for the returned count.
2726 let mut tags: HashSet<ObjectId> = HashSet::new();
2727 let mut commit_tips = Vec::new();
2728 let mut direct: Vec<(ObjectId, object::Kind)> = Vec::new();
2729 for tip in tips {
2730 let peeled = graph.peel(tip)?;
2731 tags.extend(peeled.tags);
2732 match peeled.kind {
2733 object::Kind::Commit => commit_tips.push(peeled.id),
2734 other => direct.push((peeled.id, other)),
2735 }
2736 }
2737
2738 // 2. The commits being pushed. `Missing::Fail` is what makes this a check:
2739 // a commit named by a parent and absent from the store stops the push.
2740 let commits = graph.commit_closure(
2741 &commit_tips,
2742 &frontier.commits,
2743 &HashSet::new(),
2744 None,
2745 Missing::Fail,
2746 )?;
2747
2748 // 3. Their trees and blobs. One `seen` set for the whole push, so a subtree
2749 // shared by ten pushed commits is walked once.
2750 //
2751 // **`Tally`, not `Keep`.** The only caller — `receive_pack::apply` —
2752 // throws the returned count's *identity* away and logs the number, so
2753 // materialising a `Vec<ObjectId>` of every object in the push bought
2754 // nothing and cost a second repository-sized allocation beside `seen`
2755 // (the one-set rule, on the push side). There used to be a third: a `counted`
2756 // `OidSet` deduplicating the three groups against each other, which
2757 // cannot fire — a tag, a commit and a tree are different objects with
2758 // different ids, `commit_closure` deduplicates its own output, and
2759 // `seen` is shared across every `tree_closure` call below, so each group
2760 // is already distinct and the groups are disjoint by kind. Three
2761 // repository-sized structures on this path are now one.
2762 let mut seen = OidSet::new();
2763 let mut keep = Emit::Tally(0);
2764 for tree in &commits.trees {
2765 graph.tree_closure(
2766 *tree,
2767 &mut seen,
2768 &frontier.present,
2769 None,
2770 Missing::Fail,
2771 // A push is checked for completeness, not packed. Nothing here is
2772 // materialised, so the serve path's object ceiling does not apply —
2773 // the *receive* path's ceiling is `gunnar_pack::IndexOptions`, and
2774 // it has already run by the time this does.
2775 None,
2776 &mut keep,
2777 )?;
2778 }
2779 for (id, kind) in direct {
2780 match kind {
2781 object::Kind::Tree => graph.tree_closure(
2782 id,
2783 &mut seen,
2784 &frontier.present,
2785 None,
2786 Missing::Fail,
2787 None,
2788 &mut keep,
2789 )?,
2790 // A reference pointing straight at a blob. `peel` already read it.
2791 _ => {
2792 if seen.insert(id) {
2793 keep.push(id);
2794 }
2795 }
2796 }
2797 }
2798
2799 Ok(tags.len() + commits.ids.len() + keep.len())
2800}
2801
2802/// Memoised "does this commit have an ancestor the client said it has".
2803///
2804/// This is git's `can_all_from_reach_with_flag`, which is what decides whether
2805/// the server may stop negotiating. Memoising across wants is what keeps it
2806/// `O(V+E)` for the whole request instead of `O(V+E)` per want. On a
2807/// merge-heavy repository that is milliseconds against minutes.
2808pub(crate) struct Reachability<'g, 'a> {
2809 graph: &'g Graph<'a>,
2810 common: OidSet,
2811 known: HashMap<ObjectId, bool>,
2812 cutoff: i64,
2813 visited: usize,
2814 budget: Option<usize>,
2815}
2816
2817impl<'g, 'a> Reachability<'g, 'a> {
2818 pub(crate) fn new(graph: &'g Graph<'a>, common: &[ObjectId], cutoff: i64) -> Self {
2819 Reachability {
2820 graph,
2821 common: common.iter().copied().collect(),
2822 known: HashMap::new(),
2823 cutoff,
2824 visited: 0,
2825 budget: graph.budget,
2826 }
2827 }
2828
2829 /// Can `start` reach any commit the client claimed?
2830 ///
2831 /// Commits older than the cutoff are answered `false` without being walked
2832 /// through: the client's oldest `have` bounds how far back a useful cut
2833 /// point can be, and without that bound a divergent history walks to the
2834 /// root of the repository on every round.
2835 ///
2836 /// # One read per commit, and why the obvious shape is two
2837 ///
2838 /// This used to be an iterative **post-order**: a commit was left on the
2839 /// stack under its unresolved parents and looked at again once they were
2840 /// known. That is a correct way to fold a DAG, and it reads every commit on
2841 /// the path **twice**: once going down, once on the way back up, because
2842 /// the second look has to re-parse the commit to get at its parents again.
2843 /// On a divergent negotiation that doubling was a third of the entire
2844 /// suite's most expensive arm.
2845 ///
2846 /// It is not needed, because the fold being computed is an *or*. A plain
2847 /// downward walk answers `true` the instant it touches a common commit, and
2848 /// if it exhausts without touching one then **every commit it visited** is
2849 /// proven not to reach a common. One sweep, one read each, and a stronger
2850 /// memo than the post-order produced. The `true` case only memoises `start`
2851 /// itself: the walk stops early, so nothing else on the frontier has been
2852 /// settled and claiming otherwise would be a lie the next `want` acts on.
2853 ///
2854 /// One consequence worth stating rather than discovering: `visited` now
2855 /// counts each commit **once**, where the post-order counted the ones it
2856 /// read twice twice. So the budget is reached after roughly twice as many
2857 /// commits as before. That is the count being *correct* rather than the
2858 /// ceiling being raised; the budget bounds objects visited, and it was
2859 /// previously charging some of them double. It does mean the effective
2860 /// ceiling on a divergent walk moved, so judge `DEFAULT_BUDGET` against the
2861 /// honest number.
2862 pub(crate) fn reaches_common(&mut self, start: ObjectId) -> Result<bool> {
2863 if self.common.is_empty() {
2864 return Ok(false);
2865 }
2866 if let Some(&answer) = self.known.get(&start) {
2867 return Ok(answer);
2868 }
2869
2870 // Breadth-first rather than depth-first so that a common commit a few
2871 // steps away is found in a few steps, whatever the depth of the branch
2872 // it happens to hang off.
2873 let mut queue: VecDeque<ObjectId> = VecDeque::new();
2874 let mut queued = OidSet::new();
2875 // Visited, and unless the walk stops early, proven `false`.
2876 let mut exhausted: Vec<ObjectId> = Vec::new();
2877 queue.push_back(start);
2878 queued.insert(start);
2879
2880 let mut found = false;
2881 while let Some(id) = queue.pop_front() {
2882 if self.common.contains(&id) {
2883 found = true;
2884 break;
2885 }
2886 match self.known.get(&id) {
2887 Some(true) => {
2888 found = true;
2889 break;
2890 }
2891 // Already proven not to reach a common: its ancestry adds
2892 // nothing, and re-walking it is what the memo exists to stop.
2893 Some(false) => continue,
2894 None => {}
2895 }
2896 self.visited += 1;
2897 if let Some(limit) = self.budget {
2898 if self.visited > limit {
2899 return Err(Error::WalkBudget { limit });
2900 }
2901 }
2902 // `visited` is cumulative and `queued` is live, so they are two
2903 // different quantities and both need saying. This one is the shared
2904 // charge every other walk answers to, which is what puts a
2905 // negotiation's `queued` set under the same bytes ceiling as a
2906 // clone's `seen` set instead of under none.
2907 self.graph.charge_walk()?;
2908 if !self.graph.contains(&id)? {
2909 self.known.insert(id, false);
2910 continue;
2911 }
2912 let facts = self.graph.commit(&id)?;
2913 if facts.committed_at < self.cutoff {
2914 self.known.insert(id, false);
2915 continue;
2916 }
2917 exhausted.push(id);
2918 for parent in facts.parents {
2919 if queued.insert(parent) {
2920 queue.push_back(parent);
2921 }
2922 }
2923 }
2924
2925 if found {
2926 self.known.insert(start, true);
2927 } else {
2928 // The queue drained, so every commit on this frontier had its whole
2929 // in-cutoff ancestry looked at and none of it was common.
2930 for id in exhausted {
2931 self.known.insert(id, false);
2932 }
2933 }
2934 Ok(found)
2935 }
2936}
2937
2938#[cfg(test)]
2939mod tests {
2940
2941 /// [`SelectOptions`] with the **bitmap short-circuit off**.
2942 ///
2943 /// # Why every walk test in this module needs it, as of 2026-08-10
2944 ///
2945 /// `SelectOptions::default()` leaves [`Bitmaps::Read`] on, and step 0 then
2946 /// asks [`GitServe::select`] before any walk happens. The old fixture store
2947 /// could not answer — `ObjectStore::reachable()` handed back `None`,
2948 /// because `MemObjectStore` had no reachability index — so the question was
2949 /// free and every test below fell through to the walk it was written about.
2950 ///
2951 /// [`MemGitServe::select`] **does** answer: it is a genuine reachability
2952 /// walk over an in-memory store, and it declines only for an empty store or
2953 /// an unknown `want`. So with the default options these tests measure the
2954 /// short-circuit and not the traversal — seen exactly that way when they
2955 /// first compiled again: *"the two-pass walk touched the store 3 times over
2956 /// a client history of 300 commits"*, and a fetch whose peak was 0 ids.
2957 ///
2958 /// `Bitmaps::Off` is the documented *"walk, always"*, the same switch
2959 /// `GUNNAR_BITMAP=off` sets, so this forces the subject of the test rather
2960 /// than working around anything. What step 0 itself does is
2961 /// `crate::reachable`'s to assert, and it does.
2962 fn walked() -> SelectOptions<'static> {
2963 SelectOptions {
2964 bitmaps: Bitmaps::Off,
2965 ..SelectOptions::default()
2966 }
2967 }
2968 use super::*;
2969 use gix_hash::Kind;
2970
2971 // The byte-level suites' fixture builder, reused rather than re-written
2972 // (one implementation). Declared in `lib.rs`; see the `#[path]` there.
2973 use crate::common::Fixture;
2974
2975 /// A fetch holds the client's object closure **once**.
2976 ///
2977 /// # What is measured, and why it is not a wall clock or an RSS reading
2978 ///
2979 /// The bug this guards is invisible to every other kind of assertion. It
2980 /// changes no byte on the wire, no object in the pack and no exit status;
2981 /// it only makes the server hold a second copy of a repository-sized set of
2982 /// object ids. So the instrument is a count of the ids the walkers hold at
2983 /// once ([`meter`]), which is the applied output of the thing that went
2984 /// wrong. Peak resident set is the same fact one layer down, but a
2985 /// `MemObjectStore` fixture *is* memory, with the objects outweighing the
2986 /// sets about four to one, so RSS on a fixture this size could not resolve
2987 /// the difference. That measurement belongs on a real repository, and it
2988 /// lives in the `bench/` harness.
2989 ///
2990 /// # The two bounds, and what each one catches
2991 ///
2992 /// **The ceiling** is `one closure + a scrap`. Before the fix the peak was
2993 /// the closure **twice** — `let mut seen = excluded_objects.clone();` —
2994 /// plus a `Vec` of the same ids and a separate `seen` set inside the
2995 /// exclusion walk. Seen RED: with the clone restored this asserts
2996 /// 2.0 × closure against a bound of 1.02 ×.
2997 ///
2998 /// **The floor** is what stops the guard being hollow, and it is not
2999 /// decoration: a `select_objects` that never built the exclusion closure at
3000 /// all would hold almost nothing, pass any ceiling, and be a far worse bug,
3001 /// because it would send the client the whole repository on every
3002 /// incremental fetch. `peak >= closure` says the closure really was built.
3003 /// The selection assertion below is the same claim from the other side.
3004 #[test]
3005 fn a_fetch_holds_the_clients_object_closure_once() {
3006 // 300 commits, each with its own tree and its own blob, so the closure
3007 // is ~900 ids. Far enough above the handful of scrap ids that the two
3008 // bounds cannot overlap.
3009 const HISTORY: usize = 300;
3010
3011 let fixture = Fixture::build(Kind::Sha1);
3012 let client_tip = fixture.extend(fixture.four, HISTORY, "client");
3013 let server_tip = fixture.extend(client_tip, 1, "server");
3014
3015 let graph = Graph::new(&fixture.git, fixture.kind);
3016 // `Full`, deliberately and explicitly: this test's subject is the
3017 // *whole* client closure being held exactly once, and under the
3018 // shipping [`Exclusion::Edges`] there is no whole client closure to
3019 // hold. `Edges` gets its own half at the bottom, and the two together
3020 // say more than either did alone.
3021 let options = SelectOptions {
3022 exclusion: Exclusion::Full,
3023 ..walked()
3024 };
3025
3026 // How big one copy of the client's closure is, measured by the same
3027 // walker rather than predicted: a clone with no excludes selects
3028 // exactly the set an incremental fetch has to exclude.
3029 let closure = select_objects(&graph, &[client_tip], &[], &options)
3030 .expect("the client's own closure")
3031 .objects
3032 .len();
3033 assert!(
3034 closure > 3 * HISTORY,
3035 "the fixture is not the shape this test needs: {HISTORY} commits \
3036 should close over more than {} objects, not {closure}",
3037 3 * HISTORY
3038 );
3039
3040 meter::reset_peak();
3041 assert_eq!(
3042 meter::live(),
3043 0,
3044 "a set from the sizing call above outlived it"
3045 );
3046 let selection = select_objects(&graph, &[server_tip], &[client_tip], &options)
3047 .expect("the incremental fetch");
3048 let peak = meter::peak();
3049
3050 // One new commit, its tree and its blob. Nothing the client already
3051 // had. Without this the memory bounds could be satisfied by a walk that
3052 // excluded far too much.
3053 assert_eq!(
3054 selection.objects.len(),
3055 3,
3056 "an incremental fetch one commit behind selected {} objects",
3057 selection.objects.len()
3058 );
3059
3060 assert!(
3061 peak >= closure,
3062 "the walk peaked at {peak} live object ids against a client closure \
3063 of {closure}: the exclusion set was never built, so this fetch is \
3064 about to send objects the client already has"
3065 );
3066 let ceiling = closure + 4 * selection.objects.len() + 16;
3067 assert!(
3068 peak <= ceiling,
3069 "the walk held {peak} object ids at once for a client closure of \
3070 {closure}, {:.2}× one copy, ceiling {ceiling}. The exclusion \
3071 closure is being materialised more than once; at ten \
3072 million objects each extra copy is ~450 MB per in-flight fetch",
3073 peak as f64 / closure as f64
3074 );
3075
3076 // ── the closure is now HANDED OUT, and that has a lifetime cost ──────
3077 //
3078 // The thin voucher needs the client's closure after the walk, to vouch for the
3079 // base of a thin delta, so `Selection::client_has` carries it to the
3080 // pack writer instead of dropping it. The peak asserted above is
3081 // unchanged — it is the same set moved, not a second copy — but it now
3082 // lives as long as the `Selection` does, which means it coexists with
3083 // the writer's own tables rather than dying before them. That is a
3084 // real entry in `limits.rs`'s ledger and it is asserted here rather
3085 // than described.
3086 let full_selected = selection.objects.len();
3087 let vouched = selection.client_has.len();
3088 assert!(
3089 vouched >= closure,
3090 "the walk handed back {vouched} ids to vouch with against a client \
3091 closure of {closure}: the set being carried is not the closure, so \
3092 a thin delta's base would be refused for want of evidence that was \
3093 computed and thrown away"
3094 );
3095 assert_eq!(
3096 meter::live(),
3097 vouched,
3098 "the ids alive after the walk are not exactly the set it handed \
3099 back, so something else is being retained too"
3100 );
3101 drop(selection);
3102 assert_eq!(
3103 meter::live(),
3104 0,
3105 "the client's closure outlived the Selection that owns it"
3106 );
3107
3108 // And now the shipping mode. `Edges` opens up the client's tips and
3109 // the edge and nothing else, so the set it holds is bounded by the
3110 // frontier rather than by the client's history — which is the entire
3111 // read-side claim of the serial walk, asserted here as *live ids*
3112 // rather than as a timing.
3113 meter::reset_peak();
3114 let edge_selection = select_objects(&graph, &[server_tip], &[client_tip], &walked())
3115 .expect("the incremental fetch, edge exclusion");
3116 let edge_peak = meter::peak();
3117
3118 // The pack must not have grown. `Edges` is allowed to send a *superset*
3119 // and this fixture is the shape where the superset is empty; if that
3120 // ever stops being true here, the trade has moved and this is where it
3121 // shows up rather than in a byte count nobody reads.
3122 assert_eq!(
3123 edge_selection.objects.len(),
3124 full_selected,
3125 "edge exclusion selected {} objects where the full closure selected \
3126 {full_selected}: the frontier walk is missing objects the client \
3127 demonstrably has",
3128 edge_selection.objects.len()
3129 );
3130 // And the set carried to the writer shrinks with it. This is what
3131 // bounds the thin voucher's new lifetime cost in the SHIPPING
3132 // configuration: the voucher is frontier-sized, not history-sized, so
3133 // a fetch does not hold the client's whole history alive while it
3134 // writes the pack.
3135 assert!(
3136 edge_selection.client_has.len() < vouched,
3137 "the voucher carried under edge exclusion is {} ids against the full \
3138 closure's {vouched}, so the frontier walk is handing the pack writer \
3139 the client's whole history after all",
3140 edge_selection.client_has.len()
3141 );
3142 // Seen RED by making `Exclusion::from_env`'s default `Full` and
3143 // `SelectOptions::default()` follow it: `edge_peak` then equals `peak`
3144 // and this fires with "held 915 ids ... against the full closure's 915".
3145 assert!(
3146 edge_peak < peak,
3147 "edge exclusion held {edge_peak} ids at once against the full \
3148 closure's {peak} over a client history of {HISTORY} commits. It is \
3149 walking the client's whole history after all, and the read-side \
3150 win is not there"
3151 );
3152 }
3153
3154 /// The traversal budget still bounds the **exclusion** walk.
3155 ///
3156 /// That walk stopped collecting its ids into a `Vec` when the extra copy
3157 /// was removed and tallies them instead, with the budget charged against
3158 /// whichever of the two the walk is using. A budget that silently stopped
3159 /// applying to the one walk sized by *the client's* history would be an
3160 /// unbounded client-driven allocation, which is the whole class this fix
3161 /// is in.
3162 ///
3163 /// # The first version of this test could not fail, and that is the point
3164 ///
3165 /// It used a budget of 10 against 200 commits, so the **commit** walk in
3166 /// step 3 tripped first and the assertion was green whatever the tree walk
3167 /// did. Proved by breaking `Emit::Tally`'s `len()` to return `0` and
3168 /// watching it stay green. The budget here is therefore chosen to sit in
3169 /// the gap between the two: comfortably above the commit count, and below
3170 /// the object count those commits close over. Seen RED with the same
3171 /// `len()` broken: "the exclusion walk ran to completion under a budget of
3172 /// 400 and selected 3 objects".
3173 #[test]
3174 fn the_budget_bounds_the_exclusion_walk_not_only_the_commit_walk() {
3175 // 300 + the fixture's own 5 commits, each with one tree and one blob:
3176 // ~305 commits to walk, ~610 objects under them.
3177 const HISTORY: usize = 300;
3178 const BUDGET: usize = 400;
3179
3180 let fixture = Fixture::build(Kind::Sha1);
3181 let client_tip = fixture.extend(fixture.four, HISTORY, "client");
3182 let server_tip = fixture.extend(client_tip, 1, "server");
3183
3184 // The commit walk alone must NOT reach the budget, or this test is the
3185 // hollow one it replaced. Measured against the same graph.
3186 let graph = Graph::new(&fixture.git, fixture.kind);
3187 let commits = graph
3188 .commit_closure(
3189 &[client_tip],
3190 &HashSet::new(),
3191 &HashSet::new(),
3192 None,
3193 Missing::Skip,
3194 )
3195 .expect("the client's commits");
3196 assert!(
3197 commits.ids.len() < BUDGET,
3198 "the {} commits alone reach a budget of {BUDGET}, so this test cannot tell the two walks apart",
3199 commits.ids.len()
3200 );
3201
3202 let graph = Graph::new(&fixture.git, fixture.kind).with_budget(Some(BUDGET));
3203 let outcome = select_objects(
3204 &graph,
3205 &[server_tip],
3206 &[client_tip],
3207 &SelectOptions {
3208 exclusion: Exclusion::Full,
3209 ..walked()
3210 },
3211 );
3212
3213 match outcome {
3214 Err(Error::WalkBudget { limit }) => assert_eq!(limit, BUDGET),
3215 Err(other) => panic!("the budget should have stopped this walk, not {other}"),
3216 Ok(selection) => panic!(
3217 "the exclusion walk ran to completion under a budget of \
3218 {BUDGET} and selected {} objects",
3219 selection.objects.len()
3220 ),
3221 }
3222
3223 // The other half of the same fact, and the reason the budget stopped
3224 // being reached in practice: under `Exclusion::Edges` this identical
3225 // fetch, on the identical graph, under the identical budget, does not
3226 // come near it. The budget still guards the walk — it is the walk that
3227 // got smaller.
3228 let edges = select_objects(&graph, &[server_tip], &[client_tip], &walked()).expect(
3229 "edge exclusion tripped a budget of 400 on a 305-commit history; \
3230 it is supposed to open up the frontier, not the history",
3231 );
3232 assert_eq!(
3233 edges.objects.len(),
3234 3,
3235 "edge exclusion selected {} objects where the full closure selects 3",
3236 edges.objects.len()
3237 );
3238 }
3239
3240 /// A fetch does not read the history the client just told it about.
3241 ///
3242 /// # What is measured
3243 ///
3244 /// Object **reads**, counted by the store itself. Not wall clock, which on
3245 /// a `MemObjectStore` is a `BTreeMap` lookup either way and would report a
3246 /// win of nothing; not the pack, which must not move at all. The claim of
3247 /// [`Limiting::Merged`] is that a fetch stops asking for commits the client
3248 /// already named, and a count of what the store was asked for is that claim
3249 /// as applied output.
3250 ///
3251 /// # The three bounds
3252 ///
3253 /// **The floor on the old walk.** `TwoPass` must read more commits than the
3254 /// client's history is long, or the fixture is not the shape that can tell
3255 /// the two apart and every ratio below is against nothing.
3256 ///
3257 /// **The ceiling on the new one.** `Merged` must read fewer than a tenth as
3258 /// many. Seen RED by handing both sides `Limiting::TwoPass`: "the merged
3259 /// walk read 916 objects against the two-pass walk's 916".
3260 ///
3261 /// **The pack must not move.** Same object *set*, both ways. The dangerous
3262 /// direction is a merged walk that stops too early and sends fewer objects
3263 /// than the client needs, which is a broken fetch rather than a slow one;
3264 /// a set comparison catches it in either direction. Seen RED by raising
3265 /// [`SLOP`]'s countdown into an unconditional `break` — "the merged walk
3266 /// selected 3 objects where the two-pass walk selected 18".
3267 #[test]
3268 fn a_fetch_does_not_read_the_history_the_client_already_told_it_about() {
3269 use crate::common::Counting;
3270
3271 const HISTORY: usize = 300;
3272 const DAY: usize = 5;
3273
3274 let fixture = Fixture::build(Kind::Sha1);
3275 // Committer dates ascending across BOTH calls, which `extend`'s default
3276 // does not give: it restarts its clock per call, so the commits a fetch
3277 // is about to send would be dated older than the entire history behind
3278 // them. That is a real shape — a rebased branch looks like it — but it
3279 // is the shape a date-ordered walk cannot shortcut, and pricing the
3280 // walk on it would be pricing its worst case as its behaviour.
3281 let client_tip = fixture.extend_dated(fixture.four, HISTORY, "client", |i| {
3282 1_700_200_000 + i as i64
3283 });
3284 let server_tip = fixture.extend_dated(client_tip, DAY, "server", |i| {
3285 1_700_200_000 + (HISTORY + i) as i64
3286 });
3287
3288 let measure = |limiting: Limiting| -> (usize, Vec<ObjectId>) {
3289 let store = Counting::new(fixture.git.inner.clone());
3290 let graph = Graph::new(&store, Kind::Sha1);
3291 let options = SelectOptions {
3292 limiting,
3293 ..walked()
3294 };
3295 let selection = select_objects(&graph, &[server_tip], &[client_tip], &options)
3296 .expect("the incremental fetch");
3297 let mut ids = selection.objects;
3298 ids.sort();
3299 (store.touches(), ids)
3300 };
3301
3302 let (two_pass_reads, two_pass_objects) = measure(Limiting::TwoPass);
3303 let (merged_reads, merged_objects) = measure(Limiting::Merged);
3304
3305 assert!(
3306 two_pass_reads > HISTORY,
3307 "the two-pass walk touched the store {two_pass_reads} times over a \
3308 client history of {HISTORY} commits, so it is not walking that \
3309 history and this test cannot tell the two walks apart"
3310 );
3311 assert_eq!(
3312 merged_objects,
3313 two_pass_objects,
3314 "the merged walk selected {} objects where the two-pass walk \
3315 selected {}. Fewer is a broken fetch, not a win",
3316 merged_objects.len(),
3317 two_pass_objects.len()
3318 );
3319 assert!(
3320 merged_objects.len() >= 3 * DAY,
3321 "an incremental fetch of {DAY} commits selected only {} objects, so \
3322 the comparison above is between two empty sets",
3323 merged_objects.len()
3324 );
3325 assert!(
3326 merged_reads * 10 < two_pass_reads,
3327 "the merged walk touched the store {merged_reads} times against the \
3328 two-pass walk's {two_pass_reads}, {:.2}x. It is still enumerating \
3329 the client's history, which is the whole cost this removes",
3330 two_pass_reads as f64 / merged_reads as f64
3331 );
3332 }
3333
3334 /// The merged walk and the two-pass walk pick the same objects, on the
3335 /// shapes where they could disagree.
3336 ///
3337 /// The measurement above runs one linear fetch. These are the shapes a
3338 /// date-ordered walk can get wrong and a breadth-first one cannot: a
3339 /// **merge** whose two parents are on different sides of the frontier, a
3340 /// `have` the client names that is also a `want`, a `have` that is an
3341 /// ancestor of another `have`, and a fetch whose wants are dated *older*
3342 /// than the history behind them, which is what `SLOP` exists for.
3343 ///
3344 /// Equality is asserted on the sorted id list, because the merged walk
3345 /// emits in committer-date order and the two-pass walk breadth-first, and
3346 /// the *set* is what a pack has to contain.
3347 ///
3348 /// Seen RED by dropping [`SLOP`] to `0`, which ends the walk on the first
3349 /// uninteresting pop: "skewed wants: merged selected 3, two-pass 15".
3350 #[test]
3351 fn the_two_walks_agree_on_the_shapes_that_could_split_them() {
3352 let fixture = Fixture::build(Kind::Sha1);
3353 let old_line = fixture.extend_dated(fixture.three, 8, "old", |i| 1_700_300_000 + i as i64);
3354 let new_line = fixture.extend_dated(fixture.two, 8, "new", |i| 1_700_400_000 + i as i64);
3355 // A merge of a recent line and a stale one: its second parent is older
3356 // than commits the client already has, which is exactly where a walk
3357 // ordered by date can pop things in a surprising order.
3358 let merged_tip = crate::common::commit(
3359 &fixture.git,
3360 fixture.kind,
3361 "merged.txt",
3362 "merged",
3363 &[new_line, old_line],
3364 1_700_400_100,
3365 );
3366 // Wants dated older than everything behind them: a rebased branch, and
3367 // the case `SLOP` absorbs.
3368 let skewed = fixture.extend_dated(new_line, 4, "skew", |_| 1_700_000_000);
3369
3370 // The `bool` is whether the fetch is expected to select anything at
3371 // all. Without it the "nothing to send" case would satisfy an equality
3372 // between two empty lists and quietly become the hollow half of this
3373 // guard — while still being worth asserting, because a merged walk that
3374 // sent the client its own tip back would show up here and nowhere else.
3375 let cases: [(&str, &[ObjectId], &[ObjectId], bool); 5] = [
3376 (
3377 "merge across the frontier",
3378 &[merged_tip],
3379 &[old_line],
3380 true,
3381 ),
3382 (
3383 "a want that is also a have",
3384 &[merged_tip],
3385 &[merged_tip],
3386 false,
3387 ),
3388 (
3389 "one have behind another",
3390 &[merged_tip],
3391 &[new_line, fixture.two],
3392 true,
3393 ),
3394 ("skewed wants", &[skewed], &[new_line], true),
3395 ("no haves at all", &[merged_tip], &[], true),
3396 ];
3397
3398 for (name, tips, haves, expect_objects) in cases {
3399 let pick = |limiting: Limiting| -> Vec<ObjectId> {
3400 let graph = Graph::new(&fixture.git, fixture.kind);
3401 let mut ids = select_objects(
3402 &graph,
3403 tips,
3404 haves,
3405 &SelectOptions {
3406 limiting,
3407 ..walked()
3408 },
3409 )
3410 .unwrap_or_else(|err| panic!("{name}: {err}"))
3411 .objects;
3412 ids.sort();
3413 ids
3414 };
3415 let merged = pick(Limiting::Merged);
3416 let two_pass = pick(Limiting::TwoPass);
3417 assert_eq!(
3418 !two_pass.is_empty(),
3419 expect_objects,
3420 "{name}: the two-pass walk selected {} objects, which is not the \
3421 shape this case is here to compare",
3422 two_pass.len()
3423 );
3424 assert_eq!(
3425 merged,
3426 two_pass,
3427 "{name}: merged selected {}, two-pass {}",
3428 merged.len(),
3429 two_pass.len()
3430 );
3431 }
3432 }
3433
3434 /// A `have` older than its own parent still stops the walk behind it.
3435 ///
3436 /// # The one thing the two comparisons above cannot see
3437 ///
3438 /// They assert that the merged walk and the two-pass walk pick the same
3439 /// objects. git's `mark_parents_uninteresting` is invisible to that,
3440 /// because the only shape where it changes anything is a shape where the
3441 /// two walks are *allowed* to differ — the merged one sends a documented
3442 /// superset there, so an equality assertion is either already failing or
3443 /// already blind. Proved: with the whole function replaced by an early
3444 /// `return`, all five tests in this module stay green.
3445 ///
3446 /// So this asserts the applied output directly, on the shape that needs it:
3447 ///
3448 /// ```text
3449 /// A (t=1000) ── B (t=2000) ── W (t=3000) the want
3450 /// └──────── U (t=1500) the have, OLDER than B
3451 /// ```
3452 ///
3453 /// `W` is popped first and takes `B` for interesting, so `B`'s parent `A`
3454 /// is admitted interesting too. `U` arrives afterwards and marks `B`
3455 /// uninteresting — and `A` is only reached by pushing that mark on down
3456 /// through a commit already read, which is what the function does and the
3457 /// only thing that does it.
3458 ///
3459 /// Seen RED with that early `return` in place: "the merged walk sent 3
3460 /// commits, including A, which sits behind a commit the client holds".
3461 ///
3462 /// `B` itself **is** sent, and that is the trade rather than a bug: it was
3463 /// emitted before `U` was ever popped. The two-pass walk sends only `W`.
3464 /// The assertion records both numbers so the size of the superset is a
3465 /// stated fact instead of a surprise.
3466 #[test]
3467 fn a_have_older_than_its_own_parent_still_stops_the_walk_behind_it() {
3468 use crate::common::commit;
3469 use gunnar_store::mem::MemGitServe;
3470
3471 let store = MemGitServe::new(Kind::Sha1);
3472 let a = commit(&store, Kind::Sha1, "a.txt", "a", &[], 1_000);
3473 let b = commit(&store, Kind::Sha1, "b.txt", "b", &[a], 2_000);
3474 let u = commit(&store, Kind::Sha1, "u.txt", "u", &[b], 1_500);
3475 let w = commit(&store, Kind::Sha1, "w.txt", "w", &[b], 3_000);
3476
3477 let graph = Graph::new(&store, Kind::Sha1);
3478 let pick = |limiting: Limiting| -> Vec<ObjectId> {
3479 select_objects(
3480 &graph,
3481 &[w],
3482 &[u],
3483 &SelectOptions {
3484 limiting,
3485 ..walked()
3486 },
3487 )
3488 .expect("the skewed fetch")
3489 .commits
3490 };
3491
3492 let merged = pick(Limiting::Merged);
3493 assert!(
3494 !merged.contains(&a),
3495 "the merged walk sent {} commits, including A, which sits behind a \
3496 commit the client holds: the client's side of the graph stopped \
3497 propagating at the first commit already taken for interesting",
3498 merged.len()
3499 );
3500 assert_eq!(
3501 merged,
3502 vec![w, b],
3503 "the merged walk sent {merged:?}; W and B is the documented \
3504 superset on this shape, and anything else has moved it"
3505 );
3506 assert_eq!(
3507 pick(Limiting::TwoPass),
3508 vec![w],
3509 "the two-pass walk is the exact answer here and must stay it, or \
3510 the superset above is being measured against a moving baseline"
3511 );
3512 }
3513
3514 /// The edge is the parent the two histories actually share, not the tip the
3515 /// client named.
3516 ///
3517 /// # Why this was missing, and what it was missing from
3518 ///
3519 /// [`Exclusion::Edges`] opens up two things in step 5: the commits the
3520 /// client named as `have`s, and the **edge**. On a linear fetch those are
3521 /// the same commit, so every test in this crate that fetches a line forward
3522 /// is green with the edge set emptied — proved, by emptying it. The edge
3523 /// only does work when the two histories join *behind* what the client
3524 /// named:
3525 ///
3526 /// ```text
3527 /// A ── C1 ── C2 the client's line; C2 is the `have`
3528 /// └── S1 the server's; S1 is the `want`
3529 /// ```
3530 ///
3531 /// `S1` carries the same file and the same content as `A`, so it shares
3532 /// `A`'s tree and blob object for object. `A` is two hops behind the `have`
3533 /// and is named by nobody. A step 5 that opened up only what the client
3534 /// named would put that tree and that blob in the pack again.
3535 ///
3536 /// Asserted for **both** walks, because both compute an edge set and this
3537 /// is the only test that can tell either of them is doing it.
3538 ///
3539 /// Seen RED, both ways: `Limiting::Merged`'s edge pass turned into
3540 /// `.take(0)` and `commit_closure`'s `out.edges.push(parent)` removed —
3541 /// "merged: a fetch sharing a tree and a blob with the client packed 3
3542 /// objects, not 1" and the same line for two-pass.
3543 #[test]
3544 fn the_exclusion_opens_up_the_parent_the_two_histories_share() {
3545 use crate::common::commit;
3546 use gunnar_store::mem::MemGitServe;
3547
3548 let store = MemGitServe::new(Kind::Sha1);
3549 let a = commit(&store, Kind::Sha1, "shared.txt", "shared", &[], 1_000);
3550 let c1 = commit(&store, Kind::Sha1, "c1.txt", "c1", &[a], 2_000);
3551 let c2 = commit(&store, Kind::Sha1, "c2.txt", "c2", &[c1], 3_000);
3552 // Same path, same content, so the same tree and the same blob as `A`.
3553 let s1 = commit(&store, Kind::Sha1, "shared.txt", "shared", &[a], 4_000);
3554 assert_ne!(
3555 s1, a,
3556 "the two commits must differ, only their content may not"
3557 );
3558
3559 let graph = Graph::new(&store, Kind::Sha1);
3560 for limiting in [Limiting::Merged, Limiting::TwoPass] {
3561 let selection = select_objects(
3562 &graph,
3563 &[s1],
3564 &[c2],
3565 &SelectOptions {
3566 limiting,
3567 ..walked()
3568 },
3569 )
3570 .expect("the branch fetch");
3571 assert_eq!(
3572 selection.objects,
3573 vec![s1],
3574 "{limiting:?}: a fetch sharing a tree and a blob with the client \
3575 packed {} objects, not 1. The edge behind the client's named \
3576 tip was never opened up, so content the client demonstrably \
3577 holds is going back over the wire",
3578 selection.objects.len()
3579 );
3580 }
3581 }
3582
3583 /// `CommitClosure::trees[i]` really is `ids[i]`'s root tree.
3584 ///
3585 /// # Why this needs its own guard, when 500 other tests already pack packs
3586 ///
3587 /// Nothing downstream can tell a **mispaired** tree from a correct one. Every
3588 /// tree in `trees` is a real tree of a real commit in the same closure, so a
3589 /// walk that paired them by an off-by-one would still visit exactly the same
3590 /// set of trees, still emit a complete and self-consistent pack, and still
3591 /// satisfy every "the client got what it asked for" assertion in the suite —
3592 /// on a linear history, where the closure is one commit per tree, it would
3593 /// produce a **byte-identical** pack. The bug only becomes visible on a
3594 /// shape where the sets differ, and it would then look like a corrupt
3595 /// repository rather than like a walker bug. So the pairing is asserted
3596 /// here, element by element, against a **second and independent** read of
3597 /// each commit through `Graph::commit`, which is the full
3598 /// `CommitRef::from_bytes` decoder rather than the `CommitRefIter` one the
3599 /// walk now uses. That the two decoders agree is the other half of what is
3600 /// being checked.
3601 ///
3602 /// Seen RED, both ways: `out.trees.push(tree)` moved above `out.ids.push(id)`
3603 /// with a one-element skew fails on the first pair; returning
3604 /// `graph.commit(&id)?.tree` for the *previous* commit fails likewise. The
3605 /// budget test above and the one-set test both stay green under either edit.
3606 ///
3607 /// # The floor
3608 ///
3609 /// A `zip` over two empty vectors asserts nothing and passes. `HISTORY` is
3610 /// checked to have actually arrived, because that is the shape this guard
3611 /// would otherwise silently take.
3612 #[test]
3613 fn the_commit_walk_hands_back_each_commits_own_root_tree() {
3614 const HISTORY: usize = 40;
3615
3616 let fixture = Fixture::build(Kind::Sha1);
3617 let tip = fixture.extend(fixture.four, HISTORY, "p010");
3618 let graph = Graph::new(&fixture.git, fixture.kind);
3619
3620 let closure = graph
3621 .commit_closure(
3622 &[tip],
3623 &HashSet::new(),
3624 &HashSet::new(),
3625 None,
3626 Missing::Fail,
3627 )
3628 .expect("the commit closure");
3629
3630 assert_eq!(
3631 closure.ids.len(),
3632 closure.trees.len(),
3633 "the two halves of a CommitClosure must be the same length or the \
3634 pairing is not a pairing"
3635 );
3636 assert!(
3637 closure.ids.len() > HISTORY,
3638 "the fixture is not the shape this test needs: {HISTORY} commits on \
3639 top of the base should close over more than {HISTORY} commits, not \
3640 {}. A zip over a shorter list would assert nothing.",
3641 closure.ids.len()
3642 );
3643
3644 for (i, (id, tree)) in closure.ids.iter().zip(&closure.trees).enumerate() {
3645 let independent = graph
3646 .commit(id)
3647 .expect("re-reading a commit the walk just read")
3648 .tree;
3649 assert_eq!(
3650 *tree, independent,
3651 "commit {i} ({id}) was paired with tree {tree}, but its own root \
3652 tree is {independent}"
3653 );
3654 }
3655 }
3656
3657 // ── the bytes ceiling ──────────────────────────────────────────────────
3658
3659 /// The number of ids a `walk_bytes` ceiling of `objects` objects is.
3660 fn ceiling_for(objects: usize) -> usize {
3661 objects * crate::WALK_BYTES_PER_OBJECT
3662 }
3663
3664 /// **A fetch is charged for what it HOLDS, not for what it sends**, and a
3665 /// `blob:none` filter is where the two come apart.
3666 ///
3667 /// # Why this shape and not a big clone
3668 ///
3669 /// The old ceiling compared `Emit::len()` — the objects the walk is about
3670 /// to send — against the budget. On a full clone that is nearly the whole
3671 /// set held and the difference does not show. Under `filter=blob:none` the
3672 /// walk still puts **every blob** into its `seen` set, because it has to
3673 /// know it has been there, and then declines to emit any of them. So the
3674 /// thing being counted stops growing while the thing being allocated keeps
3675 /// going, and a ceiling written against the first cannot see the second.
3676 ///
3677 /// # Both edges, and the floor
3678 ///
3679 /// | ceiling | must |
3680 /// |---|---|
3681 /// | the ids the walk EMITS | refuse — it is holding more than that |
3682 /// | the ids the walk HOLDS, and a scrap | serve |
3683 ///
3684 /// The second is not decoration. A ceiling that refused at both would be
3685 /// satisfied by a charge that is simply too high — or by a walk that cannot
3686 /// serve anything — and this test would then be green about a server that
3687 /// clones nothing.
3688 ///
3689 /// Seen RED with `charge_walk` reading zero instead of [`meter::live`]:
3690 /// *"a ceiling the size of what this fetch sends must refuse it"*.
3691 ///
3692 /// # What it does NOT guard, said plainly
3693 ///
3694 /// [`WALK_BYTES_PER_OBJECT`](crate::WALK_BYTES_PER_OBJECT). Doubling that
3695 /// constant leaves this test green, because `ceiling_for` sizes the
3696 /// ceiling from the same constant — deliberately, so the test is about the
3697 /// mechanism and survives a re-measurement, but it does mean nothing here
3698 /// can tell a well-chosen rate from a wrong one. The rate is a measurement
3699 /// and only a measurement can check it: `bench/examples/walkbudget-probe.rs`,
3700 /// and the table on the constant is what it produced.
3701 #[test]
3702 fn the_ceiling_charges_what_the_walk_holds_not_what_it_sends() {
3703 const HISTORY: usize = 300;
3704 let fixture = Fixture::build(Kind::Sha1);
3705 let tip = fixture.extend(fixture.four, HISTORY, "held");
3706
3707 let blobless = Filter::parse("blob:none").expect("blob:none parses");
3708 // The ceiling under test lives on the `Graph`, not here: it is a
3709 // property of the traversal scope, which is what has to release the
3710 // pool reservation when the request ends.
3711 let options = |filter| SelectOptions { filter, ..walked() };
3712
3713 // What the blob:none walk HOLDS, and what it SENDS. Both measured by
3714 // the same walker rather than predicted — `selection.objects.len()` is
3715 // not the first of them, which is the whole point of this test and was
3716 // worth finding out the hard way: on this fixture the walk sends 915
3717 // ids and holds 980, because `seen` carries the peeled tips and tags
3718 // the emitted vector never does.
3719 let (sent, held) = {
3720 let graph = Graph::new(&fixture.git, fixture.kind);
3721 let sent = select_objects(&graph, &[tip], &[], &options(Some(&blobless)))
3722 .expect("the blob:none clone")
3723 .objects
3724 .len();
3725 (sent, meter::peak())
3726 };
3727 assert!(
3728 held > sent + 100,
3729 "the fixture is not the shape this test needs: a blob:none clone \
3730 sent {sent} objects and held {held}, and the two have to be far \
3731 enough apart that a ceiling can sit between them"
3732 );
3733
3734 // ── the ceiling at what it SENDS must refuse ────────────────────────
3735 let graph =
3736 Graph::new(&fixture.git, fixture.kind).with_walk_limit(Some(ceiling_for(sent)), None);
3737 let refused = select_objects(&graph, &[tip], &[], &options(Some(&blobless)))
3738 .expect_err("a ceiling the size of what this fetch sends must refuse it");
3739 match refused {
3740 Error::WalkTooLarge { held: bytes, limit } => {
3741 assert_eq!(
3742 limit,
3743 ceiling_for(sent),
3744 "the refusal named another ceiling"
3745 );
3746 assert!(
3747 bytes > limit,
3748 "the refusal said it was holding {bytes} bytes against a \
3749 ceiling of {limit}, which is not over it"
3750 );
3751 }
3752 other => panic!(
3753 "a blob:none fetch over the byte ceiling failed with {other}, not WalkTooLarge — \
3754 so the ceiling is not what stopped it and this test is measuring another fault"
3755 ),
3756 }
3757
3758 // ── and the ceiling at what it HOLDS must serve ─────────────────────
3759 let graph =
3760 Graph::new(&fixture.git, fixture.kind).with_walk_limit(Some(ceiling_for(held)), None);
3761 let served = select_objects(&graph, &[tip], &[], &options(Some(&blobless)))
3762 .expect("a ceiling above what this fetch holds must serve it");
3763 assert_eq!(
3764 served.objects.len(),
3765 sent,
3766 "the served arm sent {} objects where the unbounded one sent {sent}: a ceiling that \
3767 changes the ANSWER rather than refusing is worse than the fault being measured",
3768 served.objects.len()
3769 );
3770 }
3771
3772 /// The ceiling is **off** when `walk_bytes` is `None`, and that is the arm
3773 /// that keeps the one above honest: without it, a `Graph` that refused
3774 /// every walk would satisfy the refusal half and the serve half would only
3775 /// be saying the number was large.
3776 #[test]
3777 fn no_ceiling_means_no_ceiling() {
3778 let fixture = Fixture::build(Kind::Sha1);
3779 let tip = fixture.extend(fixture.four, 50, "off");
3780 let graph = Graph::new(&fixture.git, fixture.kind).with_walk_limit(None, None);
3781 let all = select_objects(&graph, &[tip], &[], &walked())
3782 .expect("an unbounded walk serves")
3783 .objects
3784 .len();
3785 assert!(
3786 all > 50,
3787 "the fixture holds {all} objects, too few to mean anything"
3788 );
3789 }
3790
3791 /// **The pool is what bounds the box**: two walks, one purse, and the
3792 /// second is refused because the first is holding it.
3793 ///
3794 /// # Why this is a different assertion from the one above
3795 ///
3796 /// A per-request ceiling that admits one large clone admits 256 of them,
3797 /// because `gunnar serve` counts transfers and not bytes. So the fact under
3798 /// test is not "a walk can be refused" — it is "a walk that would have been
3799 /// admitted on its own is refused because of what somebody else is holding",
3800 /// and that cannot be shown with one walk.
3801 ///
3802 /// Seen RED twice, and the two failures name different halves. With
3803 /// `Graph::charge_walk`'s pool arm disabled: *"the first walk reserved 0 of
3804 /// the 16777216 it holds, so there is nothing for the second to be refused
3805 /// by"*. With `Drop for Graph` releasing nothing: *"the pool did not get
3806 /// its bytes back, so this server refuses everything after enough
3807 /// clones"* — which is the failure mode where a pool closes for good.
3808 #[test]
3809 fn the_pool_refuses_a_walk_because_of_what_another_is_holding() {
3810 // Big enough to cross `WalkPool::FLOOR_BYTES`, and not one commit more.
3811 // The floor is 8 MiB, which is 21 845 ids at the charge rate, and
3812 // `extend` lays down a commit, a tree and a blob apiece. The shipped
3813 // constants are used rather than lowered for the test: a floor that is
3814 // only ever crossed by a number the test chose is not the floor the
3815 // server has.
3816 let history = crate::WalkPool::FLOOR_BYTES / crate::WALK_BYTES_PER_OBJECT / 3 + 500;
3817 let fixture = Fixture::build(Kind::Sha1);
3818 let tip = fixture.extend(fixture.four, history, "pool");
3819
3820 let held = {
3821 let graph = Graph::new(&fixture.git, fixture.kind);
3822 select_objects(&graph, &[tip], &[], &walked()).expect("the whole clone");
3823 meter::peak()
3824 };
3825 assert!(
3826 ceiling_for(held) >= crate::WalkPool::FLOOR_BYTES,
3827 "this fixture holds {held} ids, {} bytes, which is under the pool's floor of {} \
3828 — the walk would never touch the pool and every arm below would pass against \
3829 nothing",
3830 ceiling_for(held),
3831 crate::WalkPool::FLOOR_BYTES
3832 );
3833
3834 // A purse sized to exactly what one of these walks reserves: room for
3835 // the first and none for the second. Computed from the measurement
3836 // rather than typed, so the test cannot drift out of the shape it needs
3837 // when the fixture or the charge rate moves.
3838 let reservation = ceiling_for(held).next_multiple_of(crate::WalkPool::CHUNK_BYTES);
3839 let pool = crate::WalkPool::new(reservation);
3840
3841 let outer =
3842 Graph::new(&fixture.git, fixture.kind).with_walk_limit(None, Some(pool.clone()));
3843 let holding = select_objects(&outer, &[tip], &[], &walked())
3844 .expect("the first walk fits the empty pool");
3845 assert!(!holding.objects.is_empty());
3846 assert_eq!(
3847 pool.in_flight_bytes(),
3848 reservation,
3849 "the first walk reserved {} of the {reservation} it holds, so there is nothing for \
3850 the second to be refused by",
3851 pool.in_flight_bytes()
3852 );
3853
3854 // ── the second walk, on its own perfectly reasonable ────────────────
3855 let inner =
3856 Graph::new(&fixture.git, fixture.kind).with_walk_limit(None, Some(pool.clone()));
3857 let refused =
3858 select_objects(&inner, &[tip], &[], &walked()).expect_err("a full pool must refuse");
3859 match refused {
3860 Error::WalkPoolFull { limit, .. } => assert_eq!(limit, reservation),
3861 other => panic!(
3862 "a walk against a full pool failed with {other}, not WalkPoolFull — a client \
3863 cannot tell a busy server from a request it must never send again"
3864 ),
3865 }
3866 assert_eq!(pool.refusals(), 1, "the refusal was not counted");
3867 drop(inner);
3868
3869 // ── and the purse comes back ────────────────────────────────────────
3870 drop(outer);
3871 drop(holding);
3872 assert_eq!(
3873 pool.in_flight_bytes(),
3874 0,
3875 "the pool did not get its bytes back, so this server refuses everything after enough \
3876 clones"
3877 );
3878 let again =
3879 Graph::new(&fixture.git, fixture.kind).with_walk_limit(None, Some(pool.clone()));
3880 select_objects(&again, &[tip], &[], &walked())
3881 .expect("the same walk must serve once the first has finished");
3882 }
3883
3884 /// A release the pool never handed out cannot wrap it shut.
3885 ///
3886 /// `saturating_sub` on a `usize` that went negative would be `usize::MAX`,
3887 /// and a pool whose `in_flight` is `usize::MAX` refuses every transfer
3888 /// forever. Seen RED with a plain `fetch_sub`.
3889 #[test]
3890 fn walk_pool_never_goes_negative() {
3891 let pool = crate::WalkPool::new(1024);
3892 pool.release(4096);
3893 assert_eq!(pool.in_flight_bytes(), 0);
3894 pool.try_reserve(1024)
3895 .expect("a pool at zero has all of itself");
3896 assert_eq!(pool.in_flight_bytes(), 1024);
3897 }
3898
3899 /// A pool sized to this machine is not sized to a constant.
3900 #[test]
3901 fn a_pool_for_this_machine_reads_the_machine() {
3902 let pool = crate::WalkPool::for_this_machine();
3903 assert!(
3904 pool.limit_bytes() > 0,
3905 "a pool of zero bytes refuses every transfer"
3906 );
3907 if let Ok(text) = std::fs::read_to_string("/proc/meminfo") {
3908 let kb: usize = text
3909 .lines()
3910 .find_map(|l| l.strip_prefix("MemTotal:")?.trim().strip_suffix(" kB"))
3911 .and_then(|v| v.trim().parse().ok())
3912 .expect("MemTotal is in /proc/meminfo");
3913 assert_eq!(
3914 pool.limit_bytes(),
3915 kb * 1024 / 2,
3916 "the pool is not half of this machine's memory, so it is a constant wearing a \
3917 measurement's name"
3918 );
3919 }
3920 }
3921}