mobench-macros 0.1.38

Procedural macros for mobench benchmarks with setup, teardown, and per-iteration support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
//! # mobench-macros
//!
//! [![Crates.io](https://img.shields.io/crates/v/mobench-macros.svg)](https://crates.io/crates/mobench-macros)
//! [![Documentation](https://docs.rs/mobench-macros/badge.svg)](https://docs.rs/mobench-macros)
//! [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/worldcoin/mobile-bench-rs/blob/main/LICENSE)
//!
//! Procedural macros for the mobench mobile benchmarking SDK.
//!
//! This crate provides the [`#[benchmark]`](macro@benchmark) attribute macro
//! that marks functions for mobile benchmarking. Functions annotated with this
//! macro are automatically registered in a global registry and can be discovered
//! and executed at runtime.
//!
//! ## Usage
//!
//! Most users should import the macro via [`mobench-sdk`](https://crates.io/crates/mobench-sdk)
//! rather than using this crate directly:
//!
//! ```ignore
//! use mobench_sdk::benchmark;
//!
//! #[benchmark]
//! fn my_benchmark() {
//!     // Your benchmark code here
//!     let result = expensive_computation();
//!     std::hint::black_box(result);
//! }
//! ```
//!
//! ## Setup and Teardown
//!
//! For benchmarks that need expensive setup that shouldn't be measured:
//!
//! ```ignore
//! use mobench_sdk::benchmark;
//!
//! fn setup_data() -> Vec<u8> {
//!     vec![0u8; 1_000_000]  // Not measured
//! }
//!
//! #[benchmark(setup = setup_data)]
//! fn hash_benchmark(data: &Vec<u8>) {
//!     std::hint::black_box(compute_hash(data));  // Only this is measured
//! }
//! ```
//!
//! ## How It Works
//!
//! The `#[benchmark]` macro:
//!
//! 1. **Preserves the original function** - The function remains callable as normal
//! 2. **Registers with inventory** - Creates a static registration that the SDK discovers at runtime
//! 3. **Captures the fully-qualified name** - Uses `module_path!()` to generate unique names like `my_crate::my_module::my_benchmark`
//! 4. **Handles setup/teardown** - If specified, wraps the benchmark with setup/teardown that aren't timed
//!
//! ## Requirements
//!
//! - The [`inventory`](https://crates.io/crates/inventory) crate must be in your dependency tree
//! - Simple benchmarks: no parameters, returns `()`
//! - With setup: exactly one parameter (reference to setup result), returns `()`
//! - The function should not panic during normal execution
//!
//! ## Crate Ecosystem
//!
//! This crate is part of the mobench ecosystem:
//!
//! - **[`mobench-sdk`](https://crates.io/crates/mobench-sdk)** - Core SDK with timing harness (re-exports this macro)
//! - **[`mobench`](https://crates.io/crates/mobench)** - CLI tool
//! - **`mobench-macros`** (this crate) - Proc macros

use proc_macro::TokenStream;
use quote::quote;
use syn::{
    Ident, ItemFn, ReturnType, Token,
    parse::{Parse, ParseStream},
    parse_macro_input,
    punctuated::Punctuated,
};

/// Arguments to the benchmark attribute
struct BenchmarkArgs {
    setup: Option<Ident>,
    teardown: Option<Ident>,
    per_iteration: bool,
}

impl Parse for BenchmarkArgs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut setup = None;
        let mut teardown = None;
        let mut per_iteration = false;

        if input.is_empty() {
            return Ok(Self {
                setup,
                teardown,
                per_iteration,
            });
        }

        // Parse key = value pairs separated by commas
        let args = Punctuated::<BenchmarkArg, Token![,]>::parse_terminated(input)?;

        for arg in args {
            match arg {
                BenchmarkArg::Setup(ident) => {
                    if setup.is_some() {
                        return Err(syn::Error::new_spanned(ident, "duplicate setup argument"));
                    }
                    setup = Some(ident);
                }
                BenchmarkArg::Teardown(ident) => {
                    if teardown.is_some() {
                        return Err(syn::Error::new_spanned(
                            ident,
                            "duplicate teardown argument",
                        ));
                    }
                    teardown = Some(ident);
                }
                BenchmarkArg::PerIteration => {
                    per_iteration = true;
                }
            }
        }

        // Validate: teardown without setup is invalid
        if teardown.is_some() && setup.is_none() {
            return Err(syn::Error::new(
                proc_macro2::Span::call_site(),
                "teardown requires setup to be specified",
            ));
        }

        // Validate: per_iteration with teardown is not supported
        if per_iteration && teardown.is_some() {
            return Err(syn::Error::new(
                proc_macro2::Span::call_site(),
                "per_iteration mode is not compatible with teardown",
            ));
        }

        Ok(Self {
            setup,
            teardown,
            per_iteration,
        })
    }
}

