Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
32 changes: 32 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we need cargo bench-sbf 😅

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:
Expand Down
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
41 changes: 41 additions & 0 deletions program/benches/compute_units.md
Original file line number Diff line number Diff line change
@@ -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 - |

55 changes: 55 additions & 0 deletions program/benches/compute_units.rs
Original file line number Diff line number Diff line change
@@ -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();
}
148 changes: 148 additions & 0 deletions program/benches/setup.rs
Original file line number Diff line number Diff line change
@@ -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)
}
Comment on lines +25 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit for mollusk-svm-bencher: but it would be clearer to have Bench be a struct

}

/// 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()
}
Comment on lines +109 to +111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you, but this function is the same for all implementations, so you may as well define it in the trait

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this is from the ConfigState trait, which is required for the instruction helper store. I could just not use the helper, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha, this can stay as is, no worries!

}

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";
}
15 changes: 9 additions & 6 deletions program/tests/functional.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![cfg(feature = "test-sbf")]
// #![cfg(feature = "test-sbf")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this commented out?


use {
bincode::serialized_size,
Expand Down Expand Up @@ -43,13 +43,16 @@ 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();
bincode::serialized_size(&(ConfigKeys::default(), MyConfig::default()))
.ok()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: is the ok() needed? I thought you could just do and_then on the Result: https://doc.rust-lang.org/std/result/enum.Result.html#method.and_then

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only did it because checked_add returns an option, so it was one less unwrap(). I suppose it's all the same, though.

.and_then(|s| s.checked_add(total_keys_size as u64))
.unwrap() as usize
}

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()),
Expand All @@ -66,7 +69,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...
Expand Down Expand Up @@ -518,7 +521,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())
};

Expand Down
Loading