当前位置: 首页 > 科技观察

如何将 DevTools的堆栈追踪速度提高10倍

时间:2023-03-15 11:14:46 科技观察

如何将DevTools的stacktrace速度提升10倍本文转载请联系天天向上公众号。大家好,我是天天。我今天分享的是如何将ChromeDevTools的堆栈跟踪速度提高10倍。听工作6年的同事说DevTools会影响页面性能。正好在逛国外社区的时候,发现了这篇好文章,借此机会分享给大家。文字Web开发人员已经开始期望在调试代码时几乎不会影响性能。然而,这种期望绝不是普遍的。C++开发人员永远不会期望他们的应用程序的调试版本能够实现生产性能,并且在Chrome的早期,仅仅打开DevTools就可以极大地影响页面的性能。这种性能下降现在已经察觉不到,是多年来对DevTools和V8调试功能的投资的结果。尽管如此,我们永远无法将DevTools的性能开销降低到零。设置断点、单步执行代码、收集堆栈跟踪、捕获性能跟踪等等都会不同程度地影响执行速度。毕竟,所观察到的会改变它。但当然,DevTools的开销——就像任何调试器一样——应该是合理的。我们最近看到,在某些情况下,DevTools会使应用程序变慢,以至于无法再使用的报告数量显着增加。您可以在下面看到来自报告chromium:1069425的并排比较,说明仅打开DevTools的性能开销。报告chromium:1069425链接在这里:https://bugs.chromium.org/p/chromium/issues/detail?id=1069425看看这个视频:从视频中可以看到,速度降低了5倍-10,这显然是不可接受的。第一步是了解所有时间的去向以及导致DevTools打开时速度大幅下降的原因。在Chrome渲染器进程上使用Linuxperf,发现整个渲染器的执行时间分布如下。图1虽然我们有点期待看到与收集堆栈跟踪相关的内容,但我们不会猜到整个执行时间的大约90%都花在了堆栈帧的符号化上。这里的符号化是指从原始堆栈框架中解析函数名称和特定源位置(脚本中的行号和列号)的行为。更令人惊讶的是,方法名称推断几乎所有时间都转到V8中的JSStackFrame::GetMethodName()函数。尽管我们从之前的调查中了解到,JSStackFrame::GetMethodName()在性能问题领域并不陌生。此函数尝试为被视为方法调用的帧(表示obj.func()而不是func()形式的函数调用的帧)计算方法名称。快速浏览一下代码就会发现它的工作原理是通过对对象及其原型链进行完整遍历,并寻找:一个值为func闭包的数据属性一个访问器属性,其中get或set等同于func闭包.现在,虽然这本身听起来并不特别便宜,但听起来也不能解释这种可怕的放缓。因此,我们开始深入研究chromium:1069425中报告的示例,我们发现为异步任务收集了堆栈跟踪以及来自classes.js的日志信息,这是一个10MB的JavaScript文件。仔细检查会发现这基本上是一个Java运行时,加上编译成JavaScript的应用程序代码。堆栈跟踪包含几个帧,其中一些方法在对象A上被调用,因此我们认为可能值得了解我们正在处理的对象类型。chromium:1069425:https://bugs.chromium.org/p/chromium/issues/detail?id=1069425图2显然,从Java到JavaScript的编译器生成了一个包含多达82,203个函数的对象。这显然开始变得有趣了。接下来我们回到V8的JSStackFrame::GetMethodName()看看是否有一些我们可以采摘的唾手可得的果实。它的工作原理是首先查找函数的“名称”作为对象的属性,如果找到,则检查属性的值是否与函数匹配。如果函数没有名称,或者对象没有匹配的属性,它会通过遍历对象的所有属性及其原型来进行反向查找。在我们的示例中,所有函数都是匿名的,并且具有空的“名称”属性。A.SDV=function(){//...};最初的发现是将反向查找分为两步(针对对象本身及其原型链中的每个对象执行):提取所有可枚举属性,然后对每个名称执行通用属性查找,测试得到的属性值是否与我们正在寻找的关闭。这似乎是一个低效的操作,因为提取名称需要遍历所有属性。我们可以一次完成所有工作并直接检查属性值,而不是进行两遍-名称提取的O(N)和测试的O(Nlog(N))。这使得整个功能快了大约2-10倍。第二个发现更有趣。尽管这些函数在技术上是匿名函数,但V8引擎记录了它们的名称,我们称之为推理。对于以obj.foo=function(){...}形式出现在赋值右侧的函数文字,V8解析器将“obj.foo”记住为函数文字的推断名称。所以在我们的例子中,虽然我们没有正确的名字来直接查找它,但我们确实有足够接近的东西。对于上面的A.SDV=function(){...}示例,我们将“A.SDV”作为推断名称,我们可以通过查找最后一个点从推断名称中导出属性名称,然后继续查找对于属性“SDV”。这几乎适用于所有情况,用单个属性查找代替昂贵的完整遍历。这两项改进是CL的一部分,大大减少了chromium:1069425中报告的示例的减速。错误堆栈我们可以在这里结束它,但有些地方不对劲,因为DevTools从不使用堆栈框架方法名称。事实上,C++API中的v8::StackFrame类甚至没有提供获取方法名的方法。所以我们最终首先调用了JSStackFrame::GetMethodName(),这似乎是错误的。相反,我们唯一使用(和公开)方法名称的地方是在JavaScript堆栈跟踪API中。要理解这种用法??,请考虑这个简单的示例这里我们有一个函数foo,它安装在对象名称“bar”下。在Chromium中运行此代码片段会产生以下输出:ErroratObject.foo[asbar](error-methodname.js:2)aterror-methodname.js:6我们在这里看到了方法名称查找的作用。顶部堆栈框架显示为通过名为bar的方法在Object的实例上调用函数foo。所以非标准的error.stack属性大量使用了JSStackFrame::GetMethodName(),事实上我们的性能测试表明我们的改变大大加快了速度。图3但是回到ChromeDevTools的主题,即使不使用error.stack也计算方法名称,这看起来不正确。这里有一些历史可以帮助我们:传统上,V8有两种不同的机制来收集和表示上述两个不同API(C++v8::StackFrameAPI和JavaScript堆栈跟踪API)的堆栈跟踪。用两种不同的方式(大致)做同一件事很容易出错,而且经常会导致不一致和错误,因此在2018年底,我们启动了一个项目来解决堆栈跟踪捕获的单一瓶颈。这个项目非常成功,大大减少了与堆栈跟踪收集相关的问题数量。通过非标准error.stack属性提供的大部分信息也是延迟计算的,仅在真正需要时计算,但作为重构的一部分,我们对v8::StackFrame对象采用了相同的技巧。有关堆栈帧的所有信息都是在第一次调用任何方法时计算的。这通常会提高性能,但不幸的是,它与这些C++API对象在Chromium和DevTools中的使用方式有些背道而驰。特别是因为我们引入了一个新的v8::internal::StackFrameInfo类,它保存了关于通过v8::StackFrame或通过error.stack公开的堆栈帧的所有信息,我们总是将提供的两个API算作信息的超集v8::StackFrame,这意味着对于v8::StackFrame的使用(特别是对于DevTools),我们还将在请求有关堆栈帧的任何信息时计算方法名称。事实证明,DevTools总是立即请求源和脚本信息。基于这种认识,我们能够重构并大幅简化堆栈帧表示,并使其更加惰性,因此V8和Chromium的整个使用现在只需支付计算它们所要求的信息的成本。这为DevTools和其他Chromium用例带来了巨大的性能提升,这些用例只需要关于堆栈框架的一小组信息(本质上只是脚本名称和行和列偏移量形式的源位置),并提供更多的打开性能改进的大门。函数名称通过上述重构,符号化的开销(花在v8_inspector::V8Debugger::symbolize上的时间)减少到整体执行时间的15%左右,我们可以更清楚地看到V8在(收集时)和)符号化堆栈帧以供在DevTools中使用。图4中首先引起注意的是计算行数和列数的累积成本。这里昂贵的部分实际上是计算脚本中的字符偏移量(基于我们从V8获得的字节码偏移量),事实证明,由于我们上面的重构,我们做了两次,一次计算另一次是在计算列号时。在v8::internal::StackFrameInfo实例上缓存源位置有助于快速解决此问题,并完全消除v8::internal::StackFrameInfo::GetColumnNumber的任何配置文件。对我们来说更有趣的发现是v8::StackFrame::GetFunctionName在我们查看的所有配置文件中出奇地高。深入挖掘后,我们发现计算我们在DevTools中显示的堆栈帧中函数的名称是不必要且昂贵的。首先寻找非标准的“displayName”属性,如果它产生一个带有字符串值的数据属性,我们就使用它。否则,返回寻找标准的“名称”属性,并再次检查是否产生值为字符串的数据属性。并最终返回由V8解析器推断并存储在函数字面量中的内部调试名称。添加“displayName”属性是为了解决函数实例上的“name”属性在JavaScript中是只读且不可配置的问题,但它从未被标准化或广泛使用,因为浏览器的开发工具添加了函数名称推断,这确实99.9%的时间。除其他事项外,ES2015使Function实例上的“name”属性可配置,完全消除了对特殊“displayName”属性的需要。由于“displayName”负查找代价高昂且并非真正需要(ES2015于五年前发布),我们决定从V8(和DevTools)中删除对非标准fn.displayName属性的支持。完成对“displayName”的否定查找后,v8::StackFrame::GetFunctionName的一半成本被移除。另一半用于通用的“名称”属性查询。幸运的是,我们已经有了一些逻辑来避免在(未触及的)Function实例上进行昂贵的“名称”属性查找,我们不久前在V8中引入了它以使Function.prototype.bind()本身更快。我们移植了必要的检查,以允许我们首先跳过昂贵的通用查询,结果v8::StackFrame::GetFunctionName不再出现在我们考虑的任何配置文件中。总结通过上述改进,我们显着减少了DevTools在堆栈跟踪方面的开销。视频我们知道仍有各种可能的改进。例如,使用MutationObservers时的开销仍然很明显,如chromium:1077657中所报告,但目前,我们已经解决了主要痛点,将来我们可能会回来进一步简化调试性能。铬:1077657:https://bugs.chromium.org/p/chromium/issues/detail?id=1077657