Skip to content

Commit

Permalink
perf: optimize canvas renderer performance
Browse files Browse the repository at this point in the history
  • Loading branch information
wang1212 committed Nov 1, 2024
1 parent 17de8f9 commit 7c6f3d3
Show file tree
Hide file tree
Showing 20 changed files with 233 additions and 112 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ module.exports = {
{ functions: false, classes: false },
],
'@typescript-eslint/no-redeclare': ['error'],
'@typescript-eslint/no-this-alias': ['error', { allowedNames: ['self'] }],
'@typescript-eslint/restrict-template-expressions': 'warn',
'@typescript-eslint/return-await': 'warn',
'@typescript-eslint/default-param-last': 'warn',
Expand Down
2 changes: 1 addition & 1 deletion __tests__/demos/bugfix/1760.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Canvas, Path, Line } from '@antv/g';
/**
* @see https://github.com/antvis/G/issues/1760
* @see https://github.com/antvis/G/issues/1790
* @see https://github.com/antvis/G/pull/1808
* @see https://github.com/antvis/G/pull/1809
*/
export async function issue_1760(context: { canvas: Canvas }) {
const { canvas } = context;
Expand Down
88 changes: 88 additions & 0 deletions __tests__/demos/perf/attr-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Rect, Group, CanvasEvent } from '@antv/g';
import type { Canvas } from '@antv/g';

export async function attrUpdate(context: { canvas: Canvas }) {
const { canvas } = context;
console.log(canvas);

await canvas.ready;

const { width, height } = canvas.getConfig();
const count = 2e4;
const root = new Group();
const rects = [];

const perfStore: { [k: string]: { count: number; time: number } } = {
update: { count: 0, time: 0 },
setAttribute: { count: 0, time: 0 },
};

function updatePerf(key: string, time: number) {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note test

Unused function updatePerf.
perfStore[key].count++;
perfStore[key].time += time;
console.log(
`average ${key} time: `,
perfStore[key].time / perfStore[key].count,
);
}

function update() {
// const startTime = performance.now();
// console.time('update');

const rectsToRemove = [];

// const startTime0 = performance.now();
// console.time('setAttribute');
for (let i = 0; i < count; i++) {
const rect = rects[i];
rect.x -= rect.speed;
(rect.el as Rect).setAttribute('x', rect.x);
if (rect.x + rect.size < 0) rectsToRemove.push(i);
}
// console.timeEnd('setAttribute');
// updatePerf('setAttribute', performance.now() - startTime0);

rectsToRemove.forEach((i) => {
rects[i].x = width + rects[i].size / 2;
});

// console.timeEnd('update');
// updatePerf('update', performance.now() - startTime);
}

function render() {
for (let i = 0; i < count; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const size = 10 + Math.random() * 40;
const speed = 1 + Math.random();

const rect = new Rect({
style: {
x,
y,
width: size,
height: size,
fill: 'white',
stroke: 'black',
},
});
root.appendChild(rect);
rects[i] = { x, y, size, speed, el: rect };
}
}

render();
canvas.addEventListener(CanvasEvent.BEFORE_RENDER, () => update());

canvas.appendChild(root);

canvas.addEventListener(
'rerender',
() => {
// console.timeEnd('render');
},
{ once: true },
);
}
1 change: 1 addition & 0 deletions __tests__/demos/perf/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { circles } from './circles';
export { rects } from './rect';
export { image } from './image';
export { attrUpdate } from './attr-update';
2 changes: 2 additions & 0 deletions __tests__/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
</style>
</head>
<body>
<script src="https://unpkg.com/[email protected]/dist/fpsmeter.min.js"></script>

