Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS 代码性能问题采集上报的探索:JS-Self-Profiling #23

Open
SunshowerC opened this issue Sep 18, 2024 · 0 comments
Open

JS 代码性能问题采集上报的探索:JS-Self-Profiling #23

SunshowerC opened this issue Sep 18, 2024 · 0 comments

Comments

@SunshowerC
Copy link
Owner

1. 背景

前端监控上报领域,已经有很多成熟的实践,例如通过 performance timing api 获取页面的一些性能,耗时指标。

但对于一些前端 js 代码执行性能开销导致的 性能问题,我们通常只能局限在自己设备上,通过 chrome devtool 的 proformance 进行记录,分析,排查。

通过 performance 面板分析线上 Long Task 代码
image

开发者很难知道 web 应用中的 JavaScript 在真实用户设备上的各种情况下的执行情况,而且无法有效收集堆栈样本。这样的问题排查会存在一些局限:

  • 统计样本过少,结论可能失真。
    真实用户的 js 运行情况,在环境,场景,设备,网络等方面,都可能存在差异性,从而导致不一样的 js 运行效率。从单个设备采集得到的 代码性能开销数据,仅适用于个体,而不适用于总体。这可能会导致误导性的结论。

  • 线上缺少 sourcemap,无法定位准确的源码执行函数。

2. 简要概述

2.1 JS-Self-Profiling 是什么?

JS-Self-Profiling 是一个新的 API,它提供一个标准的 Profiler API ,

通过提供 API 来操作浏览器底层的代码采样分析,Web 应用可以收集丰富的执行数据,以最小的开销进行聚合和分析。 本质上和 chrome devtool performance 功能类同。

基本原理:以固定的频率,采样运行时的调用堆栈上运行的内容。

JS-Self-Profiling API 暂时处于 ECMAScript Stage 2 draft 阶段。

2.2 JS-Self-Profiling 怎么用?

  1. 资源文件 配置响应头 Document-Policy 允许使用 js-self-profiling API
# 响应头设置
Document-Policy: js-profiling

不想跑一个服务来配置响应头的话,除了通过一些代理配置工具实现指定响应头设置外,也可以通过一些 chrome extension 配置响应头

  1. 代码执行堆栈 采集的开始与结束
// 开始采集,此处最大采集时长 = 10ms * 10000 = 100s
const profiler = new Profiler({ 
  sampleInterval: 10, // 采用间隔
  maxBufferSize: 10000  // 采样缓冲区最大数量
});

// 很多业务代码执行...

// 停止数据采集
const trace = await profiler.stop();

// 上报 采样 数据
reportToServer(trace)

配合 performance timing api ,进行上报页面加载时的 性能上报

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

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

3. 实践探索

3.1 Profile 采集上报

  1. 开始采集
  2. 业务代码执行......
  3. 结束采集,得出 profile 数据
const trace = await profiler.stop();
// trace 数据结构为:
/*
{
  "resource": string[],
  "frames": ProfileFrame[],
  "stacks": ProfileStack[],
  "sameples": ProfileSample[],

}
*/

3.1.1 采样数据结构

  • resource: string[] ; 本次 js 采集得到的调用栈对应的 js 文件
// 例子:
{
  "frames": [],
  "resource": [
     "http://localhost:3000/index.js"
     "https://lf-cdn-tos.bytescm.com/obj/static/apaas/kunlun/app_dev_main/production/externals/vendors/react-dom/16.13.1/amd/react-dom.production.min.js",
     "https://lf-cdn-tos.bytescm.com/obj/static/apaas/kunlun/app_dev_main/production/externals/vendors/react/16.13.1/amd/react.production.min.js",
     "https://lf3-short.ibytedapm.com/slardar/fe/sdk-web/browser.cn.js?bid=kunlun_fe_web&globalName=KSlardarWeb",
     // ...
  ],
  "sameples": [],
  "stacks": []
}
  • Frames: 本次采集样本执行过的所有函数,可以定位到具体的代码。根据 frames 数据可以得到当前采用所有的执行过的代码函数有哪些。
    例如,以下的数据样例说明采样中, 函数 main,sendTrace, doSomething 被执行过了。
interface ProfileFrame {
  name: string // 当前帧对应代码执行的函数名
  resourceId: number // 调用函数对应的 文件,可联合上述的 resource 定位到对应的文件
  line: number     // 函数在 代码文件的第几行, 第几列执行
  column: number   
}

// 例子
 "frames": [
      //  Profiler 的初始化
      {
          "name": "Profiler"
      },
      // main 函数, 函数位置在 resource[0]对应的 js 文件中 第 23 行 20 列
      {
          "column": 20,
          "line": 23,
          "name": "main",
          "resourceId": 0
      },
      {
          "column": 1,
          "line": 1,  
          "name": "",  // 无对应函数,js 执行的起始 
          "resourceId": 0
      },
      // sendTrace函数, 位置...
      {
          "column": 19,
          "line": 5,
          "name": "sendTrace",
          "resourceId": 0
      },
      // doSomething 函数, 位置...
      {
          "column": 21,
          "line": 14,
          "name": "doSomething",
          "resourceId": 0
      }
  ],
  • Stacks: 描述,代码之间调用顺序的调用栈信息。
    根据 stacks 数据可以得到所有函数的调用关系。例如以下例子可以得出 代码函数调用栈 A() -> B() -> C()
