Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
plan and resolves/builds the program `.so` (`--program` / `--build` via
`cargo build-sbf`). Live Mollusk execution that produces real compute units is the
Linux-only follow-up (the SBF stack does not build on every host).
- **Mollusk turnkey execution (`MolluskBackend::from_plan`).** The Linux-only
`cu-profiler-mollusk` crate can now build an execution backend directly from a
validated `BenchPlan` — parsing each fixture's base58 addresses and hex data into
Solana `Instruction`/`Account` types and metering real compute units, no
hand-written harness. Validated by the SBF CI job.
- **`cu-profiler-bench` binary (Linux-only, one-command turnkey path).** A thin
`cu-profiler-bench --fixtures bench.toml --program-name <so>` entry point in the
detached crate ties `run_plan` to the report renderer — declarative plan in, real
metered report out — keeping the main `cu-profiler` CLI Solana-free.

## [0.1.2] - 2026-06-20

Expand Down
5 changes: 5 additions & 0 deletions integration/cu-profiler-mollusk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
53 changes: 53 additions & 0 deletions integration/cu-profiler-mollusk/src/bin/cu-profiler-bench.rs
Original file line number Diff line number Diff line change
@@ -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<String> = 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 <so-stem> (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<String> {
let pos = args.iter().position(|a| a == name)?;
args.get(pos + 1).cloned()
}
190 changes: 187 additions & 3 deletions integration/cu-profiler-mollusk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <budget>` line.
Expand Down Expand Up @@ -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<Self> {
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<u8>,
metas: Vec<AccountMeta>,
accounts: Vec<(Pubkey, Account)>,
}

impl PreparedInstruction {
fn from_fixture(fixture: &InstructionFixture) -> Result<Self> {
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<Report> {
let backend = MolluskBackend::from_plan(plan, program_name)?;
let scenarios: Vec<Scenario> = 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<Pubkey> {
s.parse::<Pubkey>()
.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<Vec<u8>> {
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 {
Expand Down Expand Up @@ -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::<u8>::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
);
}
}