この仕様は、クライアントの JavaScript 実行時間を測定するためのサンプリングプロファイラーを Web アプリケーションが制御できるようにする API を説明します。

はじめに

現在、複雑な Web アプリケーションからは、クライアント上で JS の 実行時間がどこに費やされているかを限定的にしか把握できません。スタックサンプルを効率的に 収集できないため、アプリケーションは不正確であり、実行を大幅に 低速化させる可能性があるプロファイリング用フックをコードに組み込まざるを得ません。 サンプリングプロファイラーを操作する API を提供することで、 アプリケーションは最小限のオーバーヘッドで、集約および分析に使用できる 詳細な実行データを収集できます。

次の例は、コストの高い処理をユーザーがプロファイリングし、10ms ごとに JS 実行 サンプルを収集する方法を示しています。トレースをサーバーに送信して分析することで、外れ値や JS 実行 特性を集約してデバッグできます。

        const profiler = new Profiler({ sampleInterval: 10, maxBufferSize: 10000 });
        const start = performance.now();
        for (let i = 0; i < 1000000; i++) {
             doWork();
        }
        const duration = performance.now() - start;
        const trace = await profiler.stop();
        const traceJson = JSON.stringify({
          duration,
          trace,
        });
        sendTrace(traceJson);
        

実際によくあるもう一つのシナリオは、ページ読み込み全体にわたって JS をプロファイリングすることです。この例では onload イベントをプロファイリングし、トレースとともにパフォーマンスタイミングデータを送信します。

        const profiler = new Profiler({ sampleInterval: 10, maxBufferSize: 10000 });

        window.addEventListener('load', async () => {
          const trace = await profiler.stop();
          const traceJson = JSON.stringify({
            timing: performance.timing,
            trace,
          });
          sendTrace(traceJson);
        });

        // ページの残りの JS 初期化ロジック
        

定義

サンプルとは、特定の時点における実行の 瞬間的な状態を表す記述子です。各サンプルは スタックに関連付けられます。

スタックとは、最も外側のフレームから最も内側のフレームまで 順番に並べなければならないフレームのリストです。

フレームとは、現在の実行状態に関する情報を含む スタックのコンテキスト内の要素です。

プロファイリングセッション

プロファイリングセッションとは、サンプルを生成する抽象的な生成元です。各セッションは次のものを持ちます。

  1. 状態{started, paused, stopped} のいずれかです。
  2. サンプル間隔。セッションがサンプルを取得する周期として定義されます。

    UA はこの頻度でサンプルを取得することを要求されません。ただし、より高品質な トレースを生成するため、この頻度でサンプルを取得するようサンプリングを優先することが 推奨されます。

  3. プロファイリング対象のエージェント
  4. プロファイリング対象のレルム
  5. サンプルのタイムスタンプを相対的に測定する基準となる時刻起点
  6. サンプルバッファサイズ上限
  7. 取得したサンプルを格納するProfilerTrace

同じページ上で複数のプロファイリングセッションをサポートすべきです。

状態

started 状態では、UA はサンプル間隔が経過するたびに サンプルを取得するアルゴリズムを [= in parallel =] 実行し、ベストエフォートでサンプルを取得すべきです。 paused 状態および stopped 状態では、UA はサンプルを取得すべきではありません。

プロファイリングセッションは started 状態で開始しなければなりません。

UA は、セッションを started から paused へ、および paused から started へ移行してもよいです。

閲覧コンテキストがフォアグラウンドにない場合、ユーザーエージェントは プロファイリングセッションのサンプリングを一時停止することが推奨されます。

stopped のセッションは、started 状態または paused 状態へ移行してはなりません。

処理モデル

プロファイリングセッションを指定してサンプルを取得するには、次の手順を実行します。

  1. ProfilerTrace.samples の長さが、プロファイリングセッションに関連付けられた サンプルバッファサイズ上限以上である場合、関連付けられたProfilersamplebufferfull 型の新しいイベントを発火し、状態を stopped に移行して、 return します。
  2. sample を新しいProfilerSampleとします。
  3. sampleProfilerSample.timestampプロパティを、 プロファイリングセッション時刻起点を基準とする現在の高解像度時刻に設定します。
  4. stack を、プロファイリングセッションのエージェントに関連付けられた 実行コンテキストスタックとします。
  5. sampleProfilerSample.stackIdプロパティを、 stack に対するスタック ID を取得するアルゴリズムの結果に設定します。
  6. sample を、セッションのProfilerTraceに関連付けられた ProfilerTrace.samplesに追加します。

