Skip to content

Commit

Permalink
Moving demos around
Browse files Browse the repository at this point in the history
  • Loading branch information
mmocny committed Nov 21, 2023
1 parent a1087de commit 4a4af7c
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 15 deletions.
45 changes: 45 additions & 0 deletions sandbox/LoAF-and-postTask/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Long Animation Frames (LoAF) and postTask api

Demo to showcase tasks, task queues, task priority, and the interplay with Rendering and Input.

- Once a task starts running, it will not get pre-empted (it will block main thread).
- After a task is done running, the next task in the queue will get scheduled.
- There are several task queues, with different priority levels.
- Tasks are scheduled in FIFO order, within the same priority level.
- "Rendering" has the same priority ("normal" aka "user-visible") as all other tasks, by default.
- Rendering may increase its priority after:
- 100ms of being in its task queue
- A discrete input event arrives (as of chromium m109)

In other words, tasks must yield() regularly in order to allow other tasks to run-- including high priority work like input and rendering.

However, if there other normal priority tasks already in the queue, rendering may still wait for its turn.

If you really want to force rendering-- you need to empty the "user-visible" priority task queue. One way to do this is to force all queued tasks temporarily to "background" priority.

## Other notes

### isInputPending

- `isInputPending()` is a signal that can be used to decide if it is worth yield()ing...
- However:
- It is true only BEFORE an event gets dispatched (before event handlers fire)
- Inside event handlers and after events handlers (but still before next rendering opporunity), `isInputPending()` is false again.
- So, any library code that does:

```js
if (!isInputPending())
do_expensive_work();
```
- ... will actually block the main thread if called from event handlers, or rAF, or tasks that happen to get scheduled in the event->rendering gap.
- Therefore, I suggest to just remove the check for `isInputPending()`, and just always yield.
- If you know specifically that there is too much yielding and this is causing performance regressions due to too much scheduling overhead-- then you can switch something like `yieldIfNeeded()` and only actually yield every few ms.
- You can still use `await yieldIfNeeded()` and return a resolved promise to run in microtask queue, or `if (yieldIsNeed())` if you don't want to do even that.

### Controlling the loop

- I would implement a `function markNeedsNextPaint()`.
- ...and then call `markNeedsNextPaint()` in places that update important UI.
- Then, your task scheduler can use that signal to decide to postpone work until after rendering, i.e.:
- wait for requestPostAnimationFrame
- or, decrease postTask priority to background until next rAF
40 changes: 40 additions & 0 deletions sandbox/LoAF-and-postTask/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
body {
margin: 0;
padding: 0;

background-color: #333;
}

#ribbon {
position: absolute;
top: 0;
left: 0;
}

main {
min-height: 100vh;
min-width: 100vw;

display: grid;
align-items: center;
justify-items: center;

font-size: 3em;
color: #eee;
}

main > div:first-child {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

#stopButton {
margin-top: 3em;
background-color: #555;
color: #eee;
padding: 1em;
border: 2px solid #111;
border-radius: 1em;
}
20 changes: 20 additions & 0 deletions sandbox/LoAF-and-postTask/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!doctype html>

<head>
<meta name="viewport" content="width=device-width"/>
<script src="https://unpkg.com/scheduler-polyfill"></script>
<link rel="stylesheet" href="index.css"></link>
</head>

<body>
<div id="ribbon">
<a href="https://github.com/mmocny/LoAF-and-postTask"><img decoding="async" loading="lazy" width="149" height="149" src="https://github.blog/wp-content/uploads/2008/12/forkme_left_darkblue_121621.png?resize=149%2C149" class="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1"></a>
</div>
<main>
<div>
<div id="timer">0</div>
<input id="stopButton" type="button" value="stop"></input>
</div>
</main>
<script type="module" src="./index.js" async></script>
</body>
132 changes: 132 additions & 0 deletions sandbox/LoAF-and-postTask/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// const DELAY_PER_FRAME = 200; // ms. Using 100 because that is the deadline for NORMAL priority rendering in Chrome.
const DELAY_PER_TASK = 10; // ms.
const DEMO_LENGTH = 10_000; // ms.
const PRIORITY = ['user-blocking', 'user-visible', 'background'][1];

const controller = new TaskController({ priority: PRIORITY });

