Skip to content

Commit

Permalink
ci: add initial benchmark infrastructure (#3084)
Browse files Browse the repository at this point in the history
  • Loading branch information
maschad committed Sep 12, 2024
1 parent 5efe23d commit f76afd2
Show file tree
Hide file tree
Showing 24 changed files with 3,793 additions and 66 deletions.
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
on:
pull_request:
push:
branches:
- master

jobs:
benchmarks:
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
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
*/
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

0 comments on commit f76afd2

Please sign in to comment.