enum BenchmarkArg {
    Setup(Ident),
    Teardown(Ident),
    PerIteration,
}

impl Parse for BenchmarkArg {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let name: Ident = input.parse()?;

        match name.to_string().as_str() {
            "setup" => {
                input.parse::<Token![=]>()?;
                let value: Ident = input.parse()?;
                Ok(BenchmarkArg::Setup(value))
            }
            "teardown" => {
                input.parse::<Token![=]>()?;
                let value: Ident = input.parse()?;
                Ok(BenchmarkArg::Teardown(value))
            }
            "per_iteration" => Ok(BenchmarkArg::PerIteration),
            _ => Err(syn::Error::new_spanned(
                name,
                "expected 'setup', 'teardown', or 'per_iteration'",
            )),
        }
    }
}

/// Marks a function as a benchmark for mobile execution.
///
/// This attribute macro registers the function in the global benchmark registry,
/// making it discoverable and executable by the mobench runtime.
///
/// # Basic Usage
///
/// ```ignore
/// use mobench_sdk::benchmark;
///
/// #[benchmark]
/// fn fibonacci_bench() {
///     let result = fibonacci(30);
///     std::hint::black_box(result);
/// }
/// ```
///
/// # With Setup (setup runs once, not measured)
///
/// ```ignore
/// use mobench_sdk::benchmark;
///
/// fn setup_proof() -> ProofInput {
///     ProofInput::generate()  // Expensive, not measured
/// }
///
/// #[benchmark(setup = setup_proof)]
/// fn verify_proof(input: &ProofInput) {
///     verify(&input.proof);  // Only this is measured
/// }
/// ```
///
/// # With Per-Iteration Setup (for mutating benchmarks)
///
/// ```ignore
/// use mobench_sdk::benchmark;
///
/// fn generate_random_vec() -> Vec<i32> {
///     (0..1000).map(|_| rand::random()).collect()
/// }
///
/// #[benchmark(setup = generate_random_vec, per_iteration)]
/// fn sort_benchmark(data: Vec<i32>) {
///     let mut data = data;
///     data.sort();
///     std::hint::black_box(data);
/// }
/// ```
///
/// # With Setup and Teardown
///
/// ```ignore
/// use mobench_sdk::benchmark;
///
/// fn setup_db() -> Database { Database::connect("test.db") }
/// fn cleanup_db(db: Database) { db.close(); }
///
/// #[benchmark(setup = setup_db, teardown = cleanup_db)]
/// fn db_query(db: &Database) {
///     db.query("SELECT * FROM users");
/// }
/// ```
///
/// # Function Requirements
///
/// **Without setup:**
/// - Take no parameters
/// - Return `()` (unit type)
///
/// **With setup:**
/// - Take exactly one parameter (reference to setup result, or owned for per_iteration)
/// - Return `()` (unit type)
///
/// # Best Practices
///
/// ## Use `black_box` to Prevent Optimization
///
/// Always wrap results with [`std::hint::black_box`] to prevent the compiler
/// from optimizing away the computation:
///
/// ```ignore
/// #[benchmark]
/// fn good_benchmark() {
///     let result = compute_something();
///     std::hint::black_box(result);  // Prevents optimization
/// }
/// ```
#[proc_macro_attribute]
pub fn benchmark(attr: TokenStream, item: TokenStream) -> TokenStream {
    let args = parse_macro_input!(attr as BenchmarkArgs);
    let input_fn = parse_macro_input!(item as ItemFn);

    let fn_name = &input_fn.sig.ident;
    let fn_name_str = fn_name.to_string();
    let vis = &input_fn.vis;
    let sig = &input_fn.sig;
    let block = &input_fn.block;
    let attrs = &input_fn.attrs;

    if input_fn.sig.asyncness.is_some() {
        return syn::Error::new_spanned(
            input_fn.sig.asyncness,
            "#[benchmark] does not support async fn. Move async work into a synchronous runtime boundary so the benchmark measures execution instead of future creation.",
        )
        .to_compile_error()
        .into();
    }

    // Validate based on whether setup is provided
    if args.setup.is_some() {
        // With setup: must have exactly one parameter
        if input_fn.sig.inputs.len() != 1 {
            let param_count = input_fn.sig.inputs.len();
            return syn::Error::new_spanned(
                &input_fn.sig,
                format!(
                    "#[benchmark(setup = ...)] functions must take exactly one parameter.\n\
                     Found {} parameter(s).\n\n\
                     Example:\n\
                     fn setup_data() -> MyData {{ ... }}\n\n\
                     #[benchmark(setup = setup_data)]\n\
                     fn {}(input: &MyData) {{\n\
                         // input is the result of setup_data()\n\
                     }}",
                    param_count, fn_name_str
                ),
            )
            .to_compile_error()
            .into();
        }
    } else {
        // No setup: must have no parameters
        if !input_fn.sig.inputs.is_empty() {
            let param_count = input_fn.sig.inputs.len();
            let param_names: Vec<String> = input_fn
                .sig
                .inputs
                .iter()
                .map(|arg| match arg {
                    syn::FnArg::Receiver(_) => "self".to_string(),
                    syn::FnArg::Typed(pat) => quote!(#pat).to_string(),
                })
                .collect();
            return syn::Error::new_spanned(
                &input_fn.sig.inputs,
                format!(
                    "#[benchmark] functions must take no parameters.\n\
                     Found {} parameter(s): {}\n\n\
                     If you need setup data, use the setup attribute:\n\n\
                     fn setup_data() -> MyData {{ ... }}\n\n\
                     #[benchmark(setup = setup_data)]\n\
                     fn {}(input: &MyData) {{\n\
                         // Your benchmark code using input\n\
                     }}",
                    param_count,
                    param_names.join(", "),
                    fn_name_str
                ),
            )
            .to_compile_error()
            .into();
        }
    }

    // Validate: function must return () (unit type)
    match &input_fn.sig.output {
        ReturnType::Default => {} // () return type is OK
        ReturnType::Type(_, return_type) => {
            let type_str = quote!(#return_type).to_string();
            if type_str.trim() != "()" {
                return syn::Error::new_spanned(
                    return_type,
                    format!(
                        "#[benchmark] functions must return () (unit type).\n\
                         Found return type: {}\n\n\
                         Benchmark results should be consumed with std::hint::black_box() \
                         rather than returned:\n\n\
                         #[benchmark]\n\
                         fn {}() {{\n\
                             let result = compute_something();\n\
                             std::hint::black_box(result);  // Prevents optimization\n\
                         }}",
                        type_str, fn_name_str
                    ),
                )
                .to_compile_error()
                .into();
            }
        }
    }

    // Generate the runner based on configuration
    let runner = generate_runner(fn_name, &args);

    let expanded = quote! {
        // Preserve the original function
        #(#attrs)*
        #vis #sig {
            #block
        }

        // Register the function with inventory
        ::inventory::submit! {
            ::mobench_sdk::registry::BenchFunction {
                name: ::std::concat!(::std::module_path!(), "::", #fn_name_str),
                runner: #runner,
            }
        }
    };

    TokenStream::from(expanded)
}

fn generate_runner(fn_name: &Ident, args: &BenchmarkArgs) -> proc_macro2::TokenStream {
    match (&args.setup, &args.teardown, args.per_iteration) {
        // No setup - simple benchmark
        (None, None, _) => quote! {
            |spec: ::mobench_sdk::timing::BenchSpec| -> ::std::result::Result<::mobench_sdk::timing::BenchReport, ::mobench_sdk::timing::TimingError> {
                ::mobench_sdk::timing::run_closure(spec, || {
                    #fn_name();
                    Ok(())
                })
            }
        },

        // Setup only, runs once before all iterations
        (Some(setup), None, false) => quote! {
            |spec: ::mobench_sdk::timing::BenchSpec| -> ::std::result::Result<::mobench_sdk::timing::BenchReport, ::mobench_sdk::timing::TimingError> {
                ::mobench_sdk::timing::run_closure_with_setup(
                    spec,
                    || #setup(),
                    |input| {
                        #fn_name(input);
                        Ok(())
                    },
                )
            }
        },

        // Setup only, per iteration (for mutating benchmarks)
        (Some(setup), None, true) => quote! {
            |spec: ::mobench_sdk::timing::BenchSpec| -> ::std::result::Result<::mobench_sdk::timing::BenchReport, ::mobench_sdk::timing::TimingError> {
                ::mobench_sdk::timing::run_closure_with_setup_per_iter(
                    spec,
                    || #setup(),
                    |input| {
                        #fn_name(input);
                        Ok(())
                    },
                )
            }
        },

        // Setup + teardown (per_iteration with teardown is rejected during parsing)
        (Some(setup), Some(teardown), false) => quote! {
            |spec: ::mobench_sdk::timing::BenchSpec| -> ::std::result::Result<::mobench_sdk::timing::BenchReport, ::mobench_sdk::timing::TimingError> {
                ::mobench_sdk::timing::run_closure_with_setup_teardown(
                    spec,
                    || #setup(),
                    |input| {
                        #fn_name(input);
                        Ok(())
                    },
                    |input| #teardown(input),
                )
            }
        },

        // These cases are rejected during parsing, but we need to handle them
        (None, Some(_), _) | (Some(_), Some(_), true) => {
            quote! { compile_error!("invalid benchmark configuration") }
        }
    }
}