Эффективное управление памятью в масштабе Gmail

Лорина Ли
Loreena Lee

Введение

Хотя JavaScript использует сбор мусора для автоматического управления памятью, он не заменяет эффективное управление памятью в приложениях. Приложения JavaScript страдают от тех же проблем, связанных с памятью, что и собственные приложения, таких как утечки памяти и ее раздувание, однако им также приходится иметь дело с паузами при сборке мусора. Крупномасштабные приложения, такие как Gmail, сталкиваются с теми же проблемами, что и ваши более мелкие приложения. Читайте дальше, чтобы узнать, как команда Gmail использовала Chrome DevTools для выявления, изоляции и устранения проблем с памятью.

Сессия Google I/O 2013

Мы представили этот материал на Google I/O 2013. Посмотрите видео ниже:

Gmail, у нас проблема…

Команда Gmail столкнулась с серьезной проблемой. Анекдоты о вкладках Gmail, потребляющих несколько гигабайт памяти на ноутбуках и настольных компьютерах с ограниченными ресурсами, раздавались все чаще, часто с выводом о выходе из строя всего браузера. Истории о том, что процессоры загружены на 100%, приложения не отвечают и грустные вкладки Chrome («Он мертв, Джим»). Команда не знала, как начать диагностировать проблему, не говоря уже о ее устранении. Они понятия не имели, насколько широко распространена проблема, а доступные инструменты не подходили для крупных приложений. Команда объединила усилия с командами Chrome и вместе разработала новые методы устранения проблем с памятью, улучшила существующие инструменты и позволила собирать данные о памяти на местах. Но прежде чем перейти к инструментам, давайте рассмотрим основы управления памятью в JavaScript.

Основы управления памятью

Прежде чем вы сможете эффективно управлять памятью в JavaScript, вы должны понять основы. В этом разделе будут рассмотрены примитивные типы, граф объектов, а также приведены определения раздувания памяти в целом и утечки памяти в JavaScript. Память в JavaScript можно представить в виде графа, поэтому теория графов играет важную роль в управлении памятью JavaScript и профилировщике кучи .

Примитивные типы

В JavaScript есть три примитивных типа:

  1. Номер (например, 4, 3.14159)
  2. Логическое значение (истина или ложь)
  3. Строка («Привет, мир»)

Эти примитивные типы не могут ссылаться на какие-либо другие значения. В объектном графе эти значения всегда являются конечными или конечными узлами, то есть у них никогда не бывает исходящего ребра.

Существует только один тип контейнера: Object. В JavaScript объект представляет собой ассоциативный массив . Непустой объект — это внутренний узел с исходящими ребрами к другим значениям (узлам).

А как насчет массивов?

Массив в JavaScript на самом деле является объектом с числовыми ключами. Это упрощение, поскольку среда выполнения JavaScript оптимизирует объекты, подобные массивам, и представляет их в виде массивов.

Терминология

  1. Значение — экземпляр примитивного типа, объекта, массива и т. д.
  2. Переменная — имя, которое ссылается на значение.
  3. Свойство — имя в объекте, которое ссылается на значение.

Граф объектов

Все значения в JavaScript являются частью графа объекта. Граф начинается с корней, например, объекта window . Управление временем существования корней GC не находится под вашим контролем, поскольку они создаются браузером и уничтожаются при выгрузке страницы. Глобальные переменные на самом деле являются свойствами окна.

Граф объектов

Когда ценность становится мусором?

Значение становится мусором, когда нет пути от корня к значению. Другими словами, начиная с корней и исчерпывающе просматривая все свойства и переменные объекта, которые живы в кадре стека , значение невозможно достичь, оно превратилось в мусор.

Граф мусора

Что такое утечка памяти в JavaScript?

Утечка памяти в JavaScript чаще всего возникает, когда существуют узлы DOM, которые недоступны из дерева DOM страницы, но на которые все еще ссылается объект JavaScript. Хотя в современных браузерах становится все труднее непреднамеренно создавать утечки, это все же проще, чем можно подумать. Допустим, вы добавляете элемент в дерево DOM следующим образом:

