Skip to content
Closed
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
25 changes: 24 additions & 1 deletion packages/@glimmer/runtime/lib/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -181,6 +200,10 @@ export class EnvironmentImpl implements Environment {
this.debugRenderTree?.commit();

this.delegate.onTransactionCommit();

// clean completion: safe to reuse
transaction.reset();
pooledTransaction = transaction;
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/@glimmer/runtime/lib/vm/render-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 {
Expand Down
34 changes: 34 additions & 0 deletions packages/@glimmer/runtime/lib/vm/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
25 changes: 23 additions & 2 deletions packages/@glimmer/validator/lib/tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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 {
Expand Down