Skip to main content

ixa/profiling/
mod.rs

1//! This module provides a lightweight, feature-gated profiling interface for simulations.
2//! It tracks event counts and measures elapsed time for named operations ("spans"), and can
3//! export the results to the console and to a JSON file together with execution statistics. It supports:
4//!
5//! - Event counting – track how often named events occur during a run.
6//! - Rate calculation – compute rates (events per second) since the first count.
7//! - Span timing – measure time intervals with automatic closing on drop.
8//! - Query timing – aggregate entity query runtime by query shape.
9//! - Coverage – report how much of total runtime is covered by any span via a special
10//!   "Total Measured" span.
11//! - Computed statistics – define custom, derived metrics over collected data.
12//! - A default computed statistic, infection forecasting efficiency.
13//!
14//! Feature flag: all functionality is gated behind the `profiling` feature (disabled by default).
15//! When the feature is disabled, the public API remains available but becomes a no-op and gets
16//! optimized away by the compiler, so you can leave profiling calls in your code at zero cost.
17//!
18//! ## Example console output
19//! ```ignore
20//! Span Label                           Count          Duration  % runtime
21//! ----------------------------------------------------------------------
22//! load_synth_population                    1       950us 792ns      0.36%
23//! infection_attempt                     1035     6ms 33us 91ns      2.28%
24//! sample_setting                        1035     3ms 66us 52ns      1.16%
25//! get_contact                           1035   1ms 135us 202ns      0.43%
26//! schedule_next_forecasted_infection    1286  22ms 329us 102ns      8.44%
27//! Total Measured                        1385  23ms 897us 146ns      9.03%
28//!
29//! Query                    Count        Total          Min          Max
30//! --------------------------------------------------------------------
31//! Person: (Age, County)      150   12ms 340us   10us 120ns  250us 450ns
32//!
33//! Event Label                     Count  Rate (per sec)
34//! -----------------------------------------------------
35//! property progression               36          136.05
36//! recovery                           27          102.04
37//! accepted infection attempt      1,035        3,911.50
38//! forecasted infection            1,286        4,860.09
39//!
40//! Infection Forecasting Efficiency: 80.48%
41//! ```
42//!
43//! ## API functions
44//! - `increment_named_count`
45//! - `open_span`
46//! - `close_span`
47//! - `print_profiling_data`
48//! - `print_named_counts`
49//! - `print_named_spans`
50//! - `print_computed_statistics`
51//! - `add_computed_statistic`
52//!
53//! All of the above functions are no-ops without the `profiling` feature.
54//!
55//! ## Basic usage
56//!
57//! Count an event:
58//! ```rust,ignore
59//! increment_named_count("forecasted infection");
60//! increment_named_count("accepted infection attempt");
61//! ```
62//!
63//! Time an operation:
64//! ```rust,ignore
65//! let span = open_span("forecast loop");
66//! // operation code here (algorithm, function call, etc.)
67//! close_span(span); // optional; dropping the span also closes it
68//! ```
69//!
70//! You can also rely on RAII to auto-close a span at the end of scope:
71//! ```rust,ignore
72//! fn complicated_function() {
73//!     let _span = open_span("complicated function");
74//!     // Complicated control flow here, maybe with lots of `return` points.
75//! } // `_span` goes out of scope, automatically closed.
76//! ```
77//!
78//! Printing results to the console:
79//! ```rust,ignore
80//! // Call after the simulation completes
81//! print_profiling_data();
82//! ```
83//! Prints spans, counts, and any computed statistics via the functions
84//! `print_named_spans()`, `print_named_counts()`, and `print_computed_statistics()`, which
85//! you can use individually if you prefer. Context-level reporting also includes query timings.
86//!
87//! Writing results to JSON together with execution statistics:
88//! ```rust,ignore
89//! use ixa::Context; // your simulation context
90//! use crate::profiling::ProfilingContextExt;
91//!
92//! fn finalize(mut context: Context) {
93//!     // Ensure Params::profiling_data_path is set, and report options specify
94//!     // output_dir/file_prefix/overwrite. This writes a pretty JSON file with:
95//!     //   date_time, execution_statistics, named_counts, named_spans,
96//!     //   query_timings, computed_statistics
97//!     context.write_profiling_data();
98//! }
99//! ```
100//!
101//! Special names and coverage
102//! - Spans may overlap or nest. The sum of all individual span durations will not
103//!   generally equal total runtime. A special span named `"Total Measured"` is open
104//!   if and only if any other span is open. It tells you how much of the total running
105//!   time is covered by some span.
106//!
107//! ## Computed statistics
108//!
109//! You can register custom computed statistics that derive values from the current
110//! `ProfilingData`. Use `add_computed_statistic(label, description, computer, printer)`
111//! to add one. The relevant API is:
112//!
113//! ```rust, ignore
114//! // Not exactly as implemented for technical reasons.
115//! pub fn add_computed_statistic(
116//!     // The label used in the profiling JSON file
117//!     label: &'static str,
118//!     /// Description of the statistic. Used in the JSON report.
119//!     description: &'static str,
120//!     /// A function that takes a reference to the `ProfilingData` and computes a value
121//!     computer: CustomStatisticComputer,
122//!     /// A function that prints the computed value to the console.
123//!     printer: CustomStatisticPrinter,
124//! );
125//!
126//! pub type CustomStatisticComputer<T> =
127//!     Box<dyn (Fn(&ProfilingData) -> Option<T>) + Send + Sync>;
128//! pub type CustomStatisticPrinter<T> = Box<dyn (Fn(T)) + Send + Sync>;
129//!
130//! // The "computer" gets an immutable reference to all counts and spans and to the start time.
131//! pub fn add_computed_statistic<T: ComputableType>(
132//!     label: &'static str,
133//!     description: &'static str,
134//!     computer: CustomStatisticComputer<T>,
135//!     printer: CustomStatisticPrinter<T>,
136//! )
137//! ```
138//!
139//! The "computer" returns an option for cases when a statistic is only conditionally
140//! defined. The "printer" takes the computed value and prints it to the console.
141//!
142//! Computed statistics are printed by `print_computed_statistics()` and included in the
143//! JSON report under `computed_statistics` (with label, description, and value).
144//!
145//!
146//! Example of using `"forecasted infection"` and `"accepted infection attempt"`.
147//! ```rust,ignore
148//! context.add_plan(next_time, move |context| {
149//!     increment_named_count("forecasted infection");
150//!     if evaluate_forecast(context, person, forecasted_total_infectiousness) {
151//!         if let Some(setting_id) = context.get_setting_for_contact(person) {
152//!             if let Some(next_contact) = infection_attempt(context, person, setting_id) {
153//!                 increment_named_count("accepted infection attempt");
154//!                 context.infect_person(next_contact, Some(person), None, None);
155//!             }
156//!         }
157//!     }
158//!     schedule_next_forecasted_infection(context, person);
159//! });
160//! ```
161#![allow(dead_code)]
162#![allow(unused_imports)]
163
164mod computed_statistic;
165mod data;
166mod display;
167mod file;
168mod reporting;
169
170use std::path::Path;
171
172pub use computed_statistic::*;
173pub use data::*;
174pub use display::*;
175use file::write_profiling_data_to_file;
176pub use reporting::*;
177
178pub use crate::execution_stats::ProfilingInstant;
179use crate::{error, Context, ContextReportExt};
180
181#[cfg(all(test, feature = "profiling"))]
182/// Publicly expose access to profiling data only for testing.
183pub fn get_profiling_data() -> std::sync::MutexGuard<'static, ProfilingData> {
184    profiling_data()
185}
186
187// "Magic" constants used in this module
188/// The distinguished total measured time label.
189#[cfg(feature = "profiling")]
190const TOTAL_MEASURED: &str = "Total Measured";
191#[cfg(feature = "profiling")]
192const NAMED_SPANS_HEADERS: &[&str] = &["Span Label", "Count", "Duration", "% runtime"];
193#[cfg(feature = "profiling")]
194const NAMED_COUNTS_HEADERS: &[&str] = &["Event Label", "Count", "Rate (per sec)"];
195#[cfg(feature = "profiling")]
196const QUERY_TIMINGS_HEADERS: &[&str] = &["Query", "Count", "Total", "Min", "Max"];
197
198pub struct Span {
199    #[cfg(feature = "profiling")]
200    label: &'static str,
201    #[cfg(feature = "profiling")]
202    start_time: ProfilingInstant,
203}
204
205impl Span {
206    fn new(#[allow(unused_variables)] label: &'static str) -> Self {
207        Self {
208            #[cfg(feature = "profiling")]
209            label,
210            #[cfg(feature = "profiling")]
211            start_time: ProfilingInstant::now(),
212        }
213    }
214}
215
216#[cfg(feature = "profiling")]
217impl Drop for Span {
218    fn drop(&mut self) {
219        let mut container = profiling_data();
220        container.close_span(self);
221    }
222}