fix: fix issue that cause memory leak (#5587)

* fix: fix issue that may cause memories lack

* test: adjust test case to destroy graph
This commit is contained in:
Aaron 2024-03-29 11:45:18 +08:00 committed by GitHub
parent 72c5e74611
commit bcd080432f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 143 additions and 40 deletions

View File

@ -9,6 +9,10 @@ describe('behavior combo expand collapse', () => {
graph = await createDemoGraph(comboExpandCollapse, { animation: false }); graph = await createDemoGraph(comboExpandCollapse, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);
}); });

View File

@ -11,6 +11,10 @@ describe('behavior drag canvas', () => {
graph = await createDemoGraph(behaviorDragCanvas, { animation: false }); graph = await createDemoGraph(behaviorDragCanvas, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', () => { it('default status', () => {
expect(graph.getBehaviors()).toEqual([ expect(graph.getBehaviors()).toEqual([
'drag-canvas', 'drag-canvas',
@ -78,8 +82,4 @@ describe('behavior drag canvas', () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);
}); });
it('destroy', () => {
graph.destroy();
});
}); });

View File

@ -9,6 +9,10 @@ describe('behavior drag combo', () => {
graph = await createDemoGraph(comboExpandCollapse, { animation: false }); graph = await createDemoGraph(comboExpandCollapse, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
graph.setBehaviors([{ type: 'drag-element', dropEffect: 'link' }]); graph.setBehaviors([{ type: 'drag-element', dropEffect: 'link' }]);
graph.expand('combo-1'); graph.expand('combo-1');

View File

@ -10,6 +10,10 @@ describe('behavior drag element', () => {
graph = await createDemoGraph(behaviorDragNode, { animation: false }); graph = await createDemoGraph(behaviorDragNode, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);

View File

@ -11,6 +11,10 @@ describe('behavior zoom canvas', () => {
graph = await createDemoGraph(behaviorZoomCanvas, { animation: false }); graph = await createDemoGraph(behaviorZoomCanvas, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', () => { it('default status', () => {
expect(graph.getZoom()).toBe(1); expect(graph.getZoom()).toBe(1);
expect(graph.getBehaviors()).toEqual([{ type: 'zoom-canvas' }]); expect(graph.getBehaviors()).toEqual([{ type: 'zoom-canvas' }]);
@ -191,8 +195,4 @@ describe('behavior zoom canvas', () => {
container?.dispatchEvent(new Event(ContainerEvent.KEY_DOWN)); container?.dispatchEvent(new Event(ContainerEvent.KEY_DOWN));
expect(keydownListener).toHaveBeenCalledTimes(1); expect(keydownListener).toHaveBeenCalledTimes(1);
}); });
it('destroy', () => {
graph.destroy();
});
}); });

View File

@ -9,6 +9,10 @@ describe('element change type', () => {
graph = await createDemoGraph(elementChangeType, { animation: false }); graph = await createDemoGraph(elementChangeType, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);
}); });

View File

@ -9,6 +9,10 @@ describe('combo', () => {
graph = await createDemoGraph(combo, { animation: false }); graph = await createDemoGraph(combo, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);
}); });

View File

@ -26,6 +26,10 @@ describe('edge polyline', () => {
graph = await createDemoGraph(edgePolyline, { animation: false }); graph = await createDemoGraph(edgePolyline, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('Control Points', async () => { it('Control Points', async () => {
updateEdgeStyle(graph, 'edge-1', 'controlPoints', [[300, 190]]); updateEdgeStyle(graph, 'edge-1', 'controlPoints', [[300, 190]]);

View File

@ -25,6 +25,10 @@ describe('element port', () => {
graph = await createDemoGraph(elementPort, { animation: false }); graph = await createDemoGraph(elementPort, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
await expect(graph).toMatchSnapshot(__filename, 'port_hidden'); await expect(graph).toMatchSnapshot(__filename, 'port_hidden');
}); });

View File

@ -9,6 +9,10 @@ describe('element position combo', () => {
graph = await createDemoGraph(elementPositionCombo, { animation: false }); graph = await createDemoGraph(elementPositionCombo, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);
}); });

View File

@ -9,6 +9,10 @@ describe('element position', () => {
graph = await createDemoGraph(elementPosition, { animation: false }); graph = await createDemoGraph(elementPosition, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);
}); });

View File

@ -9,6 +9,10 @@ describe('element state', () => {
graph = await createDemoGraph(elementState, { animation: false }); graph = await createDemoGraph(elementState, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);
}); });

View File

@ -9,6 +9,10 @@ describe('element visibility', () => {
graph = await createDemoGraph(elementVisibility, { animation: false }); graph = await createDemoGraph(elementVisibility, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);
}); });

View File

