Skip to content

Commit

Permalink
[FEATURE] Introduce debug render tree
Browse files Browse the repository at this point in the history
1. Before loading Ember, set `ENV._DEBUG_RENDER_TREE = true;`

   This has to be set before the ember JavaScript code is evaluated
   (i.e. before the vendor `<script>` tag). This is usually done by
   setting `window.EmberENV` or `window.ENV`.

   Setting the flag *after* Ember is already loaded will not work
   correctly (it may appear to work somewhat, but fundamentally
   broken).

   This flag is on by default in development mode.

   For production, the expected flow is that Ember Inspector will
   ask the user to refresh the page after enabling the feature. It
   could also offer a feature where the user add some domains to
   the "always on" list. In either case, Ember Inspector will
   inject the code on the page to set the flag if needed.

2. With the flag on, `Ember._captureRenderTree()` is available. It
   takes the *application instance* as an argument and returns an
   array of `CapturedRenderNode`:

   ```typescript
   export interface CapturedRenderNode {
     type: 'outlet' | 'engine' | 'route-template' | 'component';
     name: string;
     args: ReturnType<CapturedArguments['value']>;
     instance: unknown;
     bounds: Option<{
       parentElement: Simple.Element;
       firstNode: Simple.Node;
       lastNode: Simple.Node;
     }>;
     children: CapturedRenderNode[];
   }
   ```

Co-authored-by: Yehuda Katz <[email protected]>
  • Loading branch information