email.message = document.createElement("div");
displayList.appendChild(email.message);

А позже вы удаляете элемент из списка отображения:

displayList.removeAllChildren();

Пока существует email , элемент DOM, на который ссылается сообщение, не будет удален, даже если теперь он отделен от дерева DOM страницы.

Что такое раздувание?

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

Что такое сбор мусора?

Сбор мусора — это способ освобождения памяти в JavaScript. Браузер решает, когда это произойдет. Во время сбора выполнение всех скриптов на вашей странице приостанавливается, пока текущие значения обнаруживаются путем обхода графа объекта, начиная с корней GC. Все значения, которые недостижимы , классифицируются как мусор. Память для мусорных значений освобождается диспетчером памяти.

Подробности о сборщике мусора V8

Чтобы лучше понять, как происходит сбор мусора, давайте подробно рассмотрим сборщик мусора V8. V8 использует коллектор поколений. Память делится на два поколения: молодое и старое. Распределение и сбор средств среди молодого поколения происходят быстро и часто. Распределение и сбор средств в рамках старшего поколения происходят медленнее и реже.

Коллекционер поколений

В V8 используется коллектор двух поколений. Возраст значения определяется как количество байтов, выделенных с момента его выделения. На практике возраст ценности часто оценивают по количеству сохранившихся коллекций молодого поколения. После того, как значение становится достаточно старым, оно сохраняется в старом поколении.

На практике недавно выделенные значения живут недолго. Исследование программ Smalltalk показало, что после сбора молодым поколением сохраняется только 7% значений. Подобные исследования в разных средах выполнения показали, что в среднем от 90% до 70% недавно выделенных значений никогда не сохраняются в старом поколении.

Молодое поколение

Куча молодого поколения в V8 разделена на два пространства с именами from и to. Память выделяется из пространства to. Распределение происходит очень быстро, пока пространство не заполнится, после чего запускается коллекция молодого поколения. Коллекция молодого поколения сначала меняет местами местами из и в пространство, старое в пространство (теперь из космоса) сканируется, и все живые значения копируются в пространство или сохраняются в старом поколении. Типичная коллекция молодого поколения займет порядка 10 миллисекунд (мс).

Интуитивно вы должны понимать, что каждое выделение памяти вашим приложением приближает вас к исчерпанию пространства и возникновению паузы в сборе мусора. Разработчики игр, обратите внимание: чтобы обеспечить время кадра 16 мс (необходимое для достижения 60 кадров в секунду), ваше приложение должно делать нулевое выделение, потому что одна коллекция молодого поколения съест большую часть времени кадра.

Куча молодого поколения

Старое поколение

Куча старого поколения в V8 использует для сбора алгоритм mark-compact . Распределение средств старых поколений происходит всякий раз, когда ценность передается от молодого поколения к старому поколению. Всякий раз, когда возникает коллекция старого поколения, также создается коллекция молодого поколения. Ваше приложение будет приостановлено примерно на несколько секунд. На практике это приемлемо, поскольку коллекции старого поколения встречаются нечасто.

Резюме V8 GC

Автоматическое управление памятью со сборкой мусора полезно для продуктивности разработчиков, но каждый раз, когда вы выделяете значение, вы приближаетесь к паузе в сборе мусора. Паузы в сборе мусора могут испортить впечатление от вашего приложения из-за зависаний. Теперь, когда вы понимаете, как JavaScript управляет памятью, вы можете сделать правильный выбор для своего приложения.

Исправление Gmail

За последний год в Chrome DevTools появилось множество функций и исправлений ошибок, что сделало их еще более мощными, чем когда-либо. Кроме того, сам браузер внес ключевые изменения в API Performance.memory, позволяющие Gmail и любому другому приложению собирать статистику памяти на местах. Вооружившись этими потрясающими инструментами, то, что когда-то казалось невыполнимой задачей, вскоре превратилось в захватывающую игру по поиску преступников.

