当浏览器的内存不足时,它们会从中卸载最早的选项卡。 这很烦人,因为单击这样的选项卡会强制重新加载页面。 今天,我们将向Habr的读者介绍Yandex.Browser团队如何使用Hibernate技术解决此问题。
基于铬的浏览器为每个选项卡创建一个过程。 这种方法有很多优点。 这是安全性(站点之间的相互隔离),稳定性(一个进程的崩溃不会拖累整个浏览器)以及加速具有大量内核的现代处理器上的工作。 但是也有一个缺点-内存消耗比使用一个进程处理所有事情都要高。 如果浏览器对此不做任何事情,那么他们的用户将不断看到以下内容:

在Chromium项目中,他们通过清除各种缓存来解决后台选项卡的内存消耗问题。 这与存储已加载页面的图像的缓存无关。 他没有问题-他住在硬盘驱动器上。 在现代浏览器中,RAM中存储了许多其他缓存的信息。
另外,Chromium已经工作了相当长的时间来在后台标签中停止JS计时器。 否则,清除缓存是没有意义的,因为 后台选项卡中的活动将其还原。 人们认为,如果站点希望在后台工作,那么您需要使用服务工作者,而不是计时器。
如果这些措施都无济于事,那么只剩下一件事-从内存中卸载整个标签呈现过程。 一个开放的站点根本就不存在了。 如果切换到选项卡,它将开始从网络下载。 如果标签中有暂停视频,则将从头开始播放。 如果在页面上填写了表格,则输入的信息可能会丢失。 如果该标签中包含沉重的JS应用程序,则需要重新启动它。
当无法访问网络时,卸载选项卡的问题尤其令人不快。 您是否已与Habr推迟在飞机上阅读的标签? 准备好将有用的文章变成南瓜。
浏览器开发人员知道,这种极端的措施对用户来说很烦人(只需
转向搜索以估计范围),因此他们在最后一刻应用了该措施。 目前,由于内存不足,计算机已经在减慢速度,用户注意到了这一点,并正在寻找解决问题的替代方法,因此,例如,
The Great Suspender扩展程序拥有超过140万用户。
人们希望保存浏览器和内存,但并没有开始放缓。 为此,不应在最后一刻卸载这些选项卡,而应将其卸载得更早一些。 为此,您需要停止丢失选项卡的内容,即 使保存过程不可见。 但是那又要节省什么呢? 圆是封闭的。 但是找到了解决方案。
在Yandex Browser上休眠
许多Habr读者可能已经猜到了清除内存的方法,但是如果您首先在硬盘驱动器上卸载状态,则很有可能保存选项卡的状态。 如果单击选项卡以从硬盘驱动器还原该选项卡,则用户将不会注意到任何事情。
我们的团队参与了Chromium项目的开发,并在其中发送了重要的
优化编辑和新
功能 。 早在2015年,我们
就与该项目的同事
讨论了保持硬盘驱动器选项卡状态的想法,甚至设法进行了一些改进,但他们决定冻结Chromium中的该区域。 我们做出了不同的决定,并继续在Yandex.Browser中进行开发。 花了比计划多的时间,但这是值得的。 下面我们将讨论Hibernate技术的技术填充,但是现在,让我们从通用逻辑开始。
每分钟几次,Yandex.Browser都会检查可用内存量,如果它小于阈值600兆字节,则Hibernate起作用。 一切始于浏览器找到最早的(使用)背景标签。 顺便说一句,普通用户打开了7个标签,但是5%的用户打开了30个以上的标签。
您无法从内存中卸载任何旧标签-您可以破坏一些非常重要的内容。 例如,播放音乐或在网络Messenger中聊天。 现在有28个此类异常,如果选项卡中至少有一个不合适,浏览器将继续检查下一个。
如果找到符合要求的选项卡,则将开始保存它的过程。
在Hibernate中保存和还原选项卡
任何页面都可以分为两个大部分,分别与引擎V8(JS)和Blink(HTML / DOM)关联。 考虑一个小例子:
<html> <head> <script type="text/javascript"> function onLoad() { var div = document.createElement("div"); div.textContent = "Look ma, I can set div text"; document.body.appendChild(div); } </script> </head> <body onload="onLoad()"></body> </html>
我们有一些DOM树和一个小的脚本,该脚本只是将div添加到主体中。 从眨眼的角度来看,此页面如下所示:

让我们使用HTMLBodyElement示例查看Blink和V8之间的关系:

