Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.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 { timeout } from '../../../../base/common/async.js';
7
import { VSBuffer } from '../../../../base/common/buffer.js';
8
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
9
import { onUnexpectedError } from '../../../../base/common/errors.js';
10
import { IDisposable } from '../../../../base/common/lifecycle.js';
11
import { Schemas } from '../../../../base/common/network.js';
12
import { joinPath } from '../../../../base/common/resources.js';
13
import { TernarySearchTree } from '../../../../base/common/ternarySearchTree.js';
14
import { URI } from '../../../../base/common/uri.js';
15
import { generateUuid } from '../../../../base/common/uuid.js';
16
import { localize } from '../../../../nls.js';
17
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
18
import { ExtensionIdentifier, ExtensionIdentifierSet, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';
19
import { IFileService } from '../../../../platform/files/common/files.js';
20
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
21
import { ILogService } from '../../../../platform/log/common/log.js';
22
import { INotificationService, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';
23
import { IProfileAnalysisWorkerService } from '../../../../platform/profiling/electron-browser/profileAnalysisWorkerService.js';
24
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
25
import { IWorkbenchContribution } from '../../../common/contributions.js';
26
import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js';
27
import { createSlowExtensionAction } from './extensionsSlowActions.js';
28
import { IExtensionHostProfileService } from './runtimeExtensionsEditor.js';
29
import { IEditorService } from '../../../services/editor/common/editorService.js';
30
import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js';
31
import { ExtensionHostKind } from '../../../services/extensions/common/extensionHostKind.js';
32
import { IExtensionHostProfile, IExtensionService, IResponsiveStateChangeEvent, ProfileSession } from '../../../services/extensions/common/extensions.js';
33
import { ExtensionHostProfiler } from '../../../services/extensions/electron-browser/extensionHostProfiler.js';
34
import { ITimerService } from '../../../services/timer/browser/timerService.js';
35
36
export class ExtensionsAutoProfiler implements IWorkbenchContribution {
37
38
private readonly _blame = new ExtensionIdentifierSet();
39
40
private _session: CancellationTokenSource | undefined;
41
private _unresponsiveListener: IDisposable | undefined;
42
private _perfBaseline: number = -1;
43
44
constructor(
45
@IExtensionService private readonly _extensionService: IExtensionService,
46
@IExtensionHostProfileService private readonly _extensionProfileService: IExtensionHostProfileService,
47
@ITelemetryService private readonly _telemetryService: ITelemetryService,
48
@ILogService private readonly _logService: ILogService,
49
@INotificationService private readonly _notificationService: INotificationService,
50
@IEditorService private readonly _editorService: IEditorService,
51
@IInstantiationService private readonly _instantiationService: IInstantiationService,
52
@INativeWorkbenchEnvironmentService private readonly _environmentServie: INativeWorkbenchEnvironmentService,
53
@IProfileAnalysisWorkerService private readonly _profileAnalysisService: IProfileAnalysisWorkerService,
54
@IConfigurationService private readonly _configService: IConfigurationService,
55
@IFileService private readonly _fileService: IFileService,
56
@ITimerService timerService: ITimerService
57
) {
58
59
timerService.perfBaseline.then(value => {
60
if (value < 0) {
61
return; // too slow for profiling
62
}
63
this._perfBaseline = value;
64
this._unresponsiveListener = _extensionService.onDidChangeResponsiveChange(this._onDidChangeResponsiveChange, this);
65
});
66
}
67
68
dispose(): void {
69
this._unresponsiveListener?.dispose();
70
this._session?.dispose(true);
71
}
72
73
private async _onDidChangeResponsiveChange(event: IResponsiveStateChangeEvent): Promise<void> {
74
if (event.extensionHostKind !== ExtensionHostKind.LocalProcess) {
75
return;
76
}
77
78
const listener = await event.getInspectListener(true);
79
80
if (!listener) {
81
return;
82
}
83
84
if (event.isResponsive && this._session) {
85
// stop profiling when responsive again
86
this._session.cancel();
87
this._logService.info('UNRESPONSIVE extension host: received responsive event and cancelling profiling session');
88
89
90
} else if (!event.isResponsive && !this._session) {
91
// start profiling if not yet profiling
92
const cts = new CancellationTokenSource();
93
this._session = cts;
94
95
96
let session: ProfileSession;
97
try {
98
session = await this._instantiationService.createInstance(ExtensionHostProfiler, listener.host, listener.port).start();
99
100
} catch (err) {
101
this._session = undefined;
102
// fail silent as this is often
103
// caused by another party being
104
// connected already
105
return;
106
}
107
this._logService.info('UNRESPONSIVE extension host: starting to profile NOW');
108
109
// wait 5 seconds or until responsive again
110
try {
111
await timeout(5e3, cts.token);
112
} catch {
113
// can throw cancellation error. that is
114
// OK, we stop profiling and analyse the
115
// profile anyways
116
}
117
118
try {
119
// stop profiling and analyse results
120
this._processCpuProfile(await session.stop());
121
} catch (err) {
122
onUnexpectedError(err);
123
} finally {
124
this._session = undefined;
125
}
126
}
127
}
128
129
private async _processCpuProfile(profile: IExtensionHostProfile) {
130
131
// get all extensions
132
await this._extensionService.whenInstalledExtensionsRegistered();
133
134
// send heavy samples iff enabled
135
if (this._configService.getValue('application.experimental.rendererProfiling')) {
136
137
const searchTree = TernarySearchTree.forUris<IExtensionDescription>();
138
searchTree.fill(this._extensionService.extensions.map(e => [e.extensionLocation, e]));
139
140
await this._profileAnalysisService.analyseBottomUp(
141
profile.data,
142
url => searchTree.findSubstr(URI.parse(url))?.identifier.value ?? '<<not-found>>',
143
this._perfBaseline,
144
false
145
);
146
}
147
148
// analyse profile by extension-category
149
const categories: [location: URI, id: string][] = this._extensionService.extensions
150
.filter(e => e.extensionLocation.scheme === Schemas.file)
151
.map(e => [e.extensionLocation, ExtensionIdentifier.toKey(e.identifier)]);
152
153
const data = await this._profileAnalysisService.analyseByLocation(profile.data, categories);
154
155
//
156
let overall: number = 0;
157
let top: string = '';
158
let topAggregated: number = -1;
159
for (const [category, aggregated] of data) {
160
overall += aggregated;
161
if (aggregated > topAggregated) {
162
topAggregated = aggregated;
163
top = category;
164
}
165
}
166
const topPercentage = topAggregated / (overall / 100);
167
168
// associate extensions to profile node
169
const extension = await this._extensionService.getExtension(top);
170
if (!extension) {
171
// not an extension => idle, gc, self?
172
return;
173
}
174
175
176
const profilingSessionId = generateUuid();
177
178
// print message to log
179
const path = joinPath(this._environmentServie.tmpDir, `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`);
180
await this._fileService.writeFile(path, VSBuffer.fromString(JSON.stringify(profile.data)));
181
this._logService.warn(`UNRESPONSIVE extension host: '${top}' took ${topPercentage}% of ${topAggregated / 1e3}ms, saved PROFILE here: '${path}'`);
182
183
type UnresponsiveData = {
184
duration: number;
185
profilingSessionId: string;
186
data: string[];
187
id: string;
188
};
189
type UnresponsiveDataClassification = {
190
owner: 'jrieken';
191
comment: 'Profiling data that was collected while the extension host was unresponsive';
192
profilingSessionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Identifier of a profiling session' };
193
duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Duration for which the extension host was unresponsive' };
194
data: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Extensions ids and core parts that were active while the extension host was frozen' };
195
id: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Top extensions id that took most of the duration' };
196
};
197
this._telemetryService.publicLog2<UnresponsiveData, UnresponsiveDataClassification>('exthostunresponsive', {
198
profilingSessionId,
199
duration: overall,
200
data: data.map(tuple => tuple[0]).flat(),
201
id: ExtensionIdentifier.toKey(extension.identifier),
202
});
203
204
205
// add to running extensions view
206
this._extensionProfileService.setUnresponsiveProfile(extension.identifier, profile);
207
208
// prompt: when really slow/greedy
209
if (!(topPercentage >= 95 && topAggregated >= 5e6)) {
210
return;
211
}
212
213
const action = await this._instantiationService.invokeFunction(createSlowExtensionAction, extension, profile);
214
215
if (!action) {
216
// cannot report issues against this extension...
217
return;
218
}
219
220
// only blame once per extension, don't blame too often
221
if (this._blame.has(extension.identifier) || this._blame.size >= 3) {
222
return;
223
}
224
this._blame.add(extension.identifier);
225
226
// user-facing message when very bad...
227
this._notificationService.prompt(
228
Severity.Warning,
229
localize(
230
'unresponsive-exthost',
231
"The extension '{0}' took a very long time to complete its last operation and it has prevented other extensions from running.",
232
extension.displayName || extension.name
233
),
234
[{
235
label: localize('show', 'Show Extensions'),
236
run: () => this._editorService.openEditor(RuntimeExtensionsInput.instance, { pinned: true })
237
},
238
action
239
],
240
{ priority: NotificationPriority.SILENT }
241
);
242
}
243
}
244
245