Как отслеживать общее использование памяти веб-страницей с помощью measureUserAgentSpecificMemory()

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

Brendan Kenny
Brendan Kenny
Ulan Degenbaev
Ulan Degenbaev

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

Однако выявление не всегда работает идеально, и проблема остановки Алана Тьюринга доказала, что идеальное выявление невозможно. Поэтому браузеры заменяют понятие "объект нужен" понятием "объект доступен". Если веб-страница не может получить доступ к объекту через его переменные и поля других доступных объектов, браузер может безопасно восстановить объект. Разница между этими двумя понятиями приводит к утечкам памяти, как показано в следующем примере.

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

В этом случае большой массив b больше не нужен, но браузер не удаляет его, поскольку он по-прежнему доступен через object.b в обратном вызове. Таким образом, память большего массива утекает.

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

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

Чем performance.measureUserAgentSpecificMemory() отличается от устаревшего performance.memory API?

Если вы знакомы с существующим нестандартным API performance.memory, вам может быть интересно, чем новый API отличается от него. Основное отличие заключается в том, что старый API возвращает размер кучи JavaScript, а новый API оценивает объем памяти, используемый веб-страницей. Это различие становится важным, когда Chrome использует одну и ту же кучу с несколькими веб-страницами (или несколькими экземплярами одной и той же веб-страницы). В таких случаях результат старого API может быть произвольным. Поскольку старый API определяется в терминах, зависящих от реализации, таких как "куча", стандартизировать его безнадежно.

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

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

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

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

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

Browser Support

  • Chrome: 89.
  • Edge: 89.
  • Firefox: not supported.
  • Safari: not supported.

Source

В настоящее время API поддерживается только в браузерах на основе Chromium, начиная с Chrome 89. Результат API сильно зависит от реализации, поскольку браузеры по-разному представляют объекты в памяти и по-разному оценивают использование памяти. Браузеры могут исключать некоторые области памяти из учета, если он слишком сложен или невозможен. Поэтому результаты в разных браузерах нельзя сравнивать. Сравнивать результаты имеет смысл только для одного и того же браузера.

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

Обнаружение функций

Функция performance.measureUserAgentSpecificMemory будет недоступна или может завершиться с ошибкой SecurityError, если среда выполнения не соответствует требованиям безопасности для предотвращения утечки информации из разных источников. Она основана на изоляции от междоменных источников, которую можно активировать на веб-странице, настроив заголовки COOP и COEP.

Поддержку можно определить во время выполнения:

if (!window.crossOriginIsolated) {
  console.log('performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages');
} else if (!performance.measureUserAgentSpecificMemory) {
  console.log('performance.measureUserAgentSpecificMemory() is not available in this browser');
} else {
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory();
  } 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 для определения глобального монитора памяти, который собирает данные об использовании памяти всей веб-страницей и отправляет результаты на сервер для агрегирования и анализа. Проще всего делать это периодически, например каждые M минут. Однако это приводит к искажению данных, поскольку пики использования памяти могут возникать между выборками.

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

Сначала определите функцию, которая планирует следующее измерение памяти с помощью setTimeout() со случайным интервалом.

function scheduleMeasurement() {
  // Check measurement API is available.
  if (!window.crossOriginIsolated) {
    console.log('performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages');
    console.log('See https://web.dev/coop-coep/ to learn more')
    return;
  }
  if (!performance.measureUserAgentSpecificMemory) {
    console.log('performance.measureUserAgentSpecificMemory() is not available in this browser');
    return;
  }
  const interval = measurementInterval();
  console.log(`Running next memory measurement in ${Math.round(interval / 1000)} seconds`);
  setTimeout(performMeasurement, interval);
}

Функция 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() {
  // 1. Invoke performance.measureUserAgentSpecificMemory().
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === 'SecurityError') {
      console.log('The context is not secure.');
      return;
    }
    // Rethrow other errors.
    throw error;
  }
  // 2. Record the result.
  console.log('Memory usage:', result);
  // 3. Schedule the next measurement.
  scheduleMeasurement();
}

Наконец, начните измерение.

// Start measurements.
scheduleMeasurement();

Результат может выглядеть следующим образом:

// Console output:
{
  bytes: 60_100_000,
  breakdown: [
    {
      bytes: 40_000_000,
      attribution: [{
        url: 'https://example.com/',
        scope: 'Window',
      }],
      types: ['JavaScript']
    },

    {
      bytes: 20_000_000,
      attribution: [{
          url: 'https://example.com/iframe',
          container: {
            id: 'iframe-id-attribute',
            src: '/iframe',
          },
          scope: 'Window',
      }],
      types: ['JavaScript']
    },

    {
      bytes: 100_000,
      attribution: [],
      types: ['DOM']
    },
  ],
}

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

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

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

Как оставить отзыв

Группа сообщества по производительности веб-страниц и команда Chrome будут рады узнать ваше мнение о performance.measureUserAgentSpecificMemory().

Расскажите нам о дизайне API

Что-то в API работает не так, как ожидалось? Или, может быть, вам не хватает каких-то свойств, чтобы реализовать свою идею? Сообщите о проблеме со спецификацией в репозитории performance.measureUserAgentSpecificMemory() на GitHub или добавьте свои мысли к существующей проблеме.

Сообщить о проблеме с реализацией

Вы нашли ошибку в реализации Chrome? Или реализация отличается от спецификации? Сообщите об ошибке на сайте new.crbug.com. Опишите ее как можно подробнее, укажите простые инструкции по ее воспроизведению и задайте для параметра Components (Компоненты) значение Blink>PerformanceAPIs.

Поддержка

Планируете ли вы использовать performance.measureUserAgentSpecificMemory()? Ваша поддержка помогает команде Chrome определять приоритеты функций и показывает другим поставщикам браузеров, насколько важно их поддерживать. Отправьте твит на @ChromiumDev и расскажите, где и как вы его используете.

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

Благодарности

Мы благодарим Доменика Дениколу (Domenic Denicola), Йоава Вайса (Yoav Weiss) и Матиаса Байненса (Mathias Bynens) за проверку дизайна API, а также Доминика Инфюра (Dominik Inführ), Ханнеса Пайера (Hannes Payer), Кентаро Хару (Kentaro Hara) и Михаэля Липпауца (Michael Lippautz) за код-ревью в Chrome. Я также благодарю Пера Паркера (Per Parker), Филиппа Вайса (Philipp Weis), Ольгу Беломестных, Мэтью Болохана (Matthew Bolohan) и Нила Маккея (Neil Mckay) за ценные отзывы, которые помогли значительно улучшить API.