Add benchmarking for compute units usage (#2466)

This commit is contained in:
acheron 2023-04-24 10:33:11 +02:00 committed by GitHub
parent 2e89b79c51
commit 5910dd3e2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2575 additions and 7 deletions

View File

@ -437,6 +437,8 @@ jobs:
path: tests/anchor-cli-idl
- cmd: cd tests/anchor-cli-account && anchor test --skip-lint
path: tests/anchor-cli-account
- cmd: cd tests/bench && anchor test --skip-lint
path: tests/bench
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup/

View File

@ -15,12 +15,15 @@ The minor version will be incremented upon a breaking change and the patch versi
- spl: Add metadata wrappers `approve_collection_authority`, `bubblegum_set_collection_size`, `burn_edition_nft`, `burn_nft`, `revoke_collection_authority`, `set_token_standard`, `utilize`, `unverify_sized_collection_item`, `unverify_collection` ([#2430](https://github.com/coral-xyz/anchor/pull/2430))
- spl: Add `token_program` constraint to `Token`, `Mint`, and `AssociatedToken` accounts in order to override required `token_program` fields and use different token interface implementations in the same instruction ([#2460](https://github.com/coral-xyz/anchor/pull/2460))
- cli: Add support for Solidity programs. `anchor init` and `anchor new` take an option `--solidity` which creates solidity code rather than rust. `anchor build` and `anchor test` work accordingly ([#2421](https://github.com/coral-xyz/anchor/pull/2421))
- bench: Add benchmarking for compute units usage ([#2466](https://github.com/coral-xyz/anchor/pull/2466))
### Fixes
- ts: Narrowed `AccountClient` type to it's appropriate account type ([#2440](https://github.com/coral-xyz/anchor/pull/2440))
- lang: Fix inability to use identifiers `program_id`, `accounts`, `ix_data`, `remaining_accounts` in instruction arguments ([#2464](https://github.com/coral-xyz/anchor/pull/2464))
### Breaking
- lang: Identifiers that are intended for internal usage(`program_id`, `accounts`, `ix_data`, `remaining_accounts`) have been renamed with `__` prefix ([#2464](https://github.com/coral-xyz/anchor/pull/2464))
## [0.27.0] - 2023-03-08

200
bench/COMPUTE_UNITS.md Normal file
View File

@ -0,0 +1,200 @@
# Compute Units
All notable changes in compute units usage will be documented in this file.
The changes are calculated by comparing the current results with the last version's results. Increase in usage is shown with 🔴 and decrease is shown with 🟢.
The programs and their tests are located in [/tests/bench](https://github.com/coral-xyz/anchor/tree/master/tests/bench).
> **Note**
> The results documented in this file are autogenerated. Running the tests will update the current results when necessary, manually editing the results should be avoided.
## [Unreleased]
| Instruction | Compute Units | +/- |
| --------------------------- | ------------- | --- |
| accountInfo1 | 954 | - |
| accountInfo2 | 1567 | - |
| accountInfo4 | 2059 | - |
| accountInfo8 | 3856 | - |
| accountEmptyInit1 | 5958 | - |
| accountEmpty1 | 1090 | - |
| accountEmptyInit2 | 10583 | - |
| accountEmpty2 | 1852 | - |
| accountEmptyInit4 | 19557 | - |
| accountEmpty4 | 2646 | - |
| accountEmptyInit8 | 37541 | - |
| accountEmpty8 | 5043 | - |
| accountSizedInit1 | 6063 | - |
| accountSized1 | 1135 | - |
| accountSizedInit2 | 10783 | - |
| accountSized2 | 1966 | - |
| accountSizedInit4 | 19975 | - |
| accountSized4 | 2787 | - |
| accountSizedInit8 | 38381 | - |
| accountSized8 | 5359 | - |
| accountUnsizedInit1 | 6193 | - |
| accountUnsized1 | 1243 | - |
| accountUnsizedInit2 | 11042 | - |
| accountUnsized2 | 1893 | - |
| accountUnsizedInit4 | 20495 | - |
| accountUnsized4 | 3104 | - |
| accountUnsizedInit8 | 39419 | - |
| accountUnsized8 | 6051 | - |
| boxedAccountEmptyInit1 | 6160 | - |
| boxedAccountEmpty1 | 976 | - |
| boxedAccountEmptyInit2 | 10784 | - |
| boxedAccountEmpty2 | 1499 | - |
| boxedAccountEmptyInit4 | 19500 | - |
| boxedAccountEmpty4 | 2530 | - |
| boxedAccountEmptyInit8 | 37415 | - |
| boxedAccountEmpty8 | 4780 | - |
| boxedAccountSizedInit1 | 6256 | - |
| boxedAccountSized1 | 1003 | - |
| boxedAccountSizedInit2 | 10975 | - |
| boxedAccountSized2 | 1554 | - |
| boxedAccountSizedInit4 | 19884 | - |
| boxedAccountSized4 | 2642 | - |
| boxedAccountSizedInit8 | 38182 | - |
| boxedAccountSized8 | 5003 | - |
| boxedAccountUnsizedInit1 | 6374 | - |
| boxedAccountUnsized1 | 1069 | - |
| boxedAccountUnsizedInit2 | 11211 | - |
| boxedAccountUnsized2 | 1679 | - |
| boxedAccountUnsizedInit4 | 20351 | - |
| boxedAccountUnsized4 | 2899 | - |
| boxedAccountUnsizedInit8 | 39118 | - |
| boxedAccountUnsized8 | 5517 | - |
| boxedInterfaceAccountMint1 | 2299 | - |
| boxedInterfaceAccountMint2 | 4053 | - |
| boxedInterfaceAccountMint4 | 7538 | - |
| boxedInterfaceAccountMint8 | 14699 | - |
| boxedInterfaceAccountToken1 | 1737 | - |
| boxedInterfaceAccountToken2 | 2928 | - |
| boxedInterfaceAccountToken4 | 5291 | - |
| boxedInterfaceAccountToken8 | 10205 | - |
| interfaceAccountMint1 | 2530 | - |
| interfaceAccountMint2 | 4726 | - |
| interfaceAccountMint4 | 9431 | - |
| interfaceAccountMint8 | 17709 | - |
| interfaceAccountToken1 | 1755 | - |
| interfaceAccountToken2 | 3211 | - |
| interfaceAccountToken4 | 6006 | - |
| interface1 | 999 | - |
| interface2 | 1574 | - |
| interface4 | 1996 | - |
| interface8 | 3651 | - |
| program1 | 999 | - |
| program2 | 1573 | - |
| program4 | 1998 | - |
| program8 | 3651 | - |
| signer1 | 958 | - |
| signer2 | 1576 | - |
| signer4 | 2079 | - |
| signer8 | 3895 | - |
| systemAccount1 | 1013 | - |
| systemAccount2 | 1686 | - |
| systemAccount4 | 2298 | - |
| systemAccount8 | 4336 | - |
| uncheckedAccount1 | 953 | - |
| uncheckedAccount2 | 1567 | - |
| uncheckedAccount4 | 2060 | - |
| uncheckedAccount8 | 3855 | - |
### Notable changes
---
## [0.27.0]
| Instruction | Compute Units | +/- |
| --------------------------- | ------------- | --- |
| accountInfo1 | 954 | N/A |
| accountInfo2 | 1567 | N/A |
| accountInfo4 | 2059 | N/A |
| accountInfo8 | 3856 | N/A |
| accountEmptyInit1 | 5958 | N/A |
| accountEmpty1 | 1090 | N/A |
| accountEmptyInit2 | 10574 | N/A |
| accountEmpty2 | 1852 | N/A |
| accountEmptyInit4 | 19557 | N/A |
| accountEmpty4 | 2646 | N/A |
| accountEmptyInit8 | 37541 | N/A |
| accountEmpty8 | 5043 | N/A |
| accountSizedInit1 | 6063 | N/A |
| accountSized1 | 1135 | N/A |
| accountSizedInit2 | 10783 | N/A |
| accountSized2 | 1966 | N/A |
| accountSizedInit4 | 19975 | N/A |
| accountSized4 | 2787 | N/A |
| accountSizedInit8 | 38381 | N/A |
| accountSized8 | 5359 | N/A |
| accountUnsizedInit1 | 6193 | N/A |
| accountUnsized1 | 1243 | N/A |
| accountUnsizedInit2 | 11042 | N/A |
| accountUnsized2 | 1893 | N/A |
| accountUnsizedInit4 | 20495 | N/A |
| accountUnsized4 | 3104 | N/A |
| accountUnsizedInit8 | 39419 | N/A |
| accountUnsized8 | 6051 | N/A |
| boxedAccountEmptyInit1 | 6160 | N/A |
| boxedAccountEmpty1 | 976 | N/A |
| boxedAccountEmptyInit2 | 10784 | N/A |
| boxedAccountEmpty2 | 1499 | N/A |
| boxedAccountEmptyInit4 | 19500 | N/A |
| boxedAccountEmpty4 | 2530 | N/A |
| boxedAccountEmptyInit8 | 37415 | N/A |
| boxedAccountEmpty8 | 4780 | N/A |
| boxedAccountSizedInit1 | 6256 | N/A |
| boxedAccountSized1 | 1003 | N/A |
| boxedAccountSizedInit2 | 10975 | N/A |
| boxedAccountSized2 | 1554 | N/A |
| boxedAccountSizedInit4 | 19884 | N/A |
| boxedAccountSized4 | 2642 | N/A |
| boxedAccountSizedInit8 | 38182 | N/A |
| boxedAccountSized8 | 5003 | N/A |
| boxedAccountUnsizedInit1 | 6374 | N/A |
| boxedAccountUnsized1 | 1069 | N/A |
| boxedAccountUnsizedInit2 | 11211 | N/A |
| boxedAccountUnsized2 | 1679 | N/A |
| boxedAccountUnsizedInit4 | 20351 | N/A |
| boxedAccountUnsized4 | 2899 | N/A |
| boxedAccountUnsizedInit8 | 39118 | N/A |
| boxedAccountUnsized8 | 5517 | N/A |
| boxedInterfaceAccountMint1 | 2299 | N/A |
| boxedInterfaceAccountMint2 | 4053 | N/A |
| boxedInterfaceAccountMint4 | 7538 | N/A |
| boxedInterfaceAccountMint8 | 14699 | N/A |
| boxedInterfaceAccountToken1 | 1737 | N/A |
| boxedInterfaceAccountToken2 | 2928 | N/A |
| boxedInterfaceAccountToken4 | 5291 | N/A |
| boxedInterfaceAccountToken8 | 10205 | N/A |
| interfaceAccountMint1 | 2530 | N/A |
| interfaceAccountMint2 | 4726 | N/A |
| interfaceAccountMint4 | 9431 | N/A |
| interfaceAccountMint8 | 17709 | N/A |
| interfaceAccountToken1 | 1755 | N/A |
| interfaceAccountToken2 | 3211 | N/A |
| interfaceAccountToken4 | 6006 | N/A |
| interface1 | 999 | N/A |
| interface2 | 1574 | N/A |
| interface4 | 1996 | N/A |
| interface8 | 3651 | N/A |
| program1 | 999 | N/A |
| program2 | 1573 | N/A |
| program4 | 1998 | N/A |
| program8 | 3651 | N/A |
| signer1 | 958 | N/A |
| signer2 | 1576 | N/A |
| signer4 | 2079 | N/A |
| signer8 | 3895 | N/A |
| systemAccount1 | 1013 | N/A |
| systemAccount2 | 1686 | N/A |
| systemAccount4 | 2298 | N/A |
| systemAccount8 | 4336 | N/A |
| uncheckedAccount1 | 953 | N/A |
| uncheckedAccount2 | 1567 | N/A |
| uncheckedAccount4 | 2060 | N/A |
| uncheckedAccount8 | 3855 | N/A |
---

15
tests/bench/Anchor.toml Normal file
View File

@ -0,0 +1,15 @@
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[programs.localnet]
bench = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
[workspace]
members = ["programs/bench"]
[scripts]
test = "yarn run ts-mocha -t 1000000 -p ./tsconfig.json -t 1000000 tests/**/*.ts"
update-bench = "yarn run ts-node scripts/update-bench.ts"
generate-ix = "yarn run ts-node scripts/generate-ix.ts"
bump-version = "yarn run ts-node scripts/bump-version.ts"

4
tests/bench/Cargo.toml Normal file
View File

@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]

27
tests/bench/README.md Normal file
View File

@ -0,0 +1,27 @@
# Benchmark tests
The bench program and its tests are used to measure the performance of Anchor programs.
## How
Create a program -> Write tests that measure usage -> Compare the results -> Save the new result
The script will check whether there is a difference between the current result and the last saved result(in `bench.json`) at the end of the tests. If the difference between the results is greater than 1%, the new data will be saved in `bench.json` and Markdown files in [/bench](https://github.com/coral-xyz/anchor/tree/master/bench) will be updated accordingly.
## Scripts
`anchor test --skip-lint`: Run all tests and update benchmark files when necessary. This is the only command that needs to be run for most use cases.
---
The following scripts are useful when making changes to how benchmarking works.
`anchor run update-bench`: Update Markdown files in [/bench](https://github.com/coral-xyz/anchor/tree/master/bench) based on the data from `bench.json`.
`anchor run generate-ix`: Generate instructions with repetitive accounts.
---
The following script is only for the maintainer(s) of Anchor.
`anchor run bump-version -- <VERSION>`: Bump the version in all benchmark files.

184
tests/bench/bench.json Normal file
View File

@ -0,0 +1,184 @@
{
"0.27.0": {
"computeUnits": {
"accountInfo1": 954,
"accountInfo2": 1567,
"accountInfo4": 2059,
"accountInfo8": 3856,
"accountEmptyInit1": 5958,
"accountEmpty1": 1090,
"accountEmptyInit2": 10574,
"accountEmpty2": 1852,
"accountEmptyInit4": 19557,
"accountEmpty4": 2646,
"accountEmptyInit8": 37541,
"accountEmpty8": 5043,
"accountSizedInit1": 6063,
"accountSized1": 1135,
"accountSizedInit2": 10783,
"accountSized2": 1966,
"accountSizedInit4": 19975,
"accountSized4": 2787,
"accountSizedInit8": 38381,
"accountSized8": 5359,
"accountUnsizedInit1": 6193,
"accountUnsized1": 1243,
"accountUnsizedInit2": 11042,
"accountUnsized2": 1893,
"accountUnsizedInit4": 20495,
"accountUnsized4": 3104,
"accountUnsizedInit8": 39419,
"accountUnsized8": 6051,
"boxedAccountEmptyInit1": 6160,
"boxedAccountEmpty1": 976,
"boxedAccountEmptyInit2": 10784,
"boxedAccountEmpty2": 1499,
"boxedAccountEmptyInit4": 19500,
"boxedAccountEmpty4": 2530,
"boxedAccountEmptyInit8": 37415,
"boxedAccountEmpty8": 4780,
"boxedAccountSizedInit1": 6256,
"boxedAccountSized1": 1003,
"boxedAccountSizedInit2": 10975,
"boxedAccountSized2": 1554,
"boxedAccountSizedInit4": 19884,
"boxedAccountSized4": 2642,
"boxedAccountSizedInit8": 38182,
"boxedAccountSized8": 5003,
"boxedAccountUnsizedInit1": 6374,
"boxedAccountUnsized1": 1069,
"boxedAccountUnsizedInit2": 11211,
"boxedAccountUnsized2": 1679,
"boxedAccountUnsizedInit4": 20351,
"boxedAccountUnsized4": 2899,
"boxedAccountUnsizedInit8": 39118,
"boxedAccountUnsized8": 5517,
"boxedInterfaceAccountMint1": 2299,
"boxedInterfaceAccountMint2": 4053,
"boxedInterfaceAccountMint4": 7538,
"boxedInterfaceAccountMint8": 14699,
"boxedInterfaceAccountToken1": 1737,
"boxedInterfaceAccountToken2": 2928,
"boxedInterfaceAccountToken4": 5291,
"boxedInterfaceAccountToken8": 10205,
"interfaceAccountMint1": 2530,
"interfaceAccountMint2": 4726,
"interfaceAccountMint4": 9431,
"interfaceAccountMint8": 17709,
"interfaceAccountToken1": 1755,
"interfaceAccountToken2": 3211,
"interfaceAccountToken4": 6006,
"interface1": 999,
"interface2": 1574,
"interface4": 1996,
"interface8": 3651,
"program1": 999,
"program2": 1573,
"program4": 1998,
"program8": 3651,
"signer1": 958,
"signer2": 1576,
"signer4": 2079,
"signer8": 3895,
"systemAccount1": 1013,
"systemAccount2": 1686,
"systemAccount4": 2298,
"systemAccount8": 4336,
"uncheckedAccount1": 953,
"uncheckedAccount2": 1567,
"uncheckedAccount4": 2060,
"uncheckedAccount8": 3855
}
},
"unreleased": {
"computeUnits": {
"accountInfo1": 954,
"accountInfo2": 1567,
"accountInfo4": 2059,
"accountInfo8": 3856,
"accountEmptyInit1": 5958,
"accountEmpty1": 1090,
"accountEmptyInit2": 10583,
"accountEmpty2": 1852,
"accountEmptyInit4": 19557,
"accountEmpty4": 2646,
"accountEmptyInit8": 37541,
"accountEmpty8": 5043,
"accountSizedInit1": 6063,
"accountSized1": 1135,
"accountSizedInit2": 10783,
"accountSized2": 1966,
"accountSizedInit4": 19975,
"accountSized4": 2787,
"accountSizedInit8": 38381,
"accountSized8": 5359,
"accountUnsizedInit1": 6193,
"accountUnsized1": 1243,
"accountUnsizedInit2": 11042,
"accountUnsized2": 1893,
"accountUnsizedInit4": 20495,
"accountUnsized4": 3104,
"accountUnsizedInit8": 39419,
"accountUnsized8": 6051,
"boxedAccountEmptyInit1": 6160,
"boxedAccountEmpty1": 976,
"boxedAccountEmptyInit2": 10784,
"boxedAccountEmpty2": 1499,
"boxedAccountEmptyInit4": 19500,
"boxedAccountEmpty4": 2530,
"boxedAccountEmptyInit8": 37415,
"boxedAccountEmpty8": 4780,
"boxedAccountSizedInit1": 6256,
"boxedAccountSized1": 1003,
"boxedAccountSizedInit2": 10975,
"boxedAccountSized2": 1554,
"boxedAccountSizedInit4": 19884,
"boxedAccountSized4": 2642,
"boxedAccountSizedInit8": 38182,
"boxedAccountSized8": 5003,
"boxedAccountUnsizedInit1": 6374,
"boxedAccountUnsized1": 1069,
"boxedAccountUnsizedInit2": 11211,
"boxedAccountUnsized2": 1679,
"boxedAccountUnsizedInit4": 20351,
"boxedAccountUnsized4": 2899,
"boxedAccountUnsizedInit8": 39118,
"boxedAccountUnsized8": 5517,
"boxedInterfaceAccountMint1": 2299,
"boxedInterfaceAccountMint2": 4053,
"boxedInterfaceAccountMint4": 7538,
"boxedInterfaceAccountMint8": 14699,
"boxedInterfaceAccountToken1": 1737,
"boxedInterfaceAccountToken2": 2928,
"boxedInterfaceAccountToken4": 5291,
"boxedInterfaceAccountToken8": 10205,
"interfaceAccountMint1": 2530,
"interfaceAccountMint2": 4726,
"interfaceAccountMint4": 9431,
"interfaceAccountMint8": 17709,
"interfaceAccountToken1": 1755,
"interfaceAccountToken2": 3211,
"interfaceAccountToken4": 6006,
"interface1": 999,
"interface2": 1574,
"interface4": 1996,
"interface8": 3651,
"program1": 999,
"program2": 1573,
"program4": 1998,
"program8": 3651,
"signer1": 958,
"signer2": 1576,
"signer4": 2079,
"signer8": 3895,
"systemAccount1": 1013,
"systemAccount2": 1686,
"systemAccount4": 2298,
"systemAccount8": 4336,
"uncheckedAccount1": 953,
"uncheckedAccount2": 1567,
"uncheckedAccount4": 2060,
"uncheckedAccount8": 3855
}
}
}

19
tests/bench/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "bench",
"version": "0.27.0",
"license": "(MIT OR Apache-2.0)",
"homepage": "https://github.com/coral-xyz/anchor#readme",
"bugs": {
"url": "https://github.com/coral-xyz/anchor/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/coral-xyz/anchor.git"
},
"engines": {
"node": ">=17"
},
"scripts": {
"test": "anchor test"
}
}

View File

@ -0,0 +1,16 @@
[package]
name = "bench"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
[dependencies]
anchor-lang = { path = "../../../../lang" }
anchor-spl = { path = "../../../../spl" }

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
/**
* Bump the version of all benchmark related files by changing the `Unreleased`
* version to a new version and adding a new `Unreleased` version.
*/
import { BenchData } from "./utils";
(async () => {
const newVersion = process.argv[2];
if (!newVersion) {
console.error("Usage: anchor run bump-version -- <VERSION>");
process.exit(1);
}
// Bump bench data
const bench = await BenchData.open();
bench.bumpVersion(newVersion);
await bench.save();
// Bump markdown files
await BenchData.forEachMarkdown((markdown) => {
markdown.bumpVersion(newVersion);
});
})();

View File

@ -0,0 +1,225 @@
/**
* Generate instructions with repetitive accounts and add them to the bench program.
*/
import * as fs from "fs/promises";
import path from "path";
type Instruction = {
/** Instruction name */
name: string;
/** Each account type in accounts struct */
accountType: string;
/** Account macro(`#[account(..)]`) */
accountMacro?: {
init: true;
space?: number | string;
};
/** Number of accounts to create per instruction */
accountCounts?: number[];
};
/**
* The following instructions will be added to the program.
*
* If an instruction already exists, it will be skipped.
*/
const INSTRUCTIONS: Instruction[] = [
{
name: "account_info",
accountType: "AccountInfo<'info>",
},
{
name: "account_empty_init",
accountType: "Account<'info, Empty>",
accountMacro: {
init: true,
},
},
{
name: "account_empty",
accountType: "Account<'info, Empty>",
},
{
name: "account_sized_init",
accountType: "Account<'info, Sized>",
accountMacro: {
init: true,
space: "8 + std::mem::size_of::<Sized>()",
},
},
{
name: "account_sized",
accountType: "Account<'info, Sized>",
},
{
name: "account_unsized_init",
accountType: "Account<'info, Unsized>",
accountMacro: {
init: true,
space: "8 + std::mem::size_of::<Unsized>()",
},
},
{
name: "account_unsized",
accountType: "Account<'info, Unsized>",
},
{
name: "boxed_account_empty_init",
accountType: "Box<Account<'info, Empty>>",
accountMacro: {
init: true,
},
},
{
name: "boxed_account_empty",
accountType: "Box<Account<'info, Empty>>",
},
{
name: "boxed_account_sized_init",
accountType: "Box<Account<'info, Sized>>",
accountMacro: {
init: true,
space: "8 + std::mem::size_of::<Sized>()",
},
},
{
name: "boxed_account_sized",
accountType: "Box<Account<'info, Sized>>",
},
{
name: "boxed_account_unsized_init",
accountType: "Box<Account<'info, Unsized>>",
accountMacro: {
init: true,
space: "8 + std::mem::size_of::<Unsized>()",
},
},
{
name: "boxed_account_unsized",
accountType: "Box<Account<'info, Unsized>>",
},
{
name: "boxed_interface_account_mint",
accountType: "Box<InterfaceAccount<'info, Mint>>",
},
{
name: "boxed_interface_account_token",
accountType: "Box<InterfaceAccount<'info, TokenAccount>>",
},
{
name: "interface_account_mint",
accountType: "InterfaceAccount<'info, Mint>",
},
{
name: "interface_account_token",
accountType: "InterfaceAccount<'info, TokenAccount>",
accountCounts: [1, 2, 4],
},
{
name: "interface",
accountType: "Interface<'info, TokenInterface>",
},
{
name: "program",
accountType: "Program<'info, System>",
},
{
name: "signer",
accountType: "Signer<'info>",
},
{
name: "system_account",
accountType: "SystemAccount<'info>",
},
{
name: "unchecked_account",
accountType: "UncheckedAccount<'info>",
},
];
(async () => {
// Get the program file
const programPath = path.join("programs", "bench", "src", "lib.rs");
let file = await fs.readFile(programPath, {
encoding: "utf8",
});
const create = (
ix: Omit<Instruction, "accountCounts"> & { count: number }
) => {
// Get the title case of the name for the accounts struct
const accountsName =
ix.name[0].toUpperCase() +
ix.name.slice(1).replace(/_\w/g, (match) => match[1].toUpperCase());
// Generate accounts
let accounts = "";
let accountMacro = "";
const INDENT = "\n ";
if (ix.accountMacro?.init) {
accounts += `${INDENT}#[account(mut)]${INDENT}pub payer: Signer<'info>,`;
accounts += `${INDENT}pub system_program: Program<'info, System>,`;
accountMacro += `init, payer = payer, space = ${
ix.accountMacro.space ?? 8
}`;
}
accountMacro = `${INDENT}#[account(${accountMacro})]`;
for (let i = 0; i < ix.count; i++) {
if (ix.accountMacro) {
accounts += accountMacro;
}
accounts += `${INDENT}pub account${i + 1}: ${ix.accountType},`;
}
return {
ix: `
pub fn ${ix.name}(_ctx: Context<${accountsName}>) -> Result<()> {
Ok(())
}`,
accounts: `
#[derive(Accounts)]
pub struct ${accountsName}<'info> {${accounts}\n}`,
};
};
const insert = (index: number, text: string) => {
file = file.slice(0, index) + "\n" + text + file.slice(index);
};
for (const instruction of INSTRUCTIONS) {
// Default count
instruction.accountCounts ??= [1, 2, 4, 8];
for (const count of instruction.accountCounts) {
// Append count to the end of the instruction name
const ixName = instruction.name + count;
// Skip existing instructions
if (file.includes(`fn ${ixName}`)) {
continue;
}
const { ix, accounts } = create({ ...instruction, name: ixName, count });
// Get the ix index to start from
const programIndex = file.indexOf("#[program]");
const fileStartingFromProgram = file.slice(programIndex);
// Add instruction
const ixIndex = programIndex + fileStartingFromProgram.indexOf("\n}");
insert(ixIndex, ix);
// Add accounts
const accountsIndex = file.length - 1;
insert(accountsIndex, accounts);
}
}
// Save
await fs.writeFile(programPath, file);
})();

View File

@ -0,0 +1,67 @@
/** Update Markdown files in /bench */
import { BenchData, Markdown } from "./utils";
(async () => {
const bench = await BenchData.open();
await BenchData.forEachMarkdown((markdown, fileName) => {
if (fileName === "COMPUTE_UNITS.md") {
const versions = bench.getVersions();
// On the first version, compare with itself to update it with no changes
versions.unshift(versions[0]);
for (const i in versions) {
const currentVersion = versions[i];
const nextVersion = versions[+i + 1];
if (currentVersion === "unreleased") {
return;
}
const newComputeUnitsResult = bench.get(nextVersion).computeUnits;
const oldComputeUnitsResult = bench.get(currentVersion).computeUnits;
// Create table
const table = Markdown.createTable(
"Instruction",
"Compute Units",
"+/-"
);
bench.compareComputeUnits(
newComputeUnitsResult,
oldComputeUnitsResult,
(ixName, newComputeUnits, oldComputeUnits) => {
const percentChange = (
(newComputeUnits / oldComputeUnits - 1) *
100
).toFixed(2);
let changeText;
if (isNaN(oldComputeUnits)) {
changeText = "N/A";
} else if (+percentChange > 0) {
changeText = `🔴 **+${percentChange}%**`;
} else {
changeText = `🟢 **${percentChange}%**`;
}
table.insert(ixName, newComputeUnits.toString(), changeText);
},
(ixName, computeUnits) => {
table.insert(
ixName,
computeUnits.toString(),
+i === 0 ? "N/A" : "-"
);
}
);
// Update version's table
markdown.updateTable(nextVersion, table);
}
}
});
})();

View File

@ -0,0 +1,298 @@
import * as fs from "fs/promises";
import path from "path";
import { spawnSync } from "child_process";
/** Persistent benchmark data(mapping of `Version -> Data`) */
type Bench = {
[key: string]: {
/** Benchmark result for compute units consumed */
computeUnits: ComputeUnits;
};
};
/** `instruction name -> compute units consumed` */
export type ComputeUnits = { [key: string]: number };
/**
* How much of a percentage difference between the current and the previous data
* should be significant. Any difference above this number should be noted in
* the benchmark file.
*/
export const THRESHOLD_PERCENTAGE = 1;
/** Path to the benchmark Markdown files */
export const BENCH_DIR_PATH = "../../bench";
/** Utility class to handle benchmark data related operations */
export class BenchData {
/** Benchmark data filepath */
static #PATH = "bench.json";
/** Benchmark data */
#data: Bench;
constructor(data: Bench) {
this.#data = data;
}
/** Open the benchmark data file */
static async open() {
let bench: Bench;
try {
const benchFile = await fs.readFile(BenchData.#PATH, {
encoding: "utf8",
});
bench = JSON.parse(benchFile);
} catch {
bench = {};
}
return new BenchData(bench);
}
/** Save the benchmark data file */
async save() {
await fs.writeFile(BenchData.#PATH, JSON.stringify(this.#data, null, 2));
}
/** Get the stored results based on version */
get(version: string) {
return this.#data[version];
}
/** Get unreleased version results */
getUnreleased() {
return this.get("unreleased");
}
/** Get all versions */
getVersions() {
return Object.keys(this.#data);
}
/** Compare and update compute units changes */
compareComputeUnits(
newComputeUnitsResult: ComputeUnits,
oldComputeUnitsResult: ComputeUnits,
changeCb: (
ixName: string,
newComputeUnits: number,
oldComputeUnits: number
) => void,
noChangeCb?: (ixName: string, computeUnits: number) => void
) {
let needsUpdate = false;
// Compare compute units changes
for (const ixName in newComputeUnitsResult) {
const oldComputeUnits = oldComputeUnitsResult[ixName];
const newComputeUnits = newComputeUnitsResult[ixName];
if (!oldComputeUnits) {
console.log(`New instruction '${ixName}'`);
needsUpdate = true;
changeCb(ixName, newComputeUnits, NaN);
continue;
}
const percentage = THRESHOLD_PERCENTAGE / 100;
const oldMaximumAllowedDelta = oldComputeUnits * percentage;
const newMaximumAllowedDelta = newComputeUnits * percentage;
const delta = newComputeUnits - oldComputeUnits;
const absDelta = Math.abs(delta);
if (
absDelta > oldMaximumAllowedDelta ||
absDelta > newMaximumAllowedDelta
) {
// Throw in CI
if (process.env.CI) {
throw new Error(
[
`Compute units for instruction '${ixName}' has changed more than ${THRESHOLD_PERCENTAGE}% but is not saved.`,
"Run `anchor test --skip-lint` in tests/bench and commit the changes.",
].join(" ")
);
}
console.log(
`Compute units change '${ixName}' (${oldComputeUnits} -> ${newComputeUnits})`
);
needsUpdate = true;
changeCb(ixName, newComputeUnits, oldComputeUnits);
} else {
noChangeCb?.(ixName, newComputeUnits);
}
}
return { needsUpdate };
}
/** Bump benchmark data version to the given version */
bumpVersion(newVersion: string) {
const versions = Object.keys(this.#data);
const unreleasedVersion = versions[versions.length - 1];
if (this.#data[newVersion]) {
console.error(`Version '${newVersion}' already exists!`);
process.exit(1);
}
// Add the new version
this.#data[newVersion] = this.get(unreleasedVersion);
// Delete the unreleased version
delete this.#data[unreleasedVersion];
// Add new unreleased version
this.#data[unreleasedVersion] = this.#data[newVersion];
}
/**
* Loop through all of the markdown files and run the given callback before
* saving the file.
*/
static async forEachMarkdown(
cb: (markdown: Markdown, fileName: string) => void
) {
const fileNames = await fs.readdir(BENCH_DIR_PATH);
const markdownFileNames = fileNames.filter((n) => n.endsWith(".md"));
for (const fileName of markdownFileNames) {
const markdown = await Markdown.open(path.join(BENCH_DIR_PATH, fileName));
cb(markdown, fileName);
await markdown.save();
}
// Format
spawnSync("yarn", [
"run",
"prettier",
"--write",
path.join(BENCH_DIR_PATH, "*.md"),
]);
}
}
/** Utility class to handle markdown related operations */
export class Markdown {
/** Unreleased version string */
static #UNRELEASED_VERSION = "[Unreleased]";
/** Markdown filepath */
#path: string;
/** Markdown text */
#text: string;
constructor(path: string, text: string) {
this.#path = path;
this.#text = text;
}
/** Open the markdown file */
static async open(path: string) {
const text = await fs.readFile(path, { encoding: "utf8" });
return new Markdown(path, text);
}
/** Create a markdown table */
static createTable(...args: string[]) {
return new MarkdownTable([args]);
}
/** Save the markdown file */
async save() {
await fs.writeFile(this.#path, this.#text);
}
/** Change version table with the given table */
updateTable(version: string, table: MarkdownTable) {
const md = this.#text;
let titleStartIndex = md.indexOf(`[${version}]`);
if (titleStartIndex === -1) {
titleStartIndex = md.indexOf(Markdown.#UNRELEASED_VERSION);
}
const startIndex = titleStartIndex + md.slice(titleStartIndex).indexOf("|");
const endIndex = startIndex + md.slice(startIndex).indexOf("\n\n");
this.#text =
md.slice(0, startIndex) + table.toString() + md.slice(endIndex + 1);
}
/** Bump the version to the given version */
bumpVersion(newVersion: string) {
newVersion = `[${newVersion}]`;
if (this.#text.includes(newVersion)) {
console.error(`Version '${newVersion}' already exists!`);
process.exit(1);
}
const startIndex = this.#text.indexOf(`## ${Markdown.#UNRELEASED_VERSION}`);
const endIndex =
startIndex + this.#text.slice(startIndex).indexOf("\n---") + 4;
let unreleasedSection = this.#text.slice(startIndex, endIndex);
// Update unreleased version to `newVersion`
const newSection = unreleasedSection.replace(
Markdown.#UNRELEASED_VERSION,
newVersion
);
// Reset unreleased version changes
unreleasedSection = unreleasedSection
.split("\n")
.map((line, i) => {
// First 4 lines don't change
if ([0, 1, 2, 3].includes(i)) return line;
const regex = /\|.*\|.*\|(.*)\|/;
const result = regex.exec(line);
const changeStr = result?.[1];
if (!changeStr) {
if (line.startsWith("#")) return line;
else if (line.startsWith("---")) return line + "\n";
else return "";
}
return line.replace(changeStr, "-");
})
.join("\n");
// Update the text
this.#text =
this.#text.slice(0, startIndex) +
unreleasedSection +
newSection +
this.#text.slice(endIndex);
}
}
/** Utility class to handle markdown table related operations */
class MarkdownTable {
/** Markdown rows stored as array of arrays */
#rows: string[][];
constructor(rows: string[][]) {
this.#rows = rows;
this.insert("-", "-", "-");
}
/** Insert a new row to the markdown table */
insert(...args: string[]) {
this.#rows.push(args);
}
/** Convert the stored rows to a markdown table */
toString() {
return this.#rows.reduce(
(acc, row) =>
acc + row.reduce((acc, cur) => `${acc} ${cur} |`, "|") + "\n",
""
);
}
}

View File

@ -0,0 +1,248 @@
import * as anchor from "@coral-xyz/anchor";
import * as token from "@coral-xyz/spl-token";
import { spawnSync } from "child_process";
import { Bench, IDL } from "../target/types/bench";
import { BenchData, ComputeUnits } from "../scripts/utils";
describe(IDL.name, () => {
// Configure the client to use the local cluster
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Bench as anchor.Program<Bench>;
const owner = program.provider.publicKey!;
let mintPk: anchor.web3.PublicKey;
let tokenPk: anchor.web3.PublicKey;
const computeUnits: ComputeUnits = {};
const measureComputeUnits = async (
ixName: string,
options?: Partial<{
accountCounts: number[];
generateKeypair: (accountName: string) => anchor.web3.Keypair;
generatePublicKey: (accountName: string) => anchor.web3.PublicKey;
}>
) => {
options ??= {};
options.accountCounts ??= [1, 2, 4, 8];
options.generateKeypair ??= () => anchor.web3.Keypair.generate();
for (const accountCount of options.accountCounts) {
// Check whether the init version of the instruction exists
const ixNameInit = `${ixName}Init`;
const hasInitVersion = IDL.instructions.some((ix) =>
ix.name.startsWith(ixNameInit)
);
const ixNames = [ixName];
if (hasInitVersion) {
// Init version has priority
ixNames.unshift(ixNameInit);
}
const accounts: { [key: string]: anchor.web3.PublicKey } = {};
const signers = [];
for (const ixName of ixNames) {
const method =
`${ixName}${accountCount}` as keyof typeof program.methods;
// Remove signers when it's not init instruction
if (ixName !== ixNameInit) {
signers.splice(0);
}
for (const ix of IDL.instructions) {
if (ix.name !== method) continue;
for (const account of ix.accounts) {
// Only set account keys if it hasn't been set before
if (accounts[account.name]) {
continue;
}
if (account.name === "payer") {
accounts[account.name] = owner;
continue;
}
// Skip other accounts to not override Anchor defaults
if (!account.name.startsWith("account")) {
continue;
}
if (options.generatePublicKey) {
accounts[account.name] = options.generatePublicKey(account.name);
continue;
}
const keypair = options.generateKeypair(account.name);
accounts[account.name] = keypair.publicKey;
if (account.isSigner) {
signers.push(keypair);
}
}
}
// Send tx
console.log({ method });
const txHash = await program.methods[method]()
.accounts(accounts)
.signers(signers)
.rpc();
// Confirm tx
await program.provider.connection.confirmTransaction(
txHash,
"confirmed"
);
// Get tx
const tx = await program.provider.connection.getTransaction(txHash, {
commitment: "confirmed",
});
computeUnits[method] = tx!.meta!.computeUnitsConsumed!;
}
}
};
before(async () => {
const tokenProgram = token.splTokenProgram({
provider: anchor.AnchorProvider.local(),
});
const tx = new anchor.web3.Transaction();
// Create mint account
const mintKp = new anchor.web3.Keypair();
mintPk = mintKp.publicKey;
const createMintIx = await tokenProgram.account.mint.createInstruction(
mintKp
);
const initMintIx = await tokenProgram.methods
.initializeMint2(0, owner, null)
.accounts({ mint: mintPk })
.instruction();
tx.add(createMintIx, initMintIx);
// Create token account
const tokenKp = new anchor.web3.Keypair();
tokenPk = tokenKp.publicKey;
const createTokenIx = await tokenProgram.account.account.createInstruction(
tokenKp
);
const initTokenIx = await tokenProgram.methods
.initializeAccount3(owner)
.accounts({ account: tokenPk, mint: mintPk })
.instruction();
tx.add(createTokenIx, initTokenIx);
await tokenProgram.provider.sendAndConfirm!(tx, [mintKp, tokenKp]);
});
it("AccountInfo", async () => {
await measureComputeUnits("accountInfo");
});
it("Account Empty", async () => {
await measureComputeUnits("accountEmpty");
});
it("Account Sized", async () => {
await measureComputeUnits("accountSized");
});
it("Account Unsized", async () => {
await measureComputeUnits("accountUnsized");
});
it("Boxed Account Empty", async () => {
await measureComputeUnits("boxedAccountEmpty");
});
it("Boxed Account Sized", async () => {
await measureComputeUnits("boxedAccountSized");
});
it("Boxed Account Unsized", async () => {
await measureComputeUnits("boxedAccountUnsized");
});
it("Boxed Interface Account Mint", async () => {
await measureComputeUnits("boxedInterfaceAccountMint", {
generatePublicKey: () => mintPk,
});
});
it("Boxed Interface Account Token", async () => {
await measureComputeUnits("boxedInterfaceAccountToken", {
generatePublicKey: () => tokenPk,
});
});
it("Interface Account Mint", async () => {
await measureComputeUnits("interfaceAccountMint", {
generatePublicKey: () => mintPk,
});
});
it("Interface Account Token", async () => {
await measureComputeUnits("interfaceAccountToken", {
generatePublicKey: () => tokenPk,
accountCounts: [1, 2, 4],
});
});
it("Interface", async () => {
await measureComputeUnits("interface", {
generatePublicKey: () => token.SPL_TOKEN_PROGRAM_ID,
});
});
it("Program", async () => {
await measureComputeUnits("program", {
generatePublicKey: () => anchor.web3.SystemProgram.programId,
});
});
it("Signer", async () => {
await measureComputeUnits("signer");
});
it("SystemAccount", async () => {
await measureComputeUnits("systemAccount");
});
it("UncheckedAccount", async () => {
await measureComputeUnits("uncheckedAccount");
});
after(async () => {
// Read the bench data file
const bench = await BenchData.open();
// Compare and update compute units changes
const oldComputeUnits = bench.getUnreleased().computeUnits;
const { needsUpdate } = bench.compareComputeUnits(
computeUnits,
oldComputeUnits,
(ixName, newComputeUnits) => {
oldComputeUnits[ixName] = newComputeUnits;
}
);
if (needsUpdate) {
console.log("Updating benchmark files...");
// Save bench data file
// (needs to happen before running the `update-bench` script)
await bench.save();
spawnSync("anchor", ["run", "update-bench"]);
}
});
});

12
tests/bench/tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"types": ["mocha", "chai", "node"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015", "dom"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
}
}

View File

@ -8,6 +8,7 @@
"workspaces": [
"anchor-cli-account",
"anchor-cli-idl",
"bench",
"cashiers-check",
"cfo",
"chat",

View File

@ -7,7 +7,9 @@ if [ $# -eq 0 ]; then
exit 1
fi
echo "Bumping versions to $1"
version=$1
echo "Bumping versions to $version"
# GNU/BSD compat
sedi=(-i)
@ -16,28 +18,34 @@ case "$(uname)" in
Darwin*) sedi=(-i "")
esac
git grep -l $(cat VERSION) -- ':!**/yarn.lock' ':!CHANGELOG.md' ':!Cargo.lock' ':!package.json' | \
# Don't replace version with the following globs
skip_globs=":!**/yarn.lock :!Cargo.lock :!package.json :!tests/bench/bench.json :!bench/*.md"
git grep -l $(cat VERSION) -- $skip_globs |
xargs sed "${sedi[@]}" \
-e "s/$(cat VERSION)/$1/g"
-e "s/$(cat VERSION)/$version/g"
# Potential for collisions in package.json files, handle those separately
# Replace only matching "version": "x.xx.x" and "@coral-xyz/anchor": "x.xx.x"
git grep -l $(cat VERSION) -- '**/package.json' | \
xargs sed "${sedi[@]}" \
-e "s/@coral-xyz\/anchor\": \"$(cat VERSION)\"/@coral-xyz\/anchor\": \"$1\"/g" \
-e "s/\"version\": \"$(cat VERSION)\"/\"version\": \"$1\"/g"
-e "s/@coral-xyz\/anchor\": \"$(cat VERSION)\"/@coral-xyz\/anchor\": \"$version\"/g" \
-e "s/\"version\": \"$(cat VERSION)\"/\"version\": \"$version\"/g"
# Potential for collisions in Cargo.lock, use cargo update to update it
cargo update --workspace
# Insert version number into CHANGELOG.md
sed "${sedi[@]}" -e "s/## \[Unreleased\]/## [Unreleased]\n\n## [$1] - $(date '+%Y-%m-%d')/g" CHANGELOG.md
sed "${sedi[@]}" -e "s/## \[Unreleased\]/## [Unreleased]\n\n## [$version] - $(date '+%Y-%m-%d')/g" CHANGELOG.md
pushd ts && yarn && popd
pushd tests && yarn && popd
pushd examples && yarn && pushd tutorial && yarn && popd && popd
echo $1 > VERSION
# Bump benchmark files
pushd tests/bench && anchor run bump-version -- $version && popd
echo $version > VERSION
echo "$(git diff --stat | tail -n1) files modified"