perf: optimize element dependencies effect trace performance (#6249)

* perf: add baseline performance report

* perf: optmize effect performance

* perf: generate performance report

* test: update test demo

---------

Co-authored-by: antv <antv@antfin.com>
This commit is contained in:
Aaron 2024-08-29 19:45:33 +08:00 committed by GitHub
parent 8553175ce0
commit 2c21154855
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 597 additions and 91 deletions

View File

@ -51,7 +51,7 @@
"eslint": "^8.57.0",
"eslint-plugin-jsdoc": "^46.10.1",
"husky": "^8.0.3",
"iperf": "^0.1.0-beta.11",
"iperf": "^0.1.0-beta.13",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jsdom": "^23.2.0",

View File

@ -1,6 +1,7 @@
import type { DisplayObject, Group } from '@antv/g';
import type { BaseComboStyleProps, GraphData, HTMLStyleProps, IElementEvent, NodeData } from '@antv/g6';
import { BaseCombo, effect, ExtensionCategory, Graph, HTML, isCollapsed, register } from '@antv/g6';
import { BaseCombo, ExtensionCategory, Graph, HTML, isCollapsed, register } from '@antv/g6';
import { isEqual } from '@antv/util';
export const elementHTMLSubGraph: TestCase = async (context) => {
interface CardNodeData {
@ -38,13 +39,14 @@ export const elementHTMLSubGraph: TestCase = async (context) => {
private graph?: Graph;
@effect((self, attributes) => {
const { data } = self.data;
return { data };
})
private previousData?: Record<string, unknown>;
private drawSubGraph() {
if (!this.isConnected) return;
const data = this.data;
if (isEqual(this.previousData, data)) return;
this.previousData = data;
this.drawGraphNode(data!.data as GraphData);
}

View File

@ -0,0 +1,246 @@
{
"version": "1.0",
"device": {
"os": {
"arch": "arm64",
"distro": "macOS",
"serial": "9821ed36011eee5abf6c71d6fc2c03fb4bf4655e674c56b7f50e2560cb6e924a"
},
"cpu": {
"manufacturer": "Apple",
"brand": "M1 Pro",
"speed": 2.4,
"cores": 10
},
"memory": {
"total": 16384,
"free": 540.28125
},
"gpu": {
"vendor": "Apple",
"model": "Apple M1 Pro",
"cores": "16"
}
},
"repo": "aa87ec67c38f03808c72d82caaa3d064b4ee9c01",
"client": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/128.0.6613.18 Safari/537.36",
"reports": {
"UpdateElementState": {
"time": [
{
"min": 167.09999999403954,
"max": 202.29999999701977,
"median": 171.79999999701977,
"avg": 172.86250000447035,
"variance": 18.562343744896353,
"reliable": true,
"key": "update element state to selected"
},
{
"min": 102.20000000298023,
"max": 116.09999999403954,
"median": 104.30000001192093,
"avg": 105.06250000186265,
"variance": 3.0598437559511513,
"reliable": true,
"key": "update element state to default"
},
{
"min": 86,
"max": 105.8999999910593,
"median": 88.5,
"avg": 88.51249999925494,
"variance": 2.073593743089587,
"reliable": true,
"key": "update element position"
}
],
"status": "passed"
},
"dataDiff1000": {
"time": [
{
"min": 2.7000000029802322,
"max": 11.300000011920929,
"median": 5.5,
"avg": 5.512499997392297,
"variance": 2.638593754386529,
"reliable": false,
"key": "data diff"
}
],
"status": "passed"
},
"dataDiff10000": {
"time": [
{
"min": 2.8999999910593033,
"max": 12.099999994039536,
"median": 6,
"avg": 6.325000004842877,
"variance": 4.859374997671694,
"reliable": false,
"key": "data diff"
}
],
"status": "passed"
},
"dataDiff100000": {
"time": [
{
"min": 11,
"max": 39.29999999701977,
"median": 12.700000002980232,
"avg": 16.387499999254942,
"variance": 60.1160937285237,
"reliable": true,
"key": "data diff"
}
],
"status": "passed"
},
"dataDiff5000": {
"time": [
{
"min": 1.8999999910593033,
"max": 9.5,
"median": 8.099999994039536,
"avg": 6.374999998137355,
"variance": 6.324374991413206,
"reliable": false,
"key": "data diff"
}
],
"status": "passed"
},
"dataDiff50000": {
"time": [
{
"min": 5.700000002980232,
"max": 17.899999991059303,
"median": 8.299999997019768,
"avg": 9.5,
"variance": 16.31750000014901,
"reliable": true,
"key": "data diff"
}
],
"status": "passed"
},
"elementDrawing100": {
"time": [
{
"min": 12.700000002980232,
"max": 24.600000008940697,
"median": 18.899999991059303,
"avg": 19.199999999254942,
"variance": 8.127499995231629,
"reliable": true,
"key": "element drawing"
},
{
"min": 7.9000000059604645,
"max": 14.100000008940697,
"median": 11.900000005960464,
"avg": 11.237500000745058,
"variance": 3.8348437521792946,
"reliable": true,
"key": "grid layout"
}
],
"status": "passed"
},
"elementDrawing1000": {
"time": [
{
"min": 48.099999994039536,
"max": 73.20000000298023,
"median": 61.70000000298023,
"avg": 61.374999998137355,
"variance": 13.14437501249835,
"reliable": true,
"key": "element drawing"
},
{
"min": 39.20000000298023,
"max": 59.1000000089407,
"median": 43.29999999701977,
"avg": 44.212499998509884,
"variance": 14.278593749292193,
"reliable": true,
"key": "grid layout"
}
],
"status": "passed"
},
"elementDrawing10000": {
"time": [
{
"min": 409.3999999910593,
"max": 536.2000000029802,
"median": 434.3999999910593,
"avg": 442.1124999951571,
"variance": 829.2260936078895,
"reliable": true,
"key": "element drawing"
},
{
"min": 372.90000000596046,
"max": 414.20000000298023,
"median": 393.29999999701977,
"avg": 389.8499999977648,
"variance": 108.12999993681908,
"reliable": true,
"key": "grid layout"
}
],
"status": "passed"
},
"elementDrawing500": {
"time": [
{
"min": 28.400000005960464,
"max": 51.099999994039536,
"median": 41.79999999701977,
"avg": 41.07499999925494,
"variance": 15.709374984912573,
"reliable": true,
"key": "element drawing"
},
{
"min": 22.400000005960464,
"max": 29,
"median": 25.599999994039536,
"avg": 25.399999998509884,
"variance": 3.570000005811453,
"reliable": true,
"key": "grid layout"
}
],
"status": "passed"
},
"elementDrawing5000": {
"time": [
{
"min": 213.79999999701977,
"max": 266.5,
"median": 223.09999999403954,
"avg": 230.39999999850988,
"variance": 165.17250003792344,
"reliable": true,
"key": "element drawing"
},
{
"min": 185.79999999701977,
"max": 224.1000000089407,
"median": 195.70000000298023,
"avg": 193.41250000149012,
"variance": 35.76859376054257,
"reliable": true,
"key": "grid layout"
}
],
"status": "passed"
}
}
}

View File

@ -0,0 +1,246 @@
{
"version": "1.0",
"device": {
"os": {
"arch": "arm64",
"distro": "macOS",
"serial": "9821ed36011eee5abf6c71d6fc2c03fb4bf4655e674c56b7f50e2560cb6e924a"
},
"cpu": {
"manufacturer": "Apple",
"brand": "M1 Pro",
"speed": 2.4,
"cores": 10
},
"memory": {
"total": 16384,
"free": 224.8125
},
"gpu": {
"vendor": "Apple",
"model": "Apple M1 Pro",
"cores": "16"
}
},
"repo": "7fbbb77580d806932e7b777b34856243600dbf35",
"client": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/128.0.6613.18 Safari/537.36",
"reports": {
"UpdateElementState": {
"time": [
{
"min": 115.70000000298023,
"max": 146.29999999701977,
"median": 118.5,
"avg": 119.1750000026077,
"variance": 11.989375011604281,
"reliable": true,
"key": "update element state to selected"
},
{
"min": 72.90000000596046,
"max": 81,
"median": 75.5,
"avg": 75.12500000186265,
"variance": 1.299374995883554,
"reliable": true,
"key": "update element state to default"
},
{
"min": 61.29999999701977,
"max": 73.59999999403954,
"median": 63.29999999701977,
"avg": 63.28749999962747,
"variance": 0.2935937537904829,
"reliable": true,
"key": "update element position"
}
],
"status": "passed"
},
"dataDiff1000": {
"time": [
{
"min": 4.399999991059303,
"max": 13.900000005960464,
"median": 6.4000000059604645,
"avg": 6.262500002980232,
"variance": 1.0873437525331973,
"reliable": false,
"key": "data diff"
}
],
"status": "passed"
},
"dataDiff10000": {
"time": [
{
"min": 2.8999999910593033,
"max": 13.700000002980232,
"median": 6.0999999940395355,
"avg": 6.199999999254942,
"variance": 3.767500012814999,
"reliable": false,
"key": "data diff"
}
],
"status": "passed"
},
"dataDiff100000": {
"time": [
{
"min": 10.899999991059303,
"max": 41.70000000298023,
"median": 12.299999997019768,
"avg": 17.51249999552965,
"variance": 96.928593772389,
"reliable": true,
"key": "data diff"
}
],
"status": "passed"
},
"dataDiff5000": {
"time": [
{
"min": 3.0999999940395355,
"max": 13,
"median": 3.6000000089406967,
"avg": 5.262499999254942,
"variance": 10.062343739960342,
"reliable": false,
"key": "data diff"
}
],
"status": "passed"
},
"dataDiff50000": {
"time": [
{
"min": 5.4000000059604645,
"max": 15.400000005960464,
"median": 8.399999991059303,
"avg": 8.549999998882413,
"variance": 8.840000012554228,
"reliable": true,
"key": "data diff"
}
],
"status": "passed"
},
"elementDrawing100": {
"time": [
{
"min": 10.200000002980232,
"max": 30.099999994039536,
"median": 18.600000008940697,
"avg": 19.499999998137355,
"variance": 17.06999999091029,
"reliable": true,
"key": "element drawing"
},
{
"min": 6.5999999940395355,
"max": 11.200000002980232,
"median": 8.400000005960464,
"avg": 8.550000002607703,
"variance": 0.5175000003352761,
"reliable": true,
"key": "grid layout"
}
],
"status": "passed"
},
"elementDrawing1000": {
"time": [
{
"min": 55.3999999910593,
"max": 75.5,
"median": 65.29999999701977,
"avg": 65.09999999962747,
"variance": 7.902500012740493,
"reliable": true,
"key": "element drawing"
},
{
"min": 31.700000002980232,
"max": 47,
"median": 36.20000000298023,
"avg": 36.34999999962747,
"variance": 5.345000000037253,
"reliable": true,
"key": "grid layout"
}
],
"status": "passed"
},
"elementDrawing10000": {
"time": [
{
"min": 404.40000000596046,
"max": 515.7999999970198,
"median": 420.6000000089407,
"avg": 427.75000000186265,
"variance": 282.06500002410263,
"reliable": true,
"key": "element drawing"
},
{
"min": 306.70000000298023,
"max": 341,
"median": 321.6000000089407,
"avg": 319.4125000014901,
"variance": 58.756093712486326,
"reliable": true,
"key": "grid layout"
}
],
"status": "passed"
},
"elementDrawing500": {
"time": [
{
"min": 25,
"max": 59.5,
"median": 40.70000000298023,
"avg": 42.03749999962747,
"variance": 15.694843770219013,
"reliable": true,
"key": "element drawing"
},
{
"min": 16.69999998807907,
"max": 24.299999997019768,
"median": 20.799999997019768,
"avg": 20.25,
"variance": 5.507500002086163,
"reliable": true,
"key": "grid layout"
}
],
"status": "passed"
},
"elementDrawing5000": {
"time": [
{
"min": 203.70000000298023,
"max": 264.30000001192093,
"median": 224.79999999701977,
"avg": 225.87499999813735,
"variance": 117.29687504237518,
"reliable": true,
"key": "element drawing"
},
{
"min": 154.29999999701977,
"max": 174.6000000089407,
"median": 164.19999998807907,
"avg": 161,
"variance": 32.70499999940395,
"reliable": true,
"key": "grid layout"
}
],
"status": "passed"
}
}
}

View File

@ -0,0 +1,38 @@
import { Graph } from '@antv/g6';
import type { Test } from 'iperf';
export const UpdateElementState: Test = async ({ container, perf }) => {
const nodes = Array(1000)
.fill(0)
.map((_, i) => ({ id: `${i}` }));
const edges = Array(999)
.fill(0)
.map((_, i) => ({ id: `edge-${i}`, source: `${i}`, target: `${i + 1}` }));
const graph = new Graph({
container,
animation: false,
data: { nodes, edges },
layout: { type: 'grid' },
});
await graph.render();
const selected = [...nodes, ...edges].map((element) => [element.id, 'selected']);
await perf.evaluate('update element state to selected', async () => {
await graph.setElementState(Object.fromEntries(selected));
});
const none = [...nodes, ...edges].map((element) => [element.id, []]);
await perf.evaluate('update element state to default', async () => {
await graph.setElementState(Object.fromEntries(none));
});
const position = nodes.map((node) => [node.id, [10, 10]]);
await perf.evaluate('update element position', async () => {
await graph.translateElementBy(Object.fromEntries(position), false);
});
};

View File

@ -154,9 +154,10 @@ export abstract class BaseCombo<S extends BaseComboStyleProps = BaseComboStylePr
return getExpandedBBox(childrenBBox, padding);
}
@effect((self, attributes) => self.getCollapsedMarkerStyle(attributes))
protected drawCollapsedMarkerShape(attributes: Required<S>, container: Group): void {
this.upsert('collapsed-marker', Icon, this.getCollapsedMarkerStyle(attributes), container);
const style = this.getCollapsedMarkerStyle(attributes);
if (!effect(this, 'collapsedMarker', style)) return;
this.upsert('collapsed-marker', Icon, style, container);
connectImage(this);
}

View File

@ -335,6 +335,8 @@ export abstract class BaseEdge extends BaseElement<BaseEdgeStyleProps> implement
if (enable) {
const arrowStyle = this.getArrowStyle(attributes, isStart);
if (!effect(this, `arrow-${type}`, arrowStyle)) return;
const [marker, markerOffset, arrowOffset] = isStart
? (['markerStart', 'markerStartOffset', 'startArrowOffset'] as const)
: (['markerEnd', 'markerEndOffset', 'endArrowOffset'] as const);
@ -376,35 +378,36 @@ export abstract class BaseEdge extends BaseElement<BaseEdgeStyleProps> implement
);
}
@effect((self, attributes) => self.getLabelStyle(attributes))
protected drawLabelShape(attributes: ParsedBaseEdgeStyleProps, container: Group) {
this.upsert('label', Label, this.getLabelStyle(attributes), container);
const style = this.getLabelStyle(attributes);
if (!effect(this, 'label', style)) return;
this.upsert('label', Label, style, container);
}
@effect((self, attributes) => self.getHaloStyle(attributes))
protected drawHaloShape(attributes: ParsedBaseEdgeStyleProps, container: Group) {
this.upsert('halo', Path, this.getHaloStyle(attributes), container);
const style = this.getHaloStyle(attributes);
if (!effect(this, 'halo', style)) return;
this.upsert('halo', Path, style, container);
}
@effect((self, attributes) => self.getBadgeStyle(attributes))
protected drawBadgeShape(attributes: ParsedBaseEdgeStyleProps, container: Group) {
this.upsert('badge', Badge, this.getBadgeStyle(attributes), container);
const style = this.getBadgeStyle(attributes);
if (!effect(this, 'badge', style)) return;
this.upsert('badge', Badge, style, container);
}
@effect((self, attributes) => self.getArrowStyle(attributes, 'start'))
protected drawSourceArrow(attributes: ParsedBaseEdgeStyleProps) {
this.drawArrow(attributes, 'start');
}
@effect((self, attributes) => self.getArrowStyle(attributes, 'end'))
protected drawTargetArrow(attributes: ParsedBaseEdgeStyleProps) {
this.drawArrow(attributes, 'end');
}
@effect((self, attributes) => self.getKeyStyle(attributes))
protected drawKeyShape(attributes: ParsedBaseEdgeStyleProps, container: Group): Path | undefined {
const key = this.upsert('key', Path, this.getKeyStyle(attributes), container);
return key;
const style = this.getKeyStyle(attributes);
if (!effect(this, 'key', style)) return;
return this.upsert('key', Path, style, container);
}
public render(attributes = this.parsedAttributes, container: Group = this): void {

View File

@ -1,61 +1,27 @@
import type { Element } from '../types';
import { getCachedStyle, setCacheStyle } from '../utils/cache';
const EFFECT_WEAKMAP = new WeakMap<Element, Record<string, any>>();
/**
* <zh/>
* <zh/>
*
* <en/> Optimize the number of method executions, and only execute the function when the style attributes change
* @param styler - <zh/> | <en/> Get style attribute function
* @returns <zh/> | <en/> Decorator
* @remarks
* <zh/> getStyle 使 attributes attributes
*
* shapeKey, attributes getStyle 使
*
* <en/> Only when getStyle is specified, the function will be called with the current attributes and the new attributes respectively. If they are the same, the function will not be executed.
*
* If shapeKey is specified, the attributes of the shape will be directly obtained as the original style attributes, which is usually used when the bounding box of the element is used in the getStyle function.
* @example
* <zh/> value
*
* <en/> Execute the function only when value changes
*
* ```typescript
* class CustomNode extends BaseNode {
*
* @effect((self, attributes) => {
* const { value } = attributes;
* return { value }
* })
* drawCustomShape(attributes, container) {
* this.upsert('custom', 'circle', { ...attributes }, container);
* }
* }
* ```
* <en/> Determine whether the given style are the same as the previous ones
* @param target - <zh/> | <en/> Target element
* @param key - <zh/> key | <en/> Cache key
* @param style - <zh/> | <en/> Style attribute
* @returns <zh/> | <en/> Whether to execute the function
*/
export function effect(styler: (self: any, attributes: Record<string, unknown>) => Record<string, unknown>) {
return function (target: Element, propertyKey: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value;
descriptor.value = function (this: Element, attr: Record<string, unknown>, ...rest: unknown[]) {
// 初始化后需要执行首次调用 / First call after initialization
const initKey = `${propertyKey}_invoke`;
if (!getCachedStyle(this, initKey)) {
setCacheStyle(this, initKey, true);
return fn.call(this, attr, ...rest);
}
const styleKey = `${propertyKey}_style`;
const original = getCachedStyle(this, styleKey);
const modified = styler(this, attr);
setCacheStyle(this, styleKey, modified);
if (isStyleEqual(original, modified)) return null;
return fn.call(this, attr, ...rest);
};
return descriptor;
};
export function effect<T extends false | Record<string, any>>(target: Element, key: string, style: T): boolean {
if (!EFFECT_WEAKMAP.has(target)) EFFECT_WEAKMAP.set(target, {});
const cache = EFFECT_WEAKMAP.get(target)!;
if (!cache[key]) {
cache[key] = style;
return true;
}
const original = cache[key];
if (isStyleEqual(original, style)) return false;
cache[key] = style;
return true;
}
/**
@ -71,7 +37,7 @@ export function effect(styler: (self: any, attributes: Record<string, unknown>)
*
* <en/> Perform a second-level shallow comparison to compare complex shape attributes such as badges and ports
*/
const isStyleEqual = (a: Record<string, unknown>, b: Record<string, unknown>, depth = 2): boolean => {
const isStyleEqual = (a: false | Record<string, unknown>, b: false | Record<string, unknown>, depth = 2): boolean => {
if (typeof a !== 'object' || typeof b !== 'object') return a === b;
const keys1 = Object.keys(a);

View File

@ -361,49 +361,54 @@ export abstract class BaseNode<S extends BaseNodeStyleProps = BaseNodeStyleProps
return getRectIntersectPoint(point, keyShapeBounds);
}
@effect((self, attributes) => self.getHaloStyle(attributes))
protected drawHaloShape(attributes: Required<S>, container: Group): void {
const style = this.getHaloStyle(attributes);
if (!effect(this, 'halo', style)) return;
const keyShape = this.getShape('key');
this.upsert(
'halo',
keyShape.constructor as new (...args: unknown[]) => DisplayObject,
this.getHaloStyle(attributes),
container,
);
this.upsert('halo', keyShape.constructor as new (...args: unknown[]) => DisplayObject, style, container);
}
@effect((self, attributes) => self.getIconStyle(attributes))
protected drawIconShape(attributes: Required<S>, container: Group): void {
this.upsert('icon', Icon, this.getIconStyle(attributes), container);
const style = this.getIconStyle(attributes);
if (!effect(this, 'icon', style)) return;
this.upsert('icon', Icon, style, container);
connectImage(this);
}
@effect((self, attributes) => self.getBadgesStyle(attributes))
protected drawBadgeShapes(attributes: Required<S>, container: Group): void {
const badgesStyle = this.getBadgesStyle(attributes);
Object.keys(badgesStyle).forEach((key) => {
this.upsert(`badge-${key}`, Badge, badgesStyle[key], container);
const style = badgesStyle[key];
if (!effect(this, `badge-${key}`, style)) return;
this.upsert(`badge-${key}`, Badge, style, container);
});
}
@effect((self, attributes) => self.getPortsStyle(attributes))
protected drawPortShapes(attributes: Required<S>, container: Group): void {
const portsStyle = this.getPortsStyle(attributes);
Object.keys(portsStyle).forEach((key) => {
this.upsert(`port-${key}`, GCircle, portsStyle[key] as CircleStyleProps, container);
const style = portsStyle[key] as CircleStyleProps;
const shapeKey = `port-${key}`;
if (!effect(this, shapeKey, style)) return;
this.upsert(shapeKey, GCircle, style, container);
});
}
@effect((self, attributes) => self.getLabelStyle(attributes))
protected drawLabelShape(attributes: Required<S>, container: Group): void {
this.upsert('label', Label, this.getLabelStyle(attributes), container);
const style = this.getLabelStyle(attributes);
if (!effect(this, 'label', style)) return;
this.upsert('label', Label, style, container);
}
protected abstract drawKeyShape(attributes: Required<S>, container: Group): DisplayObject | undefined;
// 用于装饰抽象方法 / Used to decorate abstract methods
@effect((self, attributes) => self.getKeyStyle(attributes))
private _drawKeyShape(attributes: Required<S>, container: Group) {
return this.drawKeyShape(attributes, container);
}

View File

@ -47,7 +47,7 @@ export abstract class BaseShape<StyleProps extends BaseShapeStyleProps> extends
* <en> create, update or remove shape
* @param className - <zh> | <en> shape name
* @param Ctor - <zh> | <en> shape type
* @param style - <zh> | <en> shape style
* @param style - <zh> false | <en> shape style. Pass false to remove the shape
* @param container - <zh> | <en> container
* @param hooks - <zh> | <en> hooks
* @returns <zh> | <en> shape instance

View File

@ -28,7 +28,6 @@ export {
} from './constants';
export { BaseCombo, CircleCombo, RectCombo } from './elements/combos';
export { BaseEdge, Cubic, CubicHorizontal, CubicVertical, Line, Polyline, Quadratic } from './elements/edges';
export { effect } from './elements/effect';
export {
BaseNode,
Circle,