Compare commits

...

10 Commits

Author SHA1 Message Date
Andrew Henry
850aca546b WIP 2023-08-24 15:28:25 -07:00
Andrew Henry
22962452e3 Do not Object.create LADTable component 2023-08-24 15:28:08 -07:00
Andrew Henry
cfdc6d7f20 Merge branch 'master' into one-weird-trick 2023-08-22 11:25:32 -07:00
Andrew Henry
bafe8c33a9 Automatically apply One Weird Trick to all views 2023-08-22 11:25:10 -07:00
Andrew Henry
62dcf76798 WIP 2023-08-18 16:12:10 -07:00
Andrew Henry
04adb790a0 Merge branch 'master' into memory-leak-detection-vue3 2023-08-16 15:03:08 -07:00
Andrew Henry
6f46b4d87e Fixed prettier issues 2023-07-07 10:06:04 -07:00
Andrew Henry
4fff6b035b Merge branch 'master' into memory-leak-detection 2023-07-07 09:46:01 -07:00
Andrew Henry
d48e11bbf7 Renamed existing imagery memory usage test 2023-06-07 17:41:07 -07:00
Andrew Henry
8cc4eca3e8 Added tests to detect memory leaks on navigation 2023-06-07 17:19:04 -07:00
12 changed files with 297 additions and 25 deletions

View File

@@ -23,7 +23,10 @@ const config = {
ignoreHTTPSErrors: true,
screenshot: 'off',
trace: 'on-first-retry',
video: 'off'
video: 'off',
launchOptions: {
args: ['--js-flags=--expose-gc']
}
},
projects: [
{

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,251 @@
/*****************************************************************************
* 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 filePath = '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
*
*/
test.describe('Navigation memory leak is not detected in', () => {
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 memory-leak-detection.json
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
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');
// 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');
// 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.only('LAD table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-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('LAD table set', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-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('telemetry table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'telemetry-table-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('notebook view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'notebook-memory-leak-detection-test'
);
// 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');
// 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'
);
// 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.skip('example imagery view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'example-imagery-memory-leak-test'
);
// 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.skip('display layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-images-memory-leak-test'
);
// 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.skip('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
page
}) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-simple-telemetry'
);
// 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.skip('flexible layout with plots of swgs', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'flexible-layout-plots-memory-leak-test'
);
// 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.skip('flexible layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'flexible-layout-images-memory-leak-test'
);
// 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.skip('tabbed view of display layouts and time strips', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'tab-view-simple-memory-leak-test'
);
// 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.skip('time strip view of telemetry', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'time-strip-telemetry-memory-leak-test'
);
// 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);
});
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;
}
});

View File

