Skip to main content

fdu_core/
counters.rs

1//! Low-distortion performance instrumentation for fdu.
2//!
3//! The subsystem has two complementary tiers:
4//!
5//! - application counters attribute logical work to the filesystem, memory, and index
6//!   layers;
7//! - [`process`] samples kernel counters at phase boundaries and exposes only metrics
8//!   supported by the current platform.
9//!
10//! Recording is compiled in, off by default, and enabled with `FDU_COUNTERS=1` by the
11//! shipped CLI and the repository performance probe. Per-event updates stay thread-local;
12//! workers fold their counts on exit, while a drop fallback preserves counts from callers
13//! that do not install an explicit worker guard.
14
15use std::cell::Cell;
16use std::ffi::OsStr;
17use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
18
19pub mod alloc;
20pub mod process;
21
22static ENABLED: AtomicBool = AtomicBool::new(false);
23
24/// Per-layer tallies for a walk, index build, and content pass.
25#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
26pub struct Counts {
27    /// Logical directory-open operations.
28    pub dir_opens: u64,
29    /// Enumeration syscalls in complete successful macOS bulk listings, including the
30    /// empty terminator.
31    ///
32    /// One successful directory is typically two calls (one data, one EOF), the same
33    /// multiplicity the Linux portable path pays in `getdents64`. The portable `read_dir`
34    /// path leaves this at 0 because the standard library hides that split. Abandoned
35    /// bulk attempts before portable fallback are not counted. A later Linux or `openat`
36    /// campaign can compare this to `dir_opens` without another sample.
37    pub dir_enumeration_calls: u64,
38    /// Directory entries yielded by enumeration.
39    pub dir_entries: u64,
40    /// Logical metadata-stat operations.
41    pub stats: u64,
42    /// File-open operations performed by content analysis.
43    pub file_opens: u64,
44    /// File read calls performed by content analysis.
45    pub file_reads: u64,
46    /// Bytes read from files by content analysis.
47    pub bytes_read: u64,
48    /// Allocation requests observed by the installed counting allocator.
49    pub allocs: u64,
50    /// Reallocation requests observed by the installed counting allocator.
51    pub reallocs: u64,
52    /// Deallocations observed by the installed counting allocator.
53    pub frees: u64,
54    /// Bytes requested by allocations and allocation growth.
55    pub bytes_allocated: u64,
56    /// Index upserts applied.
57    pub upserts: u64,
58    /// Parent-path resolutions performed for index updates.
59    pub parent_resolutions: u64,
60    /// Parent resolutions served by the adjacent-parent memo.
61    pub parent_memo_hits: u64,
62    /// Roll-up merge operations.
63    pub rollup_merges: u64,
64    /// Control files read from the filesystem.
65    pub control_reads: u64,
66    /// Control sources a control table refused for one of its limits.
67    ///
68    /// Counted where the table decides, so a re-read of a file it has already refused is
69    /// not: that batch cannot change the table and is never projected. A warm revalidate
70    /// of a refused tree therefore reports its reads against no refusals, which is the
71    /// projection being skipped rather than a refusal being lifted.
72    pub control_refused: u64,
73    /// Control sources that joined a retained identical content instead of parsing again.
74    pub control_sources_shared: u64,
75    /// Index entries allocated.
76    pub entries_allocated: u64,
77    /// Successful detached baseline batches arbitrated by the index.
78    pub baseline_batches: u64,
79    /// Operations accepted from successful detached baseline batches.
80    pub baseline_accepted_ops: u64,
81    /// Successful opened-root batches arbitrated by the index.
82    pub opened_batches: u64,
83    /// Operations accepted from successful opened-root batches.
84    pub opened_accepted_ops: u64,
85    /// Successful arbitrary public batches arbitrated by the index.
86    pub public_batches: u64,
87    /// Operations accepted from successful arbitrary public batches.
88    pub public_accepted_ops: u64,
89    /// Path-keyed structural-overlay inserts during ancestry preflight.
90    pub ancestry_overlay_inserts: u64,
91    /// Same-parent path comparisons during ancestry preflight.
92    pub ancestry_path_comparisons: u64,
93    /// Parent chains proved during ancestry preflight, including memo hits.
94    pub ancestry_parent_proofs: u64,
95    /// Wall microseconds spent preparing trusted scanner batches.
96    pub scanner_prepare_us: u64,
97    /// Wall microseconds spent projecting controls for trusted scanner batches.
98    pub scanner_control_projection_us: u64,
99    /// Wall microseconds spent reducing prepared scanner batches into the index.
100    pub scanner_reduce_us: u64,
101    /// Wall microseconds spent reading the content sidecar image.
102    pub content_sidecar_read_us: u64,
103    /// Wall microseconds spent on sidecar integrity and record decode.
104    pub content_sidecar_parse_us: u64,
105    /// Wall microseconds spent building the live candidate map for a sidecar restore.
106    pub content_sidecar_candidates_us: u64,
107    /// Wall microseconds spent applying restored records and rebuilding content roll-ups.
108    pub content_sidecar_apply_us: u64,
109    /// Detached cold scans that selected the directory-group builder.
110    pub detached_builds: u64,
111    /// Entries consumed by that builder.
112    pub detached_entries: u64,
113    /// Wall microseconds spent walking while the builder consumed directory groups.
114    pub detached_walk_us: u64,
115    /// Wall microseconds spent in the final directories-only roll-up pass.
116    pub detached_finish_us: u64,
117    /// Owned paths retained in exact effective changes.
118    pub effect_paths: u64,
119    /// Native encoded bytes retained by exact effective-change paths.
120    pub effect_path_bytes: u64,
121    /// Change and state paths considered for bounded impact derivation.
122    pub impact_candidates: u64,
123    /// Path ancestors visited while deriving bounded impact.
124    pub impact_ancestor_visits: u64,
125    /// Dirty paths retained in completed bounded impacts.
126    pub impact_retained_dirty_paths: u64,
127    /// Impact derivations that crossed the dirty-path bound.
128    pub impact_all_dirty: u64,
129    /// Exact commits retained in the bounded journal.
130    pub journal_retained_commits: u64,
131    /// Exact commits cloned for attempted journal retention.
132    pub journal_cloned_commits: u64,
133    /// Exact commits rejected because one commit exceeded journal capacity.
134    pub journal_oversized_commits: u64,
135    /// Older exact commits dropped to admit a new retained commit.
136    pub journal_dropped_commits: u64,
137    /// Chunk releases folded into an automatic scan's worker-scaling calibration.
138    pub adaptive_calibration_chunks: u64,
139    /// Entries folded into that calibration before it reached a decision.
140    pub adaptive_calibration_entries: u64,
141    /// Worker time, in microseconds, folded into that calibration.
142    ///
143    /// Microseconds rather than nanoseconds because the decision threshold is expressed
144    /// per entry in microseconds, and a nanosecond total over a multi-million-entry walk
145    /// is large enough to make the row hard to read.
146    pub adaptive_calibration_work_us: u64,
147    /// Reserve-pool expansions an automatic scan actually performed.
148    pub adaptive_scale_ups: u64,
149    /// Walks that ended while their scaling calibration was still undecided.
150    ///
151    /// A walk shorter than the calibration window never observes enough entries to
152    /// decide, so its worker policy is unobservable rather than merely unchanged. That
153    /// is the distinction [`crate::scan`] fails closed on, and it must not read as a
154    /// decision to hold.
155    pub adaptive_policy_undecided: u64,
156}
157
158impl Counts {
159    const ZERO: Self = Self {
160        dir_opens: 0,
161        dir_enumeration_calls: 0,
162        dir_entries: 0,
163        stats: 0,
164        file_opens: 0,
165        file_reads: 0,
166        bytes_read: 0,
167        allocs: 0,
168        reallocs: 0,
169        frees: 0,
170        bytes_allocated: 0,
171        upserts: 0,
172        parent_resolutions: 0,
173        parent_memo_hits: 0,
174        rollup_merges: 0,
175        control_reads: 0,
176        control_refused: 0,
177        control_sources_shared: 0,
178        entries_allocated: 0,
179        baseline_batches: 0,
180        baseline_accepted_ops: 0,
181        opened_batches: 0,
182        opened_accepted_ops: 0,
183        public_batches: 0,
184        public_accepted_ops: 0,
185        ancestry_overlay_inserts: 0,
186        ancestry_path_comparisons: 0,
187        ancestry_parent_proofs: 0,
188        scanner_prepare_us: 0,
189        scanner_control_projection_us: 0,
190        scanner_reduce_us: 0,
191        content_sidecar_read_us: 0,
192        content_sidecar_parse_us: 0,
193        content_sidecar_candidates_us: 0,
194        content_sidecar_apply_us: 0,
195        detached_builds: 0,
196        detached_entries: 0,
197        detached_walk_us: 0,
198        detached_finish_us: 0,
199        effect_paths: 0,
200        effect_path_bytes: 0,
201        impact_candidates: 0,
202        impact_ancestor_visits: 0,
203        impact_retained_dirty_paths: 0,
204        impact_all_dirty: 0,
205        journal_retained_commits: 0,
206        journal_cloned_commits: 0,
207        journal_oversized_commits: 0,
208        journal_dropped_commits: 0,
209        adaptive_calibration_chunks: 0,
210        adaptive_calibration_entries: 0,
211        adaptive_calibration_work_us: 0,
212        adaptive_scale_ups: 0,
213        adaptive_policy_undecided: 0,
214    };
215
216    /// Every counter as `(group, label, value)`, in report order.
217    #[must_use]
218    pub fn rows(&self) -> Vec<(&'static str, &'static str, u64)> {
219        vec![
220            ("filesystem operations", "directory opens", self.dir_opens),
221            ("filesystem operations", "directory enumeration calls", self.dir_enumeration_calls),
222            ("filesystem operations", "directory entries enumerated", self.dir_entries),
223            ("filesystem operations", "metadata stats", self.stats),
224            ("filesystem operations", "file opens", self.file_opens),
225            ("filesystem operations", "file read calls", self.file_reads),
226            ("filesystem operations", "bytes read from files", self.bytes_read),
227            ("memory", "allocations", self.allocs),
228            ("memory", "reallocations", self.reallocs),
229            ("memory", "frees", self.frees),
230            ("memory", "bytes allocated", self.bytes_allocated),
231            ("index", "upserts applied", self.upserts),
232            ("index", "parent path resolutions", self.parent_resolutions),
233            ("index", "parent memo hits", self.parent_memo_hits),
234            ("index", "roll-up merges", self.rollup_merges),
235            ("control state", "control files read", self.control_reads),
236            ("control state", "control sources refused", self.control_refused),
237            ("control state", "control sources shared", self.control_sources_shared),
238            ("index", "index entries allocated", self.entries_allocated),
239            ("mutation provenance", "baseline batches", self.baseline_batches),
240            ("mutation provenance", "baseline accepted ops", self.baseline_accepted_ops),
241            ("mutation provenance", "opened batches", self.opened_batches),
242            ("mutation provenance", "opened accepted ops", self.opened_accepted_ops),
243            ("mutation provenance", "public batches", self.public_batches),
244            ("mutation provenance", "public accepted ops", self.public_accepted_ops),
245            ("mutation preflight", "ancestry overlay inserts", self.ancestry_overlay_inserts),
246            ("mutation preflight", "same-parent path comparisons", self.ancestry_path_comparisons),
247            ("mutation preflight", "parent chains proved", self.ancestry_parent_proofs),
248            ("mutation timing", "scanner preparation microseconds", self.scanner_prepare_us),
249            (
250                "mutation timing",
251                "scanner control projection microseconds",
252                self.scanner_control_projection_us,
253            ),
254            ("mutation timing", "scanner reduction microseconds", self.scanner_reduce_us),
255            (
256                "content sidecar timing",
257                "sidecar image read microseconds",
258                self.content_sidecar_read_us,
259            ),
260            ("content sidecar timing", "sidecar parse microseconds", self.content_sidecar_parse_us),
261            (
262                "content sidecar timing",
263                "sidecar candidate-map microseconds",
264                self.content_sidecar_candidates_us,
265            ),
266            ("content sidecar timing", "sidecar apply microseconds", self.content_sidecar_apply_us),
267            ("detached builder", "builds", self.detached_builds),
268            ("detached builder", "entries", self.detached_entries),
269            ("detached builder", "walk microseconds", self.detached_walk_us),
270            ("detached builder", "finish microseconds", self.detached_finish_us),
271            ("mutation consequences", "effective paths retained", self.effect_paths),
272            ("mutation consequences", "effective path bytes", self.effect_path_bytes),
273            ("mutation consequences", "impact candidates", self.impact_candidates),
274            ("mutation consequences", "impact ancestor visits", self.impact_ancestor_visits),
275            (
276                "mutation consequences",
277                "impact dirty paths retained",
278                self.impact_retained_dirty_paths,
279            ),
280            ("mutation consequences", "impact all-dirty transitions", self.impact_all_dirty),
281            ("mutation journal", "commits retained", self.journal_retained_commits),
282            ("mutation journal", "commits cloned", self.journal_cloned_commits),
283            ("mutation journal", "oversized commits", self.journal_oversized_commits),
284            ("mutation journal", "older commits dropped", self.journal_dropped_commits),
285            ("adaptive scan policy", "calibration chunks", self.adaptive_calibration_chunks),
286            ("adaptive scan policy", "calibration entries", self.adaptive_calibration_entries),
287            (
288                "adaptive scan policy",
289                "calibration worker microseconds",
290                self.adaptive_calibration_work_us,
291            ),
292            ("adaptive scan policy", "reserve expansions", self.adaptive_scale_ups),
293            ("adaptive scan policy", "walks left undecided", self.adaptive_policy_undecided),
294        ]
295    }
296
297    /// Whether every counter is zero.
298    #[must_use]
299    pub fn is_empty(&self) -> bool {
300        *self == Self::default()
301    }
302
303    /// Fold another set into this one without panicking on overflow.
304    fn add(&mut self, other: &Self) {
305        self.dir_opens = self.dir_opens.saturating_add(other.dir_opens);
306        self.dir_enumeration_calls =
307            self.dir_enumeration_calls.saturating_add(other.dir_enumeration_calls);
308        self.dir_entries = self.dir_entries.saturating_add(other.dir_entries);
309        self.stats = self.stats.saturating_add(other.stats);
310        self.file_opens = self.file_opens.saturating_add(other.file_opens);
311        self.file_reads = self.file_reads.saturating_add(other.file_reads);
312        self.bytes_read = self.bytes_read.saturating_add(other.bytes_read);
313        self.allocs = self.allocs.saturating_add(other.allocs);
314        self.reallocs = self.reallocs.saturating_add(other.reallocs);
315        self.frees = self.frees.saturating_add(other.frees);
316        self.bytes_allocated = self.bytes_allocated.saturating_add(other.bytes_allocated);
317        self.upserts = self.upserts.saturating_add(other.upserts);
318        self.parent_resolutions = self.parent_resolutions.saturating_add(other.parent_resolutions);
319        self.parent_memo_hits = self.parent_memo_hits.saturating_add(other.parent_memo_hits);
320        self.rollup_merges = self.rollup_merges.saturating_add(other.rollup_merges);
321        self.control_reads = self.control_reads.saturating_add(other.control_reads);
322        self.control_refused = self.control_refused.saturating_add(other.control_refused);
323        self.control_sources_shared =
324            self.control_sources_shared.saturating_add(other.control_sources_shared);
325        self.entries_allocated = self.entries_allocated.saturating_add(other.entries_allocated);
326        self.baseline_batches = self.baseline_batches.saturating_add(other.baseline_batches);
327        self.baseline_accepted_ops =
328            self.baseline_accepted_ops.saturating_add(other.baseline_accepted_ops);
329        self.opened_batches = self.opened_batches.saturating_add(other.opened_batches);
330        self.opened_accepted_ops =
331            self.opened_accepted_ops.saturating_add(other.opened_accepted_ops);
332        self.public_batches = self.public_batches.saturating_add(other.public_batches);
333        self.public_accepted_ops =
334            self.public_accepted_ops.saturating_add(other.public_accepted_ops);
335        self.ancestry_overlay_inserts =
336            self.ancestry_overlay_inserts.saturating_add(other.ancestry_overlay_inserts);
337        self.ancestry_path_comparisons =
338            self.ancestry_path_comparisons.saturating_add(other.ancestry_path_comparisons);
339        self.ancestry_parent_proofs =
340            self.ancestry_parent_proofs.saturating_add(other.ancestry_parent_proofs);
341        self.scanner_prepare_us = self.scanner_prepare_us.saturating_add(other.scanner_prepare_us);
342        self.scanner_control_projection_us =
343            self.scanner_control_projection_us.saturating_add(other.scanner_control_projection_us);
344        self.scanner_reduce_us = self.scanner_reduce_us.saturating_add(other.scanner_reduce_us);
345        self.content_sidecar_read_us =
346            self.content_sidecar_read_us.saturating_add(other.content_sidecar_read_us);
347        self.content_sidecar_parse_us =
348            self.content_sidecar_parse_us.saturating_add(other.content_sidecar_parse_us);
349        self.content_sidecar_candidates_us =
350            self.content_sidecar_candidates_us.saturating_add(other.content_sidecar_candidates_us);
351        self.content_sidecar_apply_us =
352            self.content_sidecar_apply_us.saturating_add(other.content_sidecar_apply_us);
353        self.detached_builds = self.detached_builds.saturating_add(other.detached_builds);
354        self.detached_entries = self.detached_entries.saturating_add(other.detached_entries);
355        self.detached_walk_us = self.detached_walk_us.saturating_add(other.detached_walk_us);
356        self.detached_finish_us = self.detached_finish_us.saturating_add(other.detached_finish_us);
357        self.effect_paths = self.effect_paths.saturating_add(other.effect_paths);
358        self.effect_path_bytes = self.effect_path_bytes.saturating_add(other.effect_path_bytes);
359        self.impact_candidates = self.impact_candidates.saturating_add(other.impact_candidates);
360        self.impact_ancestor_visits =
361            self.impact_ancestor_visits.saturating_add(other.impact_ancestor_visits);
362        self.impact_retained_dirty_paths =
363            self.impact_retained_dirty_paths.saturating_add(other.impact_retained_dirty_paths);
364        self.impact_all_dirty = self.impact_all_dirty.saturating_add(other.impact_all_dirty);
365        self.journal_retained_commits =
366            self.journal_retained_commits.saturating_add(other.journal_retained_commits);
367        self.journal_cloned_commits =
368            self.journal_cloned_commits.saturating_add(other.journal_cloned_commits);
369        self.journal_oversized_commits =
370            self.journal_oversized_commits.saturating_add(other.journal_oversized_commits);
371        self.journal_dropped_commits =
372            self.journal_dropped_commits.saturating_add(other.journal_dropped_commits);
373        self.adaptive_calibration_chunks =
374            self.adaptive_calibration_chunks.saturating_add(other.adaptive_calibration_chunks);
375        self.adaptive_calibration_entries =
376            self.adaptive_calibration_entries.saturating_add(other.adaptive_calibration_entries);
377        self.adaptive_calibration_work_us =
378            self.adaptive_calibration_work_us.saturating_add(other.adaptive_calibration_work_us);
379        self.adaptive_scale_ups = self.adaptive_scale_ups.saturating_add(other.adaptive_scale_ups);
380        self.adaptive_policy_undecided =
381            self.adaptive_policy_undecided.saturating_add(other.adaptive_policy_undecided);
382    }
383
384    /// Per-event ratios against a denominator, in report order.
385    #[must_use]
386    pub fn per(&self, denominator: u64) -> Vec<(&'static str, &'static str, f64)> {
387        self.rows()
388            .into_iter()
389            .map(|(group, label, value)| (group, label, ratio(value, denominator)))
390            .collect()
391    }
392}
393
394struct GlobalCounts {
395    dir_opens: AtomicU64,
396    dir_enumeration_calls: AtomicU64,
397    dir_entries: AtomicU64,
398    stats: AtomicU64,
399    file_opens: AtomicU64,
400    file_reads: AtomicU64,
401    bytes_read: AtomicU64,
402    allocs: AtomicU64,
403    reallocs: AtomicU64,
404    frees: AtomicU64,
405    bytes_allocated: AtomicU64,
406    upserts: AtomicU64,
407    parent_resolutions: AtomicU64,
408    parent_memo_hits: AtomicU64,
409    rollup_merges: AtomicU64,
410    control_reads: AtomicU64,
411    control_refused: AtomicU64,
412    control_sources_shared: AtomicU64,
413    entries_allocated: AtomicU64,
414    baseline_batches: AtomicU64,
415    baseline_accepted_ops: AtomicU64,
416    opened_batches: AtomicU64,
417    opened_accepted_ops: AtomicU64,
418    public_batches: AtomicU64,
419    public_accepted_ops: AtomicU64,
420    ancestry_overlay_inserts: AtomicU64,
421    ancestry_path_comparisons: AtomicU64,
422    ancestry_parent_proofs: AtomicU64,
423    scanner_prepare_us: AtomicU64,
424    scanner_control_projection_us: AtomicU64,
425    scanner_reduce_us: AtomicU64,
426    content_sidecar_read_us: AtomicU64,
427    content_sidecar_parse_us: AtomicU64,
428    content_sidecar_candidates_us: AtomicU64,
429    content_sidecar_apply_us: AtomicU64,
430    detached_builds: AtomicU64,
431    detached_entries: AtomicU64,
432    detached_walk_us: AtomicU64,
433    detached_finish_us: AtomicU64,
434    effect_paths: AtomicU64,
435    effect_path_bytes: AtomicU64,
436    impact_candidates: AtomicU64,
437    impact_ancestor_visits: AtomicU64,
438    impact_retained_dirty_paths: AtomicU64,
439    impact_all_dirty: AtomicU64,
440    journal_retained_commits: AtomicU64,
441    journal_cloned_commits: AtomicU64,
442    journal_oversized_commits: AtomicU64,
443    journal_dropped_commits: AtomicU64,
444    adaptive_calibration_chunks: AtomicU64,
445    adaptive_calibration_entries: AtomicU64,
446    adaptive_calibration_work_us: AtomicU64,
447    adaptive_scale_ups: AtomicU64,
448    adaptive_policy_undecided: AtomicU64,
449}
450
451impl GlobalCounts {
452    const fn new() -> Self {
453        Self {
454            dir_opens: AtomicU64::new(0),
455            dir_enumeration_calls: AtomicU64::new(0),
456            dir_entries: AtomicU64::new(0),
457            stats: AtomicU64::new(0),
458            file_opens: AtomicU64::new(0),
459            file_reads: AtomicU64::new(0),
460            bytes_read: AtomicU64::new(0),
461            allocs: AtomicU64::new(0),
462            reallocs: AtomicU64::new(0),
463            frees: AtomicU64::new(0),
464            bytes_allocated: AtomicU64::new(0),
465            upserts: AtomicU64::new(0),
466            parent_resolutions: AtomicU64::new(0),
467            parent_memo_hits: AtomicU64::new(0),
468            rollup_merges: AtomicU64::new(0),
469            control_reads: AtomicU64::new(0),
470            control_refused: AtomicU64::new(0),
471            control_sources_shared: AtomicU64::new(0),
472            entries_allocated: AtomicU64::new(0),
473            baseline_batches: AtomicU64::new(0),
474            baseline_accepted_ops: AtomicU64::new(0),
475            opened_batches: AtomicU64::new(0),
476            opened_accepted_ops: AtomicU64::new(0),
477            public_batches: AtomicU64::new(0),
478            public_accepted_ops: AtomicU64::new(0),
479            ancestry_overlay_inserts: AtomicU64::new(0),
480            ancestry_path_comparisons: AtomicU64::new(0),
481            ancestry_parent_proofs: AtomicU64::new(0),
482            scanner_prepare_us: AtomicU64::new(0),
483            scanner_control_projection_us: AtomicU64::new(0),
484            scanner_reduce_us: AtomicU64::new(0),
485            content_sidecar_read_us: AtomicU64::new(0),
486            content_sidecar_parse_us: AtomicU64::new(0),
487            content_sidecar_candidates_us: AtomicU64::new(0),
488            content_sidecar_apply_us: AtomicU64::new(0),
489            detached_builds: AtomicU64::new(0),
490            detached_entries: AtomicU64::new(0),
491            detached_walk_us: AtomicU64::new(0),
492            detached_finish_us: AtomicU64::new(0),
493            effect_paths: AtomicU64::new(0),
494            effect_path_bytes: AtomicU64::new(0),
495            impact_candidates: AtomicU64::new(0),
496            impact_ancestor_visits: AtomicU64::new(0),
497            impact_retained_dirty_paths: AtomicU64::new(0),
498            impact_all_dirty: AtomicU64::new(0),
499            journal_retained_commits: AtomicU64::new(0),
500            journal_cloned_commits: AtomicU64::new(0),
501            journal_oversized_commits: AtomicU64::new(0),
502            journal_dropped_commits: AtomicU64::new(0),
503            adaptive_calibration_chunks: AtomicU64::new(0),
504            adaptive_calibration_entries: AtomicU64::new(0),
505            adaptive_calibration_work_us: AtomicU64::new(0),
506            adaptive_scale_ups: AtomicU64::new(0),
507            adaptive_policy_undecided: AtomicU64::new(0),
508        }
509    }
510
511    fn add(&self, counts: &Counts) {
512        atomic_saturating_add(&self.dir_opens, counts.dir_opens);
513        atomic_saturating_add(&self.dir_enumeration_calls, counts.dir_enumeration_calls);
514        atomic_saturating_add(&self.dir_entries, counts.dir_entries);
515        atomic_saturating_add(&self.stats, counts.stats);
516        atomic_saturating_add(&self.file_opens, counts.file_opens);
517        atomic_saturating_add(&self.file_reads, counts.file_reads);
518        atomic_saturating_add(&self.bytes_read, counts.bytes_read);
519        atomic_saturating_add(&self.allocs, counts.allocs);
520        atomic_saturating_add(&self.reallocs, counts.reallocs);
521        atomic_saturating_add(&self.frees, counts.frees);
522        atomic_saturating_add(&self.bytes_allocated, counts.bytes_allocated);
523        atomic_saturating_add(&self.upserts, counts.upserts);
524        atomic_saturating_add(&self.parent_resolutions, counts.parent_resolutions);
525        atomic_saturating_add(&self.parent_memo_hits, counts.parent_memo_hits);
526        atomic_saturating_add(&self.rollup_merges, counts.rollup_merges);
527        atomic_saturating_add(&self.control_reads, counts.control_reads);
528        atomic_saturating_add(&self.control_refused, counts.control_refused);
529        atomic_saturating_add(&self.control_sources_shared, counts.control_sources_shared);
530        atomic_saturating_add(&self.entries_allocated, counts.entries_allocated);
531        atomic_saturating_add(&self.baseline_batches, counts.baseline_batches);
532        atomic_saturating_add(&self.baseline_accepted_ops, counts.baseline_accepted_ops);
533        atomic_saturating_add(&self.opened_batches, counts.opened_batches);
534        atomic_saturating_add(&self.opened_accepted_ops, counts.opened_accepted_ops);
535        atomic_saturating_add(&self.public_batches, counts.public_batches);
536        atomic_saturating_add(&self.public_accepted_ops, counts.public_accepted_ops);
537        atomic_saturating_add(&self.ancestry_overlay_inserts, counts.ancestry_overlay_inserts);
538        atomic_saturating_add(&self.ancestry_path_comparisons, counts.ancestry_path_comparisons);
539        atomic_saturating_add(&self.ancestry_parent_proofs, counts.ancestry_parent_proofs);
540        atomic_saturating_add(&self.scanner_prepare_us, counts.scanner_prepare_us);
541        atomic_saturating_add(
542            &self.scanner_control_projection_us,
543            counts.scanner_control_projection_us,
544        );
545        atomic_saturating_add(&self.scanner_reduce_us, counts.scanner_reduce_us);
546        atomic_saturating_add(&self.content_sidecar_read_us, counts.content_sidecar_read_us);
547        atomic_saturating_add(&self.content_sidecar_parse_us, counts.content_sidecar_parse_us);
548        atomic_saturating_add(
549            &self.content_sidecar_candidates_us,
550            counts.content_sidecar_candidates_us,
551        );
552        atomic_saturating_add(&self.content_sidecar_apply_us, counts.content_sidecar_apply_us);
553        atomic_saturating_add(&self.detached_builds, counts.detached_builds);
554        atomic_saturating_add(&self.detached_entries, counts.detached_entries);
555        atomic_saturating_add(&self.detached_walk_us, counts.detached_walk_us);
556        atomic_saturating_add(&self.detached_finish_us, counts.detached_finish_us);
557        atomic_saturating_add(&self.effect_paths, counts.effect_paths);
558        atomic_saturating_add(&self.effect_path_bytes, counts.effect_path_bytes);
559        atomic_saturating_add(&self.impact_candidates, counts.impact_candidates);
560        atomic_saturating_add(&self.impact_ancestor_visits, counts.impact_ancestor_visits);
561        atomic_saturating_add(
562            &self.impact_retained_dirty_paths,
563            counts.impact_retained_dirty_paths,
564        );
565        atomic_saturating_add(&self.impact_all_dirty, counts.impact_all_dirty);
566        atomic_saturating_add(&self.journal_retained_commits, counts.journal_retained_commits);
567        atomic_saturating_add(&self.journal_cloned_commits, counts.journal_cloned_commits);
568        atomic_saturating_add(&self.journal_oversized_commits, counts.journal_oversized_commits);
569        atomic_saturating_add(&self.journal_dropped_commits, counts.journal_dropped_commits);
570        atomic_saturating_add(
571            &self.adaptive_calibration_chunks,
572            counts.adaptive_calibration_chunks,
573        );
574        atomic_saturating_add(
575            &self.adaptive_calibration_entries,
576            counts.adaptive_calibration_entries,
577        );
578        atomic_saturating_add(
579            &self.adaptive_calibration_work_us,
580            counts.adaptive_calibration_work_us,
581        );
582        atomic_saturating_add(&self.adaptive_scale_ups, counts.adaptive_scale_ups);
583        atomic_saturating_add(&self.adaptive_policy_undecided, counts.adaptive_policy_undecided);
584    }
585
586    fn snapshot(&self) -> Counts {
587        Counts {
588            dir_opens: self.dir_opens.load(Ordering::Relaxed),
589            dir_enumeration_calls: self.dir_enumeration_calls.load(Ordering::Relaxed),
590            dir_entries: self.dir_entries.load(Ordering::Relaxed),
591            stats: self.stats.load(Ordering::Relaxed),
592            file_opens: self.file_opens.load(Ordering::Relaxed),
593            file_reads: self.file_reads.load(Ordering::Relaxed),
594            bytes_read: self.bytes_read.load(Ordering::Relaxed),
595            allocs: self.allocs.load(Ordering::Relaxed),
596            reallocs: self.reallocs.load(Ordering::Relaxed),
597            frees: self.frees.load(Ordering::Relaxed),
598            bytes_allocated: self.bytes_allocated.load(Ordering::Relaxed),
599            upserts: self.upserts.load(Ordering::Relaxed),
600            parent_resolutions: self.parent_resolutions.load(Ordering::Relaxed),
601            parent_memo_hits: self.parent_memo_hits.load(Ordering::Relaxed),
602            rollup_merges: self.rollup_merges.load(Ordering::Relaxed),
603            control_reads: self.control_reads.load(Ordering::Relaxed),
604            control_refused: self.control_refused.load(Ordering::Relaxed),
605            control_sources_shared: self.control_sources_shared.load(Ordering::Relaxed),
606            entries_allocated: self.entries_allocated.load(Ordering::Relaxed),
607            baseline_batches: self.baseline_batches.load(Ordering::Relaxed),
608            baseline_accepted_ops: self.baseline_accepted_ops.load(Ordering::Relaxed),
609            opened_batches: self.opened_batches.load(Ordering::Relaxed),
610            opened_accepted_ops: self.opened_accepted_ops.load(Ordering::Relaxed),
611            public_batches: self.public_batches.load(Ordering::Relaxed),
612            public_accepted_ops: self.public_accepted_ops.load(Ordering::Relaxed),
613            ancestry_overlay_inserts: self.ancestry_overlay_inserts.load(Ordering::Relaxed),
614            ancestry_path_comparisons: self.ancestry_path_comparisons.load(Ordering::Relaxed),
615            ancestry_parent_proofs: self.ancestry_parent_proofs.load(Ordering::Relaxed),
616            scanner_prepare_us: self.scanner_prepare_us.load(Ordering::Relaxed),
617            scanner_control_projection_us: self
618                .scanner_control_projection_us
619                .load(Ordering::Relaxed),
620            scanner_reduce_us: self.scanner_reduce_us.load(Ordering::Relaxed),
621            content_sidecar_read_us: self.content_sidecar_read_us.load(Ordering::Relaxed),
622            content_sidecar_parse_us: self.content_sidecar_parse_us.load(Ordering::Relaxed),
623            content_sidecar_candidates_us: self
624                .content_sidecar_candidates_us
625                .load(Ordering::Relaxed),
626            content_sidecar_apply_us: self.content_sidecar_apply_us.load(Ordering::Relaxed),
627            detached_builds: self.detached_builds.load(Ordering::Relaxed),
628            detached_entries: self.detached_entries.load(Ordering::Relaxed),
629            detached_walk_us: self.detached_walk_us.load(Ordering::Relaxed),
630            detached_finish_us: self.detached_finish_us.load(Ordering::Relaxed),
631            effect_paths: self.effect_paths.load(Ordering::Relaxed),
632            effect_path_bytes: self.effect_path_bytes.load(Ordering::Relaxed),
633            impact_candidates: self.impact_candidates.load(Ordering::Relaxed),
634            impact_ancestor_visits: self.impact_ancestor_visits.load(Ordering::Relaxed),
635            impact_retained_dirty_paths: self.impact_retained_dirty_paths.load(Ordering::Relaxed),
636            impact_all_dirty: self.impact_all_dirty.load(Ordering::Relaxed),
637            journal_retained_commits: self.journal_retained_commits.load(Ordering::Relaxed),
638            journal_cloned_commits: self.journal_cloned_commits.load(Ordering::Relaxed),
639            journal_oversized_commits: self.journal_oversized_commits.load(Ordering::Relaxed),
640            journal_dropped_commits: self.journal_dropped_commits.load(Ordering::Relaxed),
641            adaptive_calibration_chunks: self.adaptive_calibration_chunks.load(Ordering::Relaxed),
642            adaptive_calibration_entries: self.adaptive_calibration_entries.load(Ordering::Relaxed),
643            adaptive_calibration_work_us: self.adaptive_calibration_work_us.load(Ordering::Relaxed),
644            adaptive_scale_ups: self.adaptive_scale_ups.load(Ordering::Relaxed),
645            adaptive_policy_undecided: self.adaptive_policy_undecided.load(Ordering::Relaxed),
646        }
647    }
648
649    fn reset(&self) {
650        self.dir_opens.store(0, Ordering::Relaxed);
651        self.dir_enumeration_calls.store(0, Ordering::Relaxed);
652        self.dir_entries.store(0, Ordering::Relaxed);
653        self.stats.store(0, Ordering::Relaxed);
654        self.file_opens.store(0, Ordering::Relaxed);
655        self.file_reads.store(0, Ordering::Relaxed);
656        self.bytes_read.store(0, Ordering::Relaxed);
657        self.allocs.store(0, Ordering::Relaxed);
658        self.reallocs.store(0, Ordering::Relaxed);
659        self.frees.store(0, Ordering::Relaxed);
660        self.bytes_allocated.store(0, Ordering::Relaxed);
661        self.upserts.store(0, Ordering::Relaxed);
662        self.parent_resolutions.store(0, Ordering::Relaxed);
663        self.parent_memo_hits.store(0, Ordering::Relaxed);
664        self.rollup_merges.store(0, Ordering::Relaxed);
665        self.control_reads.store(0, Ordering::Relaxed);
666        self.control_refused.store(0, Ordering::Relaxed);
667        self.control_sources_shared.store(0, Ordering::Relaxed);
668        self.entries_allocated.store(0, Ordering::Relaxed);
669        self.baseline_batches.store(0, Ordering::Relaxed);
670        self.baseline_accepted_ops.store(0, Ordering::Relaxed);
671        self.opened_batches.store(0, Ordering::Relaxed);
672        self.opened_accepted_ops.store(0, Ordering::Relaxed);
673        self.public_batches.store(0, Ordering::Relaxed);
674        self.public_accepted_ops.store(0, Ordering::Relaxed);
675        self.ancestry_overlay_inserts.store(0, Ordering::Relaxed);
676        self.ancestry_path_comparisons.store(0, Ordering::Relaxed);
677        self.ancestry_parent_proofs.store(0, Ordering::Relaxed);
678        self.scanner_prepare_us.store(0, Ordering::Relaxed);
679        self.scanner_control_projection_us.store(0, Ordering::Relaxed);
680        self.scanner_reduce_us.store(0, Ordering::Relaxed);
681        self.content_sidecar_read_us.store(0, Ordering::Relaxed);
682        self.content_sidecar_parse_us.store(0, Ordering::Relaxed);
683        self.content_sidecar_candidates_us.store(0, Ordering::Relaxed);
684        self.content_sidecar_apply_us.store(0, Ordering::Relaxed);
685        self.detached_builds.store(0, Ordering::Relaxed);
686        self.detached_entries.store(0, Ordering::Relaxed);
687        self.detached_walk_us.store(0, Ordering::Relaxed);
688        self.detached_finish_us.store(0, Ordering::Relaxed);
689        self.effect_paths.store(0, Ordering::Relaxed);
690        self.effect_path_bytes.store(0, Ordering::Relaxed);
691        self.impact_candidates.store(0, Ordering::Relaxed);
692        self.impact_ancestor_visits.store(0, Ordering::Relaxed);
693        self.impact_retained_dirty_paths.store(0, Ordering::Relaxed);
694        self.impact_all_dirty.store(0, Ordering::Relaxed);
695        self.journal_retained_commits.store(0, Ordering::Relaxed);
696        self.journal_cloned_commits.store(0, Ordering::Relaxed);
697        self.journal_oversized_commits.store(0, Ordering::Relaxed);
698        self.journal_dropped_commits.store(0, Ordering::Relaxed);
699        self.adaptive_calibration_chunks.store(0, Ordering::Relaxed);
700        self.adaptive_calibration_entries.store(0, Ordering::Relaxed);
701        self.adaptive_calibration_work_us.store(0, Ordering::Relaxed);
702        self.adaptive_scale_ups.store(0, Ordering::Relaxed);
703        self.adaptive_policy_undecided.store(0, Ordering::Relaxed);
704    }
705}
706
707static GLOBAL: GlobalCounts = GlobalCounts::new();
708
709struct LocalCounts {
710    counts: Cell<Counts>,
711}
712
713impl LocalCounts {
714    const fn new() -> Self {
715        Self { counts: Cell::new(Counts::ZERO) }
716    }
717}
718
719impl Drop for LocalCounts {
720    fn drop(&mut self) {
721        GLOBAL.add(&self.counts.replace(Counts::default()));
722    }
723}
724
725std::thread_local! {
726    // This non-dropping, const-initialized guard is accessed before `LOCAL`. If first
727    // access to a dropping TLS key causes an allocation on a platform, the allocator's
728    // nested callback sees `true` and takes the atomic fallback instead of recursing.
729    static IN_COUNTER: Cell<bool> = const { Cell::new(false) };
730    static LOCAL: LocalCounts = const { LocalCounts::new() };
731}
732
733struct ReentryGuard<'a>(&'a Cell<bool>);
734
735impl<'a> ReentryGuard<'a> {
736    fn enter(cell: &'a Cell<bool>) -> Option<Self> {
737        if cell.replace(true) { None } else { Some(Self(cell)) }
738    }
739}
740
741impl Drop for ReentryGuard<'_> {
742    fn drop(&mut self) {
743        self.0.set(false);
744    }
745}
746
747/// A guard that deterministically folds one worker's counters before it returns.
748pub(crate) struct ThreadFlushGuard;
749
750impl Drop for ThreadFlushGuard {
751    fn drop(&mut self) {
752        flush_thread();
753    }
754}
755
756/// Begin a worker lifetime whose thread-local counters must be visible after join.
757pub(crate) const fn thread_flush_guard() -> ThreadFlushGuard {
758    ThreadFlushGuard
759}
760
761/// One opt-in instrumentation interval for a binary invocation.
762///
763/// Construct this before the measured operation and call [`Self::finish`] afterward.
764/// When recording is off, construction performs no process sampling and `finish`
765/// returns `None`.
766#[must_use]
767pub struct Measurement {
768    process_before: Option<process::Snapshot>,
769}
770
771impl Measurement {
772    /// Read `FDU_COUNTERS` and begin an enabled interval when it is truthy.
773    pub fn from_env() -> Self {
774        if !enable_from_env() {
775            return Self { process_before: None };
776        }
777
778        // The boundary sample can allocate on Linux while parsing `/proc`. Take it first
779        // and reset afterward so the application tier describes fdu's work, not its
780        // measuring instrument.
781        let process_before = process::Snapshot::now();
782        reset();
783        Self { process_before: Some(process_before) }
784    }
785
786    /// Finish the interval and render application and supported process counters.
787    #[must_use]
788    pub fn finish(mut self) -> Option<String> {
789        let before = self.process_before.take()?;
790        flush_thread();
791        let counts = snapshot();
792        let process = process::Snapshot::now().since(&before);
793        enable(false);
794        Some(render(&counts, &process))
795    }
796}
797
798impl Drop for Measurement {
799    fn drop(&mut self) {
800        if self.process_before.is_some() {
801            enable(false);
802        }
803    }
804}
805
806/// Turn recording on or off for the whole process.
807pub fn enable(on: bool) {
808    ENABLED.store(on, Ordering::Relaxed);
809}
810
811/// Turn recording on when `FDU_COUNTERS` contains a truthy value.
812///
813/// Empty strings, `0`, `false`, `no`, and `off` (case-insensitive) disable recording.
814/// The parsed state is always applied, including a transition from enabled to disabled.
815pub fn enable_from_env() -> bool {
816    enable_from_value(std::env::var_os("FDU_COUNTERS").as_deref())
817}
818
819fn enable_from_value(value: Option<&OsStr>) -> bool {
820    let on = value.is_some_and(|value| {
821        value.to_str().is_none_or(|text| {
822            let text = text.trim();
823            !(text.is_empty()
824                || text == "0"
825                || text.eq_ignore_ascii_case("false")
826                || text.eq_ignore_ascii_case("no")
827                || text.eq_ignore_ascii_case("off"))
828        })
829    });
830    enable(on);
831    on
832}
833
834/// Whether recording is currently on.
835#[inline]
836#[must_use]
837pub fn enabled() -> bool {
838    ENABLED.load(Ordering::Relaxed)
839}
840
841/// Elapsed microseconds since `started`, saturating at `u64::MAX`.
842#[must_use]
843pub(crate) fn elapsed_micros(started: std::time::Instant) -> u64 {
844    u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX)
845}
846
847/// Record a whole-phase duration when recording is on.
848pub(crate) fn add_elapsed(started: Option<std::time::Instant>, add: impl FnOnce(&mut Counts, u64)) {
849    if let Some(started) = started {
850        bump(|counts| add(counts, elapsed_micros(started)));
851    }
852}
853
854/// Add to one or more counters on the calling thread.
855///
856/// The fallback path is important for allocation callbacks: it avoids re-entering a TLS
857/// initializer and remains usable while a thread's local counter has begun destruction.
858#[inline]
859pub fn bump(f: impl FnOnce(&mut Counts)) {
860    if !enabled() {
861        return;
862    }
863
864    let mut f = Some(f);
865    let stored_locally = IN_COUNTER
866        .try_with(|reentry| {
867            let Some(_guard) = ReentryGuard::enter(reentry) else { return false };
868            LOCAL
869                .try_with(|local| {
870                    if let Some(update) = f.take() {
871                        let mut counts = local.counts.get();
872                        update(&mut counts);
873                        local.counts.set(counts);
874                    }
875                })
876                .is_ok()
877        })
878        .unwrap_or(false);
879
880    if !stored_locally {
881        let mut delta = Counts::default();
882        if let Some(update) = f {
883            update(&mut delta);
884            GLOBAL.add(&delta);
885        }
886    }
887}
888
889/// Fold this thread's counts into the process total and clear them.
890pub fn flush_thread() {
891    let _ = LOCAL.try_with(|local| GLOBAL.add(&local.counts.replace(Counts::default())));
892}
893
894/// Process-wide totals, including the calling thread's unflushed counts.
895#[must_use]
896pub fn snapshot() -> Counts {
897    let mut total = GLOBAL.snapshot();
898    let _ = LOCAL.try_with(|local| total.add(&local.counts.get()));
899    total
900}
901
902/// The calling thread's unflushed counts, without consuming them.
903///
904/// Use paired reads to attribute synchronous diagnostic work without also counting
905/// workers that finish during it. A [`flush_thread`] between reads starts a new local
906/// interval; these are not lifetime totals. No other thread's counts are included.
907#[must_use]
908pub fn thread_snapshot() -> Counts {
909    LOCAL.try_with(|local| local.counts.get()).unwrap_or_default()
910}
911
912/// Clear process totals and the calling thread's local counts.
913///
914/// Call only when other instrumented workers are quiescent: another thread may still
915/// hold local counts that a process-global reset cannot reach.
916pub fn reset() {
917    let _ = LOCAL.try_with(|local| local.counts.set(Counts::default()));
918    GLOBAL.reset();
919}
920
921/// Create fdu's certified system-allocator wrapper for a binary's global allocator.
922#[must_use]
923pub const fn system_allocator() -> alloc::CountingAlloc<std::alloc::System> {
924    alloc::CountingAlloc::system(alloc::fdu_sinks())
925}
926
927/// Render application counters followed by the supported kernel process counters.
928#[must_use]
929pub fn render(counts: &Counts, process: &process::Snapshot) -> String {
930    let mut out = render_rows(&counts.rows());
931    out.push('\n');
932    out.push_str(&process.render());
933    out
934}
935
936fn record_alloc(size: u64) {
937    bump(|counts| {
938        counts.allocs = counts.allocs.saturating_add(1);
939        counts.bytes_allocated = counts.bytes_allocated.saturating_add(size);
940    });
941}
942
943fn record_realloc(growth: u64) {
944    bump(|counts| {
945        counts.reallocs = counts.reallocs.saturating_add(1);
946        counts.bytes_allocated = counts.bytes_allocated.saturating_add(growth);
947    });
948}
949
950fn record_dealloc() {
951    bump(|counts| counts.frees = counts.frees.saturating_add(1));
952}
953
954fn atomic_saturating_add(target: &AtomicU64, value: u64) {
955    if value == 0 {
956        return;
957    }
958    let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
959        Some(current.saturating_add(value))
960    });
961}
962
963#[allow(clippy::cast_precision_loss)]
964fn ratio(numerator: u64, denominator: u64) -> f64 {
965    let denominator = if denominator == 0 { 1.0 } else { denominator as f64 };
966    numerator as f64 / denominator
967}
968
969fn render_rows(rows: &[(&str, &str, u64)]) -> String {
970    use std::fmt::Write as _;
971
972    let width = rows.iter().map(|(_, label, _)| label.len()).max().unwrap_or(0);
973    let mut output = String::new();
974    let mut previous = None;
975    for (group, label, value) in rows {
976        if previous != Some(*group) {
977            if previous.is_some() {
978                output.push('\n');
979            }
980            let _ = writeln!(output, "[{group}]");
981            previous = Some(*group);
982        }
983        let _ = writeln!(output, "  {label:<width$}  {value:>13}");
984    }
985    output
986}
987
988/// Serialize tests that touch process-global counter state.
989#[cfg(test)]
990pub(crate) fn test_serial() -> std::sync::MutexGuard<'static, ()> {
991    static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
992    SERIAL.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
993}
994
995/// Calling-thread counts for exact unit tests that must ignore unrelated workers.
996#[cfg(test)]
997pub(crate) fn test_thread_snapshot() -> Counts {
998    thread_snapshot()
999}
1000
1001/// Clear only the calling test thread's counts.
1002#[cfg(test)]
1003pub(crate) fn test_thread_reset() {
1004    let _ = LOCAL.try_with(|local| local.counts.set(Counts::default()));
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    use super::*;
1010
1011    #[test]
1012    fn an_exiting_thread_folds_without_a_manual_flush() {
1013        let _serial = test_serial();
1014        enable(true);
1015        reset();
1016
1017        std::thread::spawn(|| bump(|counts| counts.dir_opens = 7)).join().expect("counting worker");
1018
1019        // Other ordinary tests can perform instrumented work while this process-wide
1020        // toggle is on, so only the worker's lower bound is deterministic here.
1021        assert!(snapshot().dir_opens >= 7);
1022        reset();
1023        enable(false);
1024    }
1025
1026    #[test]
1027    fn thread_snapshot_excludes_workers_without_clearing_local_counts() {
1028        let _serial = test_serial();
1029        enable(true);
1030        reset();
1031        bump(|counts| counts.dir_opens = 3);
1032
1033        std::thread::spawn(|| bump(|counts| counts.dir_opens = 7)).join().expect("worker");
1034
1035        assert_eq!(thread_snapshot().dir_opens, 3);
1036        assert_eq!(thread_snapshot().dir_opens, 3, "reading must not consume counts");
1037        assert!(snapshot().dir_opens >= 10);
1038        flush_thread();
1039        assert_eq!(thread_snapshot().dir_opens, 0);
1040        reset();
1041        enable(false);
1042    }
1043
1044    #[test]
1045    fn a_falsey_value_disables_a_previously_enabled_process() {
1046        let _serial = test_serial();
1047        enable(true);
1048
1049        assert!(!enable_from_value(Some(OsStr::new("FALSE"))));
1050        assert!(!enabled());
1051    }
1052
1053    #[test]
1054    fn ratios_treat_a_zero_denominator_as_one() {
1055        let counts = Counts { allocs: 10, ..Counts::default() };
1056        let allocations = counts
1057            .per(0)
1058            .into_iter()
1059            .find(|(_, label, _)| *label == "allocations")
1060            .expect("allocation row");
1061        assert!((allocations.2 - 10.0).abs() < f64::EPSILON);
1062    }
1063}