From 8c6127851454f74a7e21698efdc3068020e1af2f Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Tue, 17 Mar 2020 20:21:35 -0700 Subject: [PATCH] [FEAT] Adds Effect Queue This PR prepares the VM for a refactor toward autotracking by extracting modifiers and wrapping them in "effects". This is short for the term [side-effect](https://en.wikipedia.org/wiki/Side_effect_(computer_science)), which describes an action run within a function or computation that doesn't technically have anything to do with the _output_ of that computation. Modifiers are a form of side-effect, because they don't directly affect the output of the VM - they schedule a change that occurs _later_. This also presented a problem for autotracking, which is why I started with this PR. Autotracking operates on the premise that it is watching state used within a computation - i.e. _synchronously_. Async actions, like modifiers, violate the basic premise of autotracking. We were able to model it with Tags using UpdatableTag, which does allow this type of lazy structure, but updatable tags have been a major source of performance issues and bugs in general. Rather than having the VM be responsible for updating these async actions, the way to handle this with autotracking would be to have something _else_ handle it, specifically the place where the actual computations _occur_. This is the Effect Queue. \## How it works The VM handles registration and destruction of effects, but the environment is now responsible for updating them. Effects are scheduled to run in the same position modifiers were previously, and all effects in the queue are revalidated after each render. Ordering is also still guaranteed, in that child effects will always be triggered before parents. This is because new nodes are prepended to the tree, and thus any new children are guaranteed to run before their older parents. Due to the nature of the queue, it's possible that sibling update order could change and be unpredictable. \## Other options We could use a tree that mirrors the VM tree instead of a queue, but traversing that tree could end up being fairly expensive, especially if there are few modifiers. I opted for the simpler solution for the time being, and figured we could benchmark to see if there is a performance impact currently, and if so what solutions are better. Another option would be to make a n-ary tree that simply divides the effects up evenly in memoization chunks. This might allow us to get some wins in the general case, when most modifiers have not changed. --- packages/@glimmer/integration-tests/index.ts | 1 + .../integration-tests/lib/modifiers.ts | 48 ++--- .../integration-tests/test/modifiers-test.ts | 68 +++++- .../interfaces/lib/runtime/environment.d.ts | 9 +- .../interfaces/lib/runtime/modifier.d.ts | 2 +- packages/@glimmer/runtime/index.ts | 3 +- .../lib/compiled/opcodes/-debug-strip.ts | 6 +- .../runtime/lib/compiled/opcodes/dom.ts | 67 ++---- .../@glimmer/runtime/lib/dynamic-scope.ts | 27 --- packages/@glimmer/runtime/lib/effects.ts | 122 +++++++++++ packages/@glimmer/runtime/lib/environment.ts | 200 +++--------------- packages/@glimmer/runtime/lib/opcodes.ts | 2 +- packages/@glimmer/runtime/lib/render.ts | 2 +- packages/@glimmer/runtime/lib/scope.ts | 157 ++++++++++++++ packages/@glimmer/runtime/lib/vm/append.ts | 14 +- 15 files changed, 439 insertions(+), 289 deletions(-) delete mode 100644 packages/@glimmer/runtime/lib/dynamic-scope.ts create mode 100644 packages/@glimmer/runtime/lib/effects.ts create mode 100644 packages/@glimmer/runtime/lib/scope.ts diff --git a/packages/@glimmer/integration-tests/index.ts b/packages/@glimmer/integration-tests/index.ts index b908e4732a..026dd71906 100644 --- a/packages/@glimmer/integration-tests/index.ts +++ b/packages/@glimmer/integration-tests/index.ts @@ -17,3 +17,4 @@ export * from './lib/suites'; export * from './lib/test-helpers/module'; export * from './lib/test-helpers/strings'; export * from './lib/test-helpers/test'; +export * from './lib/test-helpers/tracked'; diff --git a/packages/@glimmer/integration-tests/lib/modifiers.ts b/packages/@glimmer/integration-tests/lib/modifiers.ts index dfa8096eb3..692862b8aa 100644 --- a/packages/@glimmer/integration-tests/lib/modifiers.ts +++ b/packages/@glimmer/integration-tests/lib/modifiers.ts @@ -3,12 +3,11 @@ import { Dict, ModifierManager, GlimmerTreeChanges, - Destroyable, DynamicScope, VMArguments, CapturedArguments, } from '@glimmer/interfaces'; -import { Tag } from '@glimmer/validator'; +import { Tag, consumeTag } from '@glimmer/validator'; export interface TestModifierConstructor { new (): TestModifierInstance; @@ -22,12 +21,7 @@ export interface TestModifierInstance { } export class TestModifierDefinitionState { - instance?: TestModifierInstance; - constructor(Klass?: TestModifierConstructor) { - if (Klass) { - this.instance = new Klass(); - } - } + constructor(public Class: TestModifierConstructor) {} } export class TestModifierManager @@ -39,46 +33,48 @@ export class TestModifierManager _dynamicScope: DynamicScope, dom: GlimmerTreeChanges ) { - return new TestModifier(element, state, args.capture(), dom); + let { Class } = state; + + return new TestModifier(element, new Class(), args.capture(), dom); } getTag({ args: { tag } }: TestModifier): Tag { return tag; } - install({ element, args, state }: TestModifier) { - if (state.instance && state.instance.didInsertElement) { - state.instance.element = element; - state.instance.didInsertElement(args.positional.value(), args.named.value()); + install({ element, args, instance }: TestModifier) { + consumeTag(args.tag); + + if (instance && instance.didInsertElement) { + instance.element = element; + instance.didInsertElement(args.positional.value(), args.named.value()); } return; } - update({ args, state }: TestModifier) { - if (state.instance && state.instance.didUpdate) { - state.instance.didUpdate(args.positional.value(), args.named.value()); + update({ args, instance }: TestModifier) { + consumeTag(args.tag); + + if (instance && instance.didUpdate) { + instance.didUpdate(args.positional.value(), args.named.value()); } return; } - getDestructor(modifier: TestModifier): Destroyable { - return { - destroy: () => { - let { state } = modifier; - if (state.instance && state.instance.willDestroyElement) { - state.instance.willDestroyElement(); - } - }, - }; + teardown(modifier: TestModifier): void { + let { instance } = modifier; + if (instance && instance.willDestroyElement) { + instance.willDestroyElement(); + } } } export class TestModifier { constructor( public element: SimpleElement, - public state: TestModifierDefinitionState, + public instance: TestModifierInstance, public args: CapturedArguments, public dom: GlimmerTreeChanges ) {} diff --git a/packages/@glimmer/integration-tests/test/modifiers-test.ts b/packages/@glimmer/integration-tests/test/modifiers-test.ts index 770951f133..d4112303a2 100644 --- a/packages/@glimmer/integration-tests/test/modifiers-test.ts +++ b/packages/@glimmer/integration-tests/test/modifiers-test.ts @@ -1,4 +1,4 @@ -import { RenderTest, jitSuite, test, Count } from '..'; +import { RenderTest, jitSuite, test, Count, tracked, EmberishGlimmerComponent, EmberishGlimmerArgs } from '..'; import { Dict } from '@glimmer/interfaces'; import { SimpleElement } from '@simple-dom/interface'; import { Option } from '@glimmer/interfaces'; @@ -302,6 +302,72 @@ class ModifierTests extends RenderTest { assert.deepEqual(elementIds, ['outer-div', 'inner-div'], 'Modifiers are called on all levels'); } + @test + 'child modifiers that are added later update before parents'(assert: Assert) { + let inserts: Option[] = []; + let updates: Option[] = []; + + class Bar extends BaseModifier { + didInsertElement(params: unknown[]) { + assert.deepEqual(params, [123]); + if (this.element) { + inserts.push(this.element.getAttribute('id')); + } + } + + didUpdate(params: unknown[]) { + assert.deepEqual(params, [124]); + if (this.element) { + updates.push(this.element.getAttribute('id')); + } + } + } + + let component: Option = null; + + class Foo extends EmberishGlimmerComponent { + constructor(args: EmberishGlimmerArgs) { + super(args); + + component = this; + } + + @tracked showing = false; + @tracked count = 123; + } + + this.registerComponent( + 'Glimmer', + 'Foo', + ` +
+ {{#if this.showing}} +
+ {{/if}} +
+ `, + Foo + ); + this.registerModifier('bar', Bar); + + this.render(''); + + assert.deepEqual(inserts, ['outer'], 'outer modifier insert called'); + assert.deepEqual(updates, [], 'no updates called'); + + component!.showing = true; + this.rerender(); + + assert.deepEqual(inserts, ['outer', 'inner'], 'inner modifier insert called'); + assert.deepEqual(updates, [], 'no updates called'); + + component!.count++; + this.rerender(); + + assert.deepEqual(inserts, ['outer', 'inner'], 'inserts not called'); + assert.deepEqual(updates, ['inner', 'outer'], 'updates called in correct order'); + } + @test 'same element insertion order'(assert: Assert) { let insertionOrder: string[] = []; diff --git a/packages/@glimmer/interfaces/lib/runtime/environment.d.ts b/packages/@glimmer/interfaces/lib/runtime/environment.d.ts index 1346626d09..c076463625 100644 --- a/packages/@glimmer/interfaces/lib/runtime/environment.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/environment.d.ts @@ -8,7 +8,7 @@ import { AttributeOperation } from '../dom/attributes'; import { AttrNamespace, SimpleElement, SimpleDocument } from '@simple-dom/interface'; import { ComponentInstanceState } from '../components'; import { ComponentManager } from '../components/component-manager'; -import { Drop, Option } from '../core'; +import { Drop, Option, SymbolDestroyable } from '../core'; import { GlimmerTreeChanges, GlimmerTreeConstruction } from '../dom/changes'; import { ModifierManager } from './modifier'; @@ -26,6 +26,10 @@ export interface Transaction {} declare const TransactionSymbol: unique symbol; export type TransactionSymbol = typeof TransactionSymbol; +export interface Effect extends SymbolDestroyable { + createOrUpdate(): void; +} + export interface Environment { [TransactionSymbol]: Option; @@ -35,8 +39,7 @@ export interface Environment { willDestroy(drop: Drop): void; didDestroy(drop: Drop): void; - scheduleInstallModifier(modifier: unknown, manager: ModifierManager): void; - scheduleUpdateModifier(modifier: unknown, manager: ModifierManager): void; + registerEffect(phase: 'layout', effect: Effect): void; begin(): void; commit(): void; diff --git a/packages/@glimmer/interfaces/lib/runtime/modifier.d.ts b/packages/@glimmer/interfaces/lib/runtime/modifier.d.ts index a23bef8393..d1b7cab5dd 100644 --- a/packages/@glimmer/interfaces/lib/runtime/modifier.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/modifier.d.ts @@ -34,7 +34,7 @@ export interface ModifierManager< // Convert the opaque token into an object that implements Destroyable. // If it returns null, the modifier will not be destroyed. - getDestructor(modifier: ModifierInstanceState): Option; + teardown(modifier: ModifierInstanceState): void; } export interface ModifierDefinition< diff --git a/packages/@glimmer/runtime/index.ts b/packages/@glimmer/runtime/index.ts index 27da6ccf6c..fe00bfcf96 100644 --- a/packages/@glimmer/runtime/index.ts +++ b/packages/@glimmer/runtime/index.ts @@ -25,13 +25,12 @@ export { isWhitespace, } from './lib/dom/helper'; export { normalizeProperty } from './lib/dom/props'; -export { DefaultDynamicScope } from './lib/dynamic-scope'; +export { PartialScopeImpl, DefaultDynamicScope } from './lib/scope'; export { AotRuntime, JitRuntime, EnvironmentImpl, EnvironmentDelegate, - ScopeImpl, JitProgramCompilationContext, JitSyntaxCompilationContext, inTransaction, diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts index c83b391558..6cc1463f62 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts @@ -28,7 +28,7 @@ import { } from '@glimmer/interfaces'; import { VersionedPathReference, Reference } from '@glimmer/reference'; import { Tag, COMPUTE } from '@glimmer/validator'; -import { ScopeImpl } from '../../environment'; +import { PartialScopeImpl } from '../../scope'; import CurryComponentReference from '../../references/curry-component'; import { CapturedNamedArgumentsImpl, @@ -99,7 +99,9 @@ export const CheckCapturedArguments: Checker = CheckInterface export const CheckCurryComponent = wrap(() => CheckInstanceof(CurryComponentReference)); -export const CheckScope: Checker> = wrap(() => CheckInstanceof(ScopeImpl)); +export const CheckScope: Checker> = wrap(() => + CheckInstanceof(PartialScopeImpl) +); export const CheckComponentManager: Checker> = CheckInterface({ getCapabilities: CheckFunction, diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts index 2e09d53911..fc741b56f8 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts @@ -1,20 +1,9 @@ import { Reference, ReferenceCache, VersionedReference } from '@glimmer/reference'; -import { - Revision, - Tag, - isConstTagged, - isConstTag, - valueForTag, - validateTag, -} from '@glimmer/validator'; +import { Revision, Tag, isConstTagged, valueForTag, validateTag } from '@glimmer/validator'; import { check, CheckString, CheckElement, CheckOption, CheckNode } from '@glimmer/debug'; import { Op, Option, ModifierManager } from '@glimmer/interfaces'; import { $t0 } from '@glimmer/vm'; -import { - ModifierDefinition, - InternalModifierManager, - ModifierInstanceState, -} from '../../modifier/interfaces'; +import { ModifierDefinition } from '../../modifier/interfaces'; import { APPEND_OPCODES, UpdatingOpcode } from '../../opcodes'; import { UpdatingVM } from '../../vm'; import { Assert } from './vm'; @@ -23,6 +12,7 @@ import { CheckReference, CheckArguments, CheckOperations } from './-debug-strip' import { CONSTANTS } from '../../symbols'; import { SimpleElement, SimpleNode } from '@simple-dom/interface'; import { expect, Maybe } from '@glimmer/util'; +import { EffectImpl } from '../../effects'; APPEND_OPCODES.add(Op.Text, (vm, { op1: text }) => { vm.elements().appendText(vm[CONSTANTS].getString(text)); @@ -93,12 +83,22 @@ APPEND_OPCODES.add(Op.CloseElement, vm => { if (modifiers) { modifiers.forEach(([manager, modifier]) => { - vm.env.scheduleInstallModifier(modifier, manager); - let d = manager.getDestructor(modifier); - - if (d) { - vm.associateDestroyable(d); - } + let effect = new EffectImpl({ + setup() { + manager.install(modifier); + }, + + update() { + manager.update(modifier); + }, + + teardown() { + manager.teardown(modifier); + }, + }); + + vm.env.registerEffect('layout', effect); + vm.associateDestroyable(effect); }); } }); @@ -123,37 +123,8 @@ APPEND_OPCODES.add(Op.Modifier, (vm, { op1: handle }) => { ); operations.addModifier(manager, modifier); - - let tag = manager.getTag(modifier); - - if (!isConstTag(tag)) { - vm.updateWith(new UpdateModifierOpcode(tag, manager, modifier)); - } }); -export class UpdateModifierOpcode extends UpdatingOpcode { - public type = 'update-modifier'; - private lastUpdated: Revision; - - constructor( - public tag: Tag, - private manager: InternalModifierManager, - private modifier: ModifierInstanceState - ) { - super(); - this.lastUpdated = valueForTag(tag); - } - - evaluate(vm: UpdatingVM) { - let { manager, modifier, tag, lastUpdated } = this; - - if (!validateTag(tag, lastUpdated)) { - vm.env.scheduleUpdateModifier(modifier, manager); - this.lastUpdated = valueForTag(tag); - } - } -} - APPEND_OPCODES.add(Op.StaticAttr, (vm, { op1: _name, op2: _value, op3: _namespace }) => { let name = vm[CONSTANTS].getString(_name); let value = vm[CONSTANTS].getString(_value); diff --git a/packages/@glimmer/runtime/lib/dynamic-scope.ts b/packages/@glimmer/runtime/lib/dynamic-scope.ts deleted file mode 100644 index 5f72f7ee2a..0000000000 --- a/packages/@glimmer/runtime/lib/dynamic-scope.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DynamicScope, Dict } from '@glimmer/interfaces'; -import { assign } from '@glimmer/util'; -import { PathReference } from '@glimmer/reference'; - -export class DefaultDynamicScope implements DynamicScope { - private bucket: Dict; - - constructor(bucket?: Dict) { - if (bucket) { - this.bucket = assign({}, bucket); - } else { - this.bucket = {}; - } - } - - get(key: string): PathReference { - return this.bucket[key]; - } - - set(key: string, reference: PathReference): PathReference { - return (this.bucket[key] = reference); - } - - child(): DefaultDynamicScope { - return new DefaultDynamicScope(this.bucket); - } -} diff --git a/packages/@glimmer/runtime/lib/effects.ts b/packages/@glimmer/runtime/lib/effects.ts new file mode 100644 index 0000000000..e370c4e11f --- /dev/null +++ b/packages/@glimmer/runtime/lib/effects.ts @@ -0,0 +1,122 @@ +import { Effect } from '@glimmer/interfaces'; +import { LinkedList, ListNode, DESTROY, associate, assert, Option } from '@glimmer/util'; +import { memo } from '@glimmer/validator'; +import { DEBUG } from '@glimmer/env'; + +const effectPhases = ['layout'] as const; +export type EffectPhase = typeof effectPhases[number]; + +interface EffectHooks { + setup(): void; + update(): void; + teardown(): void; +} + +export class EffectImpl implements Effect { + constructor(private hooks: EffectHooks) {} + + private didSetup = false; + + createOrUpdate = memo(() => { + if (this.didSetup === false) { + this.didSetup = true; + this.hooks.setup(); + } else { + this.hooks.update(); + } + }); + + [DESTROY]() { + this.hooks.teardown(); + } +} + +function defaultScheduleEffects(_phase: EffectPhase, callback: () => void) { + callback(); +} + +class EffectQueue { + /** + * The effects in this queue + */ + effects: LinkedList> = new LinkedList(); + + /** + * Tracker for the current head of the queue. This is used to coordinate + * adding effects to the queue. In a given render pass, all effects added + * should be added in the order they were received, but they should be + * _prepended_ to any pre-existing effects. For instance, let's say that the + * queue started off in this state after our first render pass: + * + * A, B, C + * + * On the next render pass, we add D and E, which are siblings, and children + * of A. The timing semantics of effects is that siblings should be + * initialized in the order they were defined in, and should run before + * parents. So, assuming D comes before E, we want to do: + * + * D, E, A, B, C + * + * This way, new children will always run in the correct order, and before + * their parents. By keeping track of the current head at the beginning of a + * transaction, we can insert new effects in the proper order during the + * transaction. + */ + currentHead: Option> = null; + + revalidate = () => this.effects.forEachNode(n => n.value.createOrUpdate()); +} + +export class EffectManager { + private inTransaction = false; + + constructor(private scheduleEffects = defaultScheduleEffects) { + let queues: Record = {}; + + for (let phase of effectPhases) { + queues[phase] = new EffectQueue(); + } + + this.queues = queues as { [key in EffectPhase]: EffectQueue }; + } + + private queues: { [key in EffectPhase]: EffectQueue }; + + begin() { + if (DEBUG) { + this.inTransaction = true; + } + } + + registerEffect(phase: EffectPhase, effect: Effect) { + assert(this.inTransaction, 'You cannot register effects unless you are in a transaction'); + + let queue = this.queues[phase]; + let effects = queue.effects; + let newNode = new ListNode(effect); + + effects.insertBefore(newNode, queue.currentHead); + + associate(effect, { + [DESTROY]() { + effects.remove(newNode); + }, + }); + } + + commit() { + if (DEBUG) { + this.inTransaction = false; + } + + let { queues, scheduleEffects } = this; + + for (let phase of effectPhases) { + let queue = queues[phase]; + + scheduleEffects(phase, queue.revalidate); + + queue.currentHead = queue.effects.head(); + } + } +} diff --git a/packages/@glimmer/runtime/lib/environment.ts b/packages/@glimmer/runtime/lib/environment.ts index 894b0a4604..56cad617eb 100644 --- a/packages/@glimmer/runtime/lib/environment.ts +++ b/packages/@glimmer/runtime/lib/environment.ts @@ -6,18 +6,12 @@ import { EnvironmentOptions, GlimmerTreeChanges, GlimmerTreeConstruction, - JitOrAotBlock, - PartialScope, - Scope, - ScopeBlock, - ScopeSlot, Transaction, TransactionSymbol, CompilerArtifacts, WithCreateInstance, ResolvedValue, RuntimeResolverDelegate, - ModifierManager, Template, AotRuntimeResolver, Invocation, @@ -32,6 +26,7 @@ import { CompileTimeConstants, CompileTimeHeap, WholeProgramCompilationContext, + Effect, } from '@glimmer/interfaces'; import { IterableImpl, @@ -44,137 +39,19 @@ import { import { assert, WILL_DROP, DID_DROP, expect, symbol } from '@glimmer/util'; import { AttrNamespace, SimpleElement } from '@simple-dom/interface'; import { DOMChangesImpl, DOMTreeConstruction } from './dom/helper'; -import { ConditionalReference, UNDEFINED_REFERENCE } from './references'; +import { ConditionalReference } from './references'; import { DynamicAttribute, dynamicAttribute } from './vm/attributes/dynamic'; import { RuntimeProgramImpl } from '@glimmer/program'; - -export function isScopeReference(s: ScopeSlot): s is VersionedPathReference { - if (s === null || Array.isArray(s)) return false; - return true; -} - -export class ScopeImpl implements PartialScope { - static root(self: PathReference, size = 0): PartialScope { - let refs: PathReference[] = new Array(size + 1); - - for (let i = 0; i <= size; i++) { - refs[i] = UNDEFINED_REFERENCE; - } - - return new ScopeImpl(refs, null, null, null).init({ self }); - } - - static sized(size = 0): Scope { - let refs: PathReference[] = new Array(size + 1); - - for (let i = 0; i <= size; i++) { - refs[i] = UNDEFINED_REFERENCE; - } - - return new ScopeImpl(refs, null, null, null); - } - - constructor( - // the 0th slot is `self` - readonly slots: Array>, - private callerScope: Option>, - // named arguments and blocks passed to a layout that uses eval - private evalScope: Option>>, - // locals in scope when the partial was invoked - private partialMap: Option>> - ) {} - - init({ self }: { self: PathReference }): this { - this.slots[0] = self; - return this; - } - - getSelf(): PathReference { - return this.get>(0); - } - - getSymbol(symbol: number): PathReference { - return this.get>(symbol); - } - - getBlock(symbol: number): Option> { - let block = this.get(symbol); - return block === UNDEFINED_REFERENCE ? null : (block as ScopeBlock); - } - - getEvalScope(): Option>> { - return this.evalScope; - } - - getPartialMap(): Option>> { - return this.partialMap; - } - - bind(symbol: number, value: ScopeSlot) { - this.set(symbol, value); - } - - bindSelf(self: PathReference) { - this.set>(0, self); - } - - bindSymbol(symbol: number, value: PathReference) { - this.set(symbol, value); - } - - bindBlock(symbol: number, value: Option>) { - this.set>>(symbol, value); - } - - bindEvalScope(map: Option>>) { - this.evalScope = map; - } - - bindPartialMap(map: Dict>) { - this.partialMap = map; - } - - bindCallerScope(scope: Option>): void { - this.callerScope = scope; - } - - getCallerScope(): Option> { - return this.callerScope; - } - - child(): Scope { - return new ScopeImpl(this.slots.slice(), this.callerScope, this.evalScope, this.partialMap); - } - - private get>(index: number): T { - if (index >= this.slots.length) { - throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`); - } - - return this.slots[index] as T; - } - - private set>(index: number, value: T): void { - if (index >= this.slots.length) { - throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`); - } - - this.slots[index] = value; - } -} +import { EffectManager, EffectPhase } from './effects'; export const TRANSACTION: TransactionSymbol = symbol('TRANSACTION'); class TransactionImpl implements Transaction { - public scheduledInstallManagers: ModifierManager[] = []; - public scheduledInstallModifiers: unknown[] = []; - public scheduledUpdateModifierManagers: ModifierManager[] = []; - public scheduledUpdateModifiers: unknown[] = []; - public createdComponents: unknown[] = []; - public createdManagers: WithCreateInstance[] = []; - public updatedComponents: unknown[] = []; - public updatedManagers: WithCreateInstance[] = []; - public destructors: Drop[] = []; + private createdComponents: unknown[] = []; + private createdManagers: WithCreateInstance[] = []; + private updatedComponents: unknown[] = []; + private updatedManagers: WithCreateInstance[] = []; + private destructors: Drop[] = []; didCreate(component: unknown, manager: WithCreateInstance) { this.createdComponents.push(component); @@ -186,16 +63,6 @@ class TransactionImpl implements Transaction { this.updatedManagers.push(manager); } - scheduleInstallModifier(modifier: unknown, manager: ModifierManager) { - this.scheduledInstallModifiers.push(modifier); - this.scheduledInstallManagers.push(manager); - } - - scheduleUpdateModifier(modifier: unknown, manager: ModifierManager) { - this.scheduledUpdateModifiers.push(modifier); - this.scheduledUpdateModifierManagers.push(manager); - } - willDestroy(d: Drop) { d[WILL_DROP](); } @@ -226,22 +93,6 @@ class TransactionImpl implements Transaction { for (let i = 0; i < destructors.length; i++) { destructors[i][DID_DROP](); } - - let { scheduledInstallManagers, scheduledInstallModifiers } = this; - - for (let i = 0; i < scheduledInstallManagers.length; i++) { - let modifier = scheduledInstallModifiers[i]; - let manager = scheduledInstallManagers[i]; - manager.install(modifier); - } - - let { scheduledUpdateModifierManagers, scheduledUpdateModifiers } = this; - - for (let i = 0; i < scheduledUpdateModifierManagers.length; i++) { - let modifier = scheduledUpdateModifiers[i]; - let manager = scheduledUpdateModifierManagers[i]; - manager.update(modifier); - } } } @@ -277,6 +128,8 @@ export class EnvironmentImpl implements Environment { public toBool = defaultDelegateFn(this.delegate.toBool, defaultToBool); public toIterator = defaultDelegateFn(this.delegate.toIterator, defaultToIterator); + private effectManager = new EffectManager(this.delegate.scheduleEffects); + constructor(options: EnvironmentOptions, private delegate: EnvironmentDelegate) { if (options.appendOperations) { this.appendOperations = options.appendOperations; @@ -337,6 +190,8 @@ export class EnvironmentImpl implements Environment { this.delegate.onTransactionBegin(); } + this.effectManager.begin(); + this[TRANSACTION] = new TransactionImpl(); } @@ -352,16 +207,8 @@ export class EnvironmentImpl implements Environment { this.transaction.didUpdate(component, manager); } - scheduleInstallModifier(modifier: unknown, manager: ModifierManager) { - if (this.isInteractive) { - this.transaction.scheduleInstallModifier(modifier, manager); - } - } - - scheduleUpdateModifier(modifier: unknown, manager: ModifierManager) { - if (this.isInteractive) { - this.transaction.scheduleUpdateModifier(modifier, manager); - } + registerEffect(phase: EffectPhase, effect: Effect) { + this.effectManager.registerEffect(phase, effect); } willDestroy(d: Drop) { @@ -373,12 +220,14 @@ export class EnvironmentImpl implements Environment { } commit() { - let transaction = this.transaction; + let { transaction, delegate } = this; this[TRANSACTION] = null; transaction.commit(); - if (this.delegate.onTransactionCommit !== undefined) { - this.delegate.onTransactionCommit(); + this.effectManager.commit(); + + if (delegate.onTransactionCommit !== undefined) { + delegate.onTransactionCommit(); } } } @@ -469,6 +318,17 @@ export interface EnvironmentDelegate { namespace: Option ): DynamicAttribute; + /** + * Allows the embedding environment to schedule effects to be run in the future. + * Different phases will be passed to this callback, and each one should be + * scheduled at an appropriate time for that phase. The callback will be + * called at the end each transaction. + * + * @param phase the phase of effects that are being scheduled + * @param runEffects the callback which runs the effects + */ + scheduleEffects?: (phase: EffectPhase, runEffects: () => void) => void; + /** * Slot for any extra values that the embedding environment wants to add, * providing/passing around additional context to various users in the VM. diff --git a/packages/@glimmer/runtime/lib/opcodes.ts b/packages/@glimmer/runtime/lib/opcodes.ts index bbec6aa1f1..9761c3b13b 100644 --- a/packages/@glimmer/runtime/lib/opcodes.ts +++ b/packages/@glimmer/runtime/lib/opcodes.ts @@ -11,7 +11,7 @@ import { debug, logOpcode } from '@glimmer/debug'; import { DESTRUCTOR_STACK, INNER_VM, CONSTANTS, STACKS } from './symbols'; import { InternalVM, InternalJitVM } from './vm/append'; import { CURSOR_STACK } from './vm/element-builder'; -import { isScopeReference } from './environment'; +import { isScopeReference } from './scope'; export interface OpcodeJSON { type: number | string; diff --git a/packages/@glimmer/runtime/lib/render.ts b/packages/@glimmer/runtime/lib/render.ts index b053520913..4e9196a4a4 100644 --- a/packages/@glimmer/runtime/lib/render.ts +++ b/packages/@glimmer/runtime/lib/render.ts @@ -24,7 +24,7 @@ import { resolveComponent } from './component/resolve'; import { ARGS } from './symbols'; import { AotVM, InternalVM, JitVM } from './vm/append'; import { NewElementBuilder } from './vm/element-builder'; -import { DefaultDynamicScope } from './dynamic-scope'; +import { DefaultDynamicScope } from './scope'; import { UNDEFINED_REFERENCE } from './references'; class TemplateIteratorImpl implements TemplateIterator { diff --git a/packages/@glimmer/runtime/lib/scope.ts b/packages/@glimmer/runtime/lib/scope.ts new file mode 100644 index 0000000000..f1947756bd --- /dev/null +++ b/packages/@glimmer/runtime/lib/scope.ts @@ -0,0 +1,157 @@ +import { + Dict, + DynamicScope, + JitOrAotBlock, + PartialScope, + Scope, + ScopeBlock, + ScopeSlot, + Option, +} from '@glimmer/interfaces'; +import { PathReference, VersionedPathReference } from '@glimmer/reference'; +import { assign } from '@glimmer/util'; +import { UNDEFINED_REFERENCE } from './references'; + +export class DefaultDynamicScope implements DynamicScope { + private bucket: Dict; + + constructor(bucket?: Dict) { + if (bucket) { + this.bucket = assign({}, bucket); + } else { + this.bucket = {}; + } + } + + get(key: string): PathReference { + return this.bucket[key]; + } + + set(key: string, reference: PathReference): PathReference { + return (this.bucket[key] = reference); + } + + child(): DefaultDynamicScope { + return new DefaultDynamicScope(this.bucket); + } +} + +export function isScopeReference(s: ScopeSlot): s is VersionedPathReference { + if (s === null || Array.isArray(s)) return false; + return true; +} + +export class PartialScopeImpl implements PartialScope { + static root(self: PathReference, size = 0): PartialScope { + let refs: PathReference[] = new Array(size + 1); + + for (let i = 0; i <= size; i++) { + refs[i] = UNDEFINED_REFERENCE; + } + + return new PartialScopeImpl(refs, null, null, null).init({ self }); + } + + static sized(size = 0): Scope { + let refs: PathReference[] = new Array(size + 1); + + for (let i = 0; i <= size; i++) { + refs[i] = UNDEFINED_REFERENCE; + } + + return new PartialScopeImpl(refs, null, null, null); + } + + constructor( + // the 0th slot is `self` + readonly slots: Array>, + private callerScope: Option>, + // named arguments and blocks passed to a layout that uses eval + private evalScope: Option>>, + // locals in scope when the partial was invoked + private partialMap: Option>> + ) {} + + init({ self }: { self: PathReference }): this { + this.slots[0] = self; + return this; + } + + getSelf(): PathReference { + return this.get>(0); + } + + getSymbol(symbol: number): PathReference { + return this.get>(symbol); + } + + getBlock(symbol: number): Option> { + let block = this.get(symbol); + return block === UNDEFINED_REFERENCE ? null : (block as ScopeBlock); + } + + getEvalScope(): Option>> { + return this.evalScope; + } + + getPartialMap(): Option>> { + return this.partialMap; + } + + bind(symbol: number, value: ScopeSlot) { + this.set(symbol, value); + } + + bindSelf(self: PathReference) { + this.set>(0, self); + } + + bindSymbol(symbol: number, value: PathReference) { + this.set(symbol, value); + } + + bindBlock(symbol: number, value: Option>) { + this.set>>(symbol, value); + } + + bindEvalScope(map: Option>>) { + this.evalScope = map; + } + + bindPartialMap(map: Dict>) { + this.partialMap = map; + } + + bindCallerScope(scope: Option>): void { + this.callerScope = scope; + } + + getCallerScope(): Option> { + return this.callerScope; + } + + child(): Scope { + return new PartialScopeImpl( + this.slots.slice(), + this.callerScope, + this.evalScope, + this.partialMap + ); + } + + private get>(index: number): T { + if (index >= this.slots.length) { + throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`); + } + + return this.slots[index] as T; + } + + private set>(index: number, value: T): void { + if (index >= this.slots.length) { + throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`); + } + + this.slots[index] = value; + } +} diff --git a/packages/@glimmer/runtime/lib/vm/append.ts b/packages/@glimmer/runtime/lib/vm/append.ts index abda2cb7e0..eb767c9788 100644 --- a/packages/@glimmer/runtime/lib/vm/append.ts +++ b/packages/@glimmer/runtime/lib/vm/append.ts @@ -53,7 +53,7 @@ import { CheckNumber, check } from '@glimmer/debug'; import { unwrapHandle } from '@glimmer/util'; import { combineSlice } from '../utils/tags'; import { DidModifyOpcode, JumpIfNotModifiedOpcode, LabelOpcode } from '../compiled/opcodes/vm'; -import { ScopeImpl } from '../environment'; +import { PartialScopeImpl } from '../scope'; import { APPEND_OPCODES, DebugState, UpdatingOpcode } from '../opcodes'; import { UNDEFINED_REFERENCE } from '../references'; import { ARGS, CONSTANTS, DESTRUCTOR_STACK, HEAP, INNER_VM, REGISTERS, STACKS } from '../symbols'; @@ -488,7 +488,7 @@ export default abstract class VM implements PublicVM, I } pushRootScope(size: number): PartialScope { - let scope = ScopeImpl.sized(size); + let scope = PartialScopeImpl.sized(size); this[STACKS].scope.push(scope); return scope; } @@ -580,7 +580,7 @@ export default abstract class VM implements PublicVM, I function vmState( pc: number, - scope: Scope = ScopeImpl.root(UNDEFINED_REFERENCE, 0), + scope: Scope = PartialScopeImpl.root(UNDEFINED_REFERENCE, 0), dynamicScope: DynamicScope ) { return { @@ -610,7 +610,7 @@ export class AotVM extends VM implements InternalVM { runtime, vmState( runtime.program.heap.getaddr(handle), - ScopeImpl.root(UNDEFINED_REFERENCE, 0), + PartialScopeImpl.root(UNDEFINED_REFERENCE, 0), dynamicScope ), treeBuilder @@ -624,7 +624,7 @@ export class AotVM extends VM implements InternalVM { { handle, self, treeBuilder, dynamicScope }: InitOptions ) { let scopeSize = runtime.program.heap.scopesizeof(handle); - let scope = ScopeImpl.root(self, scopeSize); + let scope = PartialScopeImpl.root(self, scopeSize); let pc = check(runtime.program.heap.getaddr(handle), CheckNumber); let state = vmState(pc, scope, dynamicScope); let vm = initAOT(runtime, state, treeBuilder); @@ -666,7 +666,7 @@ export class JitVM extends VM implements InternalJitVM { { handle, self, dynamicScope, treeBuilder }: InitOptions ) { let scopeSize = runtime.program.heap.scopesizeof(handle); - let scope = ScopeImpl.root(self, scopeSize); + let scope = PartialScopeImpl.root(self, scopeSize); let state = vmState(runtime.program.heap.getaddr(handle), scope, dynamicScope); let vm = initJIT(context)(runtime, state, treeBuilder); vm.pushUpdating(); @@ -682,7 +682,7 @@ export class JitVM extends VM implements InternalJitVM { runtime, vmState( runtime.program.heap.getaddr(handle), - ScopeImpl.root(UNDEFINED_REFERENCE, 0), + PartialScopeImpl.root(UNDEFINED_REFERENCE, 0), dynamicScope ), treeBuilder