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