Merge pull request #224547 from microsoft/rebornix/net-boa

Enable notebook smoke test and capture memory leaks
This commit is contained in:
Peng Lyu
2024-08-05 15:44:22 -07:00
committed by GitHub
6 changed files with 313 additions and 5 deletions
+6
View File
@@ -6,6 +6,7 @@
import { Workbench } from './workbench';
import { Code, launch, LaunchOptions } from './code';
import { Logger, measureAndLog } from './logger';
import { Profiler } from './profiler';
export const enum Quality {
Dev,
@@ -63,6 +64,10 @@ export class Application {
return this._userDataPath;
}
private _profiler: Profiler | undefined;
get profiler(): Profiler { return this._profiler!; }
async start(): Promise<void> {
await this._start();
await this.code.waitForElement('.explorer-folders-view');
@@ -110,6 +115,7 @@ export class Application {
});
this._workbench = new Workbench(this._code);
this._profiler = new Profiler(this.code);
return code;
}
-1
View File
@@ -23,7 +23,6 @@ export class Notebook {
await this.code.waitForElement(activeRowSelector);
await this.focusFirstCell();
await this.waitForActiveCellEditorContents('code()');
}
async focusNextCell() {
+77
View File
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as playwright from '@playwright/test';
import type { Protocol } from 'playwright-core/types/protocol';
import { dirname, join } from 'path';
import { promises } from 'fs';
import { IWindowDriver } from './driver';
@@ -83,6 +84,82 @@ export class PlaywrightDriver {
await this.whenLoaded;
}
private _cdpSession: playwright.CDPSession | undefined;
async startCDP() {
if (this._cdpSession) {
return;
}
this._cdpSession = await this.page.context().newCDPSession(this.page);
}
async collectGarbage() {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
await this._cdpSession.send('HeapProfiler.collectGarbage');
}
async evaluate(options: Protocol.Runtime.evaluateParameters): Promise<Protocol.Runtime.evaluateReturnValue> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
return await this._cdpSession.send('Runtime.evaluate', options);
}
async releaseObjectGroup(parameters: Protocol.Runtime.releaseObjectGroupParameters): Promise<void> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
await this._cdpSession.send('Runtime.releaseObjectGroup', parameters);
}
async queryObjects(parameters: Protocol.Runtime.queryObjectsParameters): Promise<Protocol.Runtime.queryObjectsReturnValue> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
return await this._cdpSession.send('Runtime.queryObjects', parameters);
}
async callFunctionOn(parameters: Protocol.Runtime.callFunctionOnParameters): Promise<Protocol.Runtime.callFunctionOnReturnValue> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
return await this._cdpSession.send('Runtime.callFunctionOn', parameters);
}
async takeHeapSnapshot(): Promise<string> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
let snapshot = '';
const listener = (c: { chunk: string }) => {
snapshot += c.chunk;
};
this._cdpSession.addListener('HeapProfiler.addHeapSnapshotChunk', listener);
await this._cdpSession.send('HeapProfiler.takeHeapSnapshot');
this._cdpSession.removeListener('HeapProfiler.addHeapSnapshotChunk', listener);
return snapshot;
}
async getProperties(parameters: Protocol.Runtime.getPropertiesParameters): Promise<Protocol.Runtime.getPropertiesReturnValue> {
if (!this._cdpSession) {
throw new Error('CDP not started');
}
return await this._cdpSession.send('Runtime.getProperties', parameters);
}
private async takeScreenshot(name: string): Promise<void> {
try {
const persistPath = join(this.options.logsPath, `playwright-screenshot-${PlaywrightDriver.screenShotCounter++}-${name.replace(/\s+/g, '-')}.png`);
+208
View File
@@ -0,0 +1,208 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const { decode_bytes } = require('@vscode/v8-heap-parser');
import { Code } from './code';
import { PlaywrightDriver } from './playwrightDriver';
export class Profiler {
constructor(private readonly code: Code) {
}
async checkObjectLeaks(classNames: string | string[], fn: () => Promise<void>): Promise<void> {
await this.code.driver.startCDP();
const classNamesArray = Array.isArray(classNames) ? classNames : [classNames];
const countsBefore = await getInstances(this.code.driver, classNamesArray);
await fn();
const countAfter = await getInstances(this.code.driver, classNamesArray);
const leaks: string[] = [];
for (const className of classNamesArray) {
const count = countAfter[className] ?? 0;
const countBefore = countsBefore[className] ?? 0;
if (count !== countBefore) {
leaks.push(`Leaked ${count - countBefore} ${className}`);
}
}
if (leaks.length > 0) {
throw new Error(leaks.join('\n'));
}
}
async checkHeapLeaks(classNames: string | string[], fn: () => Promise<void>): Promise<void> {
await this.code.driver.startCDP();
await fn();
const heapSnapshotAfter = await this.code.driver.takeHeapSnapshot();
const buff = Buffer.from(heapSnapshotAfter);
const graph = await decode_bytes(buff);
const counts: number[] = Array.from(graph.get_class_counts(classNames));
const leaks: string[] = [];
for (let i = 0; i < classNames.length; i++) {
if (counts[i] > 0) {
leaks.push(`Leaked ${counts[i]} ${classNames[i]}`);
}
}
if (leaks.length > 0) {
throw new Error(leaks.join('\n'));
}
}
}
function generateUuid() {
// use `randomValues` if possible
function getRandomValues(bucket: Uint8Array): Uint8Array {
for (let i = 0; i < bucket.length; i++) {
bucket[i] = Math.floor(Math.random() * 256);
}
return bucket;
}
// prep-work
const _data = new Uint8Array(16);
const _hex: string[] = [];
for (let i = 0; i < 256; i++) {
_hex.push(i.toString(16).padStart(2, '0'));
}
// get data
getRandomValues(_data);
// set version bits
_data[6] = (_data[6] & 0x0f) | 0x40;
_data[8] = (_data[8] & 0x3f) | 0x80;
// print as string
let i = 0;
let result = '';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += '-';
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
result += _hex[_data[i++]];
return result;
}
/*---------------------------------------------------------------------------------------------
* The MIT License (MIT)
* Copyright (c) 2023-present, Simon Siefke
*
* This code is derived from https://github.com/SimonSiefke/vscode-memory-leak-finder
*--------------------------------------------------------------------------------------------*/
const getInstances = async (driver: PlaywrightDriver, classNames: string[]): Promise<{ [key: string]: number }> => {
await driver.collectGarbage();
const objectGroup = `og:${generateUuid()}`;
const prototypeDescriptor = await driver.evaluate({
expression: 'Object.prototype',
returnByValue: false,
objectGroup,
});
const objects = await driver.queryObjects({
prototypeObjectId: prototypeDescriptor.result.objectId!,
objectGroup,
});
const fnResult1 = await driver.callFunctionOn({
functionDeclaration: `function(){
const objects = this
const classNames = ${JSON.stringify(classNames)}
const nativeConstructors = [
Object,
Array,
Function,
Set,
Map,
WeakMap,
WeakSet,
RegExp,
Node,
HTMLScriptElement,
DOMRectReadOnly,
DOMRect,
HTMLHtmlElement,
Node,
DOMTokenList,
HTMLUListElement,
HTMLStyleElement,
HTMLDivElement,
HTMLCollection,
FocusEvent,
Promise,
HTMLLinkElement,
HTMLLIElement,
HTMLAnchorElement,
HTMLSpanElement,
ArrayBuffer,
Uint16Array,
HTMLLabelElement,
TrustedTypePolicy,
Uint8Array,
Uint32Array,
HTMLHeadingElement,
MediaQueryList,
HTMLDocument,
TextDecoder,
TextEncoder,
HTMLInputElement,
HTMLCanvasElement,
HTMLIFrameElement,
Int32Array,
CSSStyleDeclaration
]
const isNativeConstructor = object => {
return nativeConstructors.includes(object.constructor) ||
object.constructor.name === 'AsyncFunction' ||
object.constructor.name === 'GeneratorFunction' ||
object.constructor.name === 'AsyncGeneratorFunction'
}
const isInstance = (object) => {
return object && !isNativeConstructor(object)
}
const instances = objects.filter(isInstance)
const counts = Object.create(null)
for(const instance of instances){
const name=instance.constructor.name
if(classNames.includes(name)){
counts[name]||= 0
counts[name]++
}
}
return counts
}`,
objectId: objects.objects.objectId,
returnByValue: true,
objectGroup,
});
const returnObject = fnResult1.result.value;
await driver.releaseObjectGroup({ objectGroup: objectGroup });
return returnObject;
};
+21 -3
View File
@@ -8,7 +8,7 @@ import { Application, Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
export function setup(logger: Logger) {
describe.skip('Notebooks', () => { // https://github.com/microsoft/vscode/issues/140575
describe('Notebooks', () => { // https://github.com/microsoft/vscode/issues/140575
// Shared before/after handling
installAllHandlers(logger);
@@ -25,7 +25,25 @@ export function setup(logger: Logger) {
cp.execSync('git reset --hard HEAD --quiet', { cwd: app.workspacePathOrFolder });
});
it('inserts/edits code cell', async function () {
it.skip('check heap leaks', async function () {
const app = this.app as Application;
await app.profiler.checkHeapLeaks(['NotebookTextModel', 'NotebookCellTextModel', 'NotebookEventDispatcher'], async () => {
await app.workbench.notebook.openNotebook();
await app.workbench.quickaccess.runCommand('workbench.action.files.save');
await app.workbench.quickaccess.runCommand('workbench.action.closeActiveEditor');
});
});
it('check object leaks', async function () {
const app = this.app as Application;
await app.profiler.checkObjectLeaks(['NotebookTextModel', 'NotebookCellTextModel', 'NotebookEventDispatcher'], async () => {
await app.workbench.notebook.openNotebook();
await app.workbench.quickaccess.runCommand('workbench.action.files.save');
await app.workbench.quickaccess.runCommand('workbench.action.closeActiveEditor');
});
});
it.skip('inserts/edits code cell', async function () {
const app = this.app as Application;
await app.workbench.notebook.openNotebook();
await app.workbench.notebook.focusNextCell();
@@ -34,7 +52,7 @@ export function setup(logger: Logger) {
await app.workbench.notebook.stopEditingCell();
});
it('inserts/edits markdown cell', async function () {
it.skip('inserts/edits markdown cell', async function () {
const app = this.app as Application;
await app.workbench.notebook.openNotebook();
await app.workbench.notebook.focusNextCell();