Хабрахабр

Как сэкономить память на вкладках браузера, но не потерять их содержимое. Опыт команды Яндекс.Браузера

Когда браузерам не хватает памяти, они выгружают из нее наиболее старые вкладки. Это раздражает, потому что клик по такой вкладке приводит к принудительной перезагрузке страницы. Сегодня мы расскажем читателям Хабра о том, как команда Яндекс.Браузера решает эту проблему с помощью технологии Hibernate.

У этого подхода множество достоинств. Браузеры, основанные на Chromium, создают по процессу на каждую вкладку. Но есть и минус – более высокое потребление оперативной памяти, чем при использовании одного процесса на всё. Это и безопасность (изоляция сайтов друг от друга), и стабильность (падение одного процесса не тянет за собой весь браузер), и ускорение работы на современных процессорах с большим количеством ядер. Если бы браузеры ничего с этим не делали, то их пользователи постоянно видели бы что-то подобное:

Речь не про тот кэш, в котором хранятся картинки загруженных страниц. В проекте Chromium борются с потреблением памяти фоновыми вкладками с помощью очистки различных кэшей. В современном браузере много другой закэшированной информации, которая хранится в оперативной памяти. С ним проблем нет – он живет на жестком диске.

Иначе очистка кэшей теряет смысл, т.к. Также в Chromium уже достаточно давно работают над тем, чтобы останавливать JS-таймеры в фоновых вкладках. Считается, что если сайты хотят работать в фоне, то нужно использовать service worker, а не таймеры. активности в фоновых вкладках их восстанавливают.

Открытый сайт просто перестает существовать. Если эти меры не помогают, то остается только одно – выгрузить из памяти весь процесс рендеринга вкладки. Если во вкладке было видео на паузе, то оно начнет проигрываться с начала. Если переключиться на вкладку, то начнется загрузка из сети. Если во вкладке работало тяжелое JS-приложение, то понадобится запустить его заново. Если на странице заполнялась форма, то введенная информация может быть утеряна.

Отложили вкладку с Хабром для чтения на борту самолета? Проблема выгрузки вкладок особенно неприятна при отсутствии доступа к сети. Будьте готовы, что полезная статья превратится в тыкву.

В этот момент компьютер уже тормозит из-за нехватки памяти, пользователи это замечают и ищут альтернативные способы решения проблемы, поэтому, к примеру, у расширения The Great Suspender более 1,4 млн пользователей. Разработчики браузеров понимают, что эта крайняя мера вызывает раздражение у пользователей (достаточно обратиться к поиску, чтобы оценить масштабы), поэтому применяют ее в последний момент.

Для этого вкладки нужно выгружать не в последний момент, а чуть раньше. Люди хотят, чтобы браузеры и память экономили, и тормозить при этом не начинали. сделать процесс экономии незаметным. А для этого нужно перестать терять содержимое вкладок, т.е. Круг замкнулся. Но на чем тогда экономить? Но решение нашлось.

Hibernate в Яндекс.Браузере

Многие читатели Хабра уже могли догадаться, что очистить память, но сохранить состояние вкладки вполне возможно, если предварительно выгрузить состояние на жесткий диск. Если по клику восстановить вкладку с жесткого диска, то пользователь не заметит ничего.

Ещё в 2015 году мы обсуждали с коллегами из проекта идею сохранения состояния вкладок на жесткий диск и даже успели внести ряд доработок, но это направление в Chromium решили заморозить. Наша команда участвует в разработке проекта Chromium, куда отправляет значительные оптимизирующие правки и новые возможности. На это ушло больше времени, чем планировали, но это того стоило. Мы решили иначе и продолжили разработку в Яндекс.Браузере. Чуть ниже мы расскажем о технической начинке технологии Hibernate, а пока начнем с общей логики.

Всё начинается с того, что Браузер находит наиболее старую (по использованию) фоновую вкладку. Несколько раз в минуту Яндекс.Браузер проверяет количество доступной памяти, и если ее меньше, чем пороговое значение в 600 мегабайт, то в дело вступает Hibernate. Кстати, в среднем у пользователя открыто 7 вкладок, но у 5% их более 30.

Например, воспроизведение музыки или общение в веб-мессенджере. Выгружать из памяти любую старую вкладку нельзя – можно сломать что-то действительно важное. Если вкладка не подошла хотя бы по одному из них, то Браузер переходит к проверке следующей. Таких исключений сейчас 28.

Если найдена вкладка, которая удовлетворяет требованиям, то начинается процесс ее сохранения.

Сохранение и восстановление вкладок в Hibernate

Любую страницу можно условно разделить на две большие части, связанные с движками V8 (JS) и Blink (HTML/DOM). Рассмотрим небольшой пример:

<html> <head> <script type="text/javascript"> function onLoad() </script> </head> <body onload="onLoad()"></body>
</html>

С точки зрения Blink, эта страница выглядит примерно так: У нас есть некоторое DOM-дерево и небольшой скрипт, который просто добавляет div в body.

Давайте посмотрим на связь между Blink и V8 на примере HTMLBodyElement:

Так мы пришли к первоначальной идее – сохранять полное состояние V8, а для Blink хранить лишь HTML-атрибуты в виде текста. Можно заметить, что Blink и V8 имеют разные представления одних и тех же сущностей и тесно связаны друг с другом. А еще потеряли состояния, которые хранились не в DOM. Но это было ошибкой, потому что мы потеряли те состояния DOM-объектов, которые хранились не в атрибутах. Но не всё так просто. Решением этой проблемы было полное сохранение Blink.

Поэтому в момент сохранения V8 мы не только останавливаем JS и делаем его слепок, но и собираем в памяти ссылки на DOM-объекты и прочие вспомогательные объекты, доступные для JS. Для начала нужно собрать информацию об объектах Blink. Так мы собираем информацию обо всем, что важно сохранить. Мы также проходим по всем объектам, до которых можно дотянуться из объектов Document – корневых элементов каждого фрейма страницы. Остается самое сложное – научиться сохранять.

Практически невозможно написать руками логику сохранения всех классов. Если посчитать все классы Blink, которые представляют DOM-дерево, а также разные HTML5 API (например, canvas, media, geolocation), то получим тысячи классов. Но хуже всего то, что даже если так сделать, то это будет невозможно поддерживать, потому что мы регулярно подливаем новые версии Chromium, которые вносят неожиданные изменения в любой класс.

Чтобы решить задачу сохранения классов Blink, мы создали плагин для clang, который строит AST (абстрактное синтаксическое дерево) для классов. Наш Браузер для всех платформ собирается с помощью clang. Например, вот этот код:

Код класса

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 – не единственное наше решение, направленное на экономию ресурсов. Экономия ресурсов – большая и сложная история, к которой мы еще обязательно вернемся на Хабре. К примеру, на слабых устройствах Браузер переходит в упрощенный режим, а при отключении ноутбука от источника питания – снижает энергопотребление.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть