Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
14 changes: 13 additions & 1 deletion packages/devextreme-angular/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,19 @@ module.exports = function (config) {

autoWatch: true,

browsers: ['ChromeHeadless'],
browsers: ['ChromeHeadlessWithGC'],

customLaunchers: {
ChromeHeadlessWithGC: {
base: 'ChromeHeadless',
flags: [
'--js-flags=--expose-gc',
'--no-sandbox',
'--disable-gpu',
'--enable-precise-memory-info',
],
},
},

reporters: [
'progress',
Expand Down
32 changes: 32 additions & 0 deletions packages/devextreme-angular/tests/src/ui/data-grid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,4 +558,36 @@ describe('Nested DxDataGrid', () => {
}, 1000);
}, 1000);
});

it('should not memory leak after click if dx-data-grid is on page (T1307313)', () => {
TestBed.overrideComponent(TestContainerComponent, {
set: {
template: '<dx-data-grid [dataSource]="[]"></dx-data-grid>',
},
});

const fixture = TestBed.createComponent(TestContainerComponent);
fixture.detectChanges();

for (let i = 0; i < 100; i++) {
document.body.click();
}

fixture.detectChanges();
globalThis.gc();

const { memory } = performance as any;
const jsHeapSizeBefore = memory.usedJSHeapSize;

for (let i = 0; i < 100; i++) {
document.body.click();
}

fixture.detectChanges();
globalThis.gc();

const memoryDiff = memory.usedJSHeapSize - jsHeapSizeBefore;

expect(memoryDiff <= 0).toBeTruthy();
});
});
15 changes: 13 additions & 2 deletions packages/devextreme/js/__internal/events/m_click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const misc = { requestAnimationFrame, cancelAnimationFrame };

let prevented: boolean | null = null;
let lastFiredEvent = null;
const subscriptions = new Map();

const onNodeRemove = () => {
lastFiredEvent = null;
Expand All @@ -32,9 +33,19 @@ const clickHandler = function (e) {
originalEvent.DXCLICK_FIRED = true;
}

unsubscribeNodesDisposing(lastFiredEvent, onNodeRemove);
if (subscriptions.has(lastFiredEvent)) {
const { nodes, callback } = subscriptions.get(lastFiredEvent);

unsubscribeNodesDisposing(lastFiredEvent, callback, nodes);

subscriptions.delete(lastFiredEvent);
}

lastFiredEvent = originalEvent;
subscribeNodesDisposing(lastFiredEvent, onNodeRemove);

const subscriptionData = subscribeNodesDisposing(lastFiredEvent, onNodeRemove);

subscriptions.set(lastFiredEvent, subscriptionData);

fireEvent({
type: CLICK_EVENT_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ function nodesByEvent(event) {
}

export const subscribeNodesDisposing = (event, callback) => {
eventsEngine.one(nodesByEvent(event), removeEvent, callback);
const nodes = nodesByEvent(event);
const onceCallback = function (...args) {
eventsEngine.off(nodes, removeEvent, onceCallback);
return callback(...args);
};

eventsEngine.on(nodes, removeEvent, onceCallback);

return { callback: onceCallback, nodes };
};

export const unsubscribeNodesDisposing = (event, callback) => {
eventsEngine.off(nodesByEvent(event), removeEvent, callback);
export const unsubscribeNodesDisposing = (event, callback, nodes) => {
eventsEngine.off(/* nodes || */ nodesByEvent(event) || nodes, removeEvent, callback);
};
10 changes: 7 additions & 3 deletions packages/devextreme/testing/launch
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,25 @@ function waitForRunner() {
function openBrowser() {
spawn(
getBrowserCommand(),
[ 'http://localhost:' + PORT ],
[ '""',
'--js-flags=--expose-gc',
'--enable-precise-memory-info',
'http://localhost:' + PORT,
],
{ shell: true, detached: true }
);
}

function getBrowserCommand() {
switch(platform()) {
case 'win32':
return 'start';
return 'start chrome';

case 'darwin':
return 'open';

case 'linux':
return 'xdg-open';
return 'google-chrome';
}

throw 'Not implemented';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import eventsEngine from 'common/core/events/core/events_engine';
import domAdapter from '__internal/core/m_dom_adapter';

QUnit.testStart(function() {
const markup = '';
const fixture = document.getElementById('qunit-fixture');
if(fixture) {
fixture.innerHTML = markup;
}
});

QUnit.module('event nodes disposing', {
afterEach: function() {
const document = domAdapter.getDocument();
eventsEngine.off(document, 'dxclick');
}
});

QUnit.test('should not leak memory when clicking on body with dxclick subscription on document', function(assert) {
const document = domAdapter.getDocument();
const body = document.body;

eventsEngine.on(document, 'dxclick', function() {});

if(typeof globalThis !== 'undefined' && typeof globalThis.gc === 'function') {
globalThis.gc();
}

const initialMemory = performance.memory.usedJSHeapSize;

for(let i = 0; i < 100; i++) {
eventsEngine.trigger(body, 'click');
}

if(typeof globalThis !== 'undefined' && typeof globalThis.gc === 'function') {
globalThis.gc();
}

const finalMemory = performance.memory.usedJSHeapSize;
const memoryDiff = finalMemory - initialMemory;

assert.ok(
memoryDiff <= 0,
`Memory should not leak. Memory diff: ${memoryDiff}B`
);
});
Loading