1. はじめに
このセクションは規範的ではありません。
スケジューリングはウェブサイトのパフォーマンス向上のための重要な開発ツールとなる場合があります。大きく分けると、スケジューリングによって効果のある領域は「ユーザー体感の待ち時間」と「応答性」の2つです。スケジューリングにより、ユーザー体感の待ち時間を短縮できるのは、品質に直接影響を与える高優先度の作業を優先し、低優先度作業を後回しにできるからです。たとえば、ページロード時に特定のサードパーティ ライブラリスクリプトの実行を後回しにすることで、ユーザーにより早く画面を表示させられます。同様に、ビューポート内のコンテンツに関する作業を優先することも有効です。メインスレッド上のスクリプトの場合、長時間タスクは入力やUIの更新処理をブロックし、入力・視覚的応答性を低下させます。これらのタスクを小さく分割し、その断片やタスク継続としてスケジューリングするのは、アプリケーションやフレームワーク開発者が応答性向上のために用いる実証された手法です。
ユーザースペースのスケジューラーは、通常タスクのスケジューリング・実行タイミングを制御するメソッドを提供します。タスクには通常優先度が設定され、それが他タスクとの関係で実行タイミングを大きく左右します。スケジューラーは一定時間(スケジューラークォンタム)タスクを実行し、その後またブラウザへ制御を戻す、という動作を繰り返します。スケジューラーは続きの処理用に継続タスク、例えばsetTimeout()やpostMessage()などをスケジューリングして再開します。
ユーザースペースのスケジューラーは成功してきましたが、集中管理型のブラウザスケジューラーとより良いスケジューリングプリミティブがあれば状況はさらに改善します。スケジューラーの優先度システムは、基本的にそのスケジューラーの適用範囲内に留まります。つまり、ユーザースペーススケジューラーの場合、UAがタスクの優先度を知ることは原則としてありません。ただしrequestIdleCallback()を使う場合だけ例外ですが、これは最低優先度の作業に限定されます。同様に、ページ上に複数のスケジューラーがあるケースも増えています。例えば、フレームワーク(例:React)が独自にスケジューラーを持ち、自前スケジューリングをし、さらにスケジューラーを持つ機能(例:埋め込み地図)を組み込むこともあります。ブラウザはグローバルな情報を持ち、イベントループがタスクを実行する責任を負っているため、理想的な協調ポイントといえます。
優先度以外にも、ユーザースペーススケジューラーが依存する既存プリミティブは現代的ユースケースには最適でありません。setTimeout(0)は非遅延タスクのスケジューリング手段として一般的ですが、(ネストしたタスクなどに対し)最小遅延値があることも多く、これが余計な待ち時間を発生させ、パフォーマンス低下につながる場合もあります。よく知られた回避策としてpostMessage()やMessageChannelを使う方法がありますが、これらAPIはスケジューリング向けに設計されておらず、コールバックをキューすることはできません。requestIdleCallback()は一部用途で効果的ですが、アイドルタスクのみに適用でき、優先度の変化(例:ユーザー入力に応じオフスクリーンコンテンツの再優先付け等)には対応できません。
本仕様は、開発者が優先度付きタスクおよび継続をスケジューリング・制御できる新しいインターフェースを導入します。この文脈でのタスクとは、自身のイベントループ上で非同期に実行されるJavaScriptコールバックです。継続とは、制御をいったんブラウザに戻した後に、新たなイベントループのタスクとしてJavaScriptコードが再開することです。Schedulerインターフェースは、タスクのスケジューリング用postTask()メソッドと継続スケジューリング用yield()メソッドを公開します。本仕様は、タスクおよび継続の実行順序制御に利用できるTaskPrioritiesを定義します。さらに、TaskControllerとそれに紐づくTaskSignalにより、スケジューリング済みタスクの中断や優先度制御が可能です。
2. タスクおよび継続のスケジューリング
2.1. タスクと継続の優先度
本仕様はタスクスケジューリングのために3つの優先度を正式化します:
enum {TaskPriority "user-blocking" ,"user-visible" ,"background" };
user-blockingは最も高い優先度で、できるだけ早く実行する必要があるタスク向けです。これを低優先度で実行するとユーザー体験が劣化します。例えば、ユーザー入力への直接応答や、ビューポート内UI状態の更新など(チャンク化したもの含む)が該当します。
この優先度でスケジューリングされたタスクは、通常他のタスクより高いイベントループ優先度を持ちますが、必ずしもレンダーブロックにはなりません。即時中断なく行いたい作業は同期的に実行すべきですが、それが長引くと応答性が悪化します。一方、"user-blocking"タスクを使えば、作業を分割・スケジューリングしつつ入力・描画応答性を維持し、可能な限り早期に完了させやすくなります。
user-visibleは2番目に高い優先度で、ユーザーにとって有益な副作用が発生するが、即座にユーザーに観測されたりユーザー体験に不可欠でない処理向けです。この優先度のタスクは"user-blocking"タスクより重要性・緊急性が劣ります。これがデフォルトの優先度です。
backgroundは最も低い優先度で、たとえばバックグラウンドでのログ・メトリクス処理や特定のサードパーティライブラリの初期化など、即時でなくてよい作業向けです。
注記: 指定された Scheduler
を通じてスケジュールされたタスクは 厳格な優先順位順 で実行されます。つまり、スケジューラは常に "user-blocking"
タスクを
"user-visible"
タスクより先に実行し、これらは常に "background"
タスクより先に実行されます。継続は、同じ TaskPriority
を持つタスクよりも高い effective priority を持ちます。
2.2. Schedulerインターフェース
dictionary {SchedulerPostTaskOptions AbortSignal ;signal TaskPriority ; [priority EnforceRange ]unsigned long long = 0; };delay callback =SchedulerPostTaskCallback any (); [Exposed =(Window ,Worker )]interface {Scheduler Promise <any >postTask (SchedulerPostTaskCallback ,callback optional SchedulerPostTaskOptions = {});options Promise <undefined >yield (); };
注記: signal
オプションは AbortSignal
または TaskSignal
を指定できるが、
AbortSignal
の方が TaskSignal
のスーパークラスであるため
定義されている。
優先度が変化する可能性がある場合は TaskSignal
が必要となるが、
キャンセルのみの場合は AbortSignal
だけでよいので、
AbortSignals
を使った既存コードへも簡単に統合できる。
result = scheduler .postTask(callback, options )-
callbackの戻り値でPromiseがfulfilled、タスクが中断された場合は
AbortSignalのabort reasonでrejectedとなります。callback実行中エラーが発生した場合、postTask()が返すpromiseはそのエラーでrejectされます。タスクの
priorityは、optionのpriorityとsignalの組合せで決まります:-
optionの
priorityが指定されていれば、そのTaskPriorityでスケジューリングされ、タスクの優先度は不変です。 -
それ以外でoptionの
signalにTaskSignalオブジェクトが指定されていれば、そのsignalのpriorityがタスク優先度となります。この場合、タスクの優先度は動的としてTaskController関連のcontroller.setPriority()呼び出しで変更可能です。 -
それ以外はタスクの優先度は"
user-visible"となります。
-
result = scheduler .yield()-
Promiseが
undefinedでfulfilledされるか、継続処理が中断されるとAbortSignalのabort reasonでrejectedとなります。継続処理の優先度と中断用signalは、元のタスクから受け継がれます。元タスクのスケジューリングに
postTask()+AbortSignalを指定した場合、そのsignalが継続の中断判定に使われます。元タスクの優先度(TaskSignalまたは固定優先度)も継続の優先度判定に使います。優先度の指定がなかった場合は"user-visible"になります。
Schedulerオブジェクトは、静的優先度タスクキューマップ(map)を持ちます。これは(TaskPriority,
boolean)をキー、scheduler
task queueを値とするmapで、空で初期化されます。
Schedulerオブジェクトは、動的優先度タスクキューマップ(map)も持ちます。これは(TaskSignal,
boolean)をキー、scheduler task queueを値とするmapで、空で初期化されます。
注:動的優先度の実装は、特定TaskSignalに紐づくタスクを同一タスクキューにまとめ、prioritychangeイベントでそのキューの優先度を変える方式です。動的優先度タスクキューマップには、優先度が変わりうるタスクキューが格納され、キーは共通TaskSignalです。
静的優先度タスクキューマップの値は、優先度が変わらないタスクキューです。明示的priorityやnull/AbortSignalなsignalでスケジューリングされた静的優先度タスクはここに振り分けられます。
別実装法としては各TaskPriorityごとにキューを1つだけ持ち、TaskSignalのpriority変更時にタスクキュー間でタスク単位で移動するという方式もあり、この場合は次に実行するキューの決定が簡単になる反面、優先度変更時の処理が複雑になります。
postTask(callback, options)メソッドの手順は、タスクのスケジューリングをこのcallback・options指定で実行した結果を返します。
yield()メソッドの手順は、yield継続のスケジューリングをこのインスタンスで行った結果を返します。
2.3. 定義
スケジューラータスクは、追加の数値型enqueue順序(item・初期値0)を持つタスクです。
以下のタスクソースはスケジューラータスクソースとして定義され、スケジューラータスクでのみ使います。
- posted task タスクソース
-
このタスクソースは
postTask()またはyield()でスケジューリングされたタスクに使います。
スケジューラータスクキューは、以下のitemを持つstructです:
- priority
- is continuation
-
boolean
- tasks
-
セット(スケジューラータスクの集合)
- removal steps
-
アルゴリズム
スケジューリング状態は、以下のitemを持つstructです:
- abort source
-
AbortSignalオブジェクトまたはnull(初期値: null) - priority source
-
TaskSignalオブジェクトまたはnull(初期値: null)
- state map
-
初期値は空のmap
注:state mapのキーとしてガベージコレクト付きオブジェクトを使う場合、WeakMapでの実装が可能です。
- task
-
スケジューラータスクまたはnull
- queue
-
スケジューラータスクキューまたはnull
- abort steps
-
アルゴリズム
- task complete steps
-
アルゴリズム
2.4. 処理モデル
TaskPriority
priority, boolean の isContinuation、およびアルゴリズム removalSteps を受け取り:
-
queue を新しい スケジューラータスクキュー とする。
-
queue の priority に priority を設定する。
-
queue の is continuation に isContinuation を設定する。
-
queue の removal steps に removalSteps を設定する。
-
queue を返す。
AbortSignal
または null の signal を受け取り:
-
handle を新しい タスクハンドル とする。
-
handle の task を null に設定する。
-
handle の queue を null に設定する。
-
handle の abort steps に次の手順を設定する:
-
result を signal の abort reason で reject する。
-
もし task が null でなければ、
-
task を queue から削除する。
-
もし queue が empty なら、queue の removal steps を実行する。
-
-
-
handle の task complete steps に次の手順を設定する:
-
もし signal が null でなければ、handle の abort steps を signal から削除する。
-
もし queue が empty なら、queue の removal steps を実行する。
-
-
handle を返す。
| 優先度 | 継続か | 有効優先度 |
|---|---|---|
"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. スケジューラータスクのキューイングと削除
-
task を新しい スケジューラータスク とする。
-
task の enqueue順序 に enqueue order を設定する。
-
task の steps に steps を設定する。
-
task の source に source を設定する。
-
task の document に document を設定する。
-
task の script evaluation environment settings object set に新しい空の set を設定する。
-
task を返す。
HTML仕様をリファクタリングして task のコンストラクタを追加することを検討すべきです。一つの問題点は新しいタスクが スケジューラータスク である必要がある点です(通常の task ではない)。
2.4.2. タスクと継続のスケジューリング
Scheduler) を
state (a スケジューリング状態) に設定するには:
-
継続状態値を設定する、scheduler と state を eventLoop に渡して行う。
Scheduler):
-
継続状態値を取得する、scheduler と eventLoop を渡して、その結果を返す。
Scheduler
scheduler、SchedulerPostTaskCallback
callback および SchedulerPostTaskOptions
options の場合:
-
result を 新しいPromiseとする。
-
signal が null でなく、かつ 中断されている 場合は、result を signal の abort理由 で reject し、result を返す。
-
state を新しい スケジューリング状態 とする。
-
state の abort source に signal を設定する。
-
options["
priority"] が 存在する 場合は、state の priority source に 固定優先度・中断不可タスクシグナルを作成した結果を渡す。 -
それ以外で signal が null でなく、かつ TaskSignalインターフェースを実装している 場合は、state の priority source に signal を設定する。
-
state の priority source が null の場合は、固定優先度・中断不可タスクシグナルを作成("
user-visible"を指定)した結果を設定する。 -
handle を タスクハンドル作成(resultとsignalを渡す)の結果とする。
-
signal が null でなければ、handle の abort steps を signal に追加する。
-
enqueueSteps に以下の手順を設定:
-
handle の queue に スケジューラータスクキューの選択結果(scheduler, state の priority source, false を渡す)を設定。
-
アルゴリズム実行タスクをスケジューリング(scheduler, handle, 下記手順):
-
現在のスケジューリング状態を設定(scheduler, state)。
-
callbackResult を callback を呼び出す結果とする(« » および「rethrow」指定)。 throwされた場合は result をその例外で reject、そうでなければ result を callbackResult で解決。
-
eventLoop の current continuation state を null にする。
-
-
delay を options["
delay"] とする。 -
delay が0より大きければ、timeout後の手順実行をする(scheduler の関連globalオブジェクト・"scheduler-postTask"・delay・下記手順):
-
signal が null または 中断されていなければ、enqueueStepsを実行。
-
-
それ以外は enqueueSteps を実行。
-
result を返す。
注: ここで作成される固定優先度の中断不可signalはメモリアロケーションを抑えるためキャッシュ再利用可能。
timeout後の手順はsuspensionを考慮しない場合あり。詳細は whatwg/html#5925 参照。
Scheduler
scheduler:
-
result を 新しいPromiseとする。
-
inheritedState を 現在のスケジューリング状態を取得する(scheduler)結果とする。
-
abortSource を inheritedState の abort source (nullでなければ)それ、なければnull。
-
abortSource が null でなく 中断 されていれば、result を abortSource の abort理由で reject して返す。
-
prioritySource を inheritedState の priority source (nullでなければ)それ、なければ null。
-
prioritySource がnullなら、固定優先度・中断不可タスクシグナル作成("
user-visible")の結果を設定。 -
handle を タスクハンドル作成(result, abortSource)で得る。
-
abortSource がnullでなければ、handle の abort steps を abortSource に追加。
-
handle の queue に、スケジューラータスクキュー選択(scheduler, prioritySource, true)を設定。
-
アルゴリズム実行タスクをスケジューリング(scheduler, handle, 下記手順):
-
result を resolve する。
-
-
result を返す。
注: 固定優先度・中断不可signalはキャッシュ再利用可。
Scheduler
scheduler、TaskSignal
オブジェクト signal、boolean isContinuation の場合:
-
signal が 固定優先度を持たないなら、
-
scheduler の 動的優先度タスクキューマップ に (signal, isContinuation) が 含まれていなければ、
-
queue を スケジューラータスクキュー作成(signal の priority・isContinuation・下記手順)とする。
-
動的優先度タスクキューマップ[(signal, isContinuation)] を削除。
-
-
動的優先度タスクキューマップ[(signal, isContinuation)] に queue を設定。
-
優先度変更アルゴリズム追加 を signal に対して次のステップで追加:
-
-
動的優先度タスクキューマップ[(signal, isContinuation)] を返す。
-
-
それ以外の場合
-
priority を signal の priority とする。
-
scheduler の 静的優先度タスクキューマップ に (priority, isContinuation) が 含まれていなければ、
-
queue を スケジューラータスクキュー作成(priority, isContinuation, 下記手順)とする。
-
静的優先度タスクキューマップ[(priority, isContinuation)] を削除。
-
-
静的優先度タスクキューマップ[(priority, isContinuation)] に queue を設定。
-
-
静的優先度タスクキューマップ[(priority, isContinuation)] を返す。
-
Scheduler
scheduler に タスクハンドル
handle と アルゴリズム steps を渡す場合:
-
global を 関連globalオブジェクト(schedulerの)とする。
-
document を global の 関連
Documentとする(globalがWindowオブジェクトなら、それ以外は null)。 -
enqueue order を event loop の next enqueue order とする。
-
event loop の next enqueue order を1増やす。
-
handle の task に、スケジューラータスクキューにタスクをキューする(handle の queue・enqueue order・posted task task source・document・下記手順)を設定:
-
steps を実行する。
-
handle の task complete steps を実行する。
-
このアルゴリズムは in parallel ステップから呼ばれるため一部競合状態になる可能性がある。特に next enqueue order の更新や スケジューラータスクキュー へのアクセスは原子的に行うべき。後者はイベントループのタスクキューにも影響(詳細は このissue 参照)。
2.4.3. 次に実行するタスクの選択
Scheduler
scheduler について:
-
queues を scheduler の static priority task queue map の値取得 結果とする。
-
queues を拡張。scheduler の dynamic priority task queue map の値取得結果で拡張する。
-
queues から、queue の tasks に runnable な scheduler task が含まれないものを削除。
-
queues を返す。
Scheduler
が event loop に関連付けられていて 実行可能なタスクを持つ 場合のみ返す。なければ null。
-
queues を空の セット とする。
-
schedulers を、セットで、 すべての
Schedulerのうち 関連エージェント の event loop が event loop かつ 実行可能なタスクを持つもの。 -
各 scheduler について、queues を、 実行可能なタスクキューの取得 (scheduler) の結果で拡張する。
-
もし queues が 空 なら null を返す。
-
queue を queues 内で first runnable task が もっとも古い スケジューラータスクキュー とする。
enqueue順序は一意なので2つのタスクが同じageを持つことはない。 -
queue を返す。
注: 次に実行されるタスクは、全Schedulerの中から
event loop に紐づく、最も古くて(enqueue順序が最小)、かつ最高優先度の runnable scheduler task である。
2.5. 例
TODO(shaseley): 例を追加予定。
3. タスクの制御
Scheduler
インターフェースでスケジューリングされたタスクは、TaskController
を使い、controller.signal
が提供する TaskSignal
を option
に渡して postTask()
呼び出し時に制御できます。
TaskController
インターフェースでは、タスクまたはグループ単位での中断と優先度変更がサポートされています。
3.1.
TaskPriorityChangeEvent インターフェース
[Exposed =(Window ,Worker )]interface :TaskPriorityChangeEvent Event {(constructor DOMString ,type TaskPriorityChangeEventInit );priorityChangeEventInitDict readonly attribute TaskPriority previousPriority ; };dictionary :TaskPriorityChangeEventInit EventInit {required TaskPriority ; };previousPriority
event .previousPriority-
この
TaskSignalが本prioritychangeイベントの直前に持っていたTaskPriorityを返します。新しい
TaskPriorityはevent.target.priorityで取得できます。
previousPriorityのgetter手順は、対応属性の初期値を返すことです。
3.2. TaskControllerインターフェース
dictionary {TaskControllerInit TaskPriority = "user-visible"; }; [priority Exposed =(Window ,Worker )]interface :TaskController AbortController {constructor (optional TaskControllerInit = {});init undefined setPriority (TaskPriority ); };priority
注: TaskControllerの
signal
のgetterは AbortController
から継承されており、
TaskSignal
オブジェクトを返します。
controller = newTaskController( init )-
新しい
TaskControllerを返します。そのsignalは新しく生成されたTaskSignalに割り当てられ、priorityは init のpriorityで初期化されます。 controller .setPriority( priority )-
このメソッドを呼び出すと、関連する
TaskSignalの priorityが変更され、オブザーバへ通知とprioritychangeイベントの発火が行われます。
new TaskController(init)
コンストラクタ手順:
-
signal を新しい
TaskSignalオブジェクトとする。
setPriority(priority)メソッド手順は、
signal priority change を
this の signal で priority を与え実行します。
3.3. TaskSignal インターフェイス
dictionary { (TaskSignalAnyInit 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 ; };
注記: TaskSignal
は AbortSignal
を継承し、AbortSignal
を受け入れるAPIで利用できる。また、postTask()
は AbortSignal
も受け入れ、動的な優先順位付けが不要な場合に便利である。
TaskSignal . any(signals, init)TaskSignalインスタンスを返す。このインスタンスは、signals のいずれかが中止された場合に中止される。その abort reason は、中止を引き起こした signals のものとなる。 シグナルのpriority は init のpriorityで決定される。 これは固定値のTaskPriorityまたはTaskSignalであり、後者の場合、新しいシグナルの priority はこのシグナルの値に応じて変化する。signal .priority-
このシグナルの
TaskPriorityを返す。
TaskSignal
オブジェクトは、関連付けられた priority(TaskPriority)を持つ。
TaskSignal
オブジェクトは、関連付けられた priority changing(boolean、初期値は false)を持つ。
TaskSignal
オブジェクトは、関連付けられた priority change algorithms
(アルゴリズムの集合で、priority changing が true の時に実行される。初期値は空)。
TaskSignal
オブジェクトは、関連付けられた source signal(TaskSignal
への弱参照で、
このオブジェクトの priority
の依存先)、初期値は null。
TaskSignal
オブジェクトは、関連付けられた dependent signals(TaskSignal
オブジェクトへの弱い
セット、初期値は空)がある。
TaskSignal
オブジェクトは、関連付けられた dependent(boolean、初期値は false)を持つ。
priority ゲッタの手順は、this の
priority
を返すことである。
onprioritychange 属性は、イベントハンドラー IDL 属性であり、
onprioritychange イベントハンドラー用で、イベントハンドラーイベントタイプは
prioritychange である。
static な any(signals, init) メソッドの手順は、
依存タスクシグナルの作成を
signals, init, current realm
の引数で呼び出し、その結果を返すことである。
TaskSignal
固定優先度を持つ のは、
dependent
シグナルで、その source signal が null の場合である。
優先度変更アルゴリズムの追加は、
algorithm を TaskSignal
オブジェクト signal の priority change algorithms に 追加する。
AbortSignal
オブジェクト signals、TaskSignalAnyInit
init、realm):
-
resultSignal を 依存シグナルの作成の結果として signals、
TaskSignalインターフェイスと realm で作成する。 -
resultSignal の dependent を true に設定。
-
init["
priority"] がTaskPriorityである場合: -
それ以外の場合:
-
sourceSignal を init["
priority"] とする。 -
sourceSignal が 固定優先度を持たない 場合:
-
sourceSignal の dependent が true の場合、 sourceSignal を sourceSignal の source signal にする。
-
アサート: sourceSignal は dependent ではない。
-
resultSignal の source signal を主信号への弱参照に設定する。
-
resultSignal を sourceSignal の dependent signals に追加。
-
-
-
resultSignal を返す。
TaskSignal
オブジェクト signal に対して、TaskPriority
priority を指定して行う手順:
-
もし signal の priority changing が true であれば、throw により「
NotAllowedError」 というDOMExceptionを発生させる。 -
もし signal の priority が priority と等しければ、処理を終了する。
-
signal の priority changing を true に設定する。
-
previousPriority を signal の priority とする。
-
signal の priority を priority に設定する。
-
For each algorithm of signal’s priority change algorithms、各 algorithm を実行する。
-
Fire an event named
prioritychangeを signal に対してTaskPriorityChangeEventを使って発火し、そのpreviousPriority属性を previousPriority に初期化する。 -
For each dependentSignal of signal’s dependent signals、signal priority change を dependentSignal に対して priority で実行する。
-
signal の priority changing を false に設定する。
TaskPriority
priority、realm realm):
-
init を新しい
TaskSignalAnyInitとする。 -
init["
priority"] に priority を設定する。 -
依存タスクシグナル作成(« », init, realm)の結果を返す。
3.3.1. ガベージコレクション
dependent な
TaskSignal
オブジェクトは、source signal が null でなく、かつ prioritychange
イベントのイベントリスナーまたは priority change algorithms が空でない限り
ガベージコレクションされてはならない。
3.4. 例
TODO(shaseley): 例を追加すること。
4. 他規格への修正
4.1. HTML標準
4.1.1. WindowOrWorkerGlobalScope
WindowOrWorkerGlobalScope
ミックスインを実装する各オブジェクトは、対応する scheduler
(新しい Scheduler)で初期化される。
partial interface mixin WindowOrWorkerGlobalScope { [Replaceable ]readonly attribute Scheduler scheduler ; };
scheduler属性のゲッタ手順は、
this の
scheduler を返すこと。
4.1.2. イベントループ: 定義
置き換え: 各event loopについて、すべてのtask sourceが特定のtask queueと関連付けられていなければならない。
修正後: 各event loopについて、task sourceのうちscheduler task sourceでないものは 特定のtask queueと関連付けられていなければならない。
追加: event loopは数値型の next enqueue order を持ち、初期値は 1 である。
注記:next enqueue order
は、同じ scheduler task
queue で同じ TaskPriority
である Schedulers
間のタスク実行順序を決定するために使う厳密に増加する値である。タイムスタンプを使ってもよいが、必ず一意かつ単調増加でなければならない。
追加: event loopは current continuation state(continuation state または null)を持ち、初期値は null である。
次のアルゴリズムを追加:
-
eventLoop の current continuation state が null なら、 新しい continuation state をセットする。
-
continuationState を eventLoop の current continuation state とする。
-
continuationState の state map[key] に value をセットする。
-
continuationState を eventLoop の current continuation state とする。
-
continuationState が null でなく、かつ state map[key] が 存在する場合、その値を返し、そうでなければ null を返す。
4.1.3. イベントループ: 処理モデル
イベントループ処理ステップ(step 2 の前)に次の手順を追加:
-
queues を、セットとして、 event loop の task queues から 1 つ以上 実行可能 な task を含むものを集めたものとする。
-
schedulerQueue を 全スケジューラから次のスケジューラタスクキューを選択の結果とする。
step 2 を次のように修正:
-
schedulerQueue が null でない、または queues が 空でない場合:
step 2.1 を次のように修正:
-
taskQueue を次のいずれかとして、実装依存な方法で選ぶ:
-
queues が 空でない場合、 task queues のいずれか(実装依存で選択)。
-
schedulerQueue が null でない場合、その tasks。
-
注記: HTML規格は各task
source単位での優先度付けを可能にするため、イベントループ処理手順における
次の task の task
queue の選択を 実装依存としている。
同様に、本仕様は Scheduler タスクと
イベントループの task
queues のどちらを次に選ぶかも 実装依存としており、最大のスケジューリング柔軟性をUAに与える。
ただし本仕様の意図は、TaskPriority
に応じて Scheduler
タスクの優先度をイベントループの優先度へ反映させることである。特に "background"
タスクや継続処理は、ほとんどの他のイベントループタスクよりも重要度が低いとみなされる一方、
"user-blocking"
タスクと継続、さらに "user-visible"
継続(タスクに対してではなく)はより重要とみなされる。
戦略例としては、Scheduler
タスクで effective priority 3 以上のものを、たとえば入力・描画など
urgent な処理より少し低いが他のほとんどのイベントループタスクより高い優先度で実行することや、
Scheduler
タスクで effective priority 0や1のものは、イベントループの
task
queues に他に実行可能タスクがない時だけ実行する戦略や、effective priority
2のものはタイマー系などと同様の扱いにすることなどが挙げられる。
このstepのtaskQueueは
tasks の集合か scheduler tasks の集合のいずれかであり、以降のステップはどちらにも
remove と item しか使わないので
おおむね互換となっている。本来なら共通のタスクキューインターフェイスを設け、pop() メソッドがplainな task を返せるといいが、それには相応のリファクタが要る。
4.1.4. イベントループ: タスクのキューイング
マイクロタスクをキューするアルゴリズムを、オプションのブーリアンignoreContinuationState(デフォルトはfalse)を受け入れるように変更する。
ステップ5を次のように変更する:
-
continuationState を null にする。
-
ignoreContinuationState が false かつ eventLoop の current continuation state が null でない場合、 continuationState を event loop の current continuation state のクローンに設定する。
-
microtask の steps を次のように設定する:
-
ignoreContinuationState が false の場合、eventLoop の current continuation state を continuationState に設定する。
-
steps を実行する。
-
ignoreContinuationState が false の場合、eventLoop の current continuation state を null に設定する。
-
4.1.5. HostMakeJobCallback(callable)
ステップ5の前に次を追加:
-
event loop を incumbent settings の realm の agent の event loop とする。
-
state を、event loop の current continuation state のクローン(current continuation state が null なら null)に設定する。
ステップ5を次のように修正:
-
JobCallback Record { [[Callback]]: callable, [[HostDefined]]: { [[IncumbentSettings]]: incumbent settings, [[ActiveScriptContext]]: script execution context, [[ContinuationState]]: state } } を返す。
4.1.6. HostCallJobCallback(callback, V, argumentsList)
ステップ5の前に次を追加:
-
event loop を incumbent settings の realm の agent の event loop とする。
-
event loop の current continuation state を callback.[[HostDefined]].[[ContinuationState]] に設定する。
ステップ7の後に次を追加:
-
event loop の current continuation state を null に設定する。
4.1.7. HostEnqueuePromiseJob(job, realm)
ステップ2を次のように変更:
-
ignoreContinuationState を true にして、以下の手順を実行するマイクロタスクをキューする。
4.2.
requestIdleCallback()
4.2.1. アイドルコールバックの実行アルゴリズム
ステップ3.3の前に次を追加:
-
realm を window の relevant realm とする。
-
state を新しい scheduling state とする。
-
state の priority source を、"
background" および realm を用いた 固定優先度unabortableタスクシグナル作成 の結果に設定する。 -
scheduler を
Schedulerで、その relevant realm が realm であるものとする。
ステップ3.3の後に次を追加:
-
event loop の current continuation state を null に設定する。
5. セキュリティの考慮事項
本仕様で定義されたAPIの主なセキュリティ上の考慮事項は、タイミングベースのサイドチャネル攻撃によってオリジン間で情報が漏洩する可能性があるかどうかである。
5.1.
postTask を高精度タイミングソースとして用いる場合
setTimeout()
の
timeout値と同様に、postTask()
の
delay
はミリ秒単位(最小非ゼロ遅延は1ms)で表されるため、呼び出し元は1ms未満の精度は指定できない。また、タスクは delay 終了時にキューイングされ即座に実行されるわけではないため、精度はさらに低下する。
5.2. 他オリジンのタスクの監視
postTask()
や yield()
によって他オリジンのタスク情報が漏れないかである。1つのオリジンで攻撃者が、同じスレッドでスケジュールされる別のオリジン(つまり別イベントループ)で実行されるコードの情報を得ようとすることが想定される。
UAのスレッドは同時に一つのイベントループのタスクしか実行できないため、攻撃者は自分のタスクの実行時刻を観察して他イベントループのタスク実行を検知できる可能性がある。たとえばタスクを連続投入しギャップが大きければ他タスク(別イベントループかも)が割り込んだと推測でき、このとき露呈される情報は実装依存であり、実装側で漏洩量の制御も可能である。
得られる情報は?
攻撃者はタスク連投や再帰スケジューリングによりブラウザの他タスク実行タイミングを検知可能となる。これは 既知の攻撃
で、既存APIの postMessage()
でも可能である。
割り込みタスクは、他イベントループや自分のイベントループの他タスク、UA内部タスク(GC等)でありうる。
攻撃者が大きな確率で「今動いているのは他イベントループのタスク」と判別できた場合、さらに何が分かるかが問題となる。イベントループ間のタスク選択は規定されておらず、これは実装依存であり、UAが複数イベントループのタスク順をどう決めるか次第。スレッド共有イベントループを一体として扱う優先度スキームのUAは、より多くの情報を露呈しやすい。
候補タスク集合を考えると分かりやすい。攻撃者がタスクを投げ続ける際、そのときUAがより高優先度とみなす全てのタスクが割り込み候補となる。これは静的スキーム(入力>ネット>…)でも、ダイナミック(一定周期で他タスクも実行)でもありえる。ダイナミック型ほど候補集合が大きく、漏洩精度が下がる。
postTask()
や yield()
はスケジュールするタスクや継続の優先度をサポートしている。しかしこれらが他タスクソースとどうインターリーブされるかはやはり実装依存で、攻撃者が優先度を活用して割り込み候補集合を更に狭めるのも場合による。例えば、UAがスレッド単位で単純な静的優先度スキームなら
"user-blocking"
postTask()
や
"user-visible"
以上の yield()
継続を使えば、postMessage()
タスクと比べて、タスク間の実行順や優先度次第で割り込み候補が絞れる場合もある。
対策は?
実装側でリスク低減のためとれる対策:
-
可能ならクロスオリジンのイベントループをスレッド分離する。この手の攻撃はスレッド共有時のみ有効。
-
イベントループ間スケジューリングを単なる優先度依存にしない。例えばラウンドロビンや公平スケジューリングを取り入れて優先度漏洩を防止したり、低優先タスクを定期的に実行して優先度推論を妨げる、など。
6. プライバシーの考慮事項
このセクションは規範的でない。
本仕様で定義されるAPIについてプライバシーの観点から検討したが、特筆すべき問題はないと結論付けている。