您可能会注意到Blink和V8具有相同实体的不同表示形式,并且彼此密切相关。 因此,我们想到了最初的想法-保存V8的完整状态,并让Blink仅以文本形式存储HTML属性。 但这是一个错误,因为我们丢失了未存储在属性中的DOM对象的那些状态。 我们还丢失了未存储在DOM中的状态。 解决此问题的方法是完全保存闪烁。 但不是那么简单。
首先,您需要收集有关Blink对象的信息。 因此,在保存V8时,我们不仅停止了JS并对其进行了强制转换,而且还收集了内存中对JS可用的DOM对象和其他辅助对象的引用。 我们还将遍历所有可从Document对象访问的对象-每个页面框架的根元素。 因此,我们收集有关所有重要资料的信息。 最难的部分是学习储蓄。
如果我们计算代表DOM树的所有Blink类以及不同的HTML5 API(例如canvas,media,geolocation),则会得到成千上万个类。 编写用双手保存所有类的逻辑几乎是不可能的。 但是最糟糕的是,即使执行此操作,也将无法维护,因为我们会定期更新Chromium的新版本,这些新版本会对任何类进行意外更改。
我们所有平台的浏览器都是使用clang构建的。 为了解决保留Blink类的问题,我们为clang创建了一个插件,该插件为类构建AST(抽象语法树)。 例如,此代码:
类代码 class Bar : public foo_namespace::Foo { struct BarInternal { int int_field_; float float_field_; } bar_internal_field_; std::string string_field_; };
它变成了这样的XML:
XML插件的结果 <class> <name>bar_namespace::Bar::BarInternal</name> <is_union>false</is_union> <is_abstract>false</is_abstract> <decl_source_file>src/bar.h</decl_source_file> <base_class_names></base_class_names> <fields> <field> <name>int_field_</name> <type> <builtin> <is_const>0</is_const> <name>int</name> </builtin> </type> </field> <field> <name>float_field_</name> <type> <builtin> <is_const>0</is_const> <name>float</name> </builtin> </type> </field> </class> <class> <name>bar_namespace::Bar</name> <is_union>false</is_union> <is_abstract>false</is_abstract> <decl_source_file>src/bar.h</decl_source_file> <base_class_names> <class_name>foo_namespace::Foo</class_name> </base_class_names> <fields> <field> <name>bar_internal_field_</name> <type> <class> <is_const>0</is_const> <name>bar_namespace::Bar::BarInternal</name> </class> </type> </field> <field> <name>string_field_</name> <type> <class> <is_const>0</is_const> <name>std::string</name> </class> </type> </field> </fields> </class>
此外,我们编写的其他脚本会根据此信息生成C ++代码以保存和还原类,这些代码属于Yandex.Browser程序集。
C ++保存通过脚本从XML获得的代码 void serialize_bar_namespace_Bar_BarInternal( WriteVisitor* writer, Bar::BarInternal* instance) { writer->WriteBuiltin<size_t>(instance->int_vector_field_.size()); for (auto& item : instance->int_vector_field_) { writer->WriteBuiltin<int>(item); } writer->WriteBuiltin<float>(instance->float_field_); } void serialize_bar_namespace_Bar(WriteVisitor* writer, Bar* instance) { serialize_foo_namespace_Foo(writer, instance); serialize_bar_namespace_Bar_BarInternal( writer, &instance->bar_internal_field_); writer->WriteString(instance->string_field_); }
总共,我们为大约1000个Blink类生成代码。 例如,我们学会了将复杂的类保存为Canvas。 您可以使用JS代码从中进行绘制,设置许多属性,设置绘制的画笔参数等等。 我们保存所有这些属性,参数和图片本身。
成功加密所有数据并将其保存到硬盘后,该选项卡过程将从内存中卸载,直到用户返回到该选项卡为止。 在界面中,像以前一样,它并不突出。
选项卡恢复不是即时的,但比从网络下载时要快得多。 不过,我们采取了一项棘手的措施,以免白屏闪烁而使用户烦恼。 我们显示了在保存阶段创建的页面的屏幕截图。 这有助于平滑过渡。 否则,恢复过程类似于常规导航,唯一的区别是浏览器未发出网络请求。 它在其中重新创建框架结构和DOM树,然后替换V8的状态。
我们录制了一段视频,清楚地展示了Hibernate如何卸载和恢复点击选项卡,同时保留了在文本和视频位置输入的JS游戏的进度:
总结
在不久的将来,Hyannate技术将对Yandex.Browser Windows版的所有用户可用。 我们还计划开始在Android的Alpha版中对其进行试验。 有了它,浏览器比以前更有效地节省了内存。 例如,对于具有大量打开的选项卡的用户,Hibernate平均节省330兆字节以上的内存,并且不会丢失选项卡中的信息,在任何网络条件下,单击均可保持访问状态。 我们了解网站管理员考虑卸载背景标签会很有用,因此我们计划支持
Page Lifecycle API 。
Hibernate并不是我们唯一旨在节省资源的解决方案。 这不是我们努力确保浏览器适应系统中可用资源的第一年。 例如,在性能较弱的设备上,浏览器进入简化模式,并且在断开笔记本电脑与电源的连接时,它会降低功耗。 节省资源是一个大而复杂的故事,我们一定会回到哈布雷。