Compare commits

...

14 Commits

Author SHA1 Message Date
Jesse Mazzella
45f2796f43 Merge branch 'master' into memlab-poc2 2024-01-03 09:52:10 -08:00
John Hill
5d201e7705 comment 2023-12-21 20:36:57 -08:00
John Hill
8503f91e1a update the tests 2023-12-21 16:26:11 -08:00
John Hill
47edc1604c a few more passes 2023-12-21 16:26:02 -08:00
John Hill
9b647ca628 exports.module 2023-12-21 15:51:05 -08:00
John Hill
a5700b82e3 update to use fixtures 2023-12-21 15:43:31 -08:00
John Hill
270b4279c4 remove old tst 2023-12-21 15:43:08 -08:00
John Hill
91050ad697 new test 2023-12-21 15:42:58 -08:00
John Hill
83df1cc712 move this into fixtures 2023-12-21 15:42:47 -08:00
John Hill
d39e8a69d4 out of date 2023-12-21 15:42:34 -08:00
John Hill
25b8129b93 memleaks 2023-12-19 14:29:00 -08:00
John Hill
4c1fdd8095 Merge branch 'master' of https://github.com/nasa/openmct into memlab-poc2 2023-12-19 14:28:53 -08:00
John Hill
87014319ce fixes and attempt at feedback 2023-08-23 20:55:37 -07:00
John Hill
23ed6e15c1 add first pass of memlab 2023-08-18 11:11:23 -07:00
8 changed files with 331 additions and 59 deletions

4
.gitignore vendored
View File

@@ -47,3 +47,7 @@ codecov
# :(
package-lock.json
e2e/test-data/heapsnapshots/**
!e2e/test-data/heapsnapshots/data/cur/snap-seq.json
!e2e/test-data/heapsnapshots/data/cur/run-meta.json

View File

@@ -33,12 +33,16 @@
* existing ones.
*/
import { fileURLToPath } from 'node:url';
import AxeBuilder from '@axe-core/playwright';
import { BrowserInteractionResultReader, findLeaks } from '@memlab/api';
import fs from 'fs';
import path from 'path';
import { expect, test } from './pluginFixtures.js';
const snapshotsPath = fileURLToPath(new URL('../../../test-data/snapshots', import.meta.url));
// Constants for repeated values
const TEST_RESULTS_DIR = './test-results';
@@ -56,8 +60,8 @@ const TEST_RESULTS_DIR = './test-results';
* @returns {Promise<object|null>} Returns the accessibility scan results if violations are found,
* otherwise returns null.
*/
/* eslint-disable no-undef */
export async function scanForA11yViolations(page, testCaseName, options = {}) {
async function scanForA11yViolations(page, testCaseName, options = {}) {
const builder = new AxeBuilder({ page });
builder.withTags(['wcag2aa']);
// https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md
@@ -94,4 +98,138 @@ export async function scanForA11yViolations(page, testCaseName, options = {}) {
}
}
export { expect, test };
/**
* Gets the used JS heap size from the page.
*
* @param {import('@playwright/test').Page} page - The page from which to get the heap size.
* @returns {Promise<number|null>} The used JS heap size in bytes, or null if not available.
* @note Can only be executed when running the 'chrome-memory' Playwright config at e2e/playwright-performance-prod.config.js.
*/
function getHeapSize(page) {
return page.evaluate(() => {
if (window.performance && window.performance.memory) {
return window.performance.memory.usedJSHeapSize;
}
return null;
});
}
/**
* Forces garbage collection. Useful for getting accurate memory usage in 'getHeapSize'.
*
* @param {import('@playwright/test').Page} page - The page on which to force garbage collection.
* @param {number} [repeat=6] - Number of times to repeat the garbage collection process.
* @note Can only be executed when running the 'chrome-memory' Playwright config at e2e/playwright-performance-prod.config.js.
*/
async function forceGC(page, repeat = 6) {
for (let i = 0; i < repeat; i++) {
await client.send('HeapProfiler.collectGarbage');
// wait for a while and let GC do the job
await page.waitForTimeout(200);
}
await page.waitForTimeout(1400);
}
/**
* Captures a heap snapshot and saves it to a specified path by attaching to the CDP session.
*
* @param {import('@playwright/test').Page} page - The page from which to capture the heap snapshot.
* @param {string} outputPath - The file path where the heap snapshot will be saved.
* @note Can only be executed when running the 'chrome-memory' Playwright config at e2e/playwright-performance-prod.config.js.
*/
async function captureHeapSnapshot(page, outputPath) {
const client = await page.context().newCDPSession(page);
const dir = path.dirname(outputPath);
try {
await fs.mkdir(dir, { recursive: true });
} catch (error) {
if (error.code !== 'EEXIST') {
throw error; // Throw the error if it is not because the directory already exists
}
}
const chunks = [];
function dataHandler(data) {
chunks.push(data.chunk);
}
try {
client.on('HeapProfiler.addHeapSnapshotChunk', dataHandler);
console.debug(`🚮 Running garbage collection...`);
await forceGC(page);
await client.send('HeapProfiler.enable');
console.debug(`📸 Capturing heap snapshot to ${outputPath}`);
await client.send('HeapProfiler.takeHeapSnapshot');
client.removeListener('HeapProfiler.addHeapSnapshotChunk', dataHandler);
const fullSnapshot = chunks.join('');
fs.writeFile(outputPath, fullSnapshot, { encoding: 'UTF-8' });
} catch (error) {
console.error('🛑 Error while capturing heap snapshot:', error);
} finally {
await client.detach(); // Ensure that the client is detached after the operation
}
}
/**
* Navigates to an object in the application and checks for memory leaks by forcing garbage collection.
*
* @param {import('@playwright/test').Page} page - The page on which the navigation and memory leak detection will be performed.
* @param {string} objectName - The name of the object to navigate to.
* @returns {Promise<boolean>} Returns true if no memory leak is detected.
* @note Can only be executed when running the 'chrome-memory' Playwright config at e2e/playwright-performance-prod.config.js.
*/
async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
await page.getByRole('searchbox', { name: 'Search Input' }).click();
// Fill Search input
await page.getByRole('searchbox', { name: 'Search Input' }).fill(objectName);
// Search Result Appears and is clicked
await page.getByText(objectName, { exact: true }).click();
// Register a finalization listener on the root node for the view.
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');
// Invoke garbage collection using forceGC
await forceGC(page);
// Wait for the garbage collection promise to resolve.
const gcCompleted = await page.evaluate(() => {
const gcPromise = window.gcPromise;
window.gcPromise = null;
return gcPromise;
});
// Clean up the finalization registry
await page.evaluate(() => {
window.fr = null;
});
return gcCompleted;
}
export {
BrowserInteractionResultReader,
captureHeapSnapshot,
expect,
findLeaks,
forceGC,
getHeapSize,
navigateToObjectAndDetectMemoryLeak,
scanForA11yViolations,
test
};

