Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { $, Dimension, append, clearNode } from '../../../../base/browser/dom.js';
7
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
8
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
9
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
10
import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
11
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
12
import { Action, IAction, Separator } from '../../../../base/common/actions.js';
13
import { isNonEmptyArray } from '../../../../base/common/arrays.js';
14
import { RunOnceScheduler } from '../../../../base/common/async.js';
15
import { fromNow } from '../../../../base/common/date.js';
16
import { IDisposable, dispose } from '../../../../base/common/lifecycle.js';
17
import { Schemas } from '../../../../base/common/network.js';
18
import * as nls from '../../../../nls.js';
19
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
20
import { getContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
21
import { Action2, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
22
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
23
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
24
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
25
import { ExtensionIdentifier, ExtensionIdentifierMap, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';
26
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
27
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
28
import { ILabelService } from '../../../../platform/label/common/label.js';
29
import { WorkbenchList } from '../../../../platform/list/browser/listService.js';
30
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
31
import { Registry } from '../../../../platform/registry/common/platform.js';
32
import { IStorageService } from '../../../../platform/storage/common/storage.js';
33
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
34
import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js';
35
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
36
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
37
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
38
import { IEditorService } from '../../../services/editor/common/editorService.js';
39
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
40
import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js';
41
import { EnablementState } from '../../../services/extensionManagement/common/extensionManagement.js';
42
import { LocalWebWorkerRunningLocation } from '../../../services/extensions/common/extensionRunningLocation.js';
43
import { IExtensionHostProfile, IExtensionService, IExtensionsStatus } from '../../../services/extensions/common/extensions.js';
44
import { IExtension, IExtensionsWorkbenchService } from '../common/extensions.js';
45
import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js';
46
import { errorIcon, warningIcon } from './extensionsIcons.js';
47
import { ExtensionIconWidget } from './extensionsWidgets.js';
48
import './media/runtimeExtensionsEditor.css';
49
50
interface IExtensionProfileInformation {
51
/**
52
* segment when the extension was running.
53
* 2*i = segment start time
54
* 2*i+1 = segment end time
55
*/
56
segments: number[];
57
/**
58
* total time when the extension was running.
59
* (sum of all segment lengths).
60
*/
61
totalTime: number;
62
}
63
64
export interface IRuntimeExtension {
65
originalIndex: number;
66
description: IExtensionDescription;
67
marketplaceInfo: IExtension | undefined;
68
status: IExtensionsStatus;
69
profileInfo?: IExtensionProfileInformation;
70
unresponsiveProfile?: IExtensionHostProfile;
71
}
72
73
export abstract class AbstractRuntimeExtensionsEditor extends EditorPane {
74
75
public static readonly ID: string = 'workbench.editor.runtimeExtensions';
76
77
private _list: WorkbenchList<IRuntimeExtension> | null;
78
private _elements: IRuntimeExtension[] | null;
79
private _updateSoon: RunOnceScheduler;
80
81
constructor(
82
group: IEditorGroup,
83
@ITelemetryService telemetryService: ITelemetryService,
84
@IThemeService themeService: IThemeService,
85
@IContextKeyService private readonly contextKeyService: IContextKeyService,
86
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
87
@IExtensionService private readonly _extensionService: IExtensionService,
88
@INotificationService private readonly _notificationService: INotificationService,
89
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
90
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
91
@IStorageService storageService: IStorageService,
92
@ILabelService private readonly _labelService: ILabelService,
93
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
94
@IClipboardService private readonly _clipboardService: IClipboardService,
95
@IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService,
96
@IHoverService private readonly _hoverService: IHoverService,
97
@IMenuService private readonly _menuService: IMenuService,
98
) {
99
super(AbstractRuntimeExtensionsEditor.ID, group, telemetryService, themeService, storageService);
100
101
this._list = null;
102
this._elements = null;
103
this._updateSoon = this._register(new RunOnceScheduler(() => this._updateExtensions(), 200));
104
105
this._register(this._extensionService.onDidChangeExtensionsStatus(() => this._updateSoon.schedule()));
106
this._register(this._extensionFeaturesManagementService.onDidChangeAccessData(() => this._updateSoon.schedule()));
107
this._updateExtensions();
108
}
109
110
protected async _updateExtensions(): Promise<void> {
111
this._elements = await this._resolveExtensions();
112
this._list?.splice(0, this._list.length, this._elements);
113
}
114
115
private async _resolveExtensions(): Promise<IRuntimeExtension[]> {
116
// We only deal with extensions with source code!
117
await this._extensionService.whenInstalledExtensionsRegistered();
118
const extensionsDescriptions = this._extensionService.extensions.filter((extension) => {
119
return Boolean(extension.main) || Boolean(extension.browser);
120
});
121
const marketplaceMap = new ExtensionIdentifierMap<IExtension>();
122
const marketPlaceExtensions = await this._extensionsWorkbenchService.queryLocal();
123
for (const extension of marketPlaceExtensions) {
124
marketplaceMap.set(extension.identifier.id, extension);
125
}
126
127
const statusMap = this._extensionService.getExtensionsStatus();
128
129
// group profile segments by extension
130
const segments = new ExtensionIdentifierMap<number[]>();
131
132
const profileInfo = this._getProfileInfo();
133
if (profileInfo) {
134
let currentStartTime = profileInfo.startTime;
135
for (let i = 0, len = profileInfo.deltas.length; i < len; i++) {
136
const id = profileInfo.ids[i];
137
const delta = profileInfo.deltas[i];
138
139
let extensionSegments = segments.get(id);
140
if (!extensionSegments) {
141
extensionSegments = [];
142
segments.set(id, extensionSegments);
143
}
144
145
extensionSegments.push(currentStartTime);
146
currentStartTime = currentStartTime + delta;
147
extensionSegments.push(currentStartTime);
148
}
149
}
150
151
let result: IRuntimeExtension[] = [];
152
for (let i = 0, len = extensionsDescriptions.length; i < len; i++) {
153
const extensionDescription = extensionsDescriptions[i];
154
155
let extProfileInfo: IExtensionProfileInformation | null = null;
156
if (profileInfo) {
157
const extensionSegments = segments.get(extensionDescription.identifier) || [];
158
let extensionTotalTime = 0;
159
for (let j = 0, lenJ = extensionSegments.length / 2; j < lenJ; j++) {
160
const startTime = extensionSegments[2 * j];
161
const endTime = extensionSegments[2 * j + 1];
162
extensionTotalTime += (endTime - startTime);
163
}
164
extProfileInfo = {
165
segments: extensionSegments,
166
totalTime: extensionTotalTime
167
};
168
}
169
170
result[i] = {
171
originalIndex: i,
172
description: extensionDescription,
173
marketplaceInfo: marketplaceMap.get(extensionDescription.identifier),
174
status: statusMap[extensionDescription.identifier.value],
175
profileInfo: extProfileInfo || undefined,
176
unresponsiveProfile: this._getUnresponsiveProfile(extensionDescription.identifier)
177
};
178
}
179
180
result = result.filter(element => element.status.activationStarted);
181
182
// bubble up extensions that have caused slowness
183
184
const isUnresponsive = (extension: IRuntimeExtension): boolean =>
185
extension.unresponsiveProfile === profileInfo;
186
187
const profileTime = (extension: IRuntimeExtension): number =>
188
extension.profileInfo?.totalTime ?? 0;
189
190
const activationTime = (extension: IRuntimeExtension): number =>
191
(extension.status.activationTimes?.codeLoadingTime ?? 0) +
192
(extension.status.activationTimes?.activateCallTime ?? 0);
193
194
result = result.sort((a, b) => {
195
if (isUnresponsive(a) || isUnresponsive(b)) {
196
return +isUnresponsive(b) - +isUnresponsive(a);
197
} else if (profileTime(a) || profileTime(b)) {
198
return profileTime(b) - profileTime(a);
199
} else if (activationTime(a) || activationTime(b)) {
200
return activationTime(b) - activationTime(a);
201
}
202
return a.originalIndex - b.originalIndex;
203
});
204
205
return result;
206
}
207
208
protected createEditor(parent: HTMLElement): void {
209
parent.classList.add('runtime-extensions-editor');
210
211
const TEMPLATE_ID = 'runtimeExtensionElementTemplate';
212
213
const delegate = new class implements IListVirtualDelegate<IRuntimeExtension> {
214
getHeight(element: IRuntimeExtension): number {
215
return 70;
216
}
217
getTemplateId(element: IRuntimeExtension): string {
218
return TEMPLATE_ID;
219
}
220
};
221
222
interface IRuntimeExtensionTemplateData {
223
root: HTMLElement;
224
element: HTMLElement;
225
name: HTMLElement;
226
version: HTMLElement;
227
msgContainer: HTMLElement;
228
actionbar: ActionBar;
229
activationTime: HTMLElement;
230
profileTime: HTMLElement;
231
disposables: IDisposable[];
232
elementDisposables: IDisposable[];
233
extension: IExtension | undefined;
234
}
235
236
const renderer: IListRenderer<IRuntimeExtension, IRuntimeExtensionTemplateData> = {
237
templateId: TEMPLATE_ID,
238
renderTemplate: (root: HTMLElement): IRuntimeExtensionTemplateData => {
239
const element = append(root, $('.extension'));
240
const iconContainer = append(element, $('.icon-container'));
241
const extensionIconWidget = this._instantiationService.createInstance(ExtensionIconWidget, iconContainer);
242
243
const desc = append(element, $('div.desc'));
244
const headerContainer = append(desc, $('.header-container'));
245
const header = append(headerContainer, $('.header'));
246
const name = append(header, $('div.name'));
247
const version = append(header, $('span.version'));
248
249
const msgContainer = append(desc, $('div.msg'));
250
251
const actionbar = new ActionBar(desc);
252
const listener = actionbar.onDidRun(({ error }) => error && this._notificationService.error(error));
253
254
const timeContainer = append(element, $('.time'));
255
const activationTime = append(timeContainer, $('div.activation-time'));
256
const profileTime = append(timeContainer, $('div.profile-time'));
257
258
const disposables = [extensionIconWidget, actionbar, listener];
259
260
return {
261
root,
262
element,
263
name,
264
version,
265
actionbar,
266
activationTime,
267
profileTime,
268
msgContainer,
269
set extension(extension: IExtension | undefined) {
270
extensionIconWidget.extension = extension || null;
271
},
272
disposables,
273
elementDisposables: [],
274
};
275
},
276
277
renderElement: (element: IRuntimeExtension, index: number, data: IRuntimeExtensionTemplateData): void => {
278
279
data.elementDisposables = dispose(data.elementDisposables);
280
data.extension = element.marketplaceInfo;
281
282
data.root.classList.toggle('odd', index % 2 === 1);
283
284
data.name.textContent = (element.marketplaceInfo?.displayName || element.description.identifier.value).substr(0, 50);
285
data.version.textContent = element.description.version;
286
287
const activationTimes = element.status.activationTimes;
288
if (activationTimes) {
289
const syncTime = activationTimes.codeLoadingTime + activationTimes.activateCallTime;
290
data.activationTime.textContent = activationTimes.activationReason.startup ? `Startup Activation: ${syncTime}ms` : `Activation: ${syncTime}ms`;
291
} else {
292
data.activationTime.textContent = `Activating...`;
293
}
294
295
data.actionbar.clear();
296
const slowExtensionAction = this._createSlowExtensionAction(element);
297
if (slowExtensionAction) {
298
data.actionbar.push(slowExtensionAction, { icon: false, label: true });
299
}
300
if (isNonEmptyArray(element.status.runtimeErrors)) {
301
const reportExtensionIssueAction = this._createReportExtensionIssueAction(element);
302
if (reportExtensionIssueAction) {
303
data.actionbar.push(reportExtensionIssueAction, { icon: false, label: true });
304
}
305
}
306
307
let title: string;
308
if (activationTimes) {
309
const activationId = activationTimes.activationReason.extensionId.value;
310
const activationEvent = activationTimes.activationReason.activationEvent;
311
if (activationEvent === '*') {
312
title = nls.localize({
313
key: 'starActivation',
314
comment: [
315
'{0} will be an extension identifier'
316
]
317
}, "Activated by {0} on start-up", activationId);
318
} else if (/^workspaceContains:/.test(activationEvent)) {
319
const fileNameOrGlob = activationEvent.substr('workspaceContains:'.length);
320
if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) {
321
title = nls.localize({
322
key: 'workspaceContainsGlobActivation',
323
comment: [
324
'{0} will be a glob pattern',
325
'{1} will be an extension identifier'
326
]
327
}, "Activated by {1} because a file matching {0} exists in your workspace", fileNameOrGlob, activationId);
328
} else {
329
title = nls.localize({
330
key: 'workspaceContainsFileActivation',
331
comment: [
332
'{0} will be a file name',
333
'{1} will be an extension identifier'
334
]
335
}, "Activated by {1} because file {0} exists in your workspace", fileNameOrGlob, activationId);
336
}
337
} else if (/^workspaceContainsTimeout:/.test(activationEvent)) {
338
const glob = activationEvent.substr('workspaceContainsTimeout:'.length);
339
title = nls.localize({
340
key: 'workspaceContainsTimeout',
341
comment: [
342
'{0} will be a glob pattern',
343
'{1} will be an extension identifier'
344
]
345
}, "Activated by {1} because searching for {0} took too long", glob, activationId);
346
} else if (activationEvent === 'onStartupFinished') {
347
title = nls.localize({
348
key: 'startupFinishedActivation',
349
comment: [
350
'This refers to an extension. {0} will be an activation event.'
351
]
352
}, "Activated by {0} after start-up finished", activationId);
353
} else if (/^onLanguage:/.test(activationEvent)) {
354
const language = activationEvent.substr('onLanguage:'.length);
355
title = nls.localize('languageActivation', "Activated by {1} because you opened a {0} file", language, activationId);
356
} else {
357
title = nls.localize({
358
key: 'workspaceGenericActivation',
359
comment: [
360
'{0} will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.',
361
'{1} will be an extension identifier'
362
]
363
}, "Activated by {1} on {0}", activationEvent, activationId);
364
}
365
} else {
366
title = nls.localize('extensionActivating', "Extension is activating...");
367
}
368
data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.activationTime, title));
369
370
clearNode(data.msgContainer);
371
372
if (this._getUnresponsiveProfile(element.description.identifier)) {
373
const el = $('span', undefined, ...renderLabelWithIcons(` $(alert) Unresponsive`));
374
const extensionHostFreezTitle = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze.");
375
data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), el, extensionHostFreezTitle));
376
377
data.msgContainer.appendChild(el);
378
}
379
380
if (isNonEmptyArray(element.status.runtimeErrors)) {
381
const el = $('span', undefined, ...renderLabelWithIcons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`));
382
data.msgContainer.appendChild(el);
383
}
384
385
if (element.status.messages && element.status.messages.length > 0) {
386
const el = $('span', undefined, ...renderLabelWithIcons(`$(alert) ${element.status.messages[0].message}`));
387
data.msgContainer.appendChild(el);
388
}
389
390
let extraLabel: string | null = null;
391
if (element.status.runningLocation && element.status.runningLocation.equals(new LocalWebWorkerRunningLocation(0))) {
392
extraLabel = `$(globe) web worker`;
393
} else if (element.description.extensionLocation.scheme === Schemas.vscodeRemote) {
394
const hostLabel = this._labelService.getHostLabel(Schemas.vscodeRemote, this._environmentService.remoteAuthority);
395
if (hostLabel) {
396
extraLabel = `$(remote) ${hostLabel}`;
397
} else {
398
extraLabel = `$(remote) ${element.description.extensionLocation.authority}`;
399
}
400
} else if (element.status.runningLocation && element.status.runningLocation.affinity > 0) {
401
extraLabel = element.status.runningLocation instanceof LocalWebWorkerRunningLocation
402
? `$(globe) web worker ${element.status.runningLocation.affinity + 1}`
403
: `$(server-process) local process ${element.status.runningLocation.affinity + 1}`;
404
}
405
406
if (extraLabel) {
407
const el = $('span', undefined, ...renderLabelWithIcons(extraLabel));
408
data.msgContainer.appendChild(el);
409
}
410
411
const features = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeatures();
412
for (const feature of features) {
413
const accessData = this._extensionFeaturesManagementService.getAccessData(element.description.identifier, feature.id);
414
if (accessData) {
415
const status = accessData?.current?.status;
416
if (status) {
417
data.msgContainer.appendChild($('span', undefined, `${feature.label}: `));
418
data.msgContainer.appendChild($('span', undefined, ...renderLabelWithIcons(`$(${status.severity === Severity.Error ? errorIcon.id : warningIcon.id}) ${status.message}`)));
419
}
420
if (accessData?.accessTimes.length > 0) {
421
const 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) : ''}`);
422
if (accessData.current) {
423
const title = nls.localize('requests count title', "Last request was {0}.", fromNow(accessData.current.lastAccessed, true, true));
424
data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, title));
425
}
426
427
data.msgContainer.appendChild(element);
428
}
429
}
430
}
431
432
if (element.profileInfo) {
433
data.profileTime.textContent = `Profile: ${(element.profileInfo.totalTime / 1000).toFixed(2)}ms`;
434
} else {
435
data.profileTime.textContent = '';
436
}
437
438
},
439
440
disposeTemplate: (data: IRuntimeExtensionTemplateData): void => {
441
data.disposables = dispose(data.disposables);
442
}
443
};
444
445
this._list = this._instantiationService.createInstance(WorkbenchList<IRuntimeExtension>,
446
'RuntimeExtensions',
447
parent, delegate, [renderer], {
448
multipleSelectionSupport: false,
449
setRowLineHeight: false,
450
horizontalScrolling: false,
451
overrideStyles: {
452
listBackground: editorBackground
453
},
454
accessibilityProvider: new class implements IListAccessibilityProvider<IRuntimeExtension> {
455
getWidgetAriaLabel(): string {
456
return nls.localize('runtimeExtensions', "Runtime Extensions");
457
}
458
getAriaLabel(element: IRuntimeExtension): string | null {
459
return element.description.name;
460
}
461
}
462
});
463
464
this._list.splice(0, this._list.length, this._elements || undefined);
465
466
this._register(this._list.onContextMenu((e) => {
467
if (!e.element) {
468
return;
469
}
470
471
const actions: IAction[] = [];
472
473
actions.push(new Action(
474
'runtimeExtensionsEditor.action.copyId',
475
nls.localize('copy id', "Copy id ({0})", e.element.description.identifier.value),
476
undefined,
477
true,
478
() => {
479
this._clipboardService.writeText(e.element!.description.identifier.value);
480
}
481
));
482
483
const reportExtensionIssueAction = this._createReportExtensionIssueAction(e.element);
484
if (reportExtensionIssueAction) {
485
actions.push(reportExtensionIssueAction);
486
}
487
actions.push(new Separator());
488
489
if (e.element.marketplaceInfo) {
490
actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo!, EnablementState.DisabledWorkspace)));
491
actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo!, EnablementState.DisabledGlobally)));
492
}
493
actions.push(new Separator());
494
495
const menuActions = this._menuService.getMenuActions(MenuId.ExtensionEditorContextMenu, this.contextKeyService);
496
actions.push(...getContextMenuActions(menuActions,).secondary);
497
498
this._contextMenuService.showContextMenu({
499
getAnchor: () => e.anchor,
500
getActions: () => actions
501
});
502
}));
503
}
504
505
public layout(dimension: Dimension): void {
506
this._list?.layout(dimension.height);
507
}
508
509
protected abstract _getProfileInfo(): IExtensionHostProfile | null;
510
protected abstract _getUnresponsiveProfile(extensionId: ExtensionIdentifier): IExtensionHostProfile | undefined;
511
protected abstract _createSlowExtensionAction(element: IRuntimeExtension): Action | null;
512
protected abstract _createReportExtensionIssueAction(element: IRuntimeExtension): Action | null;
513
}
514
515
export class ShowRuntimeExtensionsAction extends Action2 {
516
517
constructor() {
518
super({
519
id: 'workbench.action.showRuntimeExtensions',
520
title: nls.localize2('showRuntimeExtensions', "Show Running Extensions"),
521
category: Categories.Developer,
522
f1: true,
523
menu: {
524
id: MenuId.ViewContainerTitle,
525
when: ContextKeyExpr.equals('viewContainer', 'workbench.view.extensions'),
526
group: '2_enablement',
527
order: 3
528
}
529
});
530
}
531
532
async run(accessor: ServicesAccessor): Promise<void> {
533
await accessor.get(IEditorService).openEditor(RuntimeExtensionsInput.instance, { pinned: true });
534
}
535
}
536
537