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('');
+ expect(myCompView.nodes ![0].html).toEqual('');
+ expect(myCompView.nodes ![0].nodes ![0].html).toEqual('Hello World');
+ });
+});
\ No newline at end of file