Skip to main content

znippy_plugin_git/
exploded.rs

1//! **§14's exploded objects: the CONTRACT — who hands a resolved object over,
2//! and what the counters mean.**
3//!
4//! The store itself is [`crate::exploded_arrow`]: one Arrow IPC table, one row
5//! per object, payload in the row. This module is what both ends agree on —
6//! [`PayloadSink`] and the sinks that are not a store ([`NoSink`],
7//! [`CaptureOne`]), plus [`ExplodedStats`], so no caller can tell the medium
8//! apart by its instruments.
9//!
10//! # What §14 decided, and what it cost
11//!
12//! §14 fixed the shape — *the verbatim pack bytes are the truth and the resolved
13//! objects are a derived side table* — and left one sub-question open, "resolve
14//! eagerly or lazily?". **Decided 2026-08-08: EAGER**, built by the background
15//! indexer, with disk explicitly not a consideration ("we don't care if disk is
16//! tripled — we want wall speed minimum"). So there is no size cap, no threshold
17//! and no lazy fallback that skips an object to save space; what an operator can
18//! choose is [`crate::exploded_arrow::ExplodePolicy`], and that is a setting
19//! rather than a heuristic.
20//!
21//! ```text
22//!   push ─► verbatim bytes fsynced ─► ack        ← the truth, §14
23//!                    │
24//!                    └─ channel ─► indexer ─► resolve ─► ┬─► objects table  (oid → extent)
25//!                                                        └─► THIS SINK ─► the Arrow table
26//! ```
27//!
28//! **The decision was right and the first medium was wrong.** MEASURED on
29//! `linux.git`, oden 2026-08-13, 11 697 976 objects: 6.4 GB of verbatim pack
30//! resolves to **16.8 GB** of content — 2.6×, inside the 3× that "tripled"
31//! budgeted — and redb held those 16.8 GB in a **204 GB** file, 12× page churn,
32//! while serialising the `gatling_for_each` fan-out behind one write transaction
33//! at 96.8% of a single core. The eager decision stands; redb is gone from this
34//! path.
35//!
36//! # Three readers, and the third is why it is not optional
37//!
38//! | reader | had to do | now does |
39//! |---|---|---|
40//! | a content read | resolve a delta chain, inflate its base | **one point lookup** |
41//! | a thin pack's external base ([`crate::resolve::BaseSource`]) | read and re-resolve a whole pack | **one point lookup** |
42//! | `Derived::graph` / `reachable()` **after a clean reopen** | nothing — the payloads were stored nowhere | **a scan of the commits** |
43//!
44//! The third row is a correctness hole, not a speed one. Commit and tree
45//! payloads were held only in RAM, so a store that shut down **cleanly** came
46//! back with every pack's `indexed` bit legitimately set, nothing re-queued, and
47//! an empty commit graph — MEASURED on oden 2026-08-08, 2687 rows and **0 of 551
48//! commits**, with `reachable()` quietly returning a commit alone instead of its
49//! closure and `gc` seeing an empty live set.
50//!
51//! # Droppable, and that is not a caveat
52//!
53//! Every row is re-derivable from the verbatim pack bytes, so the table can be
54//! deleted at any time without consulting a client:
55//! [`crate::git_ops::Absorber::adopt_journal`] compares its row count against
56//! the `objects` table's and, if it is short, declares **no** pack absorbed — so
57//! every pack is re-queued and re-exploded. Absent means fall back and rebuild;
58//! it never means wrong. That is §13.12's `indexed`-bit logic applied unchanged
59//! to a derived table, and
60//! [`crate::git_ops::tests::dropping_the_exploded_table_and_reopening_still_answers`]
61//! asserts it by deleting the file.
62//!
63//! # What it costs, measured
64//!
65//! MEASURED on oden 2026-08-08, release, one real 2687-object / 5.4 MiB pack,
66//! four runs per column, **1-minute loadavg 1.87–2.04**. The two columns are the
67//! same binary with the sink swapped to [`NoSink`] and the fold stubbed out, so
68//! nothing but the table differs between them.
69//!
70//! | | without the table | with it | |
71//! |---|---:|---:|---:|
72//! | **ack** (`put_pack` returns) | 31.0–34.2 ms | 31.0–31.3 ms | **unchanged** |
73//! | **drain** (one pack absorbed) | 166.7–168.6 ms | 316.2–329.8 ms | 1.9x |
74//! | **indexer throughput** | 15 940–16 120 rows/s | 8 146–8 499 rows/s | **0.51x** |
75//!
76//! **The ack path is unchanged by construction, not by measurement.** `put_pack`
77//! walks, checks the closure, appends verbatim, fsyncs twice and queues 24 bytes,
78//! and not one of those lines touches this. The measured ack ranges overlap and
79//! the wider one is the *left* column, which is how a null result looks; no ack
80//! figure here is evidence of anything.
81//!
82//! Those figures are redb's. They are kept because the ack column is a
83//! construction argument that still holds, and the drain column is the honest
84//! record of what the eager decision cost when it was taken — not a claim about
85//! what the Arrow table costs, which has not been measured on that pack.
86
87use std::sync::Mutex;
88
89use anyhow::Result;
90
91use crate::object::GitObjectKind;
92
93/// Where every object the resolver produces is handed over.
94///
95/// A **sink** rather than a return value: [`crate::resolve::resolve_walked`]
96/// already drops a blob payload the moment nothing else in the pack deltas
97/// against it, and returning every payload instead would mean the whole pack
98/// inflated in RAM at once. Streaming it out keeps the resolver's peak footprint
99/// exactly what it was before this table existed.
100pub trait PayloadSink {
101    /// One resolved object: its oid, the type its chain resolves to, and the
102    /// inflated payload. Called **once per pack entry**, never for a delta's
103    /// intermediate state.
104    fn explode(&self, oid: &[u8], kind: GitObjectKind, payload: &[u8]) -> Result<()>;
105}
106
107/// A sink that keeps nothing. What every caller that only wants oids passes, and
108/// what makes "this resolve built no exploded rows" a visible choice rather than
109/// an omission.
110pub struct NoSink;
111
112impl PayloadSink for NoSink {
113    fn explode(&self, _oid: &[u8], _kind: GitObjectKind, _payload: &[u8]) -> Result<()> {
114        Ok(())
115    }
116}
117
118/// A sink that keeps **one** object: the fallback path's, for when the table
119/// cannot answer and the content has to come back out of the verbatim truth.
120///
121/// It exists because [`crate::resolve::Resolved::payload`] is not a complete
122/// answer to "what does this object contain" and never was — the resolver drops
123/// a blob payload the moment nothing else in the pack deltas against it, so
124/// `payload` is `None` for most blobs in most packs. Re-deriving a blob's
125/// content by reading that field therefore returned `None` for exactly the
126/// commonest object in a repository. Through the sink the payload is seen before
127/// it is dropped, so the fallback answers for every object the pack holds.
128pub struct CaptureOne<'a> {
129    want: &'a [u8],
130    got: Mutex<Option<(GitObjectKind, Vec<u8>)>>,
131}
132
133impl<'a> CaptureOne<'a> {
134    pub fn new(want: &'a [u8]) -> Self {
135        Self {
136            want,
137            got: Mutex::new(None),
138        }
139    }
140
141    /// What the resolve produced for that oid, if it produced it.
142    pub fn take(self) -> Option<(GitObjectKind, Vec<u8>)> {
143        self.got.into_inner().ok().flatten()
144    }
145}
146
147impl PayloadSink for CaptureOne<'_> {
148    fn explode(&self, oid: &[u8], kind: GitObjectKind, payload: &[u8]) -> Result<()> {
149        if oid == self.want
150            && let Ok(mut g) = self.got.lock()
151        {
152            *g = Some((kind, payload.to_vec()));
153        }
154        Ok(())
155    }
156}
157
158/// Counters, all of them **applied output**: rows that exist, lookups that
159/// happened. Nothing here is configuration echoed back.
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
161pub struct ExplodedStats {
162    /// Rows on disk plus rows buffered for the next flush.
163    pub rows: u64,
164    /// Rows this process has committed.
165    pub written: u64,
166    /// Content reads answered **from this table**.
167    pub served: u64,
168    /// Content reads that fell through and re-resolved a whole pack. The number
169    /// that distinguishes "the table answered" from "the answer happened to be
170    /// right", which identical bytes cannot.
171    pub rederived: u64,
172    /// Content reads nothing could answer.
173    pub absent: u64,
174}