diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d82abc3..89c4009 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -109,6 +109,38 @@ jobs: - name: Test Programs run: pnpm programs:test + bench_program_compute_units: + name: Benchmark Program Compute Units + runs-on: ubuntu-latest + needs: build_programs # Cargo Bench won't build the SBPF binary... + steps: + - name: Git Checkout + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup + with: + cargo-cache-key: cargo-program-benches + cargo-cache-fallback-key: cargo-programs + solana: true + + - name: Restore Program Builds + uses: actions/cache/restore@v4 + with: + path: ./**/*.so + key: ${{ runner.os }}-builds-${{ github.sha }} + + - name: Benchmark Compute Units + run: pnpm programs:bench + + - name: Check Working Directory + run: | + if [ -n "$(git status --porcelain)" ]; then + test -z "$(git status --porcelain)" + echo "CU usage has changed. Please run `cargo bench` and commit the new results."; + exit 1; + fi + ## SKIP: IDL is hand-cranked here for now. ## # generate_idls: diff --git a/Cargo.lock b/Cargo.lock index e1cf37a..0af8e60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2185,6 +2185,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "mollusk-svm-bencher" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19edc6403e493718d693b5faa0b6bcc2aebdd4947dc16c144929dc3d7c5c0f24" +dependencies = [ + "chrono", + "mollusk-svm", + "num-format", + "serde_json", + "solana-sdk", +] + [[package]] name = "mollusk-svm-fuzz-fixture" version = "0.0.5" @@ -2309,6 +2322,16 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -3818,6 +3841,7 @@ version = "0.1.0" dependencies = [ "bincode", "mollusk-svm", + "mollusk-svm-bencher", "serde", "solana-program", "solana-sdk", diff --git a/package.json b/package.json index af6f9dd..3b97bd8 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "scripts": { "programs:build": "zx ./scripts/program/build.mjs", "programs:test": "zx ./scripts/program/test.mjs", + "programs:bench": "zx ./scripts/program/bench.mjs", "programs:clean": "zx ./scripts/program/clean.mjs", "programs:format": "zx ./scripts/program/format.mjs", "programs:lint": "zx ./scripts/program/lint.mjs", diff --git a/program/Cargo.toml b/program/Cargo.toml index 36938e0..afd6725 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -22,7 +22,12 @@ solana-program = "2.0.1" [dev-dependencies] mollusk-svm = { version = "0.0.5", features = ["fuzz"] } +mollusk-svm-bencher = "0.0.5" solana-sdk = "2.0.1" [lib] crate-type = ["cdylib", "lib"] + +[[bench]] +name = "compute_units" +harness = false diff --git a/program/benches/compute_units.md b/program/benches/compute_units.md new file mode 100644 index 0000000..5ffd045 --- /dev/null +++ b/program/benches/compute_units.md @@ -0,0 +1,41 @@ +#### Compute Units: 2024-10-23 12:46:27.264402 UTC + +| Name | CUs | Delta | +|------|------|-------| +| config_small_init_0_keys | 581 | - new - | +| config_small_init_1_keys | 1204 | - new - | +| config_small_init_5_keys | 2799 | - new - | +| config_small_init_10_keys | 4839 | - new - | +| config_small_init_25_keys | 11591 | - new - | +| config_small_init_37_keys | 16522 | - new - | +| config_small_store_0_keys | 581 | - new - | +| config_small_store_1_keys | 1458 | - new - | +| config_small_store_5_keys | 3969 | - new - | +| config_small_store_10_keys | 7154 | - new - | +| config_small_store_25_keys | 17341 | - new - | +| config_small_store_37_keys | 25020 | - new - | +| config_medium_init_0_keys | 572 | - new - | +| config_medium_init_1_keys | 1151 | - new - | +| config_medium_init_5_keys | 2799 | - new - | +| config_medium_init_10_keys | 4839 | - new - | +| config_medium_init_25_keys | 11591 | - new - | +| config_medium_init_37_keys | 16522 | - new - | +| config_medium_store_0_keys | 572 | - new - | +| config_medium_store_1_keys | 1405 | - new - | +| config_medium_store_5_keys | 3969 | - new - | +| config_medium_store_10_keys | 7154 | - new - | +| config_medium_store_25_keys | 17341 | - new - | +| config_medium_store_37_keys | 25020 | - new - | +| config_large_init_0_keys | 693 | - new - | +| config_large_init_1_keys | 1272 | - new - | +| config_large_init_5_keys | 2920 | - new - | +| config_large_init_10_keys | 4961 | - new - | +| config_large_init_25_keys | 11715 | - new - | +| config_large_init_37_keys | 16647 | - new - | +| config_large_store_0_keys | 693 | - new - | +| config_large_store_1_keys | 1526 | - new - | +| config_large_store_5_keys | 4090 | - new - | +| config_large_store_10_keys | 7276 | - new - | +| config_large_store_25_keys | 17465 | - new - | +| config_large_store_37_keys | 25145 | - new - | + diff --git a/program/benches/compute_units.rs b/program/benches/compute_units.rs new file mode 100644 index 0000000..f1f9ed4 --- /dev/null +++ b/program/benches/compute_units.rs @@ -0,0 +1,55 @@ +//! Compute unit benchmark testing. + +mod setup; + +use { + crate::setup::{BenchSetup, ConfigLarge, ConfigMedium, ConfigSmall}, + mollusk_svm::Mollusk, + mollusk_svm_bencher::MolluskComputeUnitBencher, +}; + +fn main() { + std::env::set_var("SBF_OUT_DIR", "../target/deploy"); + let mollusk = Mollusk::new(&solana_config_program::id(), "solana_config_program"); + + MolluskComputeUnitBencher::new(mollusk) + .bench(ConfigSmall::init(0).bench()) + .bench(ConfigSmall::init(1).bench()) + .bench(ConfigSmall::init(5).bench()) + .bench(ConfigSmall::init(10).bench()) + .bench(ConfigSmall::init(25).bench()) + .bench(ConfigSmall::init(37).bench()) + .bench(ConfigSmall::store(0).bench()) + .bench(ConfigSmall::store(1).bench()) + .bench(ConfigSmall::store(5).bench()) + .bench(ConfigSmall::store(10).bench()) + .bench(ConfigSmall::store(25).bench()) + .bench(ConfigSmall::store(37).bench()) + .bench(ConfigMedium::init(0).bench()) + .bench(ConfigMedium::init(1).bench()) + .bench(ConfigMedium::init(5).bench()) + .bench(ConfigMedium::init(10).bench()) + .bench(ConfigMedium::init(25).bench()) + .bench(ConfigMedium::init(37).bench()) + .bench(ConfigMedium::store(0).bench()) + .bench(ConfigMedium::store(1).bench()) + .bench(ConfigMedium::store(5).bench()) + .bench(ConfigMedium::store(10).bench()) + .bench(ConfigMedium::store(25).bench()) + .bench(ConfigMedium::store(37).bench()) + .bench(ConfigLarge::init(0).bench()) + .bench(ConfigLarge::init(1).bench()) + .bench(ConfigLarge::init(5).bench()) + .bench(ConfigLarge::init(10).bench()) + .bench(ConfigLarge::init(25).bench()) + .bench(ConfigLarge::init(37).bench()) + .bench(ConfigLarge::store(0).bench()) + .bench(ConfigLarge::store(1).bench()) + .bench(ConfigLarge::store(5).bench()) + .bench(ConfigLarge::store(10).bench()) + .bench(ConfigLarge::store(25).bench()) + .bench(ConfigLarge::store(37).bench()) + .must_pass(true) + .out_dir("./benches") + .execute(); +} diff --git a/program/benches/setup.rs b/program/benches/setup.rs new file mode 100644 index 0000000..d334e2e --- /dev/null +++ b/program/benches/setup.rs @@ -0,0 +1,148 @@ +use { + mollusk_svm_bencher::Bench, + serde::Serialize, + solana_config_program::{ + instruction::store, + state::{ConfigKeys, ConfigState}, + }, + solana_sdk::{ + account::AccountSharedData, + hash::Hash, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + rent::Rent, + }, +}; + +/// Helper struct to convert to a `Bench`. +pub struct BenchContext { + label: String, + instruction: Instruction, + accounts: Vec<(Pubkey, AccountSharedData)>, +} + +impl BenchContext { + /// Convert to a `Bench`. + pub fn bench(&self) -> Bench { + (self.label.as_str(), &self.instruction, &self.accounts) + } +} + +/// Trait to avoid re-defining the same instruction and account constructors +/// for each `ConfigState`. +pub trait BenchSetup: ConfigState + Default { + const BENCH_ID: &'static str; + + fn default_account_state(keys: Vec<(Pubkey, bool)>) -> (ConfigKeys, Self) { + (ConfigKeys { keys }, Self::default()) + } + + fn default_space(keys: Vec<(Pubkey, bool)>) -> usize { + (Self::max_space() + .checked_add(ConfigKeys::serialized_size(keys)) + .unwrap()) as usize + } + + fn keys(keys_len: usize) -> Vec<(Pubkey, bool)> { + (0..keys_len) + .map(|_| (Pubkey::new_unique(), false)) + .collect() + } + + fn init(keys_len: usize) -> BenchContext { + let config_pubkey = Pubkey::new_unique(); + let keys = Self::keys(keys_len); + let space = Self::default_space(keys.clone()); + let lamports = Rent::default().minimum_balance(space); + + let instruction = { + let account_metas = vec![AccountMeta::new(config_pubkey, true)]; + let account_data = Self::default_account_state(keys); + Instruction::new_with_bincode(solana_config_program::id(), &account_data, account_metas) + }; + + let accounts = vec![( + config_pubkey, + AccountSharedData::new(lamports, space, &solana_config_program::id()), + )]; + + BenchContext { + label: format!("{}_init_{}_keys", Self::BENCH_ID, keys_len), + instruction, + accounts, + } + } + + fn store(keys_len: usize) -> BenchContext { + let config_pubkey = Pubkey::new_unique(); + let keys = Self::keys(keys_len); + let space = Self::default_space(keys.clone()); + let lamports = Rent::default().minimum_balance(space); + + let instruction = store(&config_pubkey, true, keys.clone(), &Self::default()); + + let accounts = vec![( + config_pubkey, + AccountSharedData::new_data( + lamports, + &Self::default_account_state(keys), + &solana_config_program::id(), + ) + .unwrap(), + )]; + + BenchContext { + label: format!("{}_store_{}_keys", Self::BENCH_ID, keys_len), + instruction, + accounts, + } + } +} + +/// A small config, which just stores 8 bytes. +#[derive(Debug, Default, PartialEq, Serialize)] +pub struct ConfigSmall { + pub item: u64, +} + +impl ConfigState for ConfigSmall { + fn max_space() -> u64 { + bincode::serialized_size(&Self::default()).unwrap() + } +} + +impl BenchSetup for ConfigSmall { + const BENCH_ID: &'static str = "config_small"; +} + +/// A medium config, which stores 1024 bytes. +#[derive(Debug, Default, PartialEq, Serialize)] +pub struct ConfigMedium { + pub hashes: [Hash; 32], // 32 x 32 = 1024 bytes +} + +impl ConfigState for ConfigMedium { + fn max_space() -> u64 { + bincode::serialized_size(&Self::default()).unwrap() + } +} + +impl BenchSetup for ConfigMedium { + const BENCH_ID: &'static str = "config_medium"; +} + +/// A large config, which stores 32_768 bytes. +#[derive(Debug, Default, PartialEq, Serialize)] +pub struct ConfigLarge { + pub hashes: [[Hash; 32]; 32], // 32 x 32 x 32 = 32_768 bytes +} + +impl ConfigState for ConfigLarge { + fn max_space() -> u64 { + bincode::serialized_size(&Self::default()).unwrap() + } +} + +impl BenchSetup for ConfigLarge { + const BENCH_ID: &'static str = "config_large"; +} diff --git a/program/tests/functional.rs b/program/tests/functional.rs index 2f9dbd9..2cd5d85 100644 --- a/program/tests/functional.rs +++ b/program/tests/functional.rs @@ -43,13 +43,15 @@ fn setup() -> Mollusk { fn get_config_space(key_len: usize) -> usize { let entry_size = bincode::serialized_size(&(Pubkey::default(), true)).unwrap() as usize; - bincode::serialized_size(&(ConfigKeys::default(), MyConfig::default())).unwrap() as usize - + key_len * entry_size + let total_keys_size = (key_len).checked_mul(entry_size).unwrap(); + let serialized_size = + bincode::serialized_size(&(ConfigKeys::default(), MyConfig::default())).unwrap() as usize; + serialized_size.checked_add(total_keys_size).unwrap() } fn create_config_account(mollusk: &Mollusk, keys: Vec<(Pubkey, bool)>) -> AccountSharedData { let space = get_config_space(keys.len()); - let lamports = mollusk.sysvars.rent.minimum_balance(space as usize); + let lamports = mollusk.sysvars.rent.minimum_balance(space); AccountSharedData::new_data( lamports, &(ConfigKeys { keys }, MyConfig::default()), @@ -66,7 +68,7 @@ fn test_process_create_ok() { let config_account = { let space = get_config_space(0); let lamports = mollusk.sysvars.rent.minimum_balance(space); - AccountSharedData::new(lamports, space as usize, &solana_config_program::id()) + AccountSharedData::new(lamports, space, &solana_config_program::id()) }; // `instruction::initialize_account` without making it public... @@ -518,7 +520,7 @@ fn test_config_bad_owner() { // Store a config account with the wrong owner. let config_account = { let space = get_config_space(keys.len()); - let lamports = mollusk.sysvars.rent.minimum_balance(space as usize); + let lamports = mollusk.sysvars.rent.minimum_balance(space); AccountSharedData::new(lamports, 0, &Pubkey::new_unique()) }; diff --git a/scripts/program/bench.mjs b/scripts/program/bench.mjs new file mode 100644 index 0000000..79d7d35 --- /dev/null +++ b/scripts/program/bench.mjs @@ -0,0 +1,33 @@ +#!/usr/bin/env zx +import 'zx/globals'; +import { + cliArguments, + getProgramFolders, + workingDirectory, +} from '../utils.mjs'; + +// Save external programs binaries to the output directory. +import './dump.mjs'; + +// Configure additional arguments here, e.g.: +// ['--arg1', '--arg2', ...cliArguments()] +const benchArgs = [ + '--features', + 'bpf-entrypoint', + ...cliArguments(), +]; + +const hasSolfmt = await which('solfmt', { nothrow: true }); + +// Test the programs. +await Promise.all( + getProgramFolders().map(async (folder) => { + const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); + + if (hasSolfmt) { + await $`RUST_LOG=error cargo bench --manifest-path ${manifestPath} ${benchArgs} 2>&1 | solfmt`; + } else { + await $`RUST_LOG=error cargo bench --manifest-path ${manifestPath} ${benchArgs}`; + } + }) +); diff --git a/scripts/program/build.mjs b/scripts/program/build.mjs index df63a47..94e6854 100644 --- a/scripts/program/build.mjs +++ b/scripts/program/build.mjs @@ -11,7 +11,11 @@ import './dump.mjs'; // Configure additional arguments here, e.g.: // ['--arg1', '--arg2', ...cliArguments()] -const buildArgs = cliArguments(); +const buildArgs = [ + '--features', + 'bpf-entrypoint', + ...cliArguments(), +]; // Build the programs. await Promise.all( diff --git a/scripts/program/lint.mjs b/scripts/program/lint.mjs index 2befcf3..c04053f 100644 --- a/scripts/program/lint.mjs +++ b/scripts/program/lint.mjs @@ -12,6 +12,7 @@ import { // ['--arg1', '--arg2', ...cliArguments()] const lintArgs = [ '-Zunstable-options', + '--all-targets', '--features', 'bpf-entrypoint,test-sbf', '--',