優先度付きタスクスケジューリング

コミュニティグループドラフト報告書,

このバージョン:
https://wicg.github.io/scheduling-apis/
課題トラッキング:
GitHub
仕様内インライン
編集者:
(Google)

概要

この仕様は、優先度付きタスクのスケジューリングおよび制御のためのAPIを定義します。

この文書のステータス

この仕様はWeb Platform Incubator Community Groupによって発行されました。 W3C標準ではなく、W3C標準化の過程にもありません。 W3C Community Contributor License Agreement (CLA)の下で限定的なオプトアウトがあり、他の条件も適用されることにご注意ください。 W3Cコミュニティおよびビジネスグループについて詳しくは、こちらをご覧ください。

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 delay = 0;
};

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、タスクが中断された場合はAbortSignalabort reasonでrejectedとなります。callback実行中エラーが発生した場合、postTask()が返すpromiseはそのエラーでrejectされます。

タスクのpriorityは、optionprioritysignalの組合せで決まります:

  • optionpriorityが指定されていれば、そのTaskPriorityでスケジューリングされ、タスクの優先度は不変です。

  • それ以外でoptionsignalTaskSignalオブジェクトが指定されていれば、そのsignalpriorityがタスク優先度となります。この場合、タスクの優先度は動的としてTaskController関連のcontroller.setPriority()呼び出しで変更可能です。

  • それ以外はタスクの優先度は"user-visible"となります。

optionsignalが指定されていれば、タスクの中断判定としてsignalSchedulerが使います。

optiondelayが0より大きく指定されている場合、タスク実行は最低delayミリ秒遅延されます。

result = scheduler . yield()

Promiseがundefinedでfulfilledされるか、継続処理が中断されるとAbortSignalabort 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/AbortSignalsignalでスケジューリングされた静的優先度タスクはここに振り分けられます。

別実装法としては各TaskPriorityごとにキューを1つだけ持ち、TaskSignalpriority変更時にタスクキュー間でタスク単位で移動するという方式もあり、この場合は次に実行するキューの決定が簡単になる反面、優先度変更時の処理が複雑になります。

postTask(callback, options)メソッドの手順は、タスクのスケジューリングをこのcallbackoptions指定で実行した結果を返します。

yield()メソッドの手順は、yield継続のスケジューリングをこのインスタンスで行った結果を返します。

2.3. 定義

スケジューラータスクは、追加の数値型enqueue順序item・初期値0)を持つタスクです。

以下のタスクソーススケジューラータスクソースとして定義され、スケジューラータスクでのみ使います。

posted task タスクソース

このタスクソースpostTask()またはyield()でスケジューリングされたタスクに使います。


スケジューラータスクキューは、以下のitemを持つstructです:

priority

TaskPriority

is continuation

boolean

tasks

セットスケジューラータスクの集合)

removal steps

アルゴリズム


スケジューリング状態は、以下のitemを持つstructです:

abort source

AbortSignalオブジェクトまたはnull(初期値: null)

priority source

TaskSignalオブジェクトまたはnull(初期値: null)


継続状態は、以下のitemを持つstructです:

state map

初期値は空のmap

注:state mapのキーとしてガベージコレクト付きオブジェクトを使う場合、WeakMapでの実装が可能です。


タスクハンドルは、以下のitemを持つstructです:

task

スケジューラータスクまたはnull

queue

スケジューラータスクキューまたはnull

abort steps

アルゴリズム

task complete steps

アルゴリズム

2.4. 処理モデル

スケジューラータスク t1より古い スケジューラータスク t2 である、もし t1enqueue順序t2enqueue順序 より小さい場合。
スケジューラータスクキューを作成する TaskPriority priority, boolean の isContinuation、およびアルゴリズム removalSteps を受け取り:
  1. queue を新しい スケジューラータスクキュー とする。

  2. queueprioritypriority を設定する。

  3. queueis continuationisContinuation を設定する。

  4. queuetasks に新しい空の set を設定する。

  5. queueremoval stepsremovalSteps を設定する。

  6. queue を返す。

