diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f339b30f..ec04fc13c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added constants support as an immediate value of the repeat statement ([#2548](https://github.com/0xMiden/miden-vm/pull/2548)). - Add deserialization of the `MastForest` from untrusted sources. Add fuzzing for MastForest deserialization. ([#2590](https://github.com/0xMiden/miden-vm/pull/2590)). - Added `StackInterface::get_double_word()` method for reading 8 consecutive stack elements ([#2607](https://github.com/0xMiden/miden-vm/pull/2607)). +- Added synthetic transaction kernel benchmarks driven by VM profile snapshots from miden-base ([#2638](https://github.com/0xMiden/miden-vm/pull/2638)). #### Fixes diff --git a/Cargo.lock b/Cargo.lock index c26f557e41..e955c19da7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "arrayref" version = "0.3.9" @@ -381,6 +387,34 @@ dependencies = [ "libc", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot 0.5.0", + "futures", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", +] + [[package]] name = "criterion" version = "0.7.0" @@ -391,7 +425,7 @@ dependencies = [ "cast", "ciborium", "clap", - "criterion-plot", + "criterion-plot 0.6.0", "itertools 0.13.0", "num-traits", "oorandom", @@ -405,6 +439,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "criterion-plot" version = "0.6.0" @@ -935,6 +979,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -996,12 +1046,32 @@ dependencies = [ "tempfile", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1251,7 +1321,7 @@ dependencies = [ name = "miden-core" version = "0.21.0" dependencies = [ - "criterion", + "criterion 0.7.0", "derive_more", "insta", "itertools 0.14.0", @@ -1277,7 +1347,7 @@ dependencies = [ name = "miden-core-lib" version = "0.21.0" dependencies = [ - "criterion", + "criterion 0.7.0", "env_logger", "fs-err", "miden-air", @@ -1570,7 +1640,7 @@ dependencies = [ "assert_cmd", "bincode", "clap", - "criterion", + "criterion 0.7.0", "escargot", "hex", "miden-assembly", @@ -2811,6 +2881,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synthetic-tx-kernel" +version = "0.1.0" +dependencies = [ + "anyhow", + "criterion 0.5.1", + "miden-core", + "miden-core-lib", + "miden-processor", + "miden-vm", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "target-triple" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index cce810d253..891e38d1b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "air", + "benches/synthetic-tx-kernel", "core", "crates/assembly", "crates/assembly-syntax", diff --git a/Makefile b/Makefile index 73da7f478a..99bd72f74b 100644 --- a/Makefile +++ b/Makefile @@ -188,7 +188,7 @@ build: ## Builds with default parameters .PHONY: build-no-std build-no-std: ## Builds without the standard library - $(BUILDDOCS) cargo build --no-default-features --target wasm32-unknown-unknown --workspace + $(BUILDDOCS) cargo build --no-default-features --target wasm32-unknown-unknown --workspace --exclude synthetic-tx-kernel # --- executable ---------------------------------------------------------------------------------- diff --git a/benches/synthetic-tx-kernel/Cargo.toml b/benches/synthetic-tx-kernel/Cargo.toml new file mode 100644 index 0000000000..020358a09f --- /dev/null +++ b/benches/synthetic-tx-kernel/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "synthetic-tx-kernel" +version = "0.1.0" +edition = "2021" +license.workspace = true + +[dependencies] +miden-vm = { path = "../../miden-vm" } +miden-core = { path = "../../core" } +miden-processor = { path = "../../processor", default-features = false, features = ["concurrent"] } +miden-core-lib = { path = "../../crates/lib/core" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0" + +[dev-dependencies] +criterion = { version = "0.5", features = ["async_tokio"] } +tokio = { version = "1.0", features = ["rt-multi-thread"] } + +[[bench]] +name = "component_benchmarks" +harness = false + +[[bench]] +name = "synthetic_kernel" +harness = false diff --git a/benches/synthetic-tx-kernel/README.md b/benches/synthetic-tx-kernel/README.md new file mode 100644 index 0000000000..31661e8e9a --- /dev/null +++ b/benches/synthetic-tx-kernel/README.md @@ -0,0 +1,78 @@ +# Synthetic Transaction Kernel Benchmarks + +This crate generates synthetic benchmarks that mirror the transaction kernel from miden-base, +enabling fast feedback for VM developers without requiring the full miden-base dependency. + +## Overview + +The benchmark system works by: + +1. **Profile Export** (in miden-base): The transaction kernel benchmark exports a VM profile +describing its instruction mix, operation counts, and cycle breakdown. + +2. **Profile Consumption** (in miden-vm): This crate reads the profile and generates Miden +assembly code that replicates the same workload characteristics. + +3. **Benchmark Execution**: Criterion.rs runs the generated benchmarks for statistical rigor. + +## Usage + +### Running Benchmarks + +```bash +# Run component benchmarks (isolated operations) +cargo bench -p synthetic-tx-kernel --bench component_benchmarks + +# Run synthetic kernel benchmark (representative workload) +cargo bench -p synthetic-tx-kernel --bench synthetic_kernel +``` + +### Updating the Profile + +When the transaction kernel in miden-base changes: + +1. Run benchmarks in miden-base: +```bash +cd /path/to/miden-base +cargo run --bin bench-transaction --features concurrent +``` + +2. Copy the generated profile to `latest.json`: +```bash +cp bench-tx-vm-profile.json /path/to/miden-vm/benches/synthetic-tx-kernel/profiles/latest.json +``` + +3. Commit the updated profile in miden-vm. + +## Profile Format + +Profiles are JSON files with the following structure: + +```json +{ + "profile_version": "1.0", + "source": "miden-base/bin/bench-transaction", + "timestamp": "2025-01-31T...", + "miden_vm_version": "0.20.0", + "transaction_kernel": { + "total_cycles": 73123, + "phases": { ... }, + "instruction_mix": { + "arithmetic": 0.05, + "hashing": 0.45, + "memory": 0.08, + "control_flow": 0.05, + "signature_verify": 0.37 + } + } +} +``` + +## Architecture + +- `src/profile.rs`: Profile data structures +- `src/generator.rs`: MASM code generation from profiles +- `src/validator.rs`: Profile validation and comparison +- `benches/component_benchmarks.rs`: Isolated operation benchmarks +- `benches/synthetic_kernel.rs`: Representative workload benchmark +- `profiles/`: Checked-in VM profiles from miden-base diff --git a/benches/synthetic-tx-kernel/benches/component_benchmarks.rs b/benches/synthetic-tx-kernel/benches/component_benchmarks.rs new file mode 100644 index 0000000000..f32f45a656 --- /dev/null +++ b/benches/synthetic-tx-kernel/benches/component_benchmarks.rs @@ -0,0 +1,125 @@ +//! Component-level benchmarks for individual operations + +use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion}; +use miden_core::{Felt, Word}; +use miden_core_lib::{dsa::falcon512_poseidon2, CoreLibrary}; +use miden_processor::{advice::AdviceInputs, fast::FastProcessor, ExecutionOptions}; +use miden_vm::{Assembler, DefaultHost, StackInputs}; + +/// Helper function to execute a benchmark with the given program +fn bench_program( + b: &mut criterion::Bencher, + program: &miden_vm::Program, + stack_inputs: StackInputs, + advice_inputs: AdviceInputs, + load_core_lib: bool, +) { + b.to_async(tokio::runtime::Runtime::new().expect("Failed to create tokio runtime")) + .iter_batched( + || { + let mut host = DefaultHost::default(); + if load_core_lib { + host.load_library(&CoreLibrary::default()) + .expect("Failed to load core library"); + } + let processor = FastProcessor::new_with_options( + stack_inputs, + advice_inputs.clone(), + ExecutionOptions::default(), + ); + (host, processor) + }, + |(mut host, processor)| async move { + black_box(processor.execute(program, &mut host).await.unwrap()); + }, + BatchSize::SmallInput, + ); +} + +fn benchmark_signature_verification(c: &mut Criterion) { + let mut group = c.benchmark_group("signature_verification"); + + // Falcon512 verification benchmark + group.bench_function("falcon512_verify", |b| { + // Direct assembly that calls falcon512 verify with proper setup + let source = r#" + use miden::core::crypto::dsa::falcon512poseidon2 + + begin + # Stack already has PK and MSG from inputs + exec.falcon512poseidon2::verify + end + "#; + + let program = Assembler::default() + .with_dynamic_library(CoreLibrary::default()) + .expect("Failed to load core library") + .assemble_program(source) + .expect("Failed to assemble"); + + let secret_key = falcon512_poseidon2::SecretKey::new(); + let message = Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); + let public_key = secret_key.public_key().to_commitment(); + let signature = + falcon512_poseidon2::sign(&secret_key, message).expect("Failed to generate signature"); + + let mut stack = Vec::with_capacity(8); + stack.extend_from_slice(public_key.as_slice()); + stack.extend_from_slice(message.as_slice()); + let stack_inputs = StackInputs::new(&stack).expect("Failed to build stack inputs"); + let advice_inputs = AdviceInputs::default().with_stack(signature); + + bench_program(b, &program, stack_inputs, advice_inputs, true); + }); + + group.finish(); +} + +fn benchmark_hashing(c: &mut Criterion) { + let mut group = c.benchmark_group("hashing"); + + group.bench_function("hperm", |b| { + let source = r#" + begin + repeat.100 + hperm + end + end + "#; + + let program = Assembler::default().assemble_program(source).expect("Failed to assemble"); + bench_program(b, &program, StackInputs::default(), AdviceInputs::default(), false); + }); + + group.finish(); +} + +fn benchmark_memory_operations(c: &mut Criterion) { + let mut group = c.benchmark_group("memory_operations"); + + group.bench_function("load_store", |b| { + let source = r#" + begin + repeat.100 + push.1 push.2 push.3 push.4 + push.0 mem_storew_be + push.0 mem_loadw_be + dropw + end + end + "#; + + let program = Assembler::default().assemble_program(source).expect("Failed to assemble"); + bench_program(b, &program, StackInputs::default(), AdviceInputs::default(), false); + }); + + group.finish(); +} + +criterion_group!( + benches, + benchmark_signature_verification, + benchmark_hashing, + benchmark_memory_operations +); +criterion_main!(benches); diff --git a/benches/synthetic-tx-kernel/benches/synthetic_kernel.rs b/benches/synthetic-tx-kernel/benches/synthetic_kernel.rs new file mode 100644 index 0000000000..50993154ac --- /dev/null +++ b/benches/synthetic-tx-kernel/benches/synthetic_kernel.rs @@ -0,0 +1,182 @@ +//! Synthetic transaction kernel benchmark +//! +//! This benchmark generates and executes a Miden program that mirrors +//! the instruction mix and operation profile of the real transaction kernel. +//! +//! # Environment Variables +//! +//! - `MASM_WRITE`: When set, writes the generated MASM code to `target/synthetic_kernel.masm` for +//! debugging purposes. + +use std::time::Duration; + +use criterion::{black_box, criterion_group, criterion_main, Criterion, SamplingMode}; +use miden_core_lib::CoreLibrary; +use miden_processor::{fast::FastProcessor, trace::build_trace, ExecutionOptions}; +use miden_vm::{prove_sync, Assembler, DefaultHost, ProvingOptions, StackInputs}; +use synthetic_tx_kernel::{generator::MasmGenerator, load_profile}; + +fn measure_trace_len(program: &miden_vm::Program, core_lib: &CoreLibrary) -> (u64, u64) { + let mut host = DefaultHost::default() + .with_library(core_lib) + .expect("Failed to initialize trace host"); + let processor = FastProcessor::new_with_options( + StackInputs::default(), + miden_processor::advice::AdviceInputs::default(), + ExecutionOptions::default(), + ); + let (execution_output, trace_generation_context) = processor + .execute_for_trace_sync(program, &mut host) + .expect("Failed to execute for trace"); + let trace = build_trace( + execution_output, + trace_generation_context, + miden_processor::ProgramInfo::from(program.clone()), + ); + let summary = trace.trace_len_summary(); + (summary.main_trace_len() as u64, summary.padded_trace_len() as u64) +} + +fn assemble_program(source: &str, core_lib: &CoreLibrary) -> miden_vm::Program { + let mut assembler = Assembler::default(); + assembler + .link_dynamic_library(core_lib.clone()) + .expect("Failed to load core library"); + assembler.assemble_program(source).expect("Failed to assemble synthetic kernel") +} + +fn synthetic_transaction_kernel(c: &mut Criterion) { + let mut group = c.benchmark_group("synthetic_transaction_kernel"); + + group + .sampling_mode(SamplingMode::Flat) + .sample_size(10) + .warm_up_time(Duration::from_millis(500)) + .measurement_time(Duration::from_secs(10)); + + // Load the VM profile using CARGO_MANIFEST_DIR for crate-relative path + let profile_path = format!("{}/profiles/latest.json", env!("CARGO_MANIFEST_DIR")); + let profile = load_profile(&profile_path).unwrap_or_else(|e| { + panic!( + "Failed to load VM profile from '{}': {}. Run miden-base bench-transaction first.", + profile_path, e + ) + }); + + println!("Loaded profile from: {}", profile.source); + println!("Miden VM version: {}", profile.miden_vm_version); + println!("Total cycles in reference: {}", profile.transaction_kernel.total_cycles); + + let trace_target = profile.transaction_kernel.trace_main_len; + + // Generate the synthetic kernel + let mut generator = MasmGenerator::new(profile.clone()); + let mut source = generator.generate_kernel().expect("Failed to generate synthetic kernel"); + + // Assemble with core library (create one instance and reuse it) + let core_lib = CoreLibrary::default(); + + let mut program = assemble_program(&source, &core_lib); + + if let Some(target_main) = trace_target { + let (actual_main, actual_padded) = measure_trace_len(&program, &core_lib); + println!( + "Trace sizing: target main={} actual main={} padded={}", + target_main, actual_main, actual_padded + ); + let trace_scale = actual_main as f64 / target_main as f64; + + if (trace_scale - 1.0).abs() > 0.05 { + generator = generator.with_trace_scale(trace_scale); + source = generator.generate_kernel().expect("Failed to generate trace-sized kernel"); + program = assemble_program(&source, &core_lib); + let (resized_main, resized_padded) = measure_trace_len(&program, &core_lib); + println!( + "Trace sizing result: main={} padded={} scale={:.3}", + resized_main, resized_padded, trace_scale + ); + } + } else { + println!("Trace sizing: skipped (no trace_main_len in profile)"); + } + + // Write the generated code for inspection (only if MASM_WRITE env var is set) + if std::env::var("MASM_WRITE").is_ok() { + std::fs::write("target/synthetic_kernel.masm", &source) + .expect("Failed to write generated kernel"); + } + + // Smoke test: execute once to verify the program runs correctly + let mut test_host = DefaultHost::default() + .with_library(&core_lib) + .expect("Failed to initialize test host"); + let test_processor = FastProcessor::new_with_options( + StackInputs::default(), + miden_processor::advice::AdviceInputs::default(), + ExecutionOptions::default(), + ); + let test_result = tokio::runtime::Runtime::new() + .expect("Failed to create runtime for smoke test") + .block_on(async { test_processor.execute(&program, &mut test_host).await }); + + match test_result { + Ok(_output) => { + println!("Program executed successfully"); + // Note: cycle count verification would require tracking clk from the processor + }, + Err(e) => { + panic!("Generated program failed to execute: {}", e); + }, + } + + group.bench_function("execute", |b| { + b.to_async(tokio::runtime::Runtime::new().unwrap()).iter_batched( + || { + let host = DefaultHost::default() + .with_library(&core_lib) + .expect("Failed to initialize host with core library"); + let processor = FastProcessor::new_with_options( + StackInputs::default(), + miden_processor::advice::AdviceInputs::default(), + ExecutionOptions::default(), + ); + (host, program.clone(), processor) + }, + |(mut host, program, processor)| async move { + black_box(processor.execute(&program, &mut host).await.unwrap()); + }, + criterion::BatchSize::SmallInput, + ); + }); + + group.bench_function("execute_and_prove", |b| { + b.iter_batched( + || { + let host = DefaultHost::default() + .with_library(&core_lib) + .expect("Failed to initialize host with core library"); + let stack_inputs = StackInputs::default(); + let advice_inputs = miden_processor::advice::AdviceInputs::default(); + (host, program.clone(), stack_inputs, advice_inputs) + }, + |(mut host, program, stack_inputs, advice_inputs)| { + black_box( + prove_sync( + &program, + stack_inputs, + advice_inputs, + &mut host, + ProvingOptions::default(), + ) + .unwrap(), + ); + }, + criterion::BatchSize::SmallInput, + ); + }); + + group.finish(); +} + +criterion_group!(benches, synthetic_transaction_kernel); +criterion_main!(benches); diff --git a/benches/synthetic-tx-kernel/profiles/bench-tx-vm-profile.json b/benches/synthetic-tx-kernel/profiles/bench-tx-vm-profile.json new file mode 100644 index 0000000000..1a7acb1bbc --- /dev/null +++ b/benches/synthetic-tx-kernel/profiles/bench-tx-vm-profile.json @@ -0,0 +1,41 @@ +{ + "profile_version": "1.0", + "source": "miden-base/bin/bench-transaction", + "timestamp": "2026-02-02T10:13:46.584544+00:00", + "miden_vm_version": "0.1.0", + "transaction_kernel": { + "total_cycles": 69490, + "phases": { + "prologue": { + "cycles": 2995, + "operations": {} + }, + "tx_script_processing": { + "cycles": 527, + "operations": {} + }, + "epilogue": { + "cycles": 64243, + "operations": {} + }, + "notes_processing": { + "cycles": 1725, + "operations": {} + } + }, + "instruction_mix": { + "arithmetic": 0.009715066916103033, + "hashing": 0.04857533458051516, + "memory": 0.019430133832206067, + "control_flow": 0.019430133832206067, + "signature_verify": 0.9028493308389697 + }, + "key_procedures": [ + { + "name": "auth_procedure", + "cycles": 62739, + "invocations": 1 + } + ] + } +} \ No newline at end of file diff --git a/benches/synthetic-tx-kernel/profiles/latest.json b/benches/synthetic-tx-kernel/profiles/latest.json new file mode 100644 index 0000000000..7353c83c7a --- /dev/null +++ b/benches/synthetic-tx-kernel/profiles/latest.json @@ -0,0 +1,93 @@ +{ + "profile_version": "1.0", + "source": "miden-base/bin/bench-transaction", + "timestamp": "2026-02-03T03:40:57.648937+00:00", + "miden_vm_version": "0.1.0", + "transaction_kernel": { + "total_cycles": 69454, + "trace_main_len": 68897, + "trace_padded_len": 131072, + "phases": { + "epilogue": { + "cycles": 64243, + "operations": {} + }, + "notes_processing": { + "cycles": 1707, + "operations": {} + }, + "prologue": { + "cycles": 2977, + "operations": {} + }, + "tx_script_processing": { + "cycles": 527, + "operations": {} + } + }, + "instruction_mix": { + "arithmetic": 0.009668269646096695, + "hashing": 0.04834134823048347, + "memory": 0.01933653929219339, + "control_flow": 0.01933653929219339, + "signature_verify": 0.9033173035390331 + }, + "key_procedures": [ + { + "name": "auth_procedure", + "cycles": 62739, + "invocations": 1 + } + ], + "operation_details": [ + { + "op_type": "falcon512_verify", + "input_sizes": [ + 64, + 32 + ], + "iterations": 1, + "cycle_cost": 59859 + }, + { + "op_type": "hperm", + "input_sizes": [ + 48 + ], + "iterations": 2685, + "cycle_cost": 1 + }, + { + "op_type": "hmerge", + "input_sizes": [ + 32, + 32 + ], + "iterations": 41, + "cycle_cost": 16 + }, + { + "op_type": "load_store", + "input_sizes": [ + 32 + ], + "iterations": 134, + "cycle_cost": 10 + }, + { + "op_type": "arithmetic", + "input_sizes": [ + 8 + ], + "iterations": 671, + "cycle_cost": 1 + }, + { + "op_type": "control_flow", + "input_sizes": [], + "iterations": 268, + "cycle_cost": 5 + } + ] + } +} diff --git a/benches/synthetic-tx-kernel/profiles/miden-base-v0.20.0.json b/benches/synthetic-tx-kernel/profiles/miden-base-v0.20.0.json new file mode 100644 index 0000000000..b8e596e28e --- /dev/null +++ b/benches/synthetic-tx-kernel/profiles/miden-base-v0.20.0.json @@ -0,0 +1,41 @@ +{ + "profile_version": "1.0", + "source": "miden-base/bin/bench-transaction", + "timestamp": "2025-01-31T12:00:00Z", + "miden_vm_version": "0.20.0", + "transaction_kernel": { + "total_cycles": 73123, + "phases": { + "prologue": { + "cycles": 3173, + "operations": {} + }, + "notes_processing": { + "cycles": 1714, + "operations": {} + }, + "tx_script_processing": { + "cycles": 42, + "operations": {} + }, + "epilogue": { + "cycles": 63977, + "operations": {} + } + }, + "instruction_mix": { + "arithmetic": 0.05, + "hashing": 0.45, + "memory": 0.08, + "control_flow": 0.05, + "signature_verify": 0.37 + }, + "key_procedures": [ + { + "name": "auth_procedure", + "cycles": 62667, + "invocations": 1 + } + ] + } +} diff --git a/benches/synthetic-tx-kernel/src/data_generator.rs b/benches/synthetic-tx-kernel/src/data_generator.rs new file mode 100644 index 0000000000..84c367153e --- /dev/null +++ b/benches/synthetic-tx-kernel/src/data_generator.rs @@ -0,0 +1,224 @@ +//! Data generators for realistic benchmark inputs +//! +//! This module generates fresh cryptographic data for each benchmark iteration, +//! ensuring realistic execution patterns that match real transaction kernels. + +use miden_core::{Felt, Word}; +use miden_core_lib::dsa::falcon512_poseidon2; + +/// Generates Falcon512 signature verification data +pub struct Falcon512Generator; + +impl Falcon512Generator { + /// Generate a fresh key pair, sign a message, and return verification inputs + /// + /// Returns the public key commitment, message, and signature for verification + pub fn generate_verify_data() -> anyhow::Result { + let secret_key = falcon512_poseidon2::SecretKey::new(); + let public_key = secret_key.public_key(); + let public_key_commitment = public_key.to_commitment(); + + // Create a realistic message (4 field elements) + let message = Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); + + // Sign the message + let signature = falcon512_poseidon2::sign(&secret_key, message) + .ok_or_else(|| anyhow::anyhow!("Failed to sign message"))?; + + Ok(Falcon512VerifyData { + public_key_commitment, + message, + signature, + }) + } +} + +/// Data for Falcon512 signature verification +#[derive(Debug, Clone)] +pub struct Falcon512VerifyData { + /// Public key commitment (4 field elements) + pub public_key_commitment: Word, + /// Message that was signed (4 field elements) + pub message: Word, + /// Signature (as a vector of field elements) + pub signature: Vec, +} + +impl Falcon512VerifyData { + /// Build stack inputs for the verification procedure + /// + /// Stack layout: [PK_COMMITMENT_0, PK_COMMITMENT_1, PK_COMMITMENT_2, PK_COMMITMENT_3, + /// MSG_0, MSG_1, MSG_2, MSG_3] + pub fn to_stack_inputs(&self) -> anyhow::Result { + let mut stack = Vec::with_capacity(8); + // Push public key commitment (as slice) + stack.extend_from_slice(self.public_key_commitment.as_slice()); + // Push message (as slice) + stack.extend_from_slice(self.message.as_slice()); + miden_vm::StackInputs::new(&stack) + .map_err(|e| anyhow::anyhow!("Failed to build stack inputs: {}", e)) + } +} + +/// Generates hash operation data +pub struct HashGenerator; + +impl HashGenerator { + /// Generate realistic hash state for hperm operations + /// + /// Returns a 12-element state vector representing the hash capacity and rate + pub fn generate_hperm_state() -> [Felt; 12] { + // Realistic initial state (often zeros or context-specific in transactions) + [ + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(1), + Felt::new(2), + Felt::new(3), + Felt::new(4), + Felt::new(5), + Felt::new(6), + Felt::new(7), + Felt::new(8), + ] + } + + /// Generate input data for hash operations + /// + /// Returns two 4-element words for hmerge or absorption + pub fn generate_hash_inputs() -> (Word, Word) { + let word1 = Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); + let word2 = Word::new([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]); + (word1, word2) + } +} + +/// Generates Merkle tree operation data +pub struct MerkleGenerator; + +impl MerkleGenerator { + /// Generate a Merkle path for verification + /// + /// Creates a simple 4-level tree with a leaf at index 0 + /// Returns the leaf value, its index, and the sibling path + pub fn generate_merkle_path() -> MerklePathData { + // Create leaf nodes (8 leaves for a 3-level tree) + let leaves: Vec = (0..8) + .map(|i| { + Word::new([ + Felt::new(i * 4), + Felt::new(i * 4 + 1), + Felt::new(i * 4 + 2), + Felt::new(i * 4 + 3), + ]) + }) + .collect(); + + // Compute sibling path for leaf 0 + let leaf_index = 0usize; + let sibling_path = Self::compute_sibling_path(&leaves, leaf_index); + + MerklePathData { + leaf: leaves[0], + leaf_index, + sibling_path, + } + } + + /// Compute sibling path for a leaf + fn compute_sibling_path(leaves: &[Word], leaf_index: usize) -> Vec { + let mut path = Vec::new(); + let mut current_level: Vec = leaves.to_vec(); + let mut index = leaf_index; + + while current_level.len() > 1 { + // Find sibling + let sibling_index = if index.is_multiple_of(2) { index + 1 } else { index - 1 }; + if sibling_index < current_level.len() { + path.push(current_level[sibling_index]); + } + + // Move up to parent level + let mut next_level = Vec::new(); + for i in (0..current_level.len()).step_by(2) { + if i + 1 < current_level.len() { + // Compute parent hash (simplified - just use first word for now) + next_level.push(current_level[i]); + } else { + // Odd node out - promote to next level + next_level.push(current_level[i]); + } + } + current_level = next_level; + index /= 2; + } + + path + } +} + +/// Data for Merkle path verification +#[derive(Debug, Clone)] +pub struct MerklePathData { + /// The leaf value being proven + pub leaf: Word, + /// Index of the leaf in the tree + pub leaf_index: usize, + /// Sibling nodes from leaf to root + pub sibling_path: Vec, +} + +#[cfg(test)] +mod tests { + use miden_core::field::PrimeCharacteristicRing; + + use super::*; + + #[test] + fn falcon512_generator_produces_valid_data() { + let data = + Falcon512Generator::generate_verify_data().expect("Failed to generate Falcon512 data"); + + // Verify the data has correct structure (Word is [Felt; 4]) + assert_eq!(data.public_key_commitment.as_slice().len(), 4); + assert_eq!(data.message.as_slice().len(), 4); + } + + #[test] + fn falcon512_stack_inputs_builds_correctly() { + let data = + Falcon512Generator::generate_verify_data().expect("Failed to generate Falcon512 data"); + + let stack_inputs = data.to_stack_inputs().expect("Failed to build stack inputs"); + + // StackInputs always has MIN_STACK_DEPTH (16) elements + // First 8 should be our inputs (4 for PK commitment + 4 for message) + // Remaining should be zeros + let inputs: Vec<_> = stack_inputs.iter().copied().collect(); + assert_eq!(inputs.len(), 16); + + // Check first 8 match our actual inputs + assert_eq!(&inputs[..4], data.public_key_commitment.as_slice()); + assert_eq!(&inputs[4..8], data.message.as_slice()); + + // Check last 8 are zeros (padding) + assert!(inputs[8..].iter().all(|f| *f == Felt::ZERO)); + } + + #[test] + fn hash_generator_produces_valid_state() { + let state = HashGenerator::generate_hperm_state(); + assert_eq!(state.len(), 12); + } + + #[test] + fn merkle_generator_produces_valid_path() { + let path_data = MerkleGenerator::generate_merkle_path(); + + // For an 8-leaf tree, path should have 3 siblings + assert_eq!(path_data.sibling_path.len(), 3); + assert_eq!(path_data.leaf_index, 0); + } +} diff --git a/benches/synthetic-tx-kernel/src/generator.rs b/benches/synthetic-tx-kernel/src/generator.rs new file mode 100644 index 0000000000..a6b7d51274 --- /dev/null +++ b/benches/synthetic-tx-kernel/src/generator.rs @@ -0,0 +1,584 @@ +//! Generates Miden assembly from VM profiles + +use std::fmt::Write; + +use anyhow::Result; + +use crate::profile::VmProfile; + +/// Cycle costs for individual operations (measured from actual execution) +pub const CYCLES_PER_HPERM: u64 = 1; +pub const CYCLES_PER_HMERGE: u64 = 16; +pub const CYCLES_PER_FALCON512_VERIFY: u64 = 59859; +pub const CYCLES_PER_LOAD_STORE: u64 = 10; // Approximate for push+store+load+drop +const MAX_REPEAT: u64 = 1000; + +/// Generates masm code for a synthetic transaction kernel +pub struct MasmGenerator { + profile: VmProfile, + trace_scale: f64, +} + +impl MasmGenerator { + pub fn new(profile: VmProfile) -> Self { + Self { profile, trace_scale: 1.0 } + } + + pub fn with_trace_scale(mut self, trace_scale: f64) -> Self { + self.trace_scale = if trace_scale.is_finite() && trace_scale > 0.0 { + trace_scale + } else { + 1.0 + }; + self + } + + /// Generate the complete synthetic kernel program + pub fn generate_kernel(&self) -> Result { + let mut code = String::new(); + let kernel = &self.profile.transaction_kernel; + + // Header + writeln!(code, "# Synthetic Transaction Kernel")?; + writeln!(code, "# Generated from: {}", self.profile.source)?; + writeln!(code, "# Version: {}\n", self.profile.miden_vm_version)?; + + // Use core library for crypto operations + writeln!(code, "use miden::core::crypto::dsa::falcon512poseidon2")?; + writeln!(code, "use miden::core::crypto::hashes::poseidon2\n")?; + + // Main program + writeln!(code, "begin")?; + writeln!(code, " # Synthetic transaction kernel")?; + writeln!(code, " # Total cycles: {}", kernel.total_cycles)?; + writeln!(code, " # Instruction mix: {:?}\n", kernel.instruction_mix)?; + + // Generate each phase + for (phase_name, phase) in &kernel.phases { + code.push_str(&self.generate_phase(phase_name, phase)?); + } + + writeln!(code, "end")?; + Ok(code) + } + + fn generate_phase(&self, name: &str, phase: &crate::profile::PhaseProfile) -> Result { + let mut code = String::new(); + code.push_str(&format!(" # Phase: {} ({} cycles)\n", name, phase.cycles)); + + // If phase has specific operations defined, use those + if !phase.operations.is_empty() { + for (op_name, count) in &phase.operations { + let scaled = self.scale_count(*count); + code.push_str(&self.generate_operation(op_name, scaled)?); + } + } else { + // Otherwise, generate operations based on instruction mix + code.push_str(&self.generate_phase_from_mix(name, phase.cycles)?); + } + + code.push('\n'); + Ok(code) + } + + /// Generate operations for a phase based on the global instruction mix + /// + /// This generates a representative mix of operations that approximates the + /// instruction mix without trying to exactly match every cycle (which would + /// create an impractical number of operations). + fn generate_phase_from_mix(&self, _phase_name: &str, phase_cycles: u64) -> Result { + let mix = &self.profile.transaction_kernel.instruction_mix; + let mut code = String::new(); + + // Scale down the operations to reasonable numbers while maintaining proportions + // We target ~1000-10000 cycles per phase for the synthetic benchmark + let scale_factor = if phase_cycles > 10000 { + phase_cycles as f64 / 5000.0 // Scale to ~5000 cycles + } else { + 1.0 + } * self.trace_scale; + + // Calculate how many of each operation to generate based on instruction mix + let sig_verify_count = ((phase_cycles as f64 * mix.signature_verify) + / CYCLES_PER_FALCON512_VERIFY as f64 + / scale_factor) + .max(1.0) as u64; + let hperm_count = ((phase_cycles as f64 * mix.hashing) + / CYCLES_PER_HPERM as f64 + / scale_factor) + .max(10.0) as u64; + let load_store_count = + ((phase_cycles as f64 * mix.memory) / CYCLES_PER_LOAD_STORE as f64 / scale_factor) + .max(5.0) as u64; + let arithmetic_count = + ((phase_cycles as f64 * mix.arithmetic) / scale_factor).max(10.0) as u64; + let control_count = + ((phase_cycles as f64 * mix.control_flow) / 5.0 / scale_factor).max(5.0) as u64; + + // Generate signature verifications (most expensive operation) + if mix.signature_verify > 0.0 { + code.push_str(&self.generate_falcon_verify_block(sig_verify_count)?); + } + + // Generate hashing operations + if mix.hashing > 0.0 { + code.push_str(&self.generate_hperm_block(hperm_count)?); + } + + // Generate memory operations + if mix.memory > 0.0 { + code.push_str(&self.generate_load_store_block(load_store_count)?); + } + + // Generate arithmetic operations (simple math) + if mix.arithmetic > 0.0 { + code.push_str(&self.generate_arithmetic_block(arithmetic_count)?); + } + + // Generate control flow (loops, conditionals) + if mix.control_flow > 0.0 { + code.push_str(&self.generate_control_flow_block(control_count)?); + } + + Ok(code) + } + + fn scale_count(&self, count: u64) -> u64 { + ((count as f64) / self.trace_scale).max(1.0) as u64 + } + + fn generate_operation(&self, op_name: &str, count: u64) -> Result { + match op_name { + "hperm" => self.generate_hperm_block(count), + "hmerge" => self.generate_hmerge_block(count), + "mtree_get" => self.generate_mtree_get_block(count), + "sig_verify_falcon512" => self.generate_falcon_verify_block(count), + _ => Ok(format!(" # {} {} operations (unimplemented)\n", count, op_name)), + } + } + + fn generate_hperm_block(&self, count: u64) -> Result { + let mut code = String::new(); + code.push_str(&format!(" # {} hperm operations\n", count)); + + // Set up initial hash state (12 elements) + code.push_str(" # Initialize hash state\n"); + code.push_str(" padw padw padw\n"); + + // Generate hperm operations in a loop + if count > 100 { + push_repeat_block(&mut code, count, " ", &["hperm"]); + } else { + for _ in 0..count { + code.push_str(" hperm\n"); + } + } + + // Clean up stack + code.push_str(" dropw dropw dropw\n"); + Ok(code) + } + + fn generate_hmerge_block(&self, count: u64) -> Result { + let mut code = String::new(); + code.push_str(&format!(" # {} hmerge operations\n", count)); + + // Generate hmerge operations with balanced stack per iteration + let hmerge_body = + ["push.1 push.2 push.3 push.4", "push.5 push.6 push.7 push.8", "hmerge", "dropw"]; + if count > 100 { + push_repeat_block(&mut code, count, " ", &hmerge_body); + } else { + for _ in 0..count { + code.push_str(" push.1 push.2 push.3 push.4\n"); + code.push_str(" push.5 push.6 push.7 push.8\n"); + code.push_str(" hmerge\n"); + code.push_str(" dropw\n"); + } + } + Ok(code) + } + + fn generate_mtree_get_block(&self, count: u64) -> Result { + let mut code = String::new(); + code.push_str(&format!(" # {} mtree_get operations\n", count)); + code.push_str(" # Note: mtree_get requires Merkle store setup\n"); + + // Placeholder - mtree_get requires proper Merkle store initialization + for _ in 0..count.min(10) { + code.push_str(" # mtree_get (requires store setup)\n"); + } + + Ok(code) + } + + fn generate_falcon_verify_block(&self, count: u64) -> Result { + let mut code = String::new(); + code.push_str(&format!(" # {} Falcon512 signature verifications\n", count)); + code.push_str(&format!( + " # Each verification is ~{} cycles\n", + CYCLES_PER_FALCON512_VERIFY + )); + + // For synthetic benchmarks, we simulate the cycle cost without actually + // executing the verification (which requires advice inputs). + // We use a loop of nop operations that approximates the cycle count. + // Each loop iteration costs ~1 cycle (the nop itself + loop overhead). + + let scaled_cycles = + ((CYCLES_PER_FALCON512_VERIFY as f64) / self.trace_scale).max(1.0) as u64; + + for _ in 0..count { + code.push_str(&format!( + " # Simulating falcon512_verify cycle count (~{} cycles)\n", + scaled_cycles + )); + push_repeat_block(&mut code, scaled_cycles, " ", &["nop"]); + } + + Ok(code) + } + + fn generate_load_store_block(&self, count: u64) -> Result { + let mut code = String::new(); + code.push_str(&format!(" # {} load/store operations\n", count)); + + if count > 100 { + let body = [ + "push.1 push.2 push.3 push.4", + "push.0 mem_storew_be", + "push.0 mem_loadw_be", + "dropw", + ]; + push_repeat_block(&mut code, count, " ", &body); + } else { + for _ in 0..count { + code.push_str(" push.1 push.2 push.3 push.4\n"); + code.push_str(" push.0 mem_storew_be\n"); + code.push_str(" push.0 mem_loadw_be\n"); + code.push_str(" dropw\n"); + } + } + + Ok(code) + } + + fn generate_arithmetic_block(&self, count: u64) -> Result { + let mut code = String::new(); + code.push_str(&format!(" # {} arithmetic operations\n", count)); + + // Use balanced operations that don't accumulate on the stack + // Each iteration: push two values, add them, drop the result + if count > 100 { + push_repeat_block(&mut code, count, " ", &["push.1 push.2 add drop"]); + } else { + for _ in 0..count { + code.push_str(" push.1 push.2 add drop\n"); + } + } + + Ok(code) + } + + fn generate_control_flow_block(&self, count: u64) -> Result { + let mut code = String::new(); + code.push_str(&format!(" # {} control flow operations\n", count)); + + // Simple control flow with if/else + let iterations = count / 5; // Each iteration ~5 cycles + if iterations > 10 { + let body = ["push.1", "if.true", " push.2", "else", " push.3", "end", "drop"]; + push_repeat_block(&mut code, iterations.min(100), " ", &body); + } else { + for _ in 0..iterations { + code.push_str(" push.1\n"); + code.push_str(" if.true\n"); + code.push_str(" push.2\n"); + code.push_str(" else\n"); + code.push_str(" push.3\n"); + code.push_str(" end\n"); + code.push_str(" drop\n"); + } + } + + Ok(code) + } + + /// Generate a component benchmark for a specific operation type + pub fn generate_component_benchmark( + &self, + operation: &str, + iterations: usize, + ) -> Result { + let mut code = String::new(); + + writeln!(code, "# Component Benchmark: {}", operation)?; + + match operation { + "falcon512_verify" => { + writeln!(code, "use miden::core::crypto::dsa::falcon512poseidon2\n")?; + writeln!(code, "begin")?; + writeln!(code, " # Stack must contain PK commitment and message inputs")?; + writeln!(code, " # Stack: [PK_COMMITMENT (4 elements), MSG (4 elements)]")?; + let body = ["# Execute verification", "exec.falcon512poseidon2::verify", "drop"]; + push_repeat_block(&mut code, iterations as u64, " ", &body); + writeln!(code, "end")?; + }, + "hperm" => { + writeln!(code, "begin")?; + writeln!(code, " # Initialize hash state (12 elements)")?; + writeln!(code, " padw padw padw")?; + push_repeat_block(&mut code, iterations as u64, " ", &["hperm"]); + writeln!(code, " # Clean up")?; + writeln!(code, " dropw dropw dropw")?; + writeln!(code, "end")?; + }, + "hmerge" => { + writeln!(code, "begin")?; + let body = [ + "push.1 push.2 push.3 push.4", + "push.5 push.6 push.7 push.8", + "hmerge", + "dropw", + ]; + push_repeat_block(&mut code, iterations as u64, " ", &body); + writeln!(code, "end")?; + }, + "load_store" => { + writeln!(code, "begin")?; + let body = [ + "push.1 push.2 push.3 push.4", + "push.0 mem_storew_be", + "push.0 mem_loadw_be", + "dropw", + ]; + push_repeat_block(&mut code, iterations as u64, " ", &body); + writeln!(code, "end")?; + }, + "arithmetic" => { + writeln!(code, "begin")?; + push_repeat_block( + &mut code, + iterations as u64, + " ", + &["push.1 push.2 add drop"], + ); + writeln!(code, "end")?; + }, + "control_flow" => { + writeln!(code, "begin")?; + let body = ["push.1", "if.true", " push.2", "else", " push.3", "end", "drop"]; + push_repeat_block(&mut code, iterations as u64, " ", &body); + writeln!(code, "end")?; + }, + _ => { + writeln!(code, "# {} operation (unimplemented)", operation)?; + writeln!(code, "begin")?; + push_repeat_block(&mut code, iterations as u64, " ", &["nop"]); + writeln!(code, "end")?; + }, + } + + Ok(code) + } +} + +fn push_repeat_block(code: &mut String, count: u64, indent: &str, body_lines: &[&str]) { + if count == 0 { + return; + } + if count <= MAX_REPEAT { + push_single_repeat_block(code, count, indent, body_lines); + return; + } + + let block_size = MAX_REPEAT * MAX_REPEAT; + let mut remaining = count; + + while remaining >= block_size { + push_nested_repeat_block(code, MAX_REPEAT, MAX_REPEAT, indent, body_lines); + remaining -= block_size; + } + + if remaining >= MAX_REPEAT { + let outer = remaining / MAX_REPEAT; + push_nested_repeat_block(code, outer, MAX_REPEAT, indent, body_lines); + remaining %= MAX_REPEAT; + } + + if remaining > 0 { + push_single_repeat_block(code, remaining, indent, body_lines); + } +} + +fn push_single_repeat_block(code: &mut String, count: u64, indent: &str, body_lines: &[&str]) { + writeln!(code, "{indent}repeat.{count}").unwrap(); + for line in body_lines { + writeln!(code, "{indent} {line}").unwrap(); + } + writeln!(code, "{indent}end").unwrap(); +} + +fn push_nested_repeat_block( + code: &mut String, + outer: u64, + inner: u64, + indent: &str, + body_lines: &[&str], +) { + writeln!(code, "{indent}repeat.{outer}").unwrap(); + writeln!(code, "{indent} repeat.{inner}").unwrap(); + for line in body_lines { + writeln!(code, "{indent} {line}").unwrap(); + } + writeln!(code, "{indent} end").unwrap(); + writeln!(code, "{indent}end").unwrap(); +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use miden_core_lib::CoreLibrary; + use miden_processor::{advice::AdviceInputs, fast::FastProcessor, ExecutionOptions}; + use miden_vm::{Assembler, DefaultHost, StackInputs}; + + use super::*; + use crate::{ + data_generator::Falcon512Generator, + profile::{ + InstructionMix, PhaseProfile, ProcedureProfile, TransactionKernelProfile, VmProfile, + }, + }; + + fn test_generator() -> MasmGenerator { + let profile = VmProfile { + profile_version: "1.0.0".to_string(), + source: "test".to_string(), + timestamp: "2026-02-02T00:00:00Z".to_string(), + miden_vm_version: "0.1.0".to_string(), + transaction_kernel: TransactionKernelProfile { + total_cycles: 0, + trace_main_len: None, + trace_padded_len: None, + phases: BTreeMap::from([( + "prologue".to_string(), + PhaseProfile { cycles: 0, operations: BTreeMap::new() }, + )]), + instruction_mix: InstructionMix { + arithmetic: 0.2, + hashing: 0.2, + memory: 0.2, + control_flow: 0.2, + signature_verify: 0.2, + }, + key_procedures: vec![ProcedureProfile { + name: "auth_procedure".to_string(), + cycles: 0, + invocations: 0, + }], + operation_details: Vec::new(), + }, + }; + + MasmGenerator::new(profile) + } + + #[test] + fn component_benchmarks_assemble() { + let generator = test_generator(); + let operations = [ + "falcon512_verify", + "hperm", + "hmerge", + "load_store", + "arithmetic", + "control_flow", + ]; + + for operation in operations { + let source = generator + .generate_component_benchmark(operation, 1) + .expect("failed to generate benchmark"); + + let assembler = if operation == "falcon512_verify" { + Assembler::default() + .with_dynamic_library(CoreLibrary::default()) + .expect("failed to load core library") + } else { + Assembler::default() + }; + + assembler.assemble_program(&source).expect("failed to assemble benchmark"); + } + } + + #[test] + fn component_benchmarks_execute() { + let generator = test_generator(); + let operations = ["hperm", "hmerge", "load_store", "arithmetic", "control_flow"]; + + for operation in operations { + let source = generator + .generate_component_benchmark(operation, 3) + .expect("failed to generate benchmark"); + + let program = Assembler::default() + .assemble_program(&source) + .expect("failed to assemble benchmark"); + + let mut host = DefaultHost::default(); + let processor = FastProcessor::new_with_options( + StackInputs::default(), + AdviceInputs::default(), + ExecutionOptions::default(), + ); + let runtime = tokio::runtime::Runtime::new().expect("failed to create runtime"); + + runtime + .block_on(async { processor.execute(&program, &mut host).await }) + .expect("failed to execute benchmark"); + } + } + + #[test] + fn falcon512_component_benchmark_execute() { + let generator = test_generator(); + let source = generator + .generate_component_benchmark("falcon512_verify", 1) + .expect("failed to generate benchmark"); + let program = Assembler::default() + .with_dynamic_library(CoreLibrary::default()) + .expect("failed to load core library") + .assemble_program(&source) + .expect("failed to assemble benchmark"); + + let verify_data = + Falcon512Generator::generate_verify_data().expect("failed to generate verify data"); + let stack_inputs = verify_data.to_stack_inputs().expect("failed to build stack inputs"); + let advice_inputs = AdviceInputs::default().with_stack(verify_data.signature); + + let mut host = DefaultHost::default(); + host.load_library(&CoreLibrary::default()).expect("failed to load core library"); + let processor = FastProcessor::new_with_options( + stack_inputs, + advice_inputs, + ExecutionOptions::default(), + ); + let runtime = tokio::runtime::Runtime::new().expect("failed to create runtime"); + + runtime + .block_on(async { processor.execute(&program, &mut host).await }) + .expect("failed to execute benchmark"); + } + + #[test] + fn falcon512_component_benchmark_emits_verify() { + let generator = test_generator(); + let source = generator + .generate_component_benchmark("falcon512_verify", 1) + .expect("failed to generate benchmark"); + + assert!(source.contains("exec.falcon512poseidon2::verify")); + } +} diff --git a/benches/synthetic-tx-kernel/src/lib.rs b/benches/synthetic-tx-kernel/src/lib.rs new file mode 100644 index 0000000000..1d0be54edd --- /dev/null +++ b/benches/synthetic-tx-kernel/src/lib.rs @@ -0,0 +1,36 @@ +//! Synthetic transaction kernel benchmark generator +//! +//! This crate generates Miden assembly benchmarks based on VM profiles +//! exported from miden-base's transaction kernel. + +pub mod data_generator; +pub mod generator; +pub mod profile; +pub mod validator; + +use std::path::Path; + +use anyhow::Result; + +/// Load a VM profile from a JSON file +pub fn load_profile>(path: P) -> Result { + let content = std::fs::read_to_string(path)?; + let profile = serde_json::from_str(&content)?; + Ok(profile) +} + +/// Get the latest profile from the profiles directory +/// +/// # Note +/// This function looks for the profile relative to the current working directory. +/// For workspace-relative paths, use `load_profile` with an explicit path. +pub fn latest_profile() -> Result { + // Try to find the workspace root by looking for Cargo.toml with workspace definition + let manifest_dir = + std::env::var("CARGO_MANIFEST_DIR").map(std::path::PathBuf::from).or_else(|_| { + std::env::current_dir() + .map_err(|e| anyhow::anyhow!("Failed to determine current directory: {}", e)) + })?; + + load_profile(manifest_dir.join("profiles/latest.json")) +} diff --git a/benches/synthetic-tx-kernel/src/profile.rs b/benches/synthetic-tx-kernel/src/profile.rs new file mode 100644 index 0000000000..b769cb5fa8 --- /dev/null +++ b/benches/synthetic-tx-kernel/src/profile.rs @@ -0,0 +1,376 @@ +//! VM profile types (mirrors miden-base profile format) + +// BTreeMap is used instead of HashMap for deterministic iteration order +// which ensures consistent serialization and easier testing +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VmProfile { + /// Profile format version (expected format: "major.minor.patch", e.g., "1.0.0") + pub profile_version: String, + pub source: String, + /// ISO 8601 formatted timestamp (e.g., "2024-01-15T10:30:00Z") + pub timestamp: String, + /// Miden VM version (expected format: "major.minor.patch", e.g., "0.20.0") + pub miden_vm_version: String, + pub transaction_kernel: TransactionKernelProfile, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionKernelProfile { + pub total_cycles: u64, + #[serde(default)] + pub trace_main_len: Option, + #[serde(default)] + pub trace_padded_len: Option, + /// Phase names are expected to be from a fixed set: + /// "prologue", "notes_processing", "tx_script_processing", "epilogue" + pub phases: BTreeMap, + pub instruction_mix: InstructionMix, + pub key_procedures: Vec, + /// Detailed operation information for generating realistic benchmarks + #[serde(default)] + pub operation_details: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PhaseProfile { + pub cycles: u64, + /// Operation types are expected to be from a fixed set: + /// "hperm", "hmerge", "mtree_get", "sig_verify_falcon512" + pub operations: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InstructionMix { + pub arithmetic: f64, + pub hashing: f64, + pub memory: f64, + pub control_flow: f64, + pub signature_verify: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcedureProfile { + pub name: String, + pub cycles: u64, + pub invocations: u64, +} + +/// Detailed information about a specific operation type +/// Used by synthetic benchmark generators to create realistic workloads +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OperationDetails { + /// Operation type identifier (e.g., "falcon512_verify", "hperm", "hmerge") + pub op_type: String, + /// Size of each input in bytes (for operations with variable input sizes) + pub input_sizes: Vec, + /// Number of times this operation is executed + pub iterations: u64, + /// Estimated cycle cost per operation (for validation) + pub cycle_cost: u64, +} + +impl InstructionMix { + /// Tolerance for floating point comparisons (1%) + const TOLERANCE: f64 = 0.01; + /// Validates that: + /// - All individual values are between 0.0 and 1.0 (inclusive) + /// - Values sum to approximately 1.0 (within 1% tolerance) + pub fn validate(&self) -> anyhow::Result<()> { + // Check each field is in valid range [0.0, 1.0] + let fields = [ + ("arithmetic", self.arithmetic), + ("hashing", self.hashing), + ("memory", self.memory), + ("control_flow", self.control_flow), + ("signature_verify", self.signature_verify), + ]; + + for (name, value) in fields { + if !(0.0..=1.0).contains(&value) { + anyhow::bail!( + "Instruction mix field '{}' must be between 0.0 and 1.0, got {}", + name, + value + ); + } + } + + // Check sum is approximately 1.0 + let total = self.arithmetic + + self.hashing + + self.memory + + self.control_flow + + self.signature_verify; + if (total - 1.0).abs() > Self::TOLERANCE { + anyhow::bail!("Instruction mix percentages sum to {}, expected ~1.0", total); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_valid_instruction_mix() -> InstructionMix { + InstructionMix { + arithmetic: 0.05, + hashing: 0.45, + memory: 0.08, + control_flow: 0.05, + signature_verify: 0.37, + } + } + + fn create_valid_vm_profile() -> VmProfile { + let mut phases = BTreeMap::new(); + phases.insert( + "prologue".to_string(), + PhaseProfile { + cycles: 3173, + operations: BTreeMap::new(), + }, + ); + phases.insert( + "epilogue".to_string(), + PhaseProfile { + cycles: 63977, + operations: BTreeMap::new(), + }, + ); + + VmProfile { + profile_version: "1.0.0".to_string(), + source: "test".to_string(), + timestamp: "2024-01-15T10:30:00Z".to_string(), + miden_vm_version: "0.20.0".to_string(), + transaction_kernel: TransactionKernelProfile { + total_cycles: 73123, + trace_main_len: None, + trace_padded_len: None, + phases, + instruction_mix: create_valid_instruction_mix(), + key_procedures: vec![ProcedureProfile { + name: "auth_procedure".to_string(), + cycles: 62667, + invocations: 1, + }], + operation_details: Vec::new(), + }, + } + } + + #[test] + fn instruction_mix_valid_passes() { + let mix = create_valid_instruction_mix(); + assert!(mix.validate().is_ok()); + } + + #[test] + fn instruction_mix_negative_value_fails() { + let mix = InstructionMix { + arithmetic: -0.1, + hashing: 0.5, + memory: 0.2, + control_flow: 0.2, + signature_verify: 0.2, + }; + assert!(mix.validate().is_err()); + } + + #[test] + fn instruction_mix_value_over_one_fails() { + let mix = InstructionMix { + arithmetic: 1.5, + hashing: 0.5, + memory: 0.2, + control_flow: 0.2, + signature_verify: 0.2, + }; + assert!(mix.validate().is_err()); + } + + #[test] + fn instruction_mix_sum_not_one_fails() { + let mix = InstructionMix { + arithmetic: 0.3, + hashing: 0.3, + memory: 0.2, + control_flow: 0.2, + signature_verify: 0.2, + }; + let result = mix.validate(); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("sum to")); + } + + #[test] + fn instruction_mix_sum_within_tolerance_passes() { + let mix = InstructionMix { + arithmetic: 0.2001, + hashing: 0.1999, + memory: 0.2, + control_flow: 0.2, + signature_verify: 0.2, + }; + assert!(mix.validate().is_ok()); + } + + #[test] + fn instruction_mix_tolerance_boundary_just_under_passes() { + // Sum = 1.0095 (just under 1.0 + TOLERANCE = 1.01) + let delta = 0.0019; + let mix = InstructionMix { + arithmetic: 0.2 + delta, + hashing: 0.2 + delta, + memory: 0.2 + delta, + control_flow: 0.2 + delta, + signature_verify: 0.2 + delta, + }; + assert!(mix.validate().is_ok()); + } + + #[test] + fn instruction_mix_tolerance_boundary_just_over_fails() { + // Sum = 1.0105 (just over 1.0 + TOLERANCE = 1.01) + let delta = 0.0021; + let mix = InstructionMix { + arithmetic: 0.2 + delta, + hashing: 0.2 + delta, + memory: 0.2 + delta, + control_flow: 0.2 + delta, + signature_verify: 0.2 + delta, + }; + assert!(mix.validate().is_err()); + } + + #[test] + fn instruction_mix_tolerance_boundary_just_over_min_passes() { + // Sum = 0.9905 (just over 1.0 - TOLERANCE = 0.99) + let delta = -0.0019; + let mix = InstructionMix { + arithmetic: 0.2 + delta, + hashing: 0.2 + delta, + memory: 0.2 + delta, + control_flow: 0.2 + delta, + signature_verify: 0.2 + delta, + }; + assert!(mix.validate().is_ok()); + } + + #[test] + fn instruction_mix_tolerance_boundary_just_under_min_fails() { + // Sum = 0.9895 (just under 1.0 - TOLERANCE = 0.99) + let delta = -0.0021; + let mix = InstructionMix { + arithmetic: 0.2 + delta, + hashing: 0.2 + delta, + memory: 0.2 + delta, + control_flow: 0.2 + delta, + signature_verify: 0.2 + delta, + }; + assert!(mix.validate().is_err()); + } + + #[test] + fn serde_roundtrip_vm_profile() { + let original = create_valid_vm_profile(); + let json = serde_json::to_string(&original).expect("serialize failed"); + let deserialized: VmProfile = serde_json::from_str(&json).expect("deserialize failed"); + + assert_eq!(original.profile_version, deserialized.profile_version); + assert_eq!(original.source, deserialized.source); + assert_eq!(original.timestamp, deserialized.timestamp); + assert_eq!(original.miden_vm_version, deserialized.miden_vm_version); + assert_eq!( + original.transaction_kernel.total_cycles, + deserialized.transaction_kernel.total_cycles + ); + assert_eq!( + original.transaction_kernel.phases.len(), + deserialized.transaction_kernel.phases.len() + ); + assert_eq!( + original.transaction_kernel.key_procedures.len(), + deserialized.transaction_kernel.key_procedures.len() + ); + } + + #[test] + fn serde_empty_hashmaps() { + let profile = VmProfile { + profile_version: "1.0.0".to_string(), + source: "test".to_string(), + timestamp: "2024-01-15T10:30:00Z".to_string(), + miden_vm_version: "0.20.0".to_string(), + transaction_kernel: TransactionKernelProfile { + total_cycles: 0, + trace_main_len: None, + trace_padded_len: None, + phases: BTreeMap::new(), + instruction_mix: InstructionMix { + arithmetic: 0.2, + hashing: 0.2, + memory: 0.2, + control_flow: 0.2, + signature_verify: 0.2, + }, + key_procedures: vec![], + operation_details: Vec::new(), + }, + }; + + let json = serde_json::to_string(&profile).expect("serialize failed"); + let deserialized: VmProfile = serde_json::from_str(&json).expect("deserialize failed"); + + assert!(deserialized.transaction_kernel.phases.is_empty()); + assert!(deserialized.transaction_kernel.key_procedures.is_empty()); + } + + #[test] + fn serde_zero_cycles() { + let mut phases = BTreeMap::new(); + phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 0, operations: BTreeMap::new() }, + ); + + let profile = VmProfile { + profile_version: "1.0.0".to_string(), + source: "test".to_string(), + timestamp: "2024-01-15T10:30:00Z".to_string(), + miden_vm_version: "0.20.0".to_string(), + transaction_kernel: TransactionKernelProfile { + total_cycles: 0, + trace_main_len: None, + trace_padded_len: None, + phases, + instruction_mix: InstructionMix { + arithmetic: 0.2, + hashing: 0.2, + memory: 0.2, + control_flow: 0.2, + signature_verify: 0.2, + }, + key_procedures: vec![], + operation_details: Vec::new(), + }, + }; + + let json = serde_json::to_string(&profile).expect("serialize failed"); + let deserialized: VmProfile = serde_json::from_str(&json).expect("deserialize failed"); + + assert_eq!(deserialized.transaction_kernel.total_cycles, 0); + let prologue = deserialized + .transaction_kernel + .phases + .get("prologue") + .expect("prologue phase missing"); + assert_eq!(prologue.cycles, 0); + } +} diff --git a/benches/synthetic-tx-kernel/src/validator.rs b/benches/synthetic-tx-kernel/src/validator.rs new file mode 100644 index 0000000000..6f0f26fff6 --- /dev/null +++ b/benches/synthetic-tx-kernel/src/validator.rs @@ -0,0 +1,422 @@ +//! Validates that synthetic benchmarks match their source profiles + +use anyhow::{bail, Result}; + +use crate::profile::VmProfile; + +/// Validates a VM profile for correctness +pub struct ProfileValidator; + +impl ProfileValidator { + /// Validate a profile + pub fn validate(&self, profile: &VmProfile) -> Result<()> { + // Check version - supports "1.0" or "1.0.x" format + if !profile.profile_version.starts_with("1.0") { + bail!("Unsupported profile version: {}", profile.profile_version); + } + + // Validate instruction mix sums to ~1.0 + profile.transaction_kernel.instruction_mix.validate()?; + + if let Some(main_len) = profile.transaction_kernel.trace_main_len { + if main_len == 0 { + bail!("Trace main length is zero"); + } + } + if let Some(padded_len) = profile.transaction_kernel.trace_padded_len { + if padded_len == 0 { + bail!("Trace padded length is zero"); + } + if let Some(main_len) = profile.transaction_kernel.trace_main_len { + if padded_len < main_len { + bail!( + "Trace padded length ({padded_len}) is smaller than main length ({main_len})" + ); + } + } + } + + // Check that total cycles matches sum of phases + let phase_total: u64 = profile.transaction_kernel.phases.values().map(|p| p.cycles).sum(); + + if phase_total == 0 { + bail!("Total cycles is zero"); + } + + // Allow 1% tolerance, with minimum of 1 to avoid zero tolerance for small profiles + let diff = phase_total.abs_diff(profile.transaction_kernel.total_cycles); + + let tolerance = (profile.transaction_kernel.total_cycles / 100).max(1); + if diff > tolerance { + bail!( + "Phase cycle sum ({}) differs from total ({}) by more than 1%", + phase_total, + profile.transaction_kernel.total_cycles + ); + } + + Ok(()) + } + + /// Compare two profiles and report differences + pub fn compare_profiles(&self, baseline: &VmProfile, current: &VmProfile) -> ProfileDiff { + let (phase_deltas, missing_phases, new_phases) = self.compare_phases(baseline, current); + + ProfileDiff { + total_cycles_delta: current.transaction_kernel.total_cycles as i64 + - baseline.transaction_kernel.total_cycles as i64, + phase_deltas, + missing_phases, + new_phases, + } + } + + fn compare_phases( + &self, + baseline: &VmProfile, + current: &VmProfile, + ) -> (Vec, Vec, Vec) { + let mut deltas = Vec::new(); + let mut missing_phases = Vec::new(); + let mut new_phases = Vec::new(); + + // Find phases in current that differ from or are missing in baseline + for (name, current_phase) in ¤t.transaction_kernel.phases { + if let Some(baseline_phase) = baseline.transaction_kernel.phases.get(name) { + let delta = current_phase.cycles as i64 - baseline_phase.cycles as i64; + let pct_change = if baseline_phase.cycles == 0 { + if current_phase.cycles == 0 { + 0.0 + } else { + f64::INFINITY + } + } else { + (delta as f64 / baseline_phase.cycles as f64) * 100.0 + }; + + deltas.push(PhaseDelta { + name: name.clone(), + cycles_delta: delta, + percent_change: pct_change, + }); + } else { + new_phases.push(name.clone()); + } + } + + // Find phases in baseline that are missing in current + for name in baseline.transaction_kernel.phases.keys() { + if !current.transaction_kernel.phases.contains_key(name) { + missing_phases.push(name.clone()); + } + } + + (deltas, missing_phases, new_phases) + } +} + +#[derive(Debug)] +pub struct ProfileDiff { + pub total_cycles_delta: i64, + pub phase_deltas: Vec, + /// Phases present in baseline but missing in current + pub missing_phases: Vec, + /// Phases present in current but not in baseline + pub new_phases: Vec, +} + +#[derive(Debug)] +pub struct PhaseDelta { + pub name: String, + pub cycles_delta: i64, + pub percent_change: f64, +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::*; + use crate::profile::{ + InstructionMix, PhaseProfile, ProcedureProfile, TransactionKernelProfile, + }; + + fn create_test_profile( + version: &str, + total_cycles: u64, + phases: BTreeMap, + ) -> VmProfile { + VmProfile { + profile_version: version.to_string(), + source: "test".to_string(), + timestamp: "2024-01-15T10:30:00Z".to_string(), + miden_vm_version: "0.20.0".to_string(), + transaction_kernel: TransactionKernelProfile { + total_cycles, + trace_main_len: None, + trace_padded_len: None, + phases, + instruction_mix: InstructionMix { + arithmetic: 0.2, + hashing: 0.2, + memory: 0.2, + control_flow: 0.2, + signature_verify: 0.2, + }, + key_procedures: vec![ProcedureProfile { + name: "test".to_string(), + cycles: 100, + invocations: 1, + }], + operation_details: Vec::new(), + }, + } + } + + #[test] + fn validate_valid_profile_passes() { + let mut phases = BTreeMap::new(); + phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 50, operations: BTreeMap::new() }, + ); + phases.insert( + "epilogue".to_string(), + PhaseProfile { cycles: 50, operations: BTreeMap::new() }, + ); + + // Test with "1.0.0" format (major.minor.patch) + let profile = create_test_profile("1.0.0", 100, phases); + + assert!(ProfileValidator.validate(&profile).is_ok()); + } + + #[test] + fn validate_valid_profile_short_version_passes() { + let mut phases = BTreeMap::new(); + phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 50, operations: BTreeMap::new() }, + ); + + // Test with "1.0" format (major.minor) + let profile = create_test_profile("1.0", 50, phases); + + assert!(ProfileValidator.validate(&profile).is_ok()); + } + + #[test] + fn validate_unsupported_version_fails() { + let mut phases = BTreeMap::new(); + phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 100, operations: BTreeMap::new() }, + ); + + let profile = create_test_profile("2.0", 100, phases); + + let result = ProfileValidator.validate(&profile); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Unsupported profile version")); + } + + #[test] + fn validate_zero_cycles_fails() { + let phases = BTreeMap::new(); + let profile = create_test_profile("1.0", 0, phases); + + let result = ProfileValidator.validate(&profile); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Total cycles is zero")); + } + + #[test] + fn validate_mismatched_totals_fails() { + let mut phases = BTreeMap::new(); + phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 50, operations: BTreeMap::new() }, + ); + // total_cycles is 1000 but phases only sum to 50 + let profile = create_test_profile("1.0", 1000, phases); + + let result = ProfileValidator.validate(&profile); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("differs from total")); + } + + #[test] + fn validate_small_profile_with_min_tolerance() { + // Profile with total_cycles < 100 should still work with max(1) tolerance + let mut phases = BTreeMap::new(); + phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 10, operations: BTreeMap::new() }, + ); + + // total_cycles = 10, phases sum to 10, diff = 0, tolerance = max(10/100, 1) = 1 + let profile = create_test_profile("1.0", 10, phases); + + assert!(ProfileValidator.validate(&profile).is_ok()); + } + + #[test] + fn compare_profiles_detects_deltas() { + let mut baseline_phases = BTreeMap::new(); + baseline_phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 100, operations: BTreeMap::new() }, + ); + + let mut current_phases = BTreeMap::new(); + current_phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 150, operations: BTreeMap::new() }, + ); + + let baseline = create_test_profile("1.0", 100, baseline_phases); + let current = create_test_profile("1.0", 150, current_phases); + + let diff = ProfileValidator.compare_profiles(&baseline, ¤t); + + assert_eq!(diff.total_cycles_delta, 50); + assert_eq!(diff.phase_deltas.len(), 1); + assert_eq!(diff.phase_deltas[0].name, "prologue"); + assert_eq!(diff.phase_deltas[0].cycles_delta, 50); + assert_eq!(diff.phase_deltas[0].percent_change, 50.0); + } + + #[test] + fn compare_profiles_zero_baseline_cycles() { + let mut baseline_phases = BTreeMap::new(); + baseline_phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 0, operations: BTreeMap::new() }, + ); + + let mut current_phases = BTreeMap::new(); + current_phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 50, operations: BTreeMap::new() }, + ); + + let baseline = create_test_profile("1.0", 0, baseline_phases); + let current = create_test_profile("1.0", 50, current_phases); + + let diff = ProfileValidator.compare_profiles(&baseline, ¤t); + + assert_eq!(diff.phase_deltas.len(), 1); + assert_eq!(diff.phase_deltas[0].percent_change, f64::INFINITY); + } + + #[test] + fn compare_profiles_both_zero_cycles() { + let mut baseline_phases = BTreeMap::new(); + baseline_phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 0, operations: BTreeMap::new() }, + ); + + let mut current_phases = BTreeMap::new(); + current_phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 0, operations: BTreeMap::new() }, + ); + + let baseline = create_test_profile("1.0", 0, baseline_phases); + let current = create_test_profile("1.0", 0, current_phases); + + let diff = ProfileValidator.compare_profiles(&baseline, ¤t); + + assert_eq!(diff.phase_deltas.len(), 1); + assert_eq!(diff.phase_deltas[0].percent_change, 0.0); + } + + #[test] + fn compare_profiles_detects_missing_phases() { + let mut baseline_phases = BTreeMap::new(); + baseline_phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 100, operations: BTreeMap::new() }, + ); + baseline_phases.insert( + "epilogue".to_string(), + PhaseProfile { cycles: 100, operations: BTreeMap::new() }, + ); + + let mut current_phases = BTreeMap::new(); + current_phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 100, operations: BTreeMap::new() }, + ); + // epilogue is missing + + let baseline = create_test_profile("1.0", 200, baseline_phases); + let current = create_test_profile("1.0", 100, current_phases); + + let diff = ProfileValidator.compare_profiles(&baseline, ¤t); + + assert_eq!(diff.missing_phases.len(), 1); + assert_eq!(diff.missing_phases[0], "epilogue"); + } + + #[test] + fn compare_profiles_detects_new_phases() { + let mut baseline_phases = BTreeMap::new(); + baseline_phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 100, operations: BTreeMap::new() }, + ); + + let mut current_phases = BTreeMap::new(); + current_phases.insert( + "prologue".to_string(), + PhaseProfile { cycles: 100, operations: BTreeMap::new() }, + ); + current_phases.insert( + "new_phase".to_string(), + PhaseProfile { cycles: 50, operations: BTreeMap::new() }, + ); + + let baseline = create_test_profile("1.0", 100, baseline_phases); + let current = create_test_profile("1.0", 150, current_phases); + + let diff = ProfileValidator.compare_profiles(&baseline, ¤t); + + assert_eq!(diff.new_phases.len(), 1); + assert_eq!(diff.new_phases[0], "new_phase"); + } + + #[test] + fn profile_diff_infinity_display_and_serialization() { + // Test that infinity values in ProfileDiff are handled correctly + let mut baseline_phases = BTreeMap::new(); + baseline_phases.insert( + "zero_phase".to_string(), + PhaseProfile { cycles: 0, operations: BTreeMap::new() }, + ); + + let mut current_phases = BTreeMap::new(); + current_phases.insert( + "zero_phase".to_string(), + PhaseProfile { cycles: 100, operations: BTreeMap::new() }, + ); + + let baseline = create_test_profile("1.0", 0, baseline_phases); + let current = create_test_profile("1.0", 100, current_phases); + + let diff = ProfileValidator.compare_profiles(&baseline, ¤t); + + // Verify the infinity value is present + assert_eq!(diff.phase_deltas[0].percent_change, f64::INFINITY); + + // When serialized to JSON, infinity becomes null + // This test documents this behavior for consumers + let json = serde_json::to_string(&diff.phase_deltas[0].percent_change).unwrap(); + assert_eq!(json, "null"); + + // Display/debug should show "inf" + let debug_str = format!("{:?}", diff.phase_deltas[0].percent_change); + assert!(debug_str.contains("inf")); + } +} diff --git a/core/src/mast/node/call_node.rs b/core/src/mast/node/call_node.rs index b200938a6a..1b858e0a48 100644 --- a/core/src/mast/node/call_node.rs +++ b/core/src/mast/node/call_node.rs @@ -9,12 +9,13 @@ use miden_formatting::{ use serde::{Deserialize, Serialize}; use super::{MastForestContributor, MastNodeExt}; +#[cfg(debug_assertions)] +use crate::mast::MastNode; use crate::{ Felt, Word, chiplets::hasher, mast::{ - DecoratorId, DecoratorStore, MastForest, MastForestError, MastNode, MastNodeFingerprint, - MastNodeId, + DecoratorId, DecoratorStore, MastForest, MastForestError, MastNodeFingerprint, MastNodeId, }, operations::{OPCODE_CALL, OPCODE_SYSCALL}, utils::{Idx, LookupByIdx}, diff --git a/core/src/mast/node/dyn_node.rs b/core/src/mast/node/dyn_node.rs index d077cdcab3..b49d8eca84 100644 --- a/core/src/mast/node/dyn_node.rs +++ b/core/src/mast/node/dyn_node.rs @@ -5,11 +5,12 @@ use core::fmt; use serde::{Deserialize, Serialize}; use super::{MastForestContributor, MastNodeExt}; +#[cfg(debug_assertions)] +use crate::mast::MastNode; use crate::{ Felt, Word, mast::{ - DecoratorId, DecoratorStore, MastForest, MastForestError, MastNode, MastNodeFingerprint, - MastNodeId, + DecoratorId, DecoratorStore, MastForest, MastForestError, MastNodeFingerprint, MastNodeId, }, operations::{OPCODE_DYN, OPCODE_DYNCALL}, prettier::{Document, PrettyPrint, const_text, nl}, diff --git a/core/src/mast/node/join_node.rs b/core/src/mast/node/join_node.rs index 8417894c6b..3c23502f98 100644 --- a/core/src/mast/node/join_node.rs +++ b/core/src/mast/node/join_node.rs @@ -5,12 +5,13 @@ use core::fmt; use serde::{Deserialize, Serialize}; use super::{MastForestContributor, MastNodeExt}; +#[cfg(debug_assertions)] +use crate::mast::MastNode; use crate::{ Felt, Word, chiplets::hasher, mast::{ - DecoratorId, DecoratorStore, MastForest, MastForestError, MastNode, MastNodeFingerprint, - MastNodeId, + DecoratorId, DecoratorStore, MastForest, MastForestError, MastNodeFingerprint, MastNodeId, }, operations::OPCODE_JOIN, prettier::PrettyPrint, diff --git a/core/src/mast/node/loop_node.rs b/core/src/mast/node/loop_node.rs index 71a4894e1f..fd1758b699 100644 --- a/core/src/mast/node/loop_node.rs +++ b/core/src/mast/node/loop_node.rs @@ -5,12 +5,13 @@ use core::fmt; use serde::{Deserialize, Serialize}; use super::{MastForestContributor, MastNodeExt}; +#[cfg(debug_assertions)] +use crate::mast::MastNode; use crate::{ Felt, Word, chiplets::hasher, mast::{ - DecoratorId, DecoratorStore, MastForest, MastForestError, MastNode, MastNodeFingerprint, - MastNodeId, + DecoratorId, DecoratorStore, MastForest, MastForestError, MastNodeFingerprint, MastNodeId, }, operations::OPCODE_LOOP, prettier::PrettyPrint, diff --git a/core/src/mast/node/split_node.rs b/core/src/mast/node/split_node.rs index 928e9cf258..d8fd7a0c73 100644 --- a/core/src/mast/node/split_node.rs +++ b/core/src/mast/node/split_node.rs @@ -5,12 +5,13 @@ use core::fmt; use serde::{Deserialize, Serialize}; use super::{MastForestContributor, MastNodeExt}; +#[cfg(debug_assertions)] +use crate::mast::MastNode; use crate::{ Felt, Word, chiplets::hasher, mast::{ - DecoratorId, DecoratorStore, MastForest, MastForestError, MastNode, MastNodeFingerprint, - MastNodeId, + DecoratorId, DecoratorStore, MastForest, MastForestError, MastNodeFingerprint, MastNodeId, }, operations::OPCODE_SPLIT, prettier::PrettyPrint,