From 3b940c2d3e43f709e7339220dae217b67feae143 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 1 Apr 2026 07:38:48 -0700 Subject: [PATCH] Add performance profiler command (#306548) * profile * wip * use status bar * clean * clean * clean * chore: add netlog category for network tracing --------- Co-authored-by: deepak1556 --- src/vs/platform/native/common/native.ts | 1 + .../electron-main/nativeHostMainService.ts | 24 ++++++- .../actions/developerActions.ts | 65 +++++++++++++++++-- .../electron-browser/desktop.contribution.ts | 3 +- .../electron-browser/workbenchTestServices.ts | 1 + 5 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index dbcd0f1dfd1..3e47d272d00 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -242,6 +242,7 @@ export interface ICommonNativeHostService { // Perf Introspection profileRenderer(session: string, duration: number): Promise; + startTracing(categories: string): Promise; // Connectivity resolveProxy(url: string): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 3c0dbd8a8f6..19620e7bf4d 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -1159,11 +1159,29 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } - async stopTracing(windowId: number | undefined): Promise { - if (!this.environmentMainService.args.trace) { - return; // requires tracing to be on + private _isTracing = false; + + async startTracing(windowId: number | undefined, categories: string): Promise { + 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 { + 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 diff --git a/src/vs/workbench/electron-browser/actions/developerActions.ts b/src/vs/workbench/electron-browser/actions/developerActions.ts index 85d84a9debb..f56a9d24177 100644 --- a/src/vs/workbench/electron-browser/actions/developerActions.ts +++ b/src/vs/workbench/electron-browser/actions/developerActions.ts @@ -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 { + 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 { - 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; } } diff --git a/src/vs/workbench/electron-browser/desktop.contribution.ts b/src/vs/workbench/electron-browser/desktop.contribution.ts index 4c3893ac7ed..928e7ea2173 100644 --- a/src/vs/workbench/electron-browser/desktop.contribution.ts +++ b/src/vs/workbench/electron-browser/desktop.contribution.ts @@ -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 diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 62e4132a1a7..767c590b600 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -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 { return undefined; } async createZipFile(zipPath: URI, files: { path: string; contents: string }[]): Promise { } async profileRenderer(): Promise { throw new Error(); } + async startTracing(): Promise { throw new Error(); } async getScreenshot(rect?: IRectangle): Promise { return undefined; } async showToast(options: IToastOptions): Promise { return { supported: false, clicked: false }; } async clearToast(id: string): Promise { }