chancancode and wycats committed Sep 14, 2019
1 parent 865ffd0 commit 7435de8
Show file tree
Hide file tree
Showing 29 changed files with 2,421 additions and 131 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ module.exports = {
rules: {
// the TypeScript compiler already takes care of this and
// leaving it enabled results in false positives for interface imports
'no-dupe-class-members': 'off',
'no-unused-vars': 'off',
'no-undef': 'off',

Expand Down
3 changes: 3 additions & 0 deletions packages/@ember/-internals/environment/lib/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FUNCTION_PROTOTYPE_EXTENSIONS } from '@ember/deprecated-features';
import { DEBUG } from '@glimmer/env';
import global from './global';

/**
Expand Down Expand Up @@ -98,6 +99,8 @@ export const ENV = {
*/
_TEMPLATE_ONLY_GLIMMER_COMPONENTS: false,

_DEBUG_RENDER_TREE: DEBUG,

/**
Whether the app is using jQuery. See RFC #294.
Expand Down
4 changes: 3 additions & 1 deletion packages/@ember/-internals/glimmer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,11 +388,13 @@ export { default as AbstractComponentManager } from './lib/component-managers/ab
// it supports for example
export { UpdatableReference, INVOKE } from './lib/utils/references';
export { default as iterableFor } from './lib/utils/iterable';
export { default as DebugStack } from './lib/utils/debug-stack';
export { default as getDebugStack, DebugStack } from './lib/utils/debug-stack';
export { default as OutletView } from './lib/views/outlet';
export { capabilities } from './lib/component-managers/custom';
export { setComponentManager, getComponentManager } from './lib/utils/custom-component-manager';
export { setModifierManager, getModifierManager } from './lib/utils/custom-modifier-manager';
export { capabilities as modifierCapabilities } from './lib/modifiers/custom';
export { isSerializationFirstNode } from './lib/utils/serialization-first-node-helpers';
export { setComponentTemplate, getComponentTemplate } from './lib/utils/component-template';
export { CapturedRenderNode, captureRenderTree } from './lib/utils/debug-render-tree';
export { WeakRef, WeakRefSet } from './lib/utils/weak';
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DEBUG } from '@glimmer/env';
import { ComponentCapabilities, Simple } from '@glimmer/interfaces';
import { Tag, VersionedPathReference } from '@glimmer/reference';
import {
Expand All @@ -11,20 +10,14 @@ import {
PreparedArguments,
} from '@glimmer/runtime';
import { Destroyable, Opaque, Option } from '@glimmer/util';
import DebugStack from '../utils/debug-stack';
import { DebugStack } from '../utils/debug-stack';

// implements the ComponentManager interface as defined in glimmer:
// tslint:disable-next-line:max-line-length
// https://github.com/glimmerjs/glimmer-vm/blob/v0.24.0-beta.4/packages/%40glimmer/runtime/lib/component/interfaces.ts#L21

export default abstract class AbstractManager<T, U> implements ComponentManager<T, U> {
public debugStack: typeof DebugStack;
public _pushToDebugStack!: (name: string, environment: any) => void;
public _pushEngineToDebugStack!: (name: string, environment: any) => void;

constructor() {
this.debugStack = undefined;
}
public debugStack: DebugStack | undefined = undefined;

prepareArgs(_state: U, _args: Arguments): Option<PreparedArguments> {
return null;
Expand Down Expand Up @@ -83,15 +76,3 @@ export default abstract class AbstractManager<T, U> implements ComponentManager<

abstract getDestructor(bucket: T): Option<Destroyable>;
}

if (DEBUG) {
AbstractManager.prototype._pushToDebugStack = function(name: string, environment) {
this.debugStack = environment.debugStack;
this.debugStack.push(name);
};

AbstractManager.prototype._pushEngineToDebugStack = function(name: string, environment) {
this.debugStack = environment.debugStack;
this.debugStack.pushEngine(name);
};
}
51 changes: 44 additions & 7 deletions packages/@ember/-internals/glimmer/lib/component-managers/curly.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { privatize as P } from '@ember/-internals/container';
import { ENV } from '@ember/-internals/environment';
import { getOwner } from '@ember/-internals/owner';
import { guidFor } from '@ember/-internals/utils';
import {
Expand Down Expand Up @@ -239,7 +240,7 @@ export default class CurlyComponentManager
hasBlock: boolean
): ComponentStateBucket {
if (DEBUG) {
this._pushToDebugStack(`component:${state.name}`, environment);
environment.debugStack.push(`component:${state.name}`);
}

// Get the nearest concrete component instance from the scope. "Virtual"
Expand Down Expand Up @@ -275,6 +276,12 @@ export default class CurlyComponentManager
props.layout = state.template;
}

// caller:
// <FaIcon @name="bug" />
//
// callee:
// <i class="fa-{{@name}}"></i>

// Now that we've built up all of the properties to set on the component instance,
// actually create it.
let component = factory.create(props);
Expand Down Expand Up @@ -330,6 +337,15 @@ export default class CurlyComponentManager
component.trigger('willRender');
}

if (ENV._DEBUG_RENDER_TREE) {
environment.debugRenderTree.create(bucket, {
type: 'component',
name: state.name,
args: args.capture(),
instance: component,
});
}

return bucket;
}

Expand Down Expand Up @@ -388,8 +404,12 @@ export default class CurlyComponentManager
bucket.component[BOUNDS] = bounds;
bucket.finalize();

if (ENV._DEBUG_RENDER_TREE) {
bucket.environment.debugRenderTree.didRender(bucket, bounds);
}

if (DEBUG) {
this.debugStack.pop();
bucket.environment.debugStack.pop();
}
}

Expand All @@ -408,8 +428,12 @@ export default class CurlyComponentManager
update(bucket: ComponentStateBucket): void {
let { component, args, argsRevision, environment } = bucket;

if (ENV._DEBUG_RENDER_TREE) {
environment.debugRenderTree.update(bucket);
}

if (DEBUG) {
this._pushToDebugStack(component._debugContainerKey, environment);
environment.debugStack.push(component._debugContainerKey);
}

bucket.finalizer = _instrumentStart('render.component', rerenderInstrumentDetails, component);
Expand All @@ -433,11 +457,15 @@ export default class CurlyComponentManager
}
}

didUpdateLayout(bucket: ComponentStateBucket): void {
didUpdateLayout(bucket: ComponentStateBucket, bounds: Bounds): void {
bucket.finalize();

if (ENV._DEBUG_RENDER_TREE) {
bucket.environment.debugRenderTree.didRender(bucket, bounds);
}

if (DEBUG) {
this.debugStack.pop();
bucket.environment.debugStack.pop();
}
}

Expand All @@ -448,8 +476,17 @@ export default class CurlyComponentManager
}
}

getDestructor(stateBucket: ComponentStateBucket): Option<Destroyable> {
return stateBucket;
getDestructor(bucket: ComponentStateBucket): Option<Destroyable> {
if (ENV._DEBUG_RENDER_TREE) {
return {
destroy() {
bucket.environment.debugRenderTree.willDestroy(bucket);
bucket.destroy();
},
};
} else {
return bucket;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import {
import { createTag, isConst, PathReference, Tag } from '@glimmer/reference';
import {
Arguments,
Bounds,
CapturedArguments,
ComponentDefinition,
Invocation,
WithStaticLayout,
} from '@glimmer/runtime';
import { Destroyable } from '@glimmer/util';

import { ENV } from '@ember/-internals/environment';
import Environment from '../environment';
import RuntimeResolver from '../resolver';
import { OwnedTemplate } from '../template';
Expand Down Expand Up @@ -184,7 +186,7 @@ export default class CustomComponentManager<ComponentInstance>
RuntimeResolver
> {
create(
_env: Environment,
env: Environment,
definition: CustomComponentDefinitionState<ComponentInstance>,
args: Arguments
): CustomComponentState<ComponentInstance> {
Expand Down Expand Up @@ -267,10 +269,27 @@ export default class CustomComponentManager<ComponentInstance>

const component = delegate.createComponent(definition.ComponentClass.class, value);

return new CustomComponentState(delegate, component, capturedArgs, namedArgsProxy);
let bucket = new CustomComponentState(delegate, component, capturedArgs, env, namedArgsProxy);

if (ENV._DEBUG_RENDER_TREE) {
env.debugRenderTree.create(bucket, {
type: 'component',
name: definition.name,
args: args.capture(),
instance: component,
});
}

return bucket;
}

update({ delegate, component, args, namedArgsProxy }: CustomComponentState<ComponentInstance>) {
update(bucket: CustomComponentState<ComponentInstance>) {
if (ENV._DEBUG_RENDER_TREE) {
bucket.env.debugRenderTree.update(bucket);
}

let { delegate, component, args, namedArgsProxy } = bucket;

let value;

if (EMBER_CUSTOM_COMPONENT_ARG_PROXY) {
Expand Down Expand Up @@ -308,18 +327,34 @@ export default class CustomComponentManager<ComponentInstance>
}

getDestructor(state: CustomComponentState<ComponentInstance>): Option<Destroyable> {
let destructor: Option<Destroyable> = null;

if (hasDestructors(state.delegate)) {
return state;
} else {
return null;
destructor = state;
}

if (ENV._DEBUG_RENDER_TREE) {
let inner = destructor;

destructor = {
destroy() {
state.env.debugRenderTree.willDestroy(state);

if (inner) {
inner.destroy();
}
},
};
}

return destructor;
}

getCapabilities({
delegate,
}: CustomComponentDefinitionState<ComponentInstance>): ComponentCapabilities {
return Object.assign({}, CAPABILITIES, {
updateHook: delegate.capabilities.updateHook,
updateHook: ENV._DEBUG_RENDER_TREE || delegate.capabilities.updateHook,
});
}

Expand All @@ -332,7 +367,17 @@ export default class CustomComponentManager<ComponentInstance>
}
}

didRenderLayout() {}
didRenderLayout(bucket: CustomComponentState<ComponentInstance>, bounds: Bounds) {
if (ENV._DEBUG_RENDER_TREE) {
bucket.env.debugRenderTree.didRender(bucket, bounds);
}
}

didUpdateLayout(bucket: CustomComponentState<ComponentInstance>, bounds: Bounds) {
if (ENV._DEBUG_RENDER_TREE) {
bucket.env.debugRenderTree.didRender(bucket, bounds);
}
}

getLayout(state: DefinitionState<ComponentInstance>): Invocation {
return {
Expand All @@ -351,6 +396,7 @@ export class CustomComponentState<ComponentInstance> {
public delegate: ManagerDelegate<ComponentInstance>,
public component: ComponentInstance,
public args: CapturedArguments,
public env: Environment,
public namedArgsProxy?: {}
) {}

Expand Down
Loading

0 comments on commit 7435de8

Please sign in to comment.