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;