firestorm_enabled/
lib.rs

1// The backwards compatability policy:
2// When core needs to change:
3//    It can release a new version
4//    And every point release of previous APIs can be upgraded to use it.
5//    So long as exact versions are never relied upon this should work.
6//
7// When a new version comes out, it needs to enable the previous version
8//    (which happens recursively)
9//    It would be good if this only brought in the profiling part and not
10//    the drawing part, but I can fix that later.
11
12use std::{error::Error, fs::create_dir_all, io::Write, path::Path};
13
14extern crate inferno;
15
16use {firestorm_core::*, inferno::flamegraph, std::collections::HashMap};
17pub mod internal;
18
19#[macro_export]
20macro_rules! profile_fn {
21    ($($t:tt)*) => {
22        let _firestorm_fn_guard = {
23            let event_data = $crate::internal::EventData::Start(
24                $crate::internal::Start::Func {
25                    signature: &stringify!($($t)*),
26                }
27            );
28            $crate::internal::start(event_data);
29            $crate::internal::SpanGuard::new()
30        };
31    };
32}
33
34#[macro_export]
35macro_rules! profile_method {
36    ($($t:tt)*) => {
37        let _firestorm_method_guard = {
38            let event_data = $crate::internal::EventData::Start(
39                $crate::internal::Start::Method {
40                    signature: &stringify!($($t)*),
41                    typ: ::std::any::type_name::<Self>(),
42                }
43            );
44            $crate::internal::start(event_data);
45            $crate::internal::SpanGuard::new()
46        };
47    };
48}
49
50#[macro_export]
51macro_rules! profile_section {
52    ($name:ident) => {
53        #[allow(unused_variables)]
54        let $name = {
55            let event_data = $crate::internal::EventData::Start($crate::internal::Start::Section {
56                name: &stringify!($name),
57            });
58            $crate::internal::start(event_data);
59            $crate::internal::SpanGuard::new()
60        };
61    };
62}
63
64/// Clears all of the recorded info that firestorm has tracked in this thread.
65pub fn clear() {
66    with_events(|e| e.clear());
67}
68
69fn inferno_valid_chars(s: &str) -> String {
70    s.replace(";", "").replace(" ", "")
71}
72
73fn format_start(tag: &Start) -> String {
74    let mut s = String::new();
75    match tag {
76        Start::Method { typ, signature } => {
77            s += typ;
78            s += "::";
79            s += signature;
80        }
81        Start::Func { signature } => {
82            s += signature;
83        }
84        Start::Section { name } => {
85            s += name;
86        }
87        _ => s += "Unsupported",
88    }
89    s
90}
91
92/// Convert events to the format that inferno is expecting
93fn lines(mode: Mode) -> Vec<String> {
94    with_events(|events| {
95        struct Frame {
96            name: String,
97            start: TimeSample,
98        }
99        struct Line {
100            name: String,
101            duration: u64,
102        }
103
104        let mut stack = Vec::<Frame>::new();
105        let mut collapsed = HashMap::<_, u64>::new();
106        let mut lines = Vec::<Line>::new();
107
108        for event in events.iter() {
109            let time = event.time;
110            match &event.data {
111                EventData::Start(tag) => {
112                    let mut s = format_start(tag);
113                    s = inferno_valid_chars(&s);
114                    if let Some(parent) = stack.last() {
115                        if !matches!(mode, Mode::OwnTime) {
116                            s = format!("{};{}", &parent.name, s);
117                        }
118                        if mode == Mode::TimeAxis {
119                            lines.push(Line {
120                                name: parent.name.clone(),
121                                duration: time - parent.start,
122                            });
123                        }
124                    }
125                    let frame = Frame {
126                        name: s,
127                        start: time,
128                    };
129                    stack.push(frame);
130                }
131                EventData::End => {
132                    let Frame { name, start } = stack.pop().unwrap();
133                    let elapsed = time - start;
134                    match mode {
135                        Mode::Merged | Mode::OwnTime => {
136                            let entry = collapsed.entry(name).or_default();
137                            *entry = entry.wrapping_add(elapsed);
138                            if let Some(parent) = stack.last() {
139                                let entry = collapsed.entry(parent.name.clone()).or_default();
140                                *entry = entry.wrapping_sub(elapsed);
141                            }
142                        }
143                        Mode::TimeAxis => {
144                            lines.push(Line {
145                                name,
146                                duration: elapsed,
147                            });
148                            if let Some(parent) = stack.last_mut() {
149                                parent.start = time;
150                            }
151                        }
152                    }
153                }
154                _ => panic!("Unsupported event data. Update Firestorm."),
155            }
156        }
157        assert!(stack.is_empty(), "Mimatched start/end");
158
159        fn format_line(name: &str, duration: &u64) -> Option<String> {
160            if *duration == 0 {
161                None
162            } else {
163                Some(format!("{} {}", name, duration))
164            }
165        }
166
167        match mode {
168            Mode::Merged => collapsed
169                .iter()
170                .filter_map(|(name, duration)| format_line(name, duration))
171                .collect(),
172            Mode::TimeAxis => lines
173                .iter()
174                .filter_map(|Line { name, duration }| format_line(name, duration))
175                .collect(),
176            Mode::OwnTime => {
177                let mut collapsed: Vec<_> = collapsed
178                    .into_iter()
179                    .filter(|(_, duration)| *duration != 0)
180                    .collect();
181                collapsed.sort_by_key(|(_, duration)| u64::MAX - *duration);
182
183                for i in 1..collapsed.len() {
184                    collapsed[i - 1].1 -= collapsed[i].1;
185                }
186
187                let mut collapsed = collapsed.into_iter();
188                let mut lines = Vec::new();
189                if let Some(item) = collapsed.next() {
190                    let mut name = item.0.clone();
191                    if let Some(line) = format_line(&name, &item.1) {
192                        lines.push(line);
193                    }
194                    for item in collapsed {
195                        name = format!("{};{}", name, &item.0);
196                        if let Some(line) = format_line(&name, &item.1) {
197                            lines.push(line);
198                        }
199                    }
200                }
201                lines
202            }
203        }
204    })
205}
206
207#[derive(Copy, Clone, Eq, PartialEq, Debug)]
208enum Mode {
209    TimeAxis,
210    /// Merges all instances of the same stack into a single bar.
211    /// This may give a better overview of, for example, how much total
212    /// time a method took. But, will not retain information like how
213    /// many times a method was called.
214    Merged,
215    /// The stacks have nothing to do with callstacks, this is just a
216    /// bar graph on it's side.
217    OwnTime,
218}
219
220/// Save the flamegraph to a folder.
221pub fn save<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn Error>> {
222    let data_dir = path.as_ref().join("firestorm");
223    create_dir_all(&data_dir)?;
224    for (mode, name) in [
225        (Mode::OwnTime, "owntime"),
226        (Mode::TimeAxis, "timeaxis"),
227        (Mode::Merged, "merged"),
228    ]
229    .iter()
230    {
231        let lines = lines(*mode);
232
233        let mut fg_opts = flamegraph::Options::default();
234        fg_opts.title = "".to_owned();
235        fg_opts.hash = true;
236        fg_opts.count_name = "nanoseconds".to_owned();
237        // https://github.com/jonhoo/inferno/issues/243
238        //fg_opts.count_name = "μs".to_owned();
239        //fg_opts.factor = 0.001;
240        fg_opts.flame_chart = matches!(mode, Mode::TimeAxis);
241        let name = data_dir.join(name.to_string() + ".svg");
242        let mut writer = std::fs::File::create(name)?;
243        flamegraph::from_lines(
244            &mut fg_opts,
245            lines.iter().rev().map(|s| s.as_str()),
246            &mut writer,
247        )?;
248    }
249
250    let name = path.as_ref().join("firestorm.html");
251    let mut writer = std::fs::File::create(name)?;
252    let html = include_bytes!("firestorm.html");
253    writer.write_all(html)?;
254
255    Ok(())
256}
257
258/// Finish profiling a section.
259pub(crate) fn end() {
260    with_events(|events| {
261        events.push(Event {
262            time: TimeSample::now(),
263            data: EventData::End,
264        })
265    });
266}
267
268/// Unsafe! This MUST not be used recursively
269/// TODO: Verify in Debug this is not used recursively
270pub(crate) fn with_events<T>(f: impl FnOnce(&mut Vec<Event>) -> T) -> T {
271    EVENTS.with(|e| {
272        let r = unsafe { &mut *e.get() };
273        f(r)
274    })
275}
276
277/// Returns whether or not firestorm is enabled
278#[inline(always)]
279pub const fn enabled() -> bool {
280    true
281}
282
283pub fn bench<F: Fn(), P: AsRef<Path>>(path: P, f: F) -> Result<(), Box<dyn Error>> {
284    // Warmup - pre-allocates memory for samples
285    f();
286    clear();
287
288    f();
289    save(path)
290}