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