Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: add initial benchmark infrastructure #3084

Merged
merged 38 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ad814e9
test: introduce initial wallet benchmark tests
maschad May 6, 2024
b3d98f1
Merge branch 'master' into tests/add-benchmark-tests
maschad May 29, 2024
00e7d6b
test: use clinic for flame and profile tests
maschad May 31, 2024
df3f7f6
test: added doctor and heap profiling
maschad May 31, 2024
352c33a
build: add dynamic scripts + account profiling
maschad Jun 1, 2024
2be51be
test: added more benchmark tests
maschad Jun 2, 2024
01338f5
Merge branch 'master' into mc/test/performance-tests
maschad Jun 2, 2024
5395454
test: update crypto benchmarking
maschad Jun 4, 2024
08e95fb
Merge branch 'master' into mc/test/performance-tests
maschad Sep 1, 2024
0703485
build: update lock file
maschad Sep 1, 2024
89f2878
Merge branch 'master' into mc/test/performance-tests
maschad Sep 2, 2024
b23cd12
build: resolve build issues
maschad Sep 2, 2024
bb8d58d
feat: added benchmarks for cost estimation
maschad Sep 3, 2024
7fefa3c
feat: added transaction results benchmarks
maschad Sep 3, 2024
135edfe
ci: add initial benchmark workflow
maschad Sep 3, 2024
2b0ef79
Merge branch 'master' into mc/test/performance-tests
maschad Sep 5, 2024
4d00d70
ci: integrate codespeed
maschad Sep 5, 2024
008dc78
build: move bench marks to internal folder
maschad Sep 5, 2024
6d7338c
test: remove unrelated abi coder tests
maschad Sep 7, 2024
54aa319
Merge branch 'master' into mc/test/performance-tests
maschad Sep 7, 2024
493d7ad
restore relevant abi-coder files
maschad Sep 7, 2024
b8fb315
fix: remove unnecessary delay
maschad Sep 7, 2024
c2f6036
build: update lockfile
maschad Sep 7, 2024
4277eab
restore account package
maschad Sep 7, 2024
c1cd693
ci: update benchmark command
maschad Sep 7, 2024
7ee57c2
docs: add changeset + pretest command
maschad Sep 7, 2024
a7d3e5e
lint: format forc.toml
maschad Sep 7, 2024
6ef92fd
Merge branch 'master' into mc/test/performance-tests
maschad Sep 8, 2024
6c4acd7
Merge branch 'master' into mc/test/performance-tests
maschad Sep 9, 2024
a0597b9
pr updates
maschad Sep 9, 2024
5d96aab
Merge branch 'master' into mc/test/performance-tests
maschad Sep 9, 2024
38a228a
resolve lockfile issue
maschad Sep 9, 2024
9281eae
ci: update benchmark workflow
maschad Sep 9, 2024
8385216
ci: remove browser benchmark job
maschad Sep 9, 2024
61807c4
docs: add docs for benchmarking and profiling
maschad Sep 9, 2024
c4bbf94
feat: add contract read, write, mint benches
maschad Sep 10, 2024
d1454d6
deps: remove 0x
maschad Sep 10, 2024
20ab594
Merge branch 'master' into mc/test/performance-tests
maschad Sep 12, 2024
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
4 changes: 4 additions & 0 deletions .changeset/dirty-steaks-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

ci: add initial benchmark infrastructure
25 changes: 25 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Benchmarks
arboleya marked this conversation as resolved.
Show resolved Hide resolved
on:
pull_request:
maschad marked this conversation as resolved.
Show resolved Hide resolved
push:
branches:
- master

jobs:
benchmarks:
arboleya marked this conversation as resolved.
Show resolved Hide resolved
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: CI Setup
uses: ./.github/actions/test-setup

- name: Pretest
run: pnpm pretest

