Coder Social home page Coder Social logo

wicg / scheduling-apis Goto Github PK

View Code? Open in Web Editor NEW
888.0 56.0 41.0 555 KB

APIs for scheduling and controlling prioritized tasks.

Home Page: https://wicg.github.io/scheduling-apis/

License: Other

HTML 73.50% JavaScript 1.14% Makefile 1.31% Bikeshed 24.06%

scheduling-apis's Introduction

Scheduling APIs

This document outlines the motivation for working on various scheduling APIs1, discusses some of the problems that apps and userspace schedulers face when writing scheduling code, and links to various proposals we are working on in this space.

(1The scope of this work was previously restricted to main-thread scheduling, and while main-thread scheduling remains the primary focus, the repository and some accompanying text has been renamed to "scheduling-apis" to reflect the inclusion of APIs like scheduler.postTask() on workers.)

Motivation: Main-thread Contention

Applications may experience main-thread contention at various points in their execution, e.g. during page load or as a result of user interaction. This contention can negatively affect user experience in terms of responsiveness and latency. For example, a busy main thread can prevent the UA from servicing input, leading to poor responsiveness. Similarly, tasks (e.g. fetch completions, rendering, etc.) can experience large queuing durations during times of contention, which increases task latency and can result in degraded quality of experience.

Consider a "search-as-you-type" application. This app needs to be responsive to user input, i.e. users typing in the search-box. At the same time, any animations on the page must be rendered smoothly, and the work for fetching and preparing search results and updating the page must also progress quickly. There are a lot of different deadlines to meet for the app developer. It is easy for any long running script work to hold up the main thread and cause responsiveness issues for typing, rendering animations, or updating search results.

Another example pinch-zooming in a map application. The app needs to continuously respond to the input, update the rendering, and potentially fetch new content to be displayed. Similar to the search-as-you-type example, long running script work could block other tasks, making the application feel laggy.

Current Solutions, Their Limitations, and APIs to Fill the Gaps

Dealing with contention is largely a scheduling problem: to the degree that work can be reordered in an more optimal way, scheduling can have a positive impact. What makes this problem more pronounced on the web is that tasks run to completion—the UA cannot preempt a task to run high priority work like processing user input. This problem is generally tackled in userspace by systematically chunking and scheduling main-thread work. Since long tasks and responsiveness are at odds, breaking up long tasks can help keep an app responsive when also yielding to the browser's event loop.

Userspace schedulers have evolved to manage these chunks of work—prioritizing and executing work async at an appropriate time relative to current situation of user and browser. And while userspace schedulers have been effective in improving responsiveness, there are several problems they still face:

  1. Coordination between (cooperating) actors: Most userspace schedulers have a notion of priority that allows tasks to be ordered in a way that improves user experience. But this is limited since userspace schedulers do not control all tasks on the page.

    Apps can consist of 1P, 1P library, 3P, and (one or more) framework script each of which competes for the main thread. At the same time, the browser also has tasks to run on the main thread, such as fetch() and IDB tasks and garbage collection.

    Having a shared notion of priority can help the browser make better scheduling decisions, which in turn can help improve user experience. We propose adding a prioritized task scheduling API to address this problem.

  2. A disparate set of scheduling APIs: Despite the need to schedule chunks of script, the Platform lacks a unified API to do so. Developers can choose setTimeout, postMessage, requestAnimationFrame, or requestIdleCallback, when choosing to schedule tasks.

    This disparate set of scheduling APIs makes it even more difficult for developers to write scheduling code and requires expert knowledge of the browser's event loop to do so. Creating a unified native scheduling API —scheduler.postTask() —will alleviate this.

  3. Determining when to yield to the browser: yielding has overhead—the overhead of posting a task and context switching, the cost of regaining control, etc. This can lead to increased task latency.

    Making intelligent decisions about when to yield is difficult with limited knowledge. Scheduling primitives can help userspace schedulers make better decisions, e.g. isInputPending() and isFramePending().

  4. Regaining control after yielding: chunking work and yielding is necessary for improving responsiveness, but it comes at a cost: when yielding to the event loop, a task that yields has no way to continue without arbitrary work of the same priority running first, e.g. other script. This disincentivizes yielding from a script that requires low task latency. Providing a primitive like scheduler.yield() that is designed to take into account this async userspace task model can help, as the scheduler can prioritize these continuations more fairly.