タスクハンドルを作成する Promise resultAbortSignal または null の signal を受け取り:
  1. handle を新しい タスクハンドル とする。

  2. handletask を null に設定する。

  3. handlequeue を null に設定する。

  4. handleabort steps に次の手順を設定する:

    1. resultsignalabort reason で reject する。

    2. もし task が null でなければ、

      1. taskqueue から削除する。

      2. もし queueempty なら、queueremoval steps を実行する。

  5. handletask complete steps に次の手順を設定する:

    1. もし signal が null でなければ、handleabort stepssignal から削除する。

    2. もし queueempty なら、queueremoval steps を実行する。

  6. handle を返す。

スケジューラータスクキュー queuefirst runnable taskqueuetasks の中で最初の スケジューラータスク であり、runnable であるもの。
scheduler task queue queueeffective priority(有効優先度) は、 queuepriority および is continuation に一致する行の3列目の値で決定される:

優先度 継続か 有効優先度
"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. スケジューラータスクのキューイングと削除

スケジューラータスクをキューする スケジューラータスクキュー queue 上で、一連の手順 steps を実行し、数値 enqueue orderタスクソース sourcedocument document を受け取り:
  1. task を新しい スケジューラータスク とする。

  2. taskenqueue順序enqueue order を設定する。

  3. taskstepssteps を設定する。

  4. tasksourcesource を設定する。

  5. taskdocumentdocument を設定する。

  6. taskscript evaluation environment settings object set に新しい空の set を設定する。

  7. taskqueuetasks に追加する。

  8. task を返す。

HTML仕様をリファクタリングして task のコンストラクタを追加することを検討すべきです。一つの問題点は新しいタスクが スケジューラータスク である必要がある点です(通常の task ではない)。

タスクを削除する スケジューラータスク taskスケジューラータスクキュー queue から 削除する。taskqueuetasks から削除。
スケジューラータスクキュー queueempty、 すなわち queuetasksempty の場合。

2.4.2. タスクと継続のスケジューリング

To 現在のスケジューリング状態を設定する for scheduler (a Scheduler) を state (a スケジューリング状態) に設定するには:
  1. eventLoopscheduler関連エージェントイベントループ とする。

  2. 継続状態値を設定するschedulerstateeventLoop に渡して行う。

注: state map のキーは Scheduler に固有であれば何でもよい。

To 現在のスケジューリング状態を取得する for scheduler (a Scheduler):
  1. eventLoopscheduler関連エージェントイベントループ とする。

  2. 継続状態値を取得するschedulereventLoop を渡して、その結果を返す。

To postTaskタスクをスケジューリングする for Scheduler schedulerSchedulerPostTaskCallback callback および SchedulerPostTaskOptions options の場合:
  1. result新しいPromiseとする。

  2. signaloptions["signal"] が 存在すれば それ、なければ null にする。

  3. signal が null でなく、かつ 中断されている 場合は、resultsignalabort理由 で reject し、result を返す。

  4. state を新しい スケジューリング状態 とする。

  5. stateabort sourcesignal を設定する。

  6. options["priority"] が 存在する 場合は、statepriority source固定優先度・中断不可タスクシグナルを作成した結果を渡す。

  7. それ以外で signal が null でなく、かつ TaskSignalインターフェースを実装している 場合は、statepriority sourcesignal を設定する。

  8. statepriority source が null の場合は、固定優先度・中断不可タスクシグナルを作成("user-visible"を指定)した結果を設定する。

  9. handleタスクハンドル作成resultsignalを渡す)の結果とする。

  10. signal が null でなければ、handleabort stepssignal に追加する。

  11. enqueueSteps に以下の手順を設定:

    1. handlequeueスケジューラータスクキューの選択結果scheduler, statepriority source, false を渡す)を設定。

    2. アルゴリズム実行タスクをスケジューリングscheduler, handle, 下記手順):

      1. eventLoopscheduler関連エージェントイベントループ とする。

      2. 現在のスケジューリング状態を設定scheduler, state)。

      3. callbackResultcallback を呼び出す結果とする(« » および「rethrow」指定)。 throwされた場合は result をその例外で reject、そうでなければ resultcallbackResult で解決。

      4. eventLoopcurrent continuation state を null にする。

  12. delayoptions["delay"] とする。

  13. delay が0より大きければ、timeout後の手順実行をする(scheduler関連globalオブジェクト・"scheduler-postTask"・delay・下記手順):

    1. signal が null または 中断されていなければenqueueStepsを実行。

  14. それ以外は enqueueSteps を実行。

  15. result を返す。

