Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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
);
}
}