Additional Scheduling Problems

The problem as described above only covers part of the scheduling problem space. Additionally, there are developer needs for things like detecting when a frame is pending, throttling the frame rate, and avoiding layout thrashing. Some of the other APIs we are considering in this space are noted here.

APIs and Status

API Abstract Status Links
scheduler.postTask() An API for scheduling and controlling prioritizing tasks. This feature shipped in Chromium M94 Explainer
Spec
Polyfill
scheduler.yield() An API for breaking up long tasks by yielding to the browser, continuing after being rescheduled by the scheduler. This feature is available behind a flag in Chromium M113. Explainer
scheduler.wait() This enables tasks to yield and resume after some amount of time, or perhaps after an event has occurred. This feature is currently being co-designed with scheduler.yield(). Related Discussion
scheduler.currentTaskSignal This API provides a way to get the currently running task's TaskSignal, which can be used to schedule dependent tasks. This API is currently being re-evaluating in the context of scheduler.yield(). Explainer
Prioritized Fetch Scheduling Using a TaskSignal or postTask priorities for resource fetching would enable developers to prioritize critical resources, or deprioritize less critical ones. This feature is actively being designed. Early Proposal
isInputPending() An API for determining if the current task is blocking input events. This API shipped in Chrome M87. Explainer
Spec
web.dev

Further Reading / Viewing

scheduling-apis's People

Contributors

ahaoboy avatar amilajack avatar domenic avatar jakearchibald avatar jyasskin avatar natechapin avatar shaseley avatar spanicker avatar yoavweiss avatar zo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

scheduling-apis's Issues

Controller-based API

Should the postTask API use controllers & signals like Abort[Controller|Signal], and how?

AbortController is used for fetch() and other async APIs, which is the TAG-approved way of canceling async work . The postTask API includes a task.cancel() method for dequeueing pending tasks, and changing this to use an Abort[Controller|Signal] came up in the TAG design review.

The power of using the approach comes from wanting to cancel postTask tasks and other async work en masse. The same would be true of a PrioritySignal if we end up supporting (re)prioritization of other async work.

But IMO there is a trade-off with ergonomics and abstractions if using a strict controller-based API, which is explored here. There may be options, however, to combine these controllers/signals with scheduling abstractions like Task and TaskQueue in an ergonomic way, which we should also explore.

TaskController constructor should probably take a dictionary instead of an optional priority

The TaskController constructor currently takes a single optional parameter for priority. The TAG design principles suggest preferring dictionary parameters over primitive parameters for optional parameters. Although we don't have plans right now to add more parameters, switching to a dictionary future-proofs the API at the cost of a bit of verbosity.

let controller = new TaskController('background');

would become

let controller = new TaskController({priority: 'background'});

Need to remove the entry from the task queue maps when the queue is empty

We have two task queue maps in the spec, one for dynamic tasks and the other one for static tasks. The spec defines when we should add entries to those maps, but doesn't define when we should remove entries from those maps. We should remove entries when the corresponding task queues become empty.

Scheduler throttling time/count limit per frame

Current scheduler in Chromium and Firefox behaves differently and inconsistently in terms of how many async tasks get executed per animation frame.

For example, the following code:

 for (let i = 0; i < 1000; i++) {
   setTimeout(() => {
     spin(); // do some work

     // paint something...
     document.getElementById("count").innerText = count++;

     tasks.push(`task ${i}`);
   }, 0);
 }

Will cause in Chromium around 9 tasks to be executed per frame – regardless of how long each task takes. In Firefox the behavior is different and it schedules 1-3 tasks per animation frame.
This behavior makes it easy for many async tasks to accumulate in a single frame and reduce UI responsiveness.

