hotpath_macros/lib.rs
1use proc_macro::TokenStream;
2use quote::quote;
3use syn::parse::Parser;
4use syn::{parse_macro_input, 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///
35/// # Examples
36///
37/// Basic usage with default settings (P95 percentile, table format):
38///
39/// ```rust,no_run
40/// #[cfg_attr(feature = "hotpath", hotpath::main)]
41/// fn main() {
42/// // Your code here
43/// }
44/// ```
45///
46/// Custom percentiles:
47///
48/// ```rust,no_run
49/// #[tokio::main]
50/// #[cfg_attr(feature = "hotpath", hotpath::main(percentiles = [50, 90, 95, 99]))]
51/// async fn main() {
52/// // Your code here
53/// }
54/// ```
55///
56/// JSON output format:
57///
58/// ```rust,no_run
59/// #[cfg_attr(feature = "hotpath", hotpath::main(format = "json-pretty"))]
60/// fn main() {
61/// // Your code here
62/// }
63/// ```
64///
65/// Combined parameters:
66///
67/// ```rust,no_run
68/// #[cfg_attr(feature = "hotpath", hotpath::main(percentiles = [50, 99], format = "json"))]
69/// fn main() {
70/// // Your code here
71/// }
72/// ```
73///
74/// # Usage with Tokio
75///
76/// When using with tokio, place `#[tokio::main]` before `#[hotpath::main]`:
77///
78/// ```rust,no_run
79/// #[tokio::main]
80/// #[cfg_attr(feature = "hotpath", hotpath::main)]
81/// async fn main() {
82/// // Your code here
83/// }
84/// ```
85///
86/// # Limitations
87///
88/// Only one hotpath guard can be active at a time. Creating a second guard (either via this
89/// macro or via [`GuardBuilder`](../hotpath/struct.GuardBuilder.html)) will cause a panic.
90///
91/// # See Also
92///
93/// * [`measure`](macro@measure) - Attribute macro for instrumenting functions
94/// * [`measure_block!`](../hotpath/macro.measure_block.html) - Macro for measuring code blocks
95/// * [`GuardBuilder`](../hotpath/struct.GuardBuilder.html) - Manual control over profiling lifecycle
96#[proc_macro_attribute]
97pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
98 let input = parse_macro_input!(item as ItemFn);
99 let vis = &input.vis;
100 let sig = &input.sig;
101 let block = &input.block;
102
103 // Defaults
104 let mut percentiles: Vec<u8> = vec![95];
105 let mut format = Format::Table;
106
107 // Parse named args like: percentiles=[..], format=".."
108 if !attr.is_empty() {
109 let parser = syn::meta::parser(|meta| {
110 if meta.path.is_ident("percentiles") {
111 meta.input.parse::<syn::Token![=]>()?;
112 let content;
113 syn::bracketed!(content in meta.input);
114 let mut vals = Vec::new();
115 while !content.is_empty() {
116 let li: LitInt = content.parse()?;
117 let v: u8 = li.base10_parse()?;
118 if !(0..=100).contains(&v) {
119 return Err(
120 meta.error(format!("Invalid percentile {} (must be 0..=100)", v))
121 );
122 }
123 vals.push(v);
124 if !content.is_empty() {
125 content.parse::<syn::Token![,]>()?;
126 }
127 }
128 if vals.is_empty() {
129 return Err(meta.error("At least one percentile must be specified"));
130 }
131 percentiles = vals;
132 return Ok(());
133 }
134
135 if meta.path.is_ident("format") {
136 meta.input.parse::<syn::Token![=]>()?;
137 let lit: LitStr = meta.input.parse()?;
138 format =
139 match lit.value().as_str() {
140 "table" => Format::Table,
141 "json" => Format::Json,
142 "json-pretty" => Format::JsonPretty,
143 other => return Err(meta.error(format!(
144 "Unknown format {:?}. Expected one of: \"table\", \"json\", \"json-pretty\"",
145 other
146 ))),
147 };
148 return Ok(());
149 }
150
151 Err(meta.error("Unknown parameter. Supported: percentiles=[..], format=\"..\""))
152 });
153
154 if let Err(e) = parser.parse2(proc_macro2::TokenStream::from(attr)) {
155 return e.to_compile_error().into();
156 }
157 }
158
159 let percentiles_array = quote! { &[#(#percentiles),*] };
160 let format_token = format.to_tokens();
161
162 let asyncness = sig.asyncness.is_some();
163 let fn_name = &sig.ident;
164 let measurement_name = quote! { concat!(module_path!(), "::", stringify!(#fn_name)) };
165
166 let output = if asyncness {
167 quote! {
168 #vis #sig {
169 async {
170 let _hotpath = {
171 fn __caller_fn() {}
172 let caller_name = std::any::type_name_of_val(&__caller_fn)
173 .strip_suffix("::__caller_fn")
174 .unwrap_or(std::any::type_name_of_val(&__caller_fn))
175 .replace("::{{closure}}", "");
176
177 hotpath::GuardBuilder::new(caller_name.to_string())
178 .percentiles(#percentiles_array)
179 .format(#format_token)
180 .build()
181 };
182
183 hotpath::cfg_if! {
184 if #[cfg(feature = "hotpath-off")] {
185 // No-op when hotpath-off is enabled
186 } else if #[cfg(any(
187 feature = "hotpath-alloc-bytes-total",
188 feature = "hotpath-alloc-count-total"
189 ))] {
190 use hotpath::{Handle, RuntimeFlavor};
191 let runtime_flavor = Handle::try_current().ok().map(|h| h.runtime_flavor());
192
193 let _measure_guard = match runtime_flavor {
194 Some(RuntimeFlavor::CurrentThread) => {
195 hotpath::AllocGuardType::AllocGuard(hotpath::AllocGuard::new(#measurement_name, true))
196 }
197 _ => {
198 hotpath::AllocGuardType::NoopAsyncAllocGuard(hotpath::NoopAsyncAllocGuard::new(#measurement_name, true))
199 }
200 };
201 } else {
202 let _measure_guard = hotpath::TimeGuard::new(#measurement_name, true);
203 }
204 }
205
206 #block
207 }.await
208 }
209 }
210 } else {
211 quote! {
212 #vis #sig {
213 let _hotpath = {
214 fn __caller_fn() {}
215 let caller_name = std::any::type_name_of_val(&__caller_fn)
216 .strip_suffix("::__caller_fn")
217 .unwrap_or(std::any::type_name_of_val(&__caller_fn))
218 .replace("::{{closure}}", "");
219
220 hotpath::GuardBuilder::new(caller_name.to_string())
221 .percentiles(#percentiles_array)
222 .format(#format_token)
223 .build()
224 };
225
226 hotpath::cfg_if! {
227 if #[cfg(feature = "hotpath-off")] {
228 // No-op when hotpath-off is enabled
229 } else if #[cfg(any(
230 feature = "hotpath-alloc-bytes-total",
231 feature = "hotpath-alloc-count-total"
232 ))] {
233 let _measure_guard = hotpath::AllocGuard::new(#measurement_name, true);
234 } else {
235 let _measure_guard = hotpath::TimeGuard::new(#measurement_name, true);
236 }
237 }
238
239 #block
240 }
241 }
242 };
243
244 output.into()
245}
246
247/// Instruments a function to send performance measurements to the hotpath profiler.
248///
249/// This attribute macro wraps functions with profiling code that measures execution time
250/// or memory allocations (depending on enabled feature flags). The measurements are sent
251/// to a background processing thread for aggregation.
252///
253/// # Behavior
254///
255/// The macro automatically detects whether the function is sync or async and instruments
256/// it appropriately. Measurements include:
257///
258/// * **Time profiling** (default): Execution duration using high-precision timers
259/// * **Allocation profiling**: Memory allocations when allocation features are enabled
260/// - `hotpath-alloc-bytes-total` - Total bytes allocated
261/// - `hotpath-alloc-count-total` - Total allocation count
262///
263/// # Async Function Limitations
264///
265/// When using allocation profiling features with async functions, you must use the
266/// `tokio` runtime in `current_thread` mode:
267///
268/// ```rust,no_run
269/// #[tokio::main(flavor = "current_thread")]
270/// async fn main() {
271/// // Your async code here
272/// }
273/// ```
274///
275/// This limitation exists because allocation tracking uses thread-local storage. In multi-threaded
276/// runtimes, async tasks can migrate between threads, making it impossible to accurately
277/// attribute allocations to specific function calls. Time-based profiling works with any runtime flavor.
278///
279/// When the `hotpath` feature is disabled, this macro compiles to zero overhead (no instrumentation).
280///
281/// # See Also
282///
283/// * [`main`](macro@main) - Attribute macro that initializes profiling
284/// * [`measure_block!`](../hotpath/macro.measure_block.html) - Macro for measuring code blocks
285#[proc_macro_attribute]
286pub fn measure(_attr: TokenStream, item: TokenStream) -> TokenStream {
287 let input = parse_macro_input!(item as ItemFn);
288 let vis = &input.vis;
289 let sig = &input.sig;
290 let block = &input.block;
291
292 let name = sig.ident.to_string();
293 let asyncness = sig.asyncness.is_some();
294
295 let output = if asyncness {
296 quote! {
297 #vis #sig {
298 async {
299 hotpath::cfg_if! {
300 if #[cfg(feature = "hotpath-off")] {
301 // No-op when hotpath-off is enabled
302 } else if #[cfg(any(
303 feature = "hotpath-alloc-bytes-total",
304 feature = "hotpath-alloc-count-total"
305 ))] {
306 use hotpath::{Handle, RuntimeFlavor};
307 let runtime_flavor = Handle::try_current().ok().map(|h| h.runtime_flavor());
308
309 let _guard = match runtime_flavor {
310 Some(RuntimeFlavor::CurrentThread) => {
311 hotpath::AllocGuardType::AllocGuard(hotpath::AllocGuard::new(concat!(module_path!(), "::", #name), false))
312 }
313 _ => {
314 hotpath::AllocGuardType::NoopAsyncAllocGuard(hotpath::NoopAsyncAllocGuard::new(concat!(module_path!(), "::", #name), false))
315 }
316 };
317 } else {
318 let _guard = hotpath::TimeGuard::new(concat!(module_path!(), "::", #name), false);
319 }
320 }
321
322 #block
323 }.await
324 }
325 }
326 } else {
327 quote! {
328 #vis #sig {
329 hotpath::cfg_if! {
330 if #[cfg(feature = "hotpath-off")] {
331 // No-op when hotpath-off is enabled
332 } else if #[cfg(any(
333 feature = "hotpath-alloc-bytes-total",
334 feature = "hotpath-alloc-count-total"
335 ))] {
336 let _guard = hotpath::AllocGuard::new(concat!(module_path!(), "::", #name), false);
337 } else {
338 let _guard = hotpath::TimeGuard::new(concat!(module_path!(), "::", #name), false);
339 }
340 }
341
342 #block
343 }
344 }
345 };
346
347 output.into()
348}