Automatically check additional views for memory leaks on navigation (#7300)

* add new objects for navigation testing

* add test for remaining objects

* cleanup plotly on dismount

* lint

* remove vestigial object

* do not need to call destroy here

* do not need to call destroy here

* refactor: ensure path to test file always resolves

* refactor: better locators

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
This commit is contained in:
Scott Bell 2023-12-15 10:13:41 +01:00 committed by GitHub
parent 2675220452
commit 3fccac0bfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 119 additions and 78 deletions

File diff suppressed because one or more lines are too long

View File

@ -19,10 +19,15 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/* global __dirname */
const { test, expect } = require('@playwright/test'); const { test, expect } = require('@playwright/test');
const path = require('path');
const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json'; const memoryLeakFilePath = path.resolve(
__dirname,
'../../../../e2e/test-data/memory-leak-detection.json'
);
/** /**
* Executes tests to verify that views are not leaking memory on navigation away. This sort of * Executes tests to verify that views are not leaking memory on navigation away. This sort of
* memory leak is generally caused by a failure to clean up registered listeners. * memory leak is generally caused by a failure to clean up registered listeners.
@ -40,54 +45,127 @@ const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
* *
*/ */
const NAV_LEAK_TIMEOUT = 10 * 1000; // 10s
test.describe('Navigation memory leak is not detected in', () => { test.describe('Navigation memory leak is not detected in', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Go to baseURL // Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('a:has-text("My Items")').click({ await page
.getByRole('treeitem', {
name: /My Items/
})
.click({
button: 'right' button: 'right'
}); });
await page.locator('text=Import from JSON').click(); await page
.getByRole('menuitem', {
name: /Import from JSON/
})
.click();
// Upload memory-leak-detection.json // Upload memory-leak-detection.json
await page.setInputFiles('#fileElem', memoryLeakFilePath); await page.setInputFiles('#fileElem', memoryLeakFilePath);
await page.locator('text=OK').click(); await page
.getByRole('button', {
name: 'Save'
})
.click();
await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible(); await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible();
}); });
test('plot view', async ({ page }) => { test('gauge', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg', { const result = await navigateToObjectAndDetectMemoryLeak(page, 'gauge-single-1hz-swg');
timeout: NAV_LEAK_TIMEOUT
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
}); });
test('plan', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'plan-generated');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('time list', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'time-list');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('scatter', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'scatter-plot-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('graph', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'graph-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('gantt chart', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'gantt-chart');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('clock', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'clock');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('timer', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'timer-far-future');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('web page (nasa.gov)', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'web-page');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('Complex Display Layout', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'complex-display-layout');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg');
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true); expect(result).toBe(true);
}); });
test('stacked plot view', async ({ page }) => { test('stacked plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg', { const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg');
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true); expect(result).toBe(true);
}); });
test('LAD table view', async ({ page }) => { test('LAD table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg', { const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg');
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true); expect(result).toBe(true);
}); });
test('LAD table set', async ({ page }) => { test('LAD table set', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg', { const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg');
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true); expect(result).toBe(true);
}); });
@ -96,10 +174,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('telemetry table view', async ({ page }) => { test('telemetry table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(
page, page,
'telemetry-table-single-1hz-swg', 'telemetry-table-single-1hz-swg'
{
timeout: NAV_LEAK_TIMEOUT
}
); );
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@ -110,10 +185,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('notebook view', async ({ page }) => { test('notebook view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(
page, page,
'notebook-memory-leak-detection-test', 'notebook-memory-leak-detection-test'
{
timeout: NAV_LEAK_TIMEOUT
}
); );
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@ -121,13 +193,7 @@ test.describe('Navigation memory leak is not detected in', () => {
}); });
test('display layout of a single SWG alphanumeric', async ({ page }) => { test('display layout of a single SWG alphanumeric', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(page, 'display-layout-single-1hz-swg');
page,
'display-layout-single-1hz-swg',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true); expect(result).toBe(true);
@ -136,10 +202,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('display layout of a single SWG plot', async ({ page }) => { test('display layout of a single SWG plot', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(
page, page,
'display-layout-single-overlay-plot', 'display-layout-single-overlay-plot'
{
timeout: NAV_LEAK_TIMEOUT
}
); );
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@ -150,10 +213,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('example imagery view', async ({ page }) => { test('example imagery view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(
page, page,
'example-imagery-memory-leak-test', 'example-imagery-memory-leak-test'
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
); );
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@ -163,10 +223,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('display layout of example imagery views', async ({ page }) => { test('display layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(
page, page,
'display-layout-images-memory-leak-test', 'display-layout-images-memory-leak-test'
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
); );
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@ -178,10 +235,7 @@ test.describe('Navigation memory leak is not detected in', () => {
}) => { }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(
page, page,
'display-layout-simple-telemetry', 'display-layout-simple-telemetry'
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
); );
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@ -191,10 +245,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('flexible layout with plots of swgs', async ({ page }) => { test('flexible layout with plots of swgs', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(
page, page,
'flexible-layout-plots-memory-leak-test', 'flexible-layout-plots-memory-leak-test'
{
timeout: NAV_LEAK_TIMEOUT
}
); );
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@ -204,10 +255,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('flexible layout of example imagery views', async ({ page }) => { test('flexible layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(
page, page,
'flexible-layout-images-memory-leak-test', 'flexible-layout-images-memory-leak-test'
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
); );
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@ -217,10 +265,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('tabbed view of display layouts and time strips', async ({ page }) => { test('tabbed view of display layouts and time strips', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(
page, page,
'tab-view-simple-memory-leak-test', 'tab-view-simple-memory-leak-test'
{
timeout: NAV_LEAK_TIMEOUT * 6 * 2 // 2 min
}
); );
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@ -230,10 +275,7 @@ test.describe('Navigation memory leak is not detected in', () => {
test('time strip view of telemetry', async ({ page }) => { test('time strip view of telemetry', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak( const result = await navigateToObjectAndDetectMemoryLeak(
page, page,
'time-strip-telemetry-memory-leak-test', 'time-strip-telemetry-memory-leak-test'
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
); );
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected. // If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
@ -247,15 +289,12 @@ test.describe('Navigation memory leak is not detected in', () => {
* @returns * @returns
*/ */
async function navigateToObjectAndDetectMemoryLeak(page, objectName) { async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); await page.getByRole('searchbox', { name: 'Search Input' }).click();
// Fill Search input // Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(objectName); await page.getByRole('searchbox', { name: 'Search Input' }).fill(objectName);
//Search Result Appears and is clicked //Search Result Appears and is clicked
await Promise.all([ await page.getByText(objectName, { exact: true }).click();
page.locator(`div.c-gsearch-result__title:has-text("${objectName}")`).first().click(),
page.waitForNavigation()
]);
// Register a finalization listener on the root node for the view. This tends to be the last thing to be // Register a finalization listener on the root node for the view. This tends to be the last thing to be
// garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy // garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy
@ -273,8 +312,7 @@ test.describe('Navigation memory leak is not detected in', () => {
}); });
// Nav back to folder // Nav back to folder
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); await page.goto('./#/browse/mine');
await page.waitForNavigation();
// This next code block blocks until the finalization listener is called and the gcPromise resolved. This means that the root node for the view has been garbage collected. // This next code block blocks until the finalization listener is called and the gcPromise resolved. This means that the root node for the view has been garbage collected.
// In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out. // In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out.

View File

@ -99,6 +99,7 @@ export default {
}, },
beforeUnmount() { beforeUnmount() {
if (this.plotResizeObserver) { if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
this.plotResizeObserver.disconnect(); this.plotResizeObserver.disconnect();
clearTimeout(this.resizeTimer); clearTimeout(this.resizeTimer);
} }
@ -106,6 +107,8 @@ export default {
if (this.removeBarColorListener) { if (this.removeBarColorListener) {
this.removeBarColorListener(); this.removeBarColorListener();
} }
Plotly.purge(this.$refs.plot);
}, },
methods: { methods: {
getAxisMinMax(axis) { getAxisMinMax(axis) {

View File

@ -26,7 +26,7 @@
<li> <li>
<series-options <series-options
v-for="series in plotSeries" v-for="series in plotSeries"
:key="series.key" :key="series.keyString"
:item="series" :item="series"
:color-palette="colorPalette" :color-palette="colorPalette"
/> />

View File

@ -217,7 +217,6 @@ describe('the plugin', function () {
'someNamespace:~OpenMCT~outer.test-object.foo.bar' 'someNamespace:~OpenMCT~outer.test-object.foo.bar'
].name ].name
).toEqual('A Dotful Object'); ).toEqual('A Dotful Object');
barGraphView.destroy();
}); });
}); });
@ -310,7 +309,6 @@ describe('the plugin', function () {
const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines'); const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');
expect(plotElement).not.toBeNull(); expect(plotElement).not.toBeNull();
barGraphView.destroy();
}); });
}); });

View File

@ -131,6 +131,8 @@ export default {
if (this.unobserveColorChanges) { if (this.unobserveColorChanges) {
this.unobserveColorChanges(); this.unobserveColorChanges();
} }
Plotly.purge(this.$refs.plot);
}, },
methods: { methods: {
getUnderlayPlotData() { getUnderlayPlotData() {