Skip to content

Commit

Permalink
Merge pull request #1189 from glimmerjs/upstream/debug-render-tree
Browse files Browse the repository at this point in the history
[UPSTREAM] DebugRenderTree
  • Loading branch information
Chris Garrett authored Nov 10, 2020
2 parents a1d3419 + 5dccd31 commit b955e6a
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ setGlobalContext({

export default function createEnvDelegate(isInteractive: boolean): EnvironmentDelegate {
return {
owner: {},
isInteractive,
extra: undefined,
onTransactionBegin() {},
enableDebugTooling: false,
onTransactionCommit() {
flush(scheduledDestructors);
flush(scheduledFinalizers);
Expand Down
4 changes: 2 additions & 2 deletions packages/@glimmer/integration-tests/lib/modes/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ export class NativeIteratorDelegate<T = unknown> implements IteratorDelegate {
export const BaseEnv: EnvironmentDelegate = {
isInteractive: true,

extra: undefined,
enableDebugTooling: false,

onTransactionBegin() {},
owner: {},

onTransactionCommit() {
for (let i = 0; i < scheduledDestroyables.length; i++) {
Expand Down
8 changes: 4 additions & 4 deletions packages/@glimmer/integration-tests/test/env-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ QUnit.test('assert against nested transactions', (assert) => {
let env = new EnvironmentImpl(
{ document: castToSimple(document) },
{
onTransactionBegin() {},
owner: {},
onTransactionCommit() {},
isInteractive: true,
extra: undefined,
enableDebugTooling: false,
}
);
env.begin();
Expand All @@ -24,10 +24,10 @@ QUnit.test('ensure commit cleans up when it can', (assert) => {
let env = new EnvironmentImpl(
{ document: castToSimple(document) },
{
onTransactionBegin() {},
owner: {},
onTransactionCommit() {},
isInteractive: true,
extra: undefined,
enableDebugTooling: false,
}
);
env.begin();
Expand Down
1 change: 1 addition & 0 deletions packages/@glimmer/interfaces/lib/runtime.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './runtime/arguments';
export * from './runtime/debug-render-tree';
export * from './runtime/element';
export * from './runtime/environment';
export * from './runtime/modifier';
Expand Down
48 changes: 48 additions & 0 deletions packages/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { SimpleElement, SimpleNode } from '@simple-dom/interface';
import { Bounds } from '../dom/bounds';
import { Template } from '../template';
import { Arguments, CapturedArguments } from './arguments';

export type RenderNodeType = 'outlet' | 'engine' | 'route-template' | 'component';

export interface RenderNode {
type: RenderNodeType;
name: string;
args: CapturedArguments;
instance: unknown;
template?: Template;
}

export interface CapturedRenderNode {
id: string;
type: RenderNodeType;
name: string;
args: Arguments;
instance: unknown;
template: string | null;
bounds: null | {
parentElement: SimpleElement;
firstNode: SimpleNode;
lastNode: SimpleNode;
};
children: CapturedRenderNode[];
}

export interface DebugRenderTree<Bucket extends object = object> {
begin(): void;

create(state: Bucket, node: RenderNode): void;

update(state: Bucket): void;

// for dynamic layouts
setTemplate(state: Bucket, template: Template): void;

didRender(state: Bucket, bounds: Bounds): void;

willDestroy(state: Bucket): void;

commit(): void;

capture(): CapturedRenderNode[];
}
7 changes: 5 additions & 2 deletions packages/@glimmer/interfaces/lib/runtime/environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { ComponentInstanceState } from '../components';
import { ComponentManager } from '../components/component-manager';
import { Option } from '../core';
import { GlimmerTreeChanges, GlimmerTreeConstruction } from '../dom/changes';
import { DebugRenderTree } from './debug-render-tree';
import { ModifierManager } from './modifier';
import { Owner } from './owner';

export interface EnvironmentOptions {
document?: SimpleDocument;
Expand All @@ -19,7 +21,7 @@ export interface Transaction {}
declare const TransactionSymbol: unique symbol;
export type TransactionSymbol = typeof TransactionSymbol;

export interface Environment<Extra = unknown> {
export interface Environment<O extends Owner = Owner> {
[TransactionSymbol]: Option<Transaction>;

didCreate(component: InternalComponent, manager: InternalComponentManager): void;
Expand All @@ -35,5 +37,6 @@ export interface Environment<Extra = unknown> {
getAppendOperations(): GlimmerTreeConstruction;

isInteractive: boolean;
extra: Extra;
debugRenderTree: DebugRenderTree;
owner: O;
}
4 changes: 2 additions & 2 deletions packages/@glimmer/interfaces/lib/runtime/runtime.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { Owner } from './owner';
The contents of the Runtime do not change as the VM executes, unlike
the VM state.
*/
export interface RuntimeContext<E = unknown> {
env: Environment<E>;
export interface RuntimeContext {
env: Environment;
program: RuntimeProgram;
resolver: RuntimeResolver;
}
Expand Down
204 changes: 204 additions & 0 deletions packages/@glimmer/runtime/lib/debug-render-tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { DEBUG } from '@glimmer/env';
import {
Bounds,
CapturedRenderNode,
DebugRenderTree,
Option,
RenderNode,
Template,
} from '@glimmer/interfaces';
import { expect, Stack, unwrapTemplate } from '@glimmer/util';
import { reifyArgs } from './vm/arguments';

interface InternalRenderNode<T extends object> extends RenderNode {
bounds: Option<Bounds>;
refs: Set<Ref<T>>;
parent?: InternalRenderNode<T>;
}

let GUID = 0;

export class Ref<T extends object> {
readonly id: number = GUID++;
private value: Option<T>;

constructor(value: T) {
this.value = value;
}

get(): Option<T> {
return this.value;
}

release(): void {
if (DEBUG && this.value === null) {
throw new Error('BUG: double release?');
}

this.value = null;
}

toString(): String {
let label = `Ref ${this.id}`;

if (this.value === null) {
return `${label} (released)`;
} else {
try {
return `${label}: ${this.value}`;
} catch {
return label;
}
}
}
}

export default class DebugRenderTreeImpl<Bucket extends object = object>
implements DebugRenderTree<Bucket> {
private stack = new Stack<Bucket>();

private refs = new WeakMap<Bucket, Ref<Bucket>>();
private roots = new Set<Ref<Bucket>>();
private nodes = new WeakMap<Bucket, InternalRenderNode<Bucket>>();

begin(): void {
this.reset();
}

create(state: Bucket, node: RenderNode): void {
let internalNode: InternalRenderNode<Bucket> = {
...node,
bounds: null,
refs: new Set(),
};
this.nodes.set(state, internalNode);
this.appendChild(internalNode, state);
this.enter(state);
}

update(state: Bucket): void {
this.enter(state);
}

// for dynamic layouts
setTemplate(state: Bucket, template: Template): void {
this.nodeFor(state).template = template;
}

didRender(state: Bucket, bounds: Bounds): void {
if (DEBUG && this.stack.current !== state) {
throw new Error(`BUG: expecting ${this.stack.current}, got ${state}`);
}

this.nodeFor(state).bounds = bounds;
this.exit();
}

willDestroy(state: Bucket): void {
expect(this.refs.get(state), 'BUG: missing ref').release();
}

commit(): void {
this.reset();
}

capture(): CapturedRenderNode[] {
return this.captureRefs(this.roots);
}

private reset(): void {
if (this.stack.size !== 0) {
// We probably encountered an error during the rendering loop. This will
// likely trigger undefined behavior and memory leaks as the error left
// things in an inconsistent state. It is recommended that the user
// refresh the page.

// TODO: We could warn here? But this happens all the time in our tests?

// Clean up the root reference to prevent errors from happening if we
// attempt to capture the render tree (Ember Inspector may do this)
let root = expect(this.stack.toArray()[0], 'expected root state when resetting render tree');
let ref = this.refs.get(root);

if (ref !== undefined) {
this.roots.delete(ref);
}

while (!this.stack.isEmpty()) {
this.stack.pop();
}
}
}

private enter(state: Bucket): void {
this.stack.push(state);
}

private exit(): void {
if (DEBUG && this.stack.size === 0) {
throw new Error('BUG: unbalanced pop');
}

this.stack.pop();
}

private nodeFor(state: Bucket): InternalRenderNode<Bucket> {
return expect(this.nodes.get(state), 'BUG: missing node');
}

private appendChild(node: InternalRenderNode<Bucket>, state: Bucket): void {
if (DEBUG && this.refs.has(state)) {
throw new Error('BUG: child already appended');
}

let parent = this.stack.current;
let ref = new Ref(state);

this.refs.set(state, ref);

if (parent) {
let parentNode = this.nodeFor(parent);
parentNode.refs.add(ref);
node.parent = parentNode;
} else {
this.roots.add(ref);
}
}

private captureRefs(refs: Set<Ref<Bucket>>): CapturedRenderNode[] {
let captured: CapturedRenderNode[] = [];

refs.forEach((ref) => {
let state = ref.get();

if (state) {
captured.push(this.captureNode(`render-node:${ref.id}`, state));
} else {
refs.delete(ref);
}
});

return captured;
}

private captureNode(id: string, state: Bucket): CapturedRenderNode {
let node = this.nodeFor(state);
let { type, name, args, instance, refs } = node;
let template = this.captureTemplate(node);
let bounds = this.captureBounds(node);
let children = this.captureRefs(refs);
return { id, type, name, args: reifyArgs(args), instance, template, bounds, children };
}

private captureTemplate({ template }: InternalRenderNode<Bucket>): Option<string> {
return (template && unwrapTemplate(template).moduleName) || null;
}

private captureBounds(node: InternalRenderNode<Bucket>): CapturedRenderNode['bounds'] {
let bounds = expect(node.bounds, 'BUG: missing bounds');
let parentElement = bounds.parentElement();
let firstNode = bounds.firstNode();
let lastNode = bounds.lastNode();
return { parentElement, firstNode, lastNode };
}
}
Loading

0 comments on commit b955e6a

Please sign in to comment.