With the new proposal I don’t see that this crucial problem is being addressed since there is no definition for browser implementers how many tasks should be scheduled per frame.
Users of the new scheduling API have to implement a wrapping layer on top of it to ensure that when a task needs to be scheduled – enough time is remaining in the current frame to prevent long blocking periods of the main thread.

When experimenting with this feature in it looks like that tasks with ‘user-visible’ and ‘user-blocking’ priorities get scheduled in correct order relative to one another. However – almost no throttling happens and like the setTimeout tasks they get accumulated (up to 30 tasks in Chromium) and executed in the same animation frame. This behavior limits the usefulness of these 2 priorities since if they are executed in the same frame but just in different order it doesn’t much help address the original need to prioritizing tasks since they blocking time will be the same.

The tasks scheduled with ‘background’ priority behave in Chromium a similar way to how setTimeout behaves in Firefox in terms of how many get executed per animation frame which makes this priority very useful.

I hope my explanation above makes sense.

My proposal to address this issue is to add a frame throttling limit (or any other name) for each priority, which will allow users define how many tasks from a given priority they are willing to accept.

So perhaps something like this:

scheduler.userBlockingThrottleCount = 10; 
scheduler.userVisibleThrottleCount = 5; 
scheduler.backgroundThrottleCount = 1; 

Another, maybe simpler (?), option is allow users to set on the scheduler how long tasks should be scheduled to be executed on the main thread in a single animation frame before they get throttled.

For example:

scheduler.maxPerFrameBudgetMs = 10;

So in this case - if a tasks execute on the main thread for over 10 ms in the same animation frame - no other tasks will be scheduled in that frame regardless of their priority.

This approach is used for example by https://github.com/justinfagnani/queue-scheduler

Alternatively there could reasonable throttling defaults which enable sufficient differentiation between the priorities besides order of execution. E.g.: ‘background’: 2, ‘user-visible’: 5, ‘user-blocking’: 30.

I think if we have some way to limit the tasks executed every frame it would eliminate the need to web developers for using additional wrapping code on top of native browser scheduler - which I believe is one of the main goals of the main-thread-scheduling proposal.

Attaching a file with experimentation with the new APIs which helped me understanding the behaviors and the main downside I see.

priority.zip

[Suggestion] isTaskPending

From other proposals we have isFramePending and isInputPending however it would also be useful to have a more general isTaskPending which returns true if any task of higher priority is scheduled to run. This would also include tasks created by the browser, i.e. isInputPending() and isFramePending() would both imply isTaskPending().

The usage would be similar to is{Input,Frame}Pending:

await scheduler.postTask(async function createNoiseTexture() {
    const texture = new Texture(1000, 1000);
    
    for (let x = 0; i < texture.width; x += 1) {
       for (let y = 0; i < texture.height; x += 1) {
           // Allow any higher priority tasks to run, or frame tasks
           // or input tasks as well (and possibly other browser tasks as well)
           if (scheduler.isTaskPending("background")) {
               await scheduler.yield("background");
           }
           texture.setPixel(x, y, generateNoiseRGB());
       }
    }
}, { priority: "background" });

Promise-based API

To respond to the request from the TAG for better ergonomics when working with Promises and async/await, I think there's a really simple possibility: a TaskQueue.prototype.yield method could vend a Promise that resolves with a given value when the queue gives it time to run. Here, we wouldn't be changing the default of how async/await queues tasks eagerly, but rather building on it.

A polyfill implementation and some usage examples:

// Polyfill
TaskQueue.prototype.yield = function(value) {
  let resolve;
  const promise = new Promise (r => resolve = r);
  this.postTask(() => resolve(value));
}

// Usage with async/await
async function doLater() {
  await myQueue.yield();
  await doThing();
}

// Usage with .then
myPromise.then(myQueue.yield.bind(myQueue)).then(doStuff);

There are various ways we could make this "fancier" on the JS side (making it an auto-bound method and custom thenable), to avoid the need for parens in the await case, and the bind in the then case, if desired. Then, the code could look like:

// Usage with async/await
async function doLater() {
  await myQueue.yield;
  await doThing();
}

// Usage with .then
myPromise.then(myQueue.yield).then(doStuff);