interface ProfileStack {
  frameId: number // 对应上述 frame 数组的 index 
  parentId: number // 当前 stack 数组的 index,可以确定函数的调用栈先后顺序关系。
}


// 参考例子
  "stacks": [
      {
          "frameId": 2  // 对应上述的 frames[2],无对应函数,js 执行的起始 
      },
      {
          "frameId": 1, // 对应上述的 frames[1], 函数 main()
          "parentId": 0
      },
      {
          "frameId": 0, // 对应 Profiler 
          "parentId": 1 // stacks[1],即父层级是 main 函数,所以调用栈是 main() -> Profiler
      },
      {
          "frameId": 4, // 以此类推,调用栈: main() -> doSomething()
          "parentId": 1
      },
      {
          "frameId": 3, // 调用栈: main() -> doSomething() -> sendTrace()
          "parentId": 3
      }
  ]
  • samples: 定义代码调用栈的时序。可以得到每一个调用栈的调用时间节点。
interface ProfileSample {
  // 相对于 页面初始化的时间。
  // 例如,页面打开 10s 过后,开始 profiling, 第一个 samples[0].timestamp 值是 10000 ,单位 ms
  timestamp: number
  // 对应上述 stack 数组的 index  
  stackId?: number 
}

// 例子
samples:  [{
      // 第一帧的 js 行为,调用栈是 stacks[2],即 main()->Profiler
      "stackId": 2,
      "timestamp": 256.27000004053116
  },
  {
      // 过了 10ms,进行第2次采集,该时间节点的 js 行为,调用栈是 stacks[4],
      // 即 main() -> doSomething() -> sendTrace()
      "stackId": 4,
      "timestamp": 267.8799999952316
  },
  {
     // 又过了 10ms,进行第3次采集,该时间节点的 js 行为,调用栈是 stacks[3]
     // 即 main() -> doSomething()
      "stackId": 3,
      "timestamp": 278.2450000047684
  },
  // ...
 ]

根据 samples 的时序描述,可以得出如下的调用堆栈时序图
image

3.2 数据可视化

已知 Profiler 采集到的数据包含了以下信息

  • 执行过 Js 代码的对应的 source 路径 【Resource】
  • 执行过的 JS 函数以及 函数对应的代码位置 【Frames】
  • JS 函数之间的调用堆栈关系路径 【Stacks】
  • 每个调用栈触发的时序【Samples】

根据这些信息,可以将采集到的 trace 数据可视化成 火焰图。

// TODO: [Demo 访问地址]
image

3.3 配合 SourceMap 定位问题代码

每个调用堆栈信息一般都包含了 代码调用时的:

  • resourceURI js 文件
  • Line: 函数调用的行
  • column: 函数调用列

如果获取到 resourceURI 源码对应的 sourcemap 文件,即逆向出 混淆加密前的 ts 代码源码,以及该函数对应 ts 代码的位置。

image

3.4 采集优化

3.4.1 API 自身性能开销

JS-Self-Profiling API 是为了采集业务 js 代码的执行开销。

  • 那么,"采集 js 代码执行开销" 这个动作本身,有没有开销呢? 答案是肯定的。
    因为即便是最简单的 js 代码 i++ 执行,都会占用 js 线程的资源,区别是占用资源的多与少。

对于 JS-Self-Profiling 开销大小,是否会对业务造成负面影响的问题,
FaceBook 对此有过统计:启用了 JS-Self-Profiling 的应用,加载速度比原本变慢了,幅度 < 1% 。 也就是说,采集行为本身对业务的影响微乎其微。
结论参考来源: https://github.com/WICG/js-self-profiling/blob/main/doc/tpac-2021-slides.pdf

3.4.2 数据块大小

根据 Web 应用的复杂度不同,Profiler 的 sampleInterval 采样率配置不同,采集得到的 profile 数据大小也不一样。

根据 aPaaS 开发后台 Profile 采集数据大小统计:
在 采样间隔为 10ms ,平均采集 1s ,会产生 1~2kb (gzip 后) 的数据包。

这可能会带来比较高的流量开销,以及数据存储成本。

3.4.3 优化策略

  • 数据预过滤。
    实际应用中,开发者可能只关心一些 long task 的函数调用场景,其余绝大部分的 js 代码执行调用栈数据可能都无需关注。计算过滤出 long task 的 trace 数据,丢弃掉其他数据,既可以降低数据块大小,又能够减少干扰信息。

  • 应用场景控制。
    尽管 JS-Self-Profiling 对业务影响有限,但考虑其对业务运行时依然有细微影响。应该尽可能控制 仅在某些特定的需要性能分析的页面或者业务场景 中启用,不应滥用。

  • 采样率控制。
    控制采样率,或者只在指定特征的群体启用。

5. 数据流转链路

image

  1. 局限性

还处于草案阶段,目前主流浏览器仅 chrome 浏览器内核版本 94+ 支持
image

业界使用情况: Sentry Profiling for Browser [Beta]

  1. 参考资料
  2. https://wicg.github.io/js-self-profiling/#references
  3. https://calendar.perfplanet.com/2021/js-self-profiling-api-in-practice/#js-self-profiling-conclusion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant