Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/instantiation/common/instantiationService.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 { GlobalIdleValue } from '../../../base/common/async.js';
7
import { Event } from '../../../base/common/event.js';
8
import { illegalState } from '../../../base/common/errors.js';
9
import { DisposableStore, dispose, IDisposable, isDisposable, toDisposable } from '../../../base/common/lifecycle.js';
10
import { SyncDescriptor, SyncDescriptor0 } from './descriptors.js';
11
import { Graph } from './graph.js';
12
import { GetLeadingNonServiceArgs, IInstantiationService, ServiceIdentifier, ServicesAccessor, _util } from './instantiation.js';
13
import { ServiceCollection } from './serviceCollection.js';
14
import { LinkedList } from '../../../base/common/linkedList.js';
15
16
// TRACING
17
const _enableAllTracing = false
18
// || "TRUE" // DO NOT CHECK IN!
19
;
20
21
class CyclicDependencyError extends Error {
22
constructor(graph: Graph<any>) {
23
super('cyclic dependency between services');
24
this.message = graph.findCycleSlow() ?? `UNABLE to detect cycle, dumping graph: \n${graph.toString()}`;
25
}
26
}
27
28
export class InstantiationService implements IInstantiationService {
29
30
declare readonly _serviceBrand: undefined;
31
32
readonly _globalGraph?: Graph<string>;
33
private _globalGraphImplicitDependency?: string;
34
35
private _isDisposed = false;
36
private readonly _servicesToMaybeDispose = new Set<any>();
37
private readonly _children = new Set<InstantiationService>();
38
39
constructor(
40
private readonly _services: ServiceCollection = new ServiceCollection(),
41
private readonly _strict: boolean = false,
42
private readonly _parent?: InstantiationService,
43
private readonly _enableTracing: boolean = _enableAllTracing
44
) {
45
46
this._services.set(IInstantiationService, this);
47
this._globalGraph = _enableTracing ? _parent?._globalGraph ?? new Graph(e => e) : undefined;
48
}
49
50
dispose(): void {
51
if (!this._isDisposed) {
52
this._isDisposed = true;
53
// dispose all child services
54
dispose(this._children);
55
this._children.clear();
56
57
// dispose all services created by this service
58
for (const candidate of this._servicesToMaybeDispose) {
59
if (isDisposable(candidate)) {
60
candidate.dispose();
61
}
62
}
63
this._servicesToMaybeDispose.clear();
64
}
65
}
66
67
private _throwIfDisposed(): void {
68
if (this._isDisposed) {
69
throw new Error('InstantiationService has been disposed');
70
}
71
}
72
73
createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService {
74
this._throwIfDisposed();
75
76
const that = this;
77
const result = new class extends InstantiationService {
78
override dispose(): void {
79
that._children.delete(result);
80
super.dispose();
81
}
82
}(services, this._strict, this, this._enableTracing);
83
this._children.add(result);
84
85
store?.add(result);
86
return result;
87
}
88
89
invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R {
90
this._throwIfDisposed();
91
92
const _trace = Trace.traceInvocation(this._enableTracing, fn);
93
let _done = false;
94
try {
95
const accessor: ServicesAccessor = {
96
get: <T>(id: ServiceIdentifier<T>) => {
97
98
if (_done) {
99
throw illegalState('service accessor is only valid during the invocation of its target method');
100
}
101
102
const result = this._getOrCreateServiceInstance(id, _trace);
103
if (!result) {
104
throw new Error(`[invokeFunction] unknown service '${id}'`);
105
}
106
return result;
107
},
108
getIfExists: <T>(id: ServiceIdentifier<T>) => {
109
if (_done) {
110
throw illegalState('service accessor is only valid during the invocation of its target method');
111
}
112
const result = this._getOrCreateServiceInstance(id, _trace);
113
return result;
114
}
115
};
116
return fn(accessor, ...args);
117
} finally {
118
_done = true;
119
_trace.stop();
120
}
121
}
122
123
createInstance<T>(descriptor: SyncDescriptor0<T>): T;
124
createInstance<Ctor extends new (...args: any[]) => unknown, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;
125
createInstance(ctorOrDescriptor: any | SyncDescriptor<any>, ...rest: any[]): unknown {
126
this._throwIfDisposed();
127
128
let _trace: Trace;
129
let result: unknown;
130
if (ctorOrDescriptor instanceof SyncDescriptor) {
131
_trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor.ctor);
132
result = this._createInstance(ctorOrDescriptor.ctor, ctorOrDescriptor.staticArguments.concat(rest), _trace);
133
} else {
134
_trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor);
135
result = this._createInstance(ctorOrDescriptor, rest, _trace);
136
}
137
_trace.stop();
138
return result;
139
}
140
141
private _createInstance<T>(ctor: any, args: any[] = [], _trace: Trace): T {
142
143
// arguments defined by service decorators
144
const serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);
145
const serviceArgs: any[] = [];
146
for (const dependency of serviceDependencies) {
147
const service = this._getOrCreateServiceInstance(dependency.id, _trace);
148
if (!service) {
149
this._throwIfStrict(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`, false);
150
}
151
serviceArgs.push(service);
152
}
153
154
const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;
155
156
// check for argument mismatches, adjust static args if needed
157
if (args.length !== firstServiceArgPos) {
158
console.trace(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);
159
160
const delta = firstServiceArgPos - args.length;
161
if (delta > 0) {
162
args = args.concat(new Array(delta));
163
} else {
164
args = args.slice(0, firstServiceArgPos);
165
}
166
}
167
168
// now create the instance
169
return Reflect.construct<any, T>(ctor, args.concat(serviceArgs));
170
}
171
172
private _setCreatedServiceInstance<T>(id: ServiceIdentifier<T>, instance: T): void {
173
if (this._services.get(id) instanceof SyncDescriptor) {
174
this._services.set(id, instance);
175
} else if (this._parent) {
176
this._parent._setCreatedServiceInstance(id, instance);
177
} else {
178
throw new Error('illegalState - setting UNKNOWN service instance');
179
}
180
}
181
182
private _getServiceInstanceOrDescriptor<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
183
const instanceOrDesc = this._services.get(id);
184
if (!instanceOrDesc && this._parent) {
185
return this._parent._getServiceInstanceOrDescriptor(id);
186
} else {
187
return instanceOrDesc;
188
}
189
}
190
191
protected _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>, _trace: Trace): T {
192
if (this._globalGraph && this._globalGraphImplicitDependency) {
193
this._globalGraph.insertEdge(this._globalGraphImplicitDependency, String(id));
194
}
195
const thing = this._getServiceInstanceOrDescriptor(id);
196
if (thing instanceof SyncDescriptor) {
197
return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true));
198
} else {
199
_trace.branch(id, false);
200
return thing;
201
}
202
}
203
204
private readonly _activeInstantiations = new Set<ServiceIdentifier<any>>();
205
206
207
private _safeCreateAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {
208
if (this._activeInstantiations.has(id)) {
209
throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`);
210
}
211
this._activeInstantiations.add(id);
212
try {
213
return this._createAndCacheServiceInstance(id, desc, _trace);
214
} finally {
215
this._activeInstantiations.delete(id);
216
}
217
}
218
219
private _createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {
220
221
type Triple = { id: ServiceIdentifier<any>; desc: SyncDescriptor<any>; _trace: Trace };
222
const graph = new Graph<Triple>(data => data.id.toString());
223
224
let cycleCount = 0;
225
const stack = [{ id, desc, _trace }];
226
const seen = new Set<string>();
227
while (stack.length) {
228
const item = stack.pop()!;
229
230
if (seen.has(String(item.id))) {
231
continue;
232
}
233
seen.add(String(item.id));
234
235
graph.lookupOrInsertNode(item);
236
237
// a weak but working heuristic for cycle checks
238
if (cycleCount++ > 1000) {
239
throw new CyclicDependencyError(graph);
240
}
241
242
// check all dependencies for existence and if they need to be created first
243
for (const dependency of _util.getServiceDependencies(item.desc.ctor)) {
244
245
const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
246
if (!instanceOrDesc) {
247
this._throwIfStrict(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`, true);
248
}
249
250
// take note of all service dependencies
251
this._globalGraph?.insertEdge(String(item.id), String(dependency.id));
252
253
if (instanceOrDesc instanceof SyncDescriptor) {
254
const d = { id: dependency.id, desc: instanceOrDesc, _trace: item._trace.branch(dependency.id, true) };
255
graph.insertEdge(item, d);
256
stack.push(d);
257
}
258
}
259
}
260
261
while (true) {
262
const roots = graph.roots();
263
264
// if there is no more roots but still
265
// nodes in the graph we have a cycle
266
if (roots.length === 0) {
267
if (!graph.isEmpty()) {
268
throw new CyclicDependencyError(graph);
269
}
270
break;
271
}
272
273
for (const { data } of roots) {
274
// Repeat the check for this still being a service sync descriptor. That's because
275
// instantiating a dependency might have side-effect and recursively trigger instantiation
276
// so that some dependencies are now fullfilled already.
277
const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);
278
if (instanceOrDesc instanceof SyncDescriptor) {
279
// create instance and overwrite the service collections
280
const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation, data._trace);
281
this._setCreatedServiceInstance(data.id, instance);
282
}
283
graph.removeNode(data);
284
}
285
}
286
return <T>this._getServiceInstanceOrDescriptor(id);
287
}
288
289
private _createServiceInstanceWithOwner<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace): T {
290
if (this._services.get(id) instanceof SyncDescriptor) {
291
return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation, _trace, this._servicesToMaybeDispose);
292
} else if (this._parent) {
293
return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation, _trace);
294
} else {
295
throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`);
296
}
297
}
298
299
private _createServiceInstance<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace, disposeBucket: Set<any>): T {
300
if (!supportsDelayedInstantiation) {
301
// eager instantiation
302
const result = this._createInstance<T>(ctor, args, _trace);
303
disposeBucket.add(result);
304
return result;
305
306
} else {
307
const child = new InstantiationService(undefined, this._strict, this, this._enableTracing);
308
child._globalGraphImplicitDependency = String(id);
309
310
type EaryListenerData = {
311
listener: Parameters<Event<any>>;
312
disposable?: IDisposable;
313
};
314
315
// Return a proxy object that's backed by an idle value. That
316
// strategy is to instantiate services in our idle time or when actually
317
// needed but not when injected into a consumer
318
319
// return "empty events" when the service isn't instantiated yet
320
const earlyListeners = new Map<string, LinkedList<EaryListenerData>>();
321
322
const idle = new GlobalIdleValue<any>(() => {
323
const result = child._createInstance<T>(ctor, args, _trace);
324
325
// early listeners that we kept are now being subscribed to
326
// the real service
327
for (const [key, values] of earlyListeners) {
328
const candidate = <Event<any>>(<any>result)[key];
329
if (typeof candidate === 'function') {
330
for (const value of values) {
331
value.disposable = candidate.apply(result, value.listener);
332
}
333
}
334
}
335
earlyListeners.clear();
336
disposeBucket.add(result);
337
return result;
338
});
339
return <T>new Proxy(Object.create(null), {
340
get(target: any, key: PropertyKey): unknown {
341
342
if (!idle.isInitialized) {
343
// looks like an event
344
if (typeof key === 'string' && (key.startsWith('onDid') || key.startsWith('onWill'))) {
345
let list = earlyListeners.get(key);
346
if (!list) {
347
list = new LinkedList();
348
earlyListeners.set(key, list);
349
}
350
const event: Event<any> = (callback, thisArg, disposables) => {
351
if (idle.isInitialized) {
352
return idle.value[key](callback, thisArg, disposables);
353
} else {
354
const entry: EaryListenerData = { listener: [callback, thisArg, disposables], disposable: undefined };
355
const rm = list.push(entry);
356
const result = toDisposable(() => {
357
rm();
358
entry.disposable?.dispose();
359
});
360
return result;
361
}
362
};
363
return event;
364
}
365
}
366
367
// value already exists
368
if (key in target) {
369
return target[key];
370
}
371
372
// create value
373
const obj = idle.value;
374
let prop = obj[key];
375
if (typeof prop !== 'function') {
376
return prop;
377
}
378
prop = prop.bind(obj);
379
target[key] = prop;
380
return prop;
381
},
382
set(_target: T, p: PropertyKey, value: any): boolean {
383
idle.value[p] = value;
384
return true;
385
},
386
getPrototypeOf(_target: T) {
387
return ctor.prototype;
388
}
389
});
390
}
391
}
392
393
private _throwIfStrict(msg: string, printWarning: boolean): void {
394
if (printWarning) {
395
console.warn(msg);
396
}
397
if (this._strict) {
398
throw new Error(msg);
399
}
400
}
401
}
402
403
//#region -- tracing ---
404
405
const enum TraceType {
406
None = 0,
407
Creation = 1,
408
Invocation = 2,
409
Branch = 3,
410
}
411
412
export class Trace {
413
414
static all = new Set<string>();
415
416
private static readonly _None = new class extends Trace {
417
constructor() { super(TraceType.None, null); }
418
override stop() { }
419
override branch() { return this; }
420
};
421
422
static traceInvocation(_enableTracing: boolean, ctor: any): Trace {
423
return !_enableTracing ? Trace._None : new Trace(TraceType.Invocation, ctor.name || new Error().stack!.split('\n').slice(3, 4).join('\n'));
424
}
425
426
static traceCreation(_enableTracing: boolean, ctor: any): Trace {
427
return !_enableTracing ? Trace._None : new Trace(TraceType.Creation, ctor.name);
428
}
429
430
private static _totals: number = 0;
431
private readonly _start: number = Date.now();
432
private readonly _dep: [ServiceIdentifier<any>, boolean, Trace?][] = [];
433
434
private constructor(
435
readonly type: TraceType,
436
readonly name: string | null
437
) { }
438
439
branch(id: ServiceIdentifier<any>, first: boolean): Trace {
440
const child = new Trace(TraceType.Branch, id.toString());
441
this._dep.push([id, first, child]);
442
return child;
443
}
444
445
stop() {
446
const dur = Date.now() - this._start;
447
Trace._totals += dur;
448
449
let causedCreation = false;
450
451
function printChild(n: number, trace: Trace) {
452
const res: string[] = [];
453
const prefix = new Array(n + 1).join('\t');
454
for (const [id, first, child] of trace._dep) {
455
if (first && child) {
456
causedCreation = true;
457
res.push(`${prefix}CREATES -> ${id}`);
458
const nested = printChild(n + 1, child);
459
if (nested) {
460
res.push(nested);
461
}
462
} else {
463
res.push(`${prefix}uses -> ${id}`);
464
}
465
}
466
return res.join('\n');
467
}
468
469
const lines = [
470
`${this.type === TraceType.Creation ? 'CREATE' : 'CALL'} ${this.name}`,
471
`${printChild(1, this)}`,
472
`DONE, took ${dur.toFixed(2)}ms (grand total ${Trace._totals.toFixed(2)}ms)`
473
];
474
475
if (dur > 2 || causedCreation) {
476
Trace.all.add(lines.join('\n'));
477
}
478
}
479
}
480
481
//#endregion
482
483