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 controls whether to perform extra bookkeeping needed to make
   the `captureRenderTree` work.

   This has to be set before the ember JavaScript code is evaluated.
   This is usually done by setting one of these...

   ```
   window.EmberENV = { _DEBUG_RENDER_TREE: true };
   ```

   ```
   window.ENV = { _DEBUG_RENDER_TREE: true };
   ```

   ...before the "vendor" `<script>` tag in `index.html`.

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

   This is not intended to be set directly. Ember Inspector will enable
   the flag on behalf of the user as needed.

   This flag is always on in development mode.

   The flag is off by default in production mode, due to the cost
   associated with the the bookkeeping work.

   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
   interface CapturedRenderNode {
     type: 'outlet' | 'engine' | 'route-template' | 'component';
     name: string;
     args: {
       positional: unknown[];
       named: Dict<unknown>;
     };
     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 6f8b85c
Show file tree
Hide file tree
Showing 29 changed files with 2,456 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
38 changes: 38 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,39 @@ export const ENV = {
*/
_TEMPLATE_ONLY_GLIMMER_COMPONENTS: false,

/**
Whether to perform extra bookkeeping needed to make the `captureRenderTree`
work.
This has to be set before the ember JavaScript code is evaluated. This is
usually done by setting `window.EmberENV = { _DEBUG_RENDER_TREE: true }` or
`window.ENV = { _DEBUG_RENDER_TREE: true }` before the "vendor" `<script>`
tag in `index.html`.
Setting the flag after Ember is already loaded will not work correctly (it
may appear to work somewhat, but fundamentally broken).
This is not intended to be set directly. Ember Inspector will enable the
flag on behalf of the user as needed.
This flag is always on in development mode.
The flag is off by default in production mode, due to the cost associated
with the the bookkeeping work.
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.
@property _DEBUG_RENDER_TREE
@for EmberENV
@type Boolean
@default false
@private
*/
_DEBUG_RENDER_TREE: DEBUG,

/**
Whether the app is using jQuery. See RFC #294.
Expand Down Expand Up @@ -203,6 +237,10 @@ export const ENV = {
ENV.FEATURES[feature] = FEATURES[feature] === true;
}
}

if (DEBUG) {
ENV._DEBUG_RENDER_TREE = true;
}
})(global.EmberENV || global.ENV);

export function getENV() {
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 6f8b85c

Please sign in to comment.