mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
Merge pull request #224547 from microsoft/rebornix/net-boa
Enable notebook smoke test and capture memory leaks
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ export class Notebook {
|
||||
|
||||
await this.code.waitForElement(activeRowSelector);
|
||||
await this.focusFirstCell();
|
||||
await this.waitForActiveCellEditorContents('code()');
|
||||
}
|
||||
|
||||
async focusNextCell() {
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user