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}