refactor(core): Add injector debug information to `LViewDebug` (#38707)

Extended the `LViewDebug` to display node-injector information for each
node.

PR Close #38707
This commit is contained in:
Misko Hevery 2020-09-04 16:55:51 -07:00 committed by Alex Rickabaugh
parent 9fb541787c
commit b2579d43cd
7 changed files with 316 additions and 37 deletions

View File

@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertDefined, assertEqual, throwError} from '../util/assert';
import {assertDefined, assertEqual, assertIndexInRange, assertNumber, throwError} from '../util/assert';
import {getComponentDef, getNgModuleDef} from './definition';
import {LContainer} from './interfaces/container';
import {DirectiveDef} from './interfaces/definition';
import { PARENT_INJECTOR } from './interfaces/injector';
import {TNode} from './interfaces/node';
import {isLContainer, isLView} from './interfaces/type_checks';
import {LView, TVIEW, TView} from './interfaces/view';
import {HEADER_OFFSET, LView, TVIEW, TView} from './interfaces/view';
// [Assert functions do not constraint type when they are guarded by a truthy
// expression.](https://github.com/microsoft/TypeScript/issues/37295)
@ -96,3 +96,55 @@ export function assertDirectiveDef<T>(obj: any): asserts obj is DirectiveDef<T>
`Expected a DirectiveDef/ComponentDef and this object does not seem to have the expected shape.`);
}
}
export function assertIndexInDeclRange(lView: LView, index: number) {
const tView = lView[1];
assertBetween(HEADER_OFFSET, tView.bindingStartIndex, index);
}
export function assertIndexInVarsRange(lView: LView, index: number) {
const tView = lView[1];
assertBetween(
tView.bindingStartIndex, (tView as any as {i18nStartIndex: number}).i18nStartIndex, index);
}
export function assertIndexInI18nRange(lView: LView, index: number) {
const tView = lView[1];
assertBetween(
(tView as any as {i18nStartIndex: number}).i18nStartIndex, tView.expandoStartIndex, index);
}
export function assertIndexInExpandoRange(lView: LView, index: number) {
const tView = lView[1];
assertBetween(tView.expandoStartIndex, lView.length, index);
}
export function assertBetween(lower: number, upper: number, index: number) {
if (!(lower <= index && index < upper)) {
throwError(`Index out of range (expecting ${lower} <= ${index} < ${upper})`);
}
}
/**
* This is a basic sanity check that the `injectorIndex` seems to point to what looks like a
* NodeInjector data structure.
*
* @param lView `LView` which should be checked.
* @param injectorIndex index into the `LView` where the `NodeInjector` is expected.
*/
export function assertNodeInjector(lView: LView, injectorIndex: number) {
assertIndexInExpandoRange(lView, injectorIndex);
assertIndexInExpandoRange(lView, injectorIndex + PARENT_INJECTOR);
assertNumber(lView[injectorIndex + 0], 'injectorIndex should point to a bloom filter');
assertNumber(lView[injectorIndex + 1], 'injectorIndex should point to a bloom filter');
assertNumber(lView[injectorIndex + 2], 'injectorIndex should point to a bloom filter');
assertNumber(lView[injectorIndex + 3], 'injectorIndex should point to a bloom filter');
assertNumber(lView[injectorIndex + 4], 'injectorIndex should point to a bloom filter');
assertNumber(lView[injectorIndex + 5], 'injectorIndex should point to a bloom filter');
assertNumber(lView[injectorIndex + 6], 'injectorIndex should point to a bloom filter');
assertNumber(lView[injectorIndex + 7], 'injectorIndex should point to a bloom filter');
assertNumber(
lView[injectorIndex + 8 /*PARENT_INJECTOR*/],
'injectorIndex should point to parent injector');
}

View File