It's so short to define this in JS that I am not sure if it needs to be built-in, but I wanted to suggest it if you're looking for ways to improve ergonomics.

Recurring schedules?

I didn't see anything about how this API will allow the scheduling of recurring tasks, like an interval, which has a different behavior than recursively scheduling one task at a time could ever provide. Did I just miss it? Also how does prioritization fit in there?

Use Cases

Hey,

I'm trying to figure out what use cases this API is good for. It looks pretty cool and I recently saw it's implemented in Chrome (also cool).

The README mentions a complicated use case of main thread contention like a "search as you type" but I'm not sure I grok why this API would address this.

I know there is a TODO there to add an entire document of use cases - but I think it would go a long way (motivationally) to just upload an example of:

  • The example from the README without the use of this API demonstrating the problem this API solves.
  • The example from the README with the use of this API - demonstrating how it solves them.

Aborting tasks with async hops

FF folks recently added test cases (thanks!) for aborting ongoing tasks from within the task itself. The second test fails in Chromium, which is essentially boils down to:

  const controller = new TaskController();
  const signal = controller.signal;
  const p = scheduler.postTask(async () => {
    await new Promise(resolve => setTimeout(resolve, 0));
    // Should |p| be resolved or rejected?
    controller.abort();
  });

In Chromium, p is not rejected because we resolve the postTask promise with the promise returned by the async function, so when the controller aborts it's a no-op. I believe this matches the current spec text (5.1 of the schedule a task to invoke a callback algorithm) since there we resolve the promise with the result of invoking a callback, which should return a promise in the async function case.

But, I think the behavior of the test makes more sense and that we should update the spec to match. I think that would involve "awaiting" the result of invoking the callback and resolving the promise result with that value (or propagating the error). @sefeng211 is that what you did in the FF implementation?

Standardization

What's the plan for this API? It seems it would make some sense inside HTML as it offers an API for the event loop.

cc @domenic

Consider removing additional arguments to postTask

It's not clear why anyone would want to pass additional arguments to the callback through postTask itself instead of using an arrow function. I think setTimeout did this because it predated arrow functions? I think it would be nice to avoid having that extra complexity in postTask and to use arrow functions instead.

postTask(callback, undefined, 1, 2)

becomes

postTask(() => callback(1, 2))

This is also safer for long term evolution of the API. If postTask was to change what arguments it passes into the callback it would break the first form because it would shift down the extra arguments. The second form let's us change what postTask forwards in the future in a backwards compatible way.

@shaseley

scheduler.yield seems to supersede image loading operations

I have an app where the users can have a lot of items and there's a need to filter them. These filter operations, run on each item in their collection, can take a long time. For a while I've been using the setTimeout workaround to defer to the UI thread periodically in order to show some of the items as the larger filter process continues, which may be for several seconds.

I optionally added the scheduler.yield feature, which I hoped would improve performance. I haven't done a thorough test of speeds or page responsiveness, but I have noticed some strange behavior when results include unloaded images.

In this demo video, starting at 28s, sprites for results that haven't loaded on the webpage yet are not going to load until the completion of the filter. Other UI affordances, like updating the progress bar and pagination, do continue to update.

Then I disable a flag in the app and revert back to the setTimeout behavior and do a page refresh. You can see in this second action that the sprites load as soon as they appear in the results.

Images that have previously been loaded on the page do seem to be loaded fine, such as the red 1 background. But it seems like there's some other blocking happening which I would consider detrimental to the user experience.

Consider adding a previous priority event property for priority change events

This seems like it would be useful information, and isn't currently implemented. For example, something like:

let controller = new TaskController();  // user-visible priority
controller.signal.addEventListener('prioritychange', (e) => {
  console.log(e.previousPriority);  // user-visible
  console.log(e.currentTarget.priority) // background
});
controller.setPriority('background');

Structured concurrency and cancellation

Hi,

Structured concurrency (task trees that share a cancellation & error propagation scope) is a very useful way to tame concurrent programming, and has been adopted in Kotlin, Swift, and other languages and frameworks.

