Files
google-labs-jules[bot]andAEmotionStudio 85a0a6fa33 perf: optimize node rendering by caching visible nodes
Reduced redundant iterations over `app.graph._nodes` in `OffscreenRenderer`.
Implemented `getVisibleNodes` to calculate visible nodes once per frame (O(N)) and reused this list for text, title, and image preview rendering passes (O(M)).
This improves performance for large graphs by replacing multiple O(N) traversals with a single O(N) traversal followed by O(M) traversals where M << N.

Added unit tests in `tests/unit/OffscreenRenderer.test.ts` to verify node visibility culling logic.
Updated `.jules/bolt.md` with performance learnings.

Co-authored-by: AEmotionStudio <163354043+AEmotionStudio@users.noreply.github.com>
2026-02-09 16:18:12 +00:00

159 lines
5.5 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
// Create hoisted mocks
const { appMock } = vi.hoisted(() => {
return {
appMock: {
graph: {
_nodes: [] as any[]
},
canvas: {
ds: {
scale: 1,
offset: [0, 0]
}
}
}
};
});
// Attach to global
// @ts-ignore
globalThis.app = appMock;
vi.mock('/scripts/app.js', () => ({
app: appMock
}));
import { OffscreenRenderer } from '../../src/magnify-glass/OffscreenRenderer';
import { ConfigManager } from '../../src/magnify-glass/ConfigManager';
import { MagnifierState } from '../../src/magnify-glass/MagnifierState';
describe('OffscreenRenderer Optimization', () => {
let renderer: OffscreenRenderer;
let configMock: ConfigManager;
let stateMock: MagnifierState;
let canvasMock: HTMLCanvasElement;
beforeEach(() => {
vi.clearAllMocks();
appMock.graph._nodes = [];
appMock.canvas.ds = { scale: 1, offset: [0, 0] };
// Mock ConfigManager
configMock = {
glassSize: 200,
zoomFactor: 2,
accessibilityEnabled: true,
nodeTitleEmphasis: true,
invertColors: false,
grayscaleMode: false,
forceDirectCapture: false,
fontScaleFactor: 100,
boldTextEnabled: false,
textGlowEnabled: false,
textOutlineEnabled: false,
highContrastMode: false,
showCursorPreview: false
} as unknown as ConfigManager;
// Mock MagnifierState
stateMock = {
x: 100, // Cursor backing X
y: 100, // Cursor backing Y
canvasScale: 1
} as unknown as MagnifierState;
// Mock canvas creation to return a mock context
const originalCreateElement = document.createElement.bind(document);
vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
if (tagName === 'canvas') {
const canvas = originalCreateElement(tagName) as HTMLCanvasElement;
canvas.getContext = vi.fn().mockReturnValue({
clearRect: vi.fn(),
save: vi.fn(),
restore: vi.fn(),
drawImage: vi.fn(),
beginPath: vi.fn(),
roundRect: vi.fn(),
fill: vi.fn(),
stroke: vi.fn(),
fillText: vi.fn(),
measureText: vi.fn().mockReturnValue({ width: 10 }),
clip: vi.fn(),
scale: vi.fn(),
translate: vi.fn(),
moveTo: vi.fn(),
lineTo: vi.fn(),
closePath: vi.fn(),
strokeText: vi.fn(),
});
return canvas;
}
return originalCreateElement(tagName);
});
renderer = new OffscreenRenderer(configMock, stateMock);
});
it('should identify visible nodes and render them', () => {
// Setup nodes
// Node 1: Visible (at 50, 50, size 100x100)
// Cursor at 100, 100. Glass size 200, zoom 2. Source size 100.
// Capture region: 50 to 150.
// Node 1 overlaps.
const visibleNode = {
pos: [50, 50], // Graph coords
size: [100, 100],
widgets: [
{ type: 'text', name: 'visible_widget', value: 'Visible Text', computedHeight: 50, last_y: 40 }
]
};
// Node 2: Not Visible (far away)
const invisibleNode = {
pos: [1000, 1000],
size: [100, 100],
widgets: [
{ type: 'text', name: 'invisible_widget', value: 'Invisible Text', computedHeight: 50, last_y: 40 }
]
};
appMock.graph._nodes = [visibleNode, invisibleNode];
// Mock target canvas
canvasMock = document.createElement('canvas');
canvasMock.width = 1000;
canvasMock.height = 800;
vi.spyOn(canvasMock, 'getBoundingClientRect').mockReturnValue({
x: 0, y: 0, width: 1000, height: 800, top: 0, left: 0, right: 1000, bottom: 800, toJSON: () => {}
} as DOMRect);
// Spy on private method drawWidgetTextNatively via cast
const drawSpy = vi.spyOn(renderer as any, 'drawWidgetTextNatively');
// Spy on context fillText to verify rendering
// Need to access the internal context.
const ctx = (renderer as any).offscreenCtx;
const fillTextSpy = vi.spyOn(ctx, 'fillText');
// Render
renderer.renderHighResRegion(canvasMock);
// Verify drawWidgetTextNatively was called
expect(drawSpy).toHaveBeenCalled();
// Verify that visibleNodes list was correctly filtered and passed
const callArgs = drawSpy.mock.calls[0];
const visibleNodes = callArgs[0] as any[];
expect(visibleNodes.length).toBe(1);
expect(visibleNodes[0].widgets[0].value).toBe('Visible Text');
// Verify text was drawn for visible node
expect(fillTextSpy).toHaveBeenCalledWith(expect.stringContaining('Visible Text'), expect.any(Number), expect.any(Number));
// Verify text was NOT drawn for invisible node
expect(fillTextSpy).not.toHaveBeenCalledWith(expect.stringContaining('Invisible Text'), expect.any(Number), expect.any(Number));
});
});