stack に束縛された実行コンテキストスタックを指定して スタック ID を取得するには、次の手順を実行します。

  1. stack が空の場合、undefined を返します。
  2. headstack の先頭要素とし、tailstack からその先頭要素を削除した残りとします。
  3. parentId を、tail に対してスタック ID を取得するを再帰的に呼び出した結果とします。
  4. frameId を、head に対してフレーム ID を取得するを呼び出した結果とします。
  5. frameIdundefined の場合、parentId を返します。
  6. profilerStack を、ProfilerStack.frameIdframeId と等しく、ProfilerStack.parentIdparentId と等しい 新しいProfilerStackとします。
  7. profilerStack およびProfilerTrace.stacksに対して 要素 ID を取得するを実行した結果を返します。

context に束縛された実行 コンテキストを指定してフレーム ID を取得するには、次の手順を実行します。

  1. context に関連付けられた [= realm =] が、プロファイリングセッションに関連付けられた レルムと一致しない場合、undefined を返します。
  2. instance を、context に関連付けられた 関数インスタンスと等しいものとします。
  3. scriptOrModule を、context に関連付けられた ScriptOrModule と等しいものとします。
  4. |attributedScriptOrModule : ScriptOrModule| を、次のアルゴリズムを実行した結果と等しいものとします。
    1. |scriptOrModule| が null でない場合、|scriptOrModule| を返します。
    2. |instance| が組み込み関数オブジェクトである場合、|instance| を呼び出した関数を含む ScriptOrModule を返します。

      上記のロジックの目的は、アクセス不能なスクリプトによって呼び出された組み込み関数が トレースに公開されないようにすることです。そのため、帰属先としてそれらを呼び出した ScriptOrModule を使用します。

      「|instance| を呼び出した関数を含む ScriptOrModule」は、 より厳密に定義すべきです。これを提供するため、ScriptOrModule を定義する スタック上の最上位の実行コンテキストを利用できますが、これは理想的ではありません。 理論上、組み込み関数が実行コンテキストスタックにキューされる別の仕組みが存在する可能性があり、 その場合、帰属は無効になります。

    3. それ以外の場合、null を返します。
  5. |attributedScriptOrModule| が null の場合、undefined を返します。
  6. |attributedScript : Script| を、 |attributedScriptOrModule|.[[\HostDefined]] から取得した [= script =] とします。
  7. |attributedScript| が [= classic script =] であり、その ミュートエラー真偽値が true と等しい場合、 undefined を返します。

    この検査により、CORS クロスオリジンレスポンスで提供されたクロスオリジンスクリプトの スタックフレームが含まれないようにします。この用途をより適切に反映するよう、 ミュートエラーの名称変更を検討してもよいでしょう。

  8. frame を新しいProfilerFrameとします。
  9. frameProfilerFrame.nameを、|instance| に関連付けられた 関数インスタンス名に設定します。
  10. |scriptOrModule| が null でない場合:
    1. script を、 scriptOrModule.[[\HostDefined]] から取得した スクリプトとします。
    2. resourceString を、scriptベース URLと等しいものとします。
    3. ProfilerFrame.resourceIdを、resourceString および ProfilerTrace.resourcesに対して要素 ID を取得するを実行した結果に設定します。
    4. frameProfilerFrame.lineを、 |script| 内で instance が定義されている行の 1 を起点とするインデックスに設定します。
    5. frameProfilerFrame.columnを、 |script| 内で instance が定義されている列の 1 を起点とするインデックスに設定します。
  11. frame およびProfilerTrace.framesに対して 要素 ID を取得するを実行した結果を返します。

list 内の item に対して要素 ID を取得するには、次の手順を実行します。

  1. list 内に item と成分ごとに等しい要素が存在する場合、そのインデックスを返します。
  2. それ以外の場合、itemlist の末尾に追加し、そのインデックスを返します。

Profiler インターフェイス

      [Exposed=(Window, Worker)]
      interface Profiler : EventTarget {
        readonly attribute DOMHighResTimeStamp sampleInterval;
        readonly attribute boolean stopped;

        constructor(ProfilerInitOptions options);
        Promise<ProfilerTrace> stop();
      };
      

Profilerは、正確に一つのプロファイリングセッションに関連付けられなければなりません。

sampleInterval 属性は、関連付けられたプロファイリングセッションサンプル間隔を、DOMHighResTimeStampとして表した値を反映しなければなりません。

stopped 属性は、プロファイリングセッションの状態が stopped である場合に限り、かつその場合に限って true でなければなりません。

new Profiler(options)