Since this proposal deals with tasks and cancellability, are there any plans to enable structured concurrency? I think AsyncContext would enable propagation of signals and priorities through the "async call stack".

I presume cancellation would still be have to be cooperative, listening to a AbortController.signal, so it would make sense to make this signal available from within the task (possibly using AsyncContext).

Standard APIs (like fetch, setTimeout, etc) could of course be updated to listen to a default inherited "async AbortSignal" instead of manually passing them the signal, which would provide for an amazing DX.

Kind regards,
Bjorn

Offering my help (from the perspective of building a userspace scheduler)

I'm not sure if this would be useful. I'm creating it because I suspect you need feedback in order to finalize the API. Feel free to close this issue if it's not.

I've implemented a main thread scheduling API with the current browser APIs — main-thread-scheduling.

I've been using the implementation for the last 2 years. I've been in contact with Flux.ai who are also using it extensively.

For me, an interesting question is: Does the new APIs have a way to do what isTimeToYield() method does in main-thread-scheduling?

If you think I may help you feel free to ask me questions.

postTask and scheduling microtasks: can the API make this ergonomic?

We've heard previously and recently that microtasks can be a problem for developers because they can arbitrarily extend the length of tasks, and that having an easy/ergonomic way to schedule them as macrotasks would be beneficial. Developing postTask seems like a good opportunity to evaluate if there's anything we can do to help, especially through the API itself.

I threw together this proposal based on recent partner conversations and on previous ideas we had when we started thinking about scheduling.

Problem

Microtasks can limit the effectiveness of scheduled code by arbitrarily extending the length of tasks. Often times developers want async/macrotask behavior but end up with synchronous code.

For example:

scheduler.postTask(foo)
 .then((result) => doSomethingExpensive(result))
 .then((result) => doSomethingElseExpensive(result))
 .then((result) => andSoOn(result));

Or with async-await syntax:

(async function() {
  let result = await scheduler.postTask(foo);
  result = await doSomethingExpensive(result);
  result = await doSomethingElseExpensive(result);
  await andSoOn(result);
})();

Both of these examples can be problematic if the microtasks are long, i.e. higher priority work like input doesn't have a chance to run in between functions. Or this might be intended, either for correctness or efficiency.

Proposal

Give developers an ergonomic choice between a microtask and macrotask.

  • scheduler.postTask returns a subclass of Promise that has a .schedule() method.
  • The .schedule callback runs in a macrotask via scheduler.postTask, with the signal set to scheduler.currentTaskSignal.

Example:

scheduler.postTask(foo, {signal: someSignal})
 .schedule((result) => doSomethingExpensive(result))
 .schedule((result) => doSomethingElseExpensive(result))
 .schedule((result) => andSoOn(result))

Proposal++

Extend native Promises to support .schedule(), which is used by async schedule.

(async function() {
  let result = await scheduler.postTask(foo, {signal: someSignal});

  result = await schedule doSomethingExpensive(result);
  result = await schedule doSomethingElseExpensive(result);
  await schedule andSoOn(result);
})();

The JS layer would need to pass the callback to the agent to schedule the task.

Open Questions

  • Should developers be able to pass a signal or priority to .schedule(), or is inheriting scheduler.currentTaskSignal the right thing to do? Passing priority/signal is probably more difficult for the native version.

Alternatives

Non-API Options

Things we could do that don't involve changing API shape:

  1. Defer resolving the task's Promise (the one returned by scheduler.postTask) by scheduling a task that resolves the Promise, giving higher priority work a chance to run

  2. Add yield points between all microtasks for postTask tasks

These options don't give developers a choice, might lead to poor performance when synchronous behavior is desired, and will likely lead to confusion since behavior differs from existing Promises. This could also potentially make polyfills have different (breaking) behavior, and minimally make polyfilling difficult.

Developer Options

The direct alternative is for developers to use scheduler.postTask for posting macrotasks:

