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}