注: ここで作成される固定優先度の中断不可signalはメモリアロケーションを抑えるためキャッシュ再利用可能。

timeout後の手順はsuspensionを考慮しない場合あり。詳細は whatwg/html#5925 参照。

To yield継続をスケジューリングする for Scheduler scheduler:
  1. result新しいPromiseとする。

  2. inheritedState現在のスケジューリング状態を取得するscheduler)結果とする。

  3. abortSourceinheritedStateabort source (nullでなければ)それ、なければnull。

  4. abortSource が null でなく 中断 されていれば、resultabortSourceabort理由で reject して返す。

  5. prioritySourceinheritedStatepriority source (nullでなければ)それ、なければ null。

  6. prioritySource がnullなら、固定優先度・中断不可タスクシグナル作成("user-visible")の結果を設定。

  7. handleタスクハンドル作成result, abortSource)で得る。

  8. abortSource がnullでなければ、handleabort stepsabortSource に追加。

  9. handlequeue に、スケジューラータスクキュー選択scheduler, prioritySource, true)を設定。

  10. アルゴリズム実行タスクをスケジューリングscheduler, handle, 下記手順):

    1. result を resolve する。

  11. result を返す。

注: 固定優先度・中断不可signalはキャッシュ再利用可。

To スケジューラータスクキューを選択する for a Scheduler schedulerTaskSignal オブジェクト signal、boolean isContinuation の場合:
  1. signal固定優先度を持たないなら、

    1. scheduler動的優先度タスクキューマップ に (signal, isContinuation) が 含まれていなければ

      1. queueスケジューラータスクキュー作成signalpriorityisContinuation・下記手順)とする。

        1. 動的優先度タスクキューマップ[(signal, isContinuation)] を削除。

      2. 動的優先度タスクキューマップ[(signal, isContinuation)] に queue を設定。

      3. 優先度変更アルゴリズム追加signal に対して次のステップで追加:

        1. queueprioritysignalpriority に設定。

    2. 動的優先度タスクキューマップ[(signal, isContinuation)] を返す。

  2. それ以外の場合

    1. prioritysignalpriority とする。

    2. scheduler静的優先度タスクキューマップ に (priority, isContinuation) が 含まれていなければ

      1. queueスケジューラータスクキュー作成priority, isContinuation, 下記手順)とする。

        1. 静的優先度タスクキューマップ[(priority, isContinuation)] を削除。

      2. 静的優先度タスクキューマップ[(priority, isContinuation)] に queue を設定。

    3. 静的優先度タスクキューマップ[(priority, isContinuation)] を返す。

To アルゴリズム実行タスクをスケジューリングする for Scheduler schedulerタスクハンドル handle と アルゴリズム steps を渡す場合:
  1. global関連globalオブジェクトschedulerの)とする。

  2. documentglobal関連Document とする(globalWindow オブジェクトなら、それ以外は null)。

  3. event loopscheduler関連エージェントイベントループ とする。

  4. enqueue orderevent loopnext enqueue order とする。

  5. event loopnext enqueue order を1増やす。

  6. handletask に、スケジューラータスクキューにタスクをキューするhandlequeueenqueue orderposted task task sourcedocument・下記手順)を設定:

    1. steps を実行する。

    2. handletask complete steps を実行する。

このアルゴリズムは in parallel ステップから呼ばれるため一部競合状態になる可能性がある。特に next enqueue order の更新や スケジューラータスクキュー へのアクセスは原子的に行うべき。後者はイベントループのタスクキューにも影響(詳細は このissue 参照)。

2.4.3. 次に実行するタスクの選択