View File

@@ -3,7 +3,7 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
retries: 0, //Only for debugging purposes for trace: 'on-first-retry'
testDir: 'tests/performance/',
testMatch: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
timeout: 60 * 1000,
@@ -28,7 +28,18 @@ const config = {
name: 'chrome',
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags. Shouldn't get here
use: {
browserName: 'chromium'
browserName: 'chromium',
launchOptions: {
args: [
'--no-sandbox',
'--disable-notifications',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
'--js-flags=--no-move-object-start',
'--enable-precise-memory-info',
'--display=:100'
]
}
}
}
],

23
e2e/snap-seq.json Normal file
View File

@@ -0,0 +1,23 @@
[
{
"name": "page-load",
"snapshot": true,
"type": "baseline",
"idx": 1,
"JSHeapUsedSize": 88941668
},
{
"name": "action-on-page",
"snapshot": true,
"type": "target",
"idx": 2,
"JSHeapUsedSize": 92784609
},
{
"name": "revert",
"snapshot": true,
"type": "final",
"idx": 3,
"JSHeapUsedSize": 93053124
}
]

View File

@@ -0,0 +1,6 @@
snapshot meta data invalid or missing
Use `memlab help` or `memlab <COMMAND> -h` to get helper text
snapshot meta data invalid or missing
Use `memlab help` or `memlab <COMMAND> -h` to get helper text
snapshot meta data invalid or missing
Use `memlab help` or `memlab <COMMAND> -h` to get helper text

View File

