mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(devtools): create profiler for DI and injector events (#48639)
Currently, understanding dependency injection in Angular requires a lot of context and has been sited in our surveys as one of the largest points of confusion that our users have with the framework. This commit is the beginning of our approach to make debugging dependency injection in Angular easier. This commit introduces injector profiler callbacks in parts of the framework to emit injector events. It also introduces a default handler for these events. This default handler parses the stream of events to construct some data structures that will support new injector debug APIs. We have implemented a similar pattern in the past to minimize overhead. There is also the possiblity of making the internal `setInjectorProfiler` function a public debug API in the future, so that users can implement their own handlers to debug DI events. Lastly DI in Angular maps nicely to a stream of events. For production applications there is no runtime overhead. For applications in dev mode there is some additional overhead from the default profiler handling injector events and holding debug data in memory. For production applications, dead code elimination should strip all of the code used by this PR. PR Close #48639
This commit is contained in:
parent
c1052cf7a7
commit
ff4d1b4a0e
10 changed files with 632 additions and 15 deletions
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import {RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {InjectorProfilerContext, setInjectorProfilerContext} from '../render3/debug/injector_profiler';
|
||||
import type {Injector} from './injector';
|
||||
import {getCurrentInjector, setCurrentInjector} from './injector_compatibility';
|
||||
import {getInjectImplementation, setInjectImplementation} from './inject_switch';
|
||||
|
|
@ -31,12 +32,17 @@ export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => Ret
|
|||
injector.assertNotDestroyed();
|
||||
}
|
||||
|
||||
let prevInjectorProfilerContext: InjectorProfilerContext;
|
||||
if (ngDevMode) {
|
||||
prevInjectorProfilerContext = setInjectorProfilerContext({injector, token: null});
|
||||
}
|
||||
const prevInjector = setCurrentInjector(injector);
|
||||
const previousInjectImplementation = setInjectImplementation(undefined);
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
setCurrentInjector(prevInjector);
|
||||
ngDevMode && setInjectorProfilerContext(prevInjectorProfilerContext!);
|
||||
setInjectImplementation(previousInjectImplementation);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import '../util/ng_dev_mode';
|
|||
|
||||
import {RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {Type} from '../interface/type';
|
||||
import {emitInjectEvent} from '../render3/debug/injector_profiler';
|
||||
import {stringify} from '../util/stringify';
|
||||
|
||||
import {resolveForwardRef} from './forward_ref';
|
||||
|
|
@ -65,7 +66,10 @@ export function injectInjectorOnly<T>(token: ProviderToken<T>, flags = InjectFla
|
|||
} else if (_currentInjector === null) {
|
||||
return injectRootLimpMode(token, undefined, flags);
|
||||
} else {
|
||||
return _currentInjector.get(token, flags & InjectFlags.Optional ? null : undefined, flags);
|
||||
const value =
|
||||
_currentInjector.get(token, flags & InjectFlags.Optional ? null : undefined, flags);
|
||||
ngDevMode && emitInjectEvent(token as Type<unknown>, value, flags);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import '../util/ng_dev_mode';
|
|||
import {RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {OnDestroy} from '../interface/lifecycle_hooks';
|
||||
import {Type} from '../interface/type';
|
||||
import {emitInstanceCreatedByInjectorEvent, emitProviderConfiguredEvent, InjectorProfilerContext, runInInjectorProfilerContext, setInjectorProfilerContext} from '../render3/debug/injector_profiler';
|
||||
import {FactoryFn, getFactoryDef} from '../render3/definition_factory';
|
||||
import {throwCyclicDependencyError, throwInvalidProviderError, throwMixedMultiProviderError} from '../render3/errors_di';
|
||||
import {NG_ENV_ID} from '../render3/fields';
|
||||
|
|
@ -225,11 +226,18 @@ export class R3Injector extends EnvironmentInjector {
|
|||
|
||||
const previousInjector = setCurrentInjector(this);
|
||||
const previousInjectImplementation = setInjectImplementation(undefined);
|
||||
|
||||
let prevInjectContext: InjectorProfilerContext|undefined;
|
||||
if (ngDevMode) {
|
||||
prevInjectContext = setInjectorProfilerContext({injector: this, token: null});
|
||||
}
|
||||
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
setCurrentInjector(previousInjector);
|
||||
setInjectImplementation(previousInjectImplementation);
|
||||
ngDevMode && setInjectorProfilerContext(prevInjectContext!);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -245,6 +253,10 @@ export class R3Injector extends EnvironmentInjector {
|
|||
flags = convertToBitFlags(flags) as InjectFlags;
|
||||
|
||||
// Set the injection context.
|
||||
let prevInjectContext: InjectorProfilerContext;
|
||||
if (ngDevMode) {
|
||||
prevInjectContext = setInjectorProfilerContext({injector: this, token: token as Type<T>});
|
||||
}
|
||||
const previousInjector = setCurrentInjector(this);
|
||||
const previousInjectImplementation = setInjectImplementation(undefined);
|
||||
try {
|
||||
|
|
@ -298,6 +310,7 @@ export class R3Injector extends EnvironmentInjector {
|
|||
// Lastly, restore the previous injection context.
|
||||
setInjectImplementation(previousInjectImplementation);
|
||||
setCurrentInjector(previousInjector);
|
||||
ngDevMode && setInjectorProfilerContext(prevInjectContext!);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -305,6 +318,11 @@ export class R3Injector extends EnvironmentInjector {
|
|||
resolveInjectorInitializers() {
|
||||
const previousInjector = setCurrentInjector(this);
|
||||
const previousInjectImplementation = setInjectImplementation(undefined);
|
||||
let prevInjectContext: InjectorProfilerContext|undefined;
|
||||
if (ngDevMode) {
|
||||
prevInjectContext = setInjectorProfilerContext({injector: this, token: null});
|
||||
}
|
||||
|
||||
try {
|
||||
const initializers = this.get(ENVIRONMENT_INITIALIZER.multi, EMPTY_ARRAY, InjectFlags.Self);
|
||||
if (ngDevMode && !Array.isArray(initializers)) {
|
||||
|
|
@ -321,6 +339,7 @@ export class R3Injector extends EnvironmentInjector {
|
|||
} finally {
|
||||
setCurrentInjector(previousInjector);
|
||||
setInjectImplementation(previousInjectImplementation);
|
||||
ngDevMode && setInjectorProfilerContext(prevInjectContext!);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -353,6 +372,18 @@ export class R3Injector extends EnvironmentInjector {
|
|||
|
||||
// Construct a `Record` for the provider.
|
||||
const record = providerToRecord(provider);
|
||||
if (ngDevMode) {
|
||||
runInInjectorProfilerContext(this, token, () => {
|
||||
// Emit InjectorProfilerEventType.Create if provider is a value provider because
|
||||
// these are the only providers that do not go through the value hydration logic
|
||||
// where this event would normally be emitted from.
|
||||
if (isValueProvider(provider)) {
|
||||
emitInstanceCreatedByInjectorEvent(provider.useValue);
|
||||
}
|
||||
|
||||
emitProviderConfiguredEvent(provider);
|
||||
});
|
||||
}
|
||||
|
||||
if (!isTypeProvider(provider) && provider.multi === true) {
|
||||
// If the provider indicates that it's a multi-provider, process it specially.
|
||||
|
|
@ -384,7 +415,15 @@ export class R3Injector extends EnvironmentInjector {
|
|||
throwCyclicDependencyError(stringify(token));
|
||||
} else if (record.value === NOT_YET) {
|
||||
record.value = CIRCULAR;
|
||||
record.value = record.factory!();
|
||||
|
||||
if (ngDevMode) {
|
||||
runInInjectorProfilerContext(this, token as Type<T>, () => {
|
||||
record.value = record.factory!();
|
||||
emitInstanceCreatedByInjectorEvent(record.value);
|
||||
});
|
||||
} else {
|
||||
record.value = record.factory!();
|
||||
}
|
||||
}
|
||||
if (typeof record.value === 'object' && record.value && hasOnDestroy(record.value)) {
|
||||
this._ngOnDestroyHooks.add(record.value);
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ function getNamespace(elementName: string): string|null {
|
|||
* Injector that looks up a value using a specific injector, before falling back to the module
|
||||
* injector. Used primarily when creating components or embedded views dynamically.
|
||||
*/
|
||||
class ChainedInjector implements Injector {
|
||||
constructor(private injector: Injector, private parentInjector: Injector) {}
|
||||
export class ChainedInjector implements Injector {
|
||||
constructor(public injector: Injector, public parentInjector: Injector) {}
|
||||
|
||||
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags|InjectOptions): T {
|
||||
flags = convertToBitFlags(flags);
|
||||
|
|
|
|||
246
packages/core/src/render3/debug/framework_injector_profiler.ts
Normal file
246
packages/core/src/render3/debug/framework_injector_profiler.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injector} from '../../di/injector';
|
||||
import {EnvironmentInjector} from '../../di/r3_injector';
|
||||
import {Type} from '../../interface/type';
|
||||
import {throwError} from '../../util/assert';
|
||||
import {getComponentDef} from '../definition';
|
||||
import {getNodeInjectorLView, NodeInjector} from '../di';
|
||||
import {LView} from '../interfaces/view';
|
||||
|
||||
import {InjectedService, InjectorCreatedInstance, InjectorProfilerContext, InjectorProfilerEvent, InjectorProfilerEventType, ProviderRecord, setInjectorProfiler} from './injector_profiler';
|
||||
|
||||
/**
|
||||
* These are the data structures that our framework injector profiler will fill with data in order
|
||||
* to support DI debugging APIs.
|
||||
*
|
||||
* resolverToTokenToDependencies: Maps an injector to a Map of tokens to an Array of
|
||||
* dependencies. Injector -> Token -> Dependencies This is used to support the
|
||||
* getDependenciesFromInjectable API, which takes in an injector and a token and returns it's
|
||||
* dependencies.
|
||||
*
|
||||
* resolverToProviders: Maps a DI resolver (an Injector or an LView) to the providers configured
|
||||
* within it This is used to support the getInjectorProviders API, which takes in an injector and
|
||||
* returns the providers that it was configured with.
|
||||
*
|
||||
* standaloneInjectorToComponent: Maps the injector of a standalone component to the standalone
|
||||
* component that it is associated with. Used in the getInjectorProviders API, specificially in the
|
||||
* discovery of import paths for each provider. This is necessary because the imports array of a
|
||||
* standalone component is processed and configured in its standalone injector, but exists within
|
||||
* the component's definition. Because getInjectorProviders takes in an injector, if that injector
|
||||
* is the injector of a standalone component, we need to be able to discover the place where the
|
||||
* imports array is located (the component) in order to flatten the imports array within it to
|
||||
* discover all of it's providers.
|
||||
*
|
||||
*
|
||||
* All of these data structures are instantiated with WeakMaps. This will ensure that the presence
|
||||
* of any object in the keys of these maps does not prevent the garbage collector from collecting
|
||||
* those objects. Because of this property of WeakMaps, these data structures will never be the
|
||||
* source of a memory leak.
|
||||
*
|
||||
* An example of this advantage: When components are destroyed, we don't need to do
|
||||
* any additional work to remove that component from our mappings.
|
||||
*
|
||||
*/
|
||||
class DIDebugData {
|
||||
resolverToTokenToDependencies =
|
||||
new WeakMap<Injector|LView, WeakMap<Type<unknown>, InjectedService[]>>();
|
||||
resolverToProviders = new WeakMap<Injector|LView, ProviderRecord[]>();
|
||||
standaloneInjectorToComponent = new WeakMap<Injector, Type<unknown>>();
|
||||
|
||||
reset() {
|
||||
this.resolverToTokenToDependencies =
|
||||
new WeakMap<Injector|LView, WeakMap<Type<unknown>, InjectedService[]>>();
|
||||
this.resolverToProviders = new WeakMap<Injector|LView, ProviderRecord[]>();
|
||||
this.standaloneInjectorToComponent = new WeakMap<Injector, Type<unknown>>();
|
||||
}
|
||||
}
|
||||
|
||||
let frameworkDIDebugData = new DIDebugData();
|
||||
|
||||
export function getFrameworkDIDebugData(): DIDebugData {
|
||||
return frameworkDIDebugData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initalize default handling of injector events. This handling parses events
|
||||
* as they are emitted and constructs the data structures necessary to support
|
||||
* some of debug APIs.
|
||||
*
|
||||
* See handleInjectEvent, handleCreateEvent and handleProviderConfiguredEvent
|
||||
* for descriptions of each handler
|
||||
*
|
||||
* Supported APIs:
|
||||
* - getDependenciesFromInjectable
|
||||
* - getInjectorProviders
|
||||
*/
|
||||
export function setupFrameworkInjectorProfiler(): void {
|
||||
frameworkDIDebugData.reset();
|
||||
setInjectorProfiler(
|
||||
(injectorProfilerEvent) => handleInjectorProfilerEvent(injectorProfilerEvent));
|
||||
}
|
||||
|
||||
function handleInjectorProfilerEvent(injectorProfilerEvent: InjectorProfilerEvent): void {
|
||||
const {context, type} = injectorProfilerEvent;
|
||||
|
||||
if (type === InjectorProfilerEventType.Inject) {
|
||||
handleInjectEvent(context, injectorProfilerEvent.service);
|
||||
} else if (type === InjectorProfilerEventType.InstanceCreatedByInjector) {
|
||||
handleInstanceCreatedByInjectorEvent(context, injectorProfilerEvent.instance);
|
||||
} else if (type === InjectorProfilerEventType.ProviderConfigured) {
|
||||
handleProviderConfiguredEvent(context, injectorProfilerEvent.providerRecord);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Stores the injected service in frameworkDIDebugData.resolverToTokenToDependencies
|
||||
* based on it's injector and token.
|
||||
*
|
||||
* @param context InjectorProfilerContext the injection context that this event occurred in.
|
||||
* @param data InjectedService the service associated with this inject event.
|
||||
*
|
||||
*/
|
||||
function handleInjectEvent(context: InjectorProfilerContext, data: InjectedService) {
|
||||
const diResolver = getDIResolver(context.injector);
|
||||
if (diResolver === null) {
|
||||
throwError('An Inject event must be run within an injection context.');
|
||||
}
|
||||
|
||||
const diResolverToInstantiatedToken = frameworkDIDebugData.resolverToTokenToDependencies;
|
||||
|
||||
if (!diResolverToInstantiatedToken.has(diResolver)) {
|
||||
diResolverToInstantiatedToken.set(diResolver, new WeakMap<Type<unknown>, InjectedService[]>());
|
||||
}
|
||||
|
||||
// if token is a primitive type, ignore this event. We do this because we cannot keep track of
|
||||
// non-primitive tokens in WeakMaps since they are not garbage collectable.
|
||||
if (!canBeHeldWeakly(context.token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instantiatedTokenToDependencies = diResolverToInstantiatedToken.get(diResolver)!;
|
||||
if (!instantiatedTokenToDependencies.has(context.token!)) {
|
||||
instantiatedTokenToDependencies.set(context.token!, []);
|
||||
}
|
||||
|
||||
const {token, value, flags} = data;
|
||||
instantiatedTokenToDependencies.get(context.token!)!.push({token, value, flags});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* If the created instance is an instance of a standalone component, maps the injector to that
|
||||
* standalone component in frameworkDIDebugData.standaloneInjectorToComponent
|
||||
*
|
||||
* @param context InjectorProfilerContext the injection context that this event occurred in.
|
||||
* @param data InjectorCreatedInstance an object containing the instance that was just created
|
||||
*
|
||||
*/
|
||||
function handleInstanceCreatedByInjectorEvent(
|
||||
context: InjectorProfilerContext, data: InjectorCreatedInstance): void {
|
||||
const {value} = data;
|
||||
|
||||
if (getDIResolver(context.injector) === null) {
|
||||
throwError('An InjectorCreatedInstance event must be run within an injection context.');
|
||||
}
|
||||
|
||||
// if our value is an instance of a standalone component, map the injector of that standalone
|
||||
// component to the component class. Otherwise, this event is a noop.
|
||||
let standaloneComponent: Type<unknown>|undefined = undefined;
|
||||
if (typeof value === 'object') {
|
||||
standaloneComponent = value?.constructor as Type<unknown>;
|
||||
}
|
||||
if (standaloneComponent === undefined || !isStandaloneComponent(standaloneComponent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const environmentInjector: EnvironmentInjector|null =
|
||||
context.injector.get(EnvironmentInjector, null, {optional: true});
|
||||
// Standalone components should have an environment injector. If one cannot be
|
||||
// found we may be in a test case for low level functionality that did not explictly
|
||||
// setup this injector. In those cases, we simply ignore this event.
|
||||
if (environmentInjector === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {standaloneInjectorToComponent} = frameworkDIDebugData;
|
||||
|
||||
// If our injector has already been mapped, as is the case
|
||||
// when a standalone component imports another standalone component,
|
||||
// we consider the original component (the component doing the importing)
|
||||
// as the component connected to our injector.
|
||||
if (standaloneInjectorToComponent.has(environmentInjector)) {
|
||||
return;
|
||||
}
|
||||
// If our injector hasn't been mapped, then we map it to the standalone component
|
||||
standaloneInjectorToComponent.set(environmentInjector, standaloneComponent);
|
||||
}
|
||||
|
||||
function isStandaloneComponent(value: Type<unknown>): boolean {
|
||||
const def = getComponentDef(value);
|
||||
return !!def?.standalone;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Stores the emitted ProviderRecords from the InjectorProfilerEventType.ProviderConfigured
|
||||
* event in frameworkDIDebugData.resolverToProviders
|
||||
*
|
||||
* @param context InjectorProfilerContext the injection context that this event occurred in.
|
||||
* @param data ProviderRecord an object containing the instance that was just created
|
||||
*
|
||||
*/
|
||||
function handleProviderConfiguredEvent(
|
||||
context: InjectorProfilerContext, data: ProviderRecord): void {
|
||||
const {resolverToProviders} = frameworkDIDebugData;
|
||||
|
||||
const diResolver = getDIResolver(context?.injector);
|
||||
if (diResolver === null) {
|
||||
throwError('A ProviderConfigured event must be run within an injection context.');
|
||||
}
|
||||
|
||||
if (!resolverToProviders.has(diResolver)) {
|
||||
resolverToProviders.set(diResolver, []);
|
||||
}
|
||||
|
||||
resolverToProviders.get(diResolver)!.push(data);
|
||||
}
|
||||
|
||||
function getDIResolver(injector: Injector|undefined): Injector|LView|null {
|
||||
let diResolver: Injector|LView|null = null;
|
||||
|
||||
if (injector === undefined) {
|
||||
return diResolver;
|
||||
}
|
||||
|
||||
// We use the LView as the diResolver for NodeInjectors because they
|
||||
// do not persist anywhere in the framework. They are simply wrappers around an LView and a TNode
|
||||
// that do persist. Because of this, we rely on the LView of the NodeInjector in order to use
|
||||
// as a concrete key to represent this injector. If we get the same LView back later, we know
|
||||
// we're looking at the same injector.
|
||||
if (injector instanceof NodeInjector) {
|
||||
diResolver = getNodeInjectorLView(injector);
|
||||
}
|
||||
// Other injectors can be used a keys for a map because their instances
|
||||
// persist
|
||||
else {
|
||||
diResolver = injector;
|
||||
}
|
||||
|
||||
return diResolver;
|
||||
}
|
||||
|
||||
// inspired by
|
||||
// https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-canbeheldweakly
|
||||
function canBeHeldWeakly(value: any): boolean {
|
||||
// we check for value !== null here because typeof null === 'object
|
||||
return value !== null &&
|
||||
(typeof value === 'object' || typeof value === 'function' || typeof value === 'symbol');
|
||||
}
|
||||
254
packages/core/src/render3/debug/injector_profiler.ts
Normal file
254
packages/core/src/render3/debug/injector_profiler.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {resolveForwardRef} from '../../di/forward_ref';
|
||||
import {InjectionToken} from '../../di/injection_token';
|
||||
import type {Injector} from '../../di/injector';
|
||||
import {InjectFlags, InjectOptions, InternalInjectFlags} from '../../di/interface/injector';
|
||||
import type {SingleProvider} from '../../di/provider_collection';
|
||||
import {Type} from '../../interface/type';
|
||||
import {throwError} from '../../util/assert';
|
||||
|
||||
/**
|
||||
* An enum describing the types of events that can be emitted from the injector profiler
|
||||
*/
|
||||
export const enum InjectorProfilerEventType {
|
||||
/**
|
||||
* Emits when a service is injected.
|
||||
*/
|
||||
Inject,
|
||||
|
||||
/**
|
||||
* Emits when an Angular class instance is created by an injector.
|
||||
*/
|
||||
InstanceCreatedByInjector,
|
||||
|
||||
/**
|
||||
* Emits when an injector configures a provider.
|
||||
*/
|
||||
ProviderConfigured
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that defines an injection context for the injector profiler.
|
||||
*/
|
||||
export interface InjectorProfilerContext {
|
||||
/**
|
||||
* The Injector that service is being injected into.
|
||||
* - Example: if ModuleA --provides--> ServiceA --injects--> ServiceB
|
||||
* then inject(ServiceB) in ServiceA has ModuleA as an injector context
|
||||
*/
|
||||
injector: Injector;
|
||||
|
||||
/**
|
||||
* The class where the constructor that is calling `inject` is located
|
||||
* - Example: if ModuleA --provides--> ServiceA --injects--> ServiceB
|
||||
* then inject(ServiceB) in ServiceA has ServiceA as a construction context
|
||||
*/
|
||||
token: Type<unknown>|null;
|
||||
}
|
||||
|
||||
|
||||
export interface InjectedServiceEvent {
|
||||
type: InjectorProfilerEventType.Inject;
|
||||
context: InjectorProfilerContext;
|
||||
service: InjectedService;
|
||||
}
|
||||
|
||||
export interface InjectorCreatedInstanceEvent {
|
||||
type: InjectorProfilerEventType.InstanceCreatedByInjector;
|
||||
context: InjectorProfilerContext;
|
||||
instance: InjectorCreatedInstance;
|
||||
}
|
||||
|
||||
export interface ProviderConfiguredEvent {
|
||||
type: InjectorProfilerEventType.ProviderConfigured;
|
||||
context: InjectorProfilerContext;
|
||||
providerRecord: ProviderRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object representing an event that is emitted through the injector profiler
|
||||
*/
|
||||
|
||||
export type InjectorProfilerEvent =
|
||||
InjectedServiceEvent|InjectorCreatedInstanceEvent|ProviderConfiguredEvent;
|
||||
|
||||
/**
|
||||
* An object that contains information about a provider that has been configured
|
||||
*/
|
||||
export interface ProviderRecord {
|
||||
/**
|
||||
* DI token that this provider is configuring
|
||||
*/
|
||||
token: Type<unknown>;
|
||||
|
||||
/**
|
||||
* Determines if provider is configured as view provider.
|
||||
*/
|
||||
isViewProvider: boolean;
|
||||
|
||||
/**
|
||||
* The raw provider associated with this ProviderRecord.
|
||||
*/
|
||||
provider: SingleProvider;
|
||||
|
||||
/**
|
||||
* The path of DI containers that were followed to import this provider
|
||||
*/
|
||||
importPath?: Type<unknown>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that contains information about a value that has been constructed within an injector
|
||||
*/
|
||||
export interface InjectorCreatedInstance {
|
||||
/**
|
||||
* Value of the created instance
|
||||
*/
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that contains information a service that has been injected within an
|
||||
* InjectorProfilerContext
|
||||
*/
|
||||
export interface InjectedService {
|
||||
/**
|
||||
* DI token of the Service that is injected
|
||||
*/
|
||||
token?: Type<unknown>|InjectionToken<unknown>;
|
||||
|
||||
/**
|
||||
* Value of the injected service
|
||||
*/
|
||||
value: unknown;
|
||||
|
||||
/**
|
||||
* Flags that this service was injected with
|
||||
*/
|
||||
flags?: InternalInjectFlags|InjectFlags|InjectOptions;
|
||||
|
||||
/**
|
||||
* Injector that this service was provided in.
|
||||
*/
|
||||
providedIn?: Injector;
|
||||
}
|
||||
|
||||
export interface InjectorProfiler {
|
||||
(event: InjectorProfilerEvent): void;
|
||||
}
|
||||
|
||||
let _injectorProfilerContext: InjectorProfilerContext;
|
||||
export function getInjectorProfilerContext() {
|
||||
!ngDevMode && throwError('getInjectorProfilerContext should never be called in production mode');
|
||||
return _injectorProfilerContext;
|
||||
}
|
||||
|
||||
export function setInjectorProfilerContext(context: InjectorProfilerContext) {
|
||||
!ngDevMode && throwError('setInjectorProfilerContext should never be called in production mode');
|
||||
|
||||
const previous = _injectorProfilerContext;
|
||||
_injectorProfilerContext = context;
|
||||
return previous;
|
||||
}
|
||||
|
||||
let injectorProfilerCallback: InjectorProfiler|null = null;
|
||||
|
||||
/**
|
||||
* Sets the callback function which will be invoked during certain DI events within the
|
||||
* runtime (for example: injecting services, creating injectable instances, configuring providers)
|
||||
*
|
||||
* Warning: this function is *INTERNAL* and should not be relied upon in application's code.
|
||||
* The contract of the function might be changed in any release and/or the function can be removed
|
||||
* completely.
|
||||
*
|
||||
* @param profiler function provided by the caller or null value to disable profiling.
|
||||
*/
|
||||
export const setInjectorProfiler = (injectorProfiler: InjectorProfiler|null) => {
|
||||
!ngDevMode && throwError('setInjectorProfiler should never be called in production mode');
|
||||
injectorProfilerCallback = injectorProfiler;
|
||||
};
|
||||
|
||||
/**
|
||||
* Injector profiler function which emits on DI events executed by the runtime.
|
||||
*
|
||||
* @param event InjectorProfilerEvent corresponding to the DI event being emitted
|
||||
*/
|
||||
function injectorProfiler(event: InjectorProfilerEvent): void {
|
||||
!ngDevMode && throwError('Injector profiler should never be called in production mode');
|
||||
|
||||
if (injectorProfilerCallback != null /* both `null` and `undefined` */) {
|
||||
injectorProfilerCallback!(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an InjectorProfilerEventType.ProviderConfigured to the injector profiler. The data in the
|
||||
* emitted event includes the raw provider, as well as the token that provider is providing.
|
||||
*
|
||||
* @param provider A provider object
|
||||
*/
|
||||
export function emitProviderConfiguredEvent(
|
||||
provider: SingleProvider, isViewProvider: boolean = false): void {
|
||||
!ngDevMode && throwError('Injector profiler should never be called in production mode');
|
||||
|
||||
injectorProfiler({
|
||||
type: InjectorProfilerEventType.ProviderConfigured,
|
||||
context: getInjectorProfilerContext(),
|
||||
providerRecord: {
|
||||
token: typeof provider === 'function' ? provider : resolveForwardRef(provider.provide),
|
||||
provider,
|
||||
isViewProvider
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event to the injector profiler with the instance that was created. Note that
|
||||
* the injector associated with this emission can be accessed by using getDebugInjectContext()
|
||||
*
|
||||
* @param instance an object created by an injector
|
||||
*/
|
||||
export function emitInstanceCreatedByInjectorEvent(instance: unknown): void {
|
||||
!ngDevMode && throwError('Injector profiler should never be called in production mode');
|
||||
|
||||
injectorProfiler({
|
||||
type: InjectorProfilerEventType.InstanceCreatedByInjector,
|
||||
context: getInjectorProfilerContext(),
|
||||
instance: {value: instance}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param token DI token associated with injected service
|
||||
* @param value the instance of the injected service (i.e the result of `inject(token)`)
|
||||
* @param flags the flags that the token was injected with
|
||||
*/
|
||||
export function emitInjectEvent(token: Type<unknown>, value: unknown, flags: InjectFlags): void {
|
||||
!ngDevMode && throwError('Injector profiler should never be called in production mode');
|
||||
|
||||
injectorProfiler({
|
||||
type: InjectorProfilerEventType.Inject,
|
||||
context: getInjectorProfilerContext(),
|
||||
service: {token, value, flags}
|
||||
});
|
||||
}
|
||||
|
||||
export function runInInjectorProfilerContext(
|
||||
injector: Injector, token: Type<unknown>, callback: () => void): void {
|
||||
!ngDevMode &&
|
||||
throwError('runInInjectorProfilerContext should never be called in production mode');
|
||||
|
||||
const prevInjectContext = setInjectorProfilerContext({injector, token});
|
||||
try {
|
||||
callback();
|
||||
} finally {
|
||||
setInjectorProfilerContext(prevInjectContext);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,11 +18,12 @@ import {assertDefined, assertEqual, assertIndexInRange} from '../util/assert';
|
|||
import {noSideEffects} from '../util/closure';
|
||||
|
||||
import {assertDirectiveDef, assertNodeInjector, assertTNodeForLView} from './assert';
|
||||
import {emitInstanceCreatedByInjectorEvent, InjectorProfilerContext, runInInjectorProfilerContext, setInjectorProfilerContext} from './debug/injector_profiler';
|
||||
import {getFactoryDef} from './definition_factory';
|
||||
import {throwCyclicDependencyError, throwProviderNotFoundError} from './errors_di';
|
||||
import {NG_ELEMENT_ID, NG_FACTORY_DEF} from './fields';
|
||||
import {registerPreOrderHooks} from './hooks';
|
||||
import {DirectiveDef} from './interfaces/definition';
|
||||
import {ComponentDef, DirectiveDef} from './interfaces/definition';
|
||||
import {isFactory, NO_PARENT_INJECTOR, NodeInjectorFactory, NodeInjectorOffset, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector';
|
||||
import {AttributeMarker, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeProviderIndexes, TNodeType} from './interfaces/node';
|
||||
import {isComponentDef, isComponentHost} from './interfaces/type_checks';
|
||||
|
|
@ -446,7 +447,22 @@ function lookupTokenUsingNodeInjector<T>(
|
|||
lookupTokenUsingModuleInjector<T>(lView, token, flags, notFoundValue);
|
||||
}
|
||||
try {
|
||||
const value = bloomHash(flags);
|
||||
let value: unknown;
|
||||
|
||||
if (ngDevMode) {
|
||||
runInInjectorProfilerContext(
|
||||
new NodeInjector(getCurrentTNode() as TElementNode, getLView()), token as Type<T>,
|
||||
() => {
|
||||
value = bloomHash(flags);
|
||||
|
||||
if (value != null) {
|
||||
emitInstanceCreatedByInjectorEvent(value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
value = bloomHash(flags);
|
||||
}
|
||||
|
||||
if (value == null && !(flags & InjectFlags.Optional)) {
|
||||
throwProviderNotFoundError(token);
|
||||
} else {
|
||||
|
|
@ -618,6 +634,19 @@ export function getNodeInjectable(
|
|||
}
|
||||
const previousIncludeViewProviders = setIncludeViewProviders(factory.canSeeViewProviders);
|
||||
factory.resolving = true;
|
||||
|
||||
let prevInjectContext: InjectorProfilerContext|undefined;
|
||||
if (ngDevMode) {
|
||||
// tData indexes mirror the concrete instances in its corresponding LView.
|
||||
// lView[index] here is either the injectable instace itself or a factory,
|
||||
// therefore tData[index] is the constructor of that injectable or a
|
||||
// definition object that contains the constructor in a `.type` field.
|
||||
const token =
|
||||
(tData[index] as (DirectiveDef<unknown>| ComponentDef<unknown>)).type || tData[index];
|
||||
const injector = new NodeInjector(tNode, lView);
|
||||
prevInjectContext = setInjectorProfilerContext({injector, token});
|
||||
}
|
||||
|
||||
const previousInjectImplementation =
|
||||
factory.injectImpl ? setInjectImplementation(factory.injectImpl) : null;
|
||||
const success = enterDI(lView, tNode, InjectFlags.Default);
|
||||
|
|
@ -627,6 +656,9 @@ export function getNodeInjectable(
|
|||
'Because flags do not contain \`SkipSelf\' we expect this to always succeed.');
|
||||
try {
|
||||
value = lView[index] = factory.factory(undefined, tData, lView, tNode);
|
||||
|
||||
ngDevMode && emitInstanceCreatedByInjectorEvent(value);
|
||||
|
||||
// This code path is hit for both directives and providers.
|
||||
// For perf reasons, we want to avoid searching for hooks on providers.
|
||||
// It does no harm to try (the hooks just won't exist), but the extra
|
||||
|
|
@ -638,6 +670,8 @@ export function getNodeInjectable(
|
|||
registerPreOrderHooks(index, tData[index] as DirectiveDef<any>, tView);
|
||||
}
|
||||
} finally {
|
||||
ngDevMode && setInjectorProfilerContext(prevInjectContext!);
|
||||
|
||||
previousInjectImplementation !== null &&
|
||||
setInjectImplementation(previousInjectImplementation);
|
||||
setIncludeViewProviders(previousIncludeViewProviders);
|
||||
|
|
@ -703,6 +737,16 @@ function shouldSearchParent(flags: InjectFlags, isFirstHostTNode: boolean): bool
|
|||
return !(flags & InjectFlags.Self) && !(flags & InjectFlags.Host && isFirstHostTNode);
|
||||
}
|
||||
|
||||
export function getNodeInjectorLView(nodeInjector: NodeInjector): LView {
|
||||
return (nodeInjector as any)._lView as LView;
|
||||
}
|
||||
|
||||
export function getNodeInjectorTNode(nodeInjector: NodeInjector): TElementNode|TContainerNode|
|
||||
TElementContainerNode|null {
|
||||
return (nodeInjector as any)._tNode as TElementNode | TContainerNode | TElementContainerNode |
|
||||
null;
|
||||
}
|
||||
|
||||
export class NodeInjector implements Injector {
|
||||
constructor(
|
||||
private _tNode: TElementNode|TContainerNode|TElementContainerNode|null,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@
|
|||
|
||||
import {resolveForwardRef} from '../di/forward_ref';
|
||||
import {ClassProvider, Provider} from '../di/interface/provider';
|
||||
import {isClassProvider, isTypeProvider} from '../di/provider_collection';
|
||||
import {isClassProvider, isTypeProvider, SingleProvider} from '../di/provider_collection';
|
||||
import {providerToFactory} from '../di/r3_injector';
|
||||
import {assertDefined} from '../util/assert';
|
||||
|
||||
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from './di';
|
||||
import {emitProviderConfiguredEvent, runInInjectorProfilerContext} from './debug/injector_profiler';
|
||||
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode, NodeInjector} from './di';
|
||||
import {ɵɵdirectiveInject} from './instructions/all';
|
||||
import {DirectiveDef} from './interfaces/definition';
|
||||
import {NodeInjectorFactory} from './interfaces/injector';
|
||||
|
|
@ -74,10 +75,18 @@ function resolveProvider(
|
|||
} else {
|
||||
const tView = getTView();
|
||||
const lView = getLView();
|
||||
let token: any = isTypeProvider(provider) ? provider : resolveForwardRef(provider.provide);
|
||||
let providerFactory: () => any = providerToFactory(provider);
|
||||
|
||||
const tNode = getCurrentTNode()!;
|
||||
let token: any = isTypeProvider(provider) ? provider : resolveForwardRef(provider.provide);
|
||||
|
||||
const providerFactory = providerToFactory(provider);
|
||||
if (ngDevMode) {
|
||||
const injector =
|
||||
new NodeInjector(tNode as TElementNode | TContainerNode | TElementContainerNode, lView);
|
||||
runInInjectorProfilerContext(injector, token, () => {
|
||||
emitProviderConfiguredEvent(provider as SingleProvider, isViewProvider);
|
||||
});
|
||||
}
|
||||
|
||||
const beginIndex = tNode.providerIndexes & TNodeProviderIndexes.ProvidersStartIndexMask;
|
||||
const endIndex = tNode.directiveStart;
|
||||
const cptViewProvidersCount =
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import {InjectFlags, resolveForwardRef} from '../../di';
|
|||
import {assertInjectImplementationNotEqual} from '../../di/inject_switch';
|
||||
import {ɵɵinject} from '../../di/injector_compatibility';
|
||||
import {ProviderToken} from '../../di/provider_token';
|
||||
import {Type} from '../../interface/type';
|
||||
import {emitInjectEvent} from '../debug/injector_profiler';
|
||||
import {getOrCreateInjectable} from '../di';
|
||||
import {TDirectiveHostNode} from '../interfaces/node';
|
||||
import {getCurrentTNode, getLView} from '../state';
|
||||
|
|
@ -49,8 +51,10 @@ export function ɵɵdirectiveInject<T>(token: ProviderToken<T>, flags = InjectFl
|
|||
return ɵɵinject(token, flags);
|
||||
}
|
||||
const tNode = getCurrentTNode();
|
||||
return getOrCreateInjectable<T>(
|
||||
tNode as TDirectiveHostNode, lView, resolveForwardRef(token), flags);
|
||||
const value =
|
||||
getOrCreateInjectable<T>(tNode as TDirectiveHostNode, lView, resolveForwardRef(token), flags);
|
||||
ngDevMode && emitInjectEvent(token as Type<unknown>, value, flags);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -11,14 +11,16 @@ import {setInjectImplementation} from '../di/inject_switch';
|
|||
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {Type} from '../interface/type';
|
||||
|
||||
import {InjectorProfilerContext, setInjectorProfilerContext} from './debug/injector_profiler';
|
||||
import {getFactoryDef} from './definition_factory';
|
||||
import {setIncludeViewProviders} from './di';
|
||||
import {NodeInjector, setIncludeViewProviders} from './di';
|
||||
import {store, ɵɵdirectiveInject} from './instructions/all';
|
||||
import {isHostComponentStandalone} from './instructions/element_validation';
|
||||
import {PipeDef, PipeDefList} from './interfaces/definition';
|
||||
import {TTextNode} from './interfaces/node';
|
||||
import {CONTEXT, DECLARATION_COMPONENT_VIEW, HEADER_OFFSET, LView, TVIEW} from './interfaces/view';
|
||||
import {pureFunction1Internal, pureFunction2Internal, pureFunction3Internal, pureFunction4Internal, pureFunctionVInternal} from './pure_function';
|
||||
import {getBindingRoot, getLView, getTView} from './state';
|
||||
import {getBindingRoot, getCurrentTNode, getLView, getTView} from './state';
|
||||
import {load} from './util/view_utils';
|
||||
|
||||
|
||||
|
|
@ -50,6 +52,14 @@ export function ɵɵpipe(index: number, pipeName: string): any {
|
|||
}
|
||||
|
||||
const pipeFactory = pipeDef.factory || (pipeDef.factory = getFactoryDef(pipeDef.type, true));
|
||||
|
||||
let previousInjectorProfilerContext: InjectorProfilerContext;
|
||||
if (ngDevMode) {
|
||||
previousInjectorProfilerContext = setInjectorProfilerContext({
|
||||
injector: new NodeInjector(getCurrentTNode() as TTextNode, getLView()),
|
||||
token: pipeDef.type
|
||||
});
|
||||
}
|
||||
const previousInjectImplementation = setInjectImplementation(ɵɵdirectiveInject);
|
||||
try {
|
||||
// DI for pipes is supposed to behave like directives when placed on a component
|
||||
|
|
@ -63,6 +73,7 @@ export function ɵɵpipe(index: number, pipeName: string): any {
|
|||
// we have to restore the injector implementation in finally, just in case the creation of the
|
||||
// pipe throws an error.
|
||||
setInjectImplementation(previousInjectImplementation);
|
||||
ngDevMode && setInjectorProfilerContext(previousInjectorProfilerContext!);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue