Skip to content

Commit

Permalink
perf: optimize canvas renderer performance (#1810)
Browse files Browse the repository at this point in the history
* perf: optimize canvas renderer performance

* perf: avoid unnecessary transformation matrix calculations and canvas context matrix updates

* perf: avoid frequent calls to `context.save()` and `context.restore()`

* chore: fix eslint issue

* perf: refactor hot code

* fix: memory leaks

* fix: memory leak caused by element destroy event not being triggered

* docs: add perf demo to site

* chore: add necessary comments

* chore: remove comments
  • Loading branch information
wang1212 authored Nov 4, 2024
1 parent 17de8f9 commit a3e07c1
Show file tree
Hide file tree
Showing 51 changed files with 1,566 additions and 468 deletions.
12 changes: 12 additions & 0 deletions .changeset/large-moose-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@antv/g-plugin-canvas-path-generator': minor
'@antv/g-plugin-canvaskit-renderer': minor
'@antv/g-plugin-canvas-renderer': minor
'@antv/g-plugin-device-renderer': minor
'@antv/g-plugin-canvas-picker': minor
'@antv/g-plugin-image-loader': minor
'@antv/g-plugin-svg-renderer': minor
'@antv/g-lite': minor
---

perf: optimize canvas renderer performance
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
105 changes: 105 additions & 0 deletions __tests__/demos/perf/attr-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import * as lil from 'lil-gui';
import { Rect, Group, CanvasEvent } from '@antv/g';
import type { Canvas } from '@antv/g';

export async function attrUpdate(context: { canvas: Canvas; gui: lil.GUI }) {
const { canvas, gui } = 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) {
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 },
);

// GUI
canvas.getConfig().renderer.getConfig().enableRenderingOptimization = true;

gui
.add(
{
enableRenderingOptimization: canvas.getConfig().renderer.getConfig()
.enableRenderingOptimization,
},
'enableRenderingOptimization',
)
.onChange((result) => {
canvas.getConfig().renderer.getConfig().enableRenderingOptimization =
result;
});
}
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';
24 changes: 12 additions & 12 deletions __tests__/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// import Stats from 'stats.js';
import Stats from 'stats.js';
import * as lil from 'lil-gui';
import '@antv/g-camera-api';
import { Canvas, CanvasEvent, runtime } from '@antv/g';
Expand Down 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 Down Expand Up @@ -227,23 +227,23 @@ function createSpecRender(object) {
window.__g_instances__ = [canvas];

// stats
// const stats = new Stats();
// stats.showPanel(0);
// const $stats = stats.dom;
// $stats.style.position = 'absolute';
// $stats.style.left = '4px';
// $stats.style.top = '4px';
// app.appendChild($stats);
const stats = new Stats();
stats.showPanel(0);
const $stats = stats.dom;
$stats.style.position = 'fixed';
$stats.style.left = '2px';
$stats.style.top = '2px';
// document.body.appendChild($stats);

// GUI
const gui = new lil.GUI({ autoPlace: false });
$div.appendChild(gui.domElement);

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

// canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => {
// stats.update();
// });
canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => {
stats.update();
});