@@ -0,0 +1,137 @@
const fs = require('fs');
const path = require('path');
const { forceGC, getHeapSize, test, expect } = require('../../../avpFixtures.js');
const { createDomainObjectWithDefaults } = require('../../../appActions.js');
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
const snapshotPath = 'e2e/test-data/heapsnapshots/data/cur/';
//Constants for URL handling in the Performance Tests
// We want 5 seconds in the past and 1 second in the future
const startDelta = 5 * 1000;
const endDelta = 1 * 1000;
// saturation period that all telemetry has been loaded into memory
const saturationPeriod = startDelta + endDelta;
const timeBetweenSnapshots = 2 * 1000;
// test.describe('Memory Performance tests', () => {
// test.beforeEach(async ({ page }) => {
// await page.goto(
// './#/browse/mine?tc.mode=local&tc.timeSystem=utc&view=grid&tc.startDelta=1800000&tc.endDelta=30000',
// { waitUntil: 'domcontentloaded' }
// );
// await page.waitForTimeout(3 * 1000);
// await forceGC(page);
// await captureHeapSnapshot(page, snapshotPath + 's1.heapsnapshot');
// // Get and compare JSHeapUsedSize at different points in the test
// let heapSize = await getHeapSize(page);
// console.log(`Initial JSHeapUsedSize: ${heapSize}`);
// await page.locator('a:has-text("My Items")').click({ button: 'right' });
// await page.locator('text=Import from JSON').click();
// await page.setInputFiles('#fileElem', filePath);
// 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 }) => {
// await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// await page
// .locator('[aria-label="OpenMCT Search"] input[type="search"]')
// .fill('Performance Display Layout');
// await Promise.all([
// page.waitForNavigation(),
// page.locator('a:has-text("Performance Display Layout")').first().click()
// ]);
// await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
// await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
// await page.waitForTimeout(3 * 1000);
// await forceGC(page);
// await captureHeapSnapshot(page, snapshotPath + 's2.heapsnapshot');
// // Get and compare JSHeapUsedSize at different points in the test
// let heapSize = await getHeapSize(page);
// console.log(`second JSHeapUsedSize: ${heapSize}`);
// });
// test.afterEach(async ({ page }) => {
// await page.goto(
// './#/browse/mine?tc.mode=local&tc.timeSystem=utc&view=grid&tc.startDelta=1800000&tc.endDelta=30000',
// { waitUntil: 'domcontentloaded' }
// );
// await page.waitForTimeout(3 * 1000);
// await forceGC(page);
// await captureHeapSnapshot(page, snapshotPath + 's3.heapsnapshot');
// // Get and compare JSHeapUsedSize at different points in the test
// let heapSize = await getHeapSize(page);
// console.log(`Final JSHeapUsedSize: ${heapSize}`);
// const reader = BrowserInteractionResultReader.from(snapshotPath);
// const leaks = await findLeaks(reader);
// console.log('Leaks:', leaks);
// });
// });
test.describe.only('Performance - Telemetry Memory Tests', () => {
let overlayPlot;
let alphaSineWave;
let baseHeapSize;
let heapSizes;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
await page.waitForTimeout(3 * 1000);
await forceGC(page);
// Get and compare JSHeapUsedSize at different points in the test
const initialHeapSize = await getHeapSize(page);
console.log(`Initial JSHeapUsedSize: ${initialHeapSize}`);
});
test('Plots memory does not grow unbounded', async ({ page }) => {
await page.goto(
`${overlayPlot.url}?tc.mode=local&tc.startDelta=${startDelta}&tc.endDelta=${endDelta}&tc.timeSystem=utc&view=grid`,
{ waitUntil: 'domcontentloaded' }
);
await page.waitForTimeout(saturationPeriod);
await forceGC(page);
baseHeapSize = await getHeapSize(page);
console.log(`Initial JSHeapUsedSize: ${baseHeapSize}`);
//Temporarily monitor how long it takes memory to grow
heapSizes = [];
for (let i = 0; i < 10; i++) {
await page.waitForTimeout(timeBetweenSnapshots);
await forceGC(page);
const heapSize = await getHeapSize(page);
console.log(`Heap Size at iteration ${i}: ${heapSize}`);
heapSizes.push(heapSize);
}
});
test.afterEach(async ({ page }) => {
heapSizes.forEach(heapSize => {
expect(heapSize).toBeLessThanOrEqual(baseHeapSize);
});
});
});

View File

@@ -20,12 +20,16 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { fileURLToPath } from 'node:url';
import { expect, test } from '@playwright/test';
import { fileURLToPath } from 'url';
import { navigateToObjectAndDetectMemoryLeak } from '../../../avpFixtures.js';
const memoryLeakFilePath = fileURLToPath(
new URL('../../../../e2e/test-data/memory-leak-detection.json', import.meta.url)
);
/**
* 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.
@@ -279,57 +283,4 @@ test.describe('Navigation memory leak is not detected in', () => {
// 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.getByRole('searchbox', { name: 'Search Input' }).click();
// Fill Search input
await page.getByRole('searchbox', { name: 'Search Input' }).fill(objectName);
//Search Result Appears and is clicked
await page.getByText(objectName, { exact: true }).click();
// 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');
// 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

@@ -9,6 +9,7 @@
"@babel/eslint-parser": "7.23.3",
"@braintree/sanitize-url": "6.0.4",
"@deploysentinel/playwright": "0.3.4",
"@memlab/api": "1.0.25",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.39.0",
@@ -116,6 +117,7 @@
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep-invert @unstable",
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --grep-invert @unstable",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
"test:memlab:findleaks": "npx memlab find-leaks --snapshot-dir e2e/test-data/heapsnapshots/",
"test:e2e:watch": "npx playwright test --ui --config=e2e/playwright-watch.config.js",
"test:perf:contract": "npx playwright test --config=e2e/playwright-performance-dev.config.js",
"test:perf:localhost": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome",