剖析结构和格式

在本页中,我们将介绍如何解释使用自我剖析 API 捕获的剖析数据。

Profiler.stop() 返回的对象的格式被设计成节省空间的:例如,该格式旨在避免为定义在同一脚本中的函数重复 URL 值。这意味着需要一些解释才能理解剖析对象中的样本如何映射到程序中的位置,本指南旨在解释如何执行此解释。

在第一部分,我们将描述 剖析的抽象结构。在下一部分,我们将描述 Profiler.stop() 返回的剖析对象的格式。最后,我们将 通过一个示例 来展示给定程序的剖析是什么样的以及如何解释它。

剖析的结构

在本节中,我们将描述剖析的抽象结构。请注意,这与由 Profiler.stop() 返回的对象的格式不同:我们将在本指南的下一节中描述该格式。

剖析由一个样本数组组成。每个样本由一个时间戳和一个调用堆栈组成。每个调用堆栈由一个堆栈帧数组组成,每个堆栈帧包含有关其对应函数在程序中位置的信息。

Diagram of a profile

时间戳是 DOMHighResTimeStamp,它以毫秒为单位测量自时间原点以来的时间:对于窗口上下文,这是创建窗口的时间(如果窗口是新的)或浏览器开始导航到此文档的时间。

调用堆栈是 JavaScript 调用堆栈的表示,它使您能够了解在获取样本时程序的执行路径。

调用堆栈由一个堆栈帧数组组成。堆栈帧本质上代表一个嵌套的函数调用,因此,如果函数 a() 调用函数 b(),后者又调用函数 c(),并且在浏览器执行 c() 时获取了一个样本,那么调用堆栈将由帧 [a, b, c] 组成。

js
function c() {
  // sample taken here
}

function b() {
  c();
}

function a() {
  b();
}

每个堆栈帧包含有关其对应函数在程序中位置的信息。

  • 脚本的 URL
  • 函数的名称
  • 脚本中函数定义的行号
  • 行号中的函数定义的列号

剖析格式

虽然上面的部分描述了剖析的*逻辑*结构,但由 Profiler.stop() 返回的对象的格式有所不同。原因是该格式被设计成节省空间的:例如,该格式旨在避免为定义在同一脚本中的函数重复 URL 值。

剖析对象包含四个属性,均为数组。

frames

一个对象数组,每个对象包含有关堆栈帧的信息。

  • column:函数定义的列号。
  • line:函数定义的行号。
  • name:函数的名称。
  • resourceIdresources 数组中某个项的索引,代表函数定义的脚本的 URL。

只有 name 始终存在:如果函数未在脚本中定义(例如,如果是浏览器内置函数),则其他三个属性将被省略。

资源

字符串数组,每个字符串代表一个脚本的 URL。

采样

对象数组,每个对象包含两个属性。

  • timestamp:获取样本时的时间。
  • stackIdstacks 数组中某个元素的索引。
堆栈

对象数组,每个对象包含两个属性。

  • frameIdframes 数组中某个元素的索引,代表堆栈中最嵌套的帧。
  • parentIdstacks 数组中另一个条目的索引,代表到但不包括 frameId 所代表的帧的调用堆栈。如果 frameId 所代表的帧位于堆栈的顶层,则此属性不存在。

示例

在下面的示例中,我们有一个包含按钮的网页:当用户按下按钮时,页面将生成一些素数。

HTML 只包含按钮。

html
<button id="generate">generate!</button>

JavaScript 分布在两个文件中。“main.js”脚本包含按钮的点击处理程序。它开始剖析,然后调用代码生成素数,然后记录生成的剖析数据。

js
// main.js

import { genPrimes } from "./generate.js";

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

  const primes = genPrimes();
  console.log(`Finished generating ${primes.length} primes!`);

  const trace = await profiler.stop();
  console.log(JSON.stringify(trace));
}

document.querySelector("#generate").addEventListener("click", handleClick);

“generate.js”脚本生成素数,分为两个函数:genPrimes()isPrime()

js
// generate.js

const MAX_PRIME = 1000000000;
const PRIMES_QUOTA = 10000;

function isPrime(n) {
  for (let i = 2; i <= Math.sqrt(n); i++) {
    if (n % i === 0) {
      return false;
    }
  }
  return n > 1;
}

export function genPrimes() {
  const primes = [];
  while (primes.length < PRIMES_QUOTA) {
    const candidate = Math.floor(Math.random() * MAX_PRIME);
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }
  return primes;
}

如果我们运行此代码,剖析数据将如下所示,并记录到开发者工具控制台中。

json
{
  "frames": [
    { "name": "Profiler" },
    { "column": 27, "line": 5, "name": "handleClick", "resourceId": 0 },
    { "column": 17, "line": 6, "name": "isPrime", "resourceId": 1 },
    { "column": 26, "line": 15, "name": "genPrimes", "resourceId": 1 }
  ],
  "resources": [
    "https://:3000/main.js",
    "https://:3000/generate.js"
  ],
  "samples": [
    { "stackId": 1, "timestamp": 2972.734999999404 },
    { "stackId": 3, "timestamp": 2973.4899999946356 },
    { "stackId": 3, "timestamp": 2974.5700000077486 },
    { "stackId": 3, "timestamp": 2977.8649999946356 },
    { "stackId": 3, "timestamp": 2978.4899999946356 },
    { "stackId": 3, "timestamp": 2978.6950000077486 },
    { "stackId": 3, "timestamp": 2978.9500000029802 },
    { "stackId": 3, "timestamp": 2979.405000001192 },
    { "stackId": 2, "timestamp": 2980.030000001192 },
    { "stackId": 2, "timestamp": 2980.655000001192 }
  ],
  "stacks": [
    { "frameId": 1 },
    { "frameId": 0, "parentId": 0 },
    { "frameId": 3, "parentId": 0 },
    { "frameId": 2, "parentId": 2 }
  ]
}

此剖析捕获了 10 个样本,列在 samples 属性中。

每个样本的 stackId 属性使我们能够了解样本获取时程序所在的位置,在本例中,我们在三个不同的位置获取了样本。

  • stackId: 1:一个样本。
  • stackId: 3:七个样本。
  • stackId: 2:两个样本。

要查找样本的完整调用堆栈,我们使用 stackId 检索堆栈,然后使用堆栈中的 frameId 值查找最嵌套的函数,然后递归地使用 parentId 获取父堆栈,直到达到没有 parentId 值的顶层。

例如,下图展示了如何为 stackId 为 3 的七个样本推导出完整的调用堆栈。

Deriving a call stack from a sample

另请注意,frames 中的第一项,其 name 值为 Profiler,代表在 Profiler() 构造函数中获取的样本:由于这是浏览器提供的函数,因此该帧不包含脚本信息。