Skip to content

Commit

Permalink
Merge pull request #224547 from microsoft/rebornix/net-boa
Browse files Browse the repository at this point in the history
Enable notebook smoke test and capture memory leaks
  • Loading branch information
rebornix authored Aug 5, 2024
2 parents 27fd211 + e4702fa commit df447e7
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 5 deletions.
2 changes: 1 addition & 1 deletion build/azure-pipelines/linux/product-build-linux-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ steps:
- script: yarn --cwd test/smoke compile
displayName: Compile smoke tests

- script: yarn gulp compile-extension:markdown-language-features compile-extension-media compile-extension:vscode-test-resolver
- script: yarn gulp compile-extension:markdown-language-features compile-extension:ipynb compile-extension-media compile-extension:vscode-test-resolver
displayName: Build extensions for smoke tests

- script: yarn gulp node
Expand Down
6 changes: 6 additions & 0 deletions test/automation/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -110,6 +115,7 @@ export class Application {
});

this._workbench = new Workbench(this._code);
this._profiler = new Profiler(this.code);

return code;
}
Expand Down
1 change: 0 additions & 1 deletion test/automation/src/notebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export class Notebook {

await this.code.waitForElement(activeRowSelector);
await this.focusFirstCell();
await this.waitForActiveCellEditorContents('code()');
}

async focusNextCell() {
Expand Down
77 changes: 77 additions & 0 deletions test/automation/src/playwrightDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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`);
Expand Down
208 changes: 208 additions & 0 deletions test/automation/src/profiler.ts
Original file line number Diff line number Diff line change
@@ -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;
};
24 changes: 21 additions & 3 deletions test/smoke/src/areas/notebook/notebook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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();
Expand Down

0 comments on commit df447e7

Please sign in to comment.