Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
23 changes: 22 additions & 1 deletion packages/@ember/-internals/glimmer/lib/base-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { SimpleDocument, SimpleElement } from '@simple-dom/interface';
import { hasDOM } from '../../browser-environment';
import { EmberEnvironmentDelegate } from './environment';
import ResolverImpl from './resolver';
import { _getStrategy } from '@ember/scheduler';
import { EvaluationContextImpl } from '@glimmer/opcode-compiler/lib/program-context';

export type IBuilder = (env: Environment, cursor: Cursor) => TreeBuilder;
Expand Down Expand Up @@ -368,8 +369,28 @@ export class RendererState {
}
}

#renderer: BaseRenderer | null = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not believe we are done.

Because we haven't wired up all our internals to the scheduler, a user swapping out the scheduler would see no benefit.

#21520 made a lot of progress on that work, and you'll need to copy some of that here (though, only insofar as it applies to enabling the classic strategy.

This is also way too much code.

Read https://github.com/runspired/rfcs/blob/modernized-scheduler/text/0957-modernized-scheduler.md again


// stable identity so strategies (and classic scheduleOnce dedupe) can
// coalesce repeat scheduling between flushes
#revalidateCurrent = (): void => {
if (this.#renderer !== null) {
this.revalidate(this.#renderer);
}
};

scheduleRevalidate(renderer: BaseRenderer): void {
_backburner.scheduleOnce('render', this, this.revalidate, renderer);
this.#renderer = renderer;

const strategy = _getStrategy();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should happen during construction time, i believe, because we don't want to re-check all this strategy stuff eveyr revalidation.

Additionally, I think you'll want to hook in to the environment, since that's where existing backburner integration is (and the scheduler is replacing backburner eventually


if (strategy._scheduleRevalidate !== undefined) {
strategy._scheduleRevalidate(this.#revalidateCurrent);
} else {
// a registered strategy without the internal seam leaves the
// renderer on classic runloop scheduling
_backburner.scheduleOnce('render', this, this.revalidate, renderer);
}
}

isValid(): boolean {
Expand Down
63 changes: 63 additions & 0 deletions packages/@ember/scheduler/-private/classic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { _backburner, next as runloopNext, schedule } from '@ember/runloop';
import type { Strategy } from '@ember/scheduler';

/**
* The ambient default strategy: schedules exactly the way Ember works
* today, so existing applications observe no change in timing. Phases
* map onto the runloop's queues (`render`, then `afterRender`, with
* `composite` re-scheduled behind `layout`'s queue entries within the
* same flush), and the renderer's revalidation is a
* `scheduleOnce('render', ...)`, just as it always was.
*
* Render-aware scheduling (frame-aligned phases, coalesced
* revalidation) is what a swapped-in strategy provides -- see
* `@ember/scheduler/strategy` -- and becomes the source of performance
* wins when it becomes the default.
*
* @internal
*/
class ClassicStrategy implements Strategy {
render(): Promise<void> {
return new Promise((resolve) => schedule('render', null, resolve));
}

layout(): Promise<void> {
return new Promise((resolve) => schedule('afterRender', null, resolve));
}

composite(): Promise<void> {
return new Promise((resolve) =>
schedule('afterRender', null, () => schedule('afterRender', null, resolve))
);
}

next(): Promise<void> {
return new Promise((resolve) => runloopNext(null, resolve));
}

idle(): Promise<void> {
return new Promise((resolve) => {
if (typeof requestIdleCallback === 'function') {
// fully-idle or backgrounded pages can starve requestIdleCallback
// indefinitely; cap the wait to keep the promise resolvable
requestIdleCallback(() => resolve(), { timeout: 500 });
} else {
setTimeout(resolve, 0);
}
});
}

/**
* The renderer's internal seam: how revalidation gets scheduled.
* Classic behavior is a runloop `scheduleOnce`, preserving today's
* timing exactly (the flush callback is stable per renderer, so
* scheduleOnce's dedupe applies as before).
*/
_scheduleRevalidate(flush: () => void): void {
_backburner.scheduleOnce('render', null, flush);
}
}

const classicStrategy: ClassicStrategy = new ClassicStrategy();

export default classicStrategy;
43 changes: 32 additions & 11 deletions packages/@ember/scheduler/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { assert } from '@ember/debug';
import classicStrategy from '@ember/scheduler/-private/classic';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not import this here.


/**
The `@ember/scheduler` package provides a render-aware scheduling interface,
Expand Down Expand Up @@ -67,6 +68,18 @@ export interface Strategy {
composite(): Promise<void>;
next(): Promise<void>;
idle(): Promise<void>;

/**
* Internal seam used by the renderer to schedule revalidation. The
* public phase functions are for user work; revalidation is hotter
* than any user phase, so the renderer talks to the strategy through
* this callback-based hook rather than allocating promises per
* invalidation. Optional: strategies that do not implement it leave
* the renderer on its classic runloop scheduling.
*
* @internal
*/
_scheduleRevalidate?(flush: () => void): void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this here? we can't add to the interface of the scheduler, even for compat

}

let registeredStrategy: Strategy | null = null;
Expand Down Expand Up @@ -123,12 +136,20 @@ export function _clearRegisteredStrategy(): void {
registeredStrategy = null;
}

function getStrategy(phaseName: string): Strategy {
assert(
`Attempted to schedule work into the '${phaseName}' phase, but no scheduling strategy is registered. Register a strategy when defining your Application, e.g. the default strategy:\n\n\timport { registerStrategy } from '@ember/scheduler';\n\timport strategy from '@ember/scheduler/strategy';\n\n\tregisterStrategy(strategy);`,
registeredStrategy !== null
);
return registeredStrategy;
function getStrategy(): Strategy {
// the classic strategy is the ambient default: existing applications
// keep today's runloop scheduling with no registration required, and
// `registerStrategy` swaps in a render-aware implementation
return registeredStrategy ?? classicStrategy;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

classicStrategy probably shouldn't be returned here (I'm questiening the entire purpose of getStrategy atm)

maybe we setStrategy in the environment (glimmer<->ember hookup) and pass the classicStrategy there

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took exactly this direction in df40e9e: no ambient fallback anywhere — the glimmer<->ember hookup (environment.ts, next to setGlobalContext) does registerStrategy(classicStrategy) at initialization, so a booted app always has the classic strategy unless it swaps one in. registerStrategy permits one swap over the classic default and still asserts on conflicting registrations. Full suite 9425/0 and repo lint green with the rework.

}

/**
* The renderer's accessor for the active strategy.
*
* @internal
*/
export function _getStrategy(): Strategy {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function is redundant and unneeded. don't be silly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone — _getStrategy deleted in df40e9e.

return getStrategy();
}

/**
Expand Down Expand Up @@ -156,7 +177,7 @@ function getStrategy(phaseName: string): Strategy {
@public
*/
export function render(): Promise<void> {
return getStrategy('render').render();
return getStrategy().render();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why call getStrategy so much when we can just directly access registeredStrategy?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and removed: getStrategy is gone entirely. The phase functions and the renderer access the exported live binding _registeredStrategy directly (with a pre-boot assert in the phase functions).

}

/**
Expand All @@ -182,7 +203,7 @@ export function render(): Promise<void> {
@public
*/
export function layout(): Promise<void> {
return getStrategy('layout').layout();
return getStrategy().layout();
}

/**
Expand Down Expand Up @@ -212,7 +233,7 @@ export function layout(): Promise<void> {
@public
*/
export function composite(): Promise<void> {
return getStrategy('composite').composite();
return getStrategy().composite();
}

/**
Expand All @@ -237,7 +258,7 @@ export function composite(): Promise<void> {
@public
*/
export function next(): Promise<void> {
return getStrategy('next').next();
return getStrategy().next();
}

/**
Expand All @@ -261,5 +282,5 @@ export function next(): Promise<void> {
@public
*/
export function idle(): Promise<void> {
return getStrategy('idle').idle();
return getStrategy().idle();
}
5 changes: 3 additions & 2 deletions packages/@ember/scheduler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"dependencies": {
"@ember/debug": "workspace:*",
"internal-test-helpers": "workspace:*"
"internal-test-helpers": "workspace:*",
"@ember/runloop": "workspace:*"
}
}
}
42 changes: 35 additions & 7 deletions packages/@ember/scheduler/tests/scheduler_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
idle,
registerStrategy,
_clearRegisteredStrategy,
_getStrategy,
} from '..';
import { moduleFor, AbstractTestCase } from 'internal-test-helpers';