new Profiler(options) は、ProfilerInitOptions 型のオブジェクト options を指定して、次の手順を実行します。
  1. options の {{ProfilerInitOptions/sampleInterval}} が 0 未満の場合、 RangeError をスローします。
  2. |globalObject| を現在のグローバルオブジェクトとします。
  3. |globalObject| がWindowまたはWorkerGlobalScopeである場合:
    1. |globalObject| から "js-profiling-mode"ポリシー値を取得します。 |profilingMode| をその結果とします。
    2. |profilingMode| が "eager" または "lazy" でない場合:
      1. |globalObject| から "js-profiling"ポリシー値を取得します。 |jsProfilingEnabled| をその結果とします。
      2. |jsProfilingEnabled| が false の場合、"NotAllowedError" DOMException をスローします。
  4. それ以外の場合、"NotAllowedError" DOMException をスローします。
  5. 次の条件を持つ新しいプロファイリングセッションを作成します。
    1. 関連付けられたサンプル間隔を、ProfilerInitOptions.sampleInterval または UA がサポートする次に小さい間隔のいずれかに設定します。
    2. 関連付けられた時刻起点を、|globalObject| の時刻起点と等しくします。
    3. 関連付けられたサンプルバッファサイズ上限を {{ProfilerInitOptions/maxBufferSize}} に設定します。
    4. 関連付けられた [= agent =] を 周囲のエージェントに設定します。
    5. 関連付けられた [= realm =] を 現在のレルムレコードに設定します。
    6. 関連付けられたProfilerTrace«[{{ProfilerTrace/resources}} → «», {{ProfilerTrace/frames}} → «», {{ProfilerTrace/stacks}} → «», {{ProfilerTrace/samples}} → «»]» に設定します。
  6. 新しく作成されたプロファイリングセッションに関連付けられた新しいProfilerを返します。

stop() メソッド

プロファイラーを停止し、トレースを返します。このメソッドは次の手順を実行しなければなりません。

  1. 関連付けられた [= profiling session =] の状態が stopped である場合、"InvalidStateError" DOMException で [= a promise rejected with =] を返します。
  2. [= profiling session =] の状態を stopped に設定します。
  3. |p:Promise| を [= a new promise =] とします。
  4. 次の手順を [= in parallel =] 実行します。
    1. [= profiling session =] を停止するための [= implementation-defined =] の処理をすべて実行します。
    2. プロファイラーの [= profiling session =] に関連付けられた {{ProfilerTrace}} で |p| を解決します。
  5. |p| を返します。

stop() が呼び出された後に取得されたサンプルは、 プロファイリングセッションに含めるべきではありません。

ProfilerTrace 辞書

      typedef DOMString ProfilerResource;

      dictionary ProfilerTrace {
        required sequence<ProfilerResource> resources;
        required sequence<ProfilerFrame> frames;
        required sequence<ProfilerStack> stacks;
        required sequence<ProfilerSample> samples;
      };
      

resources 属性は、サンプルを取得するアルゴリズムによって設定された ProfilerResource リストを返さなければなりません。

frames 属性は、サンプルを取得するアルゴリズムによって設定された ProfilerFrame リストを返さなければなりません。

stacks 属性は、サンプルを取得するアルゴリズムによって設定された ProfilerStack リストを返さなければなりません。

samples 属性は、サンプルを取得するアルゴリズムによって設定された ProfilerSample リストを返さなければなりません。

この表現は、V8 トレース イベント形式 およびGecko プロファイル形式から着想を得ており、 容易かつ効率的にシリアル化できるよう設計されています。

ProfilerSample 辞書

        dictionary ProfilerSample {
          required DOMHighResTimeStamp timestamp;
          unsigned long long stackId;
        };
        

timestamp は、初期化された値を返さなければなりません。

stackId は、初期化された値を返さなければなりません。

ProfilerStack 辞書

        dictionary ProfilerStack {
          unsigned long long parentId;
          required unsigned long long frameId;
        };
        

parentId は、初期化された値を返さなければなりません。

frameId は、初期化された値を返さなければなりません。

ProfilerFrame 辞書

        dictionary ProfilerFrame {
          required DOMString name;
          unsigned long long resourceId;
          unsigned long long line;
          unsigned long long column;
        };
        

name は、初期化された値を返さなければなりません。

resourceId は、初期化された値を返さなければなりません。

line は、初期化された値を返さなければなりません。

column は、初期化された値を返さなければなりません。

ProfilerInitOptions 辞書

      dictionary ProfilerInitOptions {
        required DOMHighResTimeStamp sampleInterval;
        required unsigned long maxBufferSize;
      };
      

ProfilerInitOptions は、次のフィールドをサポートしなければなりません。

文書ポリシー

この仕様は、プロファイリング機能および初期化動作を制御するため、 文書 ポリシー構成 ポイントを定義します。

js-profiling-mode

この仕様は、js-profiling-mode という名前の構成 ポイントを定義します。そのenum で、許可される値は "eager" および "lazy" です。既定 値は空文字列です。

