Skip to content

Commit 71a716e

Browse files
committed
improve batch rendering performance by moving VM blocks out of the
DESTROYABLE_META
1 parent 2607140 commit 71a716e

10 files changed

Lines changed: 106 additions & 17 deletions

File tree

‎packages/@glimmer/destroyable/index.ts‎

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { DEBUG } from '@glimmer/env';
22
import type { Destroyable, Destructor } from '@glimmer/interfaces';
33
import debugToString from '@glimmer/debug-util/lib/debug-to-string';
44
import { scheduleDestroy, scheduleDestroyed } from '@glimmer/global-context';
5+
import {
6+
DESTROYABLE_META_SLOT,
7+
type HasDestroyableMetaSlot,
8+
} from '@glimmer/util/lib/destroyable-slot';
59

610
const LIVE_STATE = 0;
711
const DESTROYING_STATE = 1;
@@ -78,22 +82,45 @@ function remove<T extends object>(collection: OneOrMany<T>, item: T, message: st
7882
}
7983
}
8084

85+
function createMeta<T extends Destroyable>(destroyable: T): DestroyableMeta<T> {
86+
let meta: DestroyableMeta<Destroyable> = {
87+
parents: null,
88+
children: null,
89+
eagerDestructors: null,
90+
destructors: null,
91+
state: LIVE_STATE,
92+
};
93+
94+
if (DEBUG) {
95+
meta.source = destroyable;
96+
}
97+
98+
return meta as unknown as DestroyableMeta<T>;
99+
}
100+
81101
function getDestroyableMeta<T extends Destroyable>(destroyable: T): DestroyableMeta<T> {
82-
let meta = DESTROYABLE_META.get(destroyable);
102+
let slotted = destroyable as HasDestroyableMetaSlot;
103+
let own = slotted[DESTROYABLE_META_SLOT];
83104

84-
if (meta === undefined) {
85-
meta = {
86-
parents: null,
87-
children: null,
88-
eagerDestructors: null,
89-
destructors: null,
90-
state: LIVE_STATE,
91-
};
92-
93-
if (DEBUG) {
94-
meta.source = destroyable;
105+
if (own !== undefined) return own as DestroyableMeta<T>;
106+
107+
// `in` rather than a write, so this stays a read for everything else.
108+
if (DESTROYABLE_META_SLOT in slotted) {
109+
let meta = createMeta(destroyable);
110+
111+
slotted[DESTROYABLE_META_SLOT] = meta;
112+
113+
if (DEBUG && DESTROYABLE_META instanceof Map) {
114+
DESTROYABLE_META.set(destroyable, meta as unknown as DestroyableMeta<Destroyable>);
95115
}
96116

117+
return meta;
118+
}
119+
120+
let meta = DESTROYABLE_META.get(destroyable);
121+
122+
if (meta === undefined) {
123+
meta = createMeta(destroyable) as unknown as DestroyableMeta<Destroyable>;
97124
DESTROYABLE_META.set(destroyable, meta);
98125
}
99126

@@ -209,20 +236,33 @@ export function destroyChildren(destroyable: Destroyable) {
209236
iterate(children, destroy);
210237
}
211238

239+
/** Meta if there is any, without creating it. Mirrors `getDestroyableMeta`. */
240+
function peekDestroyableMeta(destroyable: Destroyable): DestroyableMeta<Destroyable> | undefined {
241+
let slotted = destroyable as HasDestroyableMetaSlot;
242+
let own = slotted[DESTROYABLE_META_SLOT];
243+
244+
if (own !== undefined) return own as DestroyableMeta<Destroyable>;
245+
246+
// An empty slot is proof there is no meta, so the map can be skipped.
247+
if (DESTROYABLE_META_SLOT in slotted) return undefined;
248+
249+
return DESTROYABLE_META.get(destroyable);
250+
}
251+
212252
export function _hasDestroyableChildren(destroyable: Destroyable) {
213-
let meta = DESTROYABLE_META.get(destroyable);
253+
let meta = peekDestroyableMeta(destroyable);
214254

215255
return meta === undefined ? false : meta.children !== null;
216256
}
217257

218258
export function isDestroying(destroyable: Destroyable) {
219-
let meta = DESTROYABLE_META.get(destroyable);
259+
let meta = peekDestroyableMeta(destroyable);
220260

221261
return meta === undefined ? false : meta.state >= DESTROYING_STATE;
222262
}
223263

224264
export function isDestroyed(destroyable: Destroyable) {
225-
let meta = DESTROYABLE_META.get(destroyable);
265+
let meta = peekDestroyableMeta(destroyable);
226266

227267
return meta === undefined ? false : meta.state >= DESTROYED_STATE;
228268
}

‎packages/@glimmer/destroyable/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
},
4040
"dependencies": {
4141
"@glimmer/global-context": "workspace:*",
42-
"@glimmer/interfaces": "workspace:*"
42+
"@glimmer/interfaces": "workspace:*",
43+
"@glimmer/util": "workspace:*"
4344
},
4445
"devDependencies": {
4546
"@ember/runloop": "workspace:*",

‎packages/@glimmer/reference/lib/reference.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
import type { Revision } from '@glimmer/validator/lib/validators';
1313
import type { Tag } from '@glimmer/interfaces';
1414
import { expect } from '@glimmer/debug-util/lib/platform-utils';
15+
import { DESTROYABLE_META_SLOT } from '@glimmer/util/lib/destroyable-slot';
1516
import { getProp, setProp } from '@glimmer/global-context';
1617
import { isDict } from '@glimmer/util/lib/collections';
1718
import { CONSTANT_TAG, INITIAL, validateTag, valueForTag } from '@glimmer/validator/lib/validators';
@@ -35,6 +36,8 @@ export interface ReferenceEnvironment {
3536
}
3637

3738
class ReferenceImpl<T = unknown> implements Reference<T> {
39+
declare [DESTROYABLE_META_SLOT]: object | undefined;
40+
3841
[REFERENCE]: ReferenceType;
3942
public tag: Nullable<Tag> = null;
4043
public lastRevision: Revision = INITIAL;
@@ -49,6 +52,7 @@ class ReferenceImpl<T = unknown> implements Reference<T> {
4952

5053
constructor(type: ReferenceType) {
5154
this[REFERENCE] = type;
55+
this[DESTROYABLE_META_SLOT] = undefined;
5256
}
5357
}
5458

‎packages/@glimmer/runtime/lib/vm/append.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type { MachineRegister, Register, SyscallRegister } from '@glimmer/vm/lib
2626
import { dev, expect } from '@glimmer/debug-util/lib/platform-utils';
2727
import { unwrapHandle } from '@glimmer/debug-util/lib/template';
2828
import { associateDestroyableChild } from '@glimmer/destroyable';
29+
import { DESTROYABLE_META_SLOT } from '@glimmer/util/lib/destroyable-slot';
2930
import { assertGlobalContextWasSet } from '@glimmer/global-context';
3031
import { LOCAL_DEBUG, LOCAL_TRACE_LOGGING } from '@glimmer/local-debug-flags';
3132
import { createIteratorItemRef } from '@glimmer/reference/lib/iterable';
@@ -54,9 +55,18 @@ import RenderResultImpl from './render-result';
5455
import EvaluationStackImpl from './stack';
5556
import { ListBlockOpcode, ListItemOpcode, TryOpcode } from './update';
5657

58+
/** The VM's own root destroyable, on the fast path like the block opcodes. */
59+
class Drop {
60+
declare [DESTROYABLE_META_SLOT]: object | undefined;
61+
62+
constructor() {
63+
this[DESTROYABLE_META_SLOT] = undefined;
64+
}
65+
}
66+
5767
class Stacks {
5868
declare debug?: () => DebugStacks;
59-
readonly drop: object = {};
69+
readonly drop: object = new Drop();
6070

6171
readonly scope = new Stack<Scope>();
6272
readonly dynamicScope = new Stack<DynamicScope>();

‎packages/@glimmer/runtime/lib/vm/element-builder.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { expect } from '@glimmer/debug-util/lib/platform-utils';
2222
import assert from '@glimmer/debug-util/lib/assert';
2323
import { setLocalDebugType } from '@glimmer/debug-util/lib/debug-brand';
2424
import { destroy, registerDestructor } from '@glimmer/destroyable';
25+
import { DESTROYABLE_META_SLOT } from '@glimmer/util/lib/destroyable-slot';
2526
import { LOCAL_DEBUG } from '@glimmer/local-debug-flags';
2627
import { StackImpl as Stack } from '@glimmer/util/lib/collections';
2728

@@ -397,12 +398,15 @@ export class NewTreeBuilder implements TreeBuilder {
397398

398399
export class AppendingBlockImpl implements AppendingBlock {
399400
declare debug?: { first: () => Nullable<SimpleNode>; last: () => Nullable<SimpleNode> };
401+
declare [DESTROYABLE_META_SLOT]: object | undefined;
400402

401403
protected first: Nullable<FirstNode> = null;
402404
protected last: Nullable<LastNode> = null;
403405
protected nesting = 0;
404406

405407
constructor(private parent: SimpleElement) {
408+
this[DESTROYABLE_META_SLOT] = undefined;
409+
406410
setLocalDebugType('block:simple', this);
407411

408412
if (LOCAL_DEBUG) {

‎packages/@glimmer/runtime/lib/vm/render-result.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,22 @@ import type {
88
} from '@glimmer/interfaces';
99
import { unreachable } from '@glimmer/debug-util/lib/platform-utils';
1010
import { associateDestroyableChild, registerDestructor } from '@glimmer/destroyable';
11+
import { DESTROYABLE_META_SLOT } from '@glimmer/util/lib/destroyable-slot';
1112

1213
import { clear } from '../bounds';
1314
import { UpdatingVM } from './update';
1415

1516
export default class RenderResultImpl implements RenderResult {
17+
declare [DESTROYABLE_META_SLOT]: object | undefined;
18+
1619
constructor(
1720
public env: Environment,
1821
private updating: UpdatingOpcode[],
1922
private bounds: AppendingBlock,
2023
readonly drop: object
2124
) {
25+
this[DESTROYABLE_META_SLOT] = undefined;
26+
2227
associateDestroyableChild(this, drop);
2328
registerDestructor(this, () => clear(this.bounds));
2429
}

‎packages/@glimmer/runtime/lib/vm/update.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { OpaqueIterationItem, OpaqueIterator } from '@glimmer/reference/lib
1818
import type { Reference } from '@glimmer/reference/lib/reference';
1919
import { expect, unwrap } from '@glimmer/debug-util/lib/platform-utils';
2020
import { associateDestroyableChild, destroy, destroyChildren } from '@glimmer/destroyable';
21+
import { DESTROYABLE_META_SLOT } from '@glimmer/util/lib/destroyable-slot';
2122
import { LOCAL_DEBUG } from '@glimmer/local-debug-flags';
2223
import { updateRef, valueForRef } from '@glimmer/reference/lib/reference';
2324
import { logStep } from '@glimmer/util/lib/debug-steps';
@@ -111,6 +112,10 @@ export interface VMState {
111112
}
112113

113114
export abstract class BlockOpcode implements UpdatingOpcode, Bounds {
115+
// Declaring the slot is what opts these into the fast path in
116+
// `@glimmer/destroyable`. One of these exists per `{{#each}}` item.
117+
declare [DESTROYABLE_META_SLOT]: object | undefined;
118+
114119
public children: UpdatingOpcode[];
115120

116121
protected readonly bounds: AppendingBlock;
@@ -123,6 +128,7 @@ export abstract class BlockOpcode implements UpdatingOpcode, Bounds {
123128
) {
124129
this.children = children;
125130
this.bounds = bounds;
131+
this[DESTROYABLE_META_SLOT] = undefined;
126132
}
127133

128134
parentElement() {

‎packages/@glimmer/util/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ export function assertNever(value: never, desc = 'unexpected unreachable branch'
2121

2222
throw new Error(`code reached unreachable`);
2323
}
24+
export { DESTROYABLE_META_SLOT } from './lib/destroyable-slot';
25+
export type { HasDestroyableMetaSlot } from './lib/destroyable-slot';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Classes Glimmer constructs itself declare this slot, so that
3+
* `@glimmer/destroyable` can hold their meta directly rather than in its
4+
* `WeakMap`. It lives here so packages below `@glimmer/destroyable` can declare
5+
* the slot without depending on it.
6+
*
7+
* Never write this onto an object Glimmer did not construct. Destroyables are
8+
* public through `@ember/destroyable` and can be any object a user hands in.
9+
*/
10+
export const DESTROYABLE_META_SLOT = Symbol('DESTROYABLE_META_SLOT');
11+
12+
export interface HasDestroyableMetaSlot {
13+
[DESTROYABLE_META_SLOT]?: object | undefined;
14+
}

‎pnpm-lock.yaml‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)