Scheduler scheduler は、実行可能なタスクを持つ、とする。これは 実行可能なタスクキューの取得scheduler で行った結果が 空でない場合である。
実行可能なタスクキューの取得 Scheduler scheduler について:
  1. queuesschedulerstatic priority task queue map の値取得 結果とする。

  2. queues を拡張。schedulerdynamic priority task queue map の値取得結果で拡張する。

  3. queues から、queuetasksrunnablescheduler task が含まれないものを削除。

  4. queues を返す。

すべてのスケジューラーから次のスケジューラータスクキューを選択するイベントループ event loop を渡し、次を実行する。スケジューラータスクキュー または関連する Schedulerevent loop に関連付けられていて 実行可能なタスクを持つ 場合のみ返す。なければ null。
  1. queues を空の セット とする。

  2. schedulers を、セットで、 すべてのScheduler のうち 関連エージェントevent loopevent loop かつ 実行可能なタスクを持つもの。

  3. scheduler について、queues を、 実行可能なタスクキューの取得 (scheduler) の結果で拡張する。

  4. もし queues なら null を返す。

  5. queues から、queue実効優先度queues 内の他の item より小さいものを削除する。

  6. queuequeues 内で first runnable taskもっとも古い スケジューラータスクキュー とする。
    enqueue順序は一意なので2つのタスクが同じageを持つことはない。

  7. queue を返す。

注: 次に実行されるタスクは、全Schedulerの中から event loop に紐づく、最も古くて(enqueue順序が最小)、かつ最高優先度の runnable scheduler task である。

2.5.

TODO(shaseley): 例を追加予定。

3. タスクの制御

Scheduler インターフェースでスケジューリングされたタスクは、TaskController を使い、controller.signal が提供する TaskSignaloption に渡して 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を返します。

新しいTaskPriorityevent.target.priorityで取得できます。

previousPriorityのgetter手順は、対応属性の初期値を返すことです。

3.2. TaskControllerインターフェース

dictionary TaskControllerInit {
  TaskPriority priority = "user-visible";
};

[Exposed=(Window,Worker)]
interface TaskController : AbortController {
  constructor(optional TaskControllerInit init = {});

  undefined setPriority(TaskPriority priority);
};

注: TaskControllersignal のgetterは AbortController から継承されており、 TaskSignal オブジェクトを返します。

controller = new TaskController( init )

新しいTaskController を返します。そのsignal は新しく生成されたTaskSignal に割り当てられ、priorityinitpriority で初期化されます。

controller . setPriority( priority )

このメソッドを呼び出すと、関連するTaskSignalpriorityが変更され、オブザーバへ通知とprioritychangeイベントの発火が行われます。

new TaskController(init) コンストラクタ手順:
  1. signal を新しい TaskSignal オブジェクトとする。

  2. signalpriorityinit["priority"] に設定する。

  3. thissignalsignal に設定する。

setPriority(priority)メソッド手順は、 signal priority changethissignalpriority を与え実行します。

3.3. TaskSignal インターフェイス

dictionary TaskSignalAnyInit {
  (TaskPriority or TaskSignal) priority = "user-visible";
};

[Exposed=(Window, Worker)]
interface TaskSignal : AbortSignal {
  [NewObject] static TaskSignal _any(sequence<AbortSignal> signals, optional TaskSignalAnyInit init = {});

  readonly attribute TaskPriority priority;

  attribute EventHandler onprioritychange;
};

注記: TaskSignalAbortSignal を継承し、AbortSignal を受け入れるAPIで利用できる。また、postTask()AbortSignal も受け入れ、動的な優先順位付けが不要な場合に便利である。

TaskSignal . any(signals, init)
TaskSignal インスタンスを返す。このインスタンスは、signals のいずれかが中止された場合に中止される。その abort reason は、中止を引き起こした signals のものとなる。 シグナルのpriorityinitpriority で決定される。 これは固定値の TaskPriority または TaskSignal であり、後者の場合、新しいシグナルの priority はこのシグナルの値に応じて変化する。
signal . priority

このシグナルの TaskPriority を返す。

TaskSignal オブジェクトは、関連付けられた priorityTaskPriority)を持つ。

TaskSignal オブジェクトは、関連付けられた priority changingboolean、初期値は false)を持つ。

TaskSignal オブジェクトは、関連付けられた priority change algorithmsアルゴリズムの集合で、priority changing が true の時に実行される。初期値は空)。

TaskSignal オブジェクトは、関連付けられた source signalTaskSignal への弱参照で、 このオブジェクトの priority の依存先)、初期値は null。

TaskSignal オブジェクトは、関連付けられた dependent signalsTaskSignal オブジェクトへの弱い セット、初期値は空)がある。

TaskSignal オブジェクトは、関連付けられた dependent(boolean、初期値は false)を持つ。


priority ゲッタの手順は、thispriority を返すことである。

onprioritychange 属性は、イベントハンドラー IDL 属性であり、 onprioritychange イベントハンドラー用で、イベントハンドラーイベントタイプprioritychange である。

static な any(signals, init) メソッドの手順は、 依存タスクシグナルの作成signals, init, current realm の引数で呼び出し、その結果を返すことである。


TaskSignal 固定優先度を持つ のは、 dependent シグナルで、その source signal が null の場合である。

優先度変更アルゴリズムの追加は、 algorithmTaskSignal オブジェクト signalpriority change algorithms追加する。

依存タスクシグナルの作成 の手順(リスト of AbortSignal オブジェクト signalsTaskSignalAnyInit initrealm):
  1. resultSignal依存シグナルの作成の結果として signalsTaskSignal インターフェイスと realm で作成する。

  2. resultSignaldependent を true に設定。

  3. init["priority"] が TaskPriority である場合:

    1. resultSignalpriorityinit["priority"] を設定する。

  4. それ以外の場合:

    1. sourceSignalinit["priority"] とする。

    2. resultSignalprioritysourceSignalpriority を設定する。

    3. sourceSignal固定優先度を持たない 場合:

      1. sourceSignaldependent が true の場合、 sourceSignalsourceSignalsource signal にする。

      2. アサート: sourceSignaldependent ではない。

      3. resultSignalsource signal を主信号への弱参照に設定する。

      4. resultSignalsourceSignaldependent signals に追加。

  5. resultSignal を返す。

signal priority changeTaskSignal オブジェクト signal に対して、TaskPriority priority を指定して行う手順:
  1. もし signalpriority changing が true であれば、throw により「NotAllowedError」 という DOMException を発生させる。

  2. もし signalprioritypriority と等しければ、処理を終了する。

  3. signalpriority changing を true に設定する。

  4. previousPrioritysignalpriority とする。

  5. signalprioritypriority に設定する。

  6. For each algorithm of signal’s priority change algorithms、各 algorithm を実行する。

  7. Fire an event named prioritychangesignal に対して TaskPriorityChangeEvent を使って発火し、その previousPriority 属性を previousPriority に初期化する。

  8. For each dependentSignal of signal’s dependent signalssignal priority changedependentSignal に対して priority で実行する。

  9. signalpriority changing を false に設定する。

固定優先度・abort不可タスクシグナルの作成TaskPriority priorityrealm realm):
  1. init を新しい TaskSignalAnyInit とする。

  2. init["priority"] に priority を設定する。

  3. 依存タスクシグナル作成(« », init, realm)の結果を返す。

3.3.1. ガベージコレクション

dependentTaskSignal オブジェクトは、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属性のゲッタ手順は、 thisscheduler を返すこと。

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 loopcurrent continuation statecontinuation state または null)を持ち、初期値は null である。

次のアルゴリズムを追加:

continuation state の値を設定keyvalueeventLoop):
  1. eventLoopcurrent continuation state が null なら、 新しい continuation state をセットする。

  2. continuationStateeventLoopcurrent continuation state とする。

  3. アサート: continuationStatestate map[key] は 存在しない

  4. continuationStatestate map[key] に value をセットする。

continuation state の値を取得keyeventLoop ):
  1. continuationStateeventLoopcurrent continuation state とする。

  2. continuationState が null でなく、かつ state map[key] が 存在する場合、その値を返し、そうでなければ null を返す。

