Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { DebugRenderTree } from '@glimmer/interfaces';

/**
* The `DebugRenderTree` implementation is relatively heavy and is only needed
* when debug tooling (e.g. Ember Inspector) is in use. To allow the
* implementation to be tree-shaken out of production builds that never use
* it, the environment does not reference the implementation directly.
* Instead, an implementation is registered here:
*
* - in debug builds, eagerly (see `./environment`), preserving the existing
* behavior of `EmberENV._DEBUG_RENDER_TREE` defaulting to on in
* development; and
* - in production builds, as a side effect of importing
* `@ember/debug/lib/capture-render-tree` (the entry point Ember Inspector
* relies on), preserving the documented `EmberENV._DEBUG_RENDER_TREE`
* production opt-in for apps that include that module.
*
* Production apps whose module graph never reaches `captureRenderTree` pay
* neither the bytes nor the runtime cost.
*/
let debugRenderTreeFactory: (() => DebugRenderTree<object>) | undefined;

export function setDebugRenderTreeFactory(factory: () => DebugRenderTree<object>): void {
debugRenderTreeFactory = factory;
}

export function createDebugRenderTree(): DebugRenderTree<object> | undefined {
return debugRenderTreeFactory?.();
}
13 changes: 12 additions & 1 deletion packages/@ember/-internals/glimmer/lib/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { DEBUG } from '@glimmer/env';
import setGlobalContext from '@glimmer/global-context';
import type { EnvironmentDelegate } from '@glimmer/runtime/lib/environment';
import { debug } from '@glimmer/validator/lib/debug';
import { createDebugRenderTree } from './debug-render-tree-factory';
import { enableDebugRenderTree } from './setup-debug-render-tree';
import toIterator from './utils/iterator';
import { isHTMLSafe } from './utils/string';
import toBool from './utils/to-bool';
Expand Down Expand Up @@ -125,8 +127,17 @@ const VM_ASSERTION_OVERRIDES: { id: string; message: string }[] = [];

// Define environment delegate

if (DEBUG) {
// In debug builds the render tree is always available (`_DEBUG_RENDER_TREE`
// defaults to on in development). In production builds this block is
// stripped, so the `DebugRenderTree` implementation is only included if
// something else imports it — notably `@ember/debug/lib/capture-render-tree`,
// which registers it for the `EmberENV._DEBUG_RENDER_TREE` opt-in.
enableDebugRenderTree();
}

