Path: blob/main/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.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 { $, Dimension, append, clearNode } from '../../../../base/browser/dom.js';6import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';7import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';8import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';9import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';10import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';11import { Action, IAction, Separator } from '../../../../base/common/actions.js';12import { isNonEmptyArray } from '../../../../base/common/arrays.js';13import { RunOnceScheduler } from '../../../../base/common/async.js';14import { fromNow } from '../../../../base/common/date.js';15import { IDisposable, dispose } from '../../../../base/common/lifecycle.js';16import { Schemas } from '../../../../base/common/network.js';17import * as nls from '../../../../nls.js';18import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';19import { getContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';20import { Action2, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';21import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';22import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';23import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';24import { ExtensionIdentifier, ExtensionIdentifierMap, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';25import { IHoverService } from '../../../../platform/hover/browser/hover.js';26import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';27import { ILabelService } from '../../../../platform/label/common/label.js';28import { WorkbenchList } from '../../../../platform/list/browser/listService.js';29import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';30import { Registry } from '../../../../platform/registry/common/platform.js';31import { IStorageService } from '../../../../platform/storage/common/storage.js';32import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';33import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js';34import { IThemeService } from '../../../../platform/theme/common/themeService.js';35import { EditorPane } from '../../../browser/parts/editor/editorPane.js';36import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';37import { IEditorService } from '../../../services/editor/common/editorService.js';38import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';39import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js';40import { EnablementState } from '../../../services/extensionManagement/common/extensionManagement.js';41import { LocalWebWorkerRunningLocation } from '../../../services/extensions/common/extensionRunningLocation.js';42import { IExtensionHostProfile, IExtensionService, IExtensionsStatus } from '../../../services/extensions/common/extensions.js';43import { IExtension, IExtensionsWorkbenchService } from '../common/extensions.js';44import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js';45import { errorIcon, warningIcon } from './extensionsIcons.js';46import { ExtensionIconWidget } from './extensionsWidgets.js';47import './media/runtimeExtensionsEditor.css';4849interface IExtensionProfileInformation {50/**51* segment when the extension was running.52* 2*i = segment start time53* 2*i+1 = segment end time54*/55segments: number[];56/**57* total time when the extension was running.58* (sum of all segment lengths).59*/60totalTime: number;61}6263export interface IRuntimeExtension {64originalIndex: number;65description: IExtensionDescription;66marketplaceInfo: IExtension | undefined;67status: IExtensionsStatus;68profileInfo?: IExtensionProfileInformation;69unresponsiveProfile?: IExtensionHostProfile;70}7172export abstract class AbstractRuntimeExtensionsEditor extends EditorPane {7374public static readonly ID: string = 'workbench.editor.runtimeExtensions';7576private _list: WorkbenchList<IRuntimeExtension> | null;77private _elements: IRuntimeExtension[] | null;78private _updateSoon: RunOnceScheduler;7980constructor(81group: IEditorGroup,82@ITelemetryService telemetryService: ITelemetryService,83@IThemeService themeService: IThemeService,84@IContextKeyService private readonly contextKeyService: IContextKeyService,85@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,86@IExtensionService private readonly _extensionService: IExtensionService,87@INotificationService private readonly _notificationService: INotificationService,88@IContextMenuService private readonly _contextMenuService: IContextMenuService,89@IInstantiationService protected readonly _instantiationService: IInstantiationService,90@IStorageService storageService: IStorageService,91@ILabelService private readonly _labelService: ILabelService,92@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,93@IClipboardService private readonly _clipboardService: IClipboardService,94@IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService,95@IHoverService private readonly _hoverService: IHoverService,96@IMenuService private readonly _menuService: IMenuService,97) {98super(AbstractRuntimeExtensionsEditor.ID, group, telemetryService, themeService, storageService);99100this._list = null;101this._elements = null;102this._updateSoon = this._register(new RunOnceScheduler(() => this._updateExtensions(), 200));103104this._register(this._extensionService.onDidChangeExtensionsStatus(() => this._updateSoon.schedule()));105this._register(this._extensionFeaturesManagementService.onDidChangeAccessData(() => this._updateSoon.schedule()));106this._updateExtensions();107}108109protected async _updateExtensions(): Promise<void> {110this._elements = await this._resolveExtensions();111this._list?.splice(0, this._list.length, this._elements);112}113114private async _resolveExtensions(): Promise<IRuntimeExtension[]> {115// We only deal with extensions with source code!116await this._extensionService.whenInstalledExtensionsRegistered();117const extensionsDescriptions = this._extensionService.extensions.filter((extension) => {118return Boolean(extension.main) || Boolean(extension.browser);119});120const marketplaceMap = new ExtensionIdentifierMap<IExtension>();121const marketPlaceExtensions = await this._extensionsWorkbenchService.queryLocal();122for (const extension of marketPlaceExtensions) {123marketplaceMap.set(extension.identifier.id, extension);124}125126const statusMap = this._extensionService.getExtensionsStatus();127128// group profile segments by extension129const segments = new ExtensionIdentifierMap<number[]>();130131const profileInfo = this._getProfileInfo();132if (profileInfo) {133let currentStartTime = profileInfo.startTime;134for (let i = 0, len = profileInfo.deltas.length; i < len; i++) {135const id = profileInfo.ids[i];136const delta = profileInfo.deltas[i];137138let extensionSegments = segments.get(id);139if (!extensionSegments) {140extensionSegments = [];141segments.set(id, extensionSegments);142}143144extensionSegments.push(currentStartTime);145currentStartTime = currentStartTime + delta;146extensionSegments.push(currentStartTime);147}148}149150let result: IRuntimeExtension[] = [];151for (let i = 0, len = extensionsDescriptions.length; i < len; i++) {152const extensionDescription = extensionsDescriptions[i];153154let extProfileInfo: IExtensionProfileInformation | null = null;155if (profileInfo) {156const extensionSegments = segments.get(extensionDescription.identifier) || [];157let extensionTotalTime = 0;158for (let j = 0, lenJ = extensionSegments.length / 2; j < lenJ; j++) {159const startTime = extensionSegments[2 * j];160const endTime = extensionSegments[2 * j + 1];161extensionTotalTime += (endTime - startTime);162}163extProfileInfo = {164segments: extensionSegments,165totalTime: extensionTotalTime166};167}168169result[i] = {170originalIndex: i,171description: extensionDescription,172marketplaceInfo: marketplaceMap.get(extensionDescription.identifier),173status: statusMap[extensionDescription.identifier.value],174profileInfo: extProfileInfo || undefined,175unresponsiveProfile: this._getUnresponsiveProfile(extensionDescription.identifier)176};177}178179result = result.filter(element => element.status.activationStarted);180181// bubble up extensions that have caused slowness182183const isUnresponsive = (extension: IRuntimeExtension): boolean =>184extension.unresponsiveProfile === profileInfo;185186const profileTime = (extension: IRuntimeExtension): number =>187extension.profileInfo?.totalTime ?? 0;188189const activationTime = (extension: IRuntimeExtension): number =>190(extension.status.activationTimes?.codeLoadingTime ?? 0) +191(extension.status.activationTimes?.activateCallTime ?? 0);192193result = result.sort((a, b) => {194if (isUnresponsive(a) || isUnresponsive(b)) {195return +isUnresponsive(b) - +isUnresponsive(a);196} else if (profileTime(a) || profileTime(b)) {197return profileTime(b) - profileTime(a);198} else if (activationTime(a) || activationTime(b)) {199return activationTime(b) - activationTime(a);200}201return a.originalIndex - b.originalIndex;202});203204return result;205}206207protected createEditor(parent: HTMLElement): void {208parent.classList.add('runtime-extensions-editor');209210const TEMPLATE_ID = 'runtimeExtensionElementTemplate';211212const delegate = new class implements IListVirtualDelegate<IRuntimeExtension> {213getHeight(element: IRuntimeExtension): number {214return 70;215}216getTemplateId(element: IRuntimeExtension): string {217return TEMPLATE_ID;218}219};220221interface IRuntimeExtensionTemplateData {222root: HTMLElement;223element: HTMLElement;224name: HTMLElement;225version: HTMLElement;226msgContainer: HTMLElement;227actionbar: ActionBar;228activationTime: HTMLElement;229profileTime: HTMLElement;230disposables: IDisposable[];231elementDisposables: IDisposable[];232extension: IExtension | undefined;233}234235const renderer: IListRenderer<IRuntimeExtension, IRuntimeExtensionTemplateData> = {236templateId: TEMPLATE_ID,237renderTemplate: (root: HTMLElement): IRuntimeExtensionTemplateData => {238const element = append(root, $('.extension'));239const iconContainer = append(element, $('.icon-container'));240const extensionIconWidget = this._instantiationService.createInstance(ExtensionIconWidget, iconContainer);241242const desc = append(element, $('div.desc'));243const headerContainer = append(desc, $('.header-container'));244const header = append(headerContainer, $('.header'));245const name = append(header, $('div.name'));246const version = append(header, $('span.version'));247248const msgContainer = append(desc, $('div.msg'));249250const actionbar = new ActionBar(desc);251const listener = actionbar.onDidRun(({ error }) => error && this._notificationService.error(error));252253const timeContainer = append(element, $('.time'));254const activationTime = append(timeContainer, $('div.activation-time'));255const profileTime = append(timeContainer, $('div.profile-time'));256257const disposables = [extensionIconWidget, actionbar, listener];258259return {260root,261element,262name,263version,264actionbar,265activationTime,266profileTime,267msgContainer,268set extension(extension: IExtension | undefined) {269extensionIconWidget.extension = extension || null;270},271disposables,272elementDisposables: [],273};274},275276renderElement: (element: IRuntimeExtension, index: number, data: IRuntimeExtensionTemplateData): void => {277278data.elementDisposables = dispose(data.elementDisposables);279data.extension = element.marketplaceInfo;280281data.root.classList.toggle('odd', index % 2 === 1);282283data.name.textContent = (element.marketplaceInfo?.displayName || element.description.identifier.value).substr(0, 50);284data.version.textContent = element.description.version;285286const activationTimes = element.status.activationTimes;287if (activationTimes) {288const syncTime = activationTimes.codeLoadingTime + activationTimes.activateCallTime;289data.activationTime.textContent = activationTimes.activationReason.startup ? `Startup Activation: ${syncTime}ms` : `Activation: ${syncTime}ms`;290} else {291data.activationTime.textContent = `Activating...`;292}293294data.actionbar.clear();295const slowExtensionAction = this._createSlowExtensionAction(element);296if (slowExtensionAction) {297data.actionbar.push(slowExtensionAction, { icon: false, label: true });298}299if (isNonEmptyArray(element.status.runtimeErrors)) {300const reportExtensionIssueAction = this._createReportExtensionIssueAction(element);301if (reportExtensionIssueAction) {302data.actionbar.push(reportExtensionIssueAction, { icon: false, label: true });303}304}305306let title: string;307if (activationTimes) {308const activationId = activationTimes.activationReason.extensionId.value;309const activationEvent = activationTimes.activationReason.activationEvent;310if (activationEvent === '*') {311title = nls.localize({312key: 'starActivation',313comment: [314'{0} will be an extension identifier'315]316}, "Activated by {0} on start-up", activationId);317} else if (/^workspaceContains:/.test(activationEvent)) {318const fileNameOrGlob = activationEvent.substr('workspaceContains:'.length);319if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) {320title = nls.localize({321key: 'workspaceContainsGlobActivation',322comment: [323'{0} will be a glob pattern',324'{1} will be an extension identifier'325]326}, "Activated by {1} because a file matching {0} exists in your workspace", fileNameOrGlob, activationId);327} else {328title = nls.localize({329key: 'workspaceContainsFileActivation',330comment: [331'{0} will be a file name',332'{1} will be an extension identifier'333]334}, "Activated by {1} because file {0} exists in your workspace", fileNameOrGlob, activationId);335}336} else if (/^workspaceContainsTimeout:/.test(activationEvent)) {337const glob = activationEvent.substr('workspaceContainsTimeout:'.length);338title = nls.localize({339key: 'workspaceContainsTimeout',340comment: [341'{0} will be a glob pattern',342'{1} will be an extension identifier'343]344}, "Activated by {1} because searching for {0} took too long", glob, activationId);345} else if (activationEvent === 'onStartupFinished') {346title = nls.localize({347key: 'startupFinishedActivation',348comment: [349'This refers to an extension. {0} will be an activation event.'350]351}, "Activated by {0} after start-up finished", activationId);352} else if (/^onLanguage:/.test(activationEvent)) {353const language = activationEvent.substr('onLanguage:'.length);354title = nls.localize('languageActivation', "Activated by {1} because you opened a {0} file", language, activationId);355} else {356title = nls.localize({357key: 'workspaceGenericActivation',358comment: [359'{0} will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.',360'{1} will be an extension identifier'361]362}, "Activated by {1} on {0}", activationEvent, activationId);363}364} else {365title = nls.localize('extensionActivating', "Extension is activating...");366}367data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.activationTime, title));368369clearNode(data.msgContainer);370371if (this._getUnresponsiveProfile(element.description.identifier)) {372const el = $('span', undefined, ...renderLabelWithIcons(` $(alert) Unresponsive`));373const extensionHostFreezTitle = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze.");374data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), el, extensionHostFreezTitle));375376data.msgContainer.appendChild(el);377}378379if (isNonEmptyArray(element.status.runtimeErrors)) {380const el = $('span', undefined, ...renderLabelWithIcons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`));381data.msgContainer.appendChild(el);382}383384if (element.status.messages && element.status.messages.length > 0) {385const el = $('span', undefined, ...renderLabelWithIcons(`$(alert) ${element.status.messages[0].message}`));386data.msgContainer.appendChild(el);387}388389let extraLabel: string | null = null;390if (element.status.runningLocation && element.status.runningLocation.equals(new LocalWebWorkerRunningLocation(0))) {391extraLabel = `$(globe) web worker`;392} else if (element.description.extensionLocation.scheme === Schemas.vscodeRemote) {393const hostLabel = this._labelService.getHostLabel(Schemas.vscodeRemote, this._environmentService.remoteAuthority);394if (hostLabel) {395extraLabel = `$(remote) ${hostLabel}`;396} else {397extraLabel = `$(remote) ${element.description.extensionLocation.authority}`;398}399} else if (element.status.runningLocation && element.status.runningLocation.affinity > 0) {400extraLabel = element.status.runningLocation instanceof LocalWebWorkerRunningLocation401? `$(globe) web worker ${element.status.runningLocation.affinity + 1}`402: `$(server-process) local process ${element.status.runningLocation.affinity + 1}`;403}404405if (extraLabel) {406const el = $('span', undefined, ...renderLabelWithIcons(extraLabel));407data.msgContainer.appendChild(el);408}409410const features = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeatures();411for (const feature of features) {412const accessData = this._extensionFeaturesManagementService.getAccessData(element.description.identifier, feature.id);413if (accessData) {414const status = accessData?.current?.status;415if (status) {416data.msgContainer.appendChild($('span', undefined, `${feature.label}: `));417data.msgContainer.appendChild($('span', undefined, ...renderLabelWithIcons(`$(${status.severity === Severity.Error ? errorIcon.id : warningIcon.id}) ${status.message}`)));418}419if (accessData?.accessTimes.length > 0) {420const element = $('span', undefined, `${nls.localize('requests count', "{0} Usage: {1} Requests", feature.label, accessData.accessTimes.length)}${accessData.current ? nls.localize('session requests count', ", {0} Requests (Session)", accessData.current.accessTimes.length) : ''}`);421if (accessData.current) {422const title = nls.localize('requests count title', "Last request was {0}.", fromNow(accessData.current.lastAccessed, true, true));423data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, title));424}425426data.msgContainer.appendChild(element);427}428}429}430431if (element.profileInfo) {432data.profileTime.textContent = `Profile: ${(element.profileInfo.totalTime / 1000).toFixed(2)}ms`;433} else {434data.profileTime.textContent = '';435}436437},438439disposeTemplate: (data: IRuntimeExtensionTemplateData): void => {440data.disposables = dispose(data.disposables);441}442};443444this._list = this._instantiationService.createInstance(WorkbenchList<IRuntimeExtension>,445'RuntimeExtensions',446parent, delegate, [renderer], {447multipleSelectionSupport: false,448setRowLineHeight: false,449horizontalScrolling: false,450overrideStyles: {451listBackground: editorBackground452},453accessibilityProvider: new class implements IListAccessibilityProvider<IRuntimeExtension> {454getWidgetAriaLabel(): string {455return nls.localize('runtimeExtensions', "Runtime Extensions");456}457getAriaLabel(element: IRuntimeExtension): string | null {458return element.description.name;459}460}461});462463this._list.splice(0, this._list.length, this._elements || undefined);464465this._register(this._list.onContextMenu((e) => {466if (!e.element) {467return;468}469470const actions: IAction[] = [];471472actions.push(new Action(473'runtimeExtensionsEditor.action.copyId',474nls.localize('copy id', "Copy id ({0})", e.element.description.identifier.value),475undefined,476true,477() => {478this._clipboardService.writeText(e.element!.description.identifier.value);479}480));481482const reportExtensionIssueAction = this._createReportExtensionIssueAction(e.element);483if (reportExtensionIssueAction) {484actions.push(reportExtensionIssueAction);485}486actions.push(new Separator());487488if (e.element.marketplaceInfo) {489actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo!, EnablementState.DisabledWorkspace)));490actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo!, EnablementState.DisabledGlobally)));491}492actions.push(new Separator());493494const menuActions = this._menuService.getMenuActions(MenuId.ExtensionEditorContextMenu, this.contextKeyService);495actions.push(...getContextMenuActions(menuActions,).secondary);496497this._contextMenuService.showContextMenu({498getAnchor: () => e.anchor,499getActions: () => actions500});501}));502}503504public layout(dimension: Dimension): void {505this._list?.layout(dimension.height);506}507508protected abstract _getProfileInfo(): IExtensionHostProfile | null;509protected abstract _getUnresponsiveProfile(extensionId: ExtensionIdentifier): IExtensionHostProfile | undefined;510protected abstract _createSlowExtensionAction(element: IRuntimeExtension): Action | null;511protected abstract _createReportExtensionIssueAction(element: IRuntimeExtension): Action | null;512}513514export class ShowRuntimeExtensionsAction extends Action2 {515516constructor() {517super({518id: 'workbench.action.showRuntimeExtensions',519title: nls.localize2('showRuntimeExtensions', "Show Running Extensions"),520category: Categories.Developer,521f1: true,522menu: {523id: MenuId.ViewContainerTitle,524when: ContextKeyExpr.equals('viewContainer', 'workbench.view.extensions'),525group: '2_enablement',526order: 3527}528});529}530531async run(accessor: ServicesAccessor): Promise<void> {532await accessor.get(IEditorService).openEditor(RuntimeExtensionsInput.instance, { pinned: true });533}534}535536537