本规范描述了一种 API,允许 Web 应用程序控制采样分析器,以 测量客户端 JavaScript 的执行时间。

简介

目前,复杂的 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. 一个采样间隔,定义为会话获取样本的周期。

    不要求用户代理以此频率获取样本。但是,建议 优先按照此频率进行采样,以 生成质量更高的跟踪数据。

  3. 一个要分析的代理
  4. 一个要分析的领域
  5. 一个时间原点,样本的时间戳以它为 参照进行测量。
  6. 一个样本缓冲区大小限制
  7. 一个存储已捕获样本ProfilerTrace

应当支持同一页面上的多个分析会话。

状态

started 状态下,每当采样间隔结束时,用户代理应当尽最大努力 [= in parallel =]执行获取样本算法来 捕获样本。 在 pausedstopped 状态下,用户代理不应捕获样本。

分析会话必须以 started 状态开始。

用户代理可以将会话从 started 转换为 paused,也可以从 paused 转换为 started

如果浏览上下文不在前台,建议用户代理暂停对分析会话的 采样。

处于 stopped 状态的会话不得转换为 startedpaused 状态。

处理模型

要在给定一个分析会话的情况下获取样本,请执行以下步骤:

  1. 如果 ProfilerTrace.samples 的长度大于或等于与分析会话关联的样本缓冲区大小 限制,则向关联的 Profiler 触发一个类型为 samplebufferfull 的新事件,将状态转换为 stopped, 然后返回。
  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 的顶部元素,令 tail 为从 stack 中移除其顶部元素后剩余的部分。
  3. parentId 为以 tail 递归调用获取堆栈 ID的结果。
  4. frameId 为对 head 调用获取帧 ID的结果。
  5. 如果 frameIdundefined,则返回 parentId
  6. profilerStack 为一个新的 ProfilerStack,其 ProfilerStack.frameId 等于 frameId,且 ProfilerStack.parentId 等于 parentId
  7. 返回以 profilerStackProfilerTrace.stacks 运行获取元素 ID的结果。

要在给定绑定到 context执行 上下文的情况下获取帧 ID,请执行以下步骤:

  1. 如果与 context 关联的[= realm =]与分析会话关联的领域 不匹配,则返回 undefined
  2. instance 等于与 context 关联的函数 实例
  3. scriptOrModule 等于与 context 关联的 ScriptOrModule
  4. 令 |attributedScriptOrModule : ScriptOrModule| 等于运行以下 算法的结果:
    1. 如果 |scriptOrModule| 非空,则返回 |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| 非空:
    1. script 为从 scriptOrModule.[[\HostDefined]] 获取的脚本
    2. resourceString 等于 script基 URL
    3. ProfilerFrame.resourceId 设置为以 resourceStringProfilerTrace.resources 运行获取元素 ID的结果。
    4. frameProfilerFrame.line 设置为 |script| 中定义 instance 的行的从 1 开始的索引。
    5. frameProfilerFrame.column 设置为 |script| 中定义 instance 的列的从 1 开始的索引。
  11. 返回以 frameProfilerTrace.frames 运行获取元素 ID的结果。

要为 list 中的 item 获取元素 ID,请执行以下步骤:

  1. 如果 list 中存在与 item 按分量相等的元素,则返回其索引。
  2. 否则,将 item 追加到 list 的末尾,并返回其索引。

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)

在给定一个类型为 ProfilerInitOptions 的对象 options 时, new Profiler(options) 运行以下步骤:
  1. 如果 options 的 {{ProfilerInitOptions/sampleInterval}} 小于 0,则抛出 RangeError
  2. 令 |globalObject| 为当前全局对象
  3. 如果 |globalObject| 是 WindowWorkerGlobalScope
    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 或用户代理支持的下一个更低间隔。
    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" 时,它向用户代理表明 预计将在页面加载期间进行分析。

用户代理可以使用此提示尽早初始化分析基础设施,并在文档加载期间尽早存储所需的 元数据和预热分析组件。但是, 即使未主动使用分析,这也可能引入不可忽略的性能开销。此 开销可能会对首次内容绘制(FCP)和最大内容 绘制(LCP)等指标产生负面影响。

lazy

js-profiling-mode 设置为 "lazy" 时,它向用户代理表明 将有条件地使用分析。

用户代理可以使用此提示,将与分析相关的初始化开销推迟到首次实例化 Profiler 时,从而在未主动使用分析的关键渲染阶段避免 性能开销。但是,如果初始化发生在用户交互 处理期间,则可能对下次绘制交互(INP)产生负面影响。此模式特别 适用于根据采样决策有条件地启用分析的文档。

js-profiling(已弃用)

本规范还定义了一个名称为 js-profiling配置 点。其类型boolean默认 值false

启用后,此策略授权脚本使用 JS 自我分析 API,并指示用户代理 尽早初始化分析基础设施(在语义上等同于 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 不会暴露任何有关 跨源脚本内容或执行特征的新信息。如果用户代理选择支持极低的 采样间隔值(例如小于 一毫秒),则建议验证这一点仍然成立。

跨源执行

通过获取样本算法中的领域检查,此 API 不应能够观察到跨源执行上下文。 因此,与分析器共享代理的跨源 iframes 和其他执行上下文, 其执行情况不会通过此 API 被观察到。

时序攻击

对于任何可能引入新的高分辨率计时 信息来源的 API,时序攻击始终是一个需要关注的问题。跟踪数据中收集的时间戳应当与 [[?HR-Time]] 的 当前高分辨率时间使用相同来源,以避免暴露新的侧信道攻击途径。

请参阅 [[?HR-Time]] 中关于时钟 分辨率的讨论。