From cd6e0d6e92b1961092be3b8865dcd786771838fe Mon Sep 17 00:00:00 2001 From: luo jiyin Date: Tue, 8 Sep 2026 22:37:09 +0800 Subject: [PATCH] perf(sidebar): add large schema benchmark --- .../lib/sidebar/sidebarLargeSchema.bench.ts | 189 ++++++++++++++++++ package.json | 1 + 2 files changed, 190 insertions(+) create mode 100644 apps/desktop/src/lib/sidebar/sidebarLargeSchema.bench.ts diff --git a/apps/desktop/src/lib/sidebar/sidebarLargeSchema.bench.ts b/apps/desktop/src/lib/sidebar/sidebarLargeSchema.bench.ts new file mode 100644 index 0000000000..0e85127294 --- /dev/null +++ b/apps/desktop/src/lib/sidebar/sidebarLargeSchema.bench.ts @@ -0,0 +1,189 @@ +import { bench, describe } from "vitest"; + +import { filterSidebarTree } from "@/lib/sidebar/sidebarSearchTree"; +import { buildTableTreeNodes } from "@/lib/table/tableTree"; +import type { TableInfo, TreeNode } from "@/types/database"; + +const CONNECTION_ID = "benchmark-connection"; +const DATABASE = "benchmark"; +const SCHEMA = "public"; +const EMPTY_COLLAPSED_IDS = new Set(); +const TABLE_COUNTS = [1_000, 10_000, 50_000] as const; +const STRUCTURE_TABLE_COUNT = 10_000; + +function tableName(index: number): string { + return `customer_orders_${index.toString().padStart(5, "0")}`; +} + +function generateTables(count: number, resolveName: (index: number) => string = tableName): TableInfo[] { + return Array.from({ length: count }, (_, index) => ({ + name: resolveName(index), + table_type: "BASE TABLE", + comment: index % 10 === 0 ? `benchmark table ${index}` : null, + parent_schema: null, + parent_name: null, + })); +} + +function buildTableNodes(tables: TableInfo[], schema = SCHEMA, nodeId = `${CONNECTION_ID}:${schema}:__tables`): TreeNode[] { + return buildTableTreeNodes({ + nodeId, + connectionId: CONNECTION_ID, + database: DATABASE, + schema, + tables, + }); +} + +function tableGroup(schema: string, tableNodes: TreeNode[]): TreeNode { + return { + id: `${CONNECTION_ID}:${schema}:__tables`, + label: "Tables", + type: "group-tables", + connectionId: CONNECTION_ID, + database: DATABASE, + schema, + isExpanded: true, + children: tableNodes, + }; +} + +function buildSearchTree(tableNodes: TreeNode[]): TreeNode[] { + return buildMultiSchemaSearchTree([{ schema: SCHEMA, tableNodes }]); +} + +function buildMultiSchemaSearchTree(schemas: Array<{ schema: string; tableNodes: TreeNode[] }>): TreeNode[] { + return [ + { + id: CONNECTION_ID, + label: "Benchmark", + type: "connection", + connectionId: CONNECTION_ID, + isExpanded: true, + children: schemas.map(({ schema, tableNodes }) => ({ + id: `${CONNECTION_ID}:${schema}`, + label: schema, + type: "schema" as const, + connectionId: CONNECTION_ID, + database: DATABASE, + schema, + isExpanded: true, + children: [tableGroup(schema, tableNodes)], + })), + }, + ]; +} + +function visibleTableCount(filtered: TreeNode[]): number { + const connection = filtered[0]; + if (!connection) return 0; + return (connection.children ?? []).reduce((count, schema) => count + (schema.children?.[0]?.children?.length ?? 0), 0); +} + +function seededShuffle(items: readonly T[], seed: number): T[] { + const shuffled = [...items]; + let state = seed >>> 0; + const random = () => { + state = (Math.imul(state, 1_664_525) + 1_013_904_223) >>> 0; + return state / 0x1_0000_0000; + }; + + for (let index = shuffled.length - 1; index > 0; index -= 1) { + const target = Math.floor(random() * (index + 1)); + [shuffled[index], shuffled[target]] = [shuffled[target], shuffled[index]]; + } + return shuffled; +} + +for (const tableCount of TABLE_COUNTS) { + const tables = generateTables(tableCount); + const tableNodes = buildTableNodes(tables); + const searchTree = buildSearchTree(tableNodes); + const queries = [ + { name: "no match", query: "missing_table", expectedMatches: 0 }, + { name: "all tables", query: "customer", expectedMatches: tableCount }, + { name: "first table", query: tableName(0), expectedMatches: 1 }, + { name: "middle table", query: tableName(Math.floor(tableCount / 2)), expectedMatches: 1 }, + { name: "last table", query: tableName(tableCount - 1), expectedMatches: 1 }, + { name: "table comment", query: `benchmark table ${tableCount - 10}`, expectedMatches: 1 }, + ] as const; + + describe(`large sidebar schema: ${tableCount.toLocaleString()} tables`, () => { + bench("build table tree nodes", () => { + const nodes = buildTableNodes(tables); + if (nodes.length !== tableCount) throw new Error(`expected ${tableCount} table nodes`); + }); + + for (const { name, query, expectedMatches } of queries) { + bench(`filter sidebar tree: ${name}`, () => { + const filtered = filterSidebarTree(searchTree, query, EMPTY_COLLAPSED_IDS); + const matches = visibleTableCount(filtered); + if (matches !== expectedMatches) throw new Error(`expected ${expectedMatches} table matches, received ${matches}`); + }); + } + }); +} + +describe(`large sidebar schema data shapes: ${STRUCTURE_TABLE_COUNT.toLocaleString()} tables`, () => { + const sortedTables = generateTables(STRUCTURE_TABLE_COUNT); + const reversedTables = [...sortedTables].reverse(); + const shuffledTables = seededShuffle(sortedTables, 0x5eed_1234); + + bench("build table tree nodes from reverse order", () => { + const nodes = buildTableNodes(reversedTables); + if (nodes.length !== STRUCTURE_TABLE_COUNT) throw new Error(`expected ${STRUCTURE_TABLE_COUNT} table nodes`); + }); + + bench("build table tree nodes from seeded random order", () => { + const nodes = buildTableNodes(shuffledTables); + if (nodes.length !== STRUCTURE_TABLE_COUNT) throw new Error(`expected ${STRUCTURE_TABLE_COUNT} table nodes`); + }); + + for (const nameLength of [128, 255] as const) { + const suffixLength = nameLength - "long_table_00000_".length; + const longNameTables = generateTables(STRUCTURE_TABLE_COUNT, (index) => `long_table_${index.toString().padStart(5, "0")}_${"x".repeat(suffixLength)}`); + const longNameTree = buildSearchTree(buildTableNodes(longNameTables)); + const query = longNameTables[longNameTables.length - 1]?.name ?? ""; + + bench(`filter sidebar tree with ${nameLength}-character names`, () => { + const filtered = filterSidebarTree(longNameTree, query, EMPTY_COLLAPSED_IDS); + if (visibleTableCount(filtered) !== 1) throw new Error("expected one long-name table match"); + }); + } + + const unicodeTables = generateTables(STRUCTURE_TABLE_COUNT, (index) => `客户订单_日本語_📦_${index.toString().padStart(5, "0")}`); + const unicodeTree = buildSearchTree(buildTableNodes(unicodeTables)); + + bench("filter sidebar tree with Unicode names", () => { + const filtered = filterSidebarTree(unicodeTree, unicodeTables[unicodeTables.length - 1]?.name ?? "", EMPTY_COLLAPSED_IDS); + if (visibleTableCount(filtered) !== 1) throw new Error("expected one Unicode table match"); + }); + + const partitionTables = generateTables(STRUCTURE_TABLE_COUNT).map((table, index, tables) => { + if (index % 10 !== 9) return table; + return { ...table, parent_schema: SCHEMA, parent_name: tables[index - 1].name }; + }); + + bench("build table tree nodes with ten percent partitions", () => { + const nodes = buildTableNodes(partitionTables); + const expectedRoots = STRUCTURE_TABLE_COUNT - STRUCTURE_TABLE_COUNT / 10; + if (nodes.length !== expectedRoots) throw new Error(`expected ${expectedRoots} root table nodes`); + }); +}); + +describe("large sidebar with ten schemas", () => { + const schemaCount = 10; + const tablesPerSchema = 1_000; + const schemas = Array.from({ length: schemaCount }, (_, schemaIndex) => { + const schema = `schema_${schemaIndex.toString().padStart(2, "0")}`; + const tables = generateTables(tablesPerSchema, (tableIndex) => `${schema}_${tableName(tableIndex)}`); + return { schema, tableNodes: buildTableNodes(tables, schema) }; + }); + const searchTree = buildMultiSchemaSearchTree(schemas); + const tailQuery = `schema_09_${tableName(tablesPerSchema - 1)}`; + + bench("filter across ten schemas for a tail table", () => { + const filtered = filterSidebarTree(searchTree, tailQuery, EMPTY_COLLAPSED_IDS); + if (visibleTableCount(filtered) !== 1) throw new Error("expected one cross-schema table match"); + }); +}); diff --git a/package.json b/package.json index 37c2800be1..3d598c3eb8 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "test:ai-html-canary": "node scripts/ai-html-runtime-canary.mjs", "bench:redis-key-search": "node scripts/bench/redis-key-search.mjs", "bench:mongodb-count": "node scripts/bench/mongodb-count.mjs", + "bench:sidebar-large-schema": "vitest bench apps/desktop/src/lib/sidebar/sidebarLargeSchema.bench.ts", "bench:table-import": "cargo run -p dbx-core --no-default-features --example table_import_bench --release --", "lint": "oxlint --vue-plugin apps/desktop/src", "fmt": "oxfmt \"apps/desktop/src/**/*.{ts,vue}\"",