diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9b9789..1046afa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -108,6 +108,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 generate_clients: name: Check Client Generation diff --git a/Cargo.lock b/Cargo.lock index cd8710f..f58f8fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -755,8 +755,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.5", ] @@ -1768,6 +1770,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" @@ -1863,6 +1878,16 @@ dependencies = [ "syn 2.0.68", ] +[[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" @@ -2693,6 +2718,7 @@ dependencies = [ "bincode", "bytemuck", "mollusk-svm", + "mollusk-svm-bencher", "serde", "solana-frozen-abi 2.0.1", "solana-frozen-abi-macro 2.0.1", diff --git a/package.json b/package.json index 92c7cfb..e7fd99a 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 36f3b73..a1a02c7 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -27,12 +27,17 @@ spl-program-error = "0.5.0" [dev-dependencies] mollusk-svm = { version = "0.0.5", features = ["fuzz"] } +mollusk-svm-bencher = "0.0.5" solana-sdk = "2.0.1" test-case = "3.3.1" [lib] crate-type = ["cdylib", "lib"] +[[bench]] +name = "compute_units" +harness = false + [lints.rust.unexpected_cfgs] level = "warn" check-cfg = [ diff --git a/program/benches/compute_units.md b/program/benches/compute_units.md new file mode 100644 index 0000000..0492cf4 --- /dev/null +++ b/program/benches/compute_units.md @@ -0,0 +1,26 @@ +#### Compute Units: 2024-10-22 17:54:37.721198 UTC + +| Name | CUs | Delta | +|------|------|-------| +| create_lookup_table | 10748 | - new - | +| freeze_lookup_table | 1510 | - new - | +| extend_lookup_table_from_0_to_1 | 6216 | - new - | +| extend_lookup_table_from_0_to_10 | 8880 | - new - | +| extend_lookup_table_from_0_to_38 | 17256 | - new - | +| extend_lookup_table_from_1_to_2 | 6216 | - new - | +| extend_lookup_table_from_1_to_10 | 8581 | - new - | +| extend_lookup_table_from_1_to_39 | 17256 | - new - | +| extend_lookup_table_from_5_to_6 | 6216 | - new - | +| extend_lookup_table_from_5_to_15 | 8881 | - new - | +| extend_lookup_table_from_5_to_43 | 17256 | - new - | +| extend_lookup_table_from_25_to_26 | 6219 | - new - | +| extend_lookup_table_from_25_to_35 | 8883 | - new - | +| extend_lookup_table_from_25_to_63 | 17259 | - new - | +| extend_lookup_table_from_50_to_88 | 17262 | - new - | +| extend_lookup_table_from_100_to_138 | 17268 | - new - | +| extend_lookup_table_from_150_to_188 | 17275 | - new - | +| extend_lookup_table_from_200_to_238 | 17281 | - new - | +| extend_lookup_table_from_255_to_256 | 6248 | - new - | +| deactivate_lookup_table | 3143 | - new - | +| close_lookup_table | 2220 | - new - | + diff --git a/program/benches/compute_units.rs b/program/benches/compute_units.rs new file mode 100644 index 0000000..b6040d7 --- /dev/null +++ b/program/benches/compute_units.rs @@ -0,0 +1,49 @@ +//! Address Lookup Table program compute unit benchmark testing. + +mod setup; + +use { + crate::setup::{ + close_lookup_table, create_lookup_table, deactivate_lookup_table, extend_lookup_table, + freeze_lookup_table, TEST_CLOCK_SLOT, + }, + mollusk_svm::Mollusk, + mollusk_svm_bencher::MolluskComputeUnitBencher, +}; + +fn main() { + std::env::set_var("SBF_OUT_DIR", "../target/deploy"); + + let mut mollusk = Mollusk::new( + &solana_address_lookup_table_program::id(), + "solana_address_lookup_table_program", + ); + + mollusk.warp_to_slot(TEST_CLOCK_SLOT); + + MolluskComputeUnitBencher::new(mollusk) + .bench(create_lookup_table().bench()) + .bench(freeze_lookup_table().bench()) + .bench(extend_lookup_table(0, 1).bench()) + .bench(extend_lookup_table(0, 10).bench()) + .bench(extend_lookup_table(0, 38).bench()) + .bench(extend_lookup_table(1, 2).bench()) + .bench(extend_lookup_table(1, 10).bench()) + .bench(extend_lookup_table(1, 39).bench()) + .bench(extend_lookup_table(5, 6).bench()) + .bench(extend_lookup_table(5, 15).bench()) + .bench(extend_lookup_table(5, 43).bench()) + .bench(extend_lookup_table(25, 26).bench()) + .bench(extend_lookup_table(25, 35).bench()) + .bench(extend_lookup_table(25, 63).bench()) + .bench(extend_lookup_table(50, 88).bench()) + .bench(extend_lookup_table(100, 138).bench()) + .bench(extend_lookup_table(150, 188).bench()) + .bench(extend_lookup_table(200, 238).bench()) + .bench(extend_lookup_table(255, 256).bench()) + .bench(deactivate_lookup_table().bench()) + .bench(close_lookup_table().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..e62adb2 --- /dev/null +++ b/program/benches/setup.rs @@ -0,0 +1,169 @@ +use { + mollusk_svm::program::keyed_account_for_system_program, + mollusk_svm_bencher::Bench, + solana_address_lookup_table_program::{ + instruction::{ + close_lookup_table as close_lookup_table_ix, + create_lookup_table as create_lookup_table_ix, + deactivate_lookup_table as deactivate_lookup_table_ix, + extend_lookup_table as extend_lookup_table_ix, + freeze_lookup_table as freeze_lookup_table_ix, + }, + state::{AddressLookupTable, LookupTableMeta}, + }, + solana_sdk::{ + account::AccountSharedData, instruction::Instruction, pubkey::Pubkey, rent::Rent, + system_program, + }, + std::borrow::Cow, +}; + +pub const TEST_CLOCK_SLOT: u64 = 100_000; + +/// 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) + } +} + +fn lookup_table_account( + authority: &Pubkey, + num_keys: usize, + deactivated: bool, +) -> AccountSharedData { + let state = { + let mut addresses = Vec::with_capacity(num_keys); + addresses.resize_with(num_keys, Pubkey::new_unique); + AddressLookupTable { + meta: LookupTableMeta { + authority: Some(*authority), + deactivation_slot: if deactivated { 1 } else { u64::MAX }, + ..LookupTableMeta::default() + }, + addresses: Cow::Owned(addresses), + } + }; + let data = state.serialize_for_tests().unwrap(); + let data_len = data.len(); + let lamports = Rent::default().minimum_balance(data_len); + let mut account = AccountSharedData::new( + lamports, + data_len, + &solana_address_lookup_table_program::id(), + ); + account.set_data_from_slice(&data); + account +} + +pub fn create_lookup_table() -> BenchContext { + let authority = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + + let (instruction, lookup_table) = create_lookup_table_ix(authority, payer, TEST_CLOCK_SLOT - 1); + + let accounts = vec![ + (lookup_table, AccountSharedData::default()), + (authority, AccountSharedData::default()), + ( + payer, + AccountSharedData::new(100_000_000_000, 0, &system_program::id()), + ), + keyed_account_for_system_program(), + ]; + + BenchContext { + label: "create_lookup_table".to_string(), + instruction, + accounts, + } +} + +pub fn extend_lookup_table(from: usize, to: usize) -> BenchContext { + let lookup_table = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + + let new_addresses = (from..to).map(|_| Pubkey::new_unique()).collect::>(); + + let instruction = extend_lookup_table_ix(lookup_table, authority, Some(payer), new_addresses); + + let accounts = vec![ + (lookup_table, lookup_table_account(&authority, from, false)), + (authority, AccountSharedData::default()), + ( + payer, + AccountSharedData::new(100_000_000_000, 0, &system_program::id()), + ), + keyed_account_for_system_program(), + ]; + + BenchContext { + label: format!("extend_lookup_table_from_{}_to_{}", from, to), + instruction, + accounts, + } +} + +pub fn freeze_lookup_table() -> BenchContext { + let lookup_table = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let instruction = freeze_lookup_table_ix(lookup_table, authority); + + let accounts = vec![ + (lookup_table, lookup_table_account(&authority, 1, false)), + (authority, AccountSharedData::default()), + ]; + + BenchContext { + label: "freeze_lookup_table".to_string(), + instruction, + accounts, + } +} + +pub fn deactivate_lookup_table() -> BenchContext { + let lookup_table = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let instruction = deactivate_lookup_table_ix(lookup_table, authority); + + let accounts = vec![ + (lookup_table, lookup_table_account(&authority, 1, false)), + (authority, AccountSharedData::default()), + ]; + + BenchContext { + label: "deactivate_lookup_table".to_string(), + instruction, + accounts, + } +} + +pub fn close_lookup_table() -> BenchContext { + let lookup_table = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let recipient = Pubkey::new_unique(); + + let instruction = close_lookup_table_ix(lookup_table, authority, recipient); + + let accounts = vec![ + (lookup_table, lookup_table_account(&authority, 1, true)), + (authority, AccountSharedData::default()), + (recipient, AccountSharedData::default()), + ]; + + BenchContext { + label: "close_lookup_table".to_string(), + instruction, + accounts, + } +} diff --git a/scripts/program/bench.mjs b/scripts/program/bench.mjs new file mode 100644 index 0000000..c937b6a --- /dev/null +++ b/scripts/program/bench.mjs @@ -0,0 +1,29 @@ +#!/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 = 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 7e33f14..a95a3a8 100644 --- a/scripts/program/build.mjs +++ b/scripts/program/build.mjs @@ -10,7 +10,11 @@ import { import './dump.mjs'; // Configure arguments here. -const buildArgs = ['--features', 'bpf-entrypoint', ...cliArguments()]; +const buildArgs = [ + '--features', + 'bpf-entrypoint', + ...cliArguments() +]; // Build the programs. for (const folder of getProgramFolders()) {