@ -9,6 +9,10 @@ describe('element zIndex', () => {
graph = await createDemoGraph(elementZIndex, { animation: false }); graph = await createDemoGraph(elementZIndex, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('default status', async () => { it('default status', async () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);
}); });

View File

@ -14,6 +14,10 @@ describe('plugin grid line', () => {
.getElementsByClassName('g6-grid-line')! as HTMLCollectionOf<HTMLElement>; .getElementsByClassName('g6-grid-line')! as HTMLCollectionOf<HTMLElement>;
}); });
afterAll(() => {
graph.destroy();
});
it('default status', () => { it('default status', () => {
expect(graph.getPlugins()).toEqual([{ type: 'grid-line', follow: false }]); expect(graph.getPlugins()).toEqual([{ type: 'grid-line', follow: false }]);
expect(gridLineElement.length).toBe(1); expect(gridLineElement.length).toBe(1);

View File

@ -8,10 +8,15 @@ import { omit } from '@antv/util';
describe('ElementController', () => { describe('ElementController', () => {
let graph: Graph; let graph: Graph;
beforeAll(async () => { beforeAll(async () => {
graph = await createDemoGraph(graphElement); graph = await createDemoGraph(graphElement);
}); });
afterAll(() => {
graph.destroy();
});
it('static', async () => { it('static', async () => {
await expect(graph).toMatchSnapshot(__filename); await expect(graph).toMatchSnapshot(__filename);

View File

@ -316,7 +316,7 @@ describe('Graph', () => {
it('destroy', () => { it('destroy', () => {
graph.destroy(); graph.destroy();
// @ts-expect-error context is private. // @ts-expect-error context is private.
expect(graph.context).toBeUndefined(); expect(graph.context).toEqual({});
expect(graph.destroyed).toBe(true); expect(graph.destroyed).toBe(true);
}); });
}); });

View File

@ -6,10 +6,15 @@ import { AABB } from '@antv/g';
describe('ViewportController', () => { describe('ViewportController', () => {
let graph: Graph; let graph: Graph;
beforeAll(async () => { beforeAll(async () => {
graph = await createDemoGraph(controllerViewport); graph = await createDemoGraph(controllerViewport);
}); });
afterAll(() => {
graph.destroy();
});
it('viewport center', () => { it('viewport center', () => {
expect(graph.getViewportCenter()).toBeCloseTo([250, 250, 0]); expect(graph.getViewportCenter()).toBeCloseTo([250, 250, 0]);
}); });
@ -104,18 +109,19 @@ describe('ViewportController', () => {
// @ts-expect-error // @ts-expect-error
expect(graph.context.viewport.getBBoxInViewport(bbox).halfExtents).toBeCloseTo([100, 100, 0]); expect(graph.context.viewport.getBBoxInViewport(bbox).halfExtents).toBeCloseTo([100, 100, 0]);
}); });
afterAll(() => {
graph.destroy();
});
}); });
describe('Viewport Fit without Animation', () => { describe('Viewport Fit without Animation', () => {
let graph: Graph; let graph: Graph;
beforeAll(async () => { beforeAll(async () => {
graph = await createDemoGraph(viewportFit); graph = await createDemoGraph(viewportFit);
}); });
afterAll(() => {
graph.destroy();
});
it('default', async () => { it('default', async () => {
await expect(graph).toMatchSnapshot(__filename, 'before-fit'); await expect(graph).toMatchSnapshot(__filename, 'before-fit');
}); });
@ -144,18 +150,19 @@ describe('Viewport Fit without Animation', () => {
await graph.fitCenter(); await graph.fitCenter();
await expect(graph).toMatchSnapshot(__filename, 're-fitCenter'); await expect(graph).toMatchSnapshot(__filename, 're-fitCenter');
}); });
afterAll(() => {
graph.destroy();
});
}); });
describe('Viewport Fit with Animation', () => { describe('Viewport Fit with Animation', () => {
let graph: Graph; let graph: Graph;
beforeAll(async () => { beforeAll(async () => {
graph = await createDemoGraph(viewportFit, { animation: true }); graph = await createDemoGraph(viewportFit, { animation: true });
}); });
afterAll(() => {
graph.destroy();
});
it('default', async () => { it('default', async () => {
await expect(graph).toMatchSnapshot(__filename, 'before-fit-animation'); await expect(graph).toMatchSnapshot(__filename, 'before-fit-animation');
}); });
@ -184,10 +191,6 @@ describe('Viewport Fit with Animation', () => {
await graph.fitCenter(); await graph.fitCenter();
await expect(graph).toMatchSnapshot(__filename, 're-fitCenter-animation'); await expect(graph).toMatchSnapshot(__filename, 're-fitCenter-animation');
}); });
afterAll(() => {
graph.destroy();
});
}); });
describe('Viewport Fit with AutoFit and Padding without Animation', () => { describe('Viewport Fit with AutoFit and Padding without Animation', () => {
@ -199,6 +202,10 @@ describe('Viewport Fit with AutoFit and Padding without Animation', () => {
}); });
}); });
afterAll(() => {
graph.destroy();
});
it('default', async () => { it('default', async () => {
await expect(graph).toMatchSnapshot(__filename, 'auto-fit-with-padding'); await expect(graph).toMatchSnapshot(__filename, 'auto-fit-with-padding');
}); });
@ -214,6 +221,10 @@ describe('Viewport Fit with AutoFit and Padding with Animation', () => {
}); });
}); });
afterAll(() => {
graph.destroy();
});
it('default', async () => { it('default', async () => {
await expect(graph).toMatchSnapshot(__filename, 'auto-fit-with-padding-animation'); await expect(graph).toMatchSnapshot(__filename, 'auto-fit-with-padding-animation');
}); });

