perf(canvas): 大画布渲染与媒体 - 布局不变时复用空间索引,合并重复的资源与封面请求

连线索引只存端点 ID,文本更新不再重建几何。小地图一次遍历隐藏折叠帧子节点。资源 Blob 与签名刷新合并在途请求,视频封面优先复用已缓存文件。
This commit is contained in:
ddcat666 committed 2026-10-09 17:17:38 +08:00
1 parent b7416b8346
commit 631a85b4fe
10 files changed
+286 -107

No files matched your search

+21 -2
View File
@@ -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");
});
});
+27
View File
@@ -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) => {