From 30d166602ecd994199f95318de5caaa1a8994a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Sun, 1 Feb 2026 07:06:31 -0500 Subject: [PATCH 01/13] feat(bench): add VM profile data structures for synthetic benchmark export --- bin/bench-transaction/src/lib.rs | 1 + bin/bench-transaction/src/vm_profile.rs | 44 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 bin/bench-transaction/src/vm_profile.rs diff --git a/bin/bench-transaction/src/lib.rs b/bin/bench-transaction/src/lib.rs index 882a85de80..0a9f40bc7e 100644 --- a/bin/bench-transaction/src/lib.rs +++ b/bin/bench-transaction/src/lib.rs @@ -1 +1,2 @@ pub mod context_setups; +pub mod vm_profile; diff --git a/bin/bench-transaction/src/vm_profile.rs b/bin/bench-transaction/src/vm_profile.rs new file mode 100644 index 0000000000..febe63de6c --- /dev/null +++ b/bin/bench-transaction/src/vm_profile.rs @@ -0,0 +1,44 @@ +//! VM execution profile types for synthetic benchmark generation + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Versioned VM profile exported from transaction kernel benchmarks +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VmProfile { + pub profile_version: String, + pub source: String, + pub timestamp: String, + pub miden_vm_version: String, + pub transaction_kernel: TransactionKernelProfile, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionKernelProfile { + pub total_cycles: u64, + pub phases: HashMap, + pub instruction_mix: InstructionMix, + pub key_procedures: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PhaseProfile { + pub cycles: u64, + pub operations: HashMap, +} + +#[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, +} From 9881826069466e4c9d291400b60252a335d6111f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Sun, 1 Feb 2026 07:09:03 -0500 Subject: [PATCH 02/13] feat(bench): export VM profile to JSON after benchmark run --- bin/bench-transaction/Cargo.toml | 1 + .../src/cycle_counting_benchmarks/utils.rs | 120 ++++++++++++++++++ bin/bench-transaction/src/main.rs | 6 +- 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/bin/bench-transaction/Cargo.toml b/bin/bench-transaction/Cargo.toml index e932ad28fa..41e9380928 100644 --- a/bin/bench-transaction/Cargo.toml +++ b/bin/bench-transaction/Cargo.toml @@ -24,6 +24,7 @@ miden-tx = { workspace = true } # External dependencies anyhow = { workspace = true } +chrono = { version = "0.4", features = ["serde"] } serde = { features = ["derive"], workspace = true } serde_json = { features = ["preserve_order"], package = "serde_json", version = "1.0" } tokio = { features = ["macros", "rt"], workspace = true } diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index 49a916c390..680b718dca 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -1,6 +1,7 @@ extern crate alloc; pub use alloc::collections::BTreeMap; pub use alloc::string::String; +use std::collections::HashMap; use std::fs::{read_to_string, write}; use std::path::Path; @@ -10,6 +11,7 @@ use serde::Serialize; use serde_json::{Value, from_str, to_string_pretty}; use super::ExecutionBenchmark; +use crate::vm_profile::{VmProfile, TransactionKernelProfile, PhaseProfile, InstructionMix, ProcedureProfile}; // MEASUREMENTS PRINTER // ================================================================================================ @@ -102,3 +104,121 @@ pub fn write_bench_results_to_json( Ok(()) } + +/// Writes the VM execution profile to a JSON file for synthetic benchmark generation. +/// +/// This exports a machine-readable profile that describes the transaction kernel's +/// instruction mix and cycle characteristics, which can be used by miden-vm to +/// generate representative synthetic benchmarks. +pub fn write_vm_profile( + path: &Path, + tx_benchmarks: &[(ExecutionBenchmark, MeasurementsPrinter)], +) -> anyhow::Result<()> { + // Aggregate measurements across all benchmarks to create a representative profile + let mut total_prologue = 0usize; + let mut total_notes_processing = 0usize; + let mut total_tx_script = 0usize; + let mut total_epilogue = 0usize; + let mut total_auth = 0usize; + let mut count = 0usize; + + for (_, measurements) in tx_benchmarks { + total_prologue += measurements.prologue; + total_notes_processing += measurements.notes_processing; + total_tx_script += measurements.tx_script_processing; + total_epilogue += measurements.epilogue.total; + total_auth += measurements.epilogue.auth_procedure; + count += 1; + } + + if count == 0 { + anyhow::bail!("No benchmark results to aggregate"); + } + + // Calculate averages + let avg_prologue = (total_prologue / count) as u64; + let avg_notes_processing = (total_notes_processing / count) as u64; + let avg_tx_script = (total_tx_script / count) as u64; + let avg_epilogue = (total_epilogue / count) as u64; + let avg_auth = (total_auth / count) as u64; + + // Build phase map + let mut phases = HashMap::new(); + phases.insert( + "prologue".to_string(), + PhaseProfile { + cycles: avg_prologue, + operations: HashMap::new(), // TODO: Add operation counting when VM instrumentation is available + }, + ); + phases.insert( + "notes_processing".to_string(), + PhaseProfile { + cycles: avg_notes_processing, + operations: HashMap::new(), + }, + ); + phases.insert( + "tx_script_processing".to_string(), + PhaseProfile { + cycles: avg_tx_script, + operations: HashMap::new(), + }, + ); + phases.insert( + "epilogue".to_string(), + PhaseProfile { + cycles: avg_epilogue, + operations: HashMap::new(), + }, + ); + + // Calculate total cycles + let total_cycles = avg_prologue + avg_notes_processing + avg_tx_script + avg_epilogue; + + // Estimate instruction mix based on known characteristics + // Auth procedure (signature verification) dominates at ~85% of epilogue + let signature_ratio = if avg_epilogue > 0 { + (avg_auth as f64) / (total_cycles as f64) + } else { + 0.0 + }; + + // Remaining cycles are distributed among other operations + let remaining = 1.0 - signature_ratio; + let instruction_mix = InstructionMix { + signature_verify: signature_ratio, + hashing: remaining * 0.5, // Hashing is significant in remaining work + memory: remaining * 0.2, // Memory operations + control_flow: remaining * 0.2, // Loops, conditionals + arithmetic: remaining * 0.1, // Basic arithmetic + }; + + // Key procedures - auth is the heavyweight + let key_procedures = vec![ + ProcedureProfile { + name: "auth_procedure".to_string(), + cycles: avg_auth, + invocations: 1, + }, + ]; + + let profile = VmProfile { + profile_version: "1.0".to_string(), + source: "miden-base/bin/bench-transaction".to_string(), + timestamp: chrono::Utc::now().to_rfc3339(), + miden_vm_version: env!("CARGO_PKG_VERSION").to_string(), + transaction_kernel: TransactionKernelProfile { + total_cycles, + phases, + instruction_mix, + key_procedures, + }, + }; + + let json = serde_json::to_string_pretty(&profile)?; + write(path, json).context("failed to write VM profile to file")?; + + println!("VM profile exported to: {}", path.display()); + Ok(()) +} diff --git a/bin/bench-transaction/src/main.rs b/bin/bench-transaction/src/main.rs index 651be7993b..db11e290fb 100644 --- a/bin/bench-transaction/src/main.rs +++ b/bin/bench-transaction/src/main.rs @@ -14,7 +14,7 @@ use context_setups::{ mod cycle_counting_benchmarks; use cycle_counting_benchmarks::ExecutionBenchmark; -use cycle_counting_benchmarks::utils::write_bench_results_to_json; +use cycle_counting_benchmarks::utils::{write_bench_results_to_json, write_vm_profile}; #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { @@ -54,5 +54,9 @@ async fn main() -> Result<()> { // store benchmark results in the JSON file write_bench_results_to_json(path, benchmark_results)?; + // Export VM profile for synthetic benchmark generation + let profile_path = Path::new("bin/bench-transaction/bench-tx-vm-profile.json"); + write_vm_profile(profile_path, &benchmark_results)?; + Ok(()) } From 06a66bcc22983f3634e4a6fa99045461447a0829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Sun, 1 Feb 2026 07:14:34 -0500 Subject: [PATCH 03/13] fix(bench): resolve module imports for vm_profile --- .../src/cycle_counting_benchmarks/utils.rs | 2 +- bin/bench-transaction/src/main.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index 680b718dca..772b2cb335 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -112,7 +112,7 @@ pub fn write_bench_results_to_json( /// generate representative synthetic benchmarks. pub fn write_vm_profile( path: &Path, - tx_benchmarks: &[(ExecutionBenchmark, MeasurementsPrinter)], + tx_benchmarks: &Vec<(ExecutionBenchmark, MeasurementsPrinter)>, ) -> anyhow::Result<()> { // Aggregate measurements across all benchmarks to create a representative profile let mut total_prologue = 0usize; diff --git a/bin/bench-transaction/src/main.rs b/bin/bench-transaction/src/main.rs index db11e290fb..be7ef2098e 100644 --- a/bin/bench-transaction/src/main.rs +++ b/bin/bench-transaction/src/main.rs @@ -12,6 +12,8 @@ use context_setups::{ tx_create_single_p2id_note, }; +mod vm_profile; + mod cycle_counting_benchmarks; use cycle_counting_benchmarks::ExecutionBenchmark; use cycle_counting_benchmarks::utils::{write_bench_results_to_json, write_vm_profile}; @@ -51,12 +53,12 @@ async fn main() -> Result<()> { ), ]; - // store benchmark results in the JSON file - write_bench_results_to_json(path, benchmark_results)?; - - // Export VM profile for synthetic benchmark generation + // Export VM profile for synthetic benchmark generation (before write_bench_results_to_json consumes the data) let profile_path = Path::new("bin/bench-transaction/bench-tx-vm-profile.json"); write_vm_profile(profile_path, &benchmark_results)?; + // store benchmark results in the JSON file + write_bench_results_to_json(path, benchmark_results)?; + Ok(()) } From 9d696be86117a5d34a8203aae7891aa54ccee4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Sun, 1 Feb 2026 08:08:34 -0500 Subject: [PATCH 04/13] style(bench): formatting and use slice instead of Vec reference --- .../src/cycle_counting_benchmarks/utils.rs | 31 +++++++++++-------- bin/bench-transaction/src/main.rs | 3 +- bin/bench-transaction/src/vm_profile.rs | 3 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index 772b2cb335..5771330dd6 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -11,7 +11,13 @@ use serde::Serialize; use serde_json::{Value, from_str, to_string_pretty}; use super::ExecutionBenchmark; -use crate::vm_profile::{VmProfile, TransactionKernelProfile, PhaseProfile, InstructionMix, ProcedureProfile}; +use crate::vm_profile::{ + InstructionMix, + PhaseProfile, + ProcedureProfile, + TransactionKernelProfile, + VmProfile, +}; // MEASUREMENTS PRINTER // ================================================================================================ @@ -112,7 +118,7 @@ pub fn write_bench_results_to_json( /// generate representative synthetic benchmarks. pub fn write_vm_profile( path: &Path, - tx_benchmarks: &Vec<(ExecutionBenchmark, MeasurementsPrinter)>, + tx_benchmarks: &[(ExecutionBenchmark, MeasurementsPrinter)], ) -> anyhow::Result<()> { // Aggregate measurements across all benchmarks to create a representative profile let mut total_prologue = 0usize; @@ -148,7 +154,8 @@ pub fn write_vm_profile( "prologue".to_string(), PhaseProfile { cycles: avg_prologue, - operations: HashMap::new(), // TODO: Add operation counting when VM instrumentation is available + operations: HashMap::new(), /* TODO: Add operation counting when VM instrumentation + * is available */ }, ); phases.insert( @@ -188,20 +195,18 @@ pub fn write_vm_profile( let remaining = 1.0 - signature_ratio; let instruction_mix = InstructionMix { signature_verify: signature_ratio, - hashing: remaining * 0.5, // Hashing is significant in remaining work - memory: remaining * 0.2, // Memory operations + hashing: remaining * 0.5, // Hashing is significant in remaining work + memory: remaining * 0.2, // Memory operations control_flow: remaining * 0.2, // Loops, conditionals - arithmetic: remaining * 0.1, // Basic arithmetic + arithmetic: remaining * 0.1, // Basic arithmetic }; // Key procedures - auth is the heavyweight - let key_procedures = vec![ - ProcedureProfile { - name: "auth_procedure".to_string(), - cycles: avg_auth, - invocations: 1, - }, - ]; + let key_procedures = vec![ProcedureProfile { + name: "auth_procedure".to_string(), + cycles: avg_auth, + invocations: 1, + }]; let profile = VmProfile { profile_version: "1.0".to_string(), diff --git a/bin/bench-transaction/src/main.rs b/bin/bench-transaction/src/main.rs index be7ef2098e..d017dd451a 100644 --- a/bin/bench-transaction/src/main.rs +++ b/bin/bench-transaction/src/main.rs @@ -53,7 +53,8 @@ async fn main() -> Result<()> { ), ]; - // Export VM profile for synthetic benchmark generation (before write_bench_results_to_json consumes the data) + // Export VM profile for synthetic benchmark generation (before write_bench_results_to_json + // consumes the data) let profile_path = Path::new("bin/bench-transaction/bench-tx-vm-profile.json"); write_vm_profile(profile_path, &benchmark_results)?; diff --git a/bin/bench-transaction/src/vm_profile.rs b/bin/bench-transaction/src/vm_profile.rs index febe63de6c..ca015590a9 100644 --- a/bin/bench-transaction/src/vm_profile.rs +++ b/bin/bench-transaction/src/vm_profile.rs @@ -1,8 +1,9 @@ //! VM execution profile types for synthetic benchmark generation -use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use serde::{Deserialize, Serialize}; + /// Versioned VM profile exported from transaction kernel benchmarks #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VmProfile { From 73b92deffdb918f2d82fb0ea6e8dae76682b2468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Sun, 1 Feb 2026 08:30:44 -0500 Subject: [PATCH 05/13] chore(bench): update Cargo.lock with chrono dependency Add Cargo.lock changes from adding chrono dependency to bench-transaction for VM profile timestamp functionality. --- Cargo.lock | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ed558bfe4e..483c998959 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -224,6 +233,7 @@ name = "bench-transaction" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "criterion 0.6.0", "miden-protocol", "miden-standards", @@ -350,6 +360,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -458,6 +482,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1101,6 +1131,30 @@ dependencies = [ "digest", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indenter" version = "0.3.4" @@ -3324,6 +3378,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -3339,6 +3428,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" From 995a13cc18d6298da160878dc7cd373d74dc2bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Sun, 1 Feb 2026 08:38:24 -0500 Subject: [PATCH 06/13] style(bench): replace .unwrap() with .expect() in benchmark files Replace .unwrap() calls with .expect() in: - bin/bench-note-checker/benches/benchmarks.rs:25 - bin/bench-transaction/src/time_counting_benchmarks/prove.rs:39, 58, 91, 115 --- bin/bench-note-checker/benches/benchmarks.rs | 9 +- .../src/time_counting_benchmarks/prove.rs | 140 ++++++++++-------- 2 files changed, 85 insertions(+), 64 deletions(-) diff --git a/bin/bench-note-checker/benches/benchmarks.rs b/bin/bench-note-checker/benches/benchmarks.rs index b09a138f72..63f2911a0f 100644 --- a/bin/bench-note-checker/benches/benchmarks.rs +++ b/bin/bench-note-checker/benches/benchmarks.rs @@ -22,8 +22,13 @@ fn note_checker_benchmarks(c: &mut Criterion) { setup_mixed_notes_benchmark(MixedNotesConfig { failing_note_count: failing_count }) .expect("failed to set up mixed notes benchmark"); - b.to_async(tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap()) - .iter(|| async { black_box(run_mixed_notes_check(&setup).await) }); + b.to_async( + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("failed to build tokio runtime"), + ) + .iter(|| async { black_box(run_mixed_notes_check(&setup).await) }); }); } diff --git a/bin/bench-transaction/src/time_counting_benchmarks/prove.rs b/bin/bench-transaction/src/time_counting_benchmarks/prove.rs index 5dafb4604d..b3912e19a5 100644 --- a/bin/bench-transaction/src/time_counting_benchmarks/prove.rs +++ b/bin/bench-transaction/src/time_counting_benchmarks/prove.rs @@ -36,35 +36,43 @@ fn core_benchmarks(c: &mut Criterion) { .warm_up_time(Duration::from_millis(1000)); execute_group.bench_function(BENCH_EXECUTE_TX_CONSUME_SINGLE_P2ID, |b| { - b.to_async(tokio::runtime::Builder::new_current_thread().build().unwrap()) - .iter_batched( - || { - // prepare the transaction context - tx_consume_single_p2id_note() - .expect("failed to create a context which consumes single P2ID note") - }, - |tx_context| async move { - // benchmark the transaction execution - black_box(tx_context.execute().await) - }, - BatchSize::SmallInput, - ); + b.to_async( + tokio::runtime::Builder::new_current_thread() + .build() + .expect("failed to build tokio runtime"), + ) + .iter_batched( + || { + // prepare the transaction context + tx_consume_single_p2id_note() + .expect("failed to create a context which consumes single P2ID note") + }, + |tx_context| async move { + // benchmark the transaction execution + black_box(tx_context.execute().await) + }, + BatchSize::SmallInput, + ); }); execute_group.bench_function(BENCH_EXECUTE_TX_CONSUME_TWO_P2ID, |b| { - b.to_async(tokio::runtime::Builder::new_current_thread().build().unwrap()) - .iter_batched( - || { - // prepare the transaction context - tx_consume_two_p2id_notes() - .expect("failed to create a context which consumes two P2ID notes") - }, - |tx_context| async move { - // benchmark the transaction execution - black_box(tx_context.execute().await) - }, - BatchSize::SmallInput, - ); + b.to_async( + tokio::runtime::Builder::new_current_thread() + .build() + .expect("failed to build tokio runtime"), + ) + .iter_batched( + || { + // prepare the transaction context + tx_consume_two_p2id_notes() + .expect("failed to create a context which consumes two P2ID notes") + }, + |tx_context| async move { + // benchmark the transaction execution + black_box(tx_context.execute().await) + }, + BatchSize::SmallInput, + ); }); execute_group.finish(); @@ -80,45 +88,53 @@ fn core_benchmarks(c: &mut Criterion) { .warm_up_time(Duration::from_millis(1000)); execute_and_prove_group.bench_function(BENCH_EXECUTE_AND_PROVE_TX_CONSUME_SINGLE_P2ID, |b| { - b.to_async(tokio::runtime::Builder::new_current_thread().build().unwrap()) - .iter_batched( - || { - // prepare the transaction context - tx_consume_single_p2id_note() - .expect("failed to create a context which consumes single P2ID note") - }, - |tx_context| async move { - // benchmark the transaction execution and proving - black_box(prove_transaction( - tx_context - .execute() - .await - .expect("execution of the single P2ID note consumption tx failed"), - )) - }, - BatchSize::SmallInput, - ); + b.to_async( + tokio::runtime::Builder::new_current_thread() + .build() + .expect("failed to build tokio runtime"), + ) + .iter_batched( + || { + // prepare the transaction context + tx_consume_single_p2id_note() + .expect("failed to create a context which consumes single P2ID note") + }, + |tx_context| async move { + // benchmark the transaction execution and proving + black_box(prove_transaction( + tx_context + .execute() + .await + .expect("execution of the single P2ID note consumption tx failed"), + )) + }, + BatchSize::SmallInput, + ); }); execute_and_prove_group.bench_function(BENCH_EXECUTE_AND_PROVE_TX_CONSUME_TWO_P2ID, |b| { - b.to_async(tokio::runtime::Builder::new_current_thread().build().unwrap()) - .iter_batched( - || { - // prepare the transaction context - tx_consume_two_p2id_notes() - .expect("failed to create a context which consumes two P2ID notes") - }, - |tx_context| async move { - // benchmark the transaction execution and proving - black_box(prove_transaction( - tx_context - .execute() - .await - .expect("execution of the two P2ID note consumption tx failed"), - )) - }, - BatchSize::SmallInput, - ); + b.to_async( + tokio::runtime::Builder::new_current_thread() + .build() + .expect("failed to build tokio runtime"), + ) + .iter_batched( + || { + // prepare the transaction context + tx_consume_two_p2id_notes() + .expect("failed to create a context which consumes two P2ID notes") + }, + |tx_context| async move { + // benchmark the transaction execution and proving + black_box(prove_transaction( + tx_context + .execute() + .await + .expect("execution of the two P2ID note consumption tx failed"), + )) + }, + BatchSize::SmallInput, + ); }); execute_and_prove_group.finish(); From a98e26ffa80df6d35b9934c26b4adb5a7c6571d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Mon, 2 Feb 2026 06:41:55 -0500 Subject: [PATCH 07/13] chore(bench): add exported VM profile from transaction benchmarks Add the VM profile exported from bench-transaction for use in miden-vm synthetic benchmarks. --- .../bench-tx-vm-profile.json | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 bin/bench-transaction/bench-tx-vm-profile.json diff --git a/bin/bench-transaction/bench-tx-vm-profile.json b/bin/bench-transaction/bench-tx-vm-profile.json new file mode 100644 index 0000000000..48357f1d2d --- /dev/null +++ b/bin/bench-transaction/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 + } + ] + } +} From 074179e9910509b0c69c178b154a00410467c69f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Mon, 2 Feb 2026 18:52:37 -0500 Subject: [PATCH 08/13] feat(bench): add OperationDetails to VM profile export with tests Add detailed operation information to the VM profile for synthetic benchmark generation. This includes: - OperationDetails struct with op_type, input_sizes, iterations, cycle_cost - Profile export populates operation_details based on instruction mix - Supports falcon512_verify, hperm, hmerge, load_store, arithmetic, control_flow Fixes for realistic workload generation: - Apply minimum iteration counts only when total_cycles >= 10000 - Use MIN_MIX_RATIO (0.1%) threshold to avoid floating-point noise - Only include operations with meaningful mix ratios Added tests: - deserialize_profile_without_operation_details: backward compatibility - deserialize_profile_with_operation_details: forward compatibility - operation_details_consistent_with_total_cycles: cycle validation - tiny_workload_no_minimum_inflation: small workload handling Enables the synthetic-tx-kernel benchmark to generate realistic workloads matching the instruction mix from real transaction profiles. --- bin/bench-transaction/Cargo.toml | 2 +- .../src/cycle_counting_benchmarks/utils.rs | 237 ++++++++++++++++++ bin/bench-transaction/src/vm_profile.rs | 195 ++++++++++++++ 3 files changed, 433 insertions(+), 1 deletion(-) diff --git a/bin/bench-transaction/Cargo.toml b/bin/bench-transaction/Cargo.toml index 41e9380928..6adbcae735 100644 --- a/bin/bench-transaction/Cargo.toml +++ b/bin/bench-transaction/Cargo.toml @@ -24,7 +24,7 @@ miden-tx = { workspace = true } # External dependencies anyhow = { workspace = true } -chrono = { version = "0.4", features = ["serde"] } +chrono = { features = ["serde"], version = "0.4" } serde = { features = ["derive"], workspace = true } serde_json = { features = ["preserve_order"], package = "serde_json", version = "1.0" } tokio = { features = ["macros", "rt"], workspace = true } diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index 5771330dd6..9708796de5 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -13,6 +13,7 @@ use serde_json::{Value, from_str, to_string_pretty}; use super::ExecutionBenchmark; use crate::vm_profile::{ InstructionMix, + OperationDetails, PhaseProfile, ProcedureProfile, TransactionKernelProfile, @@ -208,6 +209,117 @@ pub fn write_vm_profile( invocations: 1, }]; + // Build operation details based on instruction mix + // These are estimates based on typical transaction patterns + let mut operation_details = Vec::new(); + + // Minimum threshold for including an operation type (avoid floating-point noise) + const MIN_MIX_RATIO: f64 = 0.001; // 0.1% + // Threshold for applying minimum iteration counts (only for substantial workloads) + const MIN_CYCLES_FOR_MINIMUMS: u64 = 10000; + let apply_minimums = total_cycles >= MIN_CYCLES_FOR_MINIMUMS; + + // Signature verification operations + // Only include if the calculated count is at least 1 (avoid inflating small workloads) + if signature_ratio > MIN_MIX_RATIO { + let sig_count = (total_cycles as f64 * signature_ratio / 59859.0) as u64; + // Only include signature verification if we have at least 1 full verification + // This avoids inflating operation_details with a 60K cycle op when the + // actual average auth cost is much smaller + if sig_count > 0 { + operation_details.push(OperationDetails { + op_type: "falcon512_verify".to_string(), + input_sizes: vec![64, 32], // PK commitment (64 bytes), message (32 bytes) + iterations: sig_count, + cycle_cost: 59859, + }); + } + } + + // Hashing operations - split hashing ratio between hperm (80%) and hmerge (20%) + // This approximates observed patterns where permutations dominate over merges + const HPERM_HASHING_RATIO: f64 = 0.8; + const HMERGE_HASHING_RATIO: f64 = 0.2; + + if instruction_mix.hashing > MIN_MIX_RATIO { + let hperm_count = + (total_cycles as f64 * instruction_mix.hashing * HPERM_HASHING_RATIO) as u64; + if hperm_count > 0 { + operation_details.push(OperationDetails { + op_type: "hperm".to_string(), + input_sizes: vec![48], // 12 field elements state + iterations: if apply_minimums { + hperm_count.max(100) + } else { + hperm_count + }, + cycle_cost: 1, + }); + } + + let hmerge_count = + (total_cycles as f64 * instruction_mix.hashing * HMERGE_HASHING_RATIO / 16.0) as u64; + if hmerge_count > 0 { + operation_details.push(OperationDetails { + op_type: "hmerge".to_string(), + input_sizes: vec![32, 32], // Two 32-byte digests + iterations: if apply_minimums { + hmerge_count.max(10) + } else { + hmerge_count + }, + cycle_cost: 16, + }); + } + } + + // Memory operations + if instruction_mix.memory > MIN_MIX_RATIO { + let mem_count = (total_cycles as f64 * instruction_mix.memory / 10.0) as u64; + if mem_count > 0 { + operation_details.push(OperationDetails { + op_type: "load_store".to_string(), + input_sizes: vec![32], // Word-sized memory operations + iterations: if apply_minimums { mem_count.max(10) } else { mem_count }, + cycle_cost: 10, + }); + } + } + + // Arithmetic operations + if instruction_mix.arithmetic > MIN_MIX_RATIO { + let arith_count = (total_cycles as f64 * instruction_mix.arithmetic) as u64; + if arith_count > 0 { + operation_details.push(OperationDetails { + op_type: "arithmetic".to_string(), + input_sizes: vec![8], // Field element operations + iterations: if apply_minimums { + arith_count.max(10) + } else { + arith_count + }, + cycle_cost: 1, + }); + } + } + + // Control flow operations + if instruction_mix.control_flow > MIN_MIX_RATIO { + let control_count = (total_cycles as f64 * instruction_mix.control_flow / 5.0) as u64; + if control_count > 0 { + operation_details.push(OperationDetails { + op_type: "control_flow".to_string(), + input_sizes: vec![], + iterations: if apply_minimums { + control_count.max(10) + } else { + control_count + }, + cycle_cost: 5, + }); + } + } + let profile = VmProfile { profile_version: "1.0".to_string(), source: "miden-base/bin/bench-transaction".to_string(), @@ -218,6 +330,7 @@ pub fn write_vm_profile( phases, instruction_mix, key_procedures, + operation_details, }, }; @@ -227,3 +340,127 @@ pub fn write_vm_profile( println!("VM profile exported to: {}", path.display()); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Test that operation details are generated with correct hashing split + #[test] + fn operation_details_hashing_split_ratio() { + let total_cycles = 100000u64; + let instruction_mix = InstructionMix { + arithmetic: 0.05, + hashing: 0.45, + memory: 0.08, + control_flow: 0.05, + signature_verify: 0.37, + }; + + // Simulate the calculation from write_vm_profile + let hperm_count = (total_cycles as f64 * instruction_mix.hashing * 0.8) as u64; + let hmerge_count = (total_cycles as f64 * instruction_mix.hashing * 0.2 / 16.0) as u64; + + // hperm should get ~80% of hashing cycles + assert_eq!(hperm_count, 36000); + // hmerge should get ~20% of hashing cycles (divided by 16 for cycle cost) + assert_eq!(hmerge_count, 562); + + // Verify the ratio is maintained + let hperm_cycles = hperm_count; + let hmerge_cycles = hmerge_count * 16; + let total_hashing_cycles = hperm_cycles + hmerge_cycles; + + // Should be close to 45% of total cycles (allowing for truncation) + let hashing_ratio = total_hashing_cycles as f64 / total_cycles as f64; + assert!((hashing_ratio - 0.45).abs() < 0.01); + } + + /// Test that small workloads don't get minimums applied + #[test] + fn small_workload_no_minimum_inflation() { + let total_cycles = 5000u64; // Below MIN_CYCLES_FOR_MINIMUMS threshold + + // For small workloads, counts should be raw calculated values + let sig_count = (total_cycles as f64 * 0.37 / 59859.0) as u64; + let hperm_count = (total_cycles as f64 * 0.45 * 0.8) as u64; + + // These should be 0 or small, not inflated to minimums + assert_eq!(sig_count, 0); // Too small for a full sig verify + assert_eq!(hperm_count, 1800); // Raw calculation, not max(100, 1800) + } + + /// Test that large workloads get minimums applied + #[test] + fn large_workload_minimums_applied() { + let total_cycles = 50000u64; // Above MIN_CYCLES_FOR_MINIMUMS threshold + let apply_minimums = total_cycles >= 10000; + + assert!(apply_minimums); + + // With minimums, small counts get bumped up + let hmerge_count = (total_cycles as f64 * 0.45 * 0.2 / 16.0) as u64; + assert_eq!(hmerge_count, 281); // Raw calculation + + // With minimum applied + let hmerge_with_min = hmerge_count.max(10); + assert_eq!(hmerge_with_min, 281); // Already above minimum + + // For a very small count that would be below minimum + let tiny_count = 5u64; + let tiny_with_min = if apply_minimums { tiny_count.max(10) } else { tiny_count }; + assert_eq!(tiny_with_min, 10); + } + + /// Test MIN_MIX_RATIO threshold - operations below threshold excluded + #[test] + fn min_mix_ratio_threshold_excludes_small_ratios() { + let _total_cycles = 50000u64; + let min_mix_ratio = 0.001; // 0.1% + + // Very small ratio below threshold + let tiny_ratio = 0.0005; + assert!(tiny_ratio < min_mix_ratio); + + // Should be excluded from operation_details + let should_include = tiny_ratio > min_mix_ratio; + assert!(!should_include); + + // Ratio above threshold should be included + let normal_ratio = 0.05; + assert!(normal_ratio > min_mix_ratio); + } + + /// Test boundary at total_cycles == 10000 + #[test] + fn minimums_boundary_at_10000_cycles() { + // Just below threshold + let below_threshold = 9999u64; + let apply_minimums_below = below_threshold >= 10000; + assert!(!apply_minimums_below); + + // At threshold + let at_threshold = 10000u64; + let apply_minimums_at = at_threshold >= 10000; + assert!(apply_minimums_at); + + // Above threshold + let above_threshold = 10001u64; + let apply_minimums_above = above_threshold >= 10000; + assert!(apply_minimums_above); + } + + /// Test that zero-iteration operations are suppressed + #[test] + fn zero_iteration_operations_suppressed() { + let total_cycles = 1000u64; + + // Very small counts that truncate to 0 + let sig_count = (total_cycles as f64 * 0.37 / 59859.0) as u64; + assert_eq!(sig_count, 0); + + // Should not be included in operation_details + let should_include = sig_count > 0; + assert!(!should_include); + } +} diff --git a/bin/bench-transaction/src/vm_profile.rs b/bin/bench-transaction/src/vm_profile.rs index ca015590a9..f67c22d96e 100644 --- a/bin/bench-transaction/src/vm_profile.rs +++ b/bin/bench-transaction/src/vm_profile.rs @@ -20,6 +20,9 @@ pub struct TransactionKernelProfile { pub phases: HashMap, 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)] @@ -43,3 +46,195 @@ pub struct ProcedureProfile { 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, +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Test that old VM profiles without operation_details can be deserialized + /// This ensures backward compatibility with existing profiles + #[test] + fn deserialize_profile_without_operation_details() { + let json = r#"{ + "profile_version": "1.0", + "source": "test", + "timestamp": "2025-01-01T00:00:00Z", + "miden_vm_version": "0.20.0", + "transaction_kernel": { + "total_cycles": 1000, + "phases": {}, + "instruction_mix": { + "arithmetic": 0.2, + "hashing": 0.2, + "memory": 0.2, + "control_flow": 0.2, + "signature_verify": 0.2 + }, + "key_procedures": [] + } + }"#; + + let profile: VmProfile = + serde_json::from_str(json).expect("should deserialize old profile"); + assert!(profile.transaction_kernel.operation_details.is_empty()); + assert_eq!(profile.transaction_kernel.total_cycles, 1000); + } + + /// Test that profiles with operation_details deserialize correctly + #[test] + fn deserialize_profile_with_operation_details() { + let json = r#"{ + "profile_version": "1.0", + "source": "test", + "timestamp": "2025-01-01T00:00:00Z", + "miden_vm_version": "0.20.0", + "transaction_kernel": { + "total_cycles": 100000, + "phases": {}, + "instruction_mix": { + "arithmetic": 0.05, + "hashing": 0.45, + "memory": 0.08, + "control_flow": 0.05, + "signature_verify": 0.37 + }, + "key_procedures": [], + "operation_details": [ + { + "op_type": "falcon512_verify", + "input_sizes": [64, 32], + "iterations": 1, + "cycle_cost": 59859 + }, + { + "op_type": "hperm", + "input_sizes": [48], + "iterations": 1000, + "cycle_cost": 1 + } + ] + } + }"#; + + let profile: VmProfile = + serde_json::from_str(json).expect("should deserialize profile with details"); + assert_eq!(profile.transaction_kernel.operation_details.len(), 2); + assert_eq!(profile.transaction_kernel.operation_details[0].op_type, "falcon512_verify"); + assert_eq!(profile.transaction_kernel.operation_details[1].iterations, 1000); + } + + /// Calculate total cycles from operation details + fn calculate_operation_cycles(details: &[OperationDetails]) -> u64 { + details.iter().map(|d| d.iterations * d.cycle_cost).sum() + } + + /// Test that operation_details cycles are consistent with total_cycles + /// This validates that the operation breakdown roughly matches the total + #[test] + fn operation_details_consistent_with_total_cycles() { + // Profile with realistic operation mix + let profile = VmProfile { + profile_version: "1.0".to_string(), + source: "test".to_string(), + timestamp: "2025-01-01T00:00:00Z".to_string(), + miden_vm_version: "0.20.0".to_string(), + transaction_kernel: TransactionKernelProfile { + total_cycles: 73123, + phases: HashMap::new(), + instruction_mix: InstructionMix { + arithmetic: 0.05, + hashing: 0.45, + memory: 0.08, + control_flow: 0.05, + signature_verify: 0.37, + }, + key_procedures: vec![], + operation_details: vec![ + OperationDetails { + op_type: "falcon512_verify".to_string(), + input_sizes: vec![64, 32], + iterations: 1, + cycle_cost: 59859, + }, + OperationDetails { + op_type: "hperm".to_string(), + input_sizes: vec![48], + iterations: 10000, + cycle_cost: 1, + }, + ], + }, + }; + + let operation_cycles = + calculate_operation_cycles(&profile.transaction_kernel.operation_details); + // Operation cycles should be within reasonable range of total_cycles + // (allowing for overhead, other operations, and estimation errors) + assert!( + operation_cycles <= profile.transaction_kernel.total_cycles * 2, + "operation cycles ({}) should not exceed 2x total_cycles ({})", + operation_cycles, + profile.transaction_kernel.total_cycles + ); + + // For this profile, falcon512_verify dominates, so operation_cycles should be significant + assert!( + operation_cycles >= profile.transaction_kernel.total_cycles / 2, + "operation cycles ({}) should be at least 50% of total_cycles ({})", + operation_cycles, + profile.transaction_kernel.total_cycles + ); + } + + /// Test that tiny workloads don't get inflated by minimums + #[test] + fn tiny_workload_no_minimum_inflation() { + // Small profile with low operation counts + let profile = VmProfile { + profile_version: "1.0".to_string(), + source: "test".to_string(), + timestamp: "2025-01-01T00:00:00Z".to_string(), + miden_vm_version: "0.20.0".to_string(), + transaction_kernel: TransactionKernelProfile { + total_cycles: 100, // Very small workload + phases: HashMap::new(), + instruction_mix: InstructionMix { + arithmetic: 0.5, + hashing: 0.0, + memory: 0.0, + control_flow: 0.0, + signature_verify: 0.5, + }, + key_procedures: vec![], + operation_details: vec![ + // Without minimums, these should be small counts + OperationDetails { + op_type: "arithmetic".to_string(), + input_sizes: vec![8], + iterations: 50, // 50 * 1 = 50 cycles, not inflated to 10 + cycle_cost: 1, + }, + ], + }, + }; + + let operation_cycles = + calculate_operation_cycles(&profile.transaction_kernel.operation_details); + // For tiny workloads without minimums applied, iterations should be small + assert_eq!(operation_cycles, 50); + } +} From 4b487c9628bc930baf68d35a7b02da99f2dbf6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Mon, 2 Feb 2026 19:47:45 -0500 Subject: [PATCH 09/13] fix: adjust instruction handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adjusted the instruction-mix handling and minimum-iteration logic to avoid inflating low ratios, and added a write-path test to assert `write_vm_profile` emits real `operation_details`. Changes are localized to `bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs`. Changes: - Normalize instruction mix when the sum drifts beyond tolerance to keep cycle budgeting consistent. - Gate minimum iteration bumps so very small raw counts don’t get inflated. - Add a `write_vm_profile` serialization test to validate exported `operation_details`. --- .../src/cycle_counting_benchmarks/utils.rs | 120 ++++++++++++++---- 1 file changed, 95 insertions(+), 25 deletions(-) diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index 9708796de5..b18a259687 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -194,13 +194,28 @@ pub fn write_vm_profile( // Remaining cycles are distributed among other operations let remaining = 1.0 - signature_ratio; - let instruction_mix = InstructionMix { + let mut instruction_mix = InstructionMix { signature_verify: signature_ratio, hashing: remaining * 0.5, // Hashing is significant in remaining work memory: remaining * 0.2, // Memory operations control_flow: remaining * 0.2, // Loops, conditionals arithmetic: remaining * 0.1, // Basic arithmetic }; + const MIX_SUM_TOLERANCE: f64 = 0.001; + let mix_sum = instruction_mix.arithmetic + + instruction_mix.hashing + + instruction_mix.memory + + instruction_mix.control_flow + + instruction_mix.signature_verify; + if mix_sum > 0.0 && (mix_sum - 1.0).abs() > MIX_SUM_TOLERANCE { + instruction_mix = InstructionMix { + arithmetic: instruction_mix.arithmetic / mix_sum, + hashing: instruction_mix.hashing / mix_sum, + memory: instruction_mix.memory / mix_sum, + control_flow: instruction_mix.control_flow / mix_sum, + signature_verify: instruction_mix.signature_verify / mix_sum, + }; + } // Key procedures - auth is the heavyweight let key_procedures = vec![ProcedureProfile { @@ -218,6 +233,13 @@ pub fn write_vm_profile( // Threshold for applying minimum iteration counts (only for substantial workloads) const MIN_CYCLES_FOR_MINIMUMS: u64 = 10000; let apply_minimums = total_cycles >= MIN_CYCLES_FOR_MINIMUMS; + let apply_minimum = |raw: u64, minimum: u64| -> u64 { + if apply_minimums && raw >= minimum / 2 { + raw.max(minimum) + } else { + raw + } + }; // Signature verification operations // Only include if the calculated count is at least 1 (avoid inflating small workloads) @@ -248,11 +270,7 @@ pub fn write_vm_profile( operation_details.push(OperationDetails { op_type: "hperm".to_string(), input_sizes: vec![48], // 12 field elements state - iterations: if apply_minimums { - hperm_count.max(100) - } else { - hperm_count - }, + iterations: apply_minimum(hperm_count, 100), cycle_cost: 1, }); } @@ -263,11 +281,7 @@ pub fn write_vm_profile( operation_details.push(OperationDetails { op_type: "hmerge".to_string(), input_sizes: vec![32, 32], // Two 32-byte digests - iterations: if apply_minimums { - hmerge_count.max(10) - } else { - hmerge_count - }, + iterations: apply_minimum(hmerge_count, 10), cycle_cost: 16, }); } @@ -280,7 +294,7 @@ pub fn write_vm_profile( operation_details.push(OperationDetails { op_type: "load_store".to_string(), input_sizes: vec![32], // Word-sized memory operations - iterations: if apply_minimums { mem_count.max(10) } else { mem_count }, + iterations: apply_minimum(mem_count, 10), cycle_cost: 10, }); } @@ -293,11 +307,7 @@ pub fn write_vm_profile( operation_details.push(OperationDetails { op_type: "arithmetic".to_string(), input_sizes: vec![8], // Field element operations - iterations: if apply_minimums { - arith_count.max(10) - } else { - arith_count - }, + iterations: apply_minimum(arith_count, 10), cycle_cost: 1, }); } @@ -310,11 +320,7 @@ pub fn write_vm_profile( operation_details.push(OperationDetails { op_type: "control_flow".to_string(), input_sizes: vec![], - iterations: if apply_minimums { - control_count.max(10) - } else { - control_count - }, + iterations: apply_minimum(control_count, 10), cycle_cost: 5, }); } @@ -402,16 +408,41 @@ mod tests { let hmerge_count = (total_cycles as f64 * 0.45 * 0.2 / 16.0) as u64; assert_eq!(hmerge_count, 281); // Raw calculation - // With minimum applied - let hmerge_with_min = hmerge_count.max(10); + // With minimum applied (raw count already above half-minimum) + let hmerge_with_min = if apply_minimums && hmerge_count >= 5 { + hmerge_count.max(10) + } else { + hmerge_count + }; assert_eq!(hmerge_with_min, 281); // Already above minimum // For a very small count that would be below minimum let tiny_count = 5u64; - let tiny_with_min = if apply_minimums { tiny_count.max(10) } else { tiny_count }; + let tiny_with_min = if apply_minimums && tiny_count >= 5 { + tiny_count.max(10) + } else { + tiny_count + }; assert_eq!(tiny_with_min, 10); } + /// Test that minimums are skipped when raw count is far below minimum + #[test] + fn minimums_skipped_when_raw_far_below_minimum() { + let total_cycles = 10000u64; // Apply minimums + let apply_minimums = total_cycles >= 10000; + + let raw_count = 11u64; + let minimum = 100u64; + let adjusted = if apply_minimums && raw_count >= minimum / 2 { + raw_count.max(minimum) + } else { + raw_count + }; + + assert_eq!(adjusted, 11); + } + /// Test MIN_MIX_RATIO threshold - operations below threshold excluded #[test] fn min_mix_ratio_threshold_excludes_small_ratios() { @@ -463,4 +494,43 @@ mod tests { let should_include = sig_count > 0; assert!(!should_include); } + + /// Test that write_vm_profile emits operation_details in the exported profile + #[test] + fn write_vm_profile_emits_operation_details() { + let measurements = MeasurementsPrinter { + prologue: 1000, + notes_processing: 1000, + note_execution: BTreeMap::new(), + tx_script_processing: 1000, + epilogue: EpilogueMeasurements::from_parts(7000, 1000, 0), + }; + + let tx_benchmarks = vec![(ExecutionBenchmark::ConsumeSingleP2ID, measurements)]; + let mut path = std::env::temp_dir(); + path.push(format!("vm_profile_write_test_{}.json", std::process::id())); + + write_vm_profile(&path, &tx_benchmarks).expect("write vm profile"); + + let json = read_to_string(&path).expect("read vm profile"); + let profile: VmProfile = serde_json::from_str(&json).expect("deserialize vm profile"); + + assert!(!profile.transaction_kernel.operation_details.is_empty()); + assert!( + profile + .transaction_kernel + .operation_details + .iter() + .all(|detail| detail.iterations > 0) + ); + assert!( + profile + .transaction_kernel + .operation_details + .iter() + .all(|detail| detail.op_type != "falcon512_verify") + ); + + let _ = std::fs::remove_file(&path); + } } From 0071e1c54cacc9cca8025f2ea744b670f87de18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Mon, 2 Feb 2026 20:19:59 -0500 Subject: [PATCH 10/13] Fix vm profile mix and tests --- Cargo.lock | 1 + bin/bench-transaction/Cargo.toml | 1 + .../bench-tx-vm-profile.json | 2 +- .../src/cycle_counting_benchmarks/utils.rs | 124 +++++++++++------- bin/bench-transaction/src/vm_profile.rs | 6 +- 5 files changed, 86 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 483c998959..86cc357fb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,7 @@ dependencies = [ "miden-tx", "serde", "serde_json", + "tempfile", "tokio", ] diff --git a/bin/bench-transaction/Cargo.toml b/bin/bench-transaction/Cargo.toml index 6adbcae735..0aa28d224f 100644 --- a/bin/bench-transaction/Cargo.toml +++ b/bin/bench-transaction/Cargo.toml @@ -31,3 +31,4 @@ tokio = { features = ["macros", "rt"], workspace = true } [dev-dependencies] criterion = { features = ["async_tokio", "html_reports"], version = "0.6" } +tempfile = { version = "3.19" } diff --git a/bin/bench-transaction/bench-tx-vm-profile.json b/bin/bench-transaction/bench-tx-vm-profile.json index 48357f1d2d..c29427ad6f 100644 --- a/bin/bench-transaction/bench-tx-vm-profile.json +++ b/bin/bench-transaction/bench-tx-vm-profile.json @@ -1,5 +1,5 @@ { - "profile_version": "1.0", + "profile_version": "1.1", "source": "miden-base/bin/bench-transaction", "timestamp": "2026-02-02T10:13:46.584544+00:00", "miden_vm_version": "0.1.0", diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index b18a259687..d0ebeb8879 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -194,28 +194,14 @@ pub fn write_vm_profile( // Remaining cycles are distributed among other operations let remaining = 1.0 - signature_ratio; - let mut instruction_mix = InstructionMix { + let instruction_mix = InstructionMix { signature_verify: signature_ratio, hashing: remaining * 0.5, // Hashing is significant in remaining work memory: remaining * 0.2, // Memory operations control_flow: remaining * 0.2, // Loops, conditionals arithmetic: remaining * 0.1, // Basic arithmetic }; - const MIX_SUM_TOLERANCE: f64 = 0.001; - let mix_sum = instruction_mix.arithmetic - + instruction_mix.hashing - + instruction_mix.memory - + instruction_mix.control_flow - + instruction_mix.signature_verify; - if mix_sum > 0.0 && (mix_sum - 1.0).abs() > MIX_SUM_TOLERANCE { - instruction_mix = InstructionMix { - arithmetic: instruction_mix.arithmetic / mix_sum, - hashing: instruction_mix.hashing / mix_sum, - memory: instruction_mix.memory / mix_sum, - control_flow: instruction_mix.control_flow / mix_sum, - signature_verify: instruction_mix.signature_verify / mix_sum, - }; - } + let instruction_mix = normalize_instruction_mix(instruction_mix); // Key procedures - auth is the heavyweight let key_procedures = vec![ProcedureProfile { @@ -230,6 +216,7 @@ pub fn write_vm_profile( // Minimum threshold for including an operation type (avoid floating-point noise) const MIN_MIX_RATIO: f64 = 0.001; // 0.1% + const SIGNATURE_VERIFY_CYCLE_COST: u64 = 59859; // Threshold for applying minimum iteration counts (only for substantial workloads) const MIN_CYCLES_FOR_MINIMUMS: u64 = 10000; let apply_minimums = total_cycles >= MIN_CYCLES_FOR_MINIMUMS; @@ -243,19 +230,17 @@ pub fn write_vm_profile( // Signature verification operations // Only include if the calculated count is at least 1 (avoid inflating small workloads) - if signature_ratio > MIN_MIX_RATIO { - let sig_count = (total_cycles as f64 * signature_ratio / 59859.0) as u64; - // Only include signature verification if we have at least 1 full verification - // This avoids inflating operation_details with a 60K cycle op when the - // actual average auth cost is much smaller - if sig_count > 0 { - operation_details.push(OperationDetails { - op_type: "falcon512_verify".to_string(), - input_sizes: vec![64, 32], // PK commitment (64 bytes), message (32 bytes) - iterations: sig_count, - cycle_cost: 59859, - }); - } + let sig_count = signature_verify_count(total_cycles, &instruction_mix); + // Only include signature verification if we have at least 1 full verification + // This avoids inflating operation_details with a 60K cycle op when the + // actual average auth cost is much smaller + if sig_count > 0 { + operation_details.push(OperationDetails { + op_type: "falcon512_verify".to_string(), + input_sizes: vec![64, 32], // PK commitment (64 bytes), message (32 bytes) + iterations: sig_count, + cycle_cost: SIGNATURE_VERIFY_CYCLE_COST, + }); } // Hashing operations - split hashing ratio between hperm (80%) and hmerge (20%) @@ -263,7 +248,7 @@ pub fn write_vm_profile( const HPERM_HASHING_RATIO: f64 = 0.8; const HMERGE_HASHING_RATIO: f64 = 0.2; - if instruction_mix.hashing > MIN_MIX_RATIO { + if instruction_mix.hashing >= MIN_MIX_RATIO { let hperm_count = (total_cycles as f64 * instruction_mix.hashing * HPERM_HASHING_RATIO) as u64; if hperm_count > 0 { @@ -288,7 +273,7 @@ pub fn write_vm_profile( } // Memory operations - if instruction_mix.memory > MIN_MIX_RATIO { + if instruction_mix.memory >= MIN_MIX_RATIO { let mem_count = (total_cycles as f64 * instruction_mix.memory / 10.0) as u64; if mem_count > 0 { operation_details.push(OperationDetails { @@ -301,7 +286,7 @@ pub fn write_vm_profile( } // Arithmetic operations - if instruction_mix.arithmetic > MIN_MIX_RATIO { + if instruction_mix.arithmetic >= MIN_MIX_RATIO { let arith_count = (total_cycles as f64 * instruction_mix.arithmetic) as u64; if arith_count > 0 { operation_details.push(OperationDetails { @@ -314,7 +299,7 @@ pub fn write_vm_profile( } // Control flow operations - if instruction_mix.control_flow > MIN_MIX_RATIO { + if instruction_mix.control_flow >= MIN_MIX_RATIO { let control_count = (total_cycles as f64 * instruction_mix.control_flow / 5.0) as u64; if control_count > 0 { operation_details.push(OperationDetails { @@ -327,7 +312,7 @@ pub fn write_vm_profile( } let profile = VmProfile { - profile_version: "1.0".to_string(), + profile_version: "1.1".to_string(), source: "miden-base/bin/bench-transaction".to_string(), timestamp: chrono::Utc::now().to_rfc3339(), miden_vm_version: env!("CARGO_PKG_VERSION").to_string(), @@ -347,6 +332,38 @@ pub fn write_vm_profile( Ok(()) } +const MIX_SUM_TOLERANCE: f64 = 0.001; + +fn normalize_instruction_mix(instruction_mix: InstructionMix) -> InstructionMix { + let mix_sum = instruction_mix.arithmetic + + instruction_mix.hashing + + instruction_mix.memory + + instruction_mix.control_flow + + instruction_mix.signature_verify; + if mix_sum > 0.0 && (mix_sum - 1.0).abs() > MIX_SUM_TOLERANCE { + InstructionMix { + arithmetic: instruction_mix.arithmetic / mix_sum, + hashing: instruction_mix.hashing / mix_sum, + memory: instruction_mix.memory / mix_sum, + control_flow: instruction_mix.control_flow / mix_sum, + signature_verify: instruction_mix.signature_verify / mix_sum, + } + } else { + instruction_mix + } +} + +fn signature_verify_count(total_cycles: u64, instruction_mix: &InstructionMix) -> u64 { + const MIN_MIX_RATIO: f64 = 0.001; // 0.1% + const SIGNATURE_VERIFY_CYCLE_COST: u64 = 59859; + if instruction_mix.signature_verify >= MIN_MIX_RATIO { + (total_cycles as f64 * instruction_mix.signature_verify + / SIGNATURE_VERIFY_CYCLE_COST as f64) as u64 + } else { + 0 + } +} + #[cfg(test)] mod tests { use super::*; @@ -462,6 +479,33 @@ mod tests { assert!(normal_ratio > min_mix_ratio); } + /// Test that normalization updates signature operation counts + #[test] + fn instruction_mix_normalization_updates_signature_ratio() { + let unnormalized_mix = InstructionMix { + arithmetic: 0.05, + hashing: 0.4, + memory: 0.2, + control_flow: 0.05, + signature_verify: 0.6, + }; + + let normalized_mix = normalize_instruction_mix(unnormalized_mix.clone()); + let mix_sum = normalized_mix.arithmetic + + normalized_mix.hashing + + normalized_mix.memory + + normalized_mix.control_flow + + normalized_mix.signature_verify; + assert!((mix_sum - 1.0).abs() < MIX_SUM_TOLERANCE); + + let total_cycles = 100000u64; + let unnormalized_sig_count = signature_verify_count(total_cycles, &unnormalized_mix); + let normalized_sig_count = signature_verify_count(total_cycles, &normalized_mix); + + assert_eq!(unnormalized_sig_count, 1); + assert_eq!(normalized_sig_count, 0); + } + /// Test boundary at total_cycles == 10000 #[test] fn minimums_boundary_at_10000_cycles() { @@ -507,8 +551,8 @@ mod tests { }; let tx_benchmarks = vec![(ExecutionBenchmark::ConsumeSingleP2ID, measurements)]; - let mut path = std::env::temp_dir(); - path.push(format!("vm_profile_write_test_{}.json", std::process::id())); + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let path = temp_dir.path().join("vm_profile_write_test.json"); write_vm_profile(&path, &tx_benchmarks).expect("write vm profile"); @@ -523,14 +567,6 @@ mod tests { .iter() .all(|detail| detail.iterations > 0) ); - assert!( - profile - .transaction_kernel - .operation_details - .iter() - .all(|detail| detail.op_type != "falcon512_verify") - ); - let _ = std::fs::remove_file(&path); } } diff --git a/bin/bench-transaction/src/vm_profile.rs b/bin/bench-transaction/src/vm_profile.rs index f67c22d96e..580f9837be 100644 --- a/bin/bench-transaction/src/vm_profile.rs +++ b/bin/bench-transaction/src/vm_profile.rs @@ -98,7 +98,7 @@ mod tests { #[test] fn deserialize_profile_with_operation_details() { let json = r#"{ - "profile_version": "1.0", + "profile_version": "1.1", "source": "test", "timestamp": "2025-01-01T00:00:00Z", "miden_vm_version": "0.20.0", @@ -148,7 +148,7 @@ mod tests { fn operation_details_consistent_with_total_cycles() { // Profile with realistic operation mix let profile = VmProfile { - profile_version: "1.0".to_string(), + profile_version: "1.1".to_string(), source: "test".to_string(), timestamp: "2025-01-01T00:00:00Z".to_string(), miden_vm_version: "0.20.0".to_string(), @@ -205,7 +205,7 @@ mod tests { fn tiny_workload_no_minimum_inflation() { // Small profile with low operation counts let profile = VmProfile { - profile_version: "1.0".to_string(), + profile_version: "1.1".to_string(), source: "test".to_string(), timestamp: "2025-01-01T00:00:00Z".to_string(), miden_vm_version: "0.20.0".to_string(), From 2c626078e7d3b7b9c5b0e529afe2aa442d96493e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Mon, 2 Feb 2026 20:32:32 -0500 Subject: [PATCH 11/13] Add kernel profile extraction test --- .gitignore | 1 + .../bench-tx-vm-profile.json | 41 ------------------- .../src/cycle_counting_benchmarks/utils.rs | 31 +++++++++++++- bin/bench-transaction/src/vm_profile.rs | 6 +-- 4 files changed, 34 insertions(+), 45 deletions(-) delete mode 100644 bin/bench-transaction/bench-tx-vm-profile.json diff --git a/.gitignore b/.gitignore index f35eaefcd9..ba4ef6141a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ env/ node_modules/ *DS_Store *.iml +bin/bench-transaction/bench-tx-vm-profile.json diff --git a/bin/bench-transaction/bench-tx-vm-profile.json b/bin/bench-transaction/bench-tx-vm-profile.json deleted file mode 100644 index c29427ad6f..0000000000 --- a/bin/bench-transaction/bench-tx-vm-profile.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "profile_version": "1.1", - "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 - } - ] - } -} diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index d0ebeb8879..fa75123326 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -312,7 +312,7 @@ pub fn write_vm_profile( } let profile = VmProfile { - profile_version: "1.1".to_string(), + profile_version: "1.0".to_string(), source: "miden-base/bin/bench-transaction".to_string(), timestamp: chrono::Utc::now().to_rfc3339(), miden_vm_version: env!("CARGO_PKG_VERSION").to_string(), @@ -367,6 +367,7 @@ fn signature_verify_count(total_cycles: u64, instruction_mix: &InstructionMix) - #[cfg(test)] mod tests { use super::*; + use crate::context_setups::tx_consume_single_p2id_note; /// Test that operation details are generated with correct hashing split #[test] @@ -567,6 +568,34 @@ mod tests { .iter() .all(|detail| detail.iterations > 0) ); + } + + /// Test that ratios can be extracted from the real transaction kernel + #[tokio::test(flavor = "current_thread")] + async fn write_vm_profile_extracts_real_kernel_mix() { + let measurements = tx_consume_single_p2id_note() + .expect("build tx") + .execute() + .await + .map(miden_protocol::transaction::TransactionMeasurements::from) + .expect("execute tx"); + + let tx_benchmarks = + vec![(ExecutionBenchmark::ConsumeSingleP2ID, MeasurementsPrinter::from(measurements))]; + + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let path = temp_dir.path().join("vm_profile_real_kernel.json"); + write_vm_profile(&path, &tx_benchmarks).expect("write vm profile"); + let json = read_to_string(&path).expect("read vm profile"); + let profile: VmProfile = serde_json::from_str(&json).expect("deserialize vm profile"); + + let mix = &profile.transaction_kernel.instruction_mix; + let mix_sum = + mix.arithmetic + mix.hashing + mix.memory + mix.control_flow + mix.signature_verify; + assert!(mix_sum.is_finite()); + assert!((mix_sum - 1.0).abs() < MIX_SUM_TOLERANCE); + assert!(profile.transaction_kernel.total_cycles > 0); + assert!(!profile.transaction_kernel.operation_details.is_empty()); } } diff --git a/bin/bench-transaction/src/vm_profile.rs b/bin/bench-transaction/src/vm_profile.rs index 580f9837be..f67c22d96e 100644 --- a/bin/bench-transaction/src/vm_profile.rs +++ b/bin/bench-transaction/src/vm_profile.rs @@ -98,7 +98,7 @@ mod tests { #[test] fn deserialize_profile_with_operation_details() { let json = r#"{ - "profile_version": "1.1", + "profile_version": "1.0", "source": "test", "timestamp": "2025-01-01T00:00:00Z", "miden_vm_version": "0.20.0", @@ -148,7 +148,7 @@ mod tests { fn operation_details_consistent_with_total_cycles() { // Profile with realistic operation mix let profile = VmProfile { - profile_version: "1.1".to_string(), + profile_version: "1.0".to_string(), source: "test".to_string(), timestamp: "2025-01-01T00:00:00Z".to_string(), miden_vm_version: "0.20.0".to_string(), @@ -205,7 +205,7 @@ mod tests { fn tiny_workload_no_minimum_inflation() { // Small profile with low operation counts let profile = VmProfile { - profile_version: "1.1".to_string(), + profile_version: "1.0".to_string(), source: "test".to_string(), timestamp: "2025-01-01T00:00:00Z".to_string(), miden_vm_version: "0.20.0".to_string(), From 416d673cb258e8c01c8caa35dc0f80af13287433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 3 Feb 2026 07:29:29 -0500 Subject: [PATCH 12/13] Export trace lengths in VM profile --- .../src/cycle_counting_benchmarks/utils.rs | 4 ++++ bin/bench-transaction/src/vm_profile.rs | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index fa75123326..3036e29d9d 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -183,6 +183,8 @@ pub fn write_vm_profile( // Calculate total cycles let total_cycles = avg_prologue + avg_notes_processing + avg_tx_script + avg_epilogue; + let trace_main_len = total_cycles; + let trace_padded_len = total_cycles.next_power_of_two(); // Estimate instruction mix based on known characteristics // Auth procedure (signature verification) dominates at ~85% of epilogue @@ -318,6 +320,8 @@ pub fn write_vm_profile( miden_vm_version: env!("CARGO_PKG_VERSION").to_string(), transaction_kernel: TransactionKernelProfile { total_cycles, + trace_main_len: Some(trace_main_len), + trace_padded_len: Some(trace_padded_len), phases, instruction_mix, key_procedures, diff --git a/bin/bench-transaction/src/vm_profile.rs b/bin/bench-transaction/src/vm_profile.rs index f67c22d96e..b5cf347e91 100644 --- a/bin/bench-transaction/src/vm_profile.rs +++ b/bin/bench-transaction/src/vm_profile.rs @@ -17,6 +17,10 @@ pub struct VmProfile { #[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, pub phases: HashMap, pub instruction_mix: InstructionMix, pub key_procedures: Vec, @@ -154,6 +158,8 @@ mod tests { miden_vm_version: "0.20.0".to_string(), transaction_kernel: TransactionKernelProfile { total_cycles: 73123, + trace_main_len: None, + trace_padded_len: None, phases: HashMap::new(), instruction_mix: InstructionMix { arithmetic: 0.05, @@ -211,6 +217,8 @@ mod tests { miden_vm_version: "0.20.0".to_string(), transaction_kernel: TransactionKernelProfile { total_cycles: 100, // Very small workload + trace_main_len: None, + trace_padded_len: None, phases: HashMap::new(), instruction_mix: InstructionMix { arithmetic: 0.5, From d48ecf027754a44ddade433174a476fa5574beaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Mon, 9 Feb 2026 19:27:39 -0500 Subject: [PATCH 13/13] chore: Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42617cfbc6..c3dcbcf41a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - [BREAKING] Added `get_asset` and `get_initial_asset` kernel procedures and removed `get_balance`, `get_initial_balance` and `has_non_fungible_asset` kernel procedures ([#2369](https://github.com/0xMiden/miden-base/pull/2369)). - Introduced `TokenMetadata` type to encapsulate fungible faucet metadata ([#2344](https://github.com/0xMiden/miden-base/issues/2344)). - Added `StandardNote::from_script_root()` and `StandardNote::name()` methods, and exposed `NoteType` `PUBLIC`/`PRIVATE` masks as public constants ([#2411](https://github.com/0xMiden/miden-base/pull/2411)). +- Added VM profile export functionality to benchmark transaction execution, enabling detailed cycle counting and trace length analysis ([#2391](https://github.com/0xMiden/miden-base/pull/2391)). ### Changes