View File

@ -9,6 +9,10 @@ describe('spec theme', () => {
graph = await createDemoGraph(theme, { animation: false }); graph = await createDemoGraph(theme, { animation: false });
}); });
afterAll(() => {
graph.destroy();
});
it('theme', async () => { it('theme', async () => {
const theme: ThemeOptions = 'light'; const theme: ThemeOptions = 'light';

View File

@ -43,6 +43,7 @@
"dev": "vite", "dev": "vite",
"fix": "eslint ./src ./__tests__ --fix && prettier ./src __tests__ --write ", "fix": "eslint ./src ./__tests__ --fix && prettier ./src __tests__ --write ",
"jest": "node --expose-gc --max-old-space-size=4096 --unhandled-rejections=strict --experimental-vm-modules ../../node_modules/jest/bin/jest --coverage --logHeapUsage --detectOpenHandles", "jest": "node --expose-gc --max-old-space-size=4096 --unhandled-rejections=strict --experimental-vm-modules ../../node_modules/jest/bin/jest --coverage --logHeapUsage --detectOpenHandles",
"jest:inspect": "node --inspect --expose-gc --max-old-space-size=4096 --unhandled-rejections=strict --experimental-vm-modules ../../node_modules/jest/bin/jest --coverage --logHeapUsage --detectOpenHandles --runInBand",
"lint": "eslint ./src __tests__ --quiet && prettier ./src __tests__ --check", "lint": "eslint ./src __tests__ --quiet && prettier ./src __tests__ --check",
"prepublishOnly": "npm run ci", "prepublishOnly": "npm run ci",
"size": "limit-size", "size": "limit-size",

View File

@ -148,4 +148,10 @@ export abstract class BaseShape<StyleProps extends BaseShapeStyleProps> extends
const { visibility } = this.attributes; const { visibility } = this.attributes;
setVisibility(this, visibility); setVisibility(this, visibility);
} }
public destroy(): void {
this.shapeMap = {};
this.animateMap = {};
super.destroy();
}
} }

View File

@ -66,7 +66,7 @@ export class Tooltip extends BasePlugin<TooltipOptions> {
} }
public update(options: Partial<TooltipOptions>) { public update(options: Partial<TooltipOptions>) {
this.unbundEvents(); this.unbindEvents();
super.update(options); super.update(options);
if (this.tooltipElement) { if (this.tooltipElement) {
this.container?.removeChild(this.tooltipElement.HTMLTooltipElement); this.container?.removeChild(this.tooltipElement.HTMLTooltipElement);
@ -83,7 +83,7 @@ export class Tooltip extends BasePlugin<TooltipOptions> {
this.tooltipElement = this.initTooltip(); this.tooltipElement = this.initTooltip();
} }
private unbundEvents() { private unbindEvents() {
const { graph } = this.context; const { graph } = this.context;
/** The previous event binding needs to be removed when updating the trigger. */ /** The previous event binding needs to be removed when updating the trigger. */
const events = this.getEvents(); const events = this.getEvents();
@ -259,7 +259,7 @@ export class Tooltip extends BasePlugin<TooltipOptions> {
}; };
public destroy(): void { public destroy(): void {
this.unbundEvents(); this.unbindEvents();
if (this.tooltipElement) { if (this.tooltipElement) {
this.container?.removeChild(this.tooltipElement.HTMLTooltipElement); this.container?.removeChild(this.tooltipElement.HTMLTooltipElement);
} }

View File

@ -72,11 +72,9 @@ export abstract class ExtensionController<Extension extends BaseExtension<Loosel
public destroy() { public destroy() {
Object.values(this.extensionMap).forEach((extension) => extension.destroy()); Object.values(this.extensionMap).forEach((extension) => extension.destroy());
// @ts-expect-error force delete // @ts-expect-error force delete
delete this.context; this.context = {};
// @ts-expect-error force delete this.extensions = [];
delete this.extensions; this.extensionMap = {};
// @ts-expect-error force delete
delete this.extensionMap;
} }
} }
@ -105,9 +103,9 @@ export class BaseExtension<T extends LooselyExtensionOption> {
public destroy() { public destroy() {
// @ts-expect-error force delete // @ts-expect-error force delete
delete this.context; this.context = {};
// @ts-expect-error force delete // @ts-expect-error force delete
delete this.options; this.options = {};
this.destroyed = true; this.destroyed = true;
} }

View File

@ -27,7 +27,7 @@ export class BehaviorController extends ExtensionController<BaseBehavior<CustomB
const container = this.context.canvas.getContainer(); const container = this.context.canvas.getContainer();
if (container) { if (container) {
[ContainerEvent.KEY_DOWN, ContainerEvent.KEY_UP].forEach((name) => { [ContainerEvent.KEY_DOWN, ContainerEvent.KEY_UP].forEach((name) => {
container.addEventListener(name, this.forwardContainerEvents.bind(this)); container.addEventListener(name, this.forwardContainerEvents);
}); });
} }
@ -105,7 +105,18 @@ export class BehaviorController extends ExtensionController<BaseBehavior<CustomB
} }
}; };
private forwardContainerEvents(event: FocusEvent | KeyboardEvent) { private forwardContainerEvents = (event: FocusEvent | KeyboardEvent) => {
this.context.graph.emit(event.type, event); this.context.graph.emit(event.type, event);
};
public destroy(): void {
const container = this.context.canvas.getContainer();
if (container) {
[ContainerEvent.KEY_DOWN, ContainerEvent.KEY_UP].forEach((name) => {
container.removeEventListener(name, this.forwardContainerEvents);
});
}
this.context.canvas.document.removeAllEventListeners();
super.destroy();
} }
} }

