diff --git a/packages/@glimmer/runtime/lib/environment.ts b/packages/@glimmer/runtime/lib/environment.ts index bafbefe2f82..3e1bd701a11 100644 --- a/packages/@glimmer/runtime/lib/environment.ts +++ b/packages/@glimmer/runtime/lib/environment.ts @@ -25,12 +25,26 @@ import { isArgumentError } from './vm/arguments'; export const TRANSACTION: TransactionSymbol = Symbol('TRANSACTION') as TransactionSymbol; +// One transaction per render was measurable allocation churn (a fresh +// TransactionImpl plus four arrays each time). Discard-on-exception +// discipline: an instance is only repooled after a fully clean commit, +// so any throw leaves the pool empty and the error path allocates +// fresh -- exactly today's behavior. +let pooledTransaction: TransactionImpl | null = null; + class TransactionImpl implements Transaction { public scheduledInstallModifiers: ModifierInstance[] = []; public scheduledUpdateModifiers: ModifierInstance[] = []; public createdComponents: ComponentInstanceWithCreate[] = []; public updatedComponents: ComponentInstanceWithCreate[] = []; + reset(): void { + this.scheduledInstallModifiers.length = 0; + this.scheduledUpdateModifiers.length = 0; + this.createdComponents.length = 0; + this.updatedComponents.length = 0; + } + didCreate(component: ComponentInstanceWithCreate) { this.createdComponents.push(component); } @@ -146,7 +160,12 @@ export class EnvironmentImpl implements Environment { this.debugRenderTree?.begin(); - this[TRANSACTION] = new TransactionImpl(); + if (pooledTransaction !== null) { + this[TRANSACTION] = pooledTransaction; + pooledTransaction = null; + } else { + this[TRANSACTION] = new TransactionImpl(); + } } private get transaction(): TransactionImpl { @@ -181,6 +200,10 @@ export class EnvironmentImpl implements Environment { this.debugRenderTree?.commit(); this.delegate.onTransactionCommit(); + + // clean completion: safe to reuse + transaction.reset(); + pooledTransaction = transaction; } } diff --git a/packages/@glimmer/runtime/lib/vm/render-result.ts b/packages/@glimmer/runtime/lib/vm/render-result.ts index 32dbca888d0..10d6c2d7f09 100644 --- a/packages/@glimmer/runtime/lib/vm/render-result.ts +++ b/packages/@glimmer/runtime/lib/vm/render-result.ts @@ -10,7 +10,7 @@ import { unreachable } from '@glimmer/debug-util/lib/platform-utils'; import { associateDestroyableChild, registerDestructor } from '@glimmer/destroyable'; import { clear } from '../bounds'; -import { UpdatingVM } from './update'; +import { acquireUpdatingVM, releaseUpdatingVM } from './update'; export default class RenderResultImpl implements RenderResult { constructor( @@ -25,8 +25,10 @@ export default class RenderResultImpl implements RenderResult { rerender({ alwaysRevalidate = false } = { alwaysRevalidate: false }) { let { env, updating } = this; - let vm = new UpdatingVM(env, { alwaysRevalidate }); + let vm = acquireUpdatingVM(env, alwaysRevalidate); vm.execute(updating, this); + // reached only on clean completion; a throw discards the instance + releaseUpdatingVM(vm); } parentElement(): SimpleElement { diff --git a/packages/@glimmer/runtime/lib/vm/update.ts b/packages/@glimmer/runtime/lib/vm/update.ts index 92981cc0531..25d640b12c5 100644 --- a/packages/@glimmer/runtime/lib/vm/update.ts +++ b/packages/@glimmer/runtime/lib/vm/update.ts @@ -31,6 +31,27 @@ import type { AppendingBlockList } from './element-builder'; import { clear, move as moveBounds } from '../bounds'; import { NewTreeBuilder } from './element-builder'; +// Same discard-on-exception discipline as the transaction pool: an +// instance is repooled only after a clean execute, so error paths +// allocate fresh and recovery behavior is unchanged. +let pooledUpdatingVM: UpdatingVM | null = null; + +export function acquireUpdatingVM(env: Environment, alwaysRevalidate: boolean): UpdatingVM { + const vm = pooledUpdatingVM; + + if (vm === null) { + return new UpdatingVM(env, { alwaysRevalidate }); + } + + pooledUpdatingVM = null; + vm.prepare(env, alwaysRevalidate); + return vm; +} + +export function releaseUpdatingVM(vm: UpdatingVM): void { + pooledUpdatingVM = vm; +} + export class UpdatingVM implements IUpdatingVM { public env: Environment; public dom: GlimmerTreeChanges; @@ -44,6 +65,19 @@ export class UpdatingVM implements IUpdatingVM { this.alwaysRevalidate = alwaysRevalidate; } + /** + * Re-arms a pooled instance. The frame stack is necessarily empty + * after a clean execute (execution runs until it is), so only the + * environment-derived fields need refreshing. + * + * @internal + */ + prepare(env: Environment, alwaysRevalidate: boolean): void { + this.env = env; + this.dom = env.getDOM(); + this.alwaysRevalidate = alwaysRevalidate; + } + execute(opcodes: UpdatingOpcode[], handler: ExceptionHandler) { if (DEBUG) { let hasErrored = true; diff --git a/packages/@glimmer/validator/lib/tracking.ts b/packages/@glimmer/validator/lib/tracking.ts index d94ae456e6b..f75dd39b721 100644 --- a/packages/@glimmer/validator/lib/tracking.ts +++ b/packages/@glimmer/validator/lib/tracking.ts @@ -37,8 +37,21 @@ class Tracker { return combine(Array.from(this.tags)); } } + + reset(): void { + this.tags.clear(); + this.last = null; + } } +// Frames are strictly LIFO, so finished trackers can be reset and +// reused instead of allocating a Tracker + Set per frame. Capped: +// depth beyond the cap (error-path resets, pathological nesting) just +// allocates, and instances abandoned by resetTracking() are simply +// never repooled. +const TRACKER_POOL: Tracker[] = []; +const TRACKER_POOL_MAX = 32; + /** * Whenever a tracked computed property is entered, the current tracker is * saved off and a new tracker is replaced. @@ -59,7 +72,7 @@ const OPEN_TRACK_FRAMES: (Tracker | null)[] = []; export function beginTrackFrame(debuggingContext?: string | false): void { OPEN_TRACK_FRAMES.push(CURRENT_TRACKER); - CURRENT_TRACKER = new Tracker(); + CURRENT_TRACKER = TRACKER_POOL.pop() ?? new Tracker(); if (DEBUG) { unwrap(debug.beginTrackingTransaction)(debuggingContext); @@ -79,7 +92,15 @@ export function endTrackFrame(): Tag { CURRENT_TRACKER = OPEN_TRACK_FRAMES.pop() || null; - return unwrap(current).combine(); + const tracker = unwrap(current); + const tag = tracker.combine(); + + if (TRACKER_POOL.length < TRACKER_POOL_MAX) { + tracker.reset(); + TRACKER_POOL.push(tracker); + } + + return tag; } export function beginUntrackFrame(): void {