memscope_rs/task_registry/
ledger.rs1use super::{TaskIdRegistry, TaskLedgerStats};
4use crate::event_store::{MemoryEvent, MemoryEventType};
5use std::collections::{BTreeMap, HashMap};
6
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
9pub struct TaskLifecycleQuality {
10 pub unmatched_reallocations: u64,
11 pub unmatched_deallocations: u64,
12 pub degraded_releases: u64,
13 pub conflicting_allocations: u64,
14 pub unattributed_operations: u64,
15}
16
17impl TaskLifecycleQuality {
18 pub fn is_complete(&self) -> bool {
19 *self == Self::default()
20 }
21}
22
23#[derive(Debug, Default)]
25pub struct TaskAccountingSnapshot {
26 pub stats: BTreeMap<u64, TaskLedgerStats>,
27 pub lifecycle_quality: TaskLifecycleQuality,
28 pub task_lifecycle_quality: BTreeMap<u64, TaskLifecycleQuality>,
29}
30
31struct OpenBinding {
32 ptr: usize,
33 serial: Option<u64>,
34 size: u64,
35 task: Option<u64>,
36}
37
38#[derive(Default)]
39struct TaskBindings {
40 slots: Vec<Option<OpenBinding>>,
41 by_addr: HashMap<usize, Vec<usize>>,
42}
43
44impl TaskBindings {
45 fn record_quality(&self, event: &MemoryEvent, snapshot: &mut TaskAccountingSnapshot) {
46 let mut gap = TaskLifecycleQuality::default();
47 let bucket = self.by_addr.get(&event.ptr);
48 match event.event_type {
49 MemoryEventType::Allocate => {
50 gap.conflicting_allocations = u64::from(bucket.is_some_and(|b| !b.is_empty()));
51 }
52 MemoryEventType::Deallocate | MemoryEventType::Reallocate => {
53 let size = if event.event_type == MemoryEventType::Deallocate {
54 Some(event.size)
55 } else {
56 event.old_size
57 };
58 let unmatched = self.find(event, size).is_none();
59 if event.event_type == MemoryEventType::Deallocate {
60 gap.unmatched_deallocations = u64::from(unmatched);
61 } else {
62 gap.unmatched_reallocations = u64::from(unmatched);
63 }
64 gap.degraded_releases = u64::from(
65 event.degraded_release
66 || (event.binding_serial.is_none() && bucket.is_some_and(|b| b.len() > 1)),
67 );
68 }
69 _ => return,
70 }
71 gap.unattributed_operations = u64::from(event.task_id.is_none());
72 snapshot.lifecycle_quality.add(gap);
73
74 let mut tasks = std::collections::BTreeSet::new();
77 tasks.extend(event.task_id);
78 if let Some(bucket) = bucket {
79 for index in bucket {
80 if let Some(binding) = &self.slots[*index] {
81 if event.binding_serial.is_none()
82 || gap.conflicting_allocations > 0
83 || binding.serial == event.binding_serial
84 {
85 tasks.extend(binding.task);
86 }
87 }
88 }
89 }
90 for task in tasks {
91 snapshot
92 .task_lifecycle_quality
93 .entry(task)
94 .or_default()
95 .add(gap);
96 }
97 }
98
99 fn push(&mut self, binding: OpenBinding) {
100 self.by_addr
101 .entry(binding.ptr)
102 .or_default()
103 .push(self.slots.len());
104 self.slots.push(Some(binding));
105 }
106
107 fn find(&self, event: &MemoryEvent, size: Option<usize>) -> Option<usize> {
108 let bucket = self.by_addr.get(&event.ptr)?;
109 if let Some(serial) = event.binding_serial {
110 return bucket.iter().copied().find(|index| {
113 self.slots[*index]
114 .as_ref()
115 .is_some_and(|binding| binding.serial == Some(serial))
116 });
117 }
118 bucket
119 .iter()
120 .copied()
121 .find(|index| {
122 self.slots[*index].as_ref().is_some_and(|binding| {
123 size.is_none_or(|size| size == 0 || binding.size == size as u64)
124 })
125 })
126 .or_else(|| bucket.first().copied())
127 }
128
129 fn close(&mut self, index: usize) -> Option<OpenBinding> {
130 let binding = self.slots[index].take()?;
131 if let Some(bucket) = self.by_addr.get_mut(&binding.ptr) {
132 bucket.retain(|candidate| *candidate != index);
133 if bucket.is_empty() {
134 self.by_addr.remove(&binding.ptr);
135 }
136 }
137 Some(binding)
138 }
139}
140
141impl TaskIdRegistry {
142 pub fn task_stats_from_events(events: &[MemoryEvent]) -> BTreeMap<u64, TaskLedgerStats> {
148 Self::task_accounting_from_events(events).stats
149 }
150
151 pub fn task_accounting_from_events(events: &[MemoryEvent]) -> TaskAccountingSnapshot {
153 let mut bindings = TaskBindings::default();
154 let mut snapshot = TaskAccountingSnapshot::default();
155 let mut stats: BTreeMap<u64, TaskLedgerStats> = BTreeMap::new();
156 for event in events {
157 bindings.record_quality(event, &mut snapshot);
158 match event.event_type {
159 MemoryEventType::Allocate => {
160 open_observed(&mut bindings, &mut stats, event, event.ptr)
161 }
162 MemoryEventType::Deallocate => {
163 let Some(index) = bindings.find(event, Some(event.size)) else {
164 continue;
165 };
166 let Some(closed) = bindings.close(index) else {
167 continue;
168 };
169 if let Some(actor_id) = event.task_id {
170 let actor = stats.entry(actor_id).or_default();
171 actor.deallocation_count += 1;
172 actor.deallocated_bytes += closed.size;
173 }
174 if let Some(owner) = closed.task.and_then(|id| stats.get_mut(&id)) {
175 owner.live_bytes = owner.live_bytes.saturating_sub(closed.size);
176 }
177 }
178 MemoryEventType::Reallocate => {
179 let new_ptr = event.new_ptr.unwrap_or(event.ptr);
180 let Some(index) = bindings.find(event, event.old_size) else {
181 open_observed(&mut bindings, &mut stats, event, new_ptr);
184 continue;
185 };
186 let migrating = new_ptr != event.ptr;
187 let binding = if migrating {
188 bindings.close(index)
189 } else {
190 bindings.slots[index].take()
191 };
192 let Some(mut binding) = binding else {
193 continue;
194 };
195 if let Some(owner) = binding.task.and_then(|id| stats.get_mut(&id)) {
196 owner.live_bytes = owner
197 .live_bytes
198 .saturating_sub(binding.size)
199 .saturating_add(event.size as u64);
200 }
201 if let Some(actor_id) = event.task_id {
202 let actor = stats.entry(actor_id).or_default();
203 actor.allocated_bytes += event.size as u64;
204 actor.deallocated_bytes += binding.size;
205 }
206 binding.ptr = new_ptr;
207 binding.size = event.size as u64;
208 if migrating {
209 bindings.push(binding);
210 } else {
211 bindings.slots[index] = Some(binding);
213 }
214 }
215 _ => {}
216 }
217 }
218 snapshot.stats = stats;
219 snapshot
220 }
221}
222
223impl TaskLifecycleQuality {
224 fn add(&mut self, other: Self) {
225 self.unmatched_reallocations += other.unmatched_reallocations;
226 self.unmatched_deallocations += other.unmatched_deallocations;
227 self.degraded_releases += other.degraded_releases;
228 self.conflicting_allocations += other.conflicting_allocations;
229 self.unattributed_operations += other.unattributed_operations;
230 }
231}
232
233fn open_observed(
234 bindings: &mut TaskBindings,
235 stats: &mut BTreeMap<u64, TaskLedgerStats>,
236 event: &MemoryEvent,
237 ptr: usize,
238) {
239 if let Some(task_id) = event.task_id {
240 let entry = stats.entry(task_id).or_default();
241 entry.allocation_count += 1;
242 entry.allocated_bytes += event.size as u64;
243 entry.live_bytes = entry.live_bytes.saturating_add(event.size as u64);
244 }
245 bindings.push(OpenBinding {
246 ptr,
247 serial: event.binding_serial,
248 size: event.size as u64,
249 task: event.task_id,
250 });
251}