Skip to content

Commit fec87f3

Browse files
authored
Analyse profiles in worker thread (microsoft#164468)
- profile renderer returns profile data - analyse profiles in separate worker thread - adjust renderer and extension host profiling - adjust build scripts to build worker file
1 parent a033a32 commit fec87f3

File tree

12 files changed

+367
-242
lines changed

12 files changed

+367
-242
lines changed

build/gulpfile.vscode.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const vscodeEntryPoints = _.flatten([
4848
buildfile.workerLanguageDetection,
4949
buildfile.workerSharedProcess,
5050
buildfile.workerLocalFileSearch,
51+
buildfile.workerProfileAnalysis,
5152
buildfile.workbenchDesktop,
5253
buildfile.code
5354
]);

build/gulpfile.vscode.web.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const vscodeWebEntryPoints = _.flatten([
7171
buildfile.workerNotebook,
7272
buildfile.workerLanguageDetection,
7373
buildfile.workerLocalFileSearch,
74+
buildfile.workerProfileAnalysis,
7475
buildfile.keyboardMaps,
7576
buildfile.workbenchWeb
7677
]);

src/buildfile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ exports.workerNotebook = [createEditorWorkerModuleDescription('vs/workbench/cont
5050
exports.workerSharedProcess = [createEditorWorkerModuleDescription('vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain')];
5151
exports.workerLanguageDetection = [createEditorWorkerModuleDescription('vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker')];
5252
exports.workerLocalFileSearch = [createEditorWorkerModuleDescription('vs/workbench/services/search/worker/localFileSearch')];
53+
exports.workerProfileAnalysis = [createEditorWorkerModuleDescription('vs/platform/profiling/electron-sandbox/profileAnalysisWorker')];
5354

5455
exports.workbenchDesktop = [
5556
createEditorWorkerModuleDescription('vs/workbench/contrib/output/common/outputLinkComputer'),

src/vs/platform/native/common/native.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { URI } from 'vs/base/common/uri';
99
import { MessageBoxOptions, MessageBoxReturnValue, MouseInputEvent, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'vs/base/parts/sandbox/common/electronTypes';
1010
import { ISerializableCommandAction } from 'vs/platform/action/common/action';
1111
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
12+
import { IV8Profile } from 'vs/platform/profiling/common/profiling';
1213
import { IPartsSplash } from 'vs/platform/theme/common/themeService';
1314
import { IColorScheme, IOpenedWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IWindowOpenable } from 'vs/platform/window/common/window';
1415

@@ -164,7 +165,7 @@ export interface ICommonNativeHostService {
164165
sendInputEvent(event: MouseInputEvent): Promise<void>;
165166

166167
// Perf Introspection
167-
profileRenderer(session: string, duration: number, perfBaseline: number): Promise<boolean>;
168+
profileRenderer(session: string, duration: number): Promise<IV8Profile>;
168169

169170
// Connectivity
170171
resolveProxy(url: string): Promise<string | undefined>;

src/vs/platform/native/electron-main/nativeHostMainService.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ import { isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/worksp
4141
import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
4242
import { VSBuffer } from 'vs/base/common/buffer';
4343
import { hasWSLFeatureInstalled } from 'vs/platform/remote/node/wsl';
44-
import { ProfilingOutput, WindowProfiler } from 'vs/platform/profiling/electron-main/windowProfiling';
45-
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
44+
import { WindowProfiler } from 'vs/platform/profiling/electron-main/windowProfiling';
45+
import { IV8Profile } from 'vs/platform/profiling/common/profiling';
4646

4747
export interface INativeHostMainService extends AddFirstParameterToFunctions<ICommonNativeHostService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
4848

@@ -61,8 +61,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
6161
@ILogService private readonly logService: ILogService,
6262
@IProductService private readonly productService: IProductService,
6363
@IThemeMainService private readonly themeMainService: IThemeMainService,
64-
@IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService,
65-
@ITelemetryService private readonly telemetryService: ITelemetryService,
64+
@IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService
6665
) {
6766
super();
6867
}
@@ -782,14 +781,14 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
782781

783782
// #region Performance
784783

785-
async profileRenderer(windowId: number | undefined, session: string, duration: number, baseline: number): Promise<boolean> {
784+
async profileRenderer(windowId: number | undefined, session: string, duration: number): Promise<IV8Profile> {
786785
const win = this.windowById(windowId);
787786
if (!win || !win.win) {
788-
return false;
787+
throw new Error();
789788
}
790-
const profiler = new WindowProfiler(win.win, session, this.logService, this.telemetryService);
791-
const result = await profiler.inspect(duration, baseline);
792-
return result === ProfilingOutput.Interesting;
789+
const profiler = new WindowProfiler(win.win, session, this.logService);
790+
const result = await profiler.inspect(duration);
791+
return result;
793792
}
794793

795794
// #endregion

src/vs/platform/profiling/common/profilingModel.ts

Lines changed: 12 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { basename } from 'vs/base/common/path';
76
import type { IV8Profile, IV8ProfileNode } from 'vs/platform/profiling/common/profiling';
87

98
// #region
109
// https://github.com/microsoft/vscode-js-profile-visualizer/blob/6e7401128ee860be113a916f80fcfe20ac99418e/packages/vscode-js-profile-core/src/cpu/model.ts#L4
1110

12-
interface IProfileModel {
11+
export interface IProfileModel {
1312
nodes: ReadonlyArray<IComputedNode>;
1413
locations: ReadonlyArray<ILocation>;
1514
samples: ReadonlyArray<number>;
@@ -18,7 +17,7 @@ interface IProfileModel {
1817
duration: number;
1918
}
2019

21-
interface IComputedNode {
20+
export interface IComputedNode {
2221
id: number;
2322
selfTime: number;
2423
aggregateTime: number;
@@ -27,53 +26,53 @@ interface IComputedNode {
2726
locationId: number;
2827
}
2928

30-
interface ISourceLocation {
29+
export interface ISourceLocation {
3130
lineNumber: number;
3231
columnNumber: number;
3332
// source: Dap.Source;
3433
relativePath?: string;
3534
}
3635

37-
interface CdpCallFrame {
36+
export interface CdpCallFrame {
3837
functionName: string;
3938
scriptId: string;
4039
url: string;
4140
lineNumber: number;
4241
columnNumber: number;
4342
}
4443

45-
interface CdpPositionTickInfo {
44+
export interface CdpPositionTickInfo {
4645
line: number;
4746
ticks: number;
4847
}
4948

50-
interface INode {
49+
export interface INode {
5150
id: number;
5251
// category: Category;
5352
callFrame: CdpCallFrame;
5453
src?: ISourceLocation;
5554
}
5655

57-
interface ILocation extends INode {
56+
export interface ILocation extends INode {
5857
selfTime: number;
5958
aggregateTime: number;
6059
ticks: number;
6160
}
6261

63-
interface IAnnotationLocation {
62+
export interface IAnnotationLocation {
6463
callFrame: CdpCallFrame;
6564
locations: ISourceLocation[];
6665
}
6766

68-
interface IProfileNode extends IV8ProfileNode {
67+
export interface IProfileNode extends IV8ProfileNode {
6968
locationId?: number;
7069
positionTicks?: (CdpPositionTickInfo & {
7170
startLocationId?: number;
7271
endLocationId?: number;
7372
})[];
7473
}
7574

76-
interface ICpuProfileRaw extends IV8Profile {
75+
export interface ICpuProfileRaw extends IV8Profile {
7776
// $vscode?: IJsDebugAnnotations;
7877
nodes: IProfileNode[];
7978
}
@@ -266,7 +265,7 @@ export const buildModel = (profile: ICpuProfileRaw): IProfileModel => {
266265
};
267266
};
268267

269-
class BottomUpNode {
268+
export class BottomUpNode {
270269
public static root() {
271270
return new BottomUpNode({
272271
id: -1,
@@ -310,7 +309,7 @@ class BottomUpNode {
310309

311310
}
312311

313-
const processNode = (aggregate: BottomUpNode, node: IComputedNode, model: IProfileModel, initialNode = node) => {
312+
export const processNode = (aggregate: BottomUpNode, node: IComputedNode, model: IProfileModel, initialNode = node) => {
314313
let child = aggregate.children[node.locationId];
315314
if (!child) {
316315
child = new BottomUpNode(model.locations[node.locationId], aggregate);
@@ -327,15 +326,6 @@ const processNode = (aggregate: BottomUpNode, node: IComputedNode, model: IProfi
327326

328327
//#endregion
329328

330-
function isSpecial(call: CdpCallFrame): boolean {
331-
return call.functionName.startsWith('(') && call.functionName.endsWith(')');
332-
}
333-
334-
function isModel(arg: IV8Profile | IProfileModel): arg is IProfileModel {
335-
return Array.isArray((<IProfileModel>arg).locations)
336-
&& Array.isArray((<IProfileModel>arg).samples)
337-
&& Array.isArray((<IProfileModel>arg).timeDeltas);
338-
}
339329

340330
export interface BottomUpSample {
341331
selfTime: number;
@@ -346,70 +336,3 @@ export interface BottomUpSample {
346336
percentage: number;
347337
isSpecial: boolean;
348338
}
349-
350-
export function bottomUp(profileOrModel: IV8Profile | IProfileModel, topN: number, fullPaths: boolean = false) {
351-
352-
const model = isModel(profileOrModel)
353-
? profileOrModel
354-
: buildModel(profileOrModel);
355-
356-
const root = BottomUpNode.root();
357-
for (const node of model.nodes) {
358-
processNode(root, node, model);
359-
root.addNode(node);
360-
}
361-
362-
const result = Object.values(root.children)
363-
.sort((a, b) => b.selfTime - a.selfTime)
364-
.slice(0, topN);
365-
366-
367-
const samples: BottomUpSample[] = [];
368-
369-
function printCallFrame(frame: CdpCallFrame): string {
370-
let result = frame.functionName || '(anonymous)';
371-
if (frame.url) {
372-
result += '#';
373-
result += fullPaths ? frame.url : basename(frame.url);
374-
if (frame.lineNumber >= 0) {
375-
result += ':';
376-
result += frame.lineNumber + 1;
377-
}
378-
}
379-
return result;
380-
}
381-
382-
for (const node of result) {
383-
384-
const sample: BottomUpSample = {
385-
selfTime: Math.round(node.selfTime / 1000),
386-
totalTime: Math.round(node.aggregateTime / 1000),
387-
location: printCallFrame(node.callFrame),
388-
url: node.callFrame.url,
389-
caller: [],
390-
percentage: Math.round(node.selfTime / (model.duration / 100)),
391-
isSpecial: isSpecial(node.callFrame)
392-
};
393-
394-
// follow the heaviest caller paths
395-
const stack = [node];
396-
while (stack.length) {
397-
const node = stack.pop()!;
398-
let top: BottomUpNode | undefined;
399-
for (const candidate of Object.values(node.children)) {
400-
if (!top || top.selfTime < candidate.selfTime) {
401-
top = candidate;
402-
}
403-
}
404-
if (top) {
405-
const percentage = Math.round(top.selfTime / (node.selfTime / 100));
406-
sample.caller.push({ percentage, location: printCallFrame(top.callFrame) });
407-
stack.push(top);
408-
}
409-
}
410-
411-
samples.push(sample);
412-
}
413-
414-
return samples;
415-
}

src/vs/platform/profiling/electron-main/windowProfiling.ts

Lines changed: 13 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,111 +3,44 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import type { Profile, ProfileResult } from 'v8-inspect-profiler';
6+
import type { ProfileResult } from 'v8-inspect-profiler';
77
import { BrowserWindow } from 'electron';
88
import { timeout } from 'vs/base/common/async';
99
import { ILogService } from 'vs/platform/log/common/log';
10-
import { Promises } from 'vs/base/node/pfs';
11-
import { tmpdir } from 'os';
12-
import { join } from 'vs/base/common/path';
13-
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
14-
import { Utils } from 'vs/platform/profiling/common/profiling';
15-
import { bottomUp, buildModel, } from 'vs/platform/profiling/common/profilingModel';
16-
import { reportSample } from 'vs/platform/profiling/common/profilingTelemetrySpec';
17-
import { onUnexpectedError } from 'vs/base/common/errors';
18-
19-
export const enum ProfilingOutput {
20-
Failure,
21-
Irrelevant,
22-
Interesting,
23-
}
10+
import { IV8Profile } from 'vs/platform/profiling/common/profiling';
2411

2512
export class WindowProfiler {
2613

2714
constructor(
2815
private readonly _window: BrowserWindow,
2916
private readonly _sessionId: string,
3017
@ILogService private readonly _logService: ILogService,
31-
@ITelemetryService private readonly _telemetryService: ITelemetryService,
32-
) {
33-
// noop
34-
}
18+
) { }
3519

36-
async inspect(duration: number, baseline: number): Promise<ProfilingOutput> {
20+
async inspect(duration: number): Promise<IV8Profile> {
3721

38-
const success = await this._connect();
39-
if (!success) {
40-
return ProfilingOutput.Failure;
41-
}
22+
await this._connect();
4223

4324
const inspector = this._window.webContents.debugger;
4425
await inspector.sendCommand('Profiler.start');
4526
this._logService.warn('[perf] profiling STARTED', this._sessionId);
4627
await timeout(duration);
4728
const data: ProfileResult = await inspector.sendCommand('Profiler.stop');
4829
this._logService.warn('[perf] profiling DONE', this._sessionId);
49-
const result = this._digest(data.profile, baseline);
30+
5031
await this._disconnect();
51-
return result;
32+
return data.profile;
5233
}
5334

5435
private async _connect() {
55-
try {
56-
const inspector = this._window.webContents.debugger;
57-
inspector.attach();
58-
await inspector.sendCommand('Profiler.enable');
59-
return true;
60-
} catch (error) {
61-
this._logService.error(error, '[perf] FAILED to enable profiler', this._sessionId);
62-
return false;
63-
}
36+
const inspector = this._window.webContents.debugger;
37+
inspector.attach();
38+
await inspector.sendCommand('Profiler.enable');
6439
}
6540

6641
private async _disconnect() {
67-
try {
68-
const inspector = this._window.webContents.debugger;
69-
await inspector.sendCommand('Profiler.disable');
70-
inspector.detach();
71-
} catch (error) {
72-
this._logService.error(error, '[perf] FAILED to disable profiler', this._sessionId);
73-
}
74-
}
75-
76-
private _digest(profile: Profile, perfBaseline: number): ProfilingOutput {
77-
if (!Utils.isValidProfile(profile)) {
78-
this._logService.warn('[perf] INVALID profile: no samples or timeDeltas', this._sessionId);
79-
return ProfilingOutput.Irrelevant;
80-
}
81-
82-
const model = buildModel(profile);
83-
const samples = bottomUp(model, 5, false)
84-
.filter(s => !s.isSpecial);
85-
86-
if (samples.length === 0 || samples[1].percentage < 10) {
87-
// ignore this profile because 90% of the time is spent inside "special" frames
88-
// like idle, GC, or program
89-
this._logService.warn('[perf] profiling did NOT reveal anything interesting', this._sessionId);
90-
return ProfilingOutput.Irrelevant;
91-
}
92-
93-
// send telemetry events
94-
for (const sample of samples) {
95-
reportSample(
96-
{ sample, perfBaseline, source: '<<renderer>>' },
97-
this._telemetryService,
98-
this._logService
99-
);
100-
}
101-
102-
// save to disk
103-
this._store(profile).catch(onUnexpectedError);
104-
105-
return ProfilingOutput.Interesting;
106-
}
107-
108-
private async _store(profile: Profile): Promise<void> {
109-
const path = join(tmpdir(), `renderer-profile-${Date.now()}.cpuprofile`);
110-
await Promises.writeFile(path, JSON.stringify(profile));
111-
this._logService.info(`[perf] stored profile to DISK '${path}'`, this._sessionId);
42+
const inspector = this._window.webContents.debugger;
43+
await inspector.sendCommand('Profiler.disable');
44+
inspector.detach();
11245
}
11346
}

0 commit comments

Comments
 (0)