hotpath_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::parse::Parser;
4use syn::{parse_macro_input, ItemFn, LitInt, LitStr};
5
6#[derive(Clone, Copy)]
7enum Format {
8    Table,
9    Json,
10    JsonPretty,
11}
12
13impl Format {
14    fn to_tokens(self) -> proc_macro2::TokenStream {
15        match self {
16            Format::Table => quote!(hotpath::Format::Table),
17            Format::Json => quote!(hotpath::Format::Json),
18            Format::JsonPretty => quote!(hotpath::Format::JsonPretty),
19        }
20    }
21}
22
23/// Initializes the hotpath profiling system and generates a performance report on program exit.
24///
25/// This attribute macro should be applied to your program's main (or other entry point) function to enable profiling.
26/// It creates a guard that initializes the background measurement processing thread and
27/// automatically displays a performance summary when the program exits.
28/// Additionally it creates a measurement guard that will be used to measure the wrapper function itself.
29///
30/// # Parameters
31///
32/// * `percentiles` - Array of percentile values (0-100) to display in the report. Default: `[95]`
33/// * `format` - Output format as a string: `"table"` (default), `"json"`, or `"json-pretty"`
34///
35/// # Examples
36///
37/// Basic usage with default settings (P95 percentile, table format):
38///
39/// ```rust,no_run
40/// #[cfg_attr(feature = "hotpath", hotpath::main)]
41/// fn main() {
42///     // Your code here
43/// }
44/// ```
45///
46/// Custom percentiles:
47///
48/// ```rust,no_run
49/// #[tokio::main]
50/// #[cfg_attr(feature = "hotpath", hotpath::main(percentiles = [50, 90, 95, 99]))]
51/// async fn main() {
52///     // Your code here
53/// }
54/// ```
55///
56/// JSON output format:
57///
58/// ```rust,no_run
59/// #[cfg_attr(feature = "hotpath", hotpath::main(format = "json-pretty"))]
60/// fn main() {
61///     // Your code here
62/// }
63/// ```
64///
65/// Combined parameters:
66///
67/// ```rust,no_run
68/// #[cfg_attr(feature = "hotpath", hotpath::main(percentiles = [50, 99], format = "json"))]
69/// fn main() {
70///     // Your code here
71/// }
72/// ```
73///
74/// # Usage with Tokio
75///
76/// When using with tokio, place `#[tokio::main]` before `#[hotpath::main]`:
77///
78/// ```rust,no_run
79/// #[tokio::main]
80/// #[cfg_attr(feature = "hotpath", hotpath::main)]
81/// async fn main() {
82///     // Your code here
83/// }
84/// ```
85///
86/// # Limitations
87///
88/// Only one hotpath guard can be active at a time. Creating a second guard (either via this
89/// macro or via [`GuardBuilder`](../hotpath/struct.GuardBuilder.html)) will cause a panic.
90///
91/// # See Also
92///
93/// * [`measure`](macro@measure) - Attribute macro for instrumenting functions
94/// * [`measure_block!`](../hotpath/macro.measure_block.html) - Macro for measuring code blocks
95/// * [`GuardBuilder`](../hotpath/struct.GuardBuilder.html) - Manual control over profiling lifecycle
96#[proc_macro_attribute]
97pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
98    let input = parse_macro_input!(item as ItemFn);
99    let vis = &input.vis;
100    let sig = &input.sig;
101    let block = &input.block;
102
103    // Defaults
104    let mut percentiles: Vec<u8> = vec![95];
105    let mut format = Format::Table;
106
107    // Parse named args like: percentiles=[..], format=".."
108    if !attr.is_empty() {
109        let parser = syn::meta::parser(|meta| {
110            if meta.path.is_ident("percentiles") {
111                meta.input.parse::<syn::Token![=]>()?;
112                let content;
113                syn::bracketed!(content in meta.input);
114                let mut vals = Vec::new();
115                while !content.is_empty() {
116                    let li: LitInt = content.parse()?;
117                    let v: u8 = li.base10_parse()?;
118                    if !(0..=100).contains(&v) {
119                        return Err(
120                            meta.error(format!("Invalid percentile {} (must be 0..=100)", v))
121                        );
122                    }
123                    vals.push(v);
124                    if !content.is_empty() {
125                        content.parse::<syn::Token![,]>()?;
126                    }
127                }
128                if vals.is_empty() {
129                    return Err(meta.error("At least one percentile must be specified"));
130                }
131                percentiles = vals;
132                return Ok(());
133            }
134
135            if meta.path.is_ident("format") {
136                meta.input.parse::<syn::Token![=]>()?;
137                let lit: LitStr = meta.input.parse()?;
138                format =
139                    match lit.value().as_str() {
140                        "table" => Format::Table,
141                        "json" => Format::Json,
142                        "json-pretty" => Format::JsonPretty,
143                        other => return Err(meta.error(format!(
144                            "Unknown format {:?}. Expected one of: \"table\", \"json\", \"json-pretty\"",
145                            other
146                        ))),
147                    };
148                return Ok(());
149            }
150
151            Err(meta.error("Unknown parameter. Supported: percentiles=[..], format=\"..\""))
152        });
153
154        if let Err(e) = parser.parse2(proc_macro2::TokenStream::from(attr)) {
155            return e.to_compile_error().into();
156        }
157    }
158
159    let percentiles_array = quote! { &[#(#percentiles),*] };
160    let format_token = format.to_tokens();
161
162    let asyncness = sig.asyncness.is_some();
163    let fn_name = &sig.ident;
164    let measurement_name = quote! { concat!(module_path!(), "::", stringify!(#fn_name)) };
165
166    let output = if asyncness {
167        quote! {
168            #vis #sig {
169                async {
170                    let _hotpath = {
171                        fn __caller_fn() {}
172                        let caller_name = std::any::type_name_of_val(&__caller_fn)
173                            .strip_suffix("::__caller_fn")
174                            .unwrap_or(std::any::type_name_of_val(&__caller_fn))
175                            .replace("::{{closure}}", "");
176
177                        hotpath::GuardBuilder::new(caller_name.to_string())
178                            .percentiles(#percentiles_array)
179                            .format(#format_token)
180                            .build()
181                    };
182
183                    hotpath::cfg_if! {
184                        if #[cfg(feature = "hotpath-off")] {
185                            // No-op when hotpath-off is enabled
186                        } else if #[cfg(any(
187                            feature = "hotpath-alloc-bytes-total",
188                            feature = "hotpath-alloc-count-total"
189                        ))] {
190                            use hotpath::{Handle, RuntimeFlavor};
191                            let runtime_flavor = Handle::try_current().ok().map(|h| h.runtime_flavor());
192
193                            let _measure_guard = match runtime_flavor {
194                                Some(RuntimeFlavor::CurrentThread) => {
195                                    hotpath::AllocGuardType::AllocGuard(hotpath::AllocGuard::new(#measurement_name, true))
196                                }
197                                _ => {
198                                    hotpath::AllocGuardType::NoopAsyncAllocGuard(hotpath::NoopAsyncAllocGuard::new(#measurement_name, true))
199                                }
200                            };
201                        } else {
202                            let _measure_guard = hotpath::TimeGuard::new(#measurement_name, true);
203                        }
204                    }
205
206                    #block
207                }.await
208            }
209        }
210    } else {
211        quote! {
212            #vis #sig {
213                let _hotpath = {
214                    fn __caller_fn() {}
215                    let caller_name = std::any::type_name_of_val(&__caller_fn)
216                        .strip_suffix("::__caller_fn")
217                        .unwrap_or(std::any::type_name_of_val(&__caller_fn))
218                        .replace("::{{closure}}", "");
219
220                    hotpath::GuardBuilder::new(caller_name.to_string())
221                        .percentiles(#percentiles_array)
222                        .format(#format_token)
223                        .build()
224                };
225
226                hotpath::cfg_if! {
227                    if #[cfg(feature = "hotpath-off")] {
228                        // No-op when hotpath-off is enabled
229                    } else if #[cfg(any(
230                        feature = "hotpath-alloc-bytes-total",
231                        feature = "hotpath-alloc-count-total"
232                    ))] {
233                        let _measure_guard = hotpath::AllocGuard::new(#measurement_name, true);
234                    } else {
235                        let _measure_guard = hotpath::TimeGuard::new(#measurement_name, true);
236                    }
237                }
238
239                #block
240            }
241        }
242    };
243
244    output.into()
245}
246
247/// Instruments a function to send performance measurements to the hotpath profiler.
248///
249/// This attribute macro wraps functions with profiling code that measures execution time
250/// or memory allocations (depending on enabled feature flags). The measurements are sent
251/// to a background processing thread for aggregation.
252///
253/// # Behavior
254///
255/// The macro automatically detects whether the function is sync or async and instruments
256/// it appropriately. Measurements include:
257///
258/// * **Time profiling** (default): Execution duration using high-precision timers
259/// * **Allocation profiling**: Memory allocations when allocation features are enabled
260///   - `hotpath-alloc-bytes-total` - Total bytes allocated
261///   - `hotpath-alloc-count-total` - Total allocation count
262///
263/// # Async Function Limitations
264///
265/// When using allocation profiling features with async functions, you must use the
266/// `tokio` runtime in `current_thread` mode:
267///
268/// ```rust,no_run
269/// #[tokio::main(flavor = "current_thread")]
270/// async fn main() {
271///     // Your async code here
272/// }
273/// ```
274///
275/// This limitation exists because allocation tracking uses thread-local storage. In multi-threaded
276/// runtimes, async tasks can migrate between threads, making it impossible to accurately
277/// attribute allocations to specific function calls. Time-based profiling works with any runtime flavor.
278///
279/// When the `hotpath` feature is disabled, this macro compiles to zero overhead (no instrumentation).
280///
281/// # See Also
282///
283/// * [`main`](macro@main) - Attribute macro that initializes profiling
284/// * [`measure_block!`](../hotpath/macro.measure_block.html) - Macro for measuring code blocks
285#[proc_macro_attribute]
286pub fn measure(_attr: TokenStream, item: TokenStream) -> TokenStream {
287    let input = parse_macro_input!(item as ItemFn);
288    let vis = &input.vis;
289    let sig = &input.sig;
290    let block = &input.block;
291
292    let name = sig.ident.to_string();
293    let asyncness = sig.asyncness.is_some();
294
295    let output = if asyncness {
296        quote! {
297            #vis #sig {
298                async {
299                    hotpath::cfg_if! {
300                        if #[cfg(feature = "hotpath-off")] {
301                            // No-op when hotpath-off is enabled
302                        } else if #[cfg(any(
303                            feature = "hotpath-alloc-bytes-total",
304                            feature = "hotpath-alloc-count-total"
305                        ))] {
306                            use hotpath::{Handle, RuntimeFlavor};
307                            let runtime_flavor = Handle::try_current().ok().map(|h| h.runtime_flavor());
308
309                            let _guard = match runtime_flavor {
310                                Some(RuntimeFlavor::CurrentThread) => {
311                                    hotpath::AllocGuardType::AllocGuard(hotpath::AllocGuard::new(concat!(module_path!(), "::", #name), false))
312                                }
313                                _ => {
314                                    hotpath::AllocGuardType::NoopAsyncAllocGuard(hotpath::NoopAsyncAllocGuard::new(concat!(module_path!(), "::", #name), false))
315                                }
316                            };
317                        } else {
318                            let _guard = hotpath::TimeGuard::new(concat!(module_path!(), "::", #name), false);
319                        }
320                    }
321
322                    #block
323                }.await
324            }
325        }
326    } else {
327        quote! {
328            #vis #sig {
329                hotpath::cfg_if! {
330                    if #[cfg(feature = "hotpath-off")] {
331                        // No-op when hotpath-off is enabled
332                    } else if #[cfg(any(
333                        feature = "hotpath-alloc-bytes-total",
334                        feature = "hotpath-alloc-count-total"
335                    ))] {
336                        let _guard = hotpath::AllocGuard::new(concat!(module_path!(), "::", #name), false);
337                    } else {
338                        let _guard = hotpath::TimeGuard::new(concat!(module_path!(), "::", #name), false);
339                    }
340                }
341
342                #block
343            }
344        }
345    };
346
347    output.into()
348}