From 479ca5c6236ffcaa7649952a86aefc1c800ab284 Mon Sep 17 00:00:00 2001 From: intentos-dev Date: Sun, 21 Jun 2026 16:27:09 +0200 Subject: [PATCH] feat(mollusk): from_plan execution + cu-profiler-bench binary The detached cu-profiler-mollusk crate gains the executor the main CLI's `bench` delegates to: - `MolluskBackend::from_plan(plan, program_name)` parses a BenchPlan's base58/hex fixtures into solana Instruction/Account types and meters real compute units (fail-fast on malformed fixtures). - `run_plan(plan, program_name) -> Report` ties that through the core Profiler into a Mollusk-tagged report. - A thin Linux-only `cu-profiler-bench` binary (reads bench.toml, runs run_plan, renders the report). Kept a *separate* binary in the detached crate rather than a feature-gated CLI dependency, so the main CLI/core stay Solana-free and Windows- buildable (the project invariant). pure helpers (decode_hex, parse_pubkey) and an end-to-end demo test cover it. Verified by the Linux SBF CI job (the crate does not build on the local Windows gate). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 6 + integration/cu-profiler-mollusk/Cargo.toml | 5 + .../src/bin/cu-profiler-bench.rs | 53 +++++ integration/cu-profiler-mollusk/src/lib.rs | 190 +++++++++++++++++- 4 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 integration/cu-profiler-mollusk/src/bin/cu-profiler-bench.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index de1396f..5242a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,12 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `cu-profiler-bench` executor over `PATH`. The executor links the Solana stack and is a runtime sibling, not a build dependency, so the main CLI stays Solana-free; when it is absent, `bench` validates the plan and fails with the exact command to run. +- **Mollusk turnkey execution + `cu-profiler-bench` binary (Linux-only).** The + detached `cu-profiler-mollusk` crate gains `MolluskBackend::from_plan` (parses a + `BenchPlan`'s base58/hex fixtures into Solana `Instruction`/`Account` types and + meters real compute units) and `run_plan` (plan → metered `Report`), exposed as a + thin `cu-profiler-bench` binary that the main CLI's `bench` delegates to. Validated + by the SBF CI job. This is the executor that produces the real CU end to end. ## [0.1.2] - 2026-06-20 diff --git a/integration/cu-profiler-mollusk/Cargo.toml b/integration/cu-profiler-mollusk/Cargo.toml index 752b460..bfd27be 100644 --- a/integration/cu-profiler-mollusk/Cargo.toml +++ b/integration/cu-profiler-mollusk/Cargo.toml @@ -10,8 +10,13 @@ edition = "2021" license = "MIT OR Apache-2.0" description = "mollusk-svm execution backend for cu-profiler — real compute-unit metering of SBF programs" +[[bin]] +name = "cu-profiler-bench" +path = "src/bin/cu-profiler-bench.rs" + [dependencies] cu-profiler-core = { path = "../../crates/cu-profiler-core" } +cu-profiler-report = { path = "../../crates/cu-profiler-report" } mollusk-svm = "0.13" # Match mollusk-svm 0.13's exact majors: in solana-pubkey 4.x, `Pubkey` is an # alias of `solana_address::Address`, which is what mollusk's public API uses. diff --git a/integration/cu-profiler-mollusk/src/bin/cu-profiler-bench.rs b/integration/cu-profiler-mollusk/src/bin/cu-profiler-bench.rs new file mode 100644 index 0000000..40e068b --- /dev/null +++ b/integration/cu-profiler-mollusk/src/bin/cu-profiler-bench.rs @@ -0,0 +1,53 @@ +//! `cu-profiler-bench` — turnkey real-CU measurement from a declarative bench plan. +//! +//! Linux-only (it links the Solana/Mollusk stack, which does not build on Windows). +//! Reads a `bench.toml`, runs every instruction through Mollusk to meter real compute +//! units, and renders the report. This is the one-command path that keeps the main +//! `cu-profiler` CLI Solana-free. +//! +//! ```text +//! cu-profiler-bench --fixtures bench.toml --program-name my_program [--format table] +//! ``` +//! The program is loaded by name from `$SBF_OUT_DIR` (build it with `cargo build-sbf`). + +use std::process::ExitCode; + +use cu_profiler_core::bench::BenchPlan; +use cu_profiler_mollusk::run_plan; +use cu_profiler_report::{render, Format}; + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("cu-profiler-bench: {e}"); + ExitCode::FAILURE + } + } +} + +fn run() -> Result<(), String> { + let args: Vec = std::env::args().skip(1).collect(); + let fixtures = flag(&args, "--fixtures").unwrap_or_else(|| "bench.toml".to_string()); + let program_name = flag(&args, "--program-name").ok_or_else(|| { + "missing --program-name (the program built with `cargo build-sbf`)".to_string() + })?; + let format = flag(&args, "--format").unwrap_or_else(|| "table".to_string()); + + let text = + std::fs::read_to_string(&fixtures).map_err(|e| format!("cannot read `{fixtures}`: {e}"))?; + let plan = BenchPlan::from_toml(&text).map_err(|e| e.to_string())?; + let report = run_plan(&plan, &program_name).map_err(|e| e.to_string())?; + let fmt: Format = format + .parse() + .map_err(|e: cu_profiler_core::Error| e.to_string())?; + let rendered = render(&report, fmt).map_err(|e| e.to_string())?; + print!("{rendered}"); + Ok(()) +} + +/// The value following `name` in `args`, if present. +fn flag(args: &[String], name: &str) -> Option { + let pos = args.iter().position(|a| a == name)?; + args.get(pos + 1).cloned() +} diff --git a/integration/cu-profiler-mollusk/src/lib.rs b/integration/cu-profiler-mollusk/src/lib.rs index 7ac4fbc..a5b47d6 100644 --- a/integration/cu-profiler-mollusk/src/lib.rs +++ b/integration/cu-profiler-mollusk/src/lib.rs @@ -17,15 +17,17 @@ use std::collections::HashMap; -use cu_profiler_core::Result; use cu_profiler_core::backend::{ExecutionBackend, SimulationOutput}; +use cu_profiler_core::bench::{BenchPlan, InstructionFixture}; use cu_profiler_core::error::Error; -use cu_profiler_core::metadata::BackendKind; +use cu_profiler_core::metadata::{BackendKind, InstrumentationMode, RunMetadata}; +use cu_profiler_core::model::Report; use cu_profiler_core::scenario::Scenario; +use cu_profiler_core::{Profiler, Result}; use mollusk_svm::Mollusk; use solana_account::Account; -use solana_instruction::Instruction; +use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; /// Compute-unit budget used in the synthesized `consumed … of ` line. @@ -80,6 +82,136 @@ impl MolluskBackend { { self.setups.insert(scenario.into(), Box::new(setup)); } + + /// Build a backend from a validated [`BenchPlan`], loading the SBF program + /// `program_name` (the `.so` stem, located from `SBF_OUT_DIR` / `target/deploy`) + /// for every instruction. Each [`InstructionFixture`] becomes one scenario setup: + /// its program id, hex data, and accounts are parsed once here (so malformed + /// fixtures fail fast), then a fresh `Mollusk` harness + `Instruction` is built + /// per run. + /// + /// This is the turnkey real-CU path: a declarative `bench.toml` in, real metered + /// compute units out, with no hand-written harness. + /// + /// # Errors + /// Returns [`Error::Config`] for a non-base58 address or non-hex data in the plan. + pub fn from_plan(plan: &BenchPlan, program_name: &str) -> Result { + let mut backend = Self::new(); + for fixture in &plan.instructions { + let prepared = PreparedInstruction::from_fixture(fixture)?; + let name = program_name.to_string(); + backend.register(fixture.scenario.clone(), move || prepared.setup(&name)); + } + Ok(backend) + } +} + +/// A [`InstructionFixture`] parsed into ready-to-run Solana types, so parsing +/// happens once (with error handling) rather than per run inside the setup closure. +#[derive(Clone)] +struct PreparedInstruction { + program_id: Pubkey, + data: Vec, + metas: Vec, + accounts: Vec<(Pubkey, Account)>, +} + +impl PreparedInstruction { + fn from_fixture(fixture: &InstructionFixture) -> Result { + let program_id = parse_pubkey(&fixture.program_id, "program_id")?; + let data = decode_hex(&fixture.data, "instruction data")?; + + let mut metas = Vec::with_capacity(fixture.accounts.len()); + let mut accounts = Vec::with_capacity(fixture.accounts.len()); + for acc in &fixture.accounts { + let pubkey = parse_pubkey(&acc.pubkey, "account pubkey")?; + metas.push(AccountMeta { + pubkey, + is_signer: acc.signer, + is_writable: acc.writable, + }); + let owner = match &acc.owner { + Some(o) => parse_pubkey(o, "account owner")?, + None => Pubkey::default(), + }; + let account_data = match &acc.data { + Some(d) => decode_hex(d, "account data")?, + None => Vec::new(), + }; + accounts.push(( + pubkey, + Account { + lamports: acc.lamports, + data: account_data, + owner, + executable: false, + rent_epoch: 0, + }, + )); + } + Ok(Self { + program_id, + data, + metas, + accounts, + }) + } + + fn setup(&self, program_name: &str) -> ScenarioSetup { + let mollusk = Mollusk::new(&self.program_id, program_name); + let instruction = + Instruction::new_with_bytes(self.program_id, &self.data, self.metas.clone()); + ScenarioSetup { + mollusk, + instruction, + accounts: self.accounts.clone(), + } + } +} + +/// Run a whole [`BenchPlan`] end-to-end and assemble a [`Report`]: build a +/// [`MolluskBackend`] from the plan (loading `program_name`), profile one scenario +/// per instruction, and meter real compute units. This is the one-call turnkey API +/// behind the `cu-profiler-bench` binary. +/// +/// # Errors +/// Returns [`Error::Config`] if the plan is malformed (bad address or hex). +pub fn run_plan(plan: &BenchPlan, program_name: &str) -> Result { + let backend = MolluskBackend::from_plan(plan, program_name)?; + let scenarios: Vec = plan + .instructions + .iter() + .map(|ix| Scenario::new(&ix.scenario)) + .collect(); + let metadata = RunMetadata { + profiler_version: cu_profiler_core::VERSION.to_string(), + backend: BackendKind::Mollusk, + instrumentation: InstrumentationMode::Off, + git_commit: None, + solana_versions: Vec::new(), + generated_at: None, + }; + Ok(Profiler::new().run(&backend, &scenarios, None, metadata)) +} + +/// Parse a base58 Solana address, mapping failure to a clear config error. +fn parse_pubkey(s: &str, what: &str) -> Result { + s.parse::() + .map_err(|e| Error::Config(format!("{what} `{s}` is not a valid address: {e}"))) +} + +/// Decode a hex string into bytes (empty string → empty vec). +fn decode_hex(s: &str, what: &str) -> Result> { + if s.len() % 2 != 0 { + return Err(Error::Config(format!("{what}: hex has odd length"))); + } + (0..s.len()) + .step_by(2) + .map(|i| { + u8::from_str_radix(&s[i..i + 2], 16) + .map_err(|e| Error::Config(format!("{what}: invalid hex: {e}"))) + }) + .collect() } impl ExecutionBackend for MolluskBackend { @@ -157,4 +289,56 @@ mod tests { let err = backend.run(&Scenario::new("missing")).unwrap_err(); assert!(err.to_string().contains("no mollusk setup")); } + + #[test] + fn decode_hex_roundtrips_and_rejects_bad_input() { + assert_eq!(decode_hex("", "x").unwrap(), Vec::::new()); + assert_eq!(decode_hex("01ab", "x").unwrap(), vec![0x01, 0xab]); + assert!(decode_hex("abc", "x").is_err()); // odd length + assert!(decode_hex("zz", "x").is_err()); // non-hex + } + + #[test] + fn from_plan_parses_a_fixture_into_a_registered_setup() { + // Build a plan whose program id is a real (valid base58) pubkey, but do not + // run it — this exercises the parse/convert path without needing the .so. + let program_id = Pubkey::new_unique(); + let toml = format!( + "[[instruction]]\nscenario=\"swap\"\nprogram_id=\"{program_id}\"\ndata=\"01ff\"\n" + ); + let plan = BenchPlan::from_toml(&toml).expect("valid plan"); + let backend = + MolluskBackend::from_plan(&plan, "cu_profiler_demo_program").expect("plan converts"); + assert!(backend.setups.contains_key("swap")); + } + + #[test] + fn run_plan_meters_the_demo_into_a_report() { + let program_id = Pubkey::new_unique(); + let toml = format!("[[instruction]]\nscenario=\"demo\"\nprogram_id=\"{program_id}\"\n"); + let plan = BenchPlan::from_toml(&toml).expect("valid plan"); + let report = run_plan(&plan, "cu_profiler_demo_program").expect("plan runs"); + assert_eq!(report.scenarios.len(), 1); + assert_eq!(report.metadata.backend, BackendKind::Mollusk); + assert!(report.scenarios[0].measurement.total_cu > 0); + } + + #[test] + fn from_plan_runs_the_demo_and_meters_real_cu() { + // End-to-end: a declarative plan, loaded against the demo .so, yields real CU. + let program_id = Pubkey::new_unique(); + let toml = format!("[[instruction]]\nscenario=\"demo\"\nprogram_id=\"{program_id}\"\n"); + let plan = BenchPlan::from_toml(&toml).expect("valid plan"); + let backend = + MolluskBackend::from_plan(&plan, "cu_profiler_demo_program").expect("plan converts"); + + let out = backend.run(&Scenario::new("demo")).expect("scenario runs"); + let analysis = analyze(&out.logs, &ProgramRegistry::with_builtins()); + assert!(out.success, "demo should succeed: {:?}", out.logs); + assert!( + analysis.total_cu > 0, + "expected real metered CU: {:?}", + out.logs + ); + } }