From 22880eae16ed588e7e56fb1e7a843e3cb47ecd08 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Sat, 23 Feb 2019 09:12:33 -0800 Subject: [PATCH] feat(ivy): add debug view of internal deta structures (#28945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change contains conditionally attached classes which provide human readable (debug) level information for `LView`, `LContainer` and other internal data structures. These data structures are stored internally as array which makes it very difficult during debugging to reason about the current state of the system. Patching the array with extra property does change the array's hidden class' but it does not change the cost of access, therefore this patching should not have significant if any impact in `ngDevMode` mode. (see: https://jsperf.com/array-vs-monkey-patch-array) So instead of seeing: ``` Array(30) [Object, 659, null, …] ``` ``` LViewDebug { views: [...], flags: {attached: true, ...} nodes: [ {html: '
', ..., nodes: [ {html: '', ..., nodes: null} ]} ] } ``` PR Close #28945 --- packages/core/src/render3/debug.ts | 232 ++++++++++++++++++++++ packages/core/src/render3/instructions.ts | 6 +- packages/core/test/render3/debug_spec.ts | 42 ++++ 3 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/render3/debug.ts create mode 100644 packages/core/test/render3/debug_spec.ts diff --git a/packages/core/src/render3/debug.ts b/packages/core/src/render3/debug.ts new file mode 100644 index 0000000000..7bd49431a8 --- /dev/null +++ b/packages/core/src/render3/debug.ts @@ -0,0 +1,232 @@ +/** + * @license + * Copyright Google Inc. 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 {assertDefined} from '../util/assert'; + +import {ACTIVE_INDEX, LContainer, NATIVE, VIEWS} from './interfaces/container'; +import {TNode} from './interfaces/node'; +import {LQueries} from './interfaces/query'; +import {RComment, RElement} from './interfaces/renderer'; +import {StylingContext} from './interfaces/styling'; +import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TVIEW, TView, T_HOST} from './interfaces/view'; +import {readElementValue} from './util/view_utils'; + +/* + * This file contains conditionally attached classes which provide human readable (debug) level + * information for `LView`, `LContainer` and other internal data structures. These data structures + * are stored internally as array which makes it very difficult during debugging to reason about the + * current state of the system. + * + * Patching the array with extra property does change the array's hidden class' but it does not + * change the cost of access, therefore this patching should not have significant if any impact in + * `ngDevMode` mode. (see: https://jsperf.com/array-vs-monkey-patch-array) + * + * So instead of seeing: + * ``` + * Array(30) [Object, 659, null, …] + * ``` + * + * You get to see: + * ``` + * LViewDebug { + * views: [...], + * flags: {attached: true, ...} + * nodes: [ + * {html: '
', ..., nodes: [ + * {html: '', ..., nodes: null} + * ]} + * ] + * } + * ``` + */ + + +export function attachLViewDebug(lView: LView) { + (lView as any).debug = new LViewDebug(lView); +} + +export function attachLContainerDebug(lContainer: LContainer) { + (lContainer as any).debug = new LContainerDebug(lContainer); +} + +export function toDebug(obj: LView): LViewDebug; +export function toDebug(obj: LView | null): LViewDebug|null; +export function toDebug(obj: LView | LContainer | null): LViewDebug|LContainerDebug|null; +export function toDebug(obj: any): any { + if (obj) { + const debug = (obj as any).debug; + assertDefined(debug, 'Object does not have a debug representation.'); + return debug; + } else { + return obj; + } +} + +/** + * Use this method to unwrap a native element in `LView` and convert it into HTML for easier + * reading. + * + * @param value possibly wrapped native DOM node. + * @param includeChildren If `true` then the serialized HTML form will include child elements (same + * as `outerHTML`). If `false` then the serialized HTML form will only contain the element itself + * (will not serialize child elements). + */ +function toHtml(value: any, includeChildren: boolean = false): string|null { + const node: HTMLElement|null = readElementValue(value) as any; + if (node) { + const isTextNode = node.nodeType === Node.TEXT_NODE; + const outerHTML = (isTextNode ? node.textContent : node.outerHTML) || ''; + if (includeChildren || isTextNode) { + return outerHTML; + } else { + const innerHTML = node.innerHTML; + return outerHTML.split(innerHTML)[0] || null; + } + } else { + return null; + } +} + +export class LViewDebug { + constructor(private readonly _raw_lView: LView) {} + + /** + * Flags associated with the `LView` unpacked into a more readable state. + */ + get flags() { + const flags = this._raw_lView[FLAGS]; + return { + __raw__flags__: flags, + initPhaseState: flags & LViewFlags.InitPhaseStateMask, + creationMode: !!(flags & LViewFlags.CreationMode), + firstViewPass: !!(flags & LViewFlags.FirstLViewPass), + checkAlways: !!(flags & LViewFlags.CheckAlways), + dirty: !!(flags & LViewFlags.Dirty), + attached: !!(flags & LViewFlags.Attached), + destroyed: !!(flags & LViewFlags.Destroyed), + isRoot: !!(flags & LViewFlags.IsRoot), + indexWithinInitPhase: flags >> LViewFlags.IndexWithinInitPhaseShift, + }; + } + get parent(): LViewDebug|LContainerDebug|null { return toDebug(this._raw_lView[PARENT]); } + get host(): string|null { return toHtml(this._raw_lView[HOST], true); } + get context(): {}|null { return this._raw_lView[CONTEXT]; } + /** + * The tree of nodes associated with the current `LView`. The nodes have been normalized into a + * tree structure with relevant details pulled out for readability. + */ + get nodes(): DebugNode[]|null { + const lView = this._raw_lView; + const tNode = lView[TVIEW].firstChild; + return toDebugNodes(tNode, lView); + } + /** + * Additional information which is hidden behind a property. The extra level of indirection is + * done so that the debug view would not be cluttered with properties which are only rarely + * relevant to the developer. + */ + get __other__() { + return { + tView: this._raw_lView[TVIEW], + cleanup: this._raw_lView[CLEANUP], + injector: this._raw_lView[INJECTOR], + rendererFactory: this._raw_lView[RENDERER_FACTORY], + renderer: this._raw_lView[RENDERER], + sanitizer: this._raw_lView[SANITIZER], + childHead: toDebug(this._raw_lView[CHILD_HEAD]), + next: toDebug(this._raw_lView[NEXT]), + childTail: toDebug(this._raw_lView[CHILD_TAIL]), + declarationView: toDebug(this._raw_lView[DECLARATION_VIEW]), + contentQueries: this._raw_lView[CONTENT_QUERIES], + queries: this._raw_lView[QUERIES], + tHost: this._raw_lView[T_HOST], + bindingIndex: this._raw_lView[BINDING_INDEX], + }; + } + + /** + * Normalized view of child views (and containers) attached at this location. + */ + get childViews(): Array { + const childViews: Array = []; + let child = this.__other__.childHead; + while (child) { + childViews.push(child); + child = child.__other__.next; + } + return childViews; + } +} + +export interface DebugNode { + html: string|null; + native: Node; + nodes: DebugNode[]|null; + component: LViewDebug|null; +} + +/** + * Turns a flat list of nodes into a tree by walking the associated `TNode` tree. + * + * @param tNode + * @param lView + */ +export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|null { + if (tNode) { + const debugNodes: DebugNode[] = []; + let tNodeCursor: TNode|null = tNode; + while (tNodeCursor) { + const rawValue = lView[tNode.index]; + const native = readElementValue(rawValue); + const componentLViewDebug = toDebug(readLViewValue(rawValue)); + debugNodes.push({ + html: toHtml(native), + native: native as any, + nodes: toDebugNodes(tNode.child, lView), + component: componentLViewDebug + }); + tNodeCursor = tNodeCursor.next; + } + return debugNodes; + } else { + return null; + } +} + +export class LContainerDebug { + constructor(private readonly _raw_lContainer: LContainer) {} + + get activeIndex(): number { return this._raw_lContainer[ACTIVE_INDEX]; } + get views(): LViewDebug[] { + return this._raw_lContainer[VIEWS].map(toDebug as(l: LView) => LViewDebug); + } + get parent(): LViewDebug|LContainerDebug|null { return toDebug(this._raw_lContainer[PARENT]); } + get queries(): LQueries|null { return this._raw_lContainer[QUERIES]; } + get host(): RElement|RComment|StylingContext|LView { return this._raw_lContainer[HOST]; } + get native(): RComment { return this._raw_lContainer[NATIVE]; } + get __other__() { + return { + next: toDebug(this._raw_lContainer[NEXT]), + }; + } +} + +/** + * Return an `LView` value if found. + * + * @param value `LView` if any + */ +export function readLViewValue(value: any): LView|null { + while (Array.isArray(value)) { + // This check is not quite right, as it does not take into account `StylingContext` + // This is why it is in debug, not in util.ts + if (value.length >= HEADER_OFFSET - 1) return value as LView; + value = value[HOST]; + } + return null; +} diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 191260dc02..eed6a49ee8 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -21,6 +21,7 @@ import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../util/ng_ import {assertHasParent, assertLContainerOrUndefined, assertLView, assertPreviousIsParent} from './assert'; import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from './bindings'; import {attachPatchData, getComponentViewByInstance} from './context_discovery'; +import {attachLContainerDebug, attachLViewDebug} from './debug'; import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreateNodeInjectorForNode, injectAttributeImpl} from './di'; import {throwMultipleComponentError} from './errors'; import {executeHooks, executeInitHooks, registerPostOrderHooks, registerPreOrderHooks} from './hooks'; @@ -187,6 +188,7 @@ export function createLView( lView[INJECTOR as any] = injector || parentLView && parentLView[INJECTOR] || null; lView[HOST] = host; lView[T_HOST] = tHostNode; + ngDevMode && attachLViewDebug(lView); return lView; } @@ -2213,7 +2215,7 @@ export function createLContainer( isForViewContainerRef?: boolean): LContainer { ngDevMode && assertDomNode(native); ngDevMode && assertLView(currentView); - return [ + const lContainer: LContainer = [ isForViewContainerRef ? -1 : 0, // active index [], // views currentView, // parent @@ -2222,6 +2224,8 @@ export function createLContainer( hostNative, // host native native, // native ]; + ngDevMode && attachLContainerDebug(lContainer); + return lContainer; } /** diff --git a/packages/core/test/render3/debug_spec.ts b/packages/core/test/render3/debug_spec.ts new file mode 100644 index 0000000000..a1492993c0 --- /dev/null +++ b/packages/core/test/render3/debug_spec.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright Google Inc. 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 {RenderFlags, defineComponent, elementEnd, elementStart, text} from '@angular/core/src/render3'; +import {getLContext} from '@angular/core/src/render3/context_discovery'; +import {LViewDebug, toDebug} from '@angular/core/src/render3/debug'; + +import {ComponentFixture} from './render_util'; + +describe('Debug Representation', () => { + it('should generate a human readable version', () => { + class MyComponent { + static ngComponentDef = defineComponent({ + type: MyComponent, + selectors: [['my-comp']], + vars: 0, + consts: 2, + factory: () => new MyComponent(), + template: function(rf: RenderFlags, ctx: MyComponent) { + if (rf == RenderFlags.Create) { + elementStart(0, 'div', ['id', '123']); + text(1, 'Hello World'); + elementEnd(); + } + } + }); + } + + const fixture = new ComponentFixture(MyComponent); + const hostView = toDebug(getLContext(fixture.component) !.lView); + expect(hostView.host).toEqual(null); + const myCompView = hostView.childViews[0] as LViewDebug; + expect(myCompView.host).toEqual('
Hello World
'); + expect(myCompView.nodes ![0].html).toEqual('
'); + expect(myCompView.nodes ![0].nodes ![0].html).toEqual('Hello World'); + }); +}); \ No newline at end of file