Path: blob/main/src/vs/platform/instantiation/common/instantiationService.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 { 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) {103throw new Error(`[invokeFunction] unknown service '${id}'`);104}105return result;106},107getIfExists: <T>(id: ServiceIdentifier<T>) => {108if (_done) {109throw illegalState('service accessor is only valid during the invocation of its target method');110}111const result = this._getOrCreateServiceInstance(id, _trace);112return result;113}114};115return fn(accessor, ...args);116} finally {117_done = true;118_trace.stop();119}120}121122createInstance<T>(descriptor: SyncDescriptor0<T>): T;123createInstance<Ctor extends new (...args: any[]) => unknown, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;124createInstance(ctorOrDescriptor: any | SyncDescriptor<any>, ...rest: any[]): unknown {125this._throwIfDisposed();126127let _trace: Trace;128let result: unknown;129if (ctorOrDescriptor instanceof SyncDescriptor) {130_trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor.ctor);131result = this._createInstance(ctorOrDescriptor.ctor, ctorOrDescriptor.staticArguments.concat(rest), _trace);132} else {133_trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor);134result = this._createInstance(ctorOrDescriptor, rest, _trace);135}136_trace.stop();137return result;138}139140private _createInstance<T>(ctor: any, args: any[] = [], _trace: Trace): T {141142// arguments defined by service decorators143const serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);144const serviceArgs: any[] = [];145for (const dependency of serviceDependencies) {146const service = this._getOrCreateServiceInstance(dependency.id, _trace);147if (!service) {148this._throwIfStrict(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`, false);149}150serviceArgs.push(service);151}152153const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;154155// check for argument mismatches, adjust static args if needed156if (args.length !== firstServiceArgPos) {157console.trace(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);158159const delta = firstServiceArgPos - args.length;160if (delta > 0) {161args = args.concat(new Array(delta));162} else {163args = args.slice(0, firstServiceArgPos);164}165}166167// now create the instance168return Reflect.construct<any, T>(ctor, args.concat(serviceArgs));169}170171private _setCreatedServiceInstance<T>(id: ServiceIdentifier<T>, instance: T): void {172if (this._services.get(id) instanceof SyncDescriptor) {173this._services.set(id, instance);174} else if (this._parent) {175this._parent._setCreatedServiceInstance(id, instance);176} else {177throw new Error('illegalState - setting UNKNOWN service instance');178}179}180181private _getServiceInstanceOrDescriptor<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {182const instanceOrDesc = this._services.get(id);183if (!instanceOrDesc && this._parent) {184return this._parent._getServiceInstanceOrDescriptor(id);185} else {186return instanceOrDesc;187}188}189190protected _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>, _trace: Trace): T {191if (this._globalGraph && this._globalGraphImplicitDependency) {192this._globalGraph.insertEdge(this._globalGraphImplicitDependency, String(id));193}194const thing = this._getServiceInstanceOrDescriptor(id);195if (thing instanceof SyncDescriptor) {196return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true));197} else {198_trace.branch(id, false);199return thing;200}201}202203private readonly _activeInstantiations = new Set<ServiceIdentifier<any>>();204205206private _safeCreateAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {207if (this._activeInstantiations.has(id)) {208throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`);209}210this._activeInstantiations.add(id);211try {212return this._createAndCacheServiceInstance(id, desc, _trace);213} finally {214this._activeInstantiations.delete(id);215}216}217218private _createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {219220type Triple = { id: ServiceIdentifier<any>; desc: SyncDescriptor<any>; _trace: Trace };221const graph = new Graph<Triple>(data => data.id.toString());222223let cycleCount = 0;224const stack = [{ id, desc, _trace }];225const seen = new Set<string>();226while (stack.length) {227const item = stack.pop()!;228229if (seen.has(String(item.id))) {230continue;231}232seen.add(String(item.id));233234graph.lookupOrInsertNode(item);235236// a weak but working heuristic for cycle checks237if (cycleCount++ > 1000) {238throw new CyclicDependencyError(graph);239}240241// check all dependencies for existence and if they need to be created first242for (const dependency of _util.getServiceDependencies(item.desc.ctor)) {243244const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);245if (!instanceOrDesc) {246this._throwIfStrict(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`, true);247}248249// take note of all service dependencies250this._globalGraph?.insertEdge(String(item.id), String(dependency.id));251252if (instanceOrDesc instanceof SyncDescriptor) {253const d = { id: dependency.id, desc: instanceOrDesc, _trace: item._trace.branch(dependency.id, true) };254graph.insertEdge(item, d);255stack.push(d);256}257}258}259260while (true) {261const roots = graph.roots();262263// if there is no more roots but still264// nodes in the graph we have a cycle265if (roots.length === 0) {266if (!graph.isEmpty()) {267throw new CyclicDependencyError(graph);268}269break;270}271272for (const { data } of roots) {273// Repeat the check for this still being a service sync descriptor. That's because274// instantiating a dependency might have side-effect and recursively trigger instantiation275// so that some dependencies are now fullfilled already.276const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);277if (instanceOrDesc instanceof SyncDescriptor) {278// create instance and overwrite the service collections279const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation, data._trace);280this._setCreatedServiceInstance(data.id, instance);281}282graph.removeNode(data);283}284}285return <T>this._getServiceInstanceOrDescriptor(id);286}287288private _createServiceInstanceWithOwner<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace): T {289if (this._services.get(id) instanceof SyncDescriptor) {290return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation, _trace, this._servicesToMaybeDispose);291} else if (this._parent) {292return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation, _trace);293} else {294throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`);295}296}297298private _createServiceInstance<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace, disposeBucket: Set<any>): T {299if (!supportsDelayedInstantiation) {300// eager instantiation301const result = this._createInstance<T>(ctor, args, _trace);302disposeBucket.add(result);303return result;304305} else {306const child = new InstantiationService(undefined, this._strict, this, this._enableTracing);307child._globalGraphImplicitDependency = String(id);308309type EaryListenerData = {310listener: Parameters<Event<any>>;311disposable?: IDisposable;312};313314// Return a proxy object that's backed by an idle value. That315// strategy is to instantiate services in our idle time or when actually316// needed but not when injected into a consumer317318// return "empty events" when the service isn't instantiated yet319const earlyListeners = new Map<string, LinkedList<EaryListenerData>>();320321const idle = new GlobalIdleValue<any>(() => {322const result = child._createInstance<T>(ctor, args, _trace);323324// early listeners that we kept are now being subscribed to325// the real service326for (const [key, values] of earlyListeners) {327const candidate = <Event<any>>(<any>result)[key];328if (typeof candidate === 'function') {329for (const value of values) {330value.disposable = candidate.apply(result, value.listener);331}332}333}334earlyListeners.clear();335disposeBucket.add(result);336return result;337});338return <T>new Proxy(Object.create(null), {339get(target: any, key: PropertyKey): unknown {340341if (!idle.isInitialized) {342// looks like an event343if (typeof key === 'string' && (key.startsWith('onDid') || key.startsWith('onWill'))) {344let list = earlyListeners.get(key);345if (!list) {346list = new LinkedList();347earlyListeners.set(key, list);348}349const event: Event<any> = (callback, thisArg, disposables) => {350if (idle.isInitialized) {351return idle.value[key](callback, thisArg, disposables);352} else {353const entry: EaryListenerData = { listener: [callback, thisArg, disposables], disposable: undefined };354const rm = list.push(entry);355const result = toDisposable(() => {356rm();357entry.disposable?.dispose();358});359return result;360}361};362return event;363}364}365366// value already exists367if (key in target) {368return target[key];369}370371// create value372const obj = idle.value;373let prop = obj[key];374if (typeof prop !== 'function') {375return prop;376}377prop = prop.bind(obj);378target[key] = prop;379return prop;380},381set(_target: T, p: PropertyKey, value: any): boolean {382idle.value[p] = value;383return true;384},385getPrototypeOf(_target: T) {386return ctor.prototype;387}388});389}390}391392private _throwIfStrict(msg: string, printWarning: boolean): void {393if (printWarning) {394console.warn(msg);395}396if (this._strict) {397throw new Error(msg);398}399}400}401402//#region -- tracing ---403404const enum TraceType {405None = 0,406Creation = 1,407Invocation = 2,408Branch = 3,409}410411export class Trace {412413static all = new Set<string>();414415private static readonly _None = new class extends Trace {416constructor() { super(TraceType.None, null); }417override stop() { }418override branch() { return this; }419};420421static traceInvocation(_enableTracing: boolean, ctor: any): Trace {422return !_enableTracing ? Trace._None : new Trace(TraceType.Invocation, ctor.name || new Error().stack!.split('\n').slice(3, 4).join('\n'));423}424425static traceCreation(_enableTracing: boolean, ctor: any): Trace {426return !_enableTracing ? Trace._None : new Trace(TraceType.Creation, ctor.name);427}428429private static _totals: number = 0;430private readonly _start: number = Date.now();431private readonly _dep: [ServiceIdentifier<any>, boolean, Trace?][] = [];432433private constructor(434readonly type: TraceType,435readonly name: string | null436) { }437438branch(id: ServiceIdentifier<any>, first: boolean): Trace {439const child = new Trace(TraceType.Branch, id.toString());440this._dep.push([id, first, child]);441return child;442}443444stop() {445const dur = Date.now() - this._start;446Trace._totals += dur;447448let causedCreation = false;449450function printChild(n: number, trace: Trace) {451const res: string[] = [];452const prefix = new Array(n + 1).join('\t');453for (const [id, first, child] of trace._dep) {454if (first && child) {455causedCreation = true;456res.push(`${prefix}CREATES -> ${id}`);457const nested = printChild(n + 1, child);458if (nested) {459res.push(nested);460}461} else {462res.push(`${prefix}uses -> ${id}`);463}464}465return res.join('\n');466}467468const lines = [469`${this.type === TraceType.Creation ? 'CREATE' : 'CALL'} ${this.name}`,470`${printChild(1, this)}`,471`DONE, took ${dur.toFixed(2)}ms (grand total ${Trace._totals.toFixed(2)}ms)`472];473474if (dur > 2 || causedCreation) {475Trace.all.add(lines.join('\n'));476}477}478}479480//#endregion481482483