@ -32,7 +32,7 @@ import {DirectiveDef} from '../interfaces/definition';
* ɵɵtextInterpolate(ctx.greeter.greet());
* }
* },
* features: [ProvidersFeature([GreeterDE])]
* features: [ɵɵProvidersFeature([GreeterDE])]
* });
* }
* ```

View File

@ -6,21 +6,25 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, SchemaMetadata} from '../../core';
import {Injector, SchemaMetadata, Type} from '../../core';
import {Sanitizer} from '../../sanitization/sanitizer';
import {KeyValueArray} from '../../util/array_utils';
import {assertDefined} from '../../util/assert';
import {createNamedArrayType} from '../../util/named_array_type';
import {initNgDevMode} from '../../util/ng_dev_mode';
import {assertNodeInjector} from '../assert';
import {getInjectorIndex} from '../di';
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container';
import {ComponentTemplate, DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition';
import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition';
import {NO_PARENT_INJECTOR, PARENT_INJECTOR, TNODE} from '../interfaces/injector';
import {AttributeMarker, PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TNodeTypeAsString, TViewNode} from '../interfaces/node';
import {SelectorFlags} from '../interfaces/projection';
import {LQueries, TQueries} from '../interfaces/query';
import {RComment, RElement, Renderer3, RendererFactory3, RNode} from '../interfaces/renderer';
import {getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate, TStylingKey, TStylingRange} from '../interfaces/styling';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, ExpandoInstructions, FLAGS, HEADER_OFFSET, HookData, HOST, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType} from '../interfaces/view';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, ExpandoInstructions, FLAGS, HEADER_OFFSET, HookData, HOST, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType, TViewTypeAsString} from '../interfaces/view';
import {attachDebugObject} from '../util/debug_utils';
import {getParentInjectorIndex, getParentInjectorView} from '../util/injector_utils';
import {unwrapRNode} from '../util/view_utils';
const NG_DEV_MODE = ((typeof ngDevMode === 'undefined' || !!ngDevMode) && initNgDevMode());
@ -152,6 +156,14 @@ export const TViewConstructor = class TView implements ITView {
processTNodeChildren(this.firstChild, buf);
return buf.join('');
}
get type_(): string {
return TViewTypeAsString[this.type] || `TViewType.?${this.type}?`;
}
get i18nStartIndex(): number {
return HEADER_OFFSET + this._decls + this._vars;
}
};
class TNode implements ITNode {
@ -189,23 +201,39 @@ class TNode implements ITNode {
public styleBindings: TStylingRange, //
) {}
get type_(): string {
switch (this.type) {
case TNodeType.Container:
return 'TNodeType.Container';
case TNodeType.Element:
return 'TNodeType.Element';
case TNodeType.ElementContainer:
return 'TNodeType.ElementContainer';
case TNodeType.IcuContainer:
return 'TNodeType.IcuContainer';
case TNodeType.Projection:
return 'TNodeType.Projection';
case TNodeType.View:
return 'TNodeType.View';
default:
return 'TNodeType.???';
/**
* Return a human debug version of the set of `NodeInjector`s which will be consulted when
* resolving tokens from this `TNode`.
*
* When debugging applications, it is often difficult to determine which `NodeInjector`s will be
* consulted. This method shows a list of `DebugNode`s representing the `TNode`s which will be
* consulted in order when resolving a token starting at this `TNode`.
*
* The original data is stored in `LView` and `TView` with a lot of offset indexes, and so it is
* difficult to reason about.
*
* @param lView The `LView` instance for this `TNode`.
*/
debugNodeInjectorPath(lView: LView): DebugNode[] {
const path: DebugNode[] = [];
let injectorIndex = getInjectorIndex(this, lView);
ngDevMode && assertNodeInjector(lView, injectorIndex);
while (injectorIndex !== -1) {
const tNode = lView[TVIEW].data[injectorIndex + TNODE] as TNode;
path.push(buildDebugNode(tNode, lView));
const parentLocation = lView[injectorIndex + PARENT_INJECTOR];
if (parentLocation === NO_PARENT_INJECTOR) {
injectorIndex = -1;
} else {
injectorIndex = getParentInjectorIndex(parentLocation);
lView = getParentInjectorView(parentLocation, lView);
}
}
return path;
}
get type_(): string {
return TNodeTypeAsString[this.type] || `TNodeType.?${this.type}?`;
}
get flags_(): string {
@ -246,6 +274,14 @@ class TNode implements ITNode {
get classBindings_(): DebugStyleBindings {
return toDebugStyleBinding(this, true);
}
get providerIndexStart_(): number {
return this.providerIndexes & TNodeProviderIndexes.ProvidersStartIndexMask;
}
get providerIndexEnd_(): number {
return this.providerIndexStart_ +
(this.providerIndexes >>> TNodeProviderIndexes.CptViewProvidersCountShift);
}
}
export const TNodeDebug = TNode;
export type TNodeDebug = TNode;
@ -462,21 +498,21 @@ export class LViewDebug implements ILViewDebug {
}
get decls(): LViewDebugRange {
const tView = this.tView as any as {_decls: number, _vars: number};
const start = HEADER_OFFSET;
return toLViewRange(this.tView, this._raw_lView, start, start + tView._decls);
return toLViewRange(this.tView, this._raw_lView, HEADER_OFFSET, this.tView.bindingStartIndex);
}
get vars(): LViewDebugRange {
const tView = this.tView as any as {_decls: number, _vars: number};
const start = HEADER_OFFSET + tView._decls;
return toLViewRange(this.tView, this._raw_lView, start, start + tView._vars);
const tView = this.tView;
return toLViewRange(
tView, this._raw_lView, tView.bindingStartIndex,
(tView as any as {i18nStartIndex: number}).i18nStartIndex);
}
get i18n(): LViewDebugRange {
const tView = this.tView as any as {_decls: number, _vars: number};
const start = HEADER_OFFSET + tView._decls + tView._vars;
return toLViewRange(this.tView, this._raw_lView, start, this.tView.expandoStartIndex);
const tView = this.tView;
return toLViewRange(
tView, this._raw_lView, (tView as any as {i18nStartIndex: number}).i18nStartIndex,
tView.expandoStartIndex);
}
get expando(): LViewDebugRange {
@ -518,7 +554,7 @@ export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[] {
const debugNodes: DebugNode[] = [];
let tNodeCursor: ITNode|null = tNode;
while (tNodeCursor) {
debugNodes.push(buildDebugNode(tNodeCursor, lView, tNodeCursor.index));
debugNodes.push(buildDebugNode(tNodeCursor, lView));
tNodeCursor = tNodeCursor.next;
}
return debugNodes;
@ -527,17 +563,75 @@ export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[] {
}
}
export function buildDebugNode(tNode: ITNode, lView: LView, nodeIndex: number): DebugNode {
const rawValue = lView[nodeIndex];
export function buildDebugNode(tNode: ITNode, lView: LView): DebugNode {
const rawValue = lView[tNode.index];
const native = unwrapRNode(rawValue);
const factories: Type<any>[] = [];
const instances: any[] = [];
const tView = lView[TVIEW];
for (let i = tNode.directiveStart; i < tNode.directiveEnd; i++) {
const def = tView.data[i] as DirectiveDef<any>;
factories.push(def.type);
instances.push(lView[i]);
}
return {
html: toHtml(native),
type: TNodeTypeAsString[tNode.type],
native: native as any,
children: toDebugNodes(tNode.child, lView),
factories,
instances,
injector: buildNodeInjectorDebug(tNode, tView, lView)
};
}
function buildNodeInjectorDebug(tNode: ITNode, tView: ITView, lView: LView) {
const viewProviders: Type<any>[] = [];
for (let i = (tNode as TNode).providerIndexStart_; i < (tNode as TNode).providerIndexEnd_; i++) {
viewProviders.push(tView.data[i] as Type<any>);
}
const providers: Type<any>[] = [];
for (let i = (tNode as TNode).providerIndexEnd_; i < (tNode as TNode).directiveEnd; i++) {
providers.push(tView.data[i] as Type<any>);
}
const nodeInjectorDebug = {
bloom: toBloom(lView, tNode.injectorIndex),
cumulativeBloom: toBloom(tView.data, tNode.injectorIndex),
providers,
viewProviders,
parentInjectorIndex: lView[(tNode as TNode).providerIndexStart_ - 1],
};
return nodeInjectorDebug;
}
/**
* Convert a number at `idx` location in `array` into binary representation.
*
* @param array
* @param idx
*/
function binary(array: any[], idx: number): string {
const value = array[idx];
// If not a number we print 8 `?` to retain alignment but let user know that it was called on
// wrong type.
if (typeof value !== 'number') return '????????';
// We prefix 0s so that we have constant length number
const text = '00000000' + value.toString(2);
return text.substring(text.length - 8);
}
/**
* Convert a bloom filter at location `idx` in `array` into binary representation.
*
* @param array
* @param idx
*/
function toBloom(array: any[], idx: number): string {
return `${binary(array, idx + 7)}_${binary(array, idx + 6)}_${binary(array, idx + 5)}_${
binary(array, idx + 4)}_${binary(array, idx + 3)}_${binary(array, idx + 2)}_${
binary(array, idx + 1)}_${binary(array, idx + 0)}`;
}
export class LContainerDebug implements ILContainerDebug {
constructor(private readonly _raw_lContainer: LContainer) {}

View File

@ -9,6 +9,7 @@
import {InjectionToken} from '../../di/injection_token';
import {InjectFlags} from '../../di/interface/injector';
import {Type} from '../../interface/type';
import {assertDefined, assertEqual} from '../../util/assert';
import {TDirectiveHostNode} from './node';
import {LView, TData} from './view';
@ -239,6 +240,8 @@ export class NodeInjectorFactory {
isViewProvider: boolean,
injectImplementation: null|
(<T>(token: Type<T>|InjectionToken<T>, flags?: InjectFlags) => T)) {
ngDevMode && assertDefined(factory, 'Factory not specified');
ngDevMode && assertEqual(typeof factory, 'function', 'Expected factory function.');
this.canSeeViewProviders = isViewProvider;
this.injectImpl = injectImplementation;
}

View File

@ -1021,4 +1021,48 @@ export interface DebugNode {
* Child nodes
*/
children: DebugNode[];
/**
* A list of Component/Directive types which need to be instantiated an this location.
*/
factories: Type<unknown>[];
/**
* A list of Component/Directive instances which were instantiated an this location.
*/
instances: unknown[];
/**
* NodeInjector information.
*/
injector: NodeInjectorDebug;
}
interface NodeInjectorDebug {
/**
* Instance bloom. Does the current injector have a provider with a given bloom mask.
*/
bloom: string;
/**
* Cumulative bloom. Do any of the above injectors have a provider with a given bloom mask.
*/
cumulativeBloom: string;
/**
* A list of providers associated with this injector.
*/
providers: (Type<unknown>|DirectiveDef<unknown>|ComponentDef<unknown>)[];
/**
* A list of providers associated with this injector visible to the view of the component only.
*/
viewProviders: Type<unknown>[];
/**
* Location of the parent `TNode`.
*/
parentInjectorIndex: number;
}

View File

@ -389,7 +389,7 @@ export function getDebugNode(element: Element): DebugNode|null {
// data. In this situation the TNode is not accessed at the same spot.
const tNode = isLView(valueInLView) ? (valueInLView[T_HOST] as TNode) :
getTNode(lView[TVIEW], nodeIndex - HEADER_OFFSET);
debugNode = buildDebugNode(tNode, lView, nodeIndex);
debugNode = buildDebugNode(tNode, lView);
}
return debugNode;

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵProvidersFeature} from '@angular/core/src/core';
import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart} from '@angular/core/src/render3/instructions/element';
import {TNodeDebug} from '@angular/core/src/render3/instructions/lview_debug';
import {createTNode, createTView} from '@angular/core/src/render3/instructions/shared';
import {TNodeType} from '@angular/core/src/render3/interfaces/node';
@ -13,7 +15,7 @@ import {LView, TView, TViewType} from '@angular/core/src/render3/interfaces/view
import {enterView, leaveView} from '@angular/core/src/render3/state';
import {insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list';
import {KeyValueArray} from '@angular/core/src/util/array_utils';
import {TemplateFixture} from '../render_util';
describe('lView_debug', () => {
const mockFirstUpdatePassLView: LView = [null, {firstUpdatePass: true}] as any;
@ -151,4 +153,88 @@ describe('lView_debug', () => {
});
});
});
describe('di', () => {
it('should show basic information', () => {
class DepA {
static ɵfac = () => new DepA();
}
class DepB {
static ɵfac = () => new DepB();
}
const instances: any[] = [];
class MyComponent {
constructor(public depA: DepA, public depB: DepB) {
instances.push(this);
}
static ɵfac = () => new MyComponent(ɵɵdirectiveInject(DepA), ɵɵdirectiveInject(DepB));
static ɵcmp = ɵɵdefineComponent({
type: MyComponent,
selectors: [['my-comp']],
decls: 1,
vars: 0,
template: function() {},
features: [ɵɵProvidersFeature(
[DepA, {provide: String, useValue: 'String'}],
[DepB, {provide: Number, useValue: 123}])]
});
}
let myChild!: MyChild;
class MyChild {
constructor() {
myChild = this;
}
static ɵfac = () => new MyChild();
static ɵdir = ɵɵdefineDirective({
type: MyChild,
selectors: [['my-child']],
});
}
class MyDirective {
constructor(public myComp: MyComponent) {
instances.push(this);
}
static ɵfac = () => new MyDirective(ɵɵdirectiveInject(MyComponent));
static ɵdir = ɵɵdefineDirective({
type: MyDirective,
selectors: [['', 'my-dir', '']],
});
}
const fixture = new TemplateFixture(
() => {
ɵɵelementStart(0, 'my-comp', 0);
ɵɵelement(1, 'my-child');
ɵɵelementEnd();
},
() => null, 2, 0, [MyComponent, MyDirective, MyChild], null, null, undefined,
[['my-dir', '']]);
const lView = fixture.hostView;
const lViewDebug = lView.debug!;
const myCompNode = lViewDebug.nodes[0];
expect(myCompNode.factories).toEqual([MyComponent, MyDirective]);
expect(myCompNode.instances).toEqual(instances);
expect(myCompNode.injector).toEqual({
bloom: jasmine.anything(),
cumulativeBloom: jasmine.anything(),
providers: [DepA, String, MyComponent.ɵcmp, MyDirective.ɵdir],
viewProviders: [DepB, Number],
parentInjectorIndex: -1,
});
const myChildNode = myCompNode.children[0];
expect(myChildNode.factories).toEqual([MyChild]);
expect(myChildNode.instances).toEqual([myChild]);
expect(myChildNode.injector).toEqual({
bloom: jasmine.anything(),
cumulativeBloom: jasmine.anything(),
providers: [MyChild.ɵdir],
viewProviders: [],
parentInjectorIndex: 22,
});
});
});
});