Skip to main content

memscope_rs/task_registry/
charge_quality.rs

1//! Charge evidence is captured with allocation events, never read from a later live cache.
2
3use super::{ChargeOutcome, ChargeQuality, TaskIdRegistry};
4use crate::event_store::{MemoryEvent, MemoryEventType};
5use std::collections::BTreeMap;
6
7impl TaskIdRegistry {
8    /// Project window and per-task charge quality from one immutable event cut.
9    ///
10    /// Reallocations and annotations do not make another registry charge attempt. Legacy
11    /// allocation events without an outcome remain readable, but cannot certify completeness.
12    pub fn charge_quality_from_events(
13        events: &[MemoryEvent],
14    ) -> (ChargeQuality, BTreeMap<u64, ChargeQuality>) {
15        let mut window = ChargeQuality::default();
16        let mut tasks: BTreeMap<u64, ChargeQuality> = BTreeMap::new();
17        for event in events
18            .iter()
19            .filter(|event| event.event_type == MemoryEventType::Allocate)
20        {
21            window.record(event.charge_outcome, event.size as u64);
22            if let Some(task) = event.task_id {
23                tasks
24                    .entry(task)
25                    .or_default()
26                    .record(event.charge_outcome, event.size as u64);
27            }
28        }
29        (window, tasks)
30    }
31}
32
33impl ChargeQuality {
34    fn record(&mut self, outcome: Option<ChargeOutcome>, bytes: u64) {
35        match outcome {
36            Some(ChargeOutcome::Charged) => {}
37            Some(ChargeOutcome::Deferred) => {
38                self.deferred_charges += 1;
39                self.deferred_bytes += bytes;
40            }
41            Some(ChargeOutcome::Unattributed) => {
42                self.unattributed_charges += 1;
43                self.unattributed_bytes += bytes;
44            }
45            None => {
46                self.unknown_charges += 1;
47                self.unknown_bytes += bytes;
48            }
49        }
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    /// Objective: Preserve per-task gaps and unknown legacy evidence through serialization.
58    /// Invariants: only allocations contribute charges; zero-byte attempts still count.
59    #[test]
60    fn charge_evidence_round_trips_without_inventing_completeness() {
61        let mut events = Vec::new();
62        for (task, size, outcome) in [
63            (Some(1), 64, Some(ChargeOutcome::Charged)),
64            (Some(2), 128, Some(ChargeOutcome::Deferred)),
65            (Some(2), 0, Some(ChargeOutcome::Deferred)),
66            (None, 32, Some(ChargeOutcome::Unattributed)),
67            (Some(3), 16, None),
68        ] {
69            let mut event = MemoryEvent::allocate(events.len(), size, 1);
70            event.task_id = task;
71            event.charge_outcome = outcome;
72            events.push(event);
73        }
74        // Other event kinds have no new charge attempt, even if an external producer
75        // erroneously copied a charge field into one of them.
76        let mut release = MemoryEvent::deallocate(1, 128, 1);
77        release.charge_outcome = Some(ChargeOutcome::Deferred);
78        events.push(release);
79        events.push(MemoryEvent::reallocate_moved(0, 8, 64, 256, 1));
80        let encoded = serde_json::to_string(&events).expect("serialize charge evidence");
81        let restored: Vec<MemoryEvent> =
82            serde_json::from_str(&encoded).expect("read captured events");
83        let (window, tasks) = TaskIdRegistry::charge_quality_from_events(&restored);
84        assert_eq!(
85            (window.deferred_charges, window.deferred_bytes),
86            (2, 128),
87            "zero-byte attempts must remain visible"
88        );
89        assert_eq!(
90            (window.unattributed_charges, window.unattributed_bytes),
91            (1, 32),
92            "unattributed bytes belong to the window"
93        );
94        assert_eq!(
95            (window.unknown_charges, window.unknown_bytes),
96            (1, 16),
97            "legacy evidence cannot certify a charge"
98        );
99        assert!(
100            tasks[&1].is_complete(),
101            "known successful charge has no gap"
102        );
103        assert!(
104            !tasks[&2].is_complete(),
105            "deferred task remains a lower bound"
106        );
107        assert!(!tasks[&3].is_complete(), "legacy task remains unknown");
108        assert_eq!(
109            tasks.len(),
110            3,
111            "unattributed charges must not invent a task ID"
112        );
113        assert!(
114            !window.is_complete(),
115            "window must expose every kind of gap"
116        );
117    }
118}