このポリシーを設定すると、スクリプトが JS セルフプロファイリング API を使用することを許可します。

eager

js-profiling-mode"eager" に設定されている場合、 ページ読み込み中のプロファイリングが想定されていることを UA に通知します。

UA はこのヒントを使用して、文書読み込み中のできるだけ早い段階で必要なメタデータを保存し、 プロファイリングコンポーネントをウォームアップするなど、プロファイリング基盤を早期に 初期化できます。ただし、プロファイリングが実際に使用されていない場合でも、 無視できないパフォーマンスオーバーヘッドが生じる可能性があります。このオーバーヘッドは、 First Contentful Paint (FCP) や Largest Contentful Paint (LCP) などの指標に 悪影響を及ぼす可能性があります。

lazy

js-profiling-mode"lazy" に設定されている場合、 プロファイリングが条件付きで使用されることを UA に通知します。

UA はこのヒントを使用して、最初のProfilerがインスタンス化されるまで プロファイリング関連の初期化オーバーヘッドを遅延させることができ、 プロファイリングが実際に使用されていない場合の重要なレンダリング期間中の パフォーマンスコストを回避できます。ただし、ユーザー操作の処理中に初期化が行われると、 Interaction to Next Paint (INP) に悪影響を及ぼす可能性があります。このモードは、 サンプリングの判断に基づいて条件付きでプロファイリングを有効にする文書に特に適しています。

js-profiling(非推奨)

この仕様は、js-profiling という名前の構成 ポイントも定義します。そのboolean で、既定 値false です。

有効にすると、このポリシーはスクリプトが JS セルフプロファイリング API を使用することを許可し、 プロファイリング基盤の早期初期化を実行するよう UA に通知します (意味的には js-profiling-mode=eager と同等です)。

js-profiling 真偽値構成ポイントは、js-profiling-mode を優先するため 非推奨です。両方が指定された場合、js-profiling-mode が優先され、 js-profiling は無視されなければなりません。実装は後方互換性のために js-profiling をサポートすべきですが、将来そのサポートを削除してもよいです。

自動化

ユーザーエージェントの自動化およびアプリケーションテストのために、この文書は次の [[WebDriver]] 拡張コマンドを定義します。

サンプルを強制取得

HTTP メソッド URI テンプレート
POST `/session/{session id}/forcesample`

サンプルを強制取得拡張コマンドは、 より決定論的なテストを可能にするため、すべての [=プロファイリングセッション=] に [=サンプルを取得する=]ことを強制します。

リモート側の手順は次のとおりです。

  1. |sessions:list| を、現在の閲覧コンテキストで作成された すべての [=プロファイリングセッション=] の [=リスト=] とします。
  2. |sessions| の各 |session:profiling session| について:
    1. |session| の [=状態=] が started である場合、 |session| を指定して [=サンプルを取得する=]を実行します。
  3. データ null を伴う成功を返します。

プライバシーとセキュリティ

次の節では、API におけるプライバシーおよびセキュリティ上の選択について詳しく説明し、 さまざまな種類の攻撃に対する保護戦略を示します。

クロスオリジンスクリプトの内容

この API は、サンプルを取得するアルゴリズムによって含まれるすべての関数が、 ミュートエラープロパティを通じてCORS 同一オリジンとして提供されたスクリプト内で 定義されていることを要求することで、クロスオリジンスクリプトの内容が公開されるのを防ぎます。 ブラウザーの組み込み関数(performance.now() など)も、 [= CORS-same-origin =] スクリプトから呼び出された場合にのみ含めなければなりません。

その結果、この API は、手動による計測ですでに取得可能な情報を超えて、 クロスオリジンスクリプトの内容や実行特性に関する新たな知見を公開しません。 UA が極端に小さいサンプル間隔値(たとえば 1 ミリ秒未満)をサポートする場合は、 この性質が維持されることを検証することが推奨されます。

クロスオリジン実行

クロスオリジン実行コンテキストは、サンプルを取得するアルゴリズム内の レルム検査を通じて API から観測可能であるべきではありません。したがって、 プロファイラーとエージェントを共有するクロスオリジン iframes および その他の実行コンテキストの実行は、この API を通じて観測できません。

タイミング攻撃

高解像度のタイミング情報を得る新たな情報源を導入する可能性がある API では、 タイミング攻撃が引き続き懸念事項となります。トレースで収集されるタイムスタンプは、 サイドチャネル攻撃の新たな経路を公開しないよう、[[?HR-Time]] の 現在の高解像度時刻と同じ情報源から取得すべきです。

[[?HR-Time]] によるクロック 解像度の議論を参照してください。