scheduler.postTask(foo, {signal: someSignal})
 .then((result) => scheduler.postTask(() => doSomethingExpensive(result), {signal: someSignal}))
 .then((result) => scheduler.postTask(() => doSomethingElseExpensive(result), {signal: someSignal}))
 .then((result) => scheduler.postTask(() => andSoOn(result), {signal: someSignal}))

Or with async-await syntax:

(async function() {
  let result = await scheduler.postTask(foo, {signal: someSignal});
  result = await scheduler.postTask(doSomethingExpensive(result), {signal: someSignal});
  result = await scheduler.postTask(doSomethingElseExpensive(result), {signal: someSignal});
  await scheduler.postTask(andSoOn(result), {signal: someSignal});
})();

These can be made a little cleaner by adding a schedule() method as shorthand, similar to the proposal:

function schedule(task) {
  return scheduler.postTask(task, {signal: scheduler.currentTaskSignal});
}

(async function() {
  let result = await scheduler.postTask(foo, {signal: someSignal});
  result = await schedule(() => doSomethingExpensive(result));
  result = await schedule(() => doSomethingElseExpensive(result));
  await schedule(() => andSoOn(result));
})();

scheduler.postTask(foo, {signal: someSignal})
 .then((result) => schedule(() => doSomethingExpensive(result)))
 .then((result) => schedule(() => doSomethingElseExpensive(result)))
 .then((result) => schedule(() => andSoOn(result)))

This still isn't as ergonomic as the .schedule() proposal, but maybe it's good enough? And if so, maybe it should be supported natively in the platform?

Another option is to add yield points between functions, using a custom yield() function or the proposed scheduler.yield().

(async function() {
  let result = await scheduler.postTask(foo);
  await scheduler.yield();
  result = await doSomethingExpensive(result);
  await scheduler.yield();
  result = await doSomethingElseExpensive(result);
  await scheduler.yield();
  await andSoOn(result);
})();

The same could be done in Promise chains:

let result;

scheduler.postTask(foo)
 .then((r) => { result = r; return scheduler.yield(); })
 .then(() => doSomethingExpensive(result))
 .then((r) => { result = r; return scheduler.yield(); })
 .then(() => doSomethingElseExpensive(result))
 .then((r) => { result = r; return scheduler.yield(); })
 .then((r) => andSoOn(result));

Something else?

These are just things that can be done in conjunction with postTask, but maybe there are other completely different APIs that could help?

Node.js

Does a postTask API make sense for Node.js (in any way)?

I've been thinking of exploring adopting this API or experimenting with it in Node.js with the use case I've been thinking about is similar main thread contention. I have a server that serves a large number of users. That server needs to manage QoS of various levels of requests. Some requests should be very fast while for other requests I really wouldn't mind the users to wait a few milliseconds.

For example if I have a request that hashes an image and stores it for later retrieval - it might be OK for that request to wait until the CPU is idle. Work that takes more than a few milliseconds typically gets put in a (persistent) queue on servers but there is certain work I probably wouldn't mind waiting for.

The same goes for CLI tools, webpack might be interested in performing certain kinds of analysis in a lower priority than actually outputting the compiled code. TypeScript might "emit" before it's done type-checking to give users a snappier experience etc.

The main issue I see here is that Node.js already exposes threads and processes (with worker_threads and child_process) - so users already have access to the operating system scheduler. That said, it would be cool if some code could be made universal and shared here.

self.navigator.scheduling vs self.scheduler

As currently proposed and implemented in a Chrome origin trial we now have two separate locations for scheduling information self.navigator.scheduling and self.scheduler.

I think it would be better if they simply shared the same interface rather than separating these highly related features into two different places.

e.g.:

scheduler.postTask(async () => {
  while (true) {
    if (scheduler.isInputPending() || scheduler.isFramePending()) {
      await scheduler.yield("background");
    }
    // do work
  }
}, { priority: "background" });

How do we measure the effectiveness of this API?

We (Firefox) have it prototyped in Nightly, but we have zero knowledge about how this API is being used in real website.

I wonder if anyone have some concrete examples of some real website usage of this API, to answer some questions like how's it being used, how does the same task gets done before and after the API, how is the task faster than before?

Any thoughts/ideas/suggestions on this topic would be appreciated.

Best way to extend AbortController and AbortSignal?

For the postTask API, we need to build on top of AbortSignal and AbortController to add reprioritizaton, and I'm wondering if folks have opinions on what they think the best (long-term) way to do that is.

Our current implementation matches the FetchController proposal, including the comment about FetchSignal inheriting from AbortSignal. In IDL:

interface TaskController : AbortController {
    constructor(optional TaskPriority priority = "user-visible");
    void setPriority(TaskPriority priority);
};

interface TaskSignal : AbortSignal {
  readonly attribute TaskPriority priority;
  attribute EventHandler onprioritychange;
};

But IMO the inheriting-from-AbortSignal approach comes with a few downsides:

  1. Conceptual correctness: Shouldn't a signal represent an individual event, or at least, shouldn't there be an individual signal for each event?
  2. Not having a separable PrioritySignal makes propagating individual signal components more difficult and less straightforward (see these examples)
  3. It's not clear how scalable a linear class hierarchy of inherited signals will be in the long term

An alternative is to create separate signal components (in this case PrioritySignal, or maybe TaskPrioritySignal) and optionally combine them in some way, for example a map of signals or a composite "meta-signal" using mixins.

I think ideally we'd still have a composite TaskSignal for encapsulation, but make it extend all of the signals it supports. But since multiple inheritance isn't supported, the best we could probably do is use mixins. This has the downside, however, that sharing signals is more complicated — either APIs need to be adjusted to accept the new composite signal, or the individual signal needs to be "plucked out" (e.g. AbortSignal) at callsites.

I'm starting to lean towards the composite/mixin approach, but would like it better if there were an easier way to still share signals. One option might be to create a composite parent class which exposes a .signals property, which APIs could accept and pluck signals out of (e.g. composite.signals['abort']).

I wrote up a bunch more thoughts, along with what different API shapes look like and what I think the pros and cons are.

Thoughts? Strong opinions either way?

/cc @domenic and @jakearchibald who were at one time involved in AbortSignal and FetchSignal and might have thoughts or know others that might.

/cc @jyasskin @natechapin @kjd564 who have been involved in conversations around this.

Questions around this proposal

I work on RxJS, and we have our own implemented schedulers (which frankly I hate, but I see the utility in them). I think it would be ideal to move to using this proposed API when it is more broadly available, however, I'm uncertain if it would meet some of our users' requirements (for better or worse).

  1. Would setting the priority of a task to immediate place it in front of all existing non-immediate tasks? Including promise resolution etc?
  2. How does this relate to and interact with requestAnimationFrame? Can this API be leveraged, at least for scheduling, in place of requestAnimationFrame?

PostTaskPropagation: Explore .NET or other experience with token propagation

https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/attached-and-detached-child-tasks#cancellation-and-child-tasks says that .NET's cancellation system doesn't provide anything like scheduler.currentTaskSignal for getting the current Task's cancelation token. This may be evidence that it's not needed, or there may be evidence out on the web that .NET developers find this frustrating. It'd be good to figure out which way that evidence goes.

Consider extending the behavior of delay to support additional delay types

While implementing early experiments using the postTask scheduler at Airbnb, we took some time to change our early initializers that load things like tracking, google maps, recaptcha and such to use the postTask scheduler. One thing that was incredibly helpful for us in this was extending delay to support waiting until a DOM event, such as load or DOMContentLoaded, we even allowed for a custom event that we fire post React hydration called hydrate-complete. The flexibility and power this gave us helped us to overwhelmingly maximize initial loading performance by deferring non-critical events until after our UI becomes interactive.

// Notice the usage of load to the delay argument instead of a number
postTask(initializeServiceWorker, { priority: 'background', delay: 'load' }); 

// here we wait for our custom hydrate-complete, waiting to do things like show
// a flash banner until just after we hydrate React
postTask(() => Flash.display(), { delay: 'hydrate-complete' });