Инструменты и методы

Полевые данные и API Performance.Memory

Начиная с Chrome 22, API Performance.Memory включен по умолчанию. Для долго работающих приложений, таких как Gmail, данные реальных пользователей имеют неоценимое значение. Эта информация позволяет нам отличить опытных пользователей — тех, кто проводит 8–16 часов в день в Gmail, получая сотни сообщений в день, — от более обычных пользователей, которые проводят в Gmail несколько минут в день и получают около дюжины сообщений. сообщений в неделю.

Этот API возвращает три части данных:

  1. jsHeapSizeLimit — объем памяти (в байтах), которым ограничена куча JavaScript.
  2. totalJSHeapSize — объем памяти (в байтах), выделенный кучей JavaScript, включая свободное пространство.
  3. UsedJSHeapSize — объем памяти (в байтах), используемый в данный момент.

Следует иметь в виду, что API возвращает значения памяти для всего процесса Chrome. Хотя это не режим по умолчанию, при определенных обстоятельствах Chrome может открывать несколько вкладок в одном и том же процессе рендеринга. Это означает, что значения, возвращаемые Performance.memory, могут содержать объем памяти, занимаемый другими вкладками браузера, помимо той, которая содержит ваше приложение.

Измерение памяти в масштабе

Gmail оснастил свой JavaScript так, чтобы использовать API Performance.memory для сбора информации о памяти примерно раз в 30 минут. Поскольку многие пользователи Gmail оставляют приложение на несколько дней, команда смогла отслеживать рост объема памяти с течением времени, а также общую статистику использования памяти. За несколько дней после того, как Gmail начал собирать информацию о памяти у случайной выборки пользователей, у команды было достаточно данных, чтобы понять, насколько широко распространены проблемы с памятью среди обычных пользователей. Они установили базовый уровень и использовали поток входящих данных для отслеживания прогресса на пути к снижению потребления памяти. В конечном итоге эти данные также будут использоваться для выявления любых регрессий памяти.

Помимо целей отслеживания, полевые измерения также дают четкое представление о корреляции между объемом памяти и производительностью приложений. Вопреки распространенному мнению, что «больше памяти приводит к повышению производительности», команда Gmail обнаружила, что чем больше объем памяти, тем дольше задержки для обычных действий Gmail. Вооруженные этим открытием, они были более мотивированы, чем когда-либо, чтобы обуздать потребление памяти.

Измерение памяти в масштабе

Выявление проблемы с памятью с помощью временной шкалы DevTools

Первым шагом в решении любой проблемы с производительностью является доказательство существования проблемы, создание воспроизводимого теста и проведение базового измерения проблемы. Без воспроизводимой программы вы не сможете надежно измерить проблему. Без базового измерения вы не узнаете, насколько вы улучшили производительность.

Панель DevTools Timeline — идеальный кандидат для доказательства существования проблемы. Он дает полный обзор того, сколько времени тратится при загрузке и взаимодействии с вашим веб-приложением или страницей. Все события, от загрузки ресурсов до анализа JavaScript, расчета стилей, приостановок сборки мусора и перерисовки, отображаются на временной шкале. В целях исследования проблем с памятью на панели «Таймлайн» также имеется режим «Память», который отслеживает общий объем выделенной памяти, количество узлов DOM, количество оконных объектов и количество выделенных прослушивателей событий.

Доказать, что проблема существует

Начните с определения последовательности действий, которые, как вы подозреваете, вызывают утечку памяти. Начните запись таймлайна и выполните последовательность действий. Используйте кнопку мусорной корзины внизу, чтобы принудительно выполнить полную сборку мусора. Если после нескольких итераций вы видите пилообразный график, вы распределяете множество недолговечных объектов. Но если не ожидается, что последовательность действий приведет к сохранению какой-либо памяти, а количество узлов DOM не упадет до исходного уровня, с которого вы начали, у вас есть веские основания подозревать наличие утечки.

Пилообразный график

