Когда браузерам не хватает памяти, они выгружают из нее наиболее старые вкладки. Это раздражает, потому что клик по такой вкладке приводит к принудительной перезагрузке страницы. Сегодня мы расскажем читателям Хабра о том, как команда Яндекс.Браузера решает эту проблему с помощью технологии Hibernate.
Браузеры, основанные на Chromium, создают по процессу на каждую вкладку. У этого подхода множество достоинств. Это и безопасность (изоляция сайтов друг от друга), и стабильность (падение одного процесса не тянет за собой весь браузер), и ускорение работы на современных процессорах с большим количеством ядер. Но есть и минус – более высокое потребление оперативной памяти, чем при использовании одного процесса на всё. Если бы браузеры ничего с этим не делали, то их пользователи постоянно видели бы что-то подобное:
В проекте Chromium борются с потреблением памяти фоновыми вкладками с помощью очистки различных кэшей. Речь не про тот кэш, в котором хранятся картинки загруженных страниц. С ним проблем нет – он живет на жестком диске. В современном браузере много другой закэшированной информации, которая хранится в оперативной памяти.
Также в Chromium уже достаточно давно работают над тем, чтобы останавливать JS-таймеры в фоновых вкладках. Иначе очистка кэшей теряет смысл, т.к. активности в фоновых вкладках их восстанавливают. Считается, что если сайты хотят работать в фоне, то нужно использовать service worker, а не таймеры.
Если эти меры не помогают, то остается только одно – выгрузить из памяти весь процесс рендеринга вкладки. Открытый сайт просто перестает существовать. Если переключиться на вкладку, то начнется загрузка из сети. Если во вкладке было видео на паузе, то оно начнет проигрываться с начала. Если на странице заполнялась форма, то введенная информация может быть утеряна. Если во вкладке работало тяжелое JS-приложение, то понадобится запустить его заново.
Проблема выгрузки вкладок особенно неприятна при отсутствии доступа к сети. Отложили вкладку с Хабром для чтения на борту самолета? Будьте готовы, что полезная статья превратится в тыкву.
Разработчики браузеров понимают, что эта крайняя мера вызывает раздражение у пользователей (достаточно обратиться к поиску, чтобы оценить масштабы), поэтому применяют ее в последний момент. В этот момент компьютер уже тормозит из-за нехватки памяти, пользователи это замечают и ищут альтернативные способы решения проблемы, поэтому, к примеру, у расширения The Great Suspender более 1,4 млн пользователей.
Люди хотят, чтобы браузеры и память экономили, и тормозить при этом не начинали. Для этого вкладки нужно выгружать не в последний момент, а чуть раньше. А для этого нужно перестать терять содержимое вкладок, т.е. сделать процесс экономии незаметным. Но на чем тогда экономить? Круг замкнулся. Но решение нашлось.
Многие читатели Хабра уже могли догадаться, что очистить память, но сохранить состояние вкладки вполне возможно, если предварительно выгрузить состояние на жесткий диск. Если по клику восстановить вкладку с жесткого диска, то пользователь не заметит ничего.
Наша команда участвует в разработке проекта Chromium, куда отправляет значительные оптимизирующие правки и новые возможности. Ещё в 2015 году мы обсуждали с коллегами из проекта идею сохранения состояния вкладок на жесткий диск и даже успели внести ряд доработок, но это направление в Chromium решили заморозить. Мы решили иначе и продолжили разработку в Яндекс.Браузере. На это ушло больше времени, чем планировали, но это того стоило. Чуть ниже мы расскажем о технической начинке технологии Hibernate, а пока начнем с общей логики.
Несколько раз в минуту Яндекс.Браузер проверяет количество доступной памяти, и если ее меньше, чем пороговое значение в 600 мегабайт, то в дело вступает Hibernate. Всё начинается с того, что Браузер находит наиболее старую (по использованию) фоновую вкладку. Кстати, в среднем у пользователя открыто 7 вкладок, но у 5% их более 30.
Выгружать из памяти любую старую вкладку нельзя – можно сломать что-то действительно важное. Например, воспроизведение музыки или общение в веб-мессенджере. Таких исключений сейчас 28. Если вкладка не подошла хотя бы по одному из них, то Браузер переходит к проверке следующей.
Если найдена вкладка, которая удовлетворяет требованиям, то начинается процесс ее сохранения.
Любую страницу можно условно разделить на две большие части, связанные с движками V8 (JS) и Blink (HTML/DOM). Рассмотрим небольшой пример:
У нас есть некоторое DOM-дерево и небольшой скрипт, который просто добавляет div в body. С точки зрения Blink, эта страница выглядит примерно так:
Давайте посмотрим на связь между Blink и V8 на примере HTMLBodyElement:
Можно заметить, что Blink и V8 имеют разные представления одних и тех же сущностей и тесно связаны друг с другом. Так мы пришли к первоначальной идее – сохранять полное состояние V8, а для Blink хранить лишь HTML-атрибуты в виде текста. Но это было ошибкой, потому что мы потеряли те состояния DOM-объектов, которые хранились не в атрибутах. А еще потеряли состояния, которые хранились не в DOM. Решением этой проблемы было полное сохранение Blink. Но не всё так просто.
Для начала нужно собрать информацию об объектах Blink. Поэтому в момент сохранения V8 мы не только останавливаем JS и делаем его слепок, но и собираем в памяти ссылки на DOM-объекты и прочие вспомогательные объекты, доступные для JS. Мы также проходим по всем объектам, до которых можно дотянуться из объектов Document – корневых элементов каждого фрейма страницы. Так мы собираем информацию обо всем, что важно сохранить. Остается самое сложное – научиться сохранять.
Если посчитать все классы Blink, которые представляют DOM-дерево, а также разные HTML5 API (например, canvas, media, geolocation), то получим тысячи классов. Практически невозможно написать руками логику сохранения всех классов. Но хуже всего то, что даже если так сделать, то это будет невозможно поддерживать, потому что мы регулярно подливаем новые версии Chromium, которые вносят неожиданные изменения в любой класс.
Наш Браузер для всех платформ собирается с помощью clang. Чтобы решить задачу сохранения классов Blink, мы создали плагин для clang, который строит AST (абстрактное синтаксическое дерево) для классов. Например, вот этот код:
Превращается в такой XML:
Дальше другие написанные нами скрипты генерируют из этой информации код на C++ для сохранения и восстановления классов, который и попадает в сборку Яндекс.Браузера.
Всего у нас генерируется код примерно для 1000 классов Blink. Например, мы научились сохранять такой сложный класс как Canvas. В него можно рисовать из JS-кода, задавать множество свойств, устанавливать параметры кисточек для рисования и так далее. Мы сохраняем все эти свойства, параметры и саму картинку.
После успешного шифрования и сохранения всех данных на жесткий диск процесс вкладки выгружается из памяти до тех пор, пока пользователь не вернется на эту вкладку. В интерфейсе, как и раньше, она никак не выделяется.
Восстановление вкладки происходит не мгновенно, но существенно быстрее, чем при загрузке из сети. Тем не менее мы пошли на хитрый ход, чтобы не раздражать пользователей вспышками белого экрана. Мы показываем скриншот страницы, созданный на этапе сохранения. Это помогает сгладить переход. В остальном процесс восстановления похож на обычную навигацию с той лишь разницей, что Браузер не делает сетевой запрос. Он воссоздает структуру фреймов и DOM-деревья в них, а затем подменяет состояние V8.
Записали видео с наглядной демонстрацией того, как Hibernate выгружает и восстанавливает по клику вкладки с сохранением прогресса в JS-игре, введенного в формах текста и положения видео:
В ближайшее время технология Hibernate станет доступна всем пользователям Яндекс.Браузера для Windows. Мы также планируем начать экспериментировать с ней в альфа-версии для Android. С ее помощью Браузер экономит память более эффективно, чем раньше. К примеру, у пользователей с большим числом открытых вкладок Hibernate в среднем экономит более 330 мегабайт памяти и не теряет при этом информацию во вкладках, которая остается доступна в один клик при любом состоянии сети. Мы понимаем, что вебмастерам было бы полезно учитывать выгрузку фоновых вкладок, поэтому планируем поддержать Page Lifecycle API.
Hibernate – не единственное наше решение, направленное на экономию ресурсов. Мы не первый год работаем над тем, чтобы Браузер адаптировался под имеющиеся в системе ресурсы. К примеру, на слабых устройствах Браузер переходит в упрощенный режим, а при отключении ноутбука от источника питания – снижает энергопотребление. Экономия ресурсов – большая и сложная история, к которой мы еще обязательно вернемся на Хабре.
Браузеры, основанные на Chromium, создают по процессу на каждую вкладку. У этого подхода множество достоинств. Это и безопасность (изоляция сайтов друг от друга), и стабильность (падение одного процесса не тянет за собой весь браузер), и ускорение работы на современных процессорах с большим количеством ядер. Но есть и минус – более высокое потребление оперативной памяти, чем при использовании одного процесса на всё. Если бы браузеры ничего с этим не делали, то их пользователи постоянно видели бы что-то подобное:
В проекте Chromium борются с потреблением памяти фоновыми вкладками с помощью очистки различных кэшей. Речь не про тот кэш, в котором хранятся картинки загруженных страниц. С ним проблем нет – он живет на жестком диске. В современном браузере много другой закэшированной информации, которая хранится в оперативной памяти.
Также в Chromium уже достаточно давно работают над тем, чтобы останавливать JS-таймеры в фоновых вкладках. Иначе очистка кэшей теряет смысл, т.к. активности в фоновых вкладках их восстанавливают. Считается, что если сайты хотят работать в фоне, то нужно использовать service worker, а не таймеры.
Если эти меры не помогают, то остается только одно – выгрузить из памяти весь процесс рендеринга вкладки. Открытый сайт просто перестает существовать. Если переключиться на вкладку, то начнется загрузка из сети. Если во вкладке было видео на паузе, то оно начнет проигрываться с начала. Если на странице заполнялась форма, то введенная информация может быть утеряна. Если во вкладке работало тяжелое JS-приложение, то понадобится запустить его заново.
Проблема выгрузки вкладок особенно неприятна при отсутствии доступа к сети. Отложили вкладку с Хабром для чтения на борту самолета? Будьте готовы, что полезная статья превратится в тыкву.
Разработчики браузеров понимают, что эта крайняя мера вызывает раздражение у пользователей (достаточно обратиться к поиску, чтобы оценить масштабы), поэтому применяют ее в последний момент. В этот момент компьютер уже тормозит из-за нехватки памяти, пользователи это замечают и ищут альтернативные способы решения проблемы, поэтому, к примеру, у расширения The Great Suspender более 1,4 млн пользователей.
Люди хотят, чтобы браузеры и память экономили, и тормозить при этом не начинали. Для этого вкладки нужно выгружать не в последний момент, а чуть раньше. А для этого нужно перестать терять содержимое вкладок, т.е. сделать процесс экономии незаметным. Но на чем тогда экономить? Круг замкнулся. Но решение нашлось.
Hibernate в Яндекс.Браузере
Многие читатели Хабра уже могли догадаться, что очистить память, но сохранить состояние вкладки вполне возможно, если предварительно выгрузить состояние на жесткий диск. Если по клику восстановить вкладку с жесткого диска, то пользователь не заметит ничего.
Наша команда участвует в разработке проекта Chromium, куда отправляет значительные оптимизирующие правки и новые возможности. Ещё в 2015 году мы обсуждали с коллегами из проекта идею сохранения состояния вкладок на жесткий диск и даже успели внести ряд доработок, но это направление в Chromium решили заморозить. Мы решили иначе и продолжили разработку в Яндекс.Браузере. На это ушло больше времени, чем планировали, но это того стоило. Чуть ниже мы расскажем о технической начинке технологии Hibernate, а пока начнем с общей логики.
Несколько раз в минуту Яндекс.Браузер проверяет количество доступной памяти, и если ее меньше, чем пороговое значение в 600 мегабайт, то в дело вступает Hibernate. Всё начинается с того, что Браузер находит наиболее старую (по использованию) фоновую вкладку. Кстати, в среднем у пользователя открыто 7 вкладок, но у 5% их более 30.
Выгружать из памяти любую старую вкладку нельзя – можно сломать что-то действительно важное. Например, воспроизведение музыки или общение в веб-мессенджере. Таких исключений сейчас 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 в body. С точки зрения Blink, эта страница выглядит примерно так:
Давайте посмотрим на связь между Blink и V8 на примере HTMLBodyElement:
Можно заметить, что Blink и V8 имеют разные представления одних и тех же сущностей и тесно связаны друг с другом. Так мы пришли к первоначальной идее – сохранять полное состояние V8, а для Blink хранить лишь HTML-атрибуты в виде текста. Но это было ошибкой, потому что мы потеряли те состояния DOM-объектов, которые хранились не в атрибутах. А еще потеряли состояния, которые хранились не в DOM. Решением этой проблемы было полное сохранение Blink. Но не всё так просто.
Для начала нужно собрать информацию об объектах Blink. Поэтому в момент сохранения V8 мы не только останавливаем JS и делаем его слепок, но и собираем в памяти ссылки на DOM-объекты и прочие вспомогательные объекты, доступные для JS. Мы также проходим по всем объектам, до которых можно дотянуться из объектов Document – корневых элементов каждого фрейма страницы. Так мы собираем информацию обо всем, что важно сохранить. Остается самое сложное – научиться сохранять.
Если посчитать все классы Blink, которые представляют DOM-дерево, а также разные 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++ для сохранения и восстановления классов, который и попадает в сборку Яндекс.Браузера.
Код сохранения на 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-игре, введенного в формах текста и положения видео:
Итоги
В ближайшее время технология Hibernate станет доступна всем пользователям Яндекс.Браузера для Windows. Мы также планируем начать экспериментировать с ней в альфа-версии для Android. С ее помощью Браузер экономит память более эффективно, чем раньше. К примеру, у пользователей с большим числом открытых вкладок Hibernate в среднем экономит более 330 мегабайт памяти и не теряет при этом информацию во вкладках, которая остается доступна в один клик при любом состоянии сети. Мы понимаем, что вебмастерам было бы полезно учитывать выгрузку фоновых вкладок, поэтому планируем поддержать Page Lifecycle API.
Hibernate – не единственное наше решение, направленное на экономию ресурсов. Мы не первый год работаем над тем, чтобы Браузер адаптировался под имеющиеся в системе ресурсы. К примеру, на слабых устройствах Браузер переходит в упрощенный режим, а при отключении ноутбука от источника питания – снижает энергопотребление. Экономия ресурсов – большая и сложная история, к которой мы еще обязательно вернемся на Хабре.