Skip to content

Commit bc22f58

Browse files
committed
test: add benchmark test
1 parent 6f392fa commit bc22f58

File tree

21 files changed

+618
-25
lines changed

21 files changed

+618
-25
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: V2 Benchmarks
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
branches:
7+
- develop
8+
paths:
9+
- 'packages/v2/**'
10+
- '.github/workflows/v2-benchmark-tests.yml'
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
bench:
18+
if: github.ref == 'refs/heads/refactor/core' || github.head_ref == 'refactor/core'
19+
runs-on: ubuntu-latest
20+
name: V2 Benchmarks
21+
env:
22+
CI: 1
23+
TESTCONTAINERS_REUSE_ENABLE: 'false'
24+
25+
strategy:
26+
matrix:
27+
node-version: [22.18.0]
28+
29+
steps:
30+
- uses: actions/checkout@v4
31+
32+
- name: Use Node.js ${{ matrix.node-version }}
33+
uses: actions/setup-node@v4
34+
with:
35+
node-version: ${{ matrix.node-version }}
36+
37+
- name: 📥 Monorepo install
38+
uses: ./.github/actions/pnpm-install
39+
40+
- name: 🧪 Run v2 benchmarks
41+
run: |
42+
pnpm -C packages/v2/benchmark-node bench

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ jobs:
3535

3636
- name: 🧪 Run v2 vitest
3737
run: |
38-
pnpm -F "@teable/v2-*" -r --if-present --workspace-concurrency=1 test-unit
38+
pnpm -F "@teable/v2-*" -r --if-present --workspace-concurrency=1 test-unit-cover

