车速表的一个问题或Chromium如何管理内存的故事

现代浏览器是一个极其复杂的项目,即使外观无害的更改也可能导致意外的意外。 因此,有许多内部测试应在发布之前捕获此类更改。 测试永远不会太多,因此使用第三方公开基准也很有用。

我的名字叫Andrey Logvinov,我在下诺夫哥罗德的渲染引擎Yandex.Browser的开发小组工作。 今天,我将以一个导致速度计测试性能下降的神秘问题为例,向哈勃的读者介绍Chromium项目中的内存管理工作原理。 这篇文章是根据我在Yandex.Inside事件中的报告撰写的。




进入性能仪表板后,我们发现车速表测试的速度有所下降。 此测试衡量浏览器在接近实际的应用程序-待办事项列表上的总体性能,该测试将项目添加到列表中,然后将其划掉。 测试结果受V8 JS引擎的性能以及Blink引擎中呈现页面的速度的影响。 Speedometer测试由几个子测试组成,其中测试应用程序是使用一种流行的JS框架(例如jQuery或ReactJS)编写的。 总体测试结果定义为所有框架结果的平均值,但是该测试使您可以分别查看每个框架的性能。 值得注意的是,该测试并非旨在评估框架的性能,它们仅用于使测试的综合性更低,更接近真实的Web应用程序。 子测试的详细显示表明,仅在使用jQuery创建的测试应用程序的版本中观察到了性能下降。 同意,这已经很有趣了。

对这种情况的调查相当标准地开始-我们确定对代码的特定提交导致了问题。 为此,我们在过去的几年中为每个(!)提交存储了Yandex.Browser程序集(重新组装是不切实际的,因为该组装需要几个小时)。 这会占用服务器上的大量空间,但通常有助于快速找到问题的根源。 但是这一次很快没用。 事实证明,测试结果的恶化与提交集成了下一版Chromium的提交相吻合。 结果并不令人鼓舞,因为新版本的Chromium一次带来了大量更改。

由于我们没有收到任何表明具体更改的信息,因此我不得不对该问题进行实质性研究。 为此,我们使用开发人员工具删除了测试痕迹。 我们注意到一个奇怪的功能-执行Javascript测试功能的“时间间隔”。

图片

我们使用about:跟踪删除了更多技术跟踪,并在Blink中看到它是垃圾回收(GC)

图片

下面的内存跟踪显示,这些GC暂停不仅花费大量时间,而且也无助于阻止内存消耗的增长。

图片

但是,如果您在测试中插入一个显式的GC调用,那么我们会看到完全不同的图片-内存保持在零区域,并且不会泄漏。 因此,我们没有内存泄漏,并且问题与收集器的功能有关。 我们继续挖掘。 我们启动调试器,然后看到垃圾回收器绕过了大约50万个对象! 如此多的对象不会影响性能。 但是它们来自哪里?

在这里,我们需要一个有关Blink中垃圾收集器设备的小型闪回。 它删除了死对象,但不移动活动对象,从而允许使用裸露的指针在C ++代码中的局部变量中进​​行操作。 此模式已在Blink中积极使用。 但是它也有其代价-收集垃圾时,您必须扫描堆栈 ,并且如果在其中找到类似于指向来自堆(堆)的对象的指针的东西,则应考虑该对象以及它直接或间接引用的所有内容都是活的。 这导致一个事实,即一些几乎无法接近并因此“死”的物体被识别为是活的。 因此,这种形式的垃圾收集也称为保守垃圾收集。

我们使用堆栈扫描检查连接并跳过它。 问题消失了。

容纳50万个对象的堆栈可能是什么? 我们在添加对象的功能中设置了一个断点-除其他外,我们看到了可疑的地方:

眨眼:: TraceTrait <blink :: HeapHashTableBacking <WTF :: HashTable <blink :: WeakMember ...

哈希表引用可能是可疑的! 我们通过跳过此链接的添加来检验假设。 问题消失了。 好吧,我们离答案更近了一步。

