perf(canvas): 大画布渲染与媒体 - 布局不变时复用空间索引,合并重复的资源与封面请求
连线索引只存端点 ID,文本更新不再重建几何。小地图一次遍历隐藏折叠帧子节点。资源 Blob 与签名刷新合并在途请求,视频封面优先复用已缓存文件。
This commit is contained in:
1 parent
b7416b8346
commit
631a85b4fe
10 files changed
+286
-107
No files matched your search
@@ -27,6 +27,8 @@ const workflowProductionSource = readFileSync(resolve(import.meta.dir, "../src/p
|
||||
const videoPlayerSource = readFileSync(resolve(import.meta.dir, "../src/components/video-player.tsx"), "utf8");
|
||||
const canvasProjectSource = moduleGroupSource("pages/canvas/project.tsx");
|
||||
const globalStylesSource = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8");
|
||||
const resourceBlobCacheSource = readFileSync(resolve(import.meta.dir, "../src/services/resource-blob-cache.ts"), "utf8");
|
||||
const resourceApiSource = readFileSync(resolve(import.meta.dir, "../src/services/api/resources.ts"), "utf8");
|
||||
|
||||
function node(id: string, type: CanvasNodeType): CanvasNodeData {
|
||||
return { id, type, title: id, position: { x: 0, y: 0 }, width: 320, height: 180, metadata: {} };
|
||||
@@ -122,9 +124,26 @@ describe("large canvas media rendering", () => {
|
||||
expect(workflowProductionSource).not.toContain("await response.blob()");
|
||||
});
|
||||
|
||||
test("merges concurrent full Blob consumers before starting the download", () => {
|
||||
expect(resourceBlobCacheSource).toContain("const blobInFlight = new Map<string, Promise<Blob | null>>();");
|
||||
expect(resourceBlobCacheSource).toContain("const pending = blobInFlight.get(target.key);");
|
||||
expect(resourceBlobCacheSource).toContain("return pending;");
|
||||
expect(resourceBlobCacheSource).toContain("getOrDownloadResourceBlob(storageKey, target)");
|
||||
});
|
||||
|
||||
test("coalesces forced access refreshes already in flight", () => {
|
||||
expect(resourceApiSource).toContain("const inFlightRefresh = accessRequests.get(key);");
|
||||
expect(resourceApiSource).toContain("if (inFlightRefresh) return inFlightRefresh;");
|
||||
});
|
||||
|
||||
test("reuses a cached full video Blob for poster capture before requesting playback access", () => {
|
||||
expect(canvasVideoPreviewSource).toContain("peekCachedResourceObjectUrl(storageKey)");
|
||||
expect(canvasVideoPreviewSource).toContain("getCachedResourceObjectUrl(storageKey)");
|
||||
});
|
||||
|
||||
test("shows a captured first frame before its OSS poster upload finishes and allows later retries", () => {
|
||||
expect(canvasVideoPreviewSource).toContain("const localUrl = URL.createObjectURL(captured.poster)");
|
||||
expect(canvasVideoPreviewSource).toContain("return { localUrl, persisted }");
|
||||
expect(canvasVideoPreviewSource).toContain("const localUrl = URL.createObjectURL(result.poster)");
|
||||
expect(canvasVideoPreviewSource).toContain("persisted: result.persisted");
|
||||
expect(canvasVideoPreviewSource).not.toContain("previewRequests");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { buildCanvasSpatialIndex, canvasNodeBounds } from "@/lib/canvas/canvas-spatial-index";
|
||||
import { buildCanvasConnectionIndex } from "@/lib/canvas/canvas-connection-index";
|
||||
import { visibleCanvasMinimapNodes } from "@/lib/canvas/canvas-frame";
|
||||
import { retainCanvasLayoutNodes } from "@/lib/canvas/canvas-layout-snapshot";
|
||||
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
describe("canvas spatial index", () => {
|
||||
@@ -49,6 +52,30 @@ describe("canvas spatial index", () => {
|
||||
expect(index.query({ left: 0, top: 0, right: 100, bottom: 100 }, 3)).toEqual([0, 1, 2]);
|
||||
});
|
||||
|
||||
test("filters collapsed frame children with one parent index", () => {
|
||||
const frame: CanvasNodeData = { id: "frame", type: CanvasNodeType.Frame, title: "框", position: { x: 0, y: 0 }, width: 400, height: 300, metadata: { frame: { collapsed: true } } };
|
||||
const child: CanvasNodeData = { id: "child", type: CanvasNodeType.Image, title: "子节点", parentId: frame.id, position: { x: 20, y: 20 }, width: 120, height: 80, metadata: {} };
|
||||
const visible = visibleCanvasMinimapNodes([frame, child]);
|
||||
expect(visible.map((node) => node.id)).toEqual([frame.id]);
|
||||
});
|
||||
|
||||
test("reuses the layout snapshot for metadata-only updates", () => {
|
||||
const original: CanvasNodeData = { id: "node", type: CanvasNodeType.Image, title: "图", position: { x: 0, y: 0 }, width: 120, height: 80, metadata: {} };
|
||||
const metadataUpdate = { ...original, metadata: { status: "success" as const } };
|
||||
const geometryUpdate = { ...metadataUpdate, position: { x: 40, y: 0 } };
|
||||
const previous = [original];
|
||||
expect(retainCanvasLayoutNodes(previous, [metadataUpdate])).toBe(previous);
|
||||
expect(retainCanvasLayoutNodes(previous, [geometryUpdate])).not.toBe(previous);
|
||||
});
|
||||
|
||||
test("keeps connection index geometry independent from node metadata", () => {
|
||||
const from: CanvasNodeData = { id: "from", type: CanvasNodeType.Text, title: "起点", position: { x: 0, y: 0 }, width: 100, height: 80, metadata: {} };
|
||||
const to: CanvasNodeData = { id: "to", type: CanvasNodeType.Text, title: "终点", position: { x: 300, y: 0 }, width: 100, height: 80, metadata: {} };
|
||||
const index = buildCanvasConnectionIndex([from, to], [{ id: "edge", fromNodeId: from.id, toNodeId: to.id }]);
|
||||
expect(index.index.query({ left: -1, top: -1, right: 500, bottom: 100 }).map((item) => item.connection.id)).toEqual(["edge"]);
|
||||
expect(index.entriesById.get("edge")).toMatchObject({ fromId: "from", toId: "to" });
|
||||
});
|
||||
|
||||
test("keeps the target 50k canvas mix query-bounded", () => {
|
||||
const nodes = buildLargeCanvasFixture();
|
||||
const counts = nodes.reduce<Record<string, number>>((result, node) => {
|
||||
|
||||
Reference in new issue
Block a user