Skip to content

Commit fe5003c

Browse files
committed
test: benchmark test bun
1 parent 5642f24 commit fe5003c

32 files changed

+1307
-0
lines changed

.github/workflows/v2-benchmark-tests.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,35 @@ jobs:
4040
- name: 🧪 Run v2 benchmarks
4141
run: |
4242
pnpm -C packages/v2/benchmark-node bench
43+
44+
bench-bun:
45+
if: github.ref == 'refs/heads/refactor/core' || github.head_ref == 'refactor/core'
46+
runs-on: ubuntu-latest
47+
name: V2 Benchmarks (Bun)
48+
env:
49+
CI: 1
50+
TESTCONTAINERS_REUSE_ENABLE: 'false'
51+
52+
strategy:
53+
matrix:
54+
node-version: [22.18.0]
55+
56+
steps:
57+
- uses: actions/checkout@v4
58+
59+
- name: Use Node.js ${{ matrix.node-version }}
60+
uses: actions/setup-node@v4
61+
with:
62+
node-version: ${{ matrix.node-version }}
63+
64+
- name: Use Bun
65+
uses: oven-sh/setup-bun@v1
66+
with:
67+
bun-version: 'latest'
68+
69+
- name: 📥 Monorepo install
70+
uses: ./.github/actions/pnpm-install
71+
72+
- name: 🧪 Run v2 bun benchmarks
73+
run: |
74+
pnpm -C packages/v2/benchmark-bun bench
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Specific eslint rules for this workspace, learn how to compose
3+
* @link https://github.com/teableio/teable/tree/main/packages/eslint-config-bases
4+
*/
5+
require('@teable/eslint-config-bases/patch/modern-module-resolution');
6+
7+
const { getDefaultIgnorePatterns } = require('@teable/eslint-config-bases/helpers');
8+
9+
module.exports = {
10+
root: true,
11+
parser: '@typescript-eslint/parser',
12+
parserOptions: {
13+
tsconfigRootDir: __dirname,
14+
project: 'tsconfig.eslint.json',
15+
},
16+
ignorePatterns: [...getDefaultIgnorePatterns()],
17+
extends: [
18+
'@teable/eslint-config-bases/typescript',
19+
'@teable/eslint-config-bases/sonar',
20+
'@teable/eslint-config-bases/regexp',
21+
'@teable/eslint-config-bases/jest',
22+
// Apply prettier and disable incompatible rules
23+
'@teable/eslint-config-bases/prettier-plugin',
24+
],
25+
rules: {},
26+
overrides: [],
27+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# build
2+
/dist
3+
4+
# dependencies
5+
node_modules
6+
7+
# testing
8+
/coverage
9+
10+
# misc
11+
.DS_Store
12+
*.pem
13+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "@teable/v2-benchmark-bun",
3+
"version": "0.0.0",
4+
"private": true,
5+
"license": "MIT",
6+
"sideEffects": false,
7+
"main": "dist/index.js",
8+
"module": "dist/index.js",
9+
"types": "dist/index.d.ts",
10+
"files": [
11+
"dist"
12+
],
13+
"scripts": {
14+
"build": "tsdown --tsconfig tsconfig.build.json",
15+
"dev": "tsdown --tsconfig tsconfig.build.json --watch",
16+
"clean": "rimraf ./dist ./coverage ./tsconfig.tsbuildinfo ./tsconfig.build.tsbuildinfo ./.eslintcache",
17+
"lint": "eslint . --ext .ts,.js,.mjs,.cjs,.mts,.cts --cache --cache-location ../../../.cache/eslint/v2-benchmark-bun.eslintcache",
18+
"typecheck": "tsc --project ./tsconfig.json --noEmit",
19+
"bench": "bun run --bun src/run.ts",
20+
"fix-all-files": "eslint . --ext .ts,.js,.mjs,.cjs,.mts,.cts --fix"
21+
},
22+
"dependencies": {
23+
"@orpc/client": "1.13.0",
24+
"@orpc/contract": "1.13.0",
25+
"@orpc/server": "1.13.0",
26+
"@teable/v2-container-bun-test": "workspace:*",
27+
"@teable/v2-contract-http-implementation": "workspace:*",
28+
"@teable/v2-contract-http": "workspace:*",
29+
"@teable/v2-core": "workspace:*",
30+
"@teable/v2-db-postgres": "workspace:*",
31+
"@teable/v2-di": "workspace:*",
32+
"@teable/v2-postgres-schema": "workspace:*",
33+
"kysely": "0.28.9",
34+
"tinybench": "2.9.0"
35+
},
36+
"devDependencies": {
37+
"@teable/v2-tsdown-config": "workspace:*",
38+
"@teable/eslint-config-bases": "workspace:^",
39+
"@types/node": "22.18.0",
40+
"eslint": "8.57.0",
41+
"prettier": "3.2.5",
42+
"rimraf": "5.0.5",
43+
"tsdown": "0.18.1",
44+
"typescript": "5.4.3",
45+
"vite-tsconfig-paths": "4.3.2"
46+
}
47+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { onError } from '@orpc/server';
2+
import { RPCHandler } from '@orpc/server/fetch';
3+
import { CORSPlugin } from '@orpc/server/plugins';
4+
import { createV2BunTestContainer } from '@teable/v2-container-bun-test';
5+
import { createV2OrpcRouter } from '@teable/v2-contract-http-implementation';
6+
import { NoopLogger, v2CoreTokens } from '@teable/v2-core';
7+
import type { DependencyContainer } from '@teable/v2-di';
8+
9+
import { createV2RpcClient } from './rpc-client';
10+
11+
type IBunServer = {
12+
port: number;
13+
stop: (closeConnections?: boolean) => void;
14+
};
15+
16+
type IBunServeOptions = {
17+
port?: number;
18+
hostname?: string;
19+
fetch: (request: Request) => Response | Promise<Response>;
20+
};
21+
22+
type IBunRuntime = {
23+
serve: (options: IBunServeOptions) => IBunServer;
24+
};
25+
26+
const getBunRuntime = (): IBunRuntime => {
27+
const bun = (globalThis as Record<string, unknown>)['Bun'] as IBunRuntime | undefined;
28+
if (!bun) {
29+
throw new Error('Bun runtime is required for v2 bun benchmarks.');
30+
}
31+
return bun;
32+
};
33+
34+
const startBunServer = (container: DependencyContainer): IBunServer => {
35+
const orpcRouter = createV2OrpcRouter({
36+
createContainer: () => container,
37+
});
38+
const handler = new RPCHandler(orpcRouter, {
39+
plugins: [new CORSPlugin()],
40+
interceptors: [
41+
onError((error) => {
42+
console.error(error);
43+
}),
44+
],
45+
});
46+
47+
const bun = getBunRuntime();
48+
49+
return bun.serve({
50+
port: 0,
51+
hostname: '127.0.0.1',
52+
async fetch(request: Request) {
53+
const { matched, response } = await handler.handle(request, {
54+
prefix: '/rpc',
55+
context: {},
56+
});
57+
58+
if (matched) {
59+
return response;
60+
}
61+
62+
return new Response('Not found', { status: 404 });
63+
},
64+
});
65+
};
66+
67+
export type IBunBenchContext = {
68+
client: ReturnType<typeof createV2RpcClient>;
69+
baseId: string;
70+
dispose: () => Promise<void>;
71+
};
72+
73+
export const createBunBenchContext = async (): Promise<IBunBenchContext> => {
74+
console.log('[bun-bench] starting test container');
75+
const testContainer = await createV2BunTestContainer();
76+
testContainer.container.registerInstance(v2CoreTokens.logger, new NoopLogger());
77+
78+
console.log('[bun-bench] test container ready');
79+
const server = startBunServer(testContainer.container);
80+
const baseUrl = `http://127.0.0.1:${server.port}/rpc`;
81+
const client = createV2RpcClient({ baseUrl });
82+
console.log(`[bun-bench] bun server ready at ${baseUrl}`);
83+
84+
return {
85+
client,
86+
baseId: testContainer.baseId.toString(),
87+
dispose: async () => {
88+
console.log('[bun-bench] shutting down');
89+
try {
90+
server.stop();
91+
} finally {
92+
await testContainer.dispose();
93+
}
94+
},
95+
};
96+
};
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { ICreateTableRequestDto } from '@teable/v2-contract-http';
2+
import { Bench } from 'tinybench';
3+
4+
import { createBunBenchContext } from './bench-context';
5+
6+
const benchOptions = {
7+
iterations: 0,
8+
warmupIterations: 0,
9+
time: 5000,
10+
warmupTime: 1000,
11+
throws: true,
12+
};
13+
14+
const createTableName = (scenario: string): string => {
15+
const random = Math.random().toString(36).slice(2, 8);
16+
return `Bench_Bun_${scenario}_${Date.now()}_${random}`;
17+
};
18+
19+
const createSimpleFields = (): ICreateTableRequestDto['fields'] => [
20+
{ type: 'singleLineText', name: 'Name' },
21+
{ type: 'number', name: 'Amount', options: { defaultValue: 1 } },
22+
{ type: 'checkbox', name: 'Done', options: { defaultValue: false } },
23+
];
24+
25+
const createAllBaseFields = (): ICreateTableRequestDto['fields'] => [
26+
{ type: 'singleLineText', name: 'Name' },
27+
{ type: 'longText', name: 'Description', options: { defaultValue: 'Notes' } },
28+
{ type: 'number', name: 'Amount', options: { defaultValue: 10 } },
29+
{ type: 'rating', name: 'Priority', max: 5, options: { icon: 'star', color: 'yellowBright' } },
30+
{ type: 'singleSelect', name: 'Status', options: ['Todo', 'Done'] },
31+
{ type: 'multipleSelect', name: 'Tags', options: ['Frontend', 'Backend'] },
32+
{ type: 'checkbox', name: 'Done', options: { defaultValue: true } },
33+
{ type: 'attachment', name: 'Files' },
34+
{ type: 'date', name: 'Due Date' },
35+
{ type: 'user', name: 'Owner', options: { isMultiple: false } },
36+
{ type: 'button', name: 'Action', options: { label: 'Run' } },
37+
];
38+
39+
const createTextColumns = (count: number): ICreateTableRequestDto['fields'] =>
40+
Array.from({ length: count }, (_, index) => ({
41+
type: 'singleLineText',
42+
name: `Column ${index + 1}`,
43+
}));
44+
45+
export const runCreateTableBench = async (): Promise<void> => {
46+
const context = await createBunBenchContext();
47+
48+
try {
49+
const bench = new Bench(benchOptions);
50+
const simpleFields = createSimpleFields();
51+
const baseFields = createAllBaseFields();
52+
const fields200 = createTextColumns(200);
53+
const fields1000 = createTextColumns(1000);
54+
55+
const runCreateTable = async (scenario: string, fields: ICreateTableRequestDto['fields']) => {
56+
const input = {
57+
baseId: context.baseId,
58+
name: createTableName(scenario),
59+
fields,
60+
};
61+
62+
const response = await context.client.tables.create(input);
63+
if (!response.ok) {
64+
throw new Error('Create table failed');
65+
}
66+
};
67+
68+
bench.add('bun: create table: 3 columns', async () => {
69+
await runCreateTable('simple', simpleFields);
70+
});
71+
72+
bench.add('bun: create table: all base fields', async () => {
73+
await runCreateTable('base', baseFields);
74+
});
75+
76+
bench.add('bun: create table: 200 columns', async () => {
77+
await runCreateTable('200', fields200);
78+
});
79+
80+
bench.add('bun: create table: 1000 columns', async () => {
81+
await runCreateTable('1000', fields1000);
82+
});
83+
84+
console.log('[bun-bench] running create table benchmarks');
85+
await bench.run();
86+
87+
console.log('CreateTable benchmarks (bun)');
88+
console.table(bench.table());
89+
} finally {
90+
await context.dispose();
91+
}
92+
};

0 commit comments

Comments
 (0)