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
43 changes: 42 additions & 1 deletion packages/@ember/-internals/glimmer/lib/base-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,28 @@ function resolveRenderPromise() {
}
}

/**
* SPIKE: revalidation deferred to an animation frame is *expected* to
* leave the renderer invalid at runloop end -- the frame will handle
* it. Without this, loopEnd spins NO_OP runloops (recursing via join)
* until the loop guard throws.
*/
let framePending = false;

export function setFramePending(value: boolean) {
framePending = value;
}

let loops = 0;
function loopEnd() {
for (let renderer of renderers) {
if (!renderer.isValid()) {
if (framePending) {
// the scheduled frame will revalidate; its own runloop will
// re-enter loopEnd and resolve the render promise
return;
}

if (loops > ENV._RERENDER_LOOP_LIMIT) {
loops = 0;
// TODO: do something better
Expand Down Expand Up @@ -368,8 +386,31 @@ export class RendererState {
}
}

#frameScheduled = false;

/**
* SPIKE: coalesce revalidation to at most once per animation frame.
*
* Invalidation bursts (sockets, workers) otherwise trigger a full
* revalidation per runloop flush -- many times per painted frame.
* Only frames that will actually paint need the DOM updated.
*/
scheduleRevalidate(renderer: BaseRenderer): void {
_backburner.scheduleOnce('render', this, this.revalidate, renderer);
if (typeof requestAnimationFrame === 'function') {
if (this.#frameScheduled) {
return;
}

this.#frameScheduled = true;
setFramePending(true);
requestAnimationFrame(() => {
this.#frameScheduled = false;
setFramePending(false);
_backburner.join(() => this.revalidate(renderer));
});
} else {
_backburner.scheduleOnce('render', this, this.revalidate, renderer);
}
}

isValid(): boolean {
Expand Down
25 changes: 7 additions & 18 deletions packages/@ember/-internals/metal/lib/property_get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,24 +111,13 @@ export function _getProp(obj: unknown, keyName: string) {
value = (obj as any)[keyName];
}

if (
value === undefined &&
typeof obj === 'object' &&
!(keyName in obj) &&
hasUnknownProperty(obj)
) {
value = obj.unknownProperty(keyName);
}

if (isTracking()) {
consumeTag(tagFor(obj, keyName));

if (Array.isArray(value) || isEmberArray(value)) {
// Add the tag of the returned value if it is an array, since arrays
// should always cause updates if they are consumed and then changed
consumeTag(tagFor(value, '[]'));
}
}
// SPIKE: deleted legacy read-path support:
// - unknownProperty (ObjectProxy / EmberObject)
// - per-(object, key) tag consumption on arbitrary objects, which
// existed so Ember.set() on POJOs invalidates renders
// - the '[]' EmberArray tag consume for array-valued reads
// Modern semantics: plain-data reads don't entangle; reactivity
// comes from @tracked, tracked collections, and value replacement.
} else {
// SAFETY: It should be ok to access properties on any non-nullish value
value = (obj as any)[keyName];
Expand Down
26 changes: 26 additions & 0 deletions packages/@glimmer/reference/lib/iterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export interface IterationItem<T, U> {
export interface AbstractIterator<T, U, V extends IterationItem<T, U>> {
isEmpty(): boolean;
next(): Nullable<V>;
/**
* SPIKE: allocation-free iteration -- writes into `target` and returns
* it, instead of allocating a fresh item per step. Optional; callers
* must not retain the returned object across steps.
*/
nextInto?(target: V): Nullable<V>;
}

export type OpaqueIterationItem = IterationItem<unknown, unknown>;
Expand Down Expand Up @@ -263,4 +269,24 @@ class ArrayIterator implements OpaqueIterator {

return { key, value, memo };
}

nextInto(target: IterationItem<unknown, number>): Nullable<IterationItem<unknown, number>> {
let value: unknown;

let current = this.current;
if (current.kind === 'first') {
this.current = { kind: 'progress' };
value = current.value;
} else if (this.pos >= this.iterator.length - 1) {
return null;
} else {
value = this.iterator[++this.pos];
}

target.key = this.keyFor(value, this.pos);
target.value = value;
target.memo = this.pos;

return target;
}
}
Loading
Loading