1
/
5

バックグラウンドタブ内のsetTimeout/setIntervalのスロットリング挙動

Photo by insung yoon on Unsplash

主要ブラウザは表示していないタブのsetTimeout/setInterval間隔を間引くことが知られています。

Chrome: timeouts/interval suspended in background tabs?
Thanks for contributing an answer to Stack Overflow! Please be sure to answer the question. Provide details and share your research! Asking for help, clarification, or responding to other answers. Making statements based on opinion; back them up with refe
https://stackoverflow.com/questions/6032429/chrome-timeouts-interval-suspended-in-background-tabs

この挙動について調査してみました。

実験的に確認する

まずは挙動を実験的に確認してみます。この方法はいつでも簡単に実施できるので、未知のブラウザや新しいバージョンに対しても容易に結果を得ることができます。

次のような簡易的なページを作って実験的に確認してみます。このページではタイマーが発火した時刻を記録し、その間隔を10秒ごとにまとめて60秒間分の履歴を表示するようにしています。これにより、バックグラウンドから復帰したときに直前60秒分の大まかな遷移がわかります。

https://gist.github.com/qnighy/898b76c0fb3369e013814c66c5fabaad

フォアグランドでは以下のように、記載通りの値に収束します。

一方、タブをバックグラウンド化 (別のタブに移動) して1分ほど放置してから戻ってくると、このような表示になります。これは1秒に1回まで間引かれていることを意味しています。 (Mac上のGoogle Chromeの例)

Mac上の主要な3つのWebブラウザに関して、手元で確認した感じでは以下のような結果になりました。 (ただし、電源状態などのテスト条件によって結果が異なる可能性は考えられるので注意してください)

  • Google Chrome: 少なくとも数分間は1秒に1回の頻度になるように間引かれて実行される。ウインドウ全体を隠してからしばらくすると20秒に1回程度まで間引かれる。
  • Firefox: 少なくとも数分間は1秒に1回の頻度になるように間引かれて実行される。
  • Safari: はじめは1〜4秒ほどまで間引かれる。その後数秒すると、徐々に間隔が増えて20秒に1回程度まで間引かれる。少なくとも数分間はその頻度が維持される。

コードリーディング

今回は特に大きく間引かれていたSafariについて、その正確な挙動を確認してみます。

タイマー用の定数として有名な "4ms" (フォアグラウンドタブで5回以上タイマーを繰り返したときのスロットリング挙動) などをヒントに関連するソースコードを探すと、DOMTimer.h に以下の定数が見つかります。

WebKit/DOMTimer.h at WebKit-7615.1.7.1 · WebKit/WebKit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters You can'
https://github.com/WebKit/WebKit/blob/WebKit-7615.1.7.1/Source/WebCore/page/DOMTimer.h#L49-L53
    static Seconds defaultMinimumInterval() { return 4_ms; }
    static Seconds defaultAlignmentInterval() { return 0_s; }
    static Seconds defaultAlignmentIntervalInLowPowerMode() { return 30_ms; }
    static Seconds nonInteractedCrossOriginFrameAlignmentInterval() { return 30_ms; }
    static Seconds hiddenPageAlignmentInterval() { return 1_s; }

そこでこれらの利用箇所を辿っていくと以下のことがわかります。

  • TimerBase::setNextFireTime 内で発火予定時刻を一定間隔にアラインさせる処理がある。
  • その計算は DOMTimer::alignedFireTime に移譲されている。アラインメントの間隔は所定のルールで決定し、オフセットは負荷分散のためにランダムに決定する (一種のjitter)。こうして決まった一定間隔の枠のうち、要求された時刻より後の最も近いタイミングを修正発火タイミングとする。
    • オフセットが毎回ランダムであるため、「アライン」という言葉から想像されるのとはやや違った結果になる。
  • アラインメントの間隔を決定するのは ScriptExecutionContext::domTimerAlignmentInterval である。これはデフォルトでは 0ms を返す ようになっているが、フォアグラウンドプロセスでは Document::domTimerAlignmentInterval でオーバーライドされている。

