eFusion 24 июня 2020

🕐 Как в Google Chrome измерить использование памяти веб-страницей

Держим руку на пульсе производительного веба. В этой статье учимся измерять утечки памяти страницы Google Chrome с помощью нового интерфейса performance.measureMemory().

Когда веб-страница создаёт объект, браузер выделяет память для его хранения. Но объем памяти ограничен, и время от времени браузер выполняет сборку мусора. Однако если веб-страница не может достичь объекта через переменные и поля других доступных объектов, она может попытаться восстановить объект. Эти манипуляции приводят к утечкам памяти:

        const object = { a: new Array(1000), b: new Array(2000) };
setInterval(() => console.log(object.a), 1000);
    

Массив b больше не нужен, но браузер его не восстанавливает, так как массив всё ещё доступен через object.b в коллбэке. То есть в большем массиве и происходит утечка памяти.

К сожалению, утечки памяти широко распространены в веб-разработке: здесь забыли отменить регистрацию event listener, там случайно начали использовать объекты из iframe, а тут накапливаем объекты в массивах... Утечки приводят к тому, что использование памяти страницей растёт, и она работает медленно.

Первый шаг в решении проблемы – её измерение. Новый API performance.measureMemory() позволяет разработчикам измерять использование памяти страницей в процессе её работы и обнаружить утечки, которые могли проскользнуть во время тестирования.

Чем performance.measureMemory() отличается от performance.memory()?

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

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

Предлагаемые варианты использования

Использование памяти зависит от времени событий, действий пользователя и сборок мусора. Поэтому API measureMemory() предназначен для агрегирования данных об использовании памяти непосредственно в продакшене. Примеры использования:

  • обнаружение регрессии во время развертывания новой версии веб-страницы и фиксирования новых утечек памяти;
  • прогон A/B-тестирования новой функции для оценки ее влияния на память и обнаружение утечек;
  • корреляция использования памяти с продолжительностью сеанса;
  • корреляция использования памяти с пользовательскими метриками для понимания общего влияния на память.

Совместимость браузеров

На момент написания материала API поддерживался только в Chrome 83 в качестве пробной версии. Результат работы API сильно зависит от реализации, поскольку браузеры имеют разные способы представления объектов в памяти и разные способы оценки ее использования. Браузеры могут исключить некоторые области памяти из учета, если надлежащий учет слишком затратен или неосуществим. Таким образом, результаты в разных браузерах сравнить не получится.

Использование performance.measureMemory()

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

Запросите токен и добавьте его на свои страницы. Есть два способа:

  1. Добавьте тег origin-trial <meta>, в head каждой страницы. Это может выглядеть примерно так: <meta http-equiv="origin-trial" content="TOKEN_GOES_HERE">
  2. Если вы можете попасть на свой сервер, добавьте токен и результирующий заголовок ответа будет выглядеть примерно так: Origin-Trial: TOKEN_GOES_HERE

Для того чтобы экспериментировать с performance.measureMemory() без «танцев» с пробной версией, активируйте флаг #experimental-web-platform-features в chrome://flags.

Некоторые особенности

Функция performance.measureMemory() может завершиться ошибкой SecurityError, если среда выполнения не удовлетворяет требованиям безопасности для предотвращения cross-origin утечки информации. Чтобы этого избежать, инструмент требует наличие включенной изоляции сайта. Веб-страница может использовать cross-origin изоляцию, установив заголовки COOP+COEP:

        if (performance.measureMemory) {
  let result;
  try {
    result = await performance.measureMemory();
  } catch (error) {
    if (error instanceof DOMException &&
        error.name === "SecurityError") {
      console.log("The context is not secure.");
    } else {
      throw error;
    }
  }
  console.log(result);
}
    

Локальное тестирование

Chrome выполняет измерение памяти во время сборки мусора. Это означает, что API не выдает результаты незамедлительно – вместо этого он ожидает следующей сборки мусора. API провоцирует сборку мусора после некоторого таймаута (сейчас это 20 секунд). Запустите Chrome с флагом --enable-blink-features='ForceEagerMeasureMemory' . Это уменьшит тайм-аут до нуля и будет полезно для локальной отладки и тестирования.

Пример

Рекомендуемое использование API заключается в определении глобального монитора памяти, который проверяет использование памяти всей веб-страницы и отправляет результаты на сервер для агрегирования и анализа.