function markNeedsNextPaint() {
// We cannot programatically *increase* the priority of Rendering...
// but we can decrease the priority of all the tasks we control
if (location.search.length > 0) {
controller.setPriority('background');
}
}

function markDoneNextPaint() {
controller.setPriority(PRIORITY);
}

/*
* Block the main thread for `ms`
*/
function block(ms) {
const target = performance.now() + ms;
while (performance.now() < target);
}

/*
* Schedule `numTasks` to run on main thread
* Rather than a "waterfall" of tasks, such as:
* setTimeout(() => setTimeout...)
* These are all scheduled up front, in FIFO order
*/
function createTasks(numTasks, cb) {
for (let i = 0; i < numTasks; i++) {
scheduler.postTask(cb, {
signal: controller.signal,
}
).catch((ex) => { });
}
}

/*
* Start a rAF loop
*/
function startRAFLoop(cb) {
let rafid = 0;
function raf() {
rafid = requestAnimationFrame(() => {
cb();
raf();
});
}
raf();
return () => cancelAnimationFrame(rafid);
}

/*
* New experimental feature, better than traditional Long Tasks
*
* open -a /Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --args --force-enable-metrics-reporting --enable-blink-featutres=LongAnimationFrameTiming
*/
function startMarkingLoAF() {
if (!PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')) {
return console.warn(
'LoAF Entry type not supported. Type launching Canary with --enable-blink-featutres=LongAnimationFrameTiming'
);
}

const observer = new PerformanceObserver((list) => {
for (let entry of list.getEntries()) {
console.log(entry);
performance.measure('LoAF', {
start: entry.startTime,
duration: entry.duration,
});
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
}



/*
* Lets get going!
*/
function main() {
startMarkingLoAF();

const start = performance.now();

createTasks(1000, () => {
// Update the DOM
const now = performance.now()
timer.innerHTML = now.toFixed(0);

// Add a bit of work to the task
block(10);

// If you "need a next paint" quickly...
markNeedsNextPaint();
});

const stopRAFLoop = startRAFLoop(() => {
markDoneNextPaint();
});

const stopTheDemo = () => {
stopRAFLoop();
controller.abort();
};
stopButton.addEventListener('click', stopTheDemo);
scheduler.postTask(stopTheDemo);
}

setTimeout(() => {
main();
}, 1000);














1 change: 1 addition & 0 deletions sandbox/event-based-event-timing
Submodule event-based-event-timing added at 0c5e16
1 change: 1 addition & 0 deletions sandbox/react-fiber-render-tests
Submodule react-fiber-render-tests added at ea351d
34 changes: 19 additions & 15 deletions sandbox/web-mightals.js/apps/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,23 @@ function primes_sol2() {
}

function primes_sol3() {
return range(2, Infinity).pipe(function pNumbers(source$) {
return source$.pipe(
first(),
concatMap((pn) =>
source$.pipe(
filter((n) => n % pn !== 0),
// startWith(pn),
pNumbers
)
),
tap(console.log)
return range(2, Infinity)
.pipe(
share(),
function filterPrimes(source$) {
return source$.pipe(
first(),
concatMap((pn) =>
source$.pipe(
tap(n => console.log('pipeMap', 'pn=', pn, 'n=', n)),
filter((n) => n % pn !== 0),
filterPrimes,
startWith(pn),
)
),
);
}
);
});
}


Expand All @@ -131,7 +135,7 @@ function pipeMap(cb) {
return source$ => source$.pipe(
first(), // Use this to stop the stream... we want to switchMap exactly once in this stream.
switchMap(value => source$.pipe( // TODO: this re-subscribes to the source$ stream, which is not ideal if that stream is COLD. Besides forcing HOT, is there some way to avoid complete+resubscribe? How does switchMap() do it internally?
tap(value2 => console.log('pipeMap', 'pn=', value, 'n=', value2)),
// tap(value2 => console.log('pipeMap', 'pn=', value, 'n=', value2)),
cb(value),
pipeMap(cb),
startWith(value),
Expand All @@ -142,14 +146,14 @@ function pipeMap(cb) {
function primes_sol4() {
return range(2, Infinity)
.pipe(
share(),
share(), // TODO: Any alternatives to this?
pipeMap(
pn => filter((n) => n % pn !== 0)
),
);
}

primes_sol4()
primes_sol3()
.pipe(
take(10),
).subscribe(
Expand Down

0 comments on commit 4a4af7c

Please sign in to comment.