From e1fc28ab861d0ebce6c02bf2fb8a98b5de8263a9 Mon Sep 17 00:00:00 2001 From: t8y2 Date: Tue, 22 Sep 2026 17:25:22 +0800 Subject: [PATCH] fix(cache): avoid repeated copies when encoding query results Refs: #9755 --- .../tabs/tabResultCache.performance.spec.ts | 110 ++++++++++++++++++ apps/desktop/src/lib/tabs/tabResultCache.ts | 44 ++++--- 2 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/tabs/tabResultCache.performance.spec.ts diff --git a/apps/desktop/src/lib/__tests__/tabs/tabResultCache.performance.spec.ts b/apps/desktop/src/lib/__tests__/tabs/tabResultCache.performance.spec.ts new file mode 100644 index 0000000000..23258ff96b --- /dev/null +++ b/apps/desktop/src/lib/__tests__/tabs/tabResultCache.performance.spec.ts @@ -0,0 +1,110 @@ +import { decode } from "@msgpack/msgpack"; +import { describe, expect, it, vi } from "vitest"; +import { decodeTabResultSnapshot, encodeTabResultSnapshot } from "@/lib/tabs/tabResultCache"; +import type { QueryResult } from "@/types/database"; + +function createResult(rowCount: number): QueryResult { + return { + columns: ["id", "name", "enabled"], + rows: Array.from({ length: rowCount }, (_, index) => [index, `row-${index}`, index % 2 === 0]), + affected_rows: 0, + execution_time_ms: 1, + }; +} + +describe("result cache serialization work", () => { + it.each([ + { columns: [], rows: [] }, + { columns: [], rows: [[]] }, + { columns: ["first", "second"], rows: [[1], []] }, + ])("preserves empty and ragged result shapes: %j", ({ columns, rows }) => { + const encoded = encodeTabResultSnapshot({ result: { columns, rows, affected_rows: 0, execution_time_ms: 0 }, cachedAt: 1 }); + expect(decodeTabResultSnapshot(encoded)?.result?.rows).toEqual(rows.map((row) => columns.map((_, columnIndex) => row[columnIndex] ?? null))); + }); + + it("does not repeatedly copy column vectors while cleaning optional metadata", () => { + const rowCount = 1000; + const result = createResult(rowCount); + let fullLengthMaps = 0; + const originalMap = Array.prototype.map; + const mapSpy = vi.spyOn(Array.prototype, "map").mockImplementation(function (this: unknown[], callback, thisArg) { + if (this.length === rowCount) fullLengthMaps += 1; + return originalMap.call(this, callback, thisArg); + }); + let encoded: Uint8Array; + try { + encoded = encodeTabResultSnapshot({ result, cachedAt: 1 }); + } finally { + mapSpy.mockRestore(); + } + + expect(fullLengthMaps).toBeLessThanOrEqual(result.columns.length); + expect(decodeTabResultSnapshot(encoded)?.result?.rows).toEqual(result.rows); + }); + + it("reads each source row once when converting a wide result to columns", () => { + const result = createResult(100); + let rowReads = 0; + result.rows = new Proxy(result.rows, { + get(target, property, receiver) { + if (typeof property === "string" && /^\d+$/.test(property)) rowReads += 1; + return Reflect.get(target, property, receiver); + }, + }); + + const encoded = encodeTabResultSnapshot({ result, cachedAt: 1 }); + + expect(rowReads).toBe(result.rows.length); + expect(decodeTabResultSnapshot(encoded)?.result?.rows).toEqual(createResult(100).rows); + }); + + it("preserves the version-1 payload and metadata cleanup for results and saved runs", () => { + const result = createResult(2); + result.rows = [ + [1, "", false], + [2, null, true], + ]; + result.session_id = "live-session"; + result.mongo_documents = [{ retained: null, omitted: undefined, nested: { empty: undefined, values: [null, undefined, 0] }, date: new Date(0) }]; + const encoded = encodeTabResultSnapshot({ + result, + results: [result], + resultRuns: [{ id: "run-1", title: "Run 1", sequence: 1, sql: "select 1", createdAt: 1, result, results: [result] }], + cachedAt: 1, + }); + const envelope = decode(encoded) as any; + const expectedResult = { + columns: ["id", "name", "enabled"], + columnValues: [ + [1, 2], + ["", null], + [false, true], + ], + rowCount: 2, + mongo_documents: [{ retained: null, nested: { values: [null, null, 0] }, date: {} }], + affected_rows: 0, + execution_time_ms: 1, + }; + + expect(envelope).toEqual({ + magic: "DBX_TAB_RESULT_CACHE", + version: 1, + codec: "msgpack-columnar", + cachedAt: 1, + rowCount: 2, + columnCount: 3, + payload: { + result: expectedResult, + results: [expectedResult], + resultRuns: [{ id: "run-1", title: "Run 1", sequence: 1, sql: "select 1", createdAt: 1, result: expectedResult, results: [expectedResult] }], + cachedAt: 1, + }, + }); + expect(result.rows).toEqual([ + [1, "", false], + [2, null, true], + ]); + expect(result.mongo_documents[0]).toHaveProperty("omitted", undefined); + expect(result.session_id).toBe("live-session"); + }); +}); diff --git a/apps/desktop/src/lib/tabs/tabResultCache.ts b/apps/desktop/src/lib/tabs/tabResultCache.ts index adce58e6de..ebff1f0a47 100644 --- a/apps/desktop/src/lib/tabs/tabResultCache.ts +++ b/apps/desktop/src/lib/tabs/tabResultCache.ts @@ -388,8 +388,19 @@ function stripResultRunSessionIds(resultRuns: QueryTab["resultRuns"]): QueryTab[ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult | undefined { if (!result) return undefined; - const columnValues = result.columns.map((_, colIndex) => result.rows.map((row) => row[colIndex] ?? null)); - return removeUndefinedFields({ + const rowCount = result.rows.length; + const columnValues = result.columns.map(() => { + const values: CellValue[] = []; + values.length = rowCount; + return values; + }); + for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { + const row = result.rows[rowIndex]; + for (let columnIndex = 0; columnIndex < columnValues.length; columnIndex++) { + columnValues[columnIndex][rowIndex] = row?.[columnIndex] ?? null; + } + } + const metadata = removeUndefinedFields({ columns: [...result.columns], execution_error: result.execution_error, statement_index: result.statement_index, @@ -398,8 +409,7 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult spatial_columns: result.spatial_columns?.map((entry) => ({ column_index: entry.column_index, srid: entry.srid })), spatial_values: result.spatial_values?.map((row) => [...row]), large_value_cells: result.large_value_cells?.map((cell) => ({ ...cell })), - columnValues, - rowCount: result.rows.length, + rowCount, mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined, mongo_copy_documents: result.mongo_copy_documents ? clonePlain(result.mongo_copy_documents) : undefined, affected_rows: result.affected_rows, @@ -413,6 +423,7 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult sourceFrom: result.sourceFrom, sourceTo: result.sourceTo, }); + return { ...metadata, columnValues }; } function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResult | undefined { @@ -445,18 +456,17 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul } function snapshotToPayload(snapshot: TabResultSnapshot): TabResultSnapshotPayload { - return removeUndefinedFields({ - ...snapshot, - result: toColumnarResult(snapshot.result), - results: snapshot.results?.map((result) => toColumnarResult(result)!), - resultRuns: snapshot.resultRuns?.map((run) => - removeUndefinedFields({ - ...run, - result: toColumnarResult(run.result), - results: run.results?.map((result) => toColumnarResult(result)!), - }), - ), - }); + const { result, results, resultRuns, ...metadata } = snapshot; + return { + ...removeUndefinedFields(metadata), + result: toColumnarResult(result), + results: results?.map((result) => toColumnarResult(result)!), + resultRuns: resultRuns?.map(({ result, results, ...run }) => ({ + ...removeUndefinedFields(run), + result: toColumnarResult(result), + results: results?.map((result) => toColumnarResult(result)!), + })), + }; } function payloadToSnapshot(payload: TabResultSnapshotPayload): TabResultSnapshot { @@ -725,7 +735,7 @@ export function encodeTabResultSnapshot(snapshot: TabResultSnapshot): Uint8Array columnCount: stats.columnCount, payload: snapshotToPayload(snapshot), }; - return encode(removeUndefinedFields(envelope)); + return encode(envelope, { ignoreUndefined: true }); } export function decodeTabResultSnapshot(bytes: Uint8Array | ArrayBuffer): TabResultSnapshot | undefined {