export class EmberEnvironmentDelegate implements EnvironmentDelegate {
public enableDebugTooling: boolean = ENV._DEBUG_RENDER_TREE;
public debugRenderTree = ENV._DEBUG_RENDER_TREE ? createDebugRenderTree() : undefined;

constructor(
public owner: InternalOwner,
Expand Down
15 changes: 15 additions & 0 deletions packages/@ember/-internals/glimmer/lib/setup-debug-render-tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import DebugRenderTreeImpl from '@glimmer/runtime/lib/debug-render-tree';

import { setDebugRenderTreeFactory } from './debug-render-tree-factory';

/**
* Registers the real `DebugRenderTree` implementation so that renderers
* created afterwards will record the render tree (when
* `EmberENV._DEBUG_RENDER_TREE` is enabled).
*
* Importing this module is what causes the `DebugRenderTree` implementation
* to be included in a build; see `./debug-render-tree-factory`.
*/
export function enableDebugRenderTree(): void {
setDebugRenderTreeFactory(() => new DebugRenderTreeImpl());
}
8 changes: 8 additions & 0 deletions packages/@ember/debug/lib/capture-render-tree.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { enableDebugRenderTree } from '@ember/-internals/glimmer/lib/setup-debug-render-tree';
import type { Renderer } from '@ember/-internals/glimmer/lib/renderer';
import type Owner from '@ember/owner';
import type { CapturedRenderNode } from '@glimmer/interfaces';

// Register the DebugRenderTree implementation as a side effect of importing
// this module. This is what makes `EmberENV._DEBUG_RENDER_TREE` work in
// production builds: any build that includes `captureRenderTree` (e.g. for
// Ember Inspector) also includes the render tree bookkeeping, while builds
// that never import it can tree-shake the implementation away entirely.
enableDebugRenderTree();

/**
@module @ember/debug
*/
Expand Down
2 changes: 0 additions & 2 deletions packages/@glimmer-workspace/integration-tests/lib/base-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export function scheduleDidDestroy(fn: () => void) {
export const BaseEnv: EnvironmentDelegate = {
isInteractive: true,

enableDebugTooling: false,

onTransactionCommit() {
for (const { destroyable, destructor } of scheduled) {
destructor(destroyable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { expect } from '@glimmer/debug-util';
import { DEBUG } from '@glimmer/env';
import { modifierCapabilities, setComponentTemplate, setModifierManager } from '@glimmer/manager';
import { EMPTY_ARGS, templateOnlyComponent, TemplateOnlyComponentManager } from '@glimmer/runtime';
import { assign } from '@glimmer/util';
import DebugRenderTreeImpl from '@glimmer/runtime/lib/debug-render-tree';
import {
BaseEnv,
createTemplate,
Expand Down Expand Up @@ -906,7 +906,14 @@ class DebugRenderTreeTest extends RenderTest {
}

suite(DebugRenderTreeTest, DebugRenderTreeDelegate, {
env: assign({}, BaseEnv, {
enableDebugTooling: true,
}),
env: {
...BaseEnv,
// A fresh DebugRenderTree per environment, mirroring what
// `EmberEnvironmentDelegate` does when debug tooling is enabled.
// (Declared as a getter in the literal so each environment gets its
// own instance; `assign` would eagerly evaluate it.)
get debugRenderTree() {
return new DebugRenderTreeImpl();
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ if (DEBUG) {
{
onTransactionCommit() {},
isInteractive: true,
enableDebugTooling: false,
}
);
env.begin();
Expand All @@ -28,7 +27,6 @@ QUnit.test('ensure commit cleans up when it can', (assert) => {
{
onTransactionCommit() {},
isInteractive: true,
enableDebugTooling: false,
}
);
env.begin();
Expand Down
17 changes: 11 additions & 6 deletions packages/@glimmer/runtime/lib/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DEBUG } from '@glimmer/env';
import type {
ClassicResolver,
ComponentInstanceWithCreate,
DebugRenderTree,
Environment,
EnvironmentOptions,
GlimmerTreeChanges,
Expand All @@ -19,7 +20,6 @@ import { ProgramImpl } from '@glimmer/program/lib/program';
import { track } from '@glimmer/validator/lib/tracking';
import { UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators';

import DebugRenderTree from './debug-render-tree';
import { DOMChangesImpl, DOMTreeConstruction } from './dom/helper';
import { isArgumentError } from './vm/arguments';

Expand Down Expand Up @@ -107,15 +107,15 @@ export class EnvironmentImpl implements Environment {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
isArgumentCaptureError: ((error: any) => boolean) | undefined;
debugRenderTree: DebugRenderTree<object> | undefined;
debugRenderTree: DebugRenderTree | undefined;

constructor(
options: EnvironmentOptions,
private delegate: EnvironmentDelegate
) {
this.isInteractive = delegate.isInteractive;
this.debugRenderTree = this.delegate.enableDebugTooling ? new DebugRenderTree() : undefined;
this.isArgumentCaptureError = this.delegate.enableDebugTooling ? isArgumentError : undefined;
this.debugRenderTree = this.delegate.debugRenderTree;
this.isArgumentCaptureError = this.debugRenderTree ? isArgumentError : undefined;
if (options.appendOperations) {
this.appendOperations = options.appendOperations;
this.updateOperations = options.updateOperations;
Expand Down Expand Up @@ -192,9 +192,14 @@ export interface EnvironmentDelegate {
isInteractive: boolean;

/**
* Used to enable debug tooling
* When provided, the environment will perform the extra bookkeeping needed
* to power debug tooling (e.g. Ember Inspector's render tree).
*
* This is provided by the delegate (rather than constructed by the
* environment) so that the `DebugRenderTree` implementation can be
* tree-shaken out of builds that do not use it.
*/
enableDebugTooling: boolean;
debugRenderTree?: DebugRenderTree | undefined;

/**
* Callback to be called when an environment transaction commits
Expand Down