Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions packages/devextreme-angular/tests/src/ui/memory-leak.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { DxButtonModule, DxSliderModule } from 'devextreme-angular';

@Component({
standalone: false,
selector: 'test-container-component',
template: '',
})
class TestContainerComponent {
isVisible = true;
}

async function forceGC(times = 3): Promise<void> {
for (let i = 0; i < times; i++) {
Array.from({ length: 10_000 }, () => ({ data: new Array(100) }));
globalThis.gc?.();
}

await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}

describe('Memory leak tests', () => {
const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;

beforeAll(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;

TestBed.configureTestingModule({
declarations: [TestContainerComponent],
imports: [DxButtonModule, DxSliderModule],
});
});

afterAll(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});

it('should not memory leak after change @if block with DxButton (T1324584)', async () => {
TestBed.overrideComponent(TestContainerComponent, {
set: {
template: '@if (isVisible) {<dx-button text="BUTTON"></dx-button>}',
},
});

const fixture = TestBed.createComponent(TestContainerComponent);
const component: TestContainerComponent = fixture.componentInstance;

fixture.detectChanges();

for (let i = 0; i < 100; i++) {
component.isVisible = !component.isVisible;
fixture.detectChanges();
}

const memoryBefore = await (performance as any).measureUserAgentSpecificMemory();

for (let i = 0; i < 100; i++) {
component.isVisible = !component.isVisible;
fixture.detectChanges();
}

await forceGC();

const memoryAfter = await (performance as any).measureUserAgentSpecificMemory();
const memoryDiff = Math.round((memoryAfter.bytes - memoryBefore.bytes) / 1024);

expect(memoryDiff).toBeLessThan(150);
});

it('should not memory leak after change @if block with DxSlider (T1324584)', async () => {
TestBed.overrideComponent(TestContainerComponent, {
set: {
template: '@if (isVisible) {<dx-slider></dx-slider>}',
},
});

const fixture = TestBed.createComponent(TestContainerComponent);
const component: TestContainerComponent = fixture.componentInstance;

fixture.detectChanges();

for (let i = 0; i < 100; i++) {
component.isVisible = !component.isVisible;
fixture.detectChanges();
}

const memoryBefore = await (performance as any).measureUserAgentSpecificMemory();

for (let i = 0; i < 100; i++) {
component.isVisible = !component.isVisible;
fixture.detectChanges();
}

await forceGC();

const memoryAfter = await (performance as any).measureUserAgentSpecificMemory();
const memoryDiff = Math.round((memoryAfter.bytes - memoryBefore.bytes) / 1024);

expect(memoryDiff).toBeLessThan(200);
});
});
3 changes: 3 additions & 0 deletions packages/devextreme/js/__internal/core/m_inferno_renderer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable spellcheck/spell-checker */
import { keyboard } from '@js/common/core/events/short';
import domAdapter from '@js/core/dom_adapter';
import { cleanDataRecursive } from '@js/core/element_data';
import injector from '@js/core/utils/dependency_injector';
Expand All @@ -7,6 +8,8 @@ import { render } from 'inferno';
import { createElement } from 'inferno-create-element';

const remove = (element) => {
keyboard.disposeProcessorsForSubtree(element);

const { parentNode } = element;

if (parentNode) {
Expand Down
51 changes: 51 additions & 0 deletions packages/devextreme/js/__internal/events/m_short.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,57 @@ export const keyboard = {
}
},

disposeProcessorsForSubtree(root: Element): void {
if (!root?.nodeType) {
return;
}

const toElements = (value: unknown): Element[] => {
if (!value) {
return [];
}

if (value instanceof Element) {
return [value];
}

if (Array.isArray(value)) {
return value.filter((item): item is Element => item instanceof Element);
}

const v = value as { toArray?: () => unknown[]; 0?: unknown };

if (typeof v.toArray === 'function') {
const arr = v.toArray();

if (Array.isArray(arr)) {
return arr.filter((item): item is Element => item instanceof Element);
}
}

const first = v[0];

return first instanceof Element ? [first] : [];
};

const touchesRoot = (value: unknown): boolean => {
const elements = toElements(value);
return elements.some((el) => el === root || root.contains(el));
};

Object.keys(keyboardProcessors).forEach((id) => {
const keyboardProcessor = keyboardProcessors[id];

if (!keyboardProcessor) {
return;
}

if (touchesRoot(keyboardProcessor._element) || touchesRoot(keyboardProcessor._focusTarget)) {
keyboard.off(id);
}
});
},

// NOTE: For tests
_getProcessor: (listenerId) => keyboardProcessors[listenerId],
};
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class SliderHandle extends Widget<SliderHandlerProperties> {

_clean(): void {
super._clean();
this._sliderTooltip?.dispose();
this._sliderTooltip = null;
}

Expand Down
Loading