if (
selectRenderer.value === 'canvas' &&
Expand Down
1 change: 1 addition & 0 deletions packages/g-lite/src/AbstractRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export class AbstractRenderer implements IRenderer {
enableDirtyRectangleRendering: true,
enableDirtyRectangleRenderingDebug: false,
enableSizeAttenuation: true,
enableRenderingOptimization: false,
...config,
};
}
Expand Down
27 changes: 17 additions & 10 deletions packages/g-lite/src/Canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ import {
} from './camera';
import type { RBushNodeAABB } from './components';
import type { CustomElement } from './display-objects';
import { DisplayObject } from './display-objects/DisplayObject';
import type { MutationEvent } from './dom/MutationEvent';
import {
DisplayObject,
attrModifiedEvent as attrModifiedEventCache,
} from './display-objects/DisplayObject';
import {
insertedEvent as insertedEventCache,
removedEvent as removedEventCache,
destroyEvent as destroyEventCache,
} from './dom/Element';
import type { CanvasContext, Element, IChildNode } from './dom';
import { CustomEvent, Document, ElementEvent, EventTarget } from './dom';
import { CustomElementRegistry } from './dom/CustomElementRegistry';
Expand Down Expand Up @@ -122,9 +131,6 @@ export class Canvas extends EventTarget implements ICanvas {
*/
isMouseEvent: (event: InteractivePointerEvent) => event is MouseEvent;

/**
* double click speed (ms), default is 200ms
*/
dblClickSpeed?: CanvasConfig['dblClickSpeed'];

/**
Expand Down Expand Up @@ -398,9 +404,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 @@ -429,19 +433,22 @@ export class Canvas extends EventTarget implements ICanvas {
this.dispatchEvent(new CustomEvent(CanvasEvent.AFTER_DESTROY));
}

const clearEventRetain = (event: CustomEvent) => {
const clearEventRetain = (event: CustomEvent | MutationEvent) => {
event.currentTarget = null;
event.manager = null;
event.target = null;
(event as MutationEvent).relatedNode = null;
};

clearEventRetain(mountedEvent);
clearEventRetain(unmountedEvent);
clearEventRetain(beforeRenderEvent);
clearEventRetain(rerenderEvent);
clearEventRetain(afterRenderEvent);

this.cancelAnimationFrame(this.frameId);
clearEventRetain(attrModifiedEventCache);
clearEventRetain(insertedEventCache);
clearEventRetain(removedEventCache);
clearEventRetain(destroyEventCache);
}

/**
Expand Down
25 changes: 11 additions & 14 deletions packages/g-lite/src/css/StyleValueRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import { EMPTY_PARSED_PATH } from '../display-objects/constants';
import type { GlobalRuntime } from '../global-runtime';
import { GeometryAABBUpdater } from '../services';
import { AABB } from '../shapes';
import type {
BaseStyleProps,
ParsedBaseStyleProps,
Tuple3Number,
} from '../types';
import type { BaseStyleProps, Tuple3Number } from '../types';
import { Shape } from '../types';
import type { CSSRGB } from './cssom';
import type {
Expand Down Expand Up @@ -615,8 +611,8 @@ export const BUILT_IN_PROPERTIES: PropertyMetadata[] = [
},
];

const GEOMETRY_ATTRIBUTE_NAMES = BUILT_IN_PROPERTIES.filter((n) => !!n.l).map(
(n) => n.n,
const GEOMETRY_ATTRIBUTE_NAMES = new Set(
BUILT_IN_PROPERTIES.filter((n) => !!n.l).map((n) => n.n),
);

export const propertyMetadataCache: Record<string, PropertyMetadata> = {};
Expand Down Expand Up @@ -667,12 +663,13 @@ export class DefaultStyleValueRegistry implements StyleValueRegistry {
Object.assign(object.parsedStyle, attributes);

let needUpdateGeometry = !!options.forceUpdateGeometry;

if (
!needUpdateGeometry &&
GEOMETRY_ATTRIBUTE_NAMES.some((name) => name in attributes)
) {
needUpdateGeometry = true;
if (!needUpdateGeometry) {
for (const i in attributes) {
if (GEOMETRY_ATTRIBUTE_NAMES.has(i)) {
needUpdateGeometry = true;
break;
}
}
}

if (attributes.fill) {
Expand Down Expand Up @@ -858,7 +855,7 @@ export class DefaultStyleValueRegistry implements StyleValueRegistry {
if (!geometry.renderBounds) {
geometry.renderBounds = new AABB();
}
const parsedStyle = object.parsedStyle as ParsedBaseStyleProps;
const parsedStyle = object.parsedStyle;
const {
cx = 0,
cy = 0,
Expand Down
4 changes: 2 additions & 2 deletions packages/g-lite/src/css/parser/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ export function isCSSRGB(object: any): object is CSSRGB {
* @see https://github.com/WebKit/WebKit/blob/main/Source/WebCore/css/parser/CSSParser.cpp#L97
*/
export const parseColor = memoize(
(colorStr: string): CSSRGB | CSSGradientValue[] | Pattern => {
(colorStr: string | Pattern): CSSRGB | CSSGradientValue[] | Pattern => {
if (isPattern(colorStr)) {
return {
repetition: 'repeat',
...(colorStr as Pattern),
...colorStr,
};
}

Expand Down
8 changes: 2 additions & 6 deletions packages/g-lite/src/css/parser/transform-origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,8 @@ export const parseTransformOrigin = memoize(

// eg. center bottom
return [
parseLengthOrPercentage(
convertKeyword2Percent(values[0]),
) as CSSUnitValue,
parseLengthOrPercentage(
convertKeyword2Percent(values[1]),
) as CSSUnitValue,
parseLengthOrPercentage(convertKeyword2Percent(values[0])),
parseLengthOrPercentage(convertKeyword2Percent(values[1])),
];
}
return [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { DisplayObject } from '../../display-objects';
import { ParsedBaseStyleProps } from '../../types';
import { UnitType, type CSSUnitValue } from '../cssom';
import type { CSSProperty } from '../CSSProperty';

Expand All @@ -15,7 +14,7 @@ export class CSSPropertyTransformOrigin
>
{
postProcessor(object: DisplayObject) {
const { transformOrigin } = object.parsedStyle as ParsedBaseStyleProps;
const { transformOrigin } = object.parsedStyle;
if (
transformOrigin[0].unit === UnitType.kPixels &&
transformOrigin[1].unit === UnitType.kPixels
Expand Down
Loading

0 comments on commit a3e07c1

Please sign in to comment.