Compare commits

...

6 Commits

16 changed files with 289 additions and 17 deletions

View File

@ -55,7 +55,7 @@ const config = {
alias: {
'@': path.join(projectRootDir, 'src'),
legacyRegistry: path.join(projectRootDir, 'src/legacyRegistry'),
saveAs: 'file-saver/src/FileSaver.js',
//saveAs: 'file-saver/src/FileSaver.js',
csv: 'comma-separated-values',
EventEmitter: 'eventemitter3',
bourbon: 'bourbon.scss',
@ -124,6 +124,7 @@ const config = {
test: /\.vue$/,
loader: 'vue-loader',
options: {
hotReload: false,
compilerOptions: {
whitespace: 'preserve',
compatConfig: {

View File

@ -13,6 +13,7 @@ const common = require('./webpack.common');
const projectRootDir = path.resolve(__dirname, '..');
module.exports = merge(common, {
cache: false,
mode: 'development',
watchOptions: {
// Since we use require.context, webpack is watching the entire directory.
@ -32,6 +33,7 @@ module.exports = merge(common, {
],
devtool: 'eval-source-map',
devServer: {
hot: false,
devMiddleware: {
writeToDisk: (filePathString) => {
const filePath = path.parse(filePathString);

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.__vnode.ctx.ctx,
'navigatedObject',
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild.__vnode.ctx.ctx
);
});
});
// 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

@ -392,7 +392,7 @@ define([
const component = appLayout.mount(domElement);
component.$nextTick(() => {
this.layout = component.$refs.layout;
this.app = appLayout;
// this.app = appLayout;
Browse(this);
window.addEventListener('beforeunload', this.destroy);
this.router.start();

View File

@ -22,13 +22,14 @@
import EventEmitter from 'EventEmitter';
import ActionCollection from './ActionCollection';
import _ from 'lodash';
import { markRaw } from 'vue';
class ActionsAPI extends EventEmitter {
constructor(openmct) {
super();
this._allActions = {};
this._actionCollections = new WeakMap();
this._actionCollections = markRaw(new WeakMap());
this._openmct = openmct;
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'export', 'import'];

View File

@ -21,7 +21,7 @@
*****************************************************************************/
import CSV from 'comma-separated-values';
import { saveAs } from 'saveAs';
//import { saveAs } from 'saveAs';
class CSVExporter {
export(rows, options) {

View File

@ -31,7 +31,7 @@ function replaceDotsWithUnderscores(filename) {
return filename.replace(regex, '_');
}
import { saveAs } from 'saveAs';
//import { saveAs } from 'saveAs';
import html2canvas from 'html2canvas';
import { v4 as uuid } from 'uuid';

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { saveAs } from 'saveAs';
//import { saveAs } from 'saveAs';
class JSONExporter {
export(obj, options) {

View File

@ -23,6 +23,7 @@
import LadTable from './components/LADTable.vue';
import LADTableConfiguration from './LADTableConfiguration';
import mount from 'utils/mount';
import { markRaw } from 'vue';
export default class LADTableView {
constructor(openmct, domainObject, objectPath) {
@ -31,26 +32,29 @@ export default class LADTableView {
this.objectPath = objectPath;
this.component = null;
this._destroy = null;
markRaw(this);
}
show(element) {
let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct);
const domainObject = this.domainObject;
const objectPath = this.objectPath;
const { vNode, destroy } = mount(
{
el: element,
components: {
LadTable
LadTable: Object.create(LadTable)
},
provide: {
openmct: this.openmct,
currentView: this,
ladTableConfiguration
//currentView: markRaw(this),
ladTableConfiguration: markRaw(ladTableConfiguration)
},
data: () => {
return {
domainObject: this.domainObject,
objectPath: this.objectPath
domainObject,
objectPath
};
},
template:
@ -77,5 +81,7 @@ export default class LADTableView {
if (this._destroy) {
this._destroy();
}
//this.openmct.app._context.optionsCache.delete(LadTable);
//this.openmct.app._context.optionsCache = new WeakMap();
}
}

View File

@ -49,7 +49,7 @@
</template>
<script>
import Vue, { toRaw } from 'vue';
import Vue, { toRaw, markRaw } from 'vue';
import LadRow from './LADRow.vue';
import StalenessUtils from '@/utils/staleness';
@ -139,8 +139,9 @@ export default {
);
this.initializeViewActions();
},
unmounted() {
beforeUnmounted() {
this.ladTableConfiguration.off('change', this.handleConfigurationChange);
this.ladTableConfiguration.destroy();
this.composition.off('add', this.addItem);
this.composition.off('remove', this.removeItem);
@ -150,6 +151,8 @@ export default {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
this.viewActionsCollection.destroy();
delete this.viewActionsCollection;
},
methods: {
addItem(domainObject) {

View File

@ -1,4 +1,4 @@
import { saveAs } from 'saveAs';
//import { saveAs } from 'saveAs';
import Moment from 'moment';
import { NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants';
const UNKNOWN_USER = 'Unknown';

View File

@ -26,7 +26,7 @@ export default {
const rawOldObject = toRaw(oldObject);
Object.assign(rawOldObject, rawNewObject);
}
this.unobservers = [];
this.objectPath.forEach((object) => {
if (object) {
const unobserve = this.openmct.objects.observe(
@ -34,12 +34,13 @@ export default {
'*',
updateObject.bind(this, object)
);
this.$once('hook:unmounted', unobserve);
this.unobservers.push(unobserve);
}
});
},
beforeUnmount() {
this.$refs.root.removeEventListener('contextMenu', this.showContextMenu);
this.unobservers.forEach((unobserve) => unobserve());
},
methods: {
showContextMenu(event) {

View File

@ -20,6 +20,9 @@ export default function mount(component, { props, children, element, app } = {})
}
el = null;
vNode = null;
// if (app && app._context) {
// app._context.optionsCache.delete(component);
// }
};
return { vNode, destroy, el };