4.1.3. イベントループ: 処理モデル

イベントループ処理ステップ(step 2 の前)に次の手順を追加:

  1. queues を、セットとして、 event looptask queues から 1 つ以上 実行可能task を含むものを集めたものとする。

  2. schedulerQueue全スケジューラから次のスケジューラタスクキューを選択の結果とする。

step 2 を次のように修正:

  1. schedulerQueue が null でない、または queues空でない場合:

step 2.1 を次のように修正:

  1. taskQueue を次のいずれかとして、実装依存な方法で選ぶ:

    • queues空でない場合、 task queues のいずれか(実装依存で選択)。

    • schedulerQueue が null でない場合、その tasks

注記: HTML規格は各task source単位での優先度付けを可能にするため、イベントループ処理手順における 次の tasktask 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のtaskQueuetasks の集合か scheduler tasks の集合のいずれかであり、以降のステップはどちらにも removeitem しか使わないので おおむね互換となっている。本来なら共通のタスクキューインターフェイスを設け、pop() メソッドがplainな task を返せるといいが、それには相応のリファクタが要る。

4.1.4. イベントループ: タスクのキューイング

マイクロタスクをキューするアルゴリズムを、オプションのブーリアンignoreContinuationState(デフォルトはfalse)を受け入れるように変更する。

ステップ5を次のように変更する:

  1. continuationState を null にする。

  2. ignoreContinuationState が false かつ eventLoopcurrent continuation state が null でない場合、 continuationStateevent loopcurrent continuation state のクローンに設定する。

  3. microtasksteps を次のように設定する:

    1. ignoreContinuationState が false の場合、eventLoopcurrent continuation statecontinuationState に設定する。

    2. steps を実行する。

    3. ignoreContinuationState が false の場合、eventLoopcurrent continuation state を null に設定する。

4.1.5. HostMakeJobCallback(callable)

ステップ5の前に次を追加:

  1. event loopincumbent settingsrealmagentevent loop とする。

  2. state を、event loopcurrent continuation state のクローン(current continuation state が null なら null)に設定する。

ステップ5を次のように修正:

  1. JobCallback Record { [[Callback]]: callable, [[HostDefined]]: { [[IncumbentSettings]]: incumbent settings, [[ActiveScriptContext]]: script execution context, [[ContinuationState]]: state } } を返す。

4.1.6. HostCallJobCallback(callback, V, argumentsList)

ステップ5の前に次を追加:

  1. event loopincumbent settingsrealmagentevent loop とする。

  2. event loopcurrent continuation statecallback.[[HostDefined]].[[ContinuationState]] に設定する。

ステップ7の後に次を追加:

  1. event loopcurrent continuation state を null に設定する。

4.1.7. HostEnqueuePromiseJob(job, realm)

ステップ2を次のように変更:

  1. ignoreContinuationState を true にして、以下の手順を実行するマイクロタスクをキューする。

4.2. requestIdleCallback()

4.2.1. アイドルコールバックの実行アルゴリズム

ステップ3.3の前に次を追加:

  1. realmwindow の relevant realm とする。

  2. state を新しい scheduling state とする。

  3. statepriority source を、"background" および realm を用いた 固定優先度unabortableタスクシグナル作成 の結果に設定する。

  4. schedulerScheduler で、その relevant realmrealm であるものとする。

  5. scheduler の current scheduling state を state に設定する。

ステップ3.3の後に次を追加:

  1. event loopcurrent continuation state を null に設定する。

5. セキュリティの考慮事項

このセクションは規範的でない。

本仕様で定義されたAPIの主なセキュリティ上の考慮事項は、タイミングベースのサイドチャネル攻撃によってオリジン間で情報が漏洩する可能性があるかどうかである。

5.1. postTask を高精度タイミングソースとして用いる場合

このAPIは高精度なタイミングソースとしては使えない。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についてプライバシーの観点から検討したが、特筆すべき問題はないと結論付けている。

適合性

文書規約