この、フォアグラウンドタブにおけるアラインメント計算ルールは以下のようになっています。

  • タイマーイベントの段数が5段未満のときは 0ms のまま。
  • 5段以上の場合は、以下の3つのうち最大のアラインメントが使われる。
    • バックグラウンドスロットリングが有効な場合は、 DOMTimer::hiddenPageAlignmentInterval の値 (1s)
    • ページオブジェクトが存在する場合は、 page->domTimerAlignmentInterval() の値
    • 不可視のクロスオリジンフレーム内のドキュメントである場合は、DOMTimer::nonInteractedCrossOriginFrameAlignmentInterval の値 (30ms)

ここで、先ほどChromeで実験したときに観測された「1秒」という間隔の根拠はわかりました。

さらにスロットリングが「20秒」まで伸びる挙動を解明するには、 page->domTimerAlignmentInterval() を読む必要があります。 (これはScriptExecutionContextやDocumentのメソッドとは別です)

domTimerAlignmentInterval メソッド自体は m_domTimerAlignmentInterval を返すだけです。その m_domTimerAlignmentInterval は以下のように更新されます。

  • コンストラクタ内で DOMTimer::defaultAlignmentInterval (0s) に初期化される
  • Page::updateDOMTimerAlignmentInterval 内で更新される。これは以下の3つのどれかになる。
    • スロットリング無効。この場合はデフォルトインターバル (0s) または省電力用のデフォルトインターバル (30ms) に上書きされる。
    • 通常のスロットリングが有効。この場合はバックグラウンド用のインターバル (1s) に上書きされる。
    • 増分スロットリングが有効。この場合インターバルはバックグラウンド用の値 (1s) から開始し、上限 (m_domTimerAlignmentIntervalIncreaseLimit) に達するまで徐々に (指数的に) 引き上げられる。

増分スロットリングでは、増分スロットリングが有効化されてからの経過時間をそのまま次のインターバルにしています。そのため、1回あたりおよそ2倍に増えているように見えることになります。

さて、この増分スロットリングの上限はWebCoreの呼び出し元から設定できるようになっています。この呼び出し元を辿ると、以下の設定に行き着きます。

WebKit/WebProcessPool.cpp at WebKit-7615.1.7.1 · WebKit/WebKit
Home of the WebKit project, the browser engine used by Safari, Mail, App Store and many other applications on macOS, iOS and Linux. - WebKit/WebProcessPool.cpp at WebKit-7615.1.7.1 · WebKit/WebKit
https://github.com/WebKit/WebKit/blob/WebKit-7615.1.7.1/Source/WebKit/UIProcess/WebProcessPool.cpp#L1727-L1730
    // We're estimating an upper bound for a set of background timer fires for a page to be 200ms
    // (including all timer fires, all paging-in, and any resulting GC). To ensure this does not
    // result in more than 1% CPU load allow for one timer fire per 100x this duration.
    static int maximumTimerThrottlePerPageInMS = 200 * 100;

つまり、20秒が上限となっているようです。

Chromeについては未確認ですが、元は同じコードベースであることも考えると、同様に実装されている可能性は高いでしょう。

どうするのが良いのか

バックグラウンドタイマーが必要なのはポーリング目的のことが多いでしょう。以下のことをまず検討するのがよいでしょう。

  • 1秒より短い間隔でポーリングする必要があるか?
  • 20秒より短い間隔でポーリングする必要があるか?
    • この場合、ポーリング間隔は指数的に減衰するので、待機時間が短ければ間引きによるロスも短くなることが期待されるので、20秒という数字ほど悪い結果にはならないとも考えられます。

もし、これらの間引かれたポーリング頻度では不十分な場合は、background workerを立ててポーリングする必要があるかもしれません。

(別の方法として、スロットリングされそうになったときにたくさんのタイマーを起動すればオフセットがいい感じに分散し、結果として十分な頻度でポーリングできる可能性はありますが、あまり良い方法ではなさそう)

まとめ

  • setIntervalはバックグラウンドタブでは間引かれて実行される。
  • 間引かれたときの間隔は実装によるが、標準的な値は「1秒に1回」である。そこから徐々にスロットリングを強化し「20秒に1回」程度まで遅くするブラウザもある。
Wantedly, Inc.では一緒に働く仲間を募集しています
11 いいね!
11 いいね!
同じタグの記事
今週のランキング
Wantedly, Inc.からお誘い
この話題に共感したら、メンバーと話してみませんか?