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}