Path: blob/main/src/vs/platform/instantiation/common/instantiationService.ts
5262 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 { GlobalIdleValue } from '../../../base/common/async.js';6import { Event } from '../../../base/common/event.js';7import { illegalState } from '../../../base/common/errors.js';8import { DisposableStore, dispose, IDisposable, isDisposable, toDisposable } from '../../../base/common/lifecycle.js';9import { SyncDescriptor, SyncDescriptor0 } from './descriptors.js';10import { Graph } from './graph.js';11import { GetLeadingNonServiceArgs, IInstantiationService, ServiceIdentifier, ServicesAccessor, _util } from './instantiation.js';12import { ServiceCollection } from './serviceCollection.js';13import { LinkedList } from '../../../base/common/linkedList.js';1415// TRACING16const _enableAllTracing = false17// || "TRUE" // DO NOT CHECK IN!18;1920class CyclicDependencyError extends Error {21constructor(graph: Graph<any>) {22super('cyclic dependency between services');23this.message = graph.findCycleSlow() ?? `UNABLE to detect cycle, dumping graph: \n${graph.toString()}`;24}25}2627export class InstantiationService implements IInstantiationService {2829declare readonly _serviceBrand: undefined;3031readonly _globalGraph?: Graph<string>;32private _globalGraphImplicitDependency?: string;3334private _isDisposed = false;35private readonly _servicesToMaybeDispose = new Set<any>();36private readonly _children = new Set<InstantiationService>();3738constructor(39private readonly _services: ServiceCollection = new ServiceCollection(),40private readonly _strict: boolean = false,41private readonly _parent?: InstantiationService,42private readonly _enableTracing: boolean = _enableAllTracing43) {4445this._services.set(IInstantiationService, this);46this._globalGraph = _enableTracing ? _parent?._globalGraph ?? new Graph(e => e) : undefined;47}4849dispose(): void {50if (!this._isDisposed) {51this._isDisposed = true;52// dispose all child services53dispose(this._children);54this._children.clear();5556// dispose all services created by this service57for (const candidate of this._servicesToMaybeDispose) {58if (isDisposable(candidate)) {59candidate.dispose();60}61}62this._servicesToMaybeDispose.clear();63}64}6566private _throwIfDisposed(): void {67if (this._isDisposed) {68throw new Error('InstantiationService has been disposed');69}70}7172createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService {73this._throwIfDisposed();7475const that = this;76const result = new class extends InstantiationService {77override dispose(): void {78that._children.delete(result);79super.dispose();80}81}(services, this._strict, this, this._enableTracing);82this._children.add(result);8384store?.add(result);85return result;86}8788invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R {89this._throwIfDisposed();9091const _trace = Trace.traceInvocation(this._enableTracing, fn);92let _done = false;93try {94const accessor: ServicesAccessor = {95get: <T>(id: ServiceIdentifier<T>) => {9697if (_done) {98throw illegalState('service accessor is only valid during the invocation of its target method');99}100101const result = this._getOrCreateServiceInstance(id, _trace);102if (!result) {103this._throwIfStrict(`[invokeFunction] unknown service '${id}'`, false);104}105return result;106}107};108return fn(accessor, ...args);109} finally {110_done = true;111_trace.stop();112}113}114115createInstance<T>(descriptor: SyncDescriptor0<T>): T;116createInstance<Ctor extends new (...args: any[]) => unknown, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;117createInstance(ctorOrDescriptor: any | SyncDescriptor<any>, ...rest: unknown[]): unknown {118this._throwIfDisposed();119120let _trace: Trace;121let result: unknown;122if (ctorOrDescriptor instanceof SyncDescriptor) {123_trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor.ctor);124result = this._createInstance(ctorOrDescriptor.ctor, ctorOrDescriptor.staticArguments.concat(rest), _trace);125} else {126_trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor);127result = this._createInstance(ctorOrDescriptor, rest, _trace);128}129_trace.stop();130return result;131}132133private _createInstance<T>(ctor: any, args: unknown[] = [], _trace: Trace): T {134135// arguments defined by service decorators136const serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);137const serviceArgs: unknown[] = [];138for (const dependency of serviceDependencies) {139const service = this._getOrCreateServiceInstance(dependency.id, _trace);140if (!service) {141this._throwIfStrict(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`, false);142}143serviceArgs.push(service);144}145146const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;147148// check for argument mismatches, adjust static args if needed149if (args.length !== firstServiceArgPos) {150console.trace(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);151152const delta = firstServiceArgPos - args.length;153if (delta > 0) {154args = args.concat(new Array(delta));155} else {156args = args.slice(0, firstServiceArgPos);157}158}159160// now create the instance161return Reflect.construct<any, T>(ctor, args.concat(serviceArgs));162}163164private _setCreatedServiceInstance<T>(id: ServiceIdentifier<T>, instance: T): void {165if (this._services.get(id) instanceof SyncDescriptor) {166this._services.set(id, instance);167} else if (this._parent) {168this._parent._setCreatedServiceInstance(id, instance);169} else {170throw new Error('illegalState - setting UNKNOWN service instance');171}172}173174private _getServiceInstanceOrDescriptor<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {175const instanceOrDesc = this._services.get(id);176if (!instanceOrDesc && this._parent) {177return this._parent._getServiceInstanceOrDescriptor(id);178} else {179return instanceOrDesc;180}181}182183protected _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>, _trace: Trace): T {184if (this._globalGraph && this._globalGraphImplicitDependency) {185this._globalGraph.insertEdge(this._globalGraphImplicitDependency, String(id));186}187const thing = this._getServiceInstanceOrDescriptor(id);188if (thing instanceof SyncDescriptor) {189return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true));190} else {191_trace.branch(id, false);192return thing;193}194}195196private readonly _activeInstantiations = new Set<ServiceIdentifier<any>>();197198199private _safeCreateAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {200if (this._activeInstantiations.has(id)) {201throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`);202}203this._activeInstantiations.add(id);204try {205return this._createAndCacheServiceInstance(id, desc, _trace);206} finally {207this._activeInstantiations.delete(id);208}209}210211private _createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {212213type Triple = { id: ServiceIdentifier<any>; desc: SyncDescriptor<any>; _trace: Trace };214const graph = new Graph<Triple>(data => data.id.toString());215216let cycleCount = 0;217const stack = [{ id, desc, _trace }];218const seen = new Set<string>();219while (stack.length) {220const item = stack.pop()!;221222if (seen.has(String(item.id))) {223continue;224}225seen.add(String(item.id));226227graph.lookupOrInsertNode(item);228229// a weak but working heuristic for cycle checks230if (cycleCount++ > 1000) {231throw new CyclicDependencyError(graph);232}233234// check all dependencies for existence and if they need to be created first235for (const dependency of _util.getServiceDependencies(item.desc.ctor)) {236237const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);238if (!instanceOrDesc) {239this._throwIfStrict(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`, true);240}241242// take note of all service dependencies243this._globalGraph?.insertEdge(String(item.id), String(dependency.id));244245if (instanceOrDesc instanceof SyncDescriptor) {246const d = { id: dependency.id, desc: instanceOrDesc, _trace: item._trace.branch(dependency.id, true) };247graph.insertEdge(item, d);248stack.push(d);249}250}251}252253while (true) {254const roots = graph.roots();255256// if there is no more roots but still257// nodes in the graph we have a cycle258if (roots.length === 0) {259if (!graph.isEmpty()) {260throw new CyclicDependencyError(graph);261}262break;263}264265for (const { data } of roots) {266// Repeat the check for this still being a service sync descriptor. That's because267// instantiating a dependency might have side-effect and recursively trigger instantiation268// so that some dependencies are now fullfilled already.269const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);270if (instanceOrDesc instanceof SyncDescriptor) {271// create instance and overwrite the service collections272const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation, data._trace);273this._setCreatedServiceInstance(data.id, instance);274}275graph.removeNode(data);276}277}278return <T>this._getServiceInstanceOrDescriptor(id);279}280281private _createServiceInstanceWithOwner<T>(id: ServiceIdentifier<T>, ctor: any, args: unknown[] = [], supportsDelayedInstantiation: boolean, _trace: Trace): T {282if (this._services.get(id) instanceof SyncDescriptor) {283return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation, _trace, this._servicesToMaybeDispose);284} else if (this._parent) {285return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation, _trace);286} else {287throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`);288}289}290291private _createServiceInstance<T>(id: ServiceIdentifier<T>, ctor: any, args: unknown[] = [], supportsDelayedInstantiation: boolean, _trace: Trace, disposeBucket: Set<any>): T {292if (!supportsDelayedInstantiation) {293// eager instantiation294const result = this._createInstance<T>(ctor, args, _trace);295disposeBucket.add(result);296return result;297298} else {299const child = new InstantiationService(undefined, this._strict, this, this._enableTracing);300child._globalGraphImplicitDependency = String(id);301302type EaryListenerData = {303listener: Parameters<Event<any>>;304disposable?: IDisposable;305};306307// Return a proxy object that's backed by an idle value. That308// strategy is to instantiate services in our idle time or when actually309// needed but not when injected into a consumer310311// return "empty events" when the service isn't instantiated yet312const earlyListeners = new Map<string, LinkedList<EaryListenerData>>();313314const idle = new GlobalIdleValue<any>(() => {315const result = child._createInstance<T>(ctor, args, _trace);316317// early listeners that we kept are now being subscribed to318// the real service319for (const [key, values] of earlyListeners) {320// eslint-disable-next-line local/code-no-any-casts321const candidate = <Event<any>>(<any>result)[key];322if (typeof candidate === 'function') {323for (const value of values) {324value.disposable = candidate.apply(result, value.listener);325}326}327}328earlyListeners.clear();329disposeBucket.add(result);330return result;331});332return <T>new Proxy(Object.create(null), {333get(target: any, key: PropertyKey): unknown {334335if (!idle.isInitialized) {336// looks like an event337if (typeof key === 'string' && (key.startsWith('onDid') || key.startsWith('onWill'))) {338let list = earlyListeners.get(key);339if (!list) {340list = new LinkedList();341earlyListeners.set(key, list);342}343const event: Event<any> = (callback, thisArg, disposables) => {344if (idle.isInitialized) {345return idle.value[key](callback, thisArg, disposables);346} else {347const entry: EaryListenerData = { listener: [callback, thisArg, disposables], disposable: undefined };348const rm = list.push(entry);349const result = toDisposable(() => {350rm();351entry.disposable?.dispose();352});353return result;354}355};356return event;357}358}359360// value already exists361if (key in target) {362return target[key];363}364365// create value366const obj = idle.value;367let prop = obj[key];368if (typeof prop !== 'function') {369return prop;370}371prop = prop.bind(obj);372target[key] = prop;373return prop;374},375set(_target: T, p: PropertyKey, value: any): boolean {376idle.value[p] = value;377return true;378},379getPrototypeOf(_target: T) {380return ctor.prototype;381}382});383}384}385386private _throwIfStrict(msg: string, printWarning: boolean): void {387if (printWarning) {388console.warn(msg);389}390if (this._strict) {391throw new Error(msg);392}393}394}395396//#region -- tracing ---397398const enum TraceType {399None = 0,400Creation = 1,401Invocation = 2,402Branch = 3,403}404405export class Trace {406407static all = new Set<string>();408409private static readonly _None = new class extends Trace {410constructor() { super(TraceType.None, null); }411override stop() { }412override branch() { return this; }413};414415static traceInvocation(_enableTracing: boolean, ctor: any): Trace {416return !_enableTracing ? Trace._None : new Trace(TraceType.Invocation, ctor.name || new Error().stack!.split('\n').slice(3, 4).join('\n'));417}418419static traceCreation(_enableTracing: boolean, ctor: any): Trace {420return !_enableTracing ? Trace._None : new Trace(TraceType.Creation, ctor.name);421}422423private static _totals: number = 0;424private readonly _start: number = Date.now();425private readonly _dep: [ServiceIdentifier<any>, boolean, Trace?][] = [];426427private constructor(428readonly type: TraceType,429readonly name: string | null430) { }431432branch(id: ServiceIdentifier<any>, first: boolean): Trace {433const child = new Trace(TraceType.Branch, id.toString());434this._dep.push([id, first, child]);435return child;436}437438stop() {439const dur = Date.now() - this._start;440Trace._totals += dur;441442let causedCreation = false;443444function printChild(n: number, trace: Trace) {445const res: string[] = [];446const prefix = new Array(n + 1).join('\t');447for (const [id, first, child] of trace._dep) {448if (first && child) {449causedCreation = true;450res.push(`${prefix}CREATES -> ${id}`);451const nested = printChild(n + 1, child);452if (nested) {453res.push(nested);454}455} else {456res.push(`${prefix}uses -> ${id}`);457}458}459return res.join('\n');460}461462const lines = [463`${this.type === TraceType.Creation ? 'CREATE' : 'CALL'} ${this.name}`,464`${printChild(1, this)}`,465`DONE, took ${dur.toFixed(2)}ms (grand total ${Trace._totals.toFixed(2)}ms)`466];467468if (dur > 2 || causedCreation) {469Trace.all.add(lines.join('\n'));470}471}472}473474//#endregion475476477