- name: Run Node benchmarks
uses: CodSpeedHQ/action@v3
with:
run: pnpm bench:node
token: ${{ secrets.CODSPEED_TOKEN }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ lib-cov
*.pid
*.gz
*.swp
*.0x/*

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
Expand Down Expand Up @@ -144,6 +145,10 @@ Forc.lock
**/out/release
**/test-predicate-*/index.ts

## Ignore perf test files
*clinic*
.clinic/*

# Ignore typegen test files
**/test/typegen

Expand Down
1 change: 1 addition & 0 deletions .knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"apps/create-fuels-counter-guide/**"
],
"ignoreDependencies": [
"autocannon",
"bun",
"@/sway-api/*",
"@fuel-ts/*",
Expand Down
22 changes: 22 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,28 @@ pnpm test:filter packages/my-desired-package/src/my.test.ts
pnpm test -- --coverage --my-other-flag
```

# Benchmarking

We currently use `vitest` 's [bench utility](https://vitest.dev/api/#bench) to run benchmarks. You can run them in both the browser and node environments.

```sh
pnpm bench:node
```

```sh
# run benchmarks for a specific package
pnpm bench:node packages/my-desired-package
```

# Profiling

We currently use [`clinic`](https://clinicjs.org/) to profile and debug our tooling. For instance you can run clinic's flame command to create a flamegraph for a specific package:

```sh
# creates a flamegraph for a specific package
npm_config_package_name=account pnpm clinic:flame // runs flame against the account package
```

### CI Test

During the CI process an automated end-to-end (e2e) test is executed. This test is crucial as it simulates real-world scenarios on the current test-net, ensuring that the changeset maintains the expected functionality and stability.
Expand Down
1 change: 1 addition & 0 deletions internal/benchmarks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test/fixtures/forc-projects/**/index.ts
1 change: 1 addition & 0 deletions internal/benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A package for running benchmarks.
9 changes: 9 additions & 0 deletions internal/benchmarks/fuels.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createConfig } from 'fuels';

export default createConfig({
workspace: './test/fixtures/forc-projects',
output: './test/typegen',
forcBuildFlags: ['--release'],
forcPath: 'fuels-forc',
fuelCorePath: 'fuels-core',
});
17 changes: 17 additions & 0 deletions internal/benchmarks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"private": true,
"name": "@internal/benchmarks",
"files": [
"dist"
],
"scripts": {
"type:check": "tsc --noEmit",
"pretest": "run-s build:forc type:check",
"build:forc": "pnpm fuels build"
},
"license": "Apache-2.0",
"dependencies": {
"fuels": "workspace:*"
},
"version": "1.0.0"
}
70 changes: 70 additions & 0 deletions internal/benchmarks/src/contract-interaction.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* eslint-disable import/no-extraneous-dependencies */

import type { WalletUnlocked } from 'fuels';
import { bn } from 'fuels';
import { launchTestNode, TestAssetId } from 'fuels/test-utils';
import { bench } from 'vitest';

import type { CounterContract, CallTestContract } from '../test/typegen/contracts';
import { CounterContractFactory, CallTestContractFactory } from '../test/typegen/contracts';
/**
* @group node
* @group browser
*/
describe('Contract Interaction Benchmarks', () => {
let contract: CounterContract;
let callTestContract: CallTestContract;
let wallet: WalletUnlocked;
let cleanup: () => void;
beforeEach(async () => {
const launched = await launchTestNode({
contractsConfigs: [{ factory: CounterContractFactory }, { factory: CallTestContractFactory }],
});

cleanup = launched.cleanup;
contract = launched.contracts[0];
callTestContract = launched.contracts[1];
wallet = launched.wallets[0];
});

afterEach(() => {
cleanup();
});

bench('should successfully execute a contract read function', async () => {
const tx = await contract.functions.get_count().call();

const { value } = await tx.waitForResult();

expect(JSON.stringify(value)).toEqual(JSON.stringify(bn(0)));
});

bench('should successfully execute a contract multi call', async () => {
const tx = await contract
.multiCall([contract.functions.increment_counter(100), contract.functions.get_count()])
.call();

const { value } = await tx.waitForResult();

expect(JSON.stringify(value)).toEqual(JSON.stringify([bn(100), bn(100)]));
});

bench('should successfully write to a contract', async () => {
const tx = await contract.functions.increment_counter(100).call();
await tx.waitForResult();
});

bench('should successfully execute a contract mint', async () => {
const tx = await callTestContract.functions.mint_coins(TestAssetId.A.value, bn(100)).call();

await tx.waitForResult();
});

bench('should successfully execute a contract deploy', async () => {
const factory = new CounterContractFactory(wallet);
const { waitForResult } = await factory.deploy();
const { contract: deployedContract } = await waitForResult();

expect(deployedContract).toBeDefined();
});
});
134 changes: 134 additions & 0 deletions internal/benchmarks/src/cost-estimation.bench.ts
maschad marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* eslint-disable import/no-extraneous-dependencies */

import type { TransferParams, Provider } from 'fuels';
import { ScriptTransactionRequest, Wallet } from 'fuels';
import { launchTestNode, TestAssetId } from 'fuels/test-utils';
import { bench } from 'vitest';

import type { CallTestContract } from '../test/typegen/contracts';
import { CallTestContractFactory } from '../test/typegen/contracts';

/**
* @group node
* @group browser
*/
describe('Cost Estimation Benchmarks', () => {
let contract: CallTestContract;
let provider: Provider;
let cleanup: () => void;
beforeEach(async () => {
const launched = await launchTestNode({
contractsConfigs: [{ factory: CallTestContractFactory }],
});

cleanup = launched.cleanup;
contract = launched.contracts[0];
provider = contract.provider;
});

afterEach(() => {
cleanup();
});

bench(
'should successfully get transaction cost estimate for a single contract call',
async () => {
const cost = await contract.functions
.return_context_amount()
.callParams({
forward: [100, contract.provider.getBaseAssetId()],
})
.getTransactionCost();

expect(cost.minFee).toBeDefined();
expect(cost.maxFee).toBeDefined();
expect(cost.gasPrice).toBeDefined();
expect(cost.gasUsed).toBeDefined();
expect(cost.gasPrice).toBeDefined();
}
);

bench('should successfully get transaction cost estimate for multi contract calls', async () => {
const invocationScope = contract.multiCall([
contract.functions.return_context_amount().callParams({
forward: [100, contract.provider.getBaseAssetId()],
}),
contract.functions.return_context_amount().callParams({
forward: [200, TestAssetId.A.value],
}),
]);

const cost = await invocationScope.getTransactionCost();

expect(cost.minFee).toBeDefined();
expect(cost.maxFee).toBeDefined();
expect(cost.gasPrice).toBeDefined();
expect(cost.gasUsed).toBeDefined();
expect(cost.gasPrice).toBeDefined();
});

bench('should successfully get transaction cost estimate for a single transfer', async () => {
const request = new ScriptTransactionRequest({ gasLimit: 1000000 });

const recipient = Wallet.generate({
provider,
});
const sender = Wallet.fromPrivateKey(
'0x30bb0bc68f5d2ec3b523cee5a65503031b40679d9c72280cd8088c2cfbc34e38',
provider
);

request.addCoinOutput(recipient.address, 10, provider.getBaseAssetId());

const cost = await sender.getTransactionCost(request);

expect(cost.minFee).toBeDefined();
expect(cost.maxFee).toBeDefined();
expect(cost.gasPrice).toBeDefined();
expect(cost.gasUsed).toBeDefined();
expect(cost.gasPrice).toBeDefined();
});

bench('should successfully get transaction cost estimate for a batch transfer', async () => {
const receiver1 = Wallet.generate({ provider });
const receiver2 = Wallet.generate({ provider });
const receiver3 = Wallet.generate({ provider });

const amountToTransfer1 = 989;
const amountToTransfer2 = 699;
const amountToTransfer3 = 122;

const transferParams: TransferParams[] = [
{
destination: receiver1.address,
amount: amountToTransfer1,
assetId: provider.getBaseAssetId(),
},
{ destination: receiver2.address, amount: amountToTransfer2, assetId: TestAssetId.A.value },
{ destination: receiver3.address, amount: amountToTransfer3, assetId: TestAssetId.B.value },
];

const cost = await contract.functions
.sum(40, 50)
.addBatchTransfer(transferParams)
.getTransactionCost();

expect(cost.minFee).toBeDefined();
expect(cost.maxFee).toBeDefined();
expect(cost.gasPrice).toBeDefined();
expect(cost.gasUsed).toBeDefined();
expect(cost.gasPrice).toBeDefined();
});

it('should successfully get transaction cost estimate for a mint', async () => {
const subId = '0x4a778acfad1abc155a009dc976d2cf0db6197d3d360194d74b1fb92b96986b00';

const cost = await contract.functions.mint_coins(subId, 1_000).getTransactionCost();

expect(cost.minFee).toBeDefined();
expect(cost.maxFee).toBeDefined();
expect(cost.gasPrice).toBeDefined();
expect(cost.gasUsed).toBeDefined();
expect(cost.gasPrice).toBeDefined();
});
});
61 changes: 61 additions & 0 deletions internal/benchmarks/src/crypto.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* eslint-disable import/no-extraneous-dependencies */
import type { Keystore } from 'fuels';
import { bufferFromString, pbkdf2, computeHmac, encrypt, decrypt } from 'fuels';
import { bench } from 'vitest';

/**
* @group node
* @group browser
maschad marked this conversation as resolved.
Show resolved Hide resolved
*/
describe('crypto bench', () => {
bench(
'should correctly convert string to Uint8Array with base64 encoding in a node environment',
() => {
const string = 'aGVsbG8='; // "hello" in Base64
bufferFromString(string, 'base64');
}
);

bench('should compute the PBKDF2 hash', () => {
const passwordBuffer = bufferFromString(String('password123').normalize('NFKC'), 'utf-8');
const saltBuffer = bufferFromString(String('salt456').normalize('NFKC'), 'utf-8');
const iterations = 1000;
const keylen = 32;
const algo = 'sha256';

pbkdf2(passwordBuffer, saltBuffer, iterations, keylen, algo);
});

bench('should compute HMAC correctly', () => {
const key = '0x0102030405060708090a0b0c0d0e0f10';
const data = '0x11121314151617181920212223242526';
const sha256Length = 64;
const sha512Length = 128;
const prefix = '0x';

expect(computeHmac('sha256', key, data).length).toBe(sha256Length + prefix.length);
expect(computeHmac('sha512', key, data).length).toBe(sha512Length + prefix.length);
});

bench('Encrypt via aes-ctr', async () => {
const password = '0b540281-f87b-49ca-be37-2264c7f260f7';
const data = {
name: 'test',
};

const encryptedResult = await encrypt(password, data);
expect(encryptedResult.data).toBeTruthy();
expect(encryptedResult.iv).toBeTruthy();
expect(encryptedResult.salt).toBeTruthy();
});

bench('Decrypt via aes-ctr', async () => {
const password = '0b540281-f87b-49ca-be37-2264c7f260f7';
const encryptedResult: Keystore = {
data: 'vj1/JyHR+NiIaWXTpl5T',
iv: '0/lqnRVK5HE/5b1cQAHfqg==',
salt: 'nHdHXW2EmOEagAH2UUDYMRNhd7LJ5XLIcZoVQZMPSlU=',
};
await decrypt(password, encryptedResult);
});
});
Loading
Loading