Самый простой способ – периодически делать выборку, например, каждые n минут. Однако это привёдет к смещению данных, поскольку скачки памяти могут возникать между выборками. В следующем примере показано, как выполнять измерения с помощью процесса Пуассона, который гарантирует, что выборки с одинаковой вероятностью будут происходить в любой момент времени (демо, источник).

Определите функцию, которая планирует следующее измерение с помощью setTimeout() с рандомным интервалом. Функция должна быть вызвана после загрузки страницы.

        function scheduleMeasurement() {
  if (!performance.measureMemory) {
    console.log("performance.measureMemory() is not available.");
    return;
  }
  const interval = measurementInterval();
  console.log("Scheduling memory measurement in " +
      Math.round(interval / 1000) + " seconds.");
  setTimeout(performMeasurement, interval);
}

// Начать измерения после того как страница загружена в главном окне
window.onload = function () {
  scheduleMeasurement();
}
    

Функция measurementInterval() вычисляет рандомный интервал в миллисекундах таким образом, что в среднем происходит одно измерение каждые пять минут. Изучите экспоненциальное распределение, если вас интересует матчасть, лежащая в основе функции.

        function measurementInterval() {
  const MEAN_INTERVAL_IN_MS = 5 * 60 * 1000;
  return -Math.log(Math.random()) * MEAN_INTERVAL_IN_MS;
}
    

Наконец, асинхронная функция performMeasurement() вызывает API, записывает результат и планирует следующее измерение.

        async function performMeasurement() {
  let result;
  try {
    result = await performance.measureMemory();
  } catch (error) {
    if (error instanceof DOMException &&
        error.name === "SecurityError") {
      console.log("The context is not secure.");
      return;
    }
    // Переносим остальные ошибки
    throw error;
  }
  // Записываем результат
  console.log("Memory usage:", result);
  // Планируем следующее измерение
  scheduleMeasurement();
}
    

Результат должен выглядеть примерно так:

        {
  bytes: 60_000_000,
  breakdown: [
    {
      bytes: 40_000_000,
      attribution: ["https://foo.com"],
      userAgentSpecificTypes: ["Window", "JS"]
    },
    {
      bytes: 20_000_000,
      attribution: ["https://foo.com/iframe"],
      userAgentSpecificTypes: ["Window", "JS"]
    }
  ]
}
    

Общая оценка использования памяти возвращается в байтах. Значение байтов определяется с помощью синтаксиса числового разделителя. Это значение зависит от реализации и не может быть сопоставлено между браузерами. Во время использования нашей origin trial версии значение несет в себе использование памяти главного окна и всех iframes того же сайта, плюс связанных с ними окон.

Список breakdown содержит дополнительную информацию об используемой памяти. Каждая запись описывает некоторую ее часть и приписывает ее набору окон, ifram-ов и воркеров, идентифицируемых URL-адресами. В поле userAgentSpecificTypes перечислены конкретные для реализации типы, связанные с памятью.

Важно рассматривать все списки в общем виде, а не основываться на конкретном браузере. Например, некоторые браузеры могут возвращать пустой breakdown или пустые атрибуты. Другие браузеры могут возвращать несколько URL-адресов в атрибуте, указывая, что они не могут различить, какой из этих URL-адресов владеет памятью.

Обратная связь

  • Web Performance Community Group и команда Chrome хотели бы услышать ваши мысли на счет опыта работы с performance.measureMemory().
  • Если в API есть недочеты или существуют недостающие свойства, которые вам необходимы – подайте запрос спецификации на performance.measureMemory или добавьте свои мысли к существующей проблеме.
  • Вы нашли ошибку в реализации Chrome? Реализация отличается от спецификации? Подайте сообщение об ошибке по адресу new.crbug.com и обязательно включите как можно больше деталей, предоставьте инструкции по воспроизведению ошибки и установите компоненты в положение Blink > PerformanceAPIs. Glitch отлично подходит для этих целей.
  • Планируете ли вы использовать performance.measureMemory()? Ваша публичная поддержка поможет команде Chrome определять приоритеты и покажет другим разработчикам важность происходящего. Отправьте твит на @ChromiumDev и сообщите, где и как вы его используете.

Полезные ссылки

Источники

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG