diff options
author | Johannes Rieken <johannes.rieken@gmail.com> | 2022-01-03 19:45:26 +0300 |
---|---|---|
committer | Johannes Rieken <johannes.rieken@gmail.com> | 2022-01-03 19:45:40 +0300 |
commit | 4636c22e55a7d4ac49740257d72d2de060f5809a (patch) | |
tree | 424dcf216489b608606431017a45e6c5d48f83fa /src/vs/workbench/contrib/extensions/electron-sandbox | |
parent | f5d5c6863e72e4b61018eeeff9e525f625f42645 (diff) |
move things to electron-sandbox, https://github.com/microsoft/vscode/issues/111211
Diffstat (limited to 'src/vs/workbench/contrib/extensions/electron-sandbox')
3 files changed, 382 insertions, 2 deletions
diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts new file mode 100644 index 00000000000..c6efc4ff567 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IExtensionHostProfile, ProfileSession, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { Disposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { IExtensionHostProfileService, ProfileSessionState } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { randomPort } from 'vs/base/common/ports'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; + +export class ExtensionHostProfileService extends Disposable implements IExtensionHostProfileService { + + declare readonly _serviceBrand: undefined; + + private readonly _onDidChangeState: Emitter<void> = this._register(new Emitter<void>()); + public readonly onDidChangeState: Event<void> = this._onDidChangeState.event; + + private readonly _onDidChangeLastProfile: Emitter<void> = this._register(new Emitter<void>()); + public readonly onDidChangeLastProfile: Event<void> = this._onDidChangeLastProfile.event; + + private readonly _unresponsiveProfiles = new Map<string, IExtensionHostProfile>(); + private _profile: IExtensionHostProfile | null; + private _profileSession: ProfileSession | null; + private _state: ProfileSessionState = ProfileSessionState.None; + + private profilingStatusBarIndicator: IStatusbarEntryAccessor | undefined; + private readonly profilingStatusBarIndicatorLabelUpdater = this._register(new MutableDisposable()); + + public get state() { return this._state; } + public get lastProfile() { return this._profile; } + + constructor( + @IExtensionService private readonly _extensionService: IExtensionService, + @IEditorService private readonly _editorService: IEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @INativeHostService private readonly _nativeHostService: INativeHostService, + @IDialogService private readonly _dialogService: IDialogService, + @IStatusbarService private readonly _statusbarService: IStatusbarService, + @IProductService private readonly _productService: IProductService + ) { + super(); + this._profile = null; + this._profileSession = null; + this._setState(ProfileSessionState.None); + + CommandsRegistry.registerCommand('workbench.action.extensionHostProfiler.stop', () => { + this.stopProfiling(); + this._editorService.openEditor(RuntimeExtensionsInput.instance, { pinned: true }); + }); + } + + private _setState(state: ProfileSessionState): void { + if (this._state === state) { + return; + } + this._state = state; + + if (this._state === ProfileSessionState.Running) { + this.updateProfilingStatusBarIndicator(true); + } else if (this._state === ProfileSessionState.Stopping) { + this.updateProfilingStatusBarIndicator(false); + } + + this._onDidChangeState.fire(undefined); + } + + private updateProfilingStatusBarIndicator(visible: boolean): void { + this.profilingStatusBarIndicatorLabelUpdater.clear(); + + if (visible) { + const indicator: IStatusbarEntry = { + name: nls.localize('status.profiler', "Extension Profiler"), + text: nls.localize('profilingExtensionHost', "Profiling Extension Host"), + showProgress: true, + ariaLabel: nls.localize('profilingExtensionHost', "Profiling Extension Host"), + tooltip: nls.localize('selectAndStartDebug', "Click to stop profiling."), + command: 'workbench.action.extensionHostProfiler.stop' + }; + + const timeStarted = Date.now(); + const handle = setInterval(() => { + if (this.profilingStatusBarIndicator) { + this.profilingStatusBarIndicator.update({ ...indicator, text: nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), }); + } + }, 1000); + this.profilingStatusBarIndicatorLabelUpdater.value = toDisposable(() => clearInterval(handle)); + + if (!this.profilingStatusBarIndicator) { + this.profilingStatusBarIndicator = this._statusbarService.addEntry(indicator, 'status.profiler', StatusbarAlignment.RIGHT); + } else { + this.profilingStatusBarIndicator.update(indicator); + } + } else { + if (this.profilingStatusBarIndicator) { + this.profilingStatusBarIndicator.dispose(); + this.profilingStatusBarIndicator = undefined; + } + } + } + + public async startProfiling(): Promise<any> { + if (this._state !== ProfileSessionState.None) { + return null; + } + + const inspectPort = await this._extensionService.getInspectPort(true); + if (!inspectPort) { + return this._dialogService.confirm({ + type: 'info', + message: nls.localize('restart1', "Profile Extensions"), + detail: nls.localize('restart2', "In order to profile extensions a restart is required. Do you want to restart '{0}' now?", this._productService.nameLong), + primaryButton: nls.localize('restart3', "&&Restart"), + secondaryButton: nls.localize('cancel', "&&Cancel") + }).then(res => { + if (res.confirmed) { + this._nativeHostService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] }); + } + }); + } + + this._setState(ProfileSessionState.Starting); + + return this._instantiationService.createInstance(ExtensionHostProfiler, inspectPort).start().then((value) => { + this._profileSession = value; + this._setState(ProfileSessionState.Running); + }, (err) => { + onUnexpectedError(err); + this._setState(ProfileSessionState.None); + }); + } + + public stopProfiling(): void { + if (this._state !== ProfileSessionState.Running || !this._profileSession) { + return; + } + + this._setState(ProfileSessionState.Stopping); + this._profileSession.stop().then((result) => { + this._setLastProfile(result); + this._setState(ProfileSessionState.None); + }, (err) => { + onUnexpectedError(err); + this._setState(ProfileSessionState.None); + }); + this._profileSession = null; + } + + private _setLastProfile(profile: IExtensionHostProfile) { + this._profile = profile; + this._onDidChangeLastProfile.fire(undefined); + } + + getUnresponsiveProfile(extensionId: ExtensionIdentifier): IExtensionHostProfile | undefined { + return this._unresponsiveProfiles.get(ExtensionIdentifier.toKey(extensionId)); + } + + setUnresponsiveProfile(extensionId: ExtensionIdentifier, profile: IExtensionHostProfile): void { + this._unresponsiveProfiles.set(ExtensionIdentifier.toKey(extensionId), profile); + this._setLastProfile(profile); + } + +} diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts index 55cb5746c3c..19a6f4de8e8 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts @@ -12,7 +12,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { RuntimeExtensionsEditor, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, SaveExtensionHostProfileAction } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; +import { RuntimeExtensionsEditor, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, SaveExtensionHostProfileAction, IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; import { DebugExtensionHostAction } from 'vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction'; import { IEditorSerializer, IEditorFactoryRegistry, ActiveEditorContext, EditorExtensions } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -24,6 +24,12 @@ import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services import { ExtensionRecommendationNotificationServiceChannel } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; import { Codicon } from 'vs/base/common/codicons'; import { RemoteExtensionsInitializerContribution } from 'vs/workbench/contrib/extensions/electron-sandbox/remoteExtensionsInit'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService'; +import { ExtensionsAutoProfiler } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler'; + +// Singletons +registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, true); // Running Extensions Editor Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane( @@ -61,8 +67,8 @@ class ExtensionsContributions implements IWorkbenchContribution { const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting); +workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(RemoteExtensionsInitializerContribution, LifecyclePhase.Restored); - // Register Commands CommandsRegistry.registerCommand(DebugExtensionHostAction.ID, (accessor: ServicesAccessor) => { diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts new file mode 100644 index 00000000000..ec49247f46d --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IExtensionService, IResponsiveStateChangeEvent, IExtensionHostProfile, ProfileSession } from 'vs/workbench/services/extensions/common/extensions'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { joinPath } from 'vs/base/common/resources'; +import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { localize } from 'vs/nls'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsSlowActions'; +import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { VSBuffer } from 'vs/base/common/buffer'; + +export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchContribution { + + private readonly _blame = new Set<string>(); + private _session: CancellationTokenSource | undefined; + + constructor( + @IExtensionService private readonly _extensionService: IExtensionService, + @IExtensionHostProfileService private readonly _extensionProfileService: IExtensionHostProfileService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService, + @INotificationService private readonly _notificationService: INotificationService, + @IEditorService private readonly _editorService: IEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @INativeWorkbenchEnvironmentService private readonly _environmentServie: INativeWorkbenchEnvironmentService, + @IFileService private readonly _fileService: IFileService + ) { + super(); + this._register(_extensionService.onDidChangeResponsiveChange(this._onDidChangeResponsiveChange, this)); + } + + private async _onDidChangeResponsiveChange(event: IResponsiveStateChangeEvent): Promise<void> { + + const port = await this._extensionService.getInspectPort(true); + + if (!port) { + return; + } + + if (event.isResponsive && this._session) { + // stop profiling when responsive again + this._session.cancel(); + + } else if (!event.isResponsive && !this._session) { + // start profiling if not yet profiling + const cts = new CancellationTokenSource(); + this._session = cts; + + + let session: ProfileSession; + try { + session = await this._instantiationService.createInstance(ExtensionHostProfiler, port).start(); + + } catch (err) { + this._session = undefined; + // fail silent as this is often + // caused by another party being + // connected already + return; + } + + // wait 5 seconds or until responsive again + await new Promise(resolve => { + cts.token.onCancellationRequested(resolve); + setTimeout(resolve, 5e3); + }); + + try { + // stop profiling and analyse results + this._processCpuProfile(await session.stop()); + } catch (err) { + onUnexpectedError(err); + } finally { + this._session = undefined; + } + } + } + + private async _processCpuProfile(profile: IExtensionHostProfile) { + + interface NamedSlice { + id: string; + total: number; + percentage: number; + } + + let data: NamedSlice[] = []; + for (let i = 0; i < profile.ids.length; i++) { + let id = profile.ids[i]; + let total = profile.deltas[i]; + data.push({ id, total, percentage: 0 }); + } + + // merge data by identifier + let anchor = 0; + data.sort((a, b) => a.id.localeCompare(b.id)); + for (let i = 1; i < data.length; i++) { + if (data[anchor].id === data[i].id) { + data[anchor].total += data[i].total; + } else { + anchor += 1; + data[anchor] = data[i]; + } + } + data = data.slice(0, anchor + 1); + + const duration = profile.endTime - profile.startTime; + const percentage = duration / 100; + let top: NamedSlice | undefined; + for (const slice of data) { + slice.percentage = Math.round(slice.total / percentage); + if (!top || top.percentage < slice.percentage) { + top = slice; + } + } + + if (!top) { + return; + } + + const extension = await this._extensionService.getExtension(top.id); + if (!extension) { + // not an extension => idle, gc, self? + return; + } + + + // print message to log + const path = joinPath(this._environmentServie.tmpDir, `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`); + await this._fileService.writeFile(path, VSBuffer.fromString(JSON.stringify(profile.data))); + this._logService.warn(`UNRESPONSIVE extension host, '${top.id}' took ${top.percentage}% of ${duration / 1e3}ms, saved PROFILE here: '${path}'`, data); + + + /* __GDPR__ + "exthostunresponsive" : { + "id" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "data": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } + */ + this._telemetryService.publicLog('exthostunresponsive', { + duration, + data, + }); + + // add to running extensions view + this._extensionProfileService.setUnresponsiveProfile(extension.identifier, profile); + + // prompt: when really slow/greedy + if (!(top.percentage >= 99 && top.total >= 5e6)) { + return; + } + + const action = await this._instantiationService.invokeFunction(createSlowExtensionAction, extension, profile); + + if (!action) { + // cannot report issues against this extension... + return; + } + + // only blame once per extension, don't blame too often + if (this._blame.has(ExtensionIdentifier.toKey(extension.identifier)) || this._blame.size >= 3) { + return; + } + this._blame.add(ExtensionIdentifier.toKey(extension.identifier)); + + // user-facing message when very bad... + this._notificationService.prompt( + Severity.Warning, + localize( + 'unresponsive-exthost', + "The extension '{0}' took a very long time to complete its last operation and it has prevented other extensions from running.", + extension.displayName || extension.name + ), + [{ + label: localize('show', 'Show Extensions'), + run: () => this._editorService.openEditor(RuntimeExtensionsInput.instance, { pinned: true }) + }, + action + ], + { silent: true } + ); + } +} |