ommx/logical_memory.rs
1//! Logical memory profiling for OMMX types.
2//!
3//! This module provides a visitor-based approach to profile memory usage
4//! of optimization problem instances. It generates folded stack format
5//! output that can be visualized with flamegraph tools.
6//!
7//! # Design Philosophy
8//!
9//! - **Only leaf nodes emit byte counts**: This avoids inclusive/exclusive calculation complexity
10//! - **Visitor pattern**: Output formats are delegated to visitor implementations
11//! - **Flexible granularity**: Each type decides its own decomposition level
12//!
13//! # Example
14//!
15//! ```rust
16//! use ommx::logical_memory::logical_memory_to_folded;
17//! use ommx::Linear;
18//!
19//! let linear = Linear::default();
20//! let folded = logical_memory_to_folded(&linear);
21//! println!("{}", folded);
22//! ```
23
24mod collections;
25mod path;
26pub use path::{Path, PathGuard};
27
28/// Types that provide logical memory profiling.
29///
30/// Implementations should enumerate their "logical memory leaves" by calling
31/// `visitor.visit_leaf()` for each leaf node, while intermediate nodes should
32/// delegate to their children.
33///
34/// # Recommended Implementation Pattern
35///
36/// Use [`Path::with()`] to create RAII guards that automatically manage path push/pop:
37///
38/// ```rust
39/// use ommx::logical_memory::{LogicalMemoryProfile, LogicalMemoryVisitor, Path};
40/// use std::mem::size_of;
41///
42/// struct MyStruct {
43/// field1: u64,
44/// field2: String,
45/// }
46///
47/// impl LogicalMemoryProfile for MyStruct {
48/// fn visit_logical_memory<V: LogicalMemoryVisitor>(
49/// &self,
50/// path: &mut Path,
51/// visitor: &mut V,
52/// ) {
53/// // Count primitive fields using path guards
54/// visitor.visit_leaf(&path.with("field1"), size_of::<u64>());
55///
56/// // Count String: stack + heap
57/// let field2_bytes = size_of::<String>() + self.field2.len();
58/// visitor.visit_leaf(&path.with("field2"), field2_bytes);
59/// }
60/// }
61/// ```
62pub trait LogicalMemoryProfile {
63 /// Enumerate the "logical memory leaves" of this value.
64 ///
65 /// # Arguments
66 /// - `path`: Logical path to the current node (mutated during recursion)
67 /// - `visitor`: Visitor that receives leaf node callbacks
68 ///
69 /// # Implementation Notes
70 /// - Use `path.with("name")` to create RAII guards for automatic cleanup
71 /// - At leaf nodes: `visitor.visit_leaf(path.with("field"), bytes)`
72 /// - For delegation: `self.field.visit_logical_memory(path.with("field").as_mut(), visitor)`
73 fn visit_logical_memory<V: LogicalMemoryVisitor>(&self, path: &mut Path, visitor: &mut V);
74}
75
76/// Visitor for logical memory leaf nodes.
77pub trait LogicalMemoryVisitor {
78 /// Callback for a single "leaf node" (logical memory chunk).
79 ///
80 /// # Arguments
81 /// - `path`: Logical path (e.g., `&Path::new("Instance").with("objective").with("terms")`)
82 /// - `bytes`: Bytes used by this node
83 fn visit_leaf(&mut self, path: &Path, bytes: usize);
84}
85
86/// Collector for generating folded stack format.
87///
88/// This format is compatible with flamegraph visualization tools like
89/// `flamegraph.pl` and `inferno`.
90///
91/// The collector automatically aggregates multiple visits to the same path,
92/// which is useful when profiling collections (e.g., multiple DecisionVariables
93/// with the same metadata structure).
94#[derive(Debug, Default)]
95pub struct FoldedCollector {
96 // Use HashMap to aggregate same paths
97 aggregated: std::collections::HashMap<String, usize>,
98}
99
100impl FoldedCollector {
101 /// Create a new folded stack collector.
102 pub fn new() -> Self {
103 Self::default()
104 }
105
106 /// Finish collecting and return the folded stack output.
107 ///
108 /// Each line has format: `"frame1;frame2;...;frameN bytes"`
109 /// Lines are sorted for deterministic output.
110 pub fn finish(self) -> String {
111 let mut lines: Vec<_> = self
112 .aggregated
113 .into_iter()
114 .map(|(path, bytes)| format!("{path} {bytes}"))
115 .collect();
116 lines.sort();
117 lines.join("\n")
118 }
119}
120
121impl LogicalMemoryVisitor for FoldedCollector {
122 fn visit_leaf(&mut self, path: &Path, bytes: usize) {
123 if bytes == 0 {
124 return;
125 }
126 let frames = path.as_slice().join(";");
127 *self.aggregated.entry(frames).or_insert(0) += bytes;
128 }
129}
130
131/// Generate folded stack format for a value.
132///
133/// # Arguments
134/// - `value`: Value to profile
135///
136/// # Returns
137/// Folded stack format string, with each line in format `"frame1;frame2;... bytes"`
138///
139/// # Example
140///
141/// ```rust
142/// use ommx::logical_memory::logical_memory_to_folded;
143/// use ommx::Linear;
144///
145/// let linear = Linear::default();
146/// let folded = logical_memory_to_folded(&linear);
147/// // Output: "PolynomialBase.terms 32" (HashMap struct overhead)
148/// assert_eq!(folded, "PolynomialBase.terms 32");
149/// ```
150pub fn logical_memory_to_folded<T: LogicalMemoryProfile>(value: &T) -> String {
151 let mut path = Path::new();
152 let mut collector = FoldedCollector::new();
153 value.visit_logical_memory(&mut path, &mut collector);
154 collector.finish()
155}
156
157/// Calculate total bytes used by a value.
158///
159/// # Arguments
160/// - `value`: Value to profile
161///
162/// # Returns
163/// Total bytes across all leaf nodes
164///
165/// # Example
166///
167/// ```rust
168/// use ommx::logical_memory::logical_total_bytes;
169/// use ommx::Linear;
170///
171/// let linear = Linear::default();
172/// let total = logical_total_bytes(&linear);
173/// // Empty polynomial has only struct overhead
174/// assert!(total > 0);
175/// ```
176pub fn logical_total_bytes<T: LogicalMemoryProfile>(value: &T) -> usize {
177 struct Sum(usize);
178 impl LogicalMemoryVisitor for Sum {
179 fn visit_leaf(&mut self, _path: &Path, bytes: usize) {
180 self.0 += bytes;
181 }
182 }
183
184 let mut path = Path::new();
185 let mut sum = Sum(0);
186 value.visit_logical_memory(&mut path, &mut sum);
187 sum.0
188}
189
190// Macro to implement LogicalMemoryProfile for structs with fields
191/// Generates a LogicalMemoryProfile implementation that delegates to each field.
192///
193/// # Example
194/// ```ignore
195/// impl_logical_memory_profile! {
196/// RemovedConstraint {
197/// constraint,
198/// removed_reason,
199/// removed_reason_parameters,
200/// }
201/// }
202///
203/// // For types with path (e.g., v1::Parameters), specify type name explicitly:
204/// impl_logical_memory_profile! {
205/// v1::Parameters as "Parameters" {
206/// entries,
207/// }
208/// }
209/// ```
210#[macro_export]
211macro_rules! impl_logical_memory_profile {
212 // For types with explicit name (e.g., v1::Parameters as "Parameters")
213 ($type_path:path as $type_name:literal { $($field:ident),* $(,)? }) => {
214 impl $crate::logical_memory::LogicalMemoryProfile for $type_path {
215 fn visit_logical_memory<V: $crate::logical_memory::LogicalMemoryVisitor>(
216 &self,
217 path: &mut $crate::logical_memory::Path,
218 visitor: &mut V,
219 ) {
220 $(
221 self.$field.visit_logical_memory(
222 path.with(concat!($type_name, ".", stringify!($field))).as_mut(),
223 visitor,
224 );
225 )*
226 }
227 }
228 };
229 // For simple types (e.g., RemovedConstraint)
230 ($type_name:ident { $($field:ident),* $(,)? }) => {
231 impl $crate::logical_memory::LogicalMemoryProfile for $type_name {
232 fn visit_logical_memory<V: $crate::logical_memory::LogicalMemoryVisitor>(
233 &self,
234 path: &mut $crate::logical_memory::Path,
235 visitor: &mut V,
236 ) {
237 $(
238 self.$field.visit_logical_memory(
239 path.with(concat!(stringify!($type_name), ".", stringify!($field))).as_mut(),
240 visitor,
241 );
242 )*
243 }
244 }
245 };
246}
247
248// Generic implementations for primitive types
249
250macro_rules! impl_logical_memory_profile_for_primitive {
251 ($($ty:ty),*) => {
252 $(
253 impl LogicalMemoryProfile for $ty {
254 fn visit_logical_memory<V: LogicalMemoryVisitor>(&self, path: &mut Path, visitor: &mut V) {
255 use std::mem::size_of;
256 visitor.visit_leaf(path, size_of::<$ty>());
257 }
258 }
259 )*
260 };
261}
262
263impl_logical_memory_profile_for_primitive!(
264 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64
265);
266
267#[cfg(test)]
268mod tests;