hotpath_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::parse::Parser;
4use syn::{parse_macro_input, ImplItem, Item, 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/// * `limit` - Maximum number of functions to display in the report (0 = show all). Default: `15`
35/// * `timeout` - Optional timeout in milliseconds. If specified, the program will print the report and exit after the timeout.
36///
37/// # Examples
38///
39/// Basic usage with default settings (P95 percentile, table format):
40///
41/// ```rust,no_run
42/// #[cfg_attr(feature = "hotpath", hotpath::main)]
43/// fn main() {
44///     // Your code here
45/// }
46/// ```
47///
48/// Custom percentiles:
49///
50/// ```rust,no_run
51/// #[tokio::main]
52/// #[cfg_attr(feature = "hotpath", hotpath::main(percentiles = [50, 90, 95, 99]))]
53/// async fn main() {
54///     // Your code here
55/// }
56/// ```
57///
58/// JSON output format:
59///
60/// ```rust,no_run
61/// #[cfg_attr(feature = "hotpath", hotpath::main(format = "json-pretty"))]
62/// fn main() {
63///     // Your code here
64/// }
65/// ```
66///
67/// Combined parameters:
68///
69/// ```rust,no_run
70/// #[cfg_attr(feature = "hotpath", hotpath::main(percentiles = [50, 99], format = "json"))]
71/// fn main() {
72///     // Your code here
73/// }
74/// ```
75///
76/// Custom limit (show top 20 functions):
77///
78/// ```rust,no_run
79/// #[cfg_attr(feature = "hotpath", hotpath::main(limit = 20))]
80/// fn main() {
81///     // Your code here
82/// }
83/// ```
84///
85/// # Usage with Tokio
86///
87/// When using with tokio, place `#[tokio::main]` before `#[hotpath::main]`:
88///
89/// ```rust,no_run
90/// #[tokio::main]
91/// #[cfg_attr(feature = "hotpath", hotpath::main)]
92/// async fn main() {
93///     // Your code here
94/// }
95/// ```
96///
97/// # Limitations
98///
99/// Only one hotpath guard can be active at a time. Creating a second guard (either via this
100/// macro or via [`GuardBuilder`](../hotpath/struct.GuardBuilder.html)) will cause a panic.
101///
102/// # See Also
103///
104/// * [`measure`](macro@measure) - Attribute macro for instrumenting functions
105/// * [`measure_block!`](../hotpath/macro.measure_block.html) - Macro for measuring code blocks
106/// * [`GuardBuilder`](../hotpath/struct.GuardBuilder.html) - Manual control over profiling lifecycle
107#[proc_macro_attribute]
108pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
109    let input = parse_macro_input!(item as ItemFn);
110    let vis = &input.vis;
111    let sig = &input.sig;
112    let block = &input.block;
113
114    // Defaults
115    let mut percentiles: Vec<u8> = vec![95];
116    let mut format = Format::Table;
117    let mut limit: usize = 15;
118    let mut timeout: Option<u64> = None;
119
120    // Parse named args like: percentiles=[..], format=".."
121    if !attr.is_empty() {
122        let parser = syn::meta::parser(|meta| {
123            if meta.path.is_ident("percentiles") {
124                meta.input.parse::<syn::Token![=]>()?;
125                let content;
126                syn::bracketed!(content in meta.input);
127                let mut vals = Vec::new();
128                while !content.is_empty() {
129                    let li: LitInt = content.parse()?;
130                    let v: u8 = li.base10_parse()?;
131                    if !(0..=100).contains(&v) {
132                        return Err(
133                            meta.error(format!("Invalid percentile {} (must be 0..=100)", v))
134                        );
135                    }
136                    vals.push(v);
137                    if !content.is_empty() {
138                        content.parse::<syn::Token![,]>()?;
139                    }
140                }
141                if vals.is_empty() {
142                    return Err(meta.error("At least one percentile must be specified"));
143                }
144                percentiles = vals;
145                return Ok(());
146            }
147
148            if meta.path.is_ident("format") {
149                meta.input.parse::<syn::Token![=]>()?;
150                let lit: LitStr = meta.input.parse()?;
151                format =
152                    match lit.value().as_str() {
153                        "table" => Format::Table,
154                        "json" => Format::Json,
155                        "json-pretty" => Format::JsonPretty,
156                        other => return Err(meta.error(format!(
157                            "Unknown format {:?}. Expected one of: \"table\", \"json\", \"json-pretty\"",
158                            other
159                        ))),
160                    };
161                return Ok(());
162            }
163
164            if meta.path.is_ident("limit") {
165                meta.input.parse::<syn::Token![=]>()?;
166                let li: LitInt = meta.input.parse()?;
167                limit = li.base10_parse()?;
168                return Ok(());
169            }
170
171            if meta.path.is_ident("timeout") {
172                meta.input.parse::<syn::Token![=]>()?;
173                let li: LitInt = meta.input.parse()?;
174                timeout = Some(li.base10_parse()?);
175                return Ok(());
176            }
177
178            Err(meta.error(
179                "Unknown parameter. Supported: percentiles=[..], format=\"..\", limit=N, timeout=N",
180            ))
181        });
182
183        if let Err(e) = parser.parse2(proc_macro2::TokenStream::from(attr)) {
184            return e.to_compile_error().into();
185        }
186    }
187
188    let percentiles_array = quote! { &[#(#percentiles),*] };
189    let format_token = format.to_tokens();
190
191    let asyncness = sig.asyncness.is_some();
192    let fn_name = &sig.ident;
193
194    let base_builder = quote! {
195        let caller_name: &'static str =
196            concat!(module_path!(), "::", stringify!(#fn_name));
197
198        hotpath::GuardBuilder::new(caller_name)
199            .percentiles(#percentiles_array)
200            .limit(#limit)
201            .format(#format_token)
202    };
203
204    let guard_init = if let Some(timeout_ms) = timeout {
205        quote! {
206            let _hotpath = {
207                #base_builder
208                    .build_with_timeout(std::time::Duration::from_millis(#timeout_ms))
209            };
210        }
211    } else {
212        quote! {
213            let _hotpath = {
214                #base_builder.build()
215            };
216        }
217    };
218
219    let body = quote! {
220        #guard_init
221        #block
222    };
223
224    let wrapped_body = if asyncness {
225        quote! { async { #body }.await }
226    } else {
227        body
228    };
229
230    let output = quote! {
231        #vis #sig {
232            #wrapped_body
233        }
234    };
235
236    output.into()
237}
238
239/// Instruments a function to send performance measurements to the hotpath profiler.
240///
241/// This attribute macro wraps functions with profiling code that measures execution time
242/// or memory allocations (depending on enabled feature flags). The measurements are sent
243/// to a background processing thread for aggregation.
244///
245/// # Behavior
246///
247/// The macro automatically detects whether the function is sync or async and instruments
248/// it appropriately. Measurements include:
249///
250/// * **Time profiling** (default): Execution duration using high-precision timers
251/// * **Allocation profiling**: Memory allocations when allocation features are enabled
252///   - `hotpath-alloc` - Total bytes allocated
253///   - `hotpath-alloc` - Total allocation count
254///
255/// # Async Function Limitations
256///
257/// When using allocation profiling features with async functions, you must use the
258/// `tokio` runtime in `current_thread` mode:
259///
260/// ```rust,no_run
261/// #[tokio::main(flavor = "current_thread")]
262/// async fn main() {
263///     // Your async code here
264/// }
265/// ```
266///
267/// This limitation exists because allocation tracking uses thread-local storage. In multi-threaded
268/// runtimes, async tasks can migrate between threads, making it impossible to accurately
269/// attribute allocations to specific function calls. Time-based profiling works with any runtime flavor.
270///
271/// When the `hotpath` feature is disabled, this macro compiles to zero overhead (no instrumentation).
272///
273/// # See Also
274///
275/// * [`main`](macro@main) - Attribute macro that initializes profiling
276/// * [`measure_block!`](../hotpath/macro.measure_block.html) - Macro for measuring code blocks
277#[proc_macro_attribute]
278pub fn measure(_attr: TokenStream, item: TokenStream) -> TokenStream {
279    let input = parse_macro_input!(item as ItemFn);
280    let vis = &input.vis;
281    let sig = &input.sig;
282    let block = &input.block;
283
284    let name = sig.ident.to_string();
285    let asyncness = sig.asyncness.is_some();
286
287    let guard_init = quote! {
288        let _guard = hotpath::MeasurementGuard::build(
289            concat!(module_path!(), "::", #name),
290            false,
291            #asyncness
292        );
293        #block
294    };
295
296    let wrapped = if asyncness {
297        quote! { async { #guard_init }.await }
298    } else {
299        guard_init
300    };
301
302    let output = quote! {
303        #vis #sig {
304            #wrapped
305        }
306    };
307
308    output.into()
309}
310
311/// Marks a function to be excluded from profiling when used with [`measure_all`](macro@measure_all).
312///
313/// # Usage
314///
315/// ```rust,no_run
316/// #[cfg_attr(feature = "hotpath", hotpath::measure_all)]
317/// impl MyStruct {
318///     fn important_method(&self) {
319///         // This will be measured
320///     }
321///
322///     #[cfg_attr(feature = "hotpath", hotpath::skip)]
323///     fn not_so_important_method(&self) -> usize {
324///         // This will NOT be measured
325///         self.value
326///     }
327/// }
328/// ```
329///
330/// # See Also
331///
332/// * [`measure_all`](macro@measure_all) - Bulk instrumentation macro
333/// * [`measure`](macro@measure) - Individual function instrumentation
334#[proc_macro_attribute]
335pub fn skip(_attr: TokenStream, item: TokenStream) -> TokenStream {
336    item
337}
338
339/// Instruments all functions in a module or impl block with the `measure` profiling macro.
340///
341/// This attribute macro applies the [`measure`](macro@measure) macro to every function
342/// in the annotated module or impl block, providing bulk instrumentation without needing
343/// to annotate each function individually.
344///
345/// # Usage
346///
347/// On modules:
348///
349/// ```rust,no_run
350/// #[cfg_attr(feature = "hotpath", hotpath::measure_all)]
351/// mod my_module {
352///     fn function_one() {
353///         // This will be automatically measured
354///     }
355///
356///     fn function_two() {
357///         // This will also be automatically measured
358///     }
359/// }
360/// ```
361///
362/// On impl blocks:
363///
364/// ```rust,no_run
365/// struct MyStruct;
366///
367/// #[cfg_attr(feature = "hotpath", hotpath::measure_all)]
368/// impl MyStruct {
369///     fn method_one(&self) {
370///         // This will be automatically measured
371///     }
372///
373///     fn method_two(&self) {
374///         // This will also be automatically measured
375///     }
376/// }
377/// ```
378///
379/// # See Also
380///
381/// * [`measure`](macro@measure) - Attribute macro for instrumenting individual functions
382/// * [`main`](macro@main) - Attribute macro that initializes profiling
383/// * [`skip`](macro@skip) - Marker to exclude specific functions from measurement
384#[proc_macro_attribute]
385pub fn measure_all(_attr: TokenStream, item: TokenStream) -> TokenStream {
386    let parsed_item = parse_macro_input!(item as Item);
387
388    match parsed_item {
389        Item::Mod(mut module) => {
390            if let Some((_brace, items)) = &mut module.content {
391                for it in items.iter_mut() {
392                    if let Item::Fn(func) = it {
393                        if !has_hotpath_skip(&func.attrs) {
394                            let func_tokens = TokenStream::from(quote!(#func));
395                            let transformed = measure(TokenStream::new(), func_tokens);
396                            *func = syn::parse_macro_input!(transformed as ItemFn);
397                        }
398                    }
399                }
400            }
401            TokenStream::from(quote!(#module))
402        }
403        Item::Impl(mut impl_block) => {
404            for item in impl_block.items.iter_mut() {
405                if let ImplItem::Fn(method) = item {
406                    if !has_hotpath_skip(&method.attrs) {
407                        let func_tokens = TokenStream::from(quote!(#method));
408                        let transformed = measure(TokenStream::new(), func_tokens);
409                        *method = syn::parse_macro_input!(transformed as syn::ImplItemFn);
410                    }
411                }
412            }
413            TokenStream::from(quote!(#impl_block))
414        }
415        _ => panic!("measure_all can only be applied to modules or impl blocks"),
416    }
417}
418
419fn has_hotpath_skip(attrs: &[syn::Attribute]) -> bool {
420    attrs.iter().any(|attr| {
421        // Check for #[skip] or #[hotpath::skip]
422        if attr.path().is_ident("skip")
423            || (attr.path().segments.len() == 2
424                && attr.path().segments[0].ident == "hotpath"
425                && attr.path().segments[1].ident == "skip")
426        {
427            return true;
428        }
429
430        // Check for #[cfg_attr(feature = "hotpath", hotpath::skip)]
431        if attr.path().is_ident("cfg_attr") {
432            let attr_str = quote!(#attr).to_string();
433            if attr_str.contains("hotpath") && attr_str.contains("skip") {
434                return true;
435            }
436        }
437
438        false
439    })
440}