|
| 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 | +}); |
0 commit comments