Skip to main content

zeppelin_embed/lifecycle/
stats.rs

1//! Exact engine-owned memory and kernel-backed process statistics.
2
3use std::ops::Deref;
4use std::sync::{Arc, Mutex};
5
6use super::budget::Budgets;
7use super::{Store, StoreError, StoreState};
8
9#[cfg(feature = "graph-cypher")]
10mod allocation_interval;
11#[cfg(feature = "graph-cypher")]
12pub use allocation_interval::{
13    AllocationInterval, AllocationIntervalError, AllocationIntervalSnapshot,
14};
15
16/// Store-cumulative native write work, including work before rejected writes.
17/// Byte metrics are logical formats, not allocation capacities or DRAM traffic.
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
19pub struct GraphWorkLedger {
20    /// Non-query storage work, including preparation and maintenance.
21    pub storage_lookups: u64,
22    /// Non-query storage work, including preparation and maintenance.
23    pub storage_scans: u64,
24    /// Non-query storage work, including preparation and maintenance.
25    pub storage_adjacency_entries: u64,
26    /// Non-query storage work, including preparation and maintenance.
27    pub storage_copied_bytes: u64,
28    /// Non-query storage work, including preparation and maintenance.
29    pub storage_pages_decoded: u64,
30    /// Non-query storage work, including preparation and maintenance.
31    pub storage_pages_copied: u64,
32    /// Non-query storage work, including preparation and maintenance.
33    pub storage_property_values: u64,
34    /// Non-query storage work, including preparation and maintenance.
35    pub storage_property_bytes: u64,
36    /// Non-query storage work, including preparation and maintenance.
37    pub storage_adjacency_physical_entries: u64,
38    /// Non-query storage work, including preparation and maintenance.
39    pub storage_adjacency_merged_visits: u64,
40    /// Non-query storage work, including preparation and maintenance.
41    pub storage_adjacency_merge_runs: u64,
42
43    /// Canonical byte pairs actually compared.
44    pub canonical_comparison_bytes: u64,
45    /// Canonical bytes emitted into real staging backing, excluding sizing passes.
46    pub canonical_encoding_bytes: u64,
47    /// Admitted WAL codec byte/descriptor units, including repeated passes.
48    pub wal_codec_units: u64,
49    /// Complete native commit envelope bytes encoded before append.
50    pub encoded_wal_bytes: u64,
51    /// Bytes supplied to successful commit-artifact writes.
52    pub artifact_bytes_written: u64,
53    /// Successful commit-artifact writes.
54    pub artifact_writes: u64,
55    /// Bytes supplied to successful native commit WAL appends.
56    pub wal_bytes_appended: u64,
57    /// Successful native commit WAL appends.
58    pub wal_appends: u64,
59    /// Attempted full file syncs at native commit publication.
60    pub full_sync_attempts: u64,
61    /// Successful full file syncs at native commit publication.
62    pub full_sync_successes: u64,
63    /// Attempted commit directory syncs, separate from file syncs.
64    pub directory_sync_attempts: u64,
65    /// Successful commit directory syncs.
66    pub directory_sync_successes: u64,
67}
68#[derive(Clone, Copy)]
69pub(crate) enum GraphWorkKind {
70    StorageLookups,
71    StorageScans,
72    StorageAdjacencyEntries,
73    StorageCopiedBytes,
74    StoragePagesDecoded,
75    StoragePagesCopied,
76    StoragePropertyValues,
77    StoragePropertyBytes,
78    StorageAdjacencyPhysicalEntries,
79    StorageAdjacencyMergedVisits,
80    StorageAdjacencyMergeRuns,
81
82    CanonicalComparisonBytes,
83    CanonicalEncodingBytes,
84    WalCodecUnits,
85    EncodedWalBytes,
86    ArtifactBytesWritten,
87    ArtifactWrites,
88    WalBytesAppended,
89    WalAppends,
90    FullSyncAttempts,
91    FullSyncSuccesses,
92    DirectorySyncAttempts,
93    DirectorySyncSuccesses,
94}
95impl GraphWorkLedger {
96    pub(crate) fn add(&mut self, kind: GraphWorkKind, units: u64) {
97        let slot = match kind {
98            GraphWorkKind::StorageLookups => &mut self.storage_lookups,
99            GraphWorkKind::StorageScans => &mut self.storage_scans,
100            GraphWorkKind::StorageAdjacencyEntries => &mut self.storage_adjacency_entries,
101            GraphWorkKind::StorageCopiedBytes => &mut self.storage_copied_bytes,
102            GraphWorkKind::StoragePagesDecoded => &mut self.storage_pages_decoded,
103            GraphWorkKind::StoragePagesCopied => &mut self.storage_pages_copied,
104            GraphWorkKind::StoragePropertyValues => &mut self.storage_property_values,
105            GraphWorkKind::StoragePropertyBytes => &mut self.storage_property_bytes,
106            GraphWorkKind::StorageAdjacencyPhysicalEntries => {
107                &mut self.storage_adjacency_physical_entries
108            }
109            GraphWorkKind::StorageAdjacencyMergedVisits => {
110                &mut self.storage_adjacency_merged_visits
111            }
112            GraphWorkKind::StorageAdjacencyMergeRuns => &mut self.storage_adjacency_merge_runs,
113
114            GraphWorkKind::CanonicalComparisonBytes => &mut self.canonical_comparison_bytes,
115            GraphWorkKind::CanonicalEncodingBytes => &mut self.canonical_encoding_bytes,
116            GraphWorkKind::WalCodecUnits => &mut self.wal_codec_units,
117            GraphWorkKind::EncodedWalBytes => &mut self.encoded_wal_bytes,
118            GraphWorkKind::ArtifactBytesWritten => &mut self.artifact_bytes_written,
119            GraphWorkKind::ArtifactWrites => &mut self.artifact_writes,
120            GraphWorkKind::WalBytesAppended => &mut self.wal_bytes_appended,
121            GraphWorkKind::WalAppends => &mut self.wal_appends,
122            GraphWorkKind::FullSyncAttempts => &mut self.full_sync_attempts,
123            GraphWorkKind::FullSyncSuccesses => &mut self.full_sync_successes,
124            GraphWorkKind::DirectorySyncAttempts => &mut self.directory_sync_attempts,
125            GraphWorkKind::DirectorySyncSuccesses => &mut self.directory_sync_successes,
126        };
127        *slot = slot.saturating_add(units);
128    }
129}
130
131impl GraphWorkLedger {
132    pub(crate) fn merge(&mut self, other: Self) {
133        self.storage_lookups = self.storage_lookups.saturating_add(other.storage_lookups);
134        self.storage_scans = self.storage_scans.saturating_add(other.storage_scans);
135        self.storage_adjacency_entries = self
136            .storage_adjacency_entries
137            .saturating_add(other.storage_adjacency_entries);
138        self.storage_copied_bytes = self
139            .storage_copied_bytes
140            .saturating_add(other.storage_copied_bytes);
141        self.storage_pages_decoded = self
142            .storage_pages_decoded
143            .saturating_add(other.storage_pages_decoded);
144        self.storage_pages_copied = self
145            .storage_pages_copied
146            .saturating_add(other.storage_pages_copied);
147        self.storage_property_values = self
148            .storage_property_values
149            .saturating_add(other.storage_property_values);
150        self.storage_property_bytes = self
151            .storage_property_bytes
152            .saturating_add(other.storage_property_bytes);
153        self.storage_adjacency_physical_entries = self
154            .storage_adjacency_physical_entries
155            .saturating_add(other.storage_adjacency_physical_entries);
156        self.storage_adjacency_merged_visits = self
157            .storage_adjacency_merged_visits
158            .saturating_add(other.storage_adjacency_merged_visits);
159        self.storage_adjacency_merge_runs = self
160            .storage_adjacency_merge_runs
161            .saturating_add(other.storage_adjacency_merge_runs);
162        self.canonical_comparison_bytes = self
163            .canonical_comparison_bytes
164            .saturating_add(other.canonical_comparison_bytes);
165        self.canonical_encoding_bytes = self
166            .canonical_encoding_bytes
167            .saturating_add(other.canonical_encoding_bytes);
168        self.wal_codec_units = self.wal_codec_units.saturating_add(other.wal_codec_units);
169        self.encoded_wal_bytes = self
170            .encoded_wal_bytes
171            .saturating_add(other.encoded_wal_bytes);
172        self.artifact_bytes_written = self
173            .artifact_bytes_written
174            .saturating_add(other.artifact_bytes_written);
175        self.artifact_writes = self.artifact_writes.saturating_add(other.artifact_writes);
176        self.wal_bytes_appended = self
177            .wal_bytes_appended
178            .saturating_add(other.wal_bytes_appended);
179        self.wal_appends = self.wal_appends.saturating_add(other.wal_appends);
180        self.full_sync_attempts = self
181            .full_sync_attempts
182            .saturating_add(other.full_sync_attempts);
183        self.full_sync_successes = self
184            .full_sync_successes
185            .saturating_add(other.full_sync_successes);
186        self.directory_sync_attempts = self
187            .directory_sync_attempts
188            .saturating_add(other.directory_sync_attempts);
189        self.directory_sync_successes = self
190            .directory_sync_successes
191            .saturating_add(other.directory_sync_successes);
192    }
193}
194#[allow(dead_code)]
195#[derive(Clone, Copy)]
196pub(crate) enum AllocationComponent {
197    Snapshot,
198    Active,
199    Wal,
200    Cache,
201    Temporary,
202    QueryPool,
203    NativeGraph,
204}
205
206impl AllocationComponent {
207    const fn name(self) -> &'static str {
208        match self {
209            Self::Snapshot => "snapshot",
210            Self::Active => "active segment",
211            Self::Wal => "wal",
212            Self::Cache => "cache",
213            Self::Temporary => "temporary",
214            Self::QueryPool => "query pool",
215            Self::NativeGraph => "native graph registry",
216        }
217    }
218
219    const fn is_temporary(self) -> bool {
220        matches!(self, Self::Temporary)
221    }
222}
223
224#[derive(Default)]
225struct AccountingState {
226    resident_owned_bytes: u64,
227    resident_peak_bytes: u64,
228    wal_bytes: u64,
229    cache_bytes: u64,
230    temporary_bytes: u64,
231    snapshot_bytes: u64,
232    active_bytes: u64,
233    query_pool_bytes: u64,
234    native_graph_bytes: u64,
235    mapped_bytes: u64,
236    scans_by_reason: [u64; crate::planner::SCAN_REASON_COUNTER_COUNT],
237    graph_segments_served: u64,
238    #[cfg(feature = "graph-cypher")]
239    allocation_interval_owner: u64,
240    #[cfg(feature = "graph-cypher")]
241    allocation_interval: Option<allocation_interval::AllocationIntervalState>,
242}
243
244pub(crate) struct Accounting {
245    budgets: Budgets,
246    state: Mutex<AccountingState>,
247    graph_work: Mutex<GraphWorkLedger>,
248    #[cfg(test)]
249    graph_work_merges: std::sync::atomic::AtomicU64,
250}
251
252impl Accounting {
253    pub(crate) const fn new(max_resident_bytes: u64, max_temp_bytes: u64) -> Self {
254        Self {
255            budgets: Budgets::new(max_resident_bytes, max_temp_bytes),
256            #[cfg(test)]
257            graph_work_merges: std::sync::atomic::AtomicU64::new(0),
258            graph_work: Mutex::new(GraphWorkLedger {
259                storage_lookups: 0,
260                storage_scans: 0,
261                storage_adjacency_entries: 0,
262                storage_copied_bytes: 0,
263                storage_pages_decoded: 0,
264                storage_pages_copied: 0,
265                storage_property_values: 0,
266                storage_property_bytes: 0,
267                storage_adjacency_physical_entries: 0,
268                storage_adjacency_merged_visits: 0,
269                storage_adjacency_merge_runs: 0,
270                canonical_comparison_bytes: 0,
271                canonical_encoding_bytes: 0,
272                wal_codec_units: 0,
273                encoded_wal_bytes: 0,
274                artifact_bytes_written: 0,
275                artifact_writes: 0,
276                wal_bytes_appended: 0,
277                wal_appends: 0,
278                full_sync_attempts: 0,
279                full_sync_successes: 0,
280                directory_sync_attempts: 0,
281                directory_sync_successes: 0,
282            }),
283            state: Mutex::new(AccountingState {
284                resident_owned_bytes: 0,
285                resident_peak_bytes: 0,
286                wal_bytes: 0,
287                cache_bytes: 0,
288                temporary_bytes: 0,
289                snapshot_bytes: 0,
290                active_bytes: 0,
291                query_pool_bytes: 0,
292                native_graph_bytes: 0,
293                mapped_bytes: 0,
294                scans_by_reason: [0; crate::planner::SCAN_REASON_COUNTER_COUNT],
295                graph_segments_served: 0,
296                #[cfg(feature = "graph-cypher")]
297                allocation_interval_owner: 0,
298                #[cfg(feature = "graph-cypher")]
299                allocation_interval: None,
300            }),
301        }
302    }
303
304    #[cfg(feature = "graph-cypher")]
305    pub(crate) const fn resident_limit(&self) -> u64 {
306        self.budgets.resident_limit()
307    }
308
309    #[cfg(feature = "graph-cypher")]
310    pub(crate) fn resident_peak_bytes(&self) -> Result<u64, StoreError> {
311        Ok(self
312            .state
313            .lock()
314            .map_err(|_| StoreError::Synchronization {
315                component: "memory accounting",
316            })?
317            .resident_peak_bytes)
318    }
319
320    fn reserve(
321        self: &Arc<Self>,
322        bytes: u64,
323        component: AllocationComponent,
324    ) -> Result<Reservation, StoreError> {
325        self.add(bytes, component)?;
326        Ok(Reservation {
327            accounting: Arc::clone(self),
328            bytes,
329            component,
330        })
331    }
332
333    fn add(&self, bytes: u64, component: AllocationComponent) -> Result<(), StoreError> {
334        let mut state = self.state.lock().map_err(|_| StoreError::Synchronization {
335            component: "memory accounting",
336        })?;
337        let (resident, temporary) = self.budgets.check(
338            state.resident_owned_bytes,
339            state.temporary_bytes,
340            bytes,
341            component.name(),
342            component.is_temporary(),
343        )?;
344        state.resident_owned_bytes = resident;
345        state.resident_peak_bytes = state.resident_peak_bytes.max(resident);
346        #[cfg(feature = "graph-cypher")]
347        if let Some(interval) = state.allocation_interval.as_mut() {
348            interval.peak_reserved_bytes = interval.peak_reserved_bytes.max(resident);
349        }
350        match component {
351            AllocationComponent::Snapshot => {
352                state.snapshot_bytes = state.snapshot_bytes.saturating_add(bytes);
353            }
354            AllocationComponent::Active => {
355                state.active_bytes = state.active_bytes.saturating_add(bytes);
356            }
357            AllocationComponent::Wal => state.wal_bytes = state.wal_bytes.saturating_add(bytes),
358            AllocationComponent::Cache => {
359                state.cache_bytes = state.cache_bytes.saturating_add(bytes);
360            }
361            AllocationComponent::Temporary => state.temporary_bytes = temporary,
362            AllocationComponent::QueryPool => {
363                state.query_pool_bytes = state.query_pool_bytes.saturating_add(bytes);
364            }
365            AllocationComponent::NativeGraph => {
366                state.native_graph_bytes = state.native_graph_bytes.saturating_add(bytes);
367            }
368        }
369        drop(state);
370        Ok(())
371    }
372
373    pub(crate) fn audit(&self) -> Result<AccountingAudit, StoreError> {
374        let state = self.state.lock().map_err(|_| StoreError::Synchronization {
375            component: "memory accounting",
376        })?;
377        Ok(AccountingAudit {
378            resident_owned_bytes: state.resident_owned_bytes,
379            wal_bytes: state.wal_bytes,
380            cache_bytes: state.cache_bytes,
381            temporary_bytes: state.temporary_bytes,
382            snapshot_bytes: state.snapshot_bytes,
383            active_bytes: state.active_bytes,
384            query_pool_bytes: state.query_pool_bytes,
385            native_graph_bytes: state.native_graph_bytes,
386            mapped_bytes: state.mapped_bytes,
387            scans_by_reason: state.scans_by_reason,
388            graph_segments_served: state.graph_segments_served,
389        })
390    }
391
392    #[cfg(feature = "graph-cypher")]
393    pub(crate) fn graph_resource_bytes(&self) -> Result<(u64, u64), StoreError> {
394        let state = self.state.lock().map_err(|_| StoreError::Synchronization {
395            component: "graph resources",
396        })?;
397        Ok((state.resident_owned_bytes, state.resident_peak_bytes))
398    }
399    #[cfg(any(test, feature = "test-seams"))]
400    pub(crate) fn graph_work(&self) -> GraphWorkLedger {
401        *self
402            .graph_work
403            .lock()
404            .unwrap_or_else(std::sync::PoisonError::into_inner)
405    }
406    pub(crate) fn merge_graph_work(&self, delta: GraphWorkLedger) {
407        #[cfg(test)]
408        self.graph_work_merges
409            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
410        self.graph_work
411            .lock()
412            .unwrap_or_else(std::sync::PoisonError::into_inner)
413            .merge(delta);
414    }
415    #[cfg(test)]
416    pub(crate) fn ze76_work_merges(&self) -> u64 {
417        self.graph_work_merges
418            .load(std::sync::atomic::Ordering::Relaxed)
419    }
420    #[cfg(test)]
421    #[allow(clippy::unwrap_used)]
422    pub(crate) fn ze76_overflow_work(&self) {
423        self.graph_work.lock().unwrap().wal_appends = u64::MAX;
424    }
425    #[cfg(test)]
426    #[allow(clippy::unwrap_used, clippy::panic)]
427    pub(crate) fn ze76_poison_work(&self) {
428        let _ = std::panic::catch_unwind(|| {
429            let _guard = self.graph_work.lock().unwrap();
430            panic!("test-only diagnostic lock poison");
431        });
432    }
433    pub(crate) fn record_plans(
434        &self,
435        plans: &[crate::planner::SegmentPlan],
436    ) -> Result<(), StoreError> {
437        let mut state = self.state.lock().map_err(|_| StoreError::Synchronization {
438            component: "query plan accounting",
439        })?;
440        let mut scans_by_reason = state.scans_by_reason;
441        let mut graph_segments_served = state.graph_segments_served;
442        for plan in plans {
443            if let Some(reason) = plan.scan_reason {
444                let slot = reason.counter().index();
445                let counter =
446                    scans_by_reason
447                        .get_mut(slot)
448                        .ok_or_else(|| StoreError::Statistics {
449                            component: "scan reason counter",
450                            source: std::io::Error::other(
451                                "scan reason slot is outside the registry",
452                            ),
453                        })?;
454                *counter = counter
455                    .checked_add(1)
456                    .ok_or_else(|| StoreError::Statistics {
457                        component: "scan reason counter",
458                        source: std::io::Error::other("scan reason counter overflow"),
459                    })?;
460            }
461            if matches!(
462                plan.branch,
463                crate::planner::SegmentBranch::Graph | crate::planner::SegmentBranch::FilteredGraph
464            ) {
465                graph_segments_served =
466                    graph_segments_served
467                        .checked_add(1)
468                        .ok_or_else(|| StoreError::Statistics {
469                            component: "graph segment counter",
470                            source: std::io::Error::other("graph segment counter overflow"),
471                        })?;
472            }
473        }
474        state.scans_by_reason = scans_by_reason;
475        state.graph_segments_served = graph_segments_served;
476        drop(state);
477        Ok(())
478    }
479
480    pub(crate) fn track_mapping(
481        self: &Arc<Self>,
482        bytes: u64,
483    ) -> Result<MappingReservation, StoreError> {
484        let mut state = self.state.lock().map_err(|_| StoreError::Synchronization {
485            component: "memory accounting",
486        })?;
487        state.mapped_bytes =
488            state
489                .mapped_bytes
490                .checked_add(bytes)
491                .ok_or_else(|| StoreError::Statistics {
492                    component: "mapped bytes",
493                    source: std::io::Error::other("mapped byte count overflow"),
494                })?;
495        drop(state);
496        Ok(MappingReservation {
497            accounting: Arc::clone(self),
498            bytes,
499        })
500    }
501}
502
503#[derive(Clone, Copy)]
504pub(crate) struct AccountingAudit {
505    pub(crate) resident_owned_bytes: u64,
506    pub(crate) wal_bytes: u64,
507    pub(crate) cache_bytes: u64,
508    pub(crate) temporary_bytes: u64,
509    pub(crate) snapshot_bytes: u64,
510    pub(crate) active_bytes: u64,
511    pub(crate) query_pool_bytes: u64,
512    pub(crate) native_graph_bytes: u64,
513    pub(crate) mapped_bytes: u64,
514    pub(crate) scans_by_reason: [u64; crate::planner::SCAN_REASON_COUNTER_COUNT],
515    pub(crate) graph_segments_served: u64,
516}
517
518impl AccountingAudit {
519    pub(crate) const fn component_sum(self) -> u64 {
520        self.wal_bytes
521            .saturating_add(self.cache_bytes)
522            .saturating_add(self.temporary_bytes)
523            .saturating_add(self.snapshot_bytes)
524            .saturating_add(self.active_bytes)
525            .saturating_add(self.query_pool_bytes)
526            .saturating_add(self.native_graph_bytes)
527    }
528}
529
530pub(crate) struct MappingReservation {
531    accounting: Arc<Accounting>,
532    bytes: u64,
533}
534
535impl Drop for MappingReservation {
536    fn drop(&mut self) {
537        let mut state = match self.accounting.state.lock() {
538            Ok(state) => state,
539            Err(poisoned) => poisoned.into_inner(),
540        };
541        state.mapped_bytes = state.mapped_bytes.saturating_sub(self.bytes);
542    }
543}
544
545struct Reservation {
546    accounting: Arc<Accounting>,
547    bytes: u64,
548    component: AllocationComponent,
549}
550
551impl Reservation {
552    fn grow(&mut self, additional_bytes: u64) -> Result<(), StoreError> {
553        self.accounting.add(additional_bytes, self.component)?;
554        self.bytes =
555            self.bytes
556                .checked_add(additional_bytes)
557                .ok_or(StoreError::BudgetExceeded {
558                    needed: u64::MAX,
559                    budget: u64::MAX,
560                    component: self.component.name(),
561                })?;
562        Ok(())
563    }
564
565    fn shrink(&mut self, released_bytes: u64) {
566        let released = released_bytes.min(self.bytes);
567        self.bytes = self.bytes.saturating_sub(released);
568        let mut state = match self.accounting.state.lock() {
569            Ok(state) => state,
570            Err(poisoned) => poisoned.into_inner(),
571        };
572        state.resident_owned_bytes = state.resident_owned_bytes.saturating_sub(released);
573        match self.component {
574            AllocationComponent::Snapshot => {
575                state.snapshot_bytes = state.snapshot_bytes.saturating_sub(released);
576            }
577            AllocationComponent::Active => {
578                state.active_bytes = state.active_bytes.saturating_sub(released);
579            }
580            AllocationComponent::Wal => {
581                state.wal_bytes = state.wal_bytes.saturating_sub(released);
582            }
583            AllocationComponent::Cache => {
584                state.cache_bytes = state.cache_bytes.saturating_sub(released);
585            }
586            AllocationComponent::Temporary => {
587                state.temporary_bytes = state.temporary_bytes.saturating_sub(released);
588            }
589            AllocationComponent::QueryPool => {
590                state.query_pool_bytes = state.query_pool_bytes.saturating_sub(released);
591            }
592            AllocationComponent::NativeGraph => {
593                state.native_graph_bytes = state.native_graph_bytes.saturating_sub(released);
594            }
595        }
596    }
597}
598
599impl Drop for Reservation {
600    fn drop(&mut self) {
601        let mut state = match self.accounting.state.lock() {
602            Ok(state) => state,
603            Err(poisoned) => poisoned.into_inner(),
604        };
605        state.resident_owned_bytes = state.resident_owned_bytes.saturating_sub(self.bytes);
606        match self.component {
607            AllocationComponent::Snapshot => {
608                state.snapshot_bytes = state.snapshot_bytes.saturating_sub(self.bytes);
609            }
610            AllocationComponent::Active => {
611                state.active_bytes = state.active_bytes.saturating_sub(self.bytes);
612            }
613            AllocationComponent::Wal => {
614                state.wal_bytes = state.wal_bytes.saturating_sub(self.bytes);
615            }
616            AllocationComponent::Cache => {
617                state.cache_bytes = state.cache_bytes.saturating_sub(self.bytes);
618            }
619            AllocationComponent::Temporary => {
620                state.temporary_bytes = state.temporary_bytes.saturating_sub(self.bytes);
621            }
622            AllocationComponent::QueryPool => {
623                state.query_pool_bytes = state.query_pool_bytes.saturating_sub(self.bytes);
624            }
625            AllocationComponent::NativeGraph => {
626                state.native_graph_bytes = state.native_graph_bytes.saturating_sub(self.bytes);
627            }
628        }
629    }
630}
631
632/// Exact non-allocating component bytes whose backing allocation is owned by
633/// another subsystem, such as the WAL's shared encoded record buffers.
634pub(crate) struct AccountedCounter {
635    reservation: Reservation,
636}
637
638impl AccountedCounter {
639    pub(crate) fn new(
640        accounting: &Arc<Accounting>,
641        component: AllocationComponent,
642    ) -> Result<Self, StoreError> {
643        Ok(Self {
644            reservation: accounting.reserve(0, component)?,
645        })
646    }
647
648    pub(crate) fn set(&mut self, bytes: usize) -> Result<(), StoreError> {
649        let bytes = u64::try_from(bytes).map_err(|_| StoreError::BudgetExceeded {
650            needed: u64::MAX,
651            budget: u64::MAX,
652            component: self.reservation.component.name(),
653        })?;
654        if bytes >= self.reservation.bytes {
655            return self
656                .reservation
657                .grow(bytes.saturating_sub(self.reservation.bytes));
658        }
659        self.reservation
660            .shrink(self.reservation.bytes.saturating_sub(bytes));
661        Ok(())
662    }
663
664    pub(crate) const fn bytes(&self) -> u64 {
665        self.reservation.bytes
666    }
667
668    #[cfg(feature = "graph-cypher")]
669    pub(crate) fn belongs_to(&self, accounting: &Arc<Accounting>) -> bool {
670        Arc::ptr_eq(&self.reservation.accounting, accounting)
671    }
672}
673
674/// An owned value whose exact requested allocation bytes have one component owner.
675pub(crate) struct Accounted<T> {
676    value: T,
677    _reservation: Option<Reservation>,
678    element_limit: Option<usize>,
679}
680
681impl<T> Deref for Accounted<T> {
682    type Target = T;
683
684    fn deref(&self) -> &Self::Target {
685        &self.value
686    }
687}
688
689impl<T> Accounted<Vec<T>> {
690    pub(crate) fn unaccounted_empty() -> Self {
691        Self {
692            value: Vec::new(),
693            _reservation: None,
694            element_limit: None,
695        }
696    }
697
698    pub(crate) fn try_with_capacity(
699        accounting: &Arc<Accounting>,
700        capacity: usize,
701        component: AllocationComponent,
702    ) -> Result<Self, StoreError> {
703        let requested =
704            capacity
705                .checked_mul(std::mem::size_of::<T>())
706                .ok_or(StoreError::BudgetExceeded {
707                    needed: u64::MAX,
708                    budget: u64::MAX,
709                    component: component.name(),
710                })?;
711        let bytes = u64::try_from(requested).map_err(|_| StoreError::BudgetExceeded {
712            needed: u64::MAX,
713            budget: u64::MAX,
714            component: component.name(),
715        })?;
716        let reservation = accounting.reserve(bytes, component)?;
717        let mut value = Vec::new();
718        #[cfg(feature = "allocation-audit")]
719        let allocation = crate::allocation_audit::attributed(|| value.try_reserve_exact(capacity));
720        #[cfg(not(feature = "allocation-audit"))]
721        let allocation = value.try_reserve_exact(capacity);
722        allocation.map_err(|_| StoreError::AllocationFailed {
723            needed: bytes,
724            component: component.name(),
725        })?;
726        Ok(Self {
727            value,
728            _reservation: Some(reservation),
729            element_limit: Some(capacity),
730        })
731    }
732
733    pub(crate) fn try_from_vec(
734        accounting: &Arc<Accounting>,
735        value: Vec<T>,
736        component: AllocationComponent,
737    ) -> Result<Self, StoreError> {
738        let capacity = value.capacity();
739        let requested =
740            capacity
741                .checked_mul(std::mem::size_of::<T>())
742                .ok_or(StoreError::BudgetExceeded {
743                    needed: u64::MAX,
744                    budget: u64::MAX,
745                    component: component.name(),
746                })?;
747        let bytes = u64::try_from(requested).map_err(|_| StoreError::BudgetExceeded {
748            needed: u64::MAX,
749            budget: u64::MAX,
750            component: component.name(),
751        })?;
752        let reservation = accounting.reserve(bytes, component)?;
753        Ok(Self {
754            value,
755            _reservation: Some(reservation),
756            element_limit: Some(capacity),
757        })
758    }
759
760    pub(crate) fn resident_bytes(&self) -> u64 {
761        self._reservation
762            .as_ref()
763            .map_or(0, |reservation| reservation.bytes)
764    }
765
766    /// Grows retained capacity while charging both old and replacement
767    /// allocations until the old buffer has actually been released.
768    pub(crate) fn try_reserve_total(
769        &mut self,
770        accounting: &Arc<Accounting>,
771        capacity: usize,
772        component: AllocationComponent,
773    ) -> Result<(), StoreError> {
774        if capacity <= self.element_limit.unwrap_or(0) {
775            return Ok(());
776        }
777        let mut replacement = Self::try_with_capacity(accounting, capacity, component)?;
778        if self.value.len() > capacity {
779            return Err(StoreError::AllocationFailed {
780                needed: self.value.len() as u64,
781                component: "accounted vector capacity",
782            });
783        }
784        // The destination was reserved for every element. Moving them keeps
785        // nested allocation owners intact and cannot trigger another growth.
786        replacement.value.append(&mut self.value);
787        *self = replacement;
788        Ok(())
789    }
790
791    pub(crate) fn push(&mut self, value: T) -> Result<(), StoreError> {
792        let accounted_elements = self.element_limit.unwrap_or(usize::MAX);
793        if self.value.len() >= accounted_elements || self.value.len() == self.value.capacity() {
794            return Err(StoreError::AllocationFailed {
795                needed: u64::try_from(self.value.len().saturating_add(1)).unwrap_or(u64::MAX),
796                component: "accounted vector capacity",
797            });
798        }
799        self.value.push(value);
800        Ok(())
801    }
802
803    pub(crate) fn extend_from_slice(&mut self, values: &[T]) -> Result<(), StoreError>
804    where
805        T: Clone,
806    {
807        let new_len =
808            self.value
809                .len()
810                .checked_add(values.len())
811                .ok_or(StoreError::AllocationFailed {
812                    needed: u64::MAX,
813                    component: "accounted vector capacity",
814                })?;
815        let accounted_elements = self.element_limit.unwrap_or(usize::MAX);
816        if new_len > accounted_elements || new_len > self.value.capacity() {
817            return Err(StoreError::AllocationFailed {
818                needed: u64::try_from(new_len).unwrap_or(u64::MAX),
819                component: "accounted vector capacity",
820            });
821        }
822        self.value.extend_from_slice(values);
823        Ok(())
824    }
825
826    pub(crate) fn replace_range(
827        &mut self,
828        range: std::ops::Range<usize>,
829        replacement: &[T],
830    ) -> Result<(), StoreError>
831    where
832        T: Copy + Default,
833    {
834        let old_len = self.value.len();
835        if range.start > range.end || range.end > old_len {
836            return Err(StoreError::ActiveRowOverflow);
837        }
838        let removed = range.end.saturating_sub(range.start);
839        let new_len = old_len
840            .checked_sub(removed)
841            .and_then(|length| length.checked_add(replacement.len()))
842            .ok_or(StoreError::ActiveRowOverflow)?;
843        let accounted_elements = self.element_limit.unwrap_or(usize::MAX);
844        if new_len > accounted_elements || new_len > self.value.capacity() {
845            return Err(StoreError::AllocationFailed {
846                needed: u64::try_from(new_len).unwrap_or(u64::MAX),
847                component: "accounted vector capacity",
848            });
849        }
850        if new_len > old_len {
851            self.value.resize(new_len, T::default());
852        }
853        let replacement_end = range
854            .start
855            .checked_add(replacement.len())
856            .ok_or(StoreError::ActiveRowOverflow)?;
857        self.value.copy_within(range.end..old_len, replacement_end);
858        self.value
859            .get_mut(range.start..replacement_end)
860            .ok_or(StoreError::ActiveRowOverflow)?
861            .copy_from_slice(replacement);
862        self.value.truncate(new_len);
863        Ok(())
864    }
865
866    pub(crate) fn as_mut_slice(&mut self) -> &mut [T] {
867        self.value.as_mut_slice()
868    }
869
870    /// Drops elements while retaining the complete accounted allocation.
871    pub(crate) fn clear(&mut self) {
872        self.value.clear();
873    }
874
875    pub(crate) fn dedup(&mut self)
876    where
877        T: PartialEq,
878    {
879        self.value.dedup();
880    }
881
882    pub(crate) fn reserve_additional_bytes(&mut self, additional: u64) -> Result<(), StoreError> {
883        match self._reservation.as_mut() {
884            Some(reservation) => reservation.grow(additional),
885            None if additional == 0 => Ok(()),
886            None => Err(StoreError::AllocationFailed {
887                needed: additional,
888                component: "unaccounted vector",
889            }),
890        }
891    }
892}
893
894#[cfg(test)]
895impl Accounted<Vec<u8>> {
896    fn try_zeroed(
897        accounting: &Arc<Accounting>,
898        bytes: u64,
899        component: AllocationComponent,
900    ) -> Result<Self, StoreError> {
901        let capacity = usize::try_from(bytes).map_err(|_| StoreError::BudgetExceeded {
902            needed: bytes,
903            budget: usize::MAX as u64,
904            component: component.name(),
905        })?;
906        let mut accounted = Self::try_with_capacity(accounting, capacity, component)?;
907        accounted.value.resize(capacity, 0);
908        Ok(accounted)
909    }
910}
911
912/// One point-in-time snapshot of resources owned by an open [`Store`].
913#[derive(Clone, Copy, Debug, Eq, PartialEq)]
914pub struct Stats {
915    /// Exact bytes in engine-owned anonymous allocation arenas and vectors.
916    pub resident_owned_bytes: u64,
917    /// Exact bytes retained by published snapshot metadata and decoded views.
918    pub snapshot_bytes: u64,
919    /// Exact byte length of the store's live read-only mappings.
920    pub mapped_bytes: u64,
921    /// Bytes in resident mapped pages, counted by `mincore`.
922    pub mapped_resident_bytes: u64,
923    /// Exact bytes in immutable segment mappings.
924    pub segment_bytes: u64,
925    /// Exact allocation capacity held by the currently published in-RAM active
926    /// segment.
927    pub active_segment_bytes: u64,
928    /// Exact allocation capacity held by retired active generations that are
929    /// still pinned by admitted queries.
930    pub retired_active_segment_bytes: u64,
931    /// Exact number of rows currently represented by the in-RAM active segment.
932    pub active_row_count: u64,
933    /// Exact number of in-memory active-segment tombstones.
934    pub tombstone_count: u64,
935    /// Exact bytes owned by active-segment tombstones.
936    pub tombstone_bytes: u64,
937    /// Exact bytes retained by the store's in-memory WAL component. This is
938    /// zero while [`Store`] has no WAL writer component.
939    pub wal_bytes: u64,
940    /// Exact bytes owned by reusable per-segment graph-search scratch caches.
941    pub cache_bytes: u64,
942    /// Exact bytes in live, explicitly accounted temporary allocations.
943    pub temporary_bytes: u64,
944    /// Exact capacity in bytes of the persistent query pool's worker registry.
945    pub query_pool_bytes: u64,
946    /// Exact capacity in bytes of the native graph publication's store-lifetime
947    /// lease, mapping, preparation, and spill registries. This is zero while
948    /// [`Store`] has no native graph publication.
949    pub native_graph_bytes: u64,
950    /// Exact number of file descriptors retained by this store handle.
951    pub open_files: u64,
952    /// Exact number of store-admitted queries that have not yet returned.
953    pub active_queries: u64,
954    /// Exact number of snapshot leases held outside the published snapshot slot.
955    pub active_snapshot_leases: u64,
956    /// Darwin's `TASK_VM_INFO` physical-footprint kernel counter, or `None` on
957    /// platforms where that counter does not exist.
958    pub phys_footprint: Option<u64>,
959    /// Monotonic count of successfully executed scan plans by typed reason.
960    pub scans_by_reason: [u64; crate::planner::SCAN_REASON_COUNTER_COUNT],
961    /// Monotonic count of segment plans served by graph traversal.
962    pub graph_segments_served: u64,
963}
964
965impl Store {
966    /// Returns exact accounting and kernel counters for this open handle.
967    ///
968    /// This operation never estimates. Engine-owned counters are maintained at
969    /// allocation/mapping boundaries, mapped residency comes from `mincore`,
970    /// and `phys_footprint` comes from Darwin `TASK_VM_INFO`.
971    pub fn stats(&self) -> Result<Stats, StoreError> {
972        let state = self
973            .state
974            .lock()
975            .map_err(|_| StoreError::Synchronization { component: "state" })?;
976        match *state {
977            StoreState::Open => {}
978            StoreState::Closing => return Err(StoreError::Closing),
979            StoreState::Closed => return Err(StoreError::Closed),
980        }
981        let stats = self.stats_while_open();
982        drop(state);
983        stats
984    }
985
986    pub(crate) fn stats_while_open(&self) -> Result<Stats, StoreError> {
987        let writer_lock = self
988            .writer_lock
989            .lock()
990            .map_err(|_| StoreError::Synchronization {
991                component: "writer lock",
992            })?;
993        let wal_writer = self
994            .wal_writer
995            .lock()
996            .map_err(|_| StoreError::Synchronization {
997                component: "WAL writer",
998            })?;
999        let base_open_files =
1000            u64::from(writer_lock.is_some()).saturating_add(u64::from(wal_writer.is_some()));
1001        let active_guard = self
1002            .active
1003            .lock()
1004            .map_err(|_| StoreError::Synchronization {
1005                component: "active segment",
1006            })?;
1007        #[cfg(feature = "graph-cypher")]
1008        let native_only = active_guard.is_none() && self.native_graph.has_publication()?;
1009        #[cfg(not(feature = "graph-cypher"))]
1010        let native_only = false;
1011        let (active_segment_bytes, active_row_count, tombstone_count, tombstone_bytes) =
1012            if let Some(active_state) = active_guard.as_ref() {
1013                let rows = u64::try_from(active_state.segment.row_count()).map_err(|_| {
1014                    StoreError::Statistics {
1015                        component: "active row count",
1016                        source: std::io::Error::other("active row count exceeds u64"),
1017                    }
1018                })?;
1019                (
1020                    active_state.segment.resident_bytes(),
1021                    rows,
1022                    active_state.segment.tombstone_count(),
1023                    active_state.segment.tombstone_bytes(),
1024                )
1025            } else if native_only {
1026                (0, 0, 0, 0)
1027            } else {
1028                return Err(StoreError::Closed);
1029            };
1030        let active_queries = self
1031            .active_queries
1032            .load(std::sync::atomic::Ordering::Relaxed);
1033        let accounting = self.accounting.audit()?;
1034        if accounting.resident_owned_bytes != accounting.component_sum() {
1035            return Err(StoreError::Statistics {
1036                component: "resident accounting conservation",
1037                source: std::io::Error::other(
1038                    "resident-owned total disagrees with component counters",
1039                ),
1040            });
1041        }
1042        let retired_active_segment_bytes = accounting
1043            .active_bytes
1044            .checked_sub(active_segment_bytes)
1045            .ok_or_else(|| StoreError::Statistics {
1046                component: "active segment accounting",
1047                source: std::io::Error::other(
1048                    "current active segment capacity exceeds the accounting component",
1049                ),
1050            })?;
1051        let snapshot_guard = self
1052            .snapshot
1053            .read()
1054            .map_err(|_| StoreError::Synchronization {
1055                component: "published snapshot",
1056            })?;
1057        let snapshot = snapshot_guard.as_ref();
1058        if snapshot.is_none() != native_only {
1059            return Err(StoreError::Closed);
1060        }
1061        let active_snapshot_leases = if let Some(snapshot) = snapshot {
1062            u64::try_from(Arc::strong_count(snapshot).saturating_sub(1)).map_err(|_| {
1063                StoreError::Statistics {
1064                    component: "active snapshot leases",
1065                    source: std::io::Error::other("snapshot lease count exceeds u64"),
1066                }
1067            })?
1068        } else {
1069            0
1070        };
1071        let mut mapped_resident_bytes = 0_u64;
1072        let mut segment_bytes = 0_u64;
1073        for segment in snapshot
1074            .into_iter()
1075            .flat_map(|snapshot| snapshot.all_segments())
1076        {
1077            let bytes =
1078                u64::try_from(segment.mapped_bytes()).map_err(|_| StoreError::Statistics {
1079                    component: "segment mapped bytes",
1080                    source: std::io::Error::other("segment mapped byte count exceeds u64"),
1081                })?;
1082            segment_bytes =
1083                segment_bytes
1084                    .checked_add(bytes)
1085                    .ok_or_else(|| StoreError::Statistics {
1086                        component: "segment mapped bytes",
1087                        source: std::io::Error::other("segment mapped byte count overflow"),
1088                    })?;
1089            let resident =
1090                segment
1091                    .mapped_resident_bytes()
1092                    .map_err(|source| StoreError::Statistics {
1093                        component: "mapped resident bytes",
1094                        source,
1095                    })?;
1096            mapped_resident_bytes =
1097                mapped_resident_bytes.checked_add(resident).ok_or_else(|| {
1098                    StoreError::Statistics {
1099                        component: "mapped resident bytes",
1100                        source: std::io::Error::other("mapped resident byte count overflow"),
1101                    }
1102                })?;
1103        }
1104        #[cfg(feature = "graph-cypher")]
1105        let (native_mapping_count, native_resident_bytes) = self.native_graph.mapping_stats()?;
1106        #[cfg(not(feature = "graph-cypher"))]
1107        let (native_mapping_count, native_resident_bytes) = (0, 0);
1108        mapped_resident_bytes = mapped_resident_bytes
1109            .checked_add(native_resident_bytes)
1110            .ok_or_else(|| StoreError::Statistics {
1111                component: "mapped resident bytes",
1112                source: std::io::Error::other("mapped resident byte count overflow"),
1113            })?;
1114        let open_files = base_open_files
1115            .checked_add(native_mapping_count)
1116            .ok_or_else(|| StoreError::Statistics {
1117                component: "open file count",
1118                source: std::io::Error::other("open file count overflow"),
1119            })?;
1120        #[cfg(any(target_os = "macos", target_os = "ios"))]
1121        let phys_footprint =
1122            Some(
1123                crate::sys::darwin::phys_footprint().map_err(|error| StoreError::Statistics {
1124                    component: "phys_footprint",
1125                    source: std::io::Error::other(error),
1126                })?,
1127            );
1128        #[cfg(not(any(target_os = "macos", target_os = "ios")))]
1129        let phys_footprint = None;
1130        drop(snapshot_guard);
1131        drop(active_guard);
1132        drop(wal_writer);
1133        drop(writer_lock);
1134        Ok(Stats {
1135            resident_owned_bytes: accounting.resident_owned_bytes,
1136            snapshot_bytes: accounting.snapshot_bytes,
1137            mapped_bytes: accounting.mapped_bytes,
1138            mapped_resident_bytes,
1139            segment_bytes,
1140            active_segment_bytes,
1141            retired_active_segment_bytes,
1142            active_row_count,
1143            tombstone_count,
1144            tombstone_bytes,
1145            wal_bytes: accounting.wal_bytes,
1146            cache_bytes: accounting.cache_bytes,
1147            temporary_bytes: accounting.temporary_bytes,
1148            query_pool_bytes: accounting.query_pool_bytes,
1149            native_graph_bytes: accounting.native_graph_bytes,
1150            open_files,
1151            active_queries,
1152            active_snapshot_leases,
1153            phys_footprint,
1154            scans_by_reason: accounting.scans_by_reason,
1155            graph_segments_served: accounting.graph_segments_served,
1156        })
1157    }
1158
1159    #[cfg(test)]
1160    fn allocate_bytes(
1161        &self,
1162        bytes: u64,
1163        component: AllocationComponent,
1164    ) -> Result<Accounted<Vec<u8>>, StoreError> {
1165        let state = self
1166            .state
1167            .lock()
1168            .map_err(|_| StoreError::Synchronization { component: "state" })?;
1169        match *state {
1170            StoreState::Open => {}
1171            StoreState::Closing => return Err(StoreError::Closing),
1172            StoreState::Closed => return Err(StoreError::Closed),
1173        }
1174        let allocation = Accounted::try_zeroed(&self.accounting, bytes, component)?;
1175        drop(state);
1176        Ok(allocation)
1177    }
1178}
1179
1180#[cfg(test)]
1181#[allow(clippy::expect_used)]
1182mod tests {
1183    use tempfile::{TempDir, tempdir};
1184
1185    use super::super::{CancelToken, OpenOptions, QueryControl, Store, StoreError};
1186    use super::{Accounted, Accounting, AllocationComponent};
1187    use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
1188    use crate::manifest::Manifest;
1189    use crate::manifest::io::commit_manifest;
1190    use crate::meta::{AliveSet, ColumnStoreBuilder, Schema};
1191    use crate::quant::Bit4Factors;
1192    use crate::scan::{F32Rows, ScanOptions, ScanQuery, ScanRequest, ScanRows};
1193    use crate::segment::SegmentId;
1194    use crate::segment::layout::RegionEntry;
1195    use crate::segment::reader::SegmentReader;
1196    use crate::segment::writer::{SegmentBuild, SegmentFactors, write_segment};
1197    use crate::vfs::StdVfs;
1198
1199    #[cfg(feature = "graph-cypher")]
1200    #[test]
1201    fn ze204_native_only_stats_succeed_until_close() {
1202        let directory = tempdir().expect("directory");
1203        let store = Store::create_native_graph(
1204            directory.path().join("native"),
1205            OpenOptions::new()
1206                .with_durability(DurabilityMode::Durable, CommitTier::Durable)
1207                .with_max_resident_bytes(256 * 1024 * 1024),
1208            None,
1209        )
1210        .expect("native store");
1211        assert!(store.active.lock().expect("active").is_none());
1212        assert!(store.snapshot.read().expect("snapshot").is_none());
1213        let stats = store.stats().expect("open native stats");
1214        assert_eq!(stats.segment_bytes, 0);
1215        assert_eq!(stats.active_segment_bytes, 0);
1216        assert_eq!(stats.retired_active_segment_bytes, 0);
1217        assert_eq!(stats.active_row_count, 0);
1218        assert_eq!(stats.tombstone_count, 0);
1219        assert_eq!(stats.tombstone_bytes, 0);
1220        assert_eq!(stats.snapshot_bytes, 0);
1221        assert_eq!(stats.active_snapshot_leases, 0);
1222        assert!(stats.native_graph_bytes > 0);
1223        assert_eq!(
1224            stats.resident_owned_bytes,
1225            store.accounting.audit().expect("audit").component_sum()
1226        );
1227        store.close().expect("close");
1228        assert!(matches!(store.stats(), Err(StoreError::Closed)));
1229    }
1230
1231    #[test]
1232    fn stats_bytes_are_conserved() {
1233        let directory = tempdir().expect("store directory");
1234        let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
1235        let baseline = store.stats().expect("baseline stats");
1236        let wal = store
1237            .allocate_bytes(37, AllocationComponent::Wal)
1238            .expect("accounted WAL bytes");
1239        let cache = store
1240            .allocate_bytes(19, AllocationComponent::Cache)
1241            .expect("accounted cache bytes");
1242        let temporary = store
1243            .allocate_bytes(11, AllocationComponent::Temporary)
1244            .expect("accounted temporary bytes");
1245
1246        let stats = store.stats().expect("stats");
1247        let audit = store.accounting.audit().expect("accounting audit");
1248        assert_eq!(stats.wal_bytes, 37);
1249        assert_eq!(stats.cache_bytes, 19);
1250        assert_eq!(stats.temporary_bytes, 11);
1251        assert_eq!(stats.retired_active_segment_bytes, 0);
1252        assert_eq!(
1253            stats.resident_owned_bytes,
1254            baseline.resident_owned_bytes + 67
1255        );
1256        assert_eq!(stats.resident_owned_bytes, audit.component_sum());
1257        assert_eq!(stats.resident_owned_bytes, audit.resident_owned_bytes);
1258
1259        drop((wal, cache, temporary));
1260        assert_eq!(
1261            store.stats().expect("released stats").resident_owned_bytes,
1262            baseline.resident_owned_bytes
1263        );
1264
1265        let published = published_store();
1266        let mapped = Store::open(published.path(), OpenOptions::default()).expect("mapped open");
1267        let snapshot = mapped.snapshot().expect("mapped snapshot");
1268        let segment = snapshot.segments().first().expect("fixture segment");
1269        let expected_snapshot_bytes = std::mem::size_of::<SegmentReader>()
1270            + std::mem::size_of_val(segment.directory())
1271            + segment.retained_validation_bytes()
1272            + usize::try_from(baseline.snapshot_bytes).expect("baseline snapshot bytes fit usize")
1273            // Store-lifetime components are resident from open, not just the
1274            // mapped snapshot. Zero unless `graph-cypher` is enabled.
1275            + usize::try_from(baseline.native_graph_bytes)
1276                .expect("baseline native graph bytes fit usize");
1277        drop(snapshot);
1278        let mapped_stats = mapped.stats().expect("mapped stats");
1279        assert_eq!(
1280            mapped_stats.resident_owned_bytes,
1281            expected_snapshot_bytes as u64
1282        );
1283        assert_eq!(
1284            mapped_stats.resident_owned_bytes,
1285            mapped
1286                .accounting
1287                .audit()
1288                .expect("mapped accounting audit")
1289                .component_sum()
1290        );
1291        assert_eq!(mapped_stats.tombstone_count, 0);
1292        assert_eq!(mapped_stats.tombstone_bytes, 0);
1293        assert_eq!(mapped_stats.wal_bytes, 0);
1294        assert_eq!(mapped_stats.cache_bytes, 0);
1295        assert_eq!(mapped_stats.temporary_bytes, 0);
1296        assert_eq!(mapped_stats.open_files, 2);
1297        #[cfg(any(target_os = "macos", target_os = "ios"))]
1298        assert!(mapped_stats.phys_footprint.is_some_and(|bytes| bytes > 0));
1299
1300        mapped.close().expect("close mapped store");
1301        assert_eq!(
1302            mapped
1303                .accounting
1304                .audit()
1305                .expect("closed mapping accounting")
1306                .mapped_bytes,
1307            0,
1308            "close must release the exact mapped-byte accounting"
1309        );
1310        assert!(matches!(mapped.stats(), Err(StoreError::Closed)));
1311    }
1312
1313    #[test]
1314    fn repeated_close_returns_exact_stats_counters_to_pre_open_baseline() {
1315        const ITERATIONS: usize = 20;
1316        const PRE_OPEN_MAPPED_BYTES: u64 = 0;
1317        const PRE_OPEN_RESIDENT_OWNED_BYTES: u64 = 0;
1318
1319        let published = published_store();
1320        for iteration in 0..ITERATIONS {
1321            let store = Store::open(published.path(), OpenOptions::default()).expect("mapped open");
1322            let query = [1.0_f32];
1323            let rows = F32Rows::new(vec![1.0_f32; 128]);
1324            store
1325                .top_k_with_options(
1326                    ScanRequest {
1327                        query: ScanQuery::F32(&query),
1328                        rows: ScanRows::F32RowMajor(&rows),
1329                        row_mask: None,
1330                    },
1331                    1,
1332                    ScanOptions { thread_budget: 1 },
1333                    QueryControl::Cancel(CancelToken::new()),
1334                )
1335                .expect("start the persistent query pool");
1336            let live = store.stats().expect("live stats");
1337            assert!(live.mapped_bytes > PRE_OPEN_MAPPED_BYTES);
1338            assert!(
1339                live.resident_owned_bytes > PRE_OPEN_RESIDENT_OWNED_BYTES,
1340                "iteration {iteration} must own mapped-snapshot bookkeeping"
1341            );
1342            assert!(
1343                live.query_pool_bytes > 0,
1344                "iteration {iteration} must own an accounted query pool"
1345            );
1346
1347            store.close().expect("close mapped store");
1348
1349            let closed = store
1350                .accounting
1351                .audit()
1352                .expect("post-close accounting source for Stats");
1353            assert_eq!(
1354                closed.mapped_bytes, PRE_OPEN_MAPPED_BYTES,
1355                "Stats::mapped_bytes source leaked at iteration {iteration}"
1356            );
1357            assert_eq!(
1358                closed.resident_owned_bytes, PRE_OPEN_RESIDENT_OWNED_BYTES,
1359                "Stats::resident_owned_bytes source leaked at iteration {iteration}"
1360            );
1361            assert_eq!(
1362                closed.query_pool_bytes, 0,
1363                "Stats::query_pool_bytes source leaked at iteration {iteration}"
1364            );
1365        }
1366    }
1367
1368    #[test]
1369    fn budget_exceeded_is_typed_and_pre_allocation() {
1370        let directory = tempdir().expect("store directory");
1371        let baseline_store =
1372            Store::open(directory.path(), OpenOptions::default()).expect("baseline open");
1373        // The resident total, not just the snapshot component: a store-lifetime
1374        // component such as the native graph registry is resident from open.
1375        let baseline = baseline_store
1376            .stats()
1377            .expect("baseline stats")
1378            .resident_owned_bytes;
1379        baseline_store.close().expect("close baseline");
1380        let options = OpenOptions::new()
1381            .with_max_resident_bytes(baseline + 64)
1382            .with_max_temp_bytes(32);
1383        let store = Store::open(directory.path(), options).expect("open");
1384
1385        let rejected = store.allocate_bytes(40, AllocationComponent::Temporary);
1386        assert!(matches!(
1387            rejected,
1388            Err(StoreError::BudgetExceeded {
1389                needed: 40,
1390                budget: 32,
1391                component: "temporary",
1392            })
1393        ));
1394        let after_rejection = store.stats().expect("stats after rejection");
1395        assert_eq!(after_rejection.resident_owned_bytes, baseline);
1396        assert_eq!(after_rejection.temporary_bytes, 0);
1397
1398        let accepted = store
1399            .allocate_bytes(24, AllocationComponent::Temporary)
1400            .expect("smaller allocation on same handle");
1401        assert_eq!(accepted.len(), 24);
1402        assert_eq!(store.stats().expect("usable stats").temporary_bytes, 24);
1403        assert!(matches!(
1404            store.allocate_bytes(50, AllocationComponent::Wal),
1405            Err(StoreError::BudgetExceeded {
1406                needed,
1407                budget,
1408                component: "wal",
1409            }) if needed == baseline + 74 && budget == baseline + 64
1410        ));
1411        assert_eq!(
1412            store.stats().expect("resident rejection stats").wal_bytes,
1413            0
1414        );
1415        store.snapshot().expect("same handle remains usable");
1416
1417        let published = published_store();
1418        let snapshot_bytes =
1419            std::mem::size_of::<SegmentReader>() + 6 * std::mem::size_of::<RegionEntry>();
1420        let budget =
1421            u64::try_from(snapshot_bytes.saturating_sub(1)).expect("snapshot budget fits u64");
1422        let needed = u64::try_from(snapshot_bytes).expect("snapshot bytes fit u64");
1423        let rejected_open = Store::open(
1424            published.path(),
1425            OpenOptions::new().with_max_resident_bytes(budget),
1426        );
1427        assert!(matches!(
1428            rejected_open,
1429            Err(StoreError::BudgetExceeded {
1430                needed: actual_needed,
1431                budget: actual_budget,
1432                component: "snapshot",
1433            }) if actual_needed == needed && actual_budget == budget
1434        ));
1435    }
1436
1437    #[test]
1438    fn accounted_vector_rejects_growth_without_a_reservation() {
1439        let mut unaccounted = Accounted::<Vec<u8>>::unaccounted_empty();
1440        unaccounted
1441            .reserve_additional_bytes(0)
1442            .expect("zero bytes require no reservation");
1443        assert!(matches!(
1444            unaccounted.reserve_additional_bytes(1),
1445            Err(StoreError::AllocationFailed {
1446                needed: 1,
1447                component: "unaccounted vector",
1448            })
1449        ));
1450
1451        let accounting = std::sync::Arc::new(Accounting::new(u64::MAX, u64::MAX));
1452        let mut fixed = Accounted::try_with_capacity(&accounting, 1, AllocationComponent::Wal)
1453            .expect("fixed-capacity vector");
1454        fixed.push(7_u8).expect("reserved element");
1455        assert!(matches!(
1456            fixed.push(8_u8),
1457            Err(StoreError::AllocationFailed {
1458                needed: 2,
1459                component: "accounted vector capacity",
1460            })
1461        ));
1462    }
1463
1464    #[test]
1465    fn accounted_allocation_after_close_is_typed_closed() {
1466        let directory = tempdir().expect("store directory");
1467        let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
1468        store.close().expect("close");
1469
1470        assert!(matches!(
1471            store.allocate_bytes(1, AllocationComponent::Wal),
1472            Err(StoreError::Closed)
1473        ));
1474    }
1475
1476    #[cfg(feature = "allocation-audit")]
1477    #[test]
1478    fn nothing_allocates_outside_accounting() {
1479        let directory = tempdir().expect("store directory");
1480        let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
1481
1482        let (unaccounted, negative_control) =
1483            crate::allocation_audit::audit_engine_path(|| Vec::<u8>::with_capacity(13));
1484        assert_eq!(negative_control.attributed_bytes, 0);
1485        assert!(
1486            negative_control.unattributed_bytes >= 13,
1487            "the audit wrapper failed to detect an unattributed engine-path allocation"
1488        );
1489        drop(unaccounted);
1490        store.stats().expect("warm accounting synchronization");
1491
1492        let (accounted, report) = crate::allocation_audit::audit_engine_path(|| {
1493            store.allocate_bytes(23, AllocationComponent::Wal)
1494        });
1495        let accounted = accounted.expect("accounted allocation");
1496        assert_eq!(report.unattributed_bytes, 0);
1497        assert_eq!(report.attributed_bytes, 23);
1498        assert_eq!(store.stats().expect("stats").wal_bytes, 23);
1499        drop(accounted);
1500    }
1501
1502    fn published_store() -> TempDir {
1503        let directory = tempdir().expect("published store directory");
1504        let schema = Schema::new(Vec::new()).expect("timestamp-only schema");
1505        let mut columns = ColumnStoreBuilder::new(schema.clone());
1506        columns.push_row(17, &[]).expect("fixture row");
1507        let columns = columns.finish().expect("fixture columns");
1508        let alive = AliveSet::new(1);
1509        let id = SegmentId::new(0x0102_0304_0506, [0x09; 10]);
1510        let codes = [0x88_u8];
1511        let factors = [Bit4Factors::from_persisted(1.0, 1.0, 1.0)];
1512        let rescore = [0.0_f32, 0.0_f32];
1513        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::Ordered)
1514            .expect("derived policy");
1515        let segment = write_segment(
1516            &StdVfs,
1517            directory.path(),
1518            SegmentBuild {
1519                id,
1520                scheme: 4,
1521                dims: 2,
1522                codes: &codes,
1523                factors: SegmentFactors::Bit4(&factors),
1524                rescore: &rescore,
1525                columns: &columns,
1526                alive: &alive,
1527            },
1528            policy,
1529        )
1530        .expect("fixture segment");
1531        commit_manifest(
1532            &StdVfs,
1533            directory.path(),
1534            &Manifest {
1535                generation: 1,
1536                log_seq: 0,
1537                segments: vec![segment],
1538                epochs: Vec::new(),
1539                epoch_alias: None,
1540                schema,
1541            },
1542            policy,
1543        )
1544        .expect("fixture manifest");
1545        directory
1546    }
1547}