適合要件は、記述的な主張と RFC 2119 の用語を組み合わせて表現される。 この文書の規範的部分におけるキーワード “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, “OPTIONAL” は RFC 2119 で説明されている通りに解釈される。 ただし、可読性のため、これらの語は本仕様書ではすべて大文字では登場しない。

本仕様のテキストは、明示的に非規範的と示された部分、例、ノートを除き、すべて規範的である。 [RFC2119]

この仕様書の例は “for example” という語句で始まるか、または class="example" により規範文書と区別されて記される。例:

これは情報提供用の例である。

情報ノートは “Note” という語で始まり、 class="note" により本文とは区別されて記述される。例:

Note, これは情報ノートの例である。

索引

本仕様で定義される用語

参照仕様で定義される用語

参考文献

規範参考文献

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/multipage/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

情報提供参考文献

[REQUESTIDLECALLBACK]
Scott Haseley. requestIdleCallback(). URL: https://w3c.github.io/requestidlecallback/

IDL 索引

enum TaskPriority {
  "user-blocking",
  "user-visible",
  "background"
};

dictionary SchedulerPostTaskOptions {
  AbortSignal signal;
  TaskPriority priority;
  [EnforceRange] unsigned long long delay = 0;
};

callback SchedulerPostTaskCallback = any ();

[Exposed=(Window, Worker)]
interface Scheduler {
  Promise<any> postTask(SchedulerPostTaskCallback callback,
                        optional SchedulerPostTaskOptions options = {});
  Promise<undefined> yield();
};

[Exposed=(Window, Worker)]
interface TaskPriorityChangeEvent : Event {
  constructor(DOMString type, TaskPriorityChangeEventInit priorityChangeEventInitDict);

  readonly attribute TaskPriority previousPriority;
};

dictionary TaskPriorityChangeEventInit : EventInit {
  required TaskPriority previousPriority;
};

dictionary TaskControllerInit {
  TaskPriority priority = "user-visible";
};

[Exposed=(Window,Worker)]
interface TaskController : AbortController {
  constructor(optional TaskControllerInit init = {});

  undefined setPriority(TaskPriority priority);
};

dictionary TaskSignalAnyInit {
  (TaskPriority or TaskSignal) priority = "user-visible";
};

[Exposed=(Window, Worker)]
interface TaskSignal : AbortSignal {
  [NewObject] static TaskSignal _any(sequence<AbortSignal> signals, optional TaskSignalAnyInit init = {});

  readonly attribute TaskPriority priority;

  attribute EventHandler onprioritychange;
};

partial interface mixin WindowOrWorkerGlobalScope {
  [Replaceable] readonly attribute Scheduler scheduler;
};

課題一覧

HTML仕様をリファクタリングして task のコンストラクターを追加すべきか検討する必要がある。一つの問題は、新しいタスクを scheduler task にする必要があり、task ではだめな点である。
タイムアウト後に手順を実行する は サスペンドを必ずしも考慮していない; whatwg/html#5925 も参照。
このアルゴリズムは in parallel 手順からも呼び出され得るため、この他の アルゴリズムともども競合条件がある。特に next enqueue order はアトミックに更新し、scheduler task queues のアクセスもアトミックに行う必要がある。後者はイベントループのタスクキューにも影響する(この issueも参照)。
この段階の taskQueueset of tasks か、set of scheduler tasks のいずれかとなる。以降の手順は itemremove するだけなので おおよそ互換になる。理想としては、タスクキュー共通のインターフェイスがあり pop() メソッドで単なる task が返せればよいが、大幅なリファクタリングが必要になる。
MDN

Scheduler/postTask

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

Scheduler

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskController/TaskController

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskController/setPriority

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskController

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskPriorityChangeEvent/TaskPriorityChangeEvent

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskPriorityChangeEvent/previousPriority

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskPriorityChangeEvent

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskSignal/priority

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskSignal/prioritychange_event

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskSignal/prioritychange_event

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

TaskSignal

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
MDN

Window/scheduler

Firefox🔰 101+SafariNoneChrome94+
Opera?Edge94+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?