packages/v2/adapter-postgres-state/src/db/schema.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable sonarjs/no-duplicate-string */
21
import type { V1TeableDatabase } from '@teable/v2-postgres-schema';
32
import type { Kysely } from 'kysely';
43
import { sql } from 'kysely';
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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "@teable/v2-benchmark-node",
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-node.eslintcache",
18+
"typecheck": "tsc --project ./tsconfig.json --noEmit",
19+
"bench": "vitest bench --run",
20+
"bench:watch": "vitest bench",
21+
"fix-all-files": "eslint . --ext .ts,.js,.mjs,.cjs,.mts,.cts --fix"
22+
},
23+
"dependencies": {
24+
"@hono/node-server": "1.13.5",
25+
"@teable/v2-container-node-test": "workspace:*",
26+
"@teable/v2-contract-http-client": "workspace:*",
27+
"@teable/v2-contract-http-express": "workspace:*",
28+
"@teable/v2-contract-http-fastify": "workspace:*",
29+
"@teable/v2-contract-http-hono": "workspace:*",
30+
"@teable/v2-core": "workspace:*",
31+
"express": "4.21.1",
32+
"fastify": "4.29.1",
33+
"hono": "4.11.1"
34+
},
35+
"devDependencies": {
36+
"@teable/v2-tsdown-config": "workspace:*",
37+
"@teable/eslint-config-bases": "workspace:^",
38+
"@types/express": "4.17.21",
39+
"@types/node": "22.18.0",
40+
"@vitest/coverage-v8": "4.0.16",
41+
"eslint": "8.57.0",
42+
"prettier": "3.2.5",
43+
"rimraf": "5.0.5",
44+
"tsdown": "0.18.1",
45+
"typescript": "5.4.3",
46+
"vite-tsconfig-paths": "4.3.2",
47+
"vitest": "4.0.16"
48+
}
49+
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import type { Server } from 'node:http';
2+
import type { AddressInfo } from 'node:net';
3+
import { serve } from '@hono/node-server';
4+
import { createV2NodeTestContainer } from '@teable/v2-container-node-test';
5+
import type { ICreateTableRequestDto } from '@teable/v2-contract-http';
6+
import { createV2HttpClient } from '@teable/v2-contract-http-client';
7+
import { createV2ExpressRouter } from '@teable/v2-contract-http-express';
8+
import { createV2FastifyPlugin } from '@teable/v2-contract-http-fastify';
9+
import { createV2HonoApp } from '@teable/v2-contract-http-hono';
10+
import { NoopLogger, v2CoreTokens } from '@teable/v2-core';
11+
import type { DependencyContainer } from '@teable/v2-di';
12+
import express from 'express';
13+
import fastify from 'fastify';
14+
import { afterAll, beforeAll, bench, describe } from 'vitest';
15+
16+
const benchOptions = {
17+
iterations: 0,
18+
warmupIterations: 0,
19+
time: 5000,
20+
warmupTime: 1000,
21+
throws: true,
22+
};
23+
24+
const createTableName = (framework: string, scenario: string): string => {
25+
const random = Math.random().toString(36).slice(2, 8);
26+
return `Bench_${framework}_${scenario}_${Date.now()}_${random}`;
27+
};
28+
29+
const createSimpleFields = (): ICreateTableRequestDto['fields'] => [
30+
{ type: 'singleLineText', name: 'Name' },
31+
{ type: 'number', name: 'Amount', options: { defaultValue: 1 } },
32+
{ type: 'checkbox', name: 'Done', options: { defaultValue: false } },
33+
];
34+
35+
const createAllBaseFields = (): ICreateTableRequestDto['fields'] => [
36+
{ type: 'singleLineText', name: 'Name' },
37+
{ type: 'longText', name: 'Description', options: { defaultValue: 'Notes' } },
38+
{ type: 'number', name: 'Amount', options: { defaultValue: 10 } },
39+
{ type: 'rating', name: 'Priority', max: 5, options: { icon: 'star', color: 'yellowBright' } },
40+
{ type: 'singleSelect', name: 'Status', options: ['Todo', 'Done'] },
41+
{ type: 'multipleSelect', name: 'Tags', options: ['Frontend', 'Backend'] },
42+
{ type: 'checkbox', name: 'Done', options: { defaultValue: true } },
43+
{ type: 'attachment', name: 'Files' },
44+
{ type: 'date', name: 'Due Date' },
45+
{ type: 'user', name: 'Owner', options: { isMultiple: false } },
46+
{ type: 'button', name: 'Action', options: { label: 'Run' } },
47+
];
48+
49+
const createTextColumns = (count: number): ICreateTableRequestDto['fields'] =>
50+
Array.from({ length: count }, (_, index) => ({
51+
type: 'singleLineText',
52+
name: `Column ${index + 1}`,
53+
}));
54+
55+
type IBenchTarget = {
56+
name: string;
57+
client: ReturnType<typeof createV2HttpClient>;
58+
close: () => Promise<void>;
59+
};
60+
61+
describe('CreateTable benchmarks', () => {
62+
let servers: IBenchTarget[] = [];
63+
let dispose: (() => Promise<void>) | undefined;
64+
let baseId: string;
65+
let setupPromise: Promise<void> | undefined;
66+
67+
const setupExpress = async (container: DependencyContainer): Promise<IBenchTarget> => {
68+
const app = express();
69+
app.use(
70+
createV2ExpressRouter({
71+
createContainer: () => container,
72+
})
73+
);
74+
75+
const server = await new Promise<Server>((resolve) => {
76+
const s = app.listen(0, '127.0.0.1', () => resolve(s));
77+
});
78+
79+
const address = server.address() as AddressInfo;
80+
const baseUrl = `http://127.0.0.1:${address.port}`;
81+
const client = createV2HttpClient({ baseUrl });
82+
83+
return {
84+
name: 'express',
85+
client,
86+
close: async () => {
87+
await new Promise<void>((resolve) => server.close(() => resolve()));
88+
},
89+
};
90+
};
91+
92+
const setupFastify = async (container: DependencyContainer): Promise<IBenchTarget> => {
93+
const app = fastify();
94+
await app.register(
95+
createV2FastifyPlugin({
96+
createContainer: () => container,
97+
})
98+
);
99+
await app.listen({ port: 0, host: '127.0.0.1' });
100+
101+
const address = app.server.address() as AddressInfo;
102+
const baseUrl = `http://127.0.0.1:${address.port}`;
103+
const client = createV2HttpClient({ baseUrl });
104+
105+
return {
106+
name: 'fastify',
107+
client,
108+
close: async () => {
109+
await app.close();
110+
},
111+
};
112+
};
113+
114+
const setupHono = async (container: DependencyContainer): Promise<IBenchTarget> => {
115+
const app = createV2HonoApp({
116+
createContainer: () => container,
117+
});
118+
const server = serve({ fetch: app.fetch, port: 0, hostname: '127.0.0.1' });
119+
await new Promise<void>((resolve) => server.once('listening', () => resolve()));
120+
const address = server.address() as AddressInfo;
121+
const baseUrl = `http://127.0.0.1:${address.port}`;
122+
const client = createV2HttpClient({ baseUrl });
123+
124+
return {
125+
name: 'hono',
126+
client,
127+
close: async () => {
128+
await new Promise<void>((resolve) => server.close(() => resolve()));
129+
},
130+
};
131+
};
132+
133+
const setup = async () => {
134+
const testContainer = await createV2NodeTestContainer();
135+
testContainer.container.registerInstance(v2CoreTokens.logger, new NoopLogger());
136+
dispose = testContainer.dispose;
137+
baseId = testContainer.baseId.toString();
138+
139+
const expressTarget = await setupExpress(testContainer.container);
140+
const fastifyTarget = await setupFastify(testContainer.container);
141+
const honoTarget = await setupHono(testContainer.container);
142+
143+
servers = [expressTarget, fastifyTarget, honoTarget];
144+
};
145+
146+
const ensureSetup = async () => {
147+
if (!setupPromise) {
148+
setupPromise = setup();
149+
}
150+
await setupPromise;
151+
};
152+
153+
beforeAll(async () => {
154+
await ensureSetup();
155+
});
156+
157+
afterAll(async () => {
158+
for (const server of servers) {
159+
await server.close();
160+
}
161+
if (dispose) await dispose();
162+
});
163+
164+
const runCreateTable = async (
165+
target: IBenchTarget,
166+
label: string,
167+
fields: ICreateTableRequestDto['fields']
168+
) => {
169+
if (!baseId) throw new Error('BaseId is missing');
170+
171+
const input = {
172+
baseId,
173+
name: createTableName(target.name, label),
174+
fields,
175+
};
176+
177+
try {
178+
const response = await target.client.tables.create(input);
179+
if (!response.ok) {
180+
throw new Error('Create table failed');
181+
}
182+
} catch (error) {
183+
const message = error instanceof Error ? error.message : 'Create table failed';
184+
throw new Error(message);
185+
}
186+
};
187+
188+
const simpleFields = createSimpleFields();
189+
const baseFields = createAllBaseFields();
190+
const fields200 = createTextColumns(200);
191+
const fields1000 = createTextColumns(1000);
192+
193+
const expressFramework = 'express';
194+
const fastifyFramework = 'fastify';
195+
const honoFramework = 'hono';
196+
const simpleScenario = 'simple';
197+
const baseScenario = 'base';
198+
const columns200Scenario = '200';
199+
const columns1000Scenario = '1000';
200+
const simpleLabel = '3 columns';
201+
const baseLabel = 'all base fields';
202+
const columns200Label = '200 columns';
203+
const columns1000Label = '1000 columns';
204+
205+
const getTarget = (name: string): IBenchTarget => {
206+
const target = servers.find((server) => server.name === name);
207+
if (!target) {
208+
throw new Error(`${name} server is not initialized`);
209+
}
210+
return target;
211+
};
212+
213+
const benchCreateTable = (
214+
framework: string,
215+
label: string,
216+
scenario: string,
217+
fields: ICreateTableRequestDto['fields']
218+
) => {
219+
bench(
220+
`${framework}: create table: ${label}`,
221+
async () => {
222+
await ensureSetup();
223+
await runCreateTable(getTarget(framework), scenario, fields);
224+
},
225+
benchOptions
226+
);
227+
};
228+
229+
benchCreateTable(expressFramework, simpleLabel, simpleScenario, simpleFields);
230+
benchCreateTable(expressFramework, baseLabel, baseScenario, baseFields);
231+
benchCreateTable(expressFramework, columns200Label, columns200Scenario, fields200);
232+
benchCreateTable(expressFramework, columns1000Label, columns1000Scenario, fields1000);
233+
234+
benchCreateTable(fastifyFramework, simpleLabel, simpleScenario, simpleFields);
235+
benchCreateTable(fastifyFramework, baseLabel, baseScenario, baseFields);
236+
benchCreateTable(fastifyFramework, columns200Label, columns200Scenario, fields200);
237+
benchCreateTable(fastifyFramework, columns1000Label, columns1000Scenario, fields1000);
238+
239+
benchCreateTable(honoFramework, simpleLabel, simpleScenario, simpleFields);
240+
benchCreateTable(honoFramework, baseLabel, baseScenario, baseFields);
241+
benchCreateTable(honoFramework, columns200Label, columns200Scenario, fields200);
242+
benchCreateTable(honoFramework, columns1000Label, columns1000Scenario, fields1000);
243+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"$schema": "https://json.schemastore.org/tsconfig",
3+
"extends": "./tsconfig.json",
4+
"compilerOptions": {
5+
"rootDir": "src",
6+
"paths": {}
7+
},
8+
"exclude": ["dist", "**/__tests__/**", "**/*.spec.ts", "**/*.test.ts", "**/*.bench.ts"],
9+
"include": ["src"]
10+
}

0 commit comments

Comments
 (0)