From 71470406ba591178fcdd69dbf652b131f2a67792 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:21:28 +0000 Subject: [PATCH] chore: memory leak fixes and tests --- .../dockview/dockviewComponent.spec.ts | 212 +++++++++--------- .../src/dockview/dockviewComponent.ts | 6 + .../src/dockview/dockviewGroupPanelModel.ts | 4 +- packages/dockview-core/src/events.ts | 8 +- .../src/gridview/baseComponentGridview.ts | 2 + 5 files changed, 123 insertions(+), 109 deletions(-) diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index fd5981b78..e1ea35cbd 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -21,7 +21,6 @@ import { DockviewApi } from '../../api/component.api'; import { DockviewDndOverlayEvent } from '../../dockview/options'; import { SizeEvent } from '../../api/gridviewPanelApi'; import { setupMockWindow } from '../__mocks__/mockWindow'; -import { exhaustMicrotaskQueue } from '../__test_utils__/utils'; class PanelContentPartTest implements IContentRenderer { element: HTMLElement = document.createElement('div'); @@ -141,109 +140,114 @@ describe('dockviewComponent', () => { expect(dockview.element.className).toBe('test-b test-c'); }); - // describe('memory leakage', () => { - // beforeEach(() => { - // window.open = () => fromPartial({ - // addEventListener: jest.fn(), - // close: jest.fn(), - // }); - // }); - - // test('event leakage', () => { - // Emitter.setLeakageMonitorEnabled(true); - - // dockview = new DockviewComponent({ - // parentElement: container, - // components: { - // default: PanelContentPartTest, - // }, - // }); - - // dockview.layout(500, 1000); - - // const panel1 = dockview.addPanel({ - // id: 'panel1', - // component: 'default', - // }); - - // const panel2 = dockview.addPanel({ - // id: 'panel2', - // component: 'default', - // }); - - // dockview.removePanel(panel2); - - // const panel3 = dockview.addPanel({ - // id: 'panel3', - // component: 'default', - // position: { - // direction: 'right', - // referencePanel: 'panel1', - // }, - // }); - - // const panel4 = dockview.addPanel({ - // id: 'panel4', - // component: 'default', - // position: { - // direction: 'above', - // }, - // }); - - // dockview.moveGroupOrPanel( - // panel4.group, - // panel3.group.id, - // panel3.id, - // 'center' - // ); - - // dockview.addPanel({ - // id: 'panel5', - // component: 'default', - // floating: true, - // }); - - // const panel6 = dockview.addPanel({ - // id: 'panel6', - // component: 'default', - // position: { - // referencePanel: 'panel5', - // direction: 'within', - // }, - // }); - - // dockview.addFloatingGroup(panel4.api.group); - - // dockview.addPopoutGroup(panel6); - - // dockview.moveGroupOrPanel( - // panel1.group, - // panel6.group.id, - // panel6.id, - // 'center' - // ); - - // dockview.moveGroupOrPanel( - // panel4.group, - // panel6.group.id, - // panel6.id, - // 'center' - // ); - - // dockview.dispose(); - - // if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { - // for (const entry of Array.from( - // Emitter.MEMORY_LEAK_WATCHER.events - // )) { - // console.log('disposal', entry[1]); - // } - // throw new Error('not all listeners disposed'); - // } - - // Emitter.setLeakageMonitorEnabled(false); - // }); - // }); + describe('memory leakage', () => { + beforeEach(() => { + window.open = () => setupMockWindow(); + }); + + test('event leakage', async () => { + Emitter.setLeakageMonitorEnabled(true); + + dockview = new DockviewComponent(container, { + createComponent(options) { + switch (options.name) { + case 'default': + return new PanelContentPartTest( + options.id, + options.name + ); + default: + throw new Error(`unsupported`); + } + }, + className: 'test-a test-b', + }); + + dockview.layout(500, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + }); + + dockview.removePanel(panel2); + + const panel3 = dockview.addPanel({ + id: 'panel3', + component: 'default', + position: { + direction: 'right', + referencePanel: 'panel1', + }, + }); + + const panel4 = dockview.addPanel({ + id: 'panel4', + component: 'default', + position: { + direction: 'above', + }, + }); + + panel4.api.group.api.moveTo({ + group: panel3.api.group, + position: 'center', + }); + + dockview.addPanel({ + id: 'panel5', + component: 'default', + floating: true, + }); + + const panel6 = dockview.addPanel({ + id: 'panel6', + component: 'default', + position: { + referencePanel: 'panel5', + direction: 'within', + }, + }); + + dockview.addFloatingGroup(panel4.api.group); + + await dockview.addPopoutGroup(panel2); + + panel1.api.group.api.moveTo({ + group: panel6.api.group, + position: 'center', + }); + + panel4.api.group.api.moveTo({ + group: panel6.api.group, + position: 'center', + }); + + dockview.dispose(); + + if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { + console.warn( + `${Emitter.MEMORY_LEAK_WATCHER.size} undisposed resources` + ); + + for (const entry of Array.from( + Emitter.MEMORY_LEAK_WATCHER.events + )) { + console.log('disposal', entry[1]); + } + throw new Error( + `${Emitter.MEMORY_LEAK_WATCHER.size} undisposed resources` + ); + } + + Emitter.setLeakageMonitorEnabled(false); + }); + }); test('duplicate panel', () => { dockview.layout(500, 1000); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 42cbe01c7..d0d1c39aa 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -409,6 +409,7 @@ export class DockviewComponent this._onDidRemoveGroup, this._onDidActiveGroupChange, this._onUnhandledDragOverEvent, + this._onDidMaximizedGroupChange, this.onDidViewVisibilityChangeMicroTaskQueue(() => { this.updateWatermark(); }), @@ -576,6 +577,11 @@ export class DockviewComponent this.updateWatermark(); } + override dispose(): void { + this.clear(); // explicitly clear the layout before cleaning up + super.dispose(); + } + override setVisible(panel: DockviewGroupPanel, visible: boolean): void { switch (panel.api.location.type) { case 'grid': diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 17af6e573..a34d5ef10 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -506,7 +506,9 @@ export class DockviewGroupPanelModel this._onDidAddPanel, this._onDidRemovePanel, this._onDidActivePanelChange, - this._onUnhandledDragOverEvent + this._onUnhandledDragOverEvent, + this._onDidPanelTitleChange, + this._onDidPanelParametersChange ); } diff --git a/packages/dockview-core/src/events.ts b/packages/dockview-core/src/events.ts index 5c7d0d260..ad35ac382 100644 --- a/packages/dockview-core/src/events.ts +++ b/packages/dockview-core/src/events.ts @@ -158,10 +158,10 @@ export class Emitter implements IDisposable { queueMicrotask(() => { // don't check until stack of execution is completed to allow for out-of-order disposals within the same execution block for (const listener of this._listeners) { - console.warn( - 'dockview: stacktrace', - listener.stacktrace?.print() - ); + // console.warn( + // 'dockview: stacktrace', + // listener.stacktrace?.print() + // ); } }); } diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index 44e810a50..9d02993f3 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -205,6 +205,8 @@ export abstract class BaseGrid )(() => { this._bufferOnDidLayoutChange.fire(); }), + this._onDidMaximizedChange, + this._onDidViewVisibilityChangeMicroTaskQueue, this._bufferOnDidLayoutChange ); }