1use 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
64pub 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
92fn 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 Merged,
215 OwnTime,
218}
219
220pub 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 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
258pub(crate) fn end() {
260 with_events(|events| {
261 events.push(Event {
262 time: TimeSample::now(),
263 data: EventData::End,
264 })
265 });
266}
267
268pub(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#[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 f();
286 clear();
287
288 f();
289 save(path)
290}