<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
Expand Down
17 changes: 14 additions & 3 deletions __tests__/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const renderers = {
};
const app = document.getElementById('app') as HTMLElement;
let currentContainer = document.createElement('div');
let canvas;
let canvas: Canvas;
let prevAfter;
const normalizeName = (name: string) => name.replace(/-/g, '').toLowerCase();
const renderOptions = (keyword = '') => {
Expand All @@ -59,7 +59,7 @@ const renderOptions = (keyword = '') => {

// Select for chart.
const selectChart = document.createElement('select') as HTMLSelectElement;
selectChart.style.margin = '1em 1em 1em 96px';
selectChart.style.margin = '1em 1em 1em 120px';
renderOptions();
selectChart.onchange = () => {
const { value } = selectChart;
Expand Down Expand Up @@ -239,7 +239,18 @@ function createSpecRender(object) {
const gui = new lil.GUI({ autoPlace: false });
$div.appendChild(gui.domElement);

await generate({ canvas, renderer, container: $div, gui });
// @see https://github.com/Darsain/fpsmeter/wiki/Options
const fpsMeter = new window.FPSMeter({
theme: 'light',
heat: 1,
graph: 1,
});

await generate({ canvas, renderer, container: $div, gui, fpsMeter });

canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => {
fpsMeter.tick();
});

// canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => {
// stats.update();
Expand Down
6 changes: 1 addition & 5 deletions packages/g-lite/src/Canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,9 +398,7 @@ export class Canvas extends EventTarget implements ICanvas {
this.dispatchEvent(new CustomEvent(CanvasEvent.BEFORE_DESTROY));
}
if (this.frameId) {
const cancelRAF =
this.getConfig().cancelAnimationFrame || cancelAnimationFrame;
cancelRAF(this.frameId);
this.cancelAnimationFrame(this.frameId);
}

// unmount all children
Expand Down Expand Up @@ -440,8 +438,6 @@ export class Canvas extends EventTarget implements ICanvas {
clearEventRetain(beforeRenderEvent);
clearEventRetain(rerenderEvent);
clearEventRetain(afterRenderEvent);

this.cancelAnimationFrame(this.frameId);
}

/**
Expand Down
17 changes: 10 additions & 7 deletions packages/g-lite/src/dom/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,16 +361,19 @@ export abstract class Node extends EventTarget implements INode {
/**
* iterate current node and its descendants
* @param callback - callback to execute for each node, return false to break
* @param assigned - whether to iterate assigned nodes
*/
forEach(callback: (o: INode) => void | boolean) {
const result = callback(this);
const stack: INode[] = [this];

if (result !== false) {
const nodes = this.childNodes;
const length = nodes.length;
for (let i = 0; i < length; i++) {
nodes[i].forEach(callback);
while (stack.length > 0) {
const node = stack.pop();
const result = callback(node);
if (result === false) {
break;
}

for (let i = node.childNodes.length - 1; i >= 0; i--) {
stack.push(node.childNodes[i]);
}
}
}
Expand Down
84 changes: 47 additions & 37 deletions packages/g-lite/src/services/RenderingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,52 +220,62 @@ export class RenderingService {
canvasConfig: Partial<CanvasConfig>,
renderingContext: RenderingContext,
) {
const self = this;
const { enableDirtyCheck, enableCulling } =
canvasConfig.renderer.getConfig();

// TODO: relayout

// dirtycheck first
const { renderable } = displayObject;
// eslint-disable-next-line no-nested-ternary
const objectChanged = enableDirtyCheck
? // @ts-ignore
renderable.dirty || renderingContext.dirtyRectangleRenderingDisabled
? displayObject
: null
: displayObject;

if (objectChanged) {
const objectToRender = enableCulling
? this.hooks.cull.call(objectChanged, this.context.camera)
: objectChanged;

if (objectToRender) {
this.stats.rendered++;
renderingContext.renderListCurrentFrame.push(objectToRender);
function internalRenderSingleDisplayObject(object: DisplayObject) {
// TODO: relayout

// dirtycheck first
const { renderable, sortable } = object;
// eslint-disable-next-line no-nested-ternary
const objectChanged = enableDirtyCheck
? // @ts-ignore
renderable.dirty || renderingContext.dirtyRectangleRenderingDisabled
? object
: null
: object;

if (objectChanged) {
const objectToRender = enableCulling
? self.hooks.cull.call(objectChanged, self.context.camera)
: objectChanged;

if (objectToRender) {
self.stats.rendered += 1;
renderingContext.renderListCurrentFrame.push(objectToRender);
}
}
}

displayObject.renderable.dirty = false;
displayObject.sortable.renderOrder = this.zIndexCounter++;
renderable.dirty = false;
sortable.renderOrder = self.zIndexCounter;

this.stats.total++;
self.zIndexCounter += 1;
self.stats.total += 1;

// sort is very expensive, use cached result if possible
const { sortable } = displayObject;
if (sortable.dirty) {
this.sort(displayObject, sortable);
sortable.dirty = false;
sortable.dirtyChildren = [];
sortable.dirtyReason = undefined;
// sort is very expensive, use cached result if possible
if (sortable.dirty) {
self.sort(object, sortable);
sortable.dirty = false;
sortable.dirtyChildren = [];
sortable.dirtyReason = undefined;
}
}

// recursive rendering its children
(sortable.sorted || displayObject.childNodes).forEach(
(child: DisplayObject) => {
this.renderDisplayObject(child, canvasConfig, renderingContext);
},
);
const stack = [displayObject];

while (stack.length > 0) {
const currentObject = stack.pop();

internalRenderSingleDisplayObject(currentObject);

// recursive rendering its children
const objects = currentObject.sortable.sorted || currentObject.childNodes;
for (let i = objects.length - 1; i >= 0; i--) {
stack.push(objects[i] as unknown as DisplayObject);
}
}
}

private sort(displayObject: DisplayObject, sortable: Sortable) {
Expand Down
5 changes: 4 additions & 1 deletion packages/g-plugin-canvas-path-generator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
RectPath,
} from './paths';

export class Plugin extends AbstractRendererPlugin {
export class Plugin extends AbstractRendererPlugin<{
pathGeneratorFactory: Record<Shape, PathGenerator<any>>;
}> {
name = 'canvas-path-generator';
init(): void {
const pathGeneratorFactory: Record<Shape, PathGenerator<any>> = {
Expand All @@ -26,6 +28,7 @@ export class Plugin extends AbstractRendererPlugin {
[Shape.IMAGE]: undefined,
[Shape.HTML]: undefined,
[Shape.MESH]: undefined,
[Shape.FRAGMENT]: undefined,
};

// @ts-ignore
Expand Down
Loading

0 comments on commit 7c6f3d3

Please sign in to comment.