@@ -993,9 +993,6 @@ export default {
this.config.yAxisLabel = this.config.yAxis.get('label');
this.cursorGuideVertical = this.$refs.cursorGuideVertical;
this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal;
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
this.yAxisListWithRange.forEach((yAxis) => {
this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange.bind(this, yAxis.id), this);
@@ -1123,8 +1120,8 @@ export default {
},
updateCrosshairs(event) {
this.cursorGuideVertical.style.left = event.clientX - this.chartElementBounds.x + 'px';
this.cursorGuideHorizontal.style.top = event.clientY - this.chartElementBounds.y + 'px';
this.$refs.cursorGuideVertical.style.left = event.clientX - this.chartElementBounds.x + 'px';
this.$refs.cursorGuideHorizontal.style.top = event.clientY - this.chartElementBounds.y + 'px';
},
trackChartElementBounds(event) {

View File

@@ -78,6 +78,7 @@ export default {
},
beforeUnmount() {
this.openmct.time.off('timeSystemChanged', this.syncXAxisToTimeSystem);
this.stopListening();
},
methods: {
isEnabledXKeyToggle() {

View File

@@ -168,6 +168,9 @@ export default {
this.loaded = true;
this.setUpYAxisOptions();
},
beforeUnmount() {
this.stopListening();
},
methods: {
initAxisAndSeriesConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);

View File

@@ -1,6 +1,7 @@
import TableComponent from './components/table.vue';
import TelemetryTable from './TelemetryTable';
import mount from 'utils/mount';
import { markRaw } from 'vue';
export default class TelemetryTableView {
constructor(openmct, domainObject, objectPath) {
@@ -9,12 +10,6 @@ export default class TelemetryTableView {
this.objectPath = objectPath;
this._destroy = null;
this.component = null;
Object.defineProperty(this, 'table', {
value: new TelemetryTable(domainObject, openmct),
enumerable: false,
configurable: false
});
}
getViewContext() {
@@ -41,9 +36,14 @@ export default class TelemetryTableView {
if (this._destroy) {
this._destroy();
}
delete this.component;
delete this._destroy;
this.openmct.app._context.optionsCache = new WeakMap();
}
show(element, editMode) {
const telemetryTable = markRaw(new TelemetryTable(this.domainObject, this.openmct));
const { vNode, destroy } = mount(
{
el: element,
@@ -53,7 +53,7 @@ export default class TelemetryTableView {
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
table: this.table,
table: telemetryTable,
currentView: this
},
data() {

View File

@@ -145,10 +145,10 @@ export default {
let column = new TelemetryTableColumn(this.openmct, metadatum);
this.tableConfiguration.addSingleColumnForObject(telemetryObject, column);
// if units are available, need to add columns to be hidden
if (metadatum.unit !== undefined) {
let unitColumn = new TelemetryTableUnitColumn(this.openmct, metadatum);
this.tableConfiguration.addSingleColumnForObject(telemetryObject, unitColumn);
}
// if (metadatum.unit !== undefined) {
// let unitColumn = new TelemetryTableUnitColumn(this.openmct, metadatum);
// this.tableConfiguration.addSingleColumnForObject(telemetryObject, unitColumn);
// }
});
},
toggleHeaderVisibility() {

View File

@@ -291,13 +291,13 @@ const AUTO_SCROLL_TRIGGER_HEIGHT = 100;
export default {
components: {
TelemetryTableRow,
TableColumnHeader,
search,
TableFooterIndicator,
ToggleSwitch,
SizingRow,
ProgressBar
TelemetryTableRow: Object.assign({}, TelemetryTableRow),
TableColumnHeader: Object.assign({}, TableColumnHeader),
search: Object.assign({}, search),
TableFooterIndicator: Object.assign({}, TableFooterIndicator),
ToggleSwitch: Object.assign({}, ToggleSwitch),
SizingRow: Object.assign({}, SizingRow),
ProgressBar: Object.assign({}, ProgressBar)
},
inject: ['openmct', 'objectPath', 'table', 'currentView'],
props: {

View File

@@ -27,6 +27,8 @@ export default {
Object.assign(rawOldObject, rawNewObject);
}
this.unmountObservers = [];
this.objectPath.forEach((object) => {
if (object) {
const unobserve = this.openmct.objects.observe(
@@ -34,12 +36,15 @@ export default {
'*',
updateObject.bind(this, object)
);
this.$once('hook:unmounted', unobserve);
this.unmountObservers.push(unobserve);
}
});
},
beforeUnmount() {
this.$refs.root.removeEventListener('contextMenu', this.showContextMenu);
this.unmountObservers.forEach((unobserve) => unobserve());
delete this.unmountObservers;
},
methods: {
showContextMenu(event) {

View File

@@ -3,6 +3,17 @@ import { h, render } from 'vue';
export default function mount(component, { props, children, element, app } = {}) {
let el = element;
if (component.components !== undefined) {
component.components = Object.keys(component.components).reduce(
(componentMap, componentKey) => {
componentMap[componentKey] = Object.assign({}, component.components[componentKey]);
return componentMap;
},
{}
);
}
let vNode = h(component, props, children);
if (app && app._context) {
vNode.appContext = app._context;