View File

@ -253,7 +253,10 @@ export class Canvas {
} }
public destroy() { public destroy() {
Object.values(this.canvas).forEach((canvas) => { this.config = {};
// @ts-expect-error force delete
this.renderers = {};
Object.entries(this.canvas).forEach(([name, canvas]) => {
const camera = canvas.getCamera(); const camera = canvas.getCamera();
// @ts-expect-error landmark is private // @ts-expect-error landmark is private
if (camera.landmarks?.length) { if (camera.landmarks?.length) {
@ -261,6 +264,8 @@ export class Canvas {
} }
canvas.destroy(); canvas.destroy();
// @ts-expect-error force delete
this[name] = undefined;
}); });
} }
} }

View File

@ -759,7 +759,7 @@ export class DataController {
model.removeNodes(nodes.map((node) => node.id)); model.removeNodes(nodes.map((node) => node.id));
// @ts-expect-error force delete // @ts-expect-error force delete
delete this.context; this.context = {};
} }
} }

View File

@ -718,14 +718,19 @@ export class ElementController {
} }
public destroy() { public destroy() {
Object.values(this.elementMap).forEach((element) => element.destroy());
Object.values(this.container).forEach((container) => container.destroy()); Object.values(this.container).forEach((container) => container.destroy());
// @ts-expect-error force delete
this.container = {};
this.elementMap = {}; this.elementMap = {};
this.shapeTypeMap = {}; this.shapeTypeMap = {};
this.defaultStyle = {}; this.defaultStyle = {};
this.stateStyle = {}; this.stateStyle = {};
this.paletteStyle = {}; this.paletteStyle = {};
// @ts-expect-error force delete // @ts-expect-error force delete
delete this.context; this.context = {};
// @ts-expect-error force delete
this.latestElementVisibilityMap = undefined;
} }
} }

View File

@ -503,16 +503,17 @@ export class Graph extends EventEmitter {
public destroy(): void { public destroy(): void {
const { layout, element, model, canvas, behavior, plugin } = this.context; const { layout, element, model, canvas, behavior, plugin } = this.context;
plugin?.destroy();
behavior?.destroy();
layout?.destroy(); layout?.destroy();
element?.destroy(); element?.destroy();
model.destroy(); model.destroy();
canvas?.destroy(); canvas?.destroy();
behavior?.destroy();
plugin?.destroy();
this.options = {}; this.options = {};
// @ts-expect-error force delete // @ts-expect-error force delete
delete this.context; this.context = {};
this.off();
window.removeEventListener('resize', this.onResize); window.removeEventListener('resize', this.onResize);
this.destroyed = true; this.destroyed = true;

View File

@ -381,7 +381,11 @@ export class LayoutController {
public destroy() { public destroy() {
this.stopLayout(); this.stopLayout();
// @ts-expect-error force delete // @ts-expect-error force delete
delete this.context; this.context = {};
this.supervisor?.kill();
this.supervisor = undefined;
this.instance = undefined;
this.animationResult = undefined;
} }
} }