我们回想起Blink中垃圾收集器的另一个功能:如果它看到一个指向哈希表内部的指针,则认为这是表上正在进行迭代的迹象,这意味着它认为该表中的所有链接都是有用的并继续绕过它们。 就我们而言,闲置。 但是,此链接的来源是什么?

我们将堆栈中的几帧向前移,取扫描仪的当前位置,查看其所属功能的堆栈帧。 这是一个称为ScheduleGCIfNeeded的函数。 看来他是罪魁祸首,但是...我们看一下该函数的源代码,发现那里根本没有哈希表。 而且,这已经是垃圾收集器本身的一部分,它根本不需要引用Blink堆中的对象。 这个“坏”链接从何而来?

我们在更改存储单元时设置了一个断点,在其中找到了到哈希表的链接。 我们看到一个名为V8PerIsolateData :: AddActiveScriptWrappable的内部函数在其中写入。 在那里,他们将一些创建的某些类型的HTML元素(包括输入)添加到单个哈希表active_script_wrappables_中。 需要此表来防止除去不再从Javascript或DOM树中引用但与任何外部活动(例如,可以生成事件)相关联的元素。

普通表遍历期间的垃圾收集器会考虑其中包含的元素的状态,并将它们标记为活动或不标记它们,然后在组装的下一阶段将其删除。 但是,在我们的情况下,扫描堆栈时会弹出指向该表内部存储的指针,并且该表的所有元素都标记为活动的。

但是,一个函数的堆栈中的值如何击中另一个函数的堆栈?

想想ScheduleGCIfNeeded。 回想一下,在此函数的源代码中没有发现有用的东西,但这仅意味着是时候下到较低的层次并检查编译器了 。 ScheduleGCIfNeeded函数的反汇编序言如下:

0FCDD13A push ebp 0FCDD13B mov ebp,esp 0FCDD13D push edi 0FCDD13E push esi 0FCDD13F and esp,0FFFFFFF8h 0FCDD142 sub esp,0B8h 0FCDD148 mov eax,dword ptr [__security_cookie (13DD3888h)] 0FCDD14D mov esi,ecx 0FCDD14F xor eax,ebp 0FCDD151 mov dword ptr [esp+0B4h],eax 

可以看出,该函数将esp向下移至0B8h ,并且不再使用该位置。 但是正因为如此,堆栈扫描仪才能看到以前由其他功能记录的内容。 碰巧的是,指向由AddActiveScriptWrappable函数留下的哈希表内部的指针进入了这个“漏洞”。 事实证明,在这种情况下出现“孔”的原因是该函数内部的VLOG调试 ,该在日志中显示其他信息。

但是为什么active_script_wrappable_表具有成千上万个元素? 为什么仅在jQuery测试中观察到性能下降? 这两个问题的答案都是相同的-在此特定测试中,对于每次更改(如复选框中的选中标记),将完全重新创建整个UI。 测试产生的元素几乎立即变成垃圾。 速度计中的其余测试更为谨慎,不会产生不必要的元素,因此,不会观察到它们的性能下降。 如果要开发Web服务,则应考虑到这一点,以免对浏览器造成不必要的工作。

但是,为什么只有VLOG宏在以前才出现问题? 没有确切的答案,但是很可能在更新期间,元素在堆栈上的相对位置发生了变化,因此,哈希表的指针被扫描程序意外访问。 实际上,我们中了彩票。 为了快速消除“漏洞”并恢复性能,我们删除了VLOG调试宏。 对于用户而言,它是无用的,并且对于我们自己的诊断需求,我们始终可以将其重新打开。 我们还与其他Chromium开发人员分享了我们的经验。 答案证实了我们的担忧:这是Blink中保守的垃圾回收的根本问题,它没有系统的解决方案。

有趣的链接


1.如果您有兴趣了解我们小组中其他不寻常的日常生活,那么我们回想一下黑色矩形故事 ,它不仅使Yandex.Browser加速,而且使整个Chromium项目加速。

2.另外,我还邀请您在下一次Yandex.Inside活动中收听其他报道。2月16日的内部活动已开始接受报名,直播也将进行。

Source: https://habr.com/ru/post/zh-CN436154/


All Articles