Path: blob/main/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { timeout } from '../../../../base/common/async.js';6import { VSBuffer } from '../../../../base/common/buffer.js';7import { CancellationTokenSource } from '../../../../base/common/cancellation.js';8import { onUnexpectedError } from '../../../../base/common/errors.js';9import { IDisposable } from '../../../../base/common/lifecycle.js';10import { Schemas } from '../../../../base/common/network.js';11import { joinPath } from '../../../../base/common/resources.js';12import { TernarySearchTree } from '../../../../base/common/ternarySearchTree.js';13import { URI } from '../../../../base/common/uri.js';14import { generateUuid } from '../../../../base/common/uuid.js';15import { localize } from '../../../../nls.js';16import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';17import { ExtensionIdentifier, ExtensionIdentifierSet, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';18import { IFileService } from '../../../../platform/files/common/files.js';19import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';20import { ILogService } from '../../../../platform/log/common/log.js';21import { INotificationService, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';22import { IProfileAnalysisWorkerService } from '../../../../platform/profiling/electron-browser/profileAnalysisWorkerService.js';23import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';24import { IWorkbenchContribution } from '../../../common/contributions.js';25import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js';26import { createSlowExtensionAction } from './extensionsSlowActions.js';27import { IExtensionHostProfileService } from './runtimeExtensionsEditor.js';28import { IEditorService } from '../../../services/editor/common/editorService.js';29import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js';30import { ExtensionHostKind } from '../../../services/extensions/common/extensionHostKind.js';31import { IExtensionHostProfile, IExtensionService, IResponsiveStateChangeEvent, ProfileSession } from '../../../services/extensions/common/extensions.js';32import { ExtensionHostProfiler } from '../../../services/extensions/electron-browser/extensionHostProfiler.js';33import { ITimerService } from '../../../services/timer/browser/timerService.js';3435export class ExtensionsAutoProfiler implements IWorkbenchContribution {3637private readonly _blame = new ExtensionIdentifierSet();3839private _session: CancellationTokenSource | undefined;40private _unresponsiveListener: IDisposable | undefined;41private _perfBaseline: number = -1;4243constructor(44@IExtensionService private readonly _extensionService: IExtensionService,45@IExtensionHostProfileService private readonly _extensionProfileService: IExtensionHostProfileService,46@ITelemetryService private readonly _telemetryService: ITelemetryService,47@ILogService private readonly _logService: ILogService,48@INotificationService private readonly _notificationService: INotificationService,49@IEditorService private readonly _editorService: IEditorService,50@IInstantiationService private readonly _instantiationService: IInstantiationService,51@INativeWorkbenchEnvironmentService private readonly _environmentServie: INativeWorkbenchEnvironmentService,52@IProfileAnalysisWorkerService private readonly _profileAnalysisService: IProfileAnalysisWorkerService,53@IConfigurationService private readonly _configService: IConfigurationService,54@IFileService private readonly _fileService: IFileService,55@ITimerService timerService: ITimerService56) {5758timerService.perfBaseline.then(value => {59if (value < 0) {60return; // too slow for profiling61}62this._perfBaseline = value;63this._unresponsiveListener = _extensionService.onDidChangeResponsiveChange(this._onDidChangeResponsiveChange, this);64});65}6667dispose(): void {68this._unresponsiveListener?.dispose();69this._session?.dispose(true);70}7172private async _onDidChangeResponsiveChange(event: IResponsiveStateChangeEvent): Promise<void> {73if (event.extensionHostKind !== ExtensionHostKind.LocalProcess) {74return;75}7677const listener = await event.getInspectListener(true);7879if (!listener) {80return;81}8283if (event.isResponsive && this._session) {84// stop profiling when responsive again85this._session.cancel();86this._logService.info('UNRESPONSIVE extension host: received responsive event and cancelling profiling session');878889} else if (!event.isResponsive && !this._session) {90// start profiling if not yet profiling91const cts = new CancellationTokenSource();92this._session = cts;939495let session: ProfileSession;96try {97session = await this._instantiationService.createInstance(ExtensionHostProfiler, listener.host, listener.port).start();9899} catch (err) {100this._session = undefined;101// fail silent as this is often102// caused by another party being103// connected already104return;105}106this._logService.info('UNRESPONSIVE extension host: starting to profile NOW');107108// wait 5 seconds or until responsive again109try {110await timeout(5e3, cts.token);111} catch {112// can throw cancellation error. that is113// OK, we stop profiling and analyse the114// profile anyways115}116117try {118// stop profiling and analyse results119this._processCpuProfile(await session.stop());120} catch (err) {121onUnexpectedError(err);122} finally {123this._session = undefined;124}125}126}127128private async _processCpuProfile(profile: IExtensionHostProfile) {129130// get all extensions131await this._extensionService.whenInstalledExtensionsRegistered();132133// send heavy samples iff enabled134if (this._configService.getValue('application.experimental.rendererProfiling')) {135136const searchTree = TernarySearchTree.forUris<IExtensionDescription>();137searchTree.fill(this._extensionService.extensions.map(e => [e.extensionLocation, e]));138139await this._profileAnalysisService.analyseBottomUp(140profile.data,141url => searchTree.findSubstr(URI.parse(url))?.identifier.value ?? '<<not-found>>',142this._perfBaseline,143false144);145}146147// analyse profile by extension-category148const categories: [location: URI, id: string][] = this._extensionService.extensions149.filter(e => e.extensionLocation.scheme === Schemas.file)150.map(e => [e.extensionLocation, ExtensionIdentifier.toKey(e.identifier)]);151152const data = await this._profileAnalysisService.analyseByLocation(profile.data, categories);153154//155let overall: number = 0;156let top: string = '';157let topAggregated: number = -1;158for (const [category, aggregated] of data) {159overall += aggregated;160if (aggregated > topAggregated) {161topAggregated = aggregated;162top = category;163}164}165const topPercentage = topAggregated / (overall / 100);166167// associate extensions to profile node168const extension = await this._extensionService.getExtension(top);169if (!extension) {170// not an extension => idle, gc, self?171return;172}173174175const profilingSessionId = generateUuid();176177// print message to log178const path = joinPath(this._environmentServie.tmpDir, `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`);179await this._fileService.writeFile(path, VSBuffer.fromString(JSON.stringify(profile.data)));180this._logService.warn(`UNRESPONSIVE extension host: '${top}' took ${topPercentage}% of ${topAggregated / 1e3}ms, saved PROFILE here: '${path}'`);181182type UnresponsiveData = {183duration: number;184profilingSessionId: string;185data: string[];186id: string;187};188type UnresponsiveDataClassification = {189owner: 'jrieken';190comment: 'Profiling data that was collected while the extension host was unresponsive';191profilingSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Identifier of a profiling session' };192duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Duration for which the extension host was unresponsive' };193data: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Extensions ids and core parts that were active while the extension host was frozen' };194id: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Top extensions id that took most of the duration' };195};196this._telemetryService.publicLog2<UnresponsiveData, UnresponsiveDataClassification>('exthostunresponsive', {197profilingSessionId,198duration: overall,199data: data.map(tuple => tuple[0]).flat(),200id: ExtensionIdentifier.toKey(extension.identifier),201});202203204// add to running extensions view205this._extensionProfileService.setUnresponsiveProfile(extension.identifier, profile);206207// prompt: when really slow/greedy208if (!(topPercentage >= 95 && topAggregated >= 5e6)) {209return;210}211212const action = await this._instantiationService.invokeFunction(createSlowExtensionAction, extension, profile);213214if (!action) {215// cannot report issues against this extension...216return;217}218219// only blame once per extension, don't blame too often220if (this._blame.has(extension.identifier) || this._blame.size >= 3) {221return;222}223this._blame.add(extension.identifier);224225// user-facing message when very bad...226this._notificationService.prompt(227Severity.Warning,228localize(229'unresponsive-exthost',230"The extension '{0}' took a very long time to complete its last operation and it has prevented other extensions from running.",231extension.displayName || extension.name232),233[{234label: localize('show', 'Show Extensions'),235run: () => this._editorService.openEditor(RuntimeExtensionsInput.instance, { pinned: true })236},237action238],239{ priority: NotificationPriority.SILENT }240);241}242}243244245