memscope_rs/task_registry/
charge_quality.rs1use super::{ChargeOutcome, ChargeQuality, TaskIdRegistry};
4use crate::event_store::{MemoryEvent, MemoryEventType};
5use std::collections::BTreeMap;
6
7impl TaskIdRegistry {
8 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 #[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 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}