Memory leak fixes for several views (#7057)

* Change the mount utility to use Vue's createApp and defineComponent methods

* Fix display layout memory leaks caused by `getSelectionContext`

* fix some display layout leaks due to use of slots

* Fix imagery memory leak (removed span tag). NOTE: CompassRose svg leaks memory - must test on firefox to see if this is a Chrome leak.

* Fix ActionsAPI action collection and applicable actions leak.

* Fix flexible layout memory leaks - remove listeners on unmount. NOTE: One type of overlay plot (Rover Yaw) is still leaking.

* pass in the el on mount

* e2e test config and spec changes

* Remove mounting of limit lines. Use components directly

* test: remove `.only()`

* Fix display layout memory leaks

* Enable passing tests

* e2e README and appActions should be what master has.

* lint: add word to cspell list

* lint: fixes

* lint:fix

* fix: revert `el` change

* fix: remove empty span

* fix: creating shapes in displayLayout

* fix: avoid `splice` as it loses reactivity

* test: reduce timeout time

* quick fixes

* add prod mode and convert the test config to select the correct mode

* Fix webpack prod config

* Add launch flag for exposing window.gc

* never worked

* explicit naming

* rename

* We don't need to destroy view providers

* test: increase timeout time

* test: unskip all mem tests

* fix(vue-loader): disable static hoisting

* chore: run `test:perf:memory`

* Don't destroy view providers

* Move context menu once listener to beforeUnmount instead.

* Disconnect all resize observers on unmount

* Delete Test vue component

* Use beforeUnmount and remove splice(0) in favor of [] for emptying arrays

* re-structure

* fix: unregister listener in pane.vue

* test: tweak timeouts

* chore: lint:fix

* test: unskip perf tests

* fix: unregister events properly

* fix: unregister listener

* fix: unregister listener

* fix: unregister listener

* fix: use `unmounted()`

* fix: unregister listeners

* fix: unregister listener properly

* chore: lint:fix

* test: fix imagery layer toggle test

* test: increase timeout

* Don't use anonymous functions for listeners

* Destroy objects and event listeners properly

* Delete config stores that are created by components

* Use the right unmount hook. Destroy mounted view on unmount.

* Use unmounted, not beforeUnmounted

* Lint fixes

* Fix time strip memory leak

* Undo unneeded change for memory leaks.

* chore: combine common webpack configs

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
This commit is contained in:
Shefali Joshi
2023-09-20 10:34:05 -07:00
committed by GitHub
parent 61e7050391
commit b8949db767
65 changed files with 887 additions and 393 deletions

View File

@@ -1,121 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is an initial example for memory leak testing using performance. This configuration and execution must
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
or profiling playwright and/or the browser.
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
and https://github.com/paulirish/automated-chrome-profiling/issues/3
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
*/
const { test, expect } = require('@playwright/test');
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
// eslint-disable-next-line playwright/no-skipped-test
test.describe.skip('Memory Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
// Click text=Import from JSON
await page.locator('text=Import from JSON').click();
// Upload Performance Display Layout.json
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('text=OK').click();
await expect(
page.locator('a:has-text("Performance Display Layout Display Layout")')
).toBeVisible();
});
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// To to Search Available after Launch
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill Search input
await page
.locator('[aria-label="OpenMCT Search"] input[type="search"]')
.fill('Performance Display Layout');
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Display Layout")').first().click()
]);
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
const client = await page.context().newCDPSession(page);
await client.send('HeapProfiler.enable');
await client.send('HeapProfiler.startSampling');
// await client.send('HeapProfiler.collectGarbage');
await client.send('Performance.enable');
let performanceMetricsBefore = await client.send('Performance.getMetrics');
console.log(performanceMetricsBefore.metrics);
//await client.send('Performance.disable');
//Open Large view
await page.locator('button:has-text("Large View")').click();
await client.send('HeapProfiler.takeHeapSnapshot');
//Time to Imagery Rendered in Large Frame
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
// Click Close Icon
await page.locator('.c-click-icon').click();
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
await client.send('HeapProfiler.collectGarbage');
//await client.send('Performance.enable');
let performanceMetricsAfter = await client.send('Performance.getMetrics');
console.log(performanceMetricsAfter.metrics);
//await client.send('Performance.disable');
});
});

View File

@@ -0,0 +1,299 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('@playwright/test');
const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
/**
* 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.
*
* These tests are executed on a set of pre-built displays loaded from ../test-data/memory-leak-detection.json.
*
* In order to modify the test data set:
* 1. Run Open MCT locally (npm start)
* 2. Right click on a folder in the tree, and select "Import From JSON"
* 3. In the subsequent dialog, select the file ../test-data/memory-leak-detection.json
* 4. Click "OK"
* 5. Modify test objects as desired
* 6. Right click on the "Memory Leak Detection" folder, and select "Export to JSON"
* 7. Copy the exported file to ../test-data/memory-leak-detection.json
*
*/
const NAV_LEAK_TIMEOUT = 10 * 1000; // 10s
test.describe('Navigation memory leak is not detected in', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
await page.locator('text=Import from JSON').click();
// Upload memory-leak-detection.json
await page.setInputFiles('#fileElem', memoryLeakFilePath);
await page.locator('text=OK').click();
await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible();
});
test('plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-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.
expect(result).toBe(true);
});
test('stacked plot view', async ({ page }) => {
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.
expect(result).toBe(true);
});
test('LAD table view', async ({ page }) => {
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.
expect(result).toBe(true);
});
test('LAD table set', async ({ page }) => {
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.
expect(result).toBe(true);
});
//TODO: Figure out why using the `table-row` component inside the `table` component leaks TelemetryTableRow objects
test('telemetry table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'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.
expect(result).toBe(true);
});
//TODO: Figure out why using the `SideBar` component inside the leaks Notebook objects
test('notebook view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'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.
expect(result).toBe(true);
});
test('display layout of a single SWG alphanumeric', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
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.
expect(result).toBe(true);
});
test('display layout of a single SWG plot', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'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.
expect(result).toBe(true);
});
//TODO: Figure out why `svg` in the CompassRose component leaks imagery
test('example imagery view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'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.
expect(result).toBe(true);
});
test('display layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'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.
expect(result).toBe(true);
});
test('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
page
}) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'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.
expect(result).toBe(true);
});
test('flexible layout with plots of swgs', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'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.
expect(result).toBe(true);
});
test('flexible layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'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.
expect(result).toBe(true);
});
test('tabbed view of display layouts and time strips', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'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.
expect(result).toBe(true);
});
test('time strip view of telemetry', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'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.
expect(result).toBe(true);
});
/**
*
* @param {import('@playwright/test').Page} page
* @param {*} objectName
* @returns
*/
async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(objectName);
//Search Result Appears and is clicked
await Promise.all([
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
// garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy
// for detecting memory leaks.
await page.evaluate(() => {
window.gcPromise = new Promise((resolve) => {
// eslint-disable-next-line no-undef
window.fr = new FinalizationRegistry(resolve);
window.fr.register(
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild,
'navigatedObject',
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild
);
});
});
// Nav back to folder
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
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.
// In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out.
await page.evaluate(() => {
const gcPromise = window.gcPromise;
window.gcPromise = null;
// Manually invoke the garbage collector once all references are removed.
window.gc();
return gcPromise;
});
// Clean up the finalization registry since we don't need it any more.
await page.evaluate(() => {
window.fr = null;
});
// If we get here without timing out, it means the garbage collection promise resolved and the test passed.
return true;
}
});