Add performance profiler command (#306548)

* profile

* wip

* use status bar

* clean

* clean

* clean

* chore: add netlog category for network tracing

---------

Co-authored-by: deepak1556 <hop2deep@gmail.com>
This commit is contained in:
Paul
2026-04-01 07:38:48 -07:00
committed by GitHub
parent 015aef6054
commit 3b940c2d3e
5 changed files with 84 additions and 10 deletions

View File

@@ -242,6 +242,7 @@ export interface ICommonNativeHostService {
// Perf Introspection
profileRenderer(session: string, duration: number): Promise<IV8Profile>;
startTracing(categories: string): Promise<void>;
// Connectivity
resolveProxy(url: string): Promise<string | undefined>;

View File

@@ -1159,11 +1159,29 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
}
async stopTracing(windowId: number | undefined): Promise<void> {
if (!this.environmentMainService.args.trace) {
return; // requires tracing to be on
private _isTracing = false;
async startTracing(windowId: number | undefined, categories: string): Promise<void> {
if (this._isTracing) {
throw new Error(localize('tracing.alreadyInProgress', 'A tracing session is already in progress. Use command `"{0}"` to stop it first.', 'workbench.action.stopTracing'));
}
const traceOptions = ['record-until-full', 'enable-sampling'];
await contentTracing.startRecording({
categoryFilter: categories,
traceOptions: traceOptions.join(',')
});
this._isTracing = true;
}
async stopTracing(windowId: number | undefined): Promise<void> {
if (!this._isTracing && !this.environmentMainService.args.trace) {
return; // no tracing in progress
}
this._isTracing = false;
const path = await contentTracing.stopRecording(`${randomPath(this.environmentMainService.userHome.fsPath, this.productService.applicationName)}.trace.txt`);
// Inform user to report an issue

View File

@@ -16,9 +16,9 @@ import { KeyCode, KeyMod } from '../../../base/common/keyCodes.js';
import { INativeWorkbenchEnvironmentService } from '../../services/environment/electron-browser/environmentService.js';
import { URI } from '../../../base/common/uri.js';
import { getActiveWindow } from '../../../base/browser/dom.js';
import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';
import { INativeEnvironmentService } from '../../../platform/environment/common/environment.js';
import { IProgressService, ProgressLocation } from '../../../platform/progress/common/progress.js';
import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';
import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../services/statusbar/browser/statusbar.js';
export class ToggleDevToolsAction extends Action2 {
@@ -140,6 +140,54 @@ export class ShowContentTracingAction extends Action2 {
}
}
let activeTracingEntry: IStatusbarEntryAccessor | undefined;
export class StartTracing extends Action2 {
constructor() {
super({
id: 'workbench.action.startTracing',
title: localize2('startTracing', 'Start Tracing'),
category: Categories.Developer,
f1: true
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
const nativeHostService = accessor.get(INativeHostService);
const statusbarService = accessor.get(IStatusbarService);
const categories = [
'content',
'renderer_host',
'browser',
'renderer',
'blink',
'blink.user_timing',
'netlog',
'net',
'v8',
'disabled-by-default-v8.cpu_profiler',
'disabled-by-default-devtools.timeline',
'disabled-by-default-network',
'disabled-by-default-net',
'disabled-by-default-v8.gc_stats',
'disabled-by-default-v8.stack_trace',
];
await nativeHostService.startTracing(categories.join(','));
activeTracingEntry?.dispose();
activeTracingEntry = statusbarService.addEntry({
name: localize('startTracing.name', "Performance Trace"),
text: '$(record) ' + localize('startTracing.recording', "Recording trace (click to stop)"),
ariaLabel: localize('startTracing.ariaLabel', "Recording performance trace. Click to stop recording."),
tooltip: localize('startTracing.tooltip', "Click to stop recording"),
kind: 'error',
command: StopTracing.ID
}, 'status.tracing', StatusbarAlignment.LEFT, -Number.MAX_VALUE);
}
}
export class StopTracing extends Action2 {
static readonly ID = 'workbench.action.stopTracing';
@@ -154,20 +202,22 @@ export class StopTracing extends Action2 {
}
override async run(accessor: ServicesAccessor): Promise<void> {
const environmentService = accessor.get(INativeEnvironmentService);
const dialogService = accessor.get(IDialogService);
const nativeHostService = accessor.get(INativeHostService);
const environmentService = accessor.get(INativeWorkbenchEnvironmentService);
const dialogService = accessor.get(IDialogService);
const progressService = accessor.get(IProgressService);
if (!environmentService.args.trace) {
if (!activeTracingEntry && !environmentService.args.trace) {
const { confirmed } = await dialogService.confirm({
message: localize('stopTracing.message', "Tracing requires to launch with a '--trace' argument"),
message: localize('stopTracing.message', "No tracing session is in progress. Use 'Developer: Start Tracing' or launch with a '--trace' argument to begin tracing."),
primaryButton: localize({ key: 'stopTracing.button', comment: ['&& denotes a mnemonic'] }, "&&Relaunch and Enable Tracing"),
});
if (confirmed) {
return nativeHostService.relaunch({ addArgs: ['--trace'] });
}
return;
}
await progressService.withProgress({
@@ -176,5 +226,8 @@ export class StopTracing extends Action2 {
cancellable: false,
detail: localize('stopTracing.detail', "This can take up to one minute to complete.")
}, () => nativeHostService.stopTracing());
activeTracingEntry?.dispose();
activeTracingEntry = undefined;
}
}

View File

@@ -9,7 +9,7 @@ import { MenuRegistry, MenuId, registerAction2 } from '../../platform/actions/co
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../platform/configuration/common/configurationRegistry.js';
import { KeyMod, KeyCode } from '../../base/common/keyCodes.js';
import { isLinux, isMacintosh, isWindows } from '../../base/common/platform.js';
import { ConfigureRuntimeArgumentsAction, ToggleDevToolsAction, ReloadWindowWithExtensionsDisabledAction, OpenUserDataFolderAction, ShowGPUInfoAction, ShowContentTracingAction, StopTracing } from './actions/developerActions.js';
import { ConfigureRuntimeArgumentsAction, ToggleDevToolsAction, ReloadWindowWithExtensionsDisabledAction, OpenUserDataFolderAction, ShowGPUInfoAction, ShowContentTracingAction, StopTracing, StartTracing } from './actions/developerActions.js';
import { ZoomResetAction, ZoomOutAction, ZoomInAction, CloseWindowAction, SwitchWindowAction, QuickSwitchWindowAction, NewWindowTabHandler, ShowPreviousWindowTabHandler, ShowNextWindowTabHandler, MoveWindowTabToNewWindowHandler, MergeWindowTabsHandlerHandler, ToggleWindowTabsBarHandler, ToggleWindowAlwaysOnTopAction, DisableWindowAlwaysOnTopAction, EnableWindowAlwaysOnTopAction, CloseOtherWindowsAction } from './actions/windowActions.js';
import { ContextKeyExpr } from '../../platform/contextkey/common/contextkey.js';
import { KeybindingsRegistry, KeybindingWeight } from '../../platform/keybinding/common/keybindingsRegistry.js';
@@ -116,6 +116,7 @@ import product from '../../platform/product/common/product.js';
registerAction2(ShowGPUInfoAction);
registerAction2(ShowContentTracingAction);
registerAction2(StopTracing);
registerAction2(StartTracing);
})();
// Menu

View File

@@ -184,6 +184,7 @@ export class TestNativeHostService implements INativeHostService {
async windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise<string | undefined> { return undefined; }
async createZipFile(zipPath: URI, files: { path: string; contents: string }[]): Promise<void> { }
async profileRenderer(): Promise<any> { throw new Error(); }
async startTracing(): Promise<void> { throw new Error(); }
async getScreenshot(rect?: IRectangle): Promise<VSBuffer | undefined> { return undefined; }
async showToast(options: IToastOptions): Promise<IToastResult> { return { supported: false, clicked: false }; }
async clearToast(id: string): Promise<void> { }