Убедившись, что проблема существует, вы можете получить помощь в определении источника проблемы с помощью профилировщика кучи DevTools.

Поиск утечек памяти с помощью профилировщика кучи DevTools

Панель «Профилировщик» предоставляет как профилировщик ЦП, так и профилировщик кучи. Профилирование кучи работает путем создания моментального снимка графа объекта. Прежде чем сделать снимок, как молодое, так и старое поколение собирают мусор. Другими словами, вы увидите только те значения, которые были активны на момент создания снимка.

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

Использование профилировщика распределения кучи

Профилировщик распределения кучи объединяет подробную информацию о снимках профилировщика кучи с инкрементальным обновлением и отслеживанием панели «Таймлайн». Откройте панель «Профили», запустите профиль «Выделение кучи записей» , выполните последовательность действий, затем остановите запись для анализа. Профилировщик распределения периодически делает снимки кучи на протяжении всей записи (каждые 50 мс!) и один последний снимок в конце записи.

Профилировщик распределения кучи

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

В приведенном выше примере действие было выполнено 10 раз. Пример программы кэширует пять объектов, поэтому ожидаются последние пять синих полос. Но крайняя левая синяя полоса указывает на потенциальную проблему. Затем вы можете использовать ползунки на временной шкале выше, чтобы увеличить масштаб этого конкретного снимка и увидеть объекты, которые недавно были выделены в этот момент. Если щелкнуть конкретный объект в куче, в нижней части снимка кучи отобразится его сохраняющее дерево. Проверка пути сохранения объекта должна дать вам достаточно информации, чтобы понять, почему объект не был собран, и вы сможете внести необходимые изменения в код, чтобы удалить ненужную ссылку.

Разрешение кризиса памяти Gmail

Используя инструменты и методы, описанные выше, команде Gmail удалось выявить несколько категорий ошибок: неограниченные кэши, бесконечно растущие массивы обратных вызовов, ожидающих чего-то, что на самом деле никогда не происходит, и прослушиватели событий, непреднамеренно сохраняющие свои цели. Устранив эти проблемы, общее использование памяти Gmail значительно сократилось. Пользователи из 99% процентов использовали на 80% меньше памяти, чем раньше, а потребление памяти среди медианных пользователей снизилось почти на 50%.

Использование памяти Gmail

Поскольку Gmail использовал меньше памяти, задержка паузы GC была уменьшена, что повысило общее удобство работы пользователя.

Также следует отметить, что команда Gmail, собирающая статистику использования памяти, смогла обнаружить регрессию сборки мусора внутри Chrome. В частности, были обнаружены две ошибки фрагментации, когда данные памяти Gmail начали показывать резкое увеличение разрыва между общим выделенным объемом памяти и оперативной памятью.

Призыв к действию

Задайте себе эти вопросы:

  1. Сколько памяти использует мое приложение? Возможно, вы используете слишком много памяти, что, вопреки распространенному мнению, негативно влияет на общую производительность приложения. Трудно точно определить правильное число, но обязательно убедитесь, что любое дополнительное кэширование, которое использует ваша страница, оказывает измеримое влияние на производительность.
  2. Моя страница защищена от утечек? Если на вашей странице есть утечки памяти, это может повлиять не только на производительность вашей страницы, но и на производительность других вкладок. Используйте средство отслеживания объектов, чтобы выявить любые утечки.
  3. Как часто моя страница проверяется? Вы можете увидеть любую паузу сборщика мусора с помощью панели «Таймлайн» в инструментах разработчика Chrome . Если ваша страница часто выполняет сборку мусора, скорее всего, вы выделяете слишком часто, перегружая память молодого поколения.

Заключение

Мы начали в кризис. Описаны основные основы управления памятью в JavaScript и V8 в частности. Вы узнали, как использовать эти инструменты, в том числе новую функцию отслеживания объектов, доступную в последних сборках Chrome. Команда Gmail, вооружившись этими знаниями, решила проблему использования памяти и добилась повышения производительности. Вы можете сделать то же самое со своими веб-приложениями!