While the hydrate-complete custom option might be questionable, the extension to allow waiting for standard DOM events seems genuinely useful, especially around 3rd party script loading and deferring non-critical tasks until later. We've seen issues where some of our 3rd party scripts do unfortunate things, like cookie checks or things that might case a repaint or reflow or do some really heavy computation and lead to unnecessary blocking, having this mechanism allowed us to improve both our total blocking time and first input delay considerably, and would love to get folks thoughts on if they think this would be a valuable addition to the spec.

[question] The relationship between the scheduling APIs and built-in functions.

Hi, I am currently working on the zone.js project, zone.js monkey patches the native scheduler APIs such as setTimeout, Promise.then to monitor the async operations. So I have several questions about this new scheduling API.

  1. The priority of the new scheduling API is user blocking, user-visible and background, what is the relationship with the traditional macroTask and microTask?
  2. If the new scheduling API is released, will the current built-in APIs such as setTimeout and Promise.then also call this scheduling API internally?
  3. async/await will schedule microTask internally, if the new scheduling API is released, will the async/await also use this scheduling API internally?
  4. Is that possible to implement customized scheduler provided by user, the purpose is to add some logic to when schedule a task so the user can monitor the status of the async task. Or at lease allow to monkey patch the postTask API.

Thank you.

Specify event loop integration / priority intent

Forking this from here into its own issue.

Problem: The way scheduler task priorities are integrated into the event loop in the spec is underspecified. The original goal was to allow UAs maximum flexibility to integrate these into their schedulers, but the intent is not clear, which is bad for other implementors and compat.

Task Priorities in Blink

This section describes how Blink implements priority mappings, included for reference for how postTask() fits in. Typically, scheduled tasks run in priority order as follows:

Blink Priority Task/Source
Highest Input: Discrete input, e.g. clicks and typing (NOT User Interaction task source), and input blocking tasks (hit test IPC)
Rendering: The first frame after discrete input, some use cases on Android, e.g. main thread gestures and gesture start (touch start)
Very High Rendering: if we haven't rendering in > 100 ms (rendering starvation prevention). This includes rAF-aligned input.
Internal tasks, including find-in-page and IPC forwarding (postMessage)
High 'user-blocking' postTask() tasks
Normal The vast majority of task sources, including networking, timers, posted-message, user interaction, database, etc.
'user-visible' postTask() tasks
Rendering default state
Low 'background' postTask() tasks
Rendering: during compositor-driven scrolling (under experiment to remove)
Best Effort Idle callbacks (during idle periods), a couple internal task types

This is the current state, but there are quite a few inactive — and a few active — experiments I left out.

Notes:

  • Rendering is a special case. We treat rendering (including rAF, rAF-aligned input, paint, etc.) as its own task (compositor task queue), and its priority is very dynamic, fluctuating between low and highest
  • 'user-blocking' can starve other tasks, but not input or rendering
    • Discrete input is always selected first
    • Rendering, including rAF-aligned input, has a starvation prevention mechanism

Specifying Priority

The intention of postTask() priorities:

  1. 'user-blocking' tasks should generally be considered higher priority than other tasks -- especially other task scheduling methods, including setTimeout() and same-window postMessage() (which is used as a scheduling API). But they shouldn't indefinitely block input and rendering.
  2. 'user-visible' tasks should be ~= other ways of scheduling tasks
  3. 'background' should be lower, but can run outside of idle periods

A challenge is that UAs are currently free to choose rendering opportunities and schedule between task types as they see fit, and I don't want to create a total ordering of tasks and stifle UA experimentation. But we need to make clear the intention and consider providing stronger guarantees.

This is also a challenge for scheduler.yield(), where want stronger guarantees (see this section). I wrote there about maybe using a denylist approach, where the relative ordering vs. some tasks sources is specified, providing some guarantees. Doing that for both APIs would be simpler/cleaner, and maybe a good place to start? And it could be potentially augmented with notes about intent and guidance.

Publish postTask Polyfill

Filing an issue so folks can track progress on this.

The current status is that we have a polyfill that was tested during the origin trial, but since the API is still experimental we're waiting until the spec settles to publish it (hopefully soon!). In the meantime, the API is available behind a flag in Chromium if folks want to do local testing.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.