1. Introduction
This section is non-normative.
Scheduling can be an important developer tool for improving website performance. Broadly speaking, there are two areas where scheduling can be impactful: user-percieved latency and responsiveness. Scheduling can improve user-perceived latency to the degree that lower priority work can be pushed off in favor of higher priority work that directly impacts quality of experience. For example, pushing off execution of certain 3P library scripts during page load can benefit the user by getting pixels to the screen faster. The same applies to prioritizing work associated with content within the viewport. For script running on the main thread, long tasks can negatively affect both input and visual responsiveness by blocking input and UI updates from running. Breaking up these tasks into smaller pieces and scheduling the chunks or task continuations is a proven approach that applications and framework developers use to improve responsiveness.
Userspace
schedulers
typically
work
by
providing
methods
to
schedule
tasks
and
controlling
when
those
tasks
execute.
Tasks
usually
have
an
associated
priority,
which
in
large
part
determines
when
the
task
will
run,
in
relation
to
other
tasks
the
scheduler
controls.
The
scheduler
typically
operates
by
executing
tasks
for
some
amount
of
time
(a
scheduler
quantum)
before
yielding
control
back
to
the
browser.
The
scheduler
resumes
by
scheduling
a
continuation
task,
e.g.
a
call
to
setTimeout()
or
postMessage()
.
While
userspace
schedulers
have
been
successful,
the
situation
could
be
improved
with
a
centralized
browser
scheduler
and
better
scheduling
primitives.
The
priority
system
of
a
scheduler
extends
only
as
far
as
the
scheduler’s
reach.
A
consequence
of
this
for
userspace
schedulers
is
that
the
UA
generally
has
no
knowledge
of
userspace
task
priorities.
The
one
exception
is
if
the
scheduler
uses
requestIdleCallback()
for
some
of
its
work,
but
this
is
limited
to
the
lowest
priority
work.
The
same
holds
if
there
are
multiple
schedulers
on
the
page,
which
is
increasingly
common.
For
example,
an
app
might
be
built
with
a
framework
that
has
a
schedueler
(e.g.
React),
do
some
scheduling
on
its
own,
and
even
embed
a
feature
that
has
a
scheduler
(e.g.
an
embedded
map).
The
browser
is
the
ideal
coordination
point
since
the
browser
has
global
information,
and
the
event
loop
is
responsible
for
running
tasks.
Prioritization
aside,
the
current
primitives
that
userspace
schedulers
rely
on
are
not
ideal
for
modern
use
cases.
setTimeout(0)
is
the
canonical
way
to
schedule
a
non-delayed
task,
but
there
are
often
minimum
delay
values
(e.g.
for
nested
tasks)
which
can
lead
to
poor
performance
due
to
increased
latency.
A
well-known
workaround
is
to
use
postMessage()
or
a
MessageChannel
,
but
these
APIs
were
not
designed
for
scheduling,
e.g.
you
cannot
queue
callbacks.
requestIdleCallback()
can
be
effective
for
some
use
cases,
but
this
only
applies
to
idle
tasks
and
does
not
account
for
tasks
whose
priority
can
change,
e.g.
re-prioritizing
off-screen
content
in
response
to
user
input,
like
scrolling.
This
document
specification
introduces
a
new
interface
for
developers
to
schedule
and
control
prioritized
tasks.
tasks
and
continuations.
A
task
in
this
context
is
a
JavaScript
callback
that
runs
asynchronously
in
its
own
event
loop
task
.
A
continuation
is
the
resumption
of
JavaScript
code
in
a
new
event
loop
task
after
yielding
control
to
the
browser.
The
Scheduler
interface
exposes
a
postTask()
method
to
schedule
tasks,
tasks
and
the
a
yield()
method
to
schedule
continuations.
The
specification
defines
a
number
of
TaskPriorities
that
to
control
task
and
continuation
execution
order.
Additionally,
a
TaskController
and
its
associated
TaskSignal
can
be
used
to
abort
scheduled
tasks
and
control
their
priorities.
2. Scheduling Tasks and Continuations
2.1. Task and Continuation Priorities
This spec formalizes three priorities to support scheduling tasks:
enum {TaskPriority "user-blocking" ,"user-visible" ,"background" };
user-blocking
is
the
highest
priority,
and
is
meant
to
be
used
for
tasks
that
are
blocking
the
user’s
ability
to
interact
with
the
page,
such
as
rendering
the
core
experience
or
responding
to
user
input.
user-visible
is
the
second
highest
priority,
and
is
meant
to
be
used
for
tasks
that
are
observable
to
the
user
but
not
necessarily
blocking
user
actions,
such
as
updating
secondary
parts
of
the
page.
This
is
the
default
priority.
background
is
the
lowest
priority,
and
is
meant
to
be
used
for
tasks
that
are
not
time-critical,
such
as
background
log
processing
or
initializing
certain
third
party
libraries.
Continuation priorities mirror task priorities, with an additional option to inherit the current priority:
enum {ContinuationPriority ,"user-blocking" ,"user-visible" ,"background" };"inherit"
Note:
Tasks
scheduled
through
a
given
Scheduler
run
in
strict
priority
order
,
meaning
the
scheduler
will
always
run
"
user-blocking
"
tasks
before
"
user-visible
"
tasks,
which
in
turn
always
run
before
"
background
"
tasks.
Continuation
priorities
are
slotted
in
just
above
their
TaskPriority
p1
is
less
than
counterparts,
e.g.
a
TaskPriority
user-visible
p2
if
p1
is
less
than
p2
in
the
following
total
ordering:
"
background
continuation
has
a
higher
effective
priority
"
<
"
than
a
user-visible
"
<
"
user-blocking
"
task.
2.2.
The
Scheduler
Interface
dictionary {SchedulerPostTaskOptions AbortSignal ;signal ;TaskPriority ; [priority EnforceRange ]unsigned long long = 0; };delay enum {SchedulerSignalInherit };"inherit" dictionary { (SchedulerYieldOptions AbortSignal or SchedulerSignalInherit );signal ContinuationPriority ; };priority callback =SchedulerPostTaskCallback any (); [Exposed =(Window ,Worker )]interface {Scheduler Promise <any >postTask (SchedulerPostTaskCallback ,callback optional SchedulerPostTaskOptions = {});options Promise <undefined >yield (optional SchedulerYieldOptions = {}); };options
Note:
The
signal
option
can
be
either
an
AbortSignal
or
a
TaskSignal
,
but
is
defined
as
an
AbortSignal
since
it
is
a
superclass
of
TaskSignal
.
For
cases
where
the
priority
might
change,
a
TaskSignal
is
needed.
But
for
cases
where
only
cancellation
is
needed,
an
AbortSignal
would
suffice,
potentially
making
it
easier
to
integrate
the
API
into
existing
code
that
uses
AbortSignals
.
-
result = scheduler .postTask( callback , options ) -
Returns a promise that is fulfilled with the return value of callback
,or rejected with theAbortSignal's abort reason , if the task is aborted. If callback throws an error during execution, the promise returned bypostTask()will be rejected with that error.The task’s
priorityis determined by the combination of option ’spriorityandsignal:-
If option ’s
priorityis specified, then thatTaskPrioritywill be used to schedule the task, and the task’s priority is immutable. -
Otherwise, if option ’s
signalis specified and is aTaskSignalobject, then the task’s priority is determined by option ’ssignal's priority . In this case the task’s priority is dynamic , and can be changed by callingcontroller.setPriority()for the associatedTaskController. -
Otherwise, the task’s priority defaults to "
user-visible".
If option ’s
signalis specified, then thesignalis used by theSchedulerto determine if the task is aborted.If option ’s
delayis specified and greater than 0, then the execution of the task will be delayed for at leastdelaymilliseconds. -
-
result = scheduler .yield( options ) Returns a promise that is fulfilled with
undefinedor rejected with theAbortSignal's abort reason , if the continuation is aborted.By default, the priority of the continuation and the signal used to abort it are inherited from the originating task, but they can optionally be specified in a similar manner as
postTask()through thesignalandpriorityoptions .For determining the
AbortSignalused to abort the continuation:-
If
neither
the
signalnor thepriorityoptions are specified, then option ’ssignalis defaulted to "inherit". If option ’s
signalis "inherit" and the originating task was scheduled with viapostTask()with anAbortSignal, then that signal is used to determine if the continuation is aborted.Otherwise if option ’s
signalis specified, then that is used to determine if the continuation is aborted.
For determining the continuation’s priority:
-
If
neither
the
signalnor thepriorityoptions are specified, then option ’spriorityis defaulted to "inherit". If option ’s
priorityis "inherit", or if option ’spriorityis not set andsignalis "inherit", then the originating task’s priority is used (aTaskSignalor fixed priority). If the originating task did not have a priority, then "user-visible" is used.Otherwise if option ’s
priorityis specified, then thatContinuationPrioritywill be used to schedule the continuation, and the continuation’s priority is immutable.Otherwise, if option ’s
signalis specified and is aTaskSignalobject, then the continuation’s priority is determined dynamically by option ’ssignal's priority .Otherwise, the continuation’s priority defaults to "
user-visible".
-
If
neither
the
A
Scheduler
object
has
an
associated
static
priority
task
queue
map
,
which
is
a
map
from
(
TaskPriority
,
boolean)
to
scheduler
task
queue
.
This
map
is
initialized
to
a
new
empty
map
.
A
Scheduler
object
has
an
associated
dynamic
priority
task
queue
map
,
which
is
a
map
from
(
TaskSignal
,
boolean)
to
scheduler
task
queue
.
This
map
is
initialized
to
a
new
empty
map
.
Note:
We
implement
dynamic
prioritization
by
enqueuing
tasks
associated
with
a
specific
TaskSignal
into
the
same
scheduler
task
queue
,
and
changing
that
queue’s
priority
in
response
to
prioritychange
events.
The
dynamic
priority
task
queue
map
holds
the
scheduler
task
queues
whose
priorities
can
change,
and
the
map
key
is
the
TaskSignal
which
all
tasks
in
the
queue
are
associated
with.
The
values
of
the
static
priority
task
queue
map
are
scheduler
task
queues
whose
priorities
do
not
change.
Tasks
with
static
priorities
—
those
that
were
scheduled
with
an
explicit
priority
option
or
a
signal
option
that
is
null
or
is
an
AbortSignal
—
are
placed
in
these
queues,
based
on
TaskPriority
,
which
is
the
key
for
the
map.
An
alternative,
and
logicially
equivalent
implementation,
would
be
to
maintain
a
single
per-
TaskPriority
scheduler
task
queue
,
and
move
tasks
between
scheduler
task
queues
in
response
to
a
TaskSignal
's
priority
changing,
inserting
based
on
enqueue
order
.
This
approach
would
simplify
selecting
the
next
scheduler
task
queue
from
all
schedulers
,
but
make
priority
changes
more
complex.
The
postTask(
callback
,
options
)
method
steps
are
to
return
the
result
of
scheduling
a
postTask
task
for
this
given
callback
and
options
.
The
yield(
options
)
method
steps
are
to
return
the
result
of
scheduling
a
yield
continuation
for
this
given
options
.
2.3. Definitions
A scheduler task is a task with an additional numeric enqueue order item , initially set to 0.
The following task sources are defined as scheduler task sources , and must only be used for scheduler tasks .
- The posted task task source
-
This task source is used for tasks scheduled through
postTask()oryield().
A scheduler task queue is a struct with the following items :
- priority
-
A
TaskPriority. - is continuation
A boolean.
- tasks
-
A set of scheduler tasks .
- removal steps
-
An algorithm.
A scheduling state is a struct with the following items :
- abort source
An
AbortSignalobject or, initially null.- priority source
A
TaskSignalobject or null, initially null.
A task handle is a struct with the following items :
- task
-
A scheduler task or null.
- queue
-
A scheduler task queue or null.
- abort steps
-
An algorithm.
- task complete steps
-
An algorithm.
2.4. Processing Model
TaskPriority
priority
,
a
boolean
isContinuation
,
and
an
algorithm
removalSteps
:
-
Let queue be a new scheduler task queue .
-
Set queue ’s priority to priority .
-
Set queue ’s is continuation to isContinuation .
-
Set queue ’s removal steps to removalSteps .
-
Return queue .
AbortSignal
or
null
signal
:
-
Let handle be a new task handle .
-
Set handle ’s task to null.
-
Set handle ’s queue to null.
-
Set handle ’s abort steps to the following steps:
-
Reject result with signal ’s abort reason .
-
If task is not null, then
-
Remove task from queue .
-
If queue is empty , then run queue ’s removal steps .
-
-
-
Set handle ’s task complete steps to the following steps:
-
If signal is not null, then remove handle ’s abort steps from signal .
-
If queue is empty , then run queue ’s removal steps .
-
-
Return handle .
| Priority | Is Continuation | Effective Priority |
|---|---|---|
"
background
"
|
false
| 0 |
"
background
"
|
true
| 1 |
"
user-visible
"
|
false
| 2 |
"
user-visible
"
|
true
| 3 |
"
user-blocking
"
|
false
| 4 |
"
user-blocking
"
|
true
| 5 |
2.4.1. Queueing and Removing Scheduler Tasks
-
Let task be a new scheduler task .
-
Set task ’s enqueue order to enqueue order .
-
Set task ’s steps to steps .
-
Set task ’s source to source .
-
Set task ’s document to document .
-
Set task ’s script evaluation environment settings object set to a new empty set .
-
Return task .
We should consider refactoring the HTML spec to add a constructor for task . One problem is we need the new task to be a scheduler task rather than a task .
2.4.2. Scheduling Tasks and Continuations
Scheduler
scheduler
given
a
SchedulerPostTaskCallback
callback
and
SchedulerPostTaskOptions
options
:
-
Let result be a new promise .
-
Let
signalstate be the result of computing the scheduling state from options given scheduler , options ["signal"] if it exists , or otherwise null, and options [""] if it exists , or otherwise null.signalpriority -
Let signal be state ’s abort source .
If signal is not null and it is aborted , then reject result with signal ’s abort reason and return result .
-
Let handle be the result of creating a task handle given result and signal .
-
If signal is not null, then add handle ’s abort steps to signal .
-
Let
priority be options [" priority "] if options [" priority "] exists , or otherwise null. LetenqueueSteps be the following steps:-
Set handle ’s queue to the result of selecting the scheduler task queue for scheduler given
signalstateand’s priority.source and false. -
Schedule a task to invoke
a callbackan algorithm for scheduler given handle and the following steps:Let event loop be the scheduler ’s relevant agent 's event loop .
Set event loop ’s current scheduling state to state .
Let callbackResult be the result of invoking callback
,. If that threw an exception, then reject result, andwith that, otherwise resolvehandleresult with callbackResult .-
Set event loop ’s current scheduling state to null.
-
-
Let delay be options ["
delay"]. -
If delay is greater than 0, then run steps after a timeout given scheduler ’s relevant global object , "
scheduler-postTask", delay , and the following steps:-
If signal is null or signal is not aborted , then run enqueueSteps .
-
-
Otherwise, run enqueueSteps .
-
Return result .
Run steps after a timeout doesn’t necessarily account for suspension; see whatwg/html#5925 .
Scheduler
scheduler
given
SchedulerYieldOptions
options
:Let result be a new promise .
Let abortOption be options ["
signal"] if it exists , otherwise null.Let priorityOption be options ["
priority"] if it exists , otherwise null.If abortOption is null and priorityOption is null, then set abortOption to "
inherit" and set priorityOption to "inherit".Otherwise if abortOption is "
inherit" and priorityOption is null, then set priorityOption to "inherit".Let state be the result of computing the scheduling state from options given scheduler , abortOption , and priorityOption .
Let signal be state ’s abort source .
If signal is not null and it is aborted , then reject result with signal ’s abort reason and return result .
Let handle be the result of creating a task handle given result and signal .
If signal is not null, then add handle ’s abort steps to signal .
Set handle ’s queue to the result of selecting the scheduler task queue for scheduler given state ’s priority source and true.
Schedule a task to invoke an algorithm for scheduler given handle and the following steps:
Resolve result .
Return result .
Scheduler
object
scheduler
AbortSignal
object,
"
inherit
",
or
null
TaskPriority
,
"
inherit
",
or
null
-
Let result be a new scheduling state .
Let inheritedState be the scheduler ’s relevant agent 's event loop 's current scheduling state .
If
prioritysignalOption isnull,"inherit", then:If
signalinheritedState is notnull and implementsnull, then set result ’s abort sourcetheto inheritedState ’s abort source .
Otherwise, set result ’s abort source to signalOption .
If priorityOption is "
TaskSignalinheritinterface, and", then:If
signalinheritedStatehas fixed priority ,is not null, then setpriorityresult ’s priority source tosignalinheritedState ’s priority source .
-
IfOtherwise ifprioritypriorityOption isnull andnot null, then set result ’s priority source to the result of creating a fixed priority unabortable task signal given priorityOption . Otherwise if signalOption is not null and implements the
TaskSignalinterface, then set result ’s priority source to signalOption .If result ’s priority source is null, then set result ’s priority source to the result of creating a fixed priority unabortable task signal given "
user-visible".Return result .
Note: The fixed priority unabortable signals created here can be cached and reused to avoid extra memory allocations.
Scheduler
scheduler
given
a
TaskSignal
object
signal
and
a
boolean
isContinuation
:If signal does not have fixed priority , then
-
If scheduler ’s dynamic priority task queue map does not contain ( signal , isContinuation ), then
-
Let queue be the result of creating a scheduler task queue given signal ’s priority , isContinuation , and the following steps:
-
Remove dynamic priority task queue map
[[( signal , isContinuation].)].
-
-
Set dynamic priority task queue map
[[( signal , isContinuation])] to queue . -
Add a priority change algorithm to signal that runs the following steps:
-
-
Return dynamic priority task queue map
[[( signal , isContinuation].)].
-
-
Otherwise
priority is used to determine the task queue:-
IfLet priorityis null, setbeprioritysignalto " user-visible ".’s priority . -
If scheduler ’s static priority task queue map does not contain ( priority , isContinuation ) , then
-
Let queue be the result of creating a scheduler task queue given priority , isContinuation , and the following steps:
-
Remove static priority task queue map
[[( priority , isContinuation].)].
-
-
Set static priority task queue map
[[( priority , isContinuation])] to queue .
-
-
Return static priority task queue map
[[( priority , isContinuation].)].
-
Scheduler
scheduler
given
a
-
Let global be the relevant global object for scheduler .
-
Let document be global ’s associated
Documentif global is aWindowobject; otherwise null. -
Let event loop be the scheduler ’s relevant
agent’sagent 's event loop . -
Let enqueue order be event loop ’s next enqueue order .
-
Increment event loop ’s next enqueue order by 1.
-
Set handle ’s task to the result of queuing a scheduler task on handle ’s queue given enqueue order , the posted task task source , and document , and that performs the following steps:
-
Let callback result be the result of invoking callback . If that threw an exception, then reject result with that, otherwise resolve result withRuncallback resultsteps . -
Run handle ’s task complete steps .
-
Because this algorithm can be called from in parallel steps, parts of this and other algorithms are racy. Specifically, the next enqueue order should be updated atomically, and accessing the scheduler task queues should occur atomically. The latter also affects the event loop task queues (see this issue ).
2.4.3. Selecting the Next Task to Run
Scheduler
scheduler
has
a
runnable
task
if
the
result
of
getting
the
runnable
task
queues
for
scheduler
is
non-
empty
.
Scheduler
scheduler
:
-
Let queues be the result of getting the values of scheduler ’s static priority task queue map .
-
Extend queues with the result of getting the values of scheduler ’s dynamic priority task queue map .
-
Remove from queues any queue such that queue ’s tasks do not contain a runnable scheduler task .
-
Return queues .
Scheduler
associated
with
the
event
loop
has
a
runnable
task
.
-
Let queues be an empty set .
-
Let schedulers be the set of all
Schedulerobjects whose relevant agent’s event loop is event loop and that have a runnable task . -
For each scheduler in schedulers , extend queues with the result of getting the runnable task queues for scheduler .
-
If queues is empty return null.
-
Remove from queues any queue such that queue ’s effective priority is less than any other item of queues .
-
Let queue be the scheduler task queue in queues whose first runnable task is the oldest .
Two tasks cannot have the same age since enqueue order is unique. -
Return queue .
Note:
The
next
task
to
run
is
the
oldest,
highest
priority
runnable
scheduler
task
from
all
Scheduler
s
associated
with
the
event
loop
.
2.5. Examples
TODO (shaseley): Add examples.
3. Controlling Tasks
Tasks
scheduled
through
the
Scheduler
interface
can
be
controlled
with
a
TaskController
by
passing
the
TaskSignal
provided
by
controller.signal
as
the
option
when
calling
postTask()
.
The
TaskController
interface
supports
aborting
and
changing
the
priority
of
a
task
or
group
of
tasks.
3.1.
The
TaskPriorityChangeEvent
Interface
[Exposed =(Window ,Worker )]interface :TaskPriorityChangeEvent Event {(constructor DOMString ,type TaskPriorityChangeEventInit );priorityChangeEventInitDict ;readonly attribute TaskPriority previousPriority ; };dictionary :TaskPriorityChangeEventInit EventInit {;required TaskPriority ; };previousPriority
-
event .previousPriority -
Returns the
TaskPriorityof the correspondingTaskSignalprior to thisprioritychangeevent.The new
TaskPrioritycan be read withevent.target.priority.
The
previousPriority
getter
steps
are
to
return
the
value
that
the
corresponding
attribute
was
initialized
to.
3.2.
The
TaskController
Interface
dictionary {TaskControllerInit = "user-visible";TaskPriority = "user-visible"; }; [priority Exposed =(Window ,Worker )]interface :TaskController AbortController {constructor (optional TaskControllerInit = {});init );undefined setPriority (TaskPriority ); };priority
Note:
TaskController
's
signal
getter,
which
is
inherited
from
AbortController
,
returns
a
TaskSignal
object.
-
controller = newTaskController( init ) -
Returns a new
TaskControllerwhosesignalis set to a newly createdTaskSignalwith itspriorityinitialized to init ’spriority. -
controller .setPriority( priority ) -
Invoking this method will change the associated
TaskSignal's priority , signal the priority change to any observers, and causeprioritychangeevents to be dispatched.
new
TaskController(
init
)
constructor
steps
are:
-
Let signal be a new
TaskSignalobject.
The
setPriority(
priority
)
method
steps
are
to
signal
priority
change
on
this
's
signal
given
priority
.
3.3.
The
TaskSignal
Interface
dictionary {TaskSignalAnyInit ( = "user-visible";(TaskPriority or TaskSignal )= "user-visible"; }; [priority Exposed =(Window ,Worker )]{ [ = {});interface :TaskSignal AbortSignal { [NewObject ]static TaskSignal _any (sequence <AbortSignal >,signals optional TaskSignalAnyInit = {});init ;readonly attribute TaskPriority priority ;attribute EventHandler onprioritychange ; };
Note:
TaskSignal
inherits
from
AbortSignal
and
can
be
used
in
APIs
that
accept
an
AbortSignal
.
Additionally,
postTask()
accepts
an
AbortSignal
,
which
can
be
useful
if
dynamic
prioritization
is
not
needed.
-
TaskSignal . any ( signals , init ) -
Returns
a
TaskSignalinstance which will be aborted if any of signals is aborted. Its abort reason will be set to whichever one of signals caused it to be aborted. The signal’s priority will be determined by init ’spriority, which can either be a fixedTaskPriorityor aTaskSignal, in which case the new signal’s priority will change along with this signal. -
signal .priority -
Returns the
TaskPriorityof the signal.
A
TaskSignal
object
has
an
associated
priority
(a
TaskPriority
).
A
TaskSignal
object
has
an
associated
priority
changing
(a
boolean
),
which
is
intially
set
to
false.
A
TaskSignal
object
has
associated
priority
change
algorithms
,
(a
set
of
algorithms
that
are
to
be
executed
when
its
priority
changing
value
is
true),
which
is
initially
empty.
A
TaskSignal
object
has
an
associated
source
signal
(a
weak
refernece
to
a
TaskSignal
that
the
object
is
dependent
on
for
its
priority
),
which
is
initially
null.
A
TaskSignal
object
has
associated
dependent
signals
(a
weak
set
of
TaskSignal
objects
that
are
dependent
on
the
object
for
their
priority
),
which
is
initially
empty.
A
TaskSignal
object
has
an
associated
dependent
(a
boolean),
which
is
initially
false.
The
priority
getter
steps
are
to
return
this
's
priority
.
The
onprioritychange
attribute
is
an
event
handler
IDL
attribute
for
the
onprioritychange
event
handler
,
whose
event
handler
event
type
is
prioritychange
.
The
static
any(
signals
,
init
)
method
steps
are
to
return
the
result
of
creating
a
dependent
task
signal
from
signals
,
init
,
and
the
current
realm
.
A
TaskSignal
has
fixed
priority
if
it
is
a
dependent
signal
with
a
null
source
signal
.
To
add
a
priority
change
algorithm
algorithm
to
a
TaskSignal
object
signal
,
append
algorithm
to
signal
’s
priority
change
algorithms
.
AbortSignal
objects
signals
,
a
TaskSignalAnyInit
init
,
and
a
realm
:
-
Let resultSignal be the result of creating a dependent signal from signals using the
TaskSignalinterface and realm . -
Set resultSignal ’s dependent to true.
-
If init ["
priority"] is aTaskPriority, then: -
Otherwise:
-
Let sourceSignal be init ["
priority"]. -
If sourceSignal does not have fixed priority , then:
-
If sourceSignal ’s dependent is true, then set sourceSignal to sourceSignal ’s source signal .
-
Assert: sourceSignal is not dependent .
-
Set resultSignal ’s source signal to a weak reference to sourceSignal .
-
Append resultSignal to sourceSignal ’s dependent signals .
-
-
-
Return resultSignal .
TaskSignal
object
signal
,
given
a
TaskPriority
priority
:
-
If signal ’s priority changing is true, then throw a "
NotAllowedError"DOMException. -
If signal ’s priority equals priority then return.
-
Set signal ’s priority changing to true.
-
Let previousPriority be signal ’s priority .
-
Set signal ’s priority to priority .
-
For each algorithm of signal ’s priority change algorithms , run algorithm .
-
Fire an event named
prioritychangeat signal usingTaskPriorityChangeEvent, with itspreviousPriorityattribute initialized to previousPriority . -
For each dependentSignal of signal ’s dependent signals , signal priority change on dependentSignal with priority .
-
Set signal ’s priority changing to false.
TaskPriority
priority
and
a
realm
realm
.Let init be a new
TaskSignalAnyInit.Set init ["
priority"] to priority .Return the result of creating a dependent task signal from « », init , and realm .
3.3.1. Garbage Collection
A
dependent
TaskSignal
object
must
not
be
garbage
collected
while
its
source
signal
is
non-null
and
it
has
registered
event
listeners
for
its
prioritychange
event
or
its
priority
change
algorithms
is
non-empty.
3.4. Examples
TODO (shaseley): Add examples.
4. Modifications to Other Standards
4.1. The HTML Standard
4.1.1.
WindowOrWorkerGlobalScope
Each
object
implementing
the
WindowOrWorkerGlobalScope
mixin
has
a
corresponding
scheduler
,
which
is
initialized
as
a
new
Scheduler
.
partial interface mixin WindowOrWorkerGlobalScope {[;[Replaceable ]readonly attribute Scheduler scheduler ; };
The
scheduler
attribute’s
getter
steps
are
to
return
this
's
scheduler
.
4.1.2. Event loop: definitions
Replace: For each event loop , every task source must be associated with a specific task queue .
With: For each event loop , every task source that is not a scheduler task source must be associated with a specific task queue .
Add: An event loop has a numeric next enqueue order which is initialized to 1.
Note:
The
next
enqueue
order
is
a
strictly
increasing
number
that
is
used
to
determine
task
execution
order
across
scheduler
task
queues
of
the
same
TaskPriority
across
all
Scheduler
s
associated
with
the
same
event
loop
.
A
timestamp
would
also
suffice
as
long
as
it
is
guaranteed
to
be
strictly
increasing
and
unique.
Add: An event loop has a current scheduling state (a scheduling state or null), which is initialized to null.
4.1.3. Event loop: processing model
Add the following steps to the event loop processing steps, before step 2:
-
Let queues be the set of the event loop 's task queues that contain at least one runnable task .
-
Let schedulerQueue be the result of selecting the next scheduler task queue from all schedulers .
Modify step 2 to read:
-
If schedulerQueue is not null or queues is not empty :
Modify step 2.1 to read:
-
Let taskQueue be one of the following, chosen in an implementation-defined manner:
-
If queues is not empty , one of the task queues in queues , chosen in an implementation-defined manner.
-
schedulerQueue ’s tasks , if schedulerQueue is not null.
-
The
taskQueue
in
this
step
will
either
be
a
set
of
tasks
or
a
set
of
scheduler
tasks
.
The
steps
that
follow
only
remove
an
item
,
so
they
are
roughly
compatible.
Ideally,
there
would
be
a
common
task
queue
interface
that
supports
a
pop()
method
that
would
return
a
plain
task
,
but
that
would
involve
a
fair
amount
of
refactoring.
4.1.4. HostMakeJobCallback(callable)
Add the following before step 5:
Let event loop be incumbent settings 's realm 's agent 's event loop .
Let state be event loop ’s current scheduling state .
Modify step 5 to read:
Return the JobCallback Record { [[Callback]]: callable , [[HostDefined]]: { [[IncumbentSettings]]: incumbent settings , [[ActiveScriptContext]]: script execution context , [[SchedulingState]]: state } }.
4.1.5. HostCallJobCallback(callback, V, argumentsList)
Add the following steps before step 5:
Let event loop be incumbent settings 's realm 's agent 's event loop .
Set event loop ’s current scheduling state to callback .[[HostDefined]].[[SchedulingState]].
Add the following after step 7:
Set event loop ’s current scheduling state to null.
4.2.
requestIdleCallback()
4.2.1. Invoke idle callbacks algorithm
Add the following step before step 3.3:
Let realm be the relevant realm for window .
Let state be a new scheduling state .
Set state ’s priority source to the result of creating a fixed priority unabortable task signal given "
background" and realm .Let event loop be realm ’s agent 's event loop .
Set event loop ’s current scheduling state to state .
Add the following after step 3.3:
Set event loop ’s current scheduling state to null.
5. Security Considerations
The main security consideration for the APIs defined in this specification is whether or not any information is potentially leaked between origins by timing-based side-channel attacks.
5.1.
postTask
as
a
High-Resolution
Timing
Source
setTimeout()
's
timeout
value,
postTask()
's
delay
is
expressed
in
whole
milliseconds
(the
minimum
non-zero
delay
being
1
ms),
so
callers
cannot
express
any
timing
more
precise
than
1
ms.
Further,
since
tasks
are
queued
when
their
delay
expires
and
not
run
instantly,
the
precision
available
to
callers
is
further
reduced.
5.2. Monitoring Another Origin’s Tasks
postTask()
leaks
any
information
about
other
origins'
tasks.
We
consider
an
attacker
running
on
one
origin
trying
to
obtain
information
about
code
executing
in
another
origin
(and
hence
in
a
separate
event
loop)
that
is
scheduled
in
the
same
thread
in
a
browser.
Because a thread within a UA can only run tasks from one event loop at a time, an attacker might be able to gain information about tasks running in another event loop by monitoring when their tasks run. For example, an attacker could flood the system with tasks and expect them to run consecutively; if there are large gaps in between, then the attacker could infer that another task ran, potentially in a different event loop. The information exposed in such a case would depend on implementation details, and implementations can reduce the amount of information as described below.
What
Information
Might
Be
Gained?
Concretely,
an
attacker
would
be
able
to
detect
when
other
tasks
are
executed
by
the
browser
by
either
flooding
the
system
with
tasks
or
by
recursively
scheduling
tasks.
This
is
a
known
attack
that
can
be
executed
with
existing
APIs
like
postMessage()
.
The
tasks
that
run
instead
of
the
attacker’s
can
be
tasks
in
other
event
loops
as
well
as
other
tasks
in
the
attacker’s
event
loop,
including
internal
UA
tasks
(e.g.
garbage
collection).
Assuming the attacker can determine with a high degree of probability that the task executing is in another event loop, then the question becomes what additional information can the attacker learn? Since inter-event-loop task selection is not specified, this information will be implementation-dependent and depends on how UAs order tasks between event loops. But UAs that use a prioritization scheme that treats event loops sharing a thread as a single event loop are vulnerable to exposing more information.
It is helpful to think about the set of potential tasks that a UA might choose instead of the attacker’s, which corresponds to the information gained. When an attacker floods the system with tasks, the set of possible tasks would be anything the UA deems to be higher priority at that moment. This could be the result of a static prioritization scheme, e.g. input is always highest priority, network is second highest, etc., or this could be more dynamic, e.g. the UA occasionally chooses to run tasks from other task sources depending on how long they’ve been starved. Using a dynamic scheme increases the set of potential task which in turn decreases the fidelity of the information.
postTask()
supports
prioritization
for
tasks
scheduled
with
it.
How
these
tasks
are
interleaved
with
other
task
sources
is
also
implementation-dependent,
however
it
might
be
possible
for
an
attacker
to
further
reduce
the
set
of
potential
tasks
that
can
run
instead
of
its
own
by
leveraging
this
priority.
For
example,
if
a
UA
uses
a
simple
static
prioritization
scheme
spanning
all
event
loops
in
a
thread,
then
using
user-blocking
postTask()
tasks
instead
of
postMessage()
tasks
might
decrease
this
set,
depending
on
their
relative
prioritization
and
what
is
between.
What
Mitigations
are
Possible?
There
are
mitigations
that
implementers
can
consider
to
minimize
the
risk:
-
Where possible, isolate cross-origin event loops by running them in different threads. This type of attack depends on the event loops sharing a thread.
-
Use an inter-event-loop scheduling policy that is not strictly based on priority. For example, an implementation might use round-robin or fair-scheduling between event loops to prevent leaking information about task priority. Another possibility is to ensure lower priority tasks are periodically cycled in to prevent inferring priority information.
6. Privacy Considerations
This section is non-normative.
We have evaluated the APIs defined in this specification from a privacy perspective and do not believe there to be any privacy considerations.