Expand Down Expand Up @@ -45,14 +46,41 @@ moduleFor(
_clearRegisteredStrategy();
}

['@test phase functions assert when no strategy is registered'](assert) {
for (let phase of [render, layout, composite, next, idle]) {
expectAssertion(() => {
phase();
}, /no scheduling strategy is registered/);
}
async ['@test phase functions fall back to the classic strategy when none is registered'](
assert
) {
// no registerStrategy call: the ambient classic default handles
// phases with runloop semantics
let order = [];

await Promise.all([
composite().then(() => order.push('composite')),
layout().then(() => order.push('layout')),
render().then(() => order.push('render')),
]);

assert.deepEqual(order, ['render', 'layout', 'composite']);
}

['@test the renderer seam prefers a registered strategy that implements it'](assert) {
let scheduled = [];

registerStrategy({
render: () => Promise.resolve(),
layout: () => Promise.resolve(),
composite: () => Promise.resolve(),
next: () => Promise.resolve(),
idle: () => Promise.resolve(),
_scheduleRevalidate(flush) {
scheduled.push(flush);
},
});

let flush = () => {};
_getStrategy()._scheduleRevalidate(flush);

assert.expect(5);
assert.strictEqual(scheduled.length, 1, 'the registered strategy received the flush');
assert.strictEqual(scheduled[0], flush, 'with the stable callback');
}

['@test phase functions delegate to the registered strategy'](assert) {
Expand Down
Loading