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/// # Parameters
274///
275/// * `log` - If `true`, logs the result value when the function returns (requires `Debug` on return type)
276///
277/// # Examples
278///
279/// With result logging (requires Debug on return type):
280///
281/// ```rust,no_run
282/// #[cfg_attr(feature = "hotpath", hotpath::measure(log = true))]
283/// fn compute() -> i32 {
284///     // The result value will be logged in TUI console
285///     42
286/// }
287/// ```
288///
289/// # See Also
290///
291/// * [`main`](macro@main) - Attribute macro that initializes profiling
292/// * [`measure_block!`](../hotpath/macro.measure_block.html) - Macro for measuring code blocks
293#[proc_macro_attribute]
294pub fn measure(attr: TokenStream, item: TokenStream) -> TokenStream {
295    let input = parse_macro_input!(item as ItemFn);
296
297    let attrs = &input.attrs;
298    let vis = &input.vis;
299    let sig = &input.sig;
300    let block = &input.block;
301
302    let name = sig.ident.to_string();
303    let asyncness = sig.asyncness.is_some();
304
305    // Parse optional `log = true` attribute
306    let mut log_result = false;
307
308    if !attr.is_empty() {
309        let parser = syn::meta::parser(|meta| {
310            if meta.path.is_ident("log") {
311                meta.input.parse::<syn::Token![=]>()?;
312                let lit: syn::LitBool = meta.input.parse()?;
313                log_result = lit.value();
314                return Ok(());
315            }
316
317            Err(meta.error("Unknown parameter. Supported: log = true"))
318        });
319
320        if let Err(e) = parser.parse2(proc_macro2::TokenStream::from(attr)) {
321            return e.to_compile_error().into();
322        }
323    }
324
325    let wrapped = if log_result {
326        let loc = quote! { concat!(module_path!(), "::", #name) };
327        if asyncness {
328            quote! {
329                hotpath::measure_with_log_async(#loc, || async #block).await
330            }
331        } else {
332            quote! {
333                hotpath::measure_with_log(#loc, false, false, || #block)
334            }
335        }
336    } else {
337        let guard_init = quote! {
338            let _guard = hotpath::MeasurementGuard::build(
339                concat!(module_path!(), "::", #name),
340                false,
341                #asyncness
342            );
343            #block
344        };
345
346        if asyncness {
347            quote! { async { #guard_init }.await }
348        } else {
349            guard_init
350        }
351    };
352
353    let output = quote! {
354        #(#attrs)*
355        #vis #sig {
356            #wrapped
357        }
358    };
359
360    output.into()
361}
362
363/// Instruments an async function to track its lifecycle as a Future.
364///
365/// This attribute macro wraps async functions with the `future!` macro, enabling
366/// tracking of poll counts, state transitions (pending/ready/cancelled), and
367/// optionally logging the result value.
368///
369/// # Parameters
370///
371/// * `log` - If `true`, logs the result value when the future completes (requires `Debug` on return type)
372///
373/// # Examples
374///
375/// Basic usage (no Debug requirement on return type):
376///
377/// ```rust,no_run
378/// #[cfg_attr(feature = "hotpath", hotpath::future_fn)]
379/// async fn fetch_data() -> Vec<u8> {
380///     // This future's lifecycle will be tracked
381///     vec![1, 2, 3]
382/// }
383/// ```
384///
385/// With result logging (requires Debug on return type):
386///
387/// ```rust,no_run
388/// #[cfg_attr(feature = "hotpath", hotpath::future_fn(log = true))]
389/// async fn compute() -> i32 {
390///     // The result value will be logged in TUI console
391///     42
392/// }
393/// ```
394///
395/// # See Also
396///
397/// * [`measure`](macro@measure) - Attribute macro for instrumenting sync/async function timing
398/// * [`future!`](../hotpath/macro.future.html) - Declarative macro for instrumenting future expressions
399#[proc_macro_attribute]
400pub fn future_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
401    let input = parse_macro_input!(item as ItemFn);
402
403    let attrs = &input.attrs;
404    let vis = &input.vis;
405    let sig = &input.sig;
406    let block = &input.block;
407
408    // Ensure the function is async
409    if sig.asyncness.is_none() {
410        return syn::Error::new_spanned(
411            sig.fn_token,
412            "The #[future_fn] attribute can only be applied to async functions",
413        )
414        .to_compile_error()
415        .into();
416    }
417
418    // Parse optional `log = true` attribute
419    let mut log_result = false;
420
421    if !attr.is_empty() {
422        let parser = syn::meta::parser(|meta| {
423            if meta.path.is_ident("log") {
424                meta.input.parse::<syn::Token![=]>()?;
425                let lit: syn::LitBool = meta.input.parse()?;
426                log_result = lit.value();
427                return Ok(());
428            }
429
430            Err(meta.error("Unknown parameter. Supported: log = true"))
431        });
432
433        if let Err(e) = parser.parse2(proc_macro2::TokenStream::from(attr)) {
434            return e.to_compile_error().into();
435        }
436    }
437
438    let fn_name = &sig.ident;
439
440    // Generate the wrapped body using the future! macro pattern
441    let wrapped_body = if log_result {
442        quote! {
443            {
444                const FUTURE_LOC: &'static str = concat!(module_path!(), "::", stringify!(#fn_name));
445                hotpath::futures::init_futures_state();
446                hotpath::InstrumentFutureLog::instrument_future_log(
447                    async #block,
448                    FUTURE_LOC
449                ).await
450            }
451        }
452    } else {
453        quote! {
454            {
455                const FUTURE_LOC: &'static str = concat!(module_path!(), "::", stringify!(#fn_name));
456                hotpath::futures::init_futures_state();
457                hotpath::InstrumentFuture::instrument_future(
458                    async #block,
459                    FUTURE_LOC
460                ).await
461            }
462        }
463    };
464
465    let output = quote! {
466        #(#attrs)*
467        #vis #sig {
468            #wrapped_body
469        }
470    };
471
472    output.into()
473}
474
475/// Marks a function to be excluded from profiling when used with [`measure_all`](macro@measure_all).
476///
477/// # Usage
478///
479/// ```rust,no_run
480/// #[cfg_attr(feature = "hotpath", hotpath::measure_all)]
481/// impl MyStruct {
482///     fn important_method(&self) {
483///         // This will be measured
484///     }
485///
486///     #[cfg_attr(feature = "hotpath", hotpath::skip)]
487///     fn not_so_important_method(&self) -> usize {
488///         // This will NOT be measured
489///         self.value
490///     }
491/// }
492/// ```
493///
494/// # See Also
495///
496/// * [`measure_all`](macro@measure_all) - Bulk instrumentation macro
497/// * [`measure`](macro@measure) - Individual function instrumentation
498#[proc_macro_attribute]
499pub fn skip(_attr: TokenStream, item: TokenStream) -> TokenStream {
500    item
501}
502
503/// Instruments all functions in a module or impl block with the `measure` profiling macro.
504///
505/// This attribute macro applies the [`measure`](macro@measure) macro to every function
506/// in the annotated module or impl block, providing bulk instrumentation without needing
507/// to annotate each function individually.
508///
509/// # Usage
510///
511/// On modules:
512///
513/// ```rust,no_run
514/// #[cfg_attr(feature = "hotpath", hotpath::measure_all)]
515/// mod my_module {
516///     fn function_one() {
517///         // This will be automatically measured
518///     }
519///
520///     fn function_two() {
521///         // This will also be automatically measured
522///     }
523/// }
524/// ```
525///
526/// On impl blocks:
527///
528/// ```rust,no_run
529/// struct MyStruct;
530///
531/// #[cfg_attr(feature = "hotpath", hotpath::measure_all)]
532/// impl MyStruct {
533///     fn method_one(&self) {
534///         // This will be automatically measured
535///     }
536///
537///     fn method_two(&self) {
538///         // This will also be automatically measured
539///     }
540/// }
541/// ```
542///
543/// # See Also
544///
545/// * [`measure`](macro@measure) - Attribute macro for instrumenting individual functions
546/// * [`main`](macro@main) - Attribute macro that initializes profiling
547/// * [`skip`](macro@skip) - Marker to exclude specific functions from measurement
548#[proc_macro_attribute]
549pub fn measure_all(_attr: TokenStream, item: TokenStream) -> TokenStream {
550    let parsed_item = parse_macro_input!(item as Item);
551
552    match parsed_item {
553        Item::Mod(mut module) => {
554            if let Some((_brace, items)) = &mut module.content {
555                for it in items.iter_mut() {
556                    if let Item::Fn(func) = it {
557                        if !has_hotpath_skip(&func.attrs) {
558                            let func_tokens = TokenStream::from(quote!(#func));
559                            let transformed = measure(TokenStream::new(), func_tokens);
560                            *func = syn::parse_macro_input!(transformed as ItemFn);
561                        }
562                    }
563                }
564            }
565            TokenStream::from(quote!(#module))
566        }
567        Item::Impl(mut impl_block) => {
568            for item in impl_block.items.iter_mut() {
569                if let ImplItem::Fn(method) = item {
570                    if !has_hotpath_skip(&method.attrs) {
571                        let func_tokens = TokenStream::from(quote!(#method));
572                        let transformed = measure(TokenStream::new(), func_tokens);
573                        *method = syn::parse_macro_input!(transformed as syn::ImplItemFn);
574                    }
575                }
576            }
577            TokenStream::from(quote!(#impl_block))
578        }
579        _ => panic!("measure_all can only be applied to modules or impl blocks"),
580    }
581}
582
583fn has_hotpath_skip(attrs: &[syn::Attribute]) -> bool {
584    attrs.iter().any(|attr| {
585        // Check for #[skip] or #[hotpath::skip]
586        if attr.path().is_ident("skip")
587            || (attr.path().segments.len() == 2
588                && attr.path().segments[0].ident == "hotpath"
589                && attr.path().segments[1].ident == "skip")
590        {
591            return true;
592        }
593
594        // Check for #[cfg_attr(feature = "hotpath", hotpath::skip)]
595        if attr.path().is_ident("cfg_attr") {
596            let attr_str = quote!(#attr).to_string();
597            if attr_str.contains("hotpath") && attr_str.contains("skip") {
598                return true;
599            }
600        }
601
602        false
603    })
604}