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

Chrome Renderer 进程 CPU 占用 100% 排查 (下) #77

Open
xiaoxiaojx opened this issue Dec 1, 2024 · 0 comments
Open

Chrome Renderer 进程 CPU 占用 100% 排查 (下) #77

xiaoxiaojx opened this issue Dec 1, 2024 · 0 comments

Comments

@xiaoxiaojx
Copy link
Owner

xiaoxiaojx commented Dec 1, 2024

截屏2024-12-01 22 24 19

前情提要

上一篇 Chrome Renderer 进程 CPU 占用 100% 排查 (上) 文章排查了我调试某个详情页时, 每当打开 Chrome Devtool 时页面就立马会卡死的问题。疑似因为开启了Enable Local Overrides功能触发了 Chrome 的 bug, 只能暂时关闭了该功能规避了问题

代码分析

上一篇文章我们知道了最后卡死时代码的执行堆栈停留在了 FindEditPath 函数, 而卡死时正在 diff 的文件是 detail.js

让我们先分析一下 🧐 FindEditPath 函数的代码, 原来这里是 v8 实现的 Myer's Algorithm

Myer's Algorithm 是一种用于计算 最长公共子序列(Longest Common Subsequence,LCS)的算法,尤其是用于 文本差异比较(diff)任务中的优化算法。它被广泛应用于版本控制系统(如 Git)中,以高效地找到两个文本之间的差异。

// v8/src/debug/liveedit-diff.cc

MyersDiffer(Comparator::Input* input, Comparator::Output* output)
      : input_(input),
        output_(output),
        fr_forward_(input->GetLength1() + input->GetLength2() + 1),
        fr_reverse_(input->GetLength1() + input->GetLength2() + 1) {
    // Length1 + Length2 + 1 is the upper bound for our work arrays.
    // We allocate the work arrays once and re-use them for all invocations of
    // `FindMiddleSnake`.
  }

  std::optional<Path> FindEditPath() {
    return FindEditPath(Point{0, 0},
                        Point{input_->GetLength1(), input_->GetLength2()});
  }

问题分析

Myer's Algorithm 的时间复杂度为 O(ND), 其中:

  • N 是第一个序列的长度。
  • D 是两个序列之间的差异数量(即“编辑距离”)

看到这里有种不好的预感, 看了一下 detail.js 文件的大小, 竟然有 11M ⚠️

➜  src git:(a5f3f617bb) ls -lh /Users/temp/detail.js
total 22568
-rw-------@ 1 duoxiaokai  staff    11M 12  1 17:16 detail.js

文件这么大耗时严重貌似解释得通, 严谨一点还是要排查 v8 这里实现的 Myer's Algorithm 是否有性能问题

验证问题

逐行阅读算法的代码寻找优化空间当然最好, 如果你比较擅长算法这块。对于算法渣, 验证问题时确保做到了单一变量法则也能得出正确的结论

此时我通过另外一种语言比如 Node.js 中 Myer's Algorithm 的实现来看看性能表现如何

// diff.js

const myers = require('myers-diff');
const fs = require('fs');
const lhs = fs.readFileSync('/Users/temp/detail.js', 'utf8');
const rhs = lhs + ';alert(1)';

const diff = myers.diff(lhs, rhs);

console.log(myers.formats.GnuNormalFormat(diff));
console.log(diff);

接着运行node diff.js, 然后运行top命令, 最后发现 CPU 占用也达到了 100%+。到这里基本也排除 v8 实现的 Myer's Algorithm 存在性能问题的可能, 因为不同语言的实现版本对于大文本的输入表现基本一致

PID   COMMAND      %CPU  TIME     #TH    #WQ  #PORT MEM    PURG   CMPRS  PGRP PPID STATE    BOOSTS         %CPU_ME %CPU_OTHRS UID  FAULTS   COW    MSGSENT  MSGRECV  SYSBSD    SYSMACH
5480  node         149.1 00:44.05 7/1    0    30    333M-  0B     7552K  5480 4926 running  *0[1]          0.00000 0.00000    501  1269421+ 181    56       21       1147842+  309+

问题探索

其实我有个疑问, 当用户 Overrides 了文件的内容, 为什么需要 diff 了? 直接读取最新文件的内容不就好了么?

此时我们需要对v8/src/debug/liveedit-diff.cc的功能有所了解, liveedit 听上去是实时编辑, 实时生效?

为了验证这个问题我随意打开了一个网页比如 taobao.com, 然后随意覆写了一个 JS 内容, 加上了前面的 4行代码

截屏2024-12-01 22 37 05

然后你可能想到了, 在不刷新页面的情况下, 把 greet 函数的console.log("Hello, world!");改为console.log("Hello, world1!");, 让我们看看运行的结果

截屏2024-12-01 22 42 52

v8 实现了用户实时修改函数代码, 热重载了函数的实现, 引用地址没有变化证明了是同一个对象

问题结论

v8 的 liveedit 是基于 Myer's Algorithm 实现了增量热重载 JS 变量的更新, 相比于全量的 JS 内容更新

  • 优点: 不会修改原对象引用且未改动到的代码部分不会重复执行
  • 缺点: 该算法对于大文本的输入会造成页面卡死 (优化手段可能是把该算法 diff 的逻辑移到其他线程或者将大文本的更新策略降级为全量更新)
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