Узнайте, как измерить использование памяти веб-страницей в рабочей среде, чтобы обнаружить регрессии.
Браузеры автоматически управляют памятью веб-страниц. Каждый раз, когда веб-страница создает объект, браузер выделяет фрагмент памяти для его хранения. Поскольку память – это ограниченный ресурс, браузер выполняет сборку мусора, чтобы определить, когда объект больше не нужен, и освободить соответствующий фрагмент памяти.
Однако выявление не всегда работает идеально, и проблема остановки Алана Тьюринга доказала, что идеальное выявление невозможно. Поэтому браузеры заменяют понятие "объект нужен" понятием "объект доступен". Если веб-страница не может получить доступ к объекту через его переменные и поля других доступных объектов, браузер может безопасно восстановить объект. Разница между этими двумя понятиями приводит к утечкам памяти, как показано в следующем примере.
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-тестирование новой функции для оценки ее влияния на память и обнаружения утечек памяти.
- Сопоставление использования памяти с длительностью сеанса, чтобы проверить наличие или отсутствие утечек памяти.
- Сопоставление использования памяти с пользовательскими показателями, чтобы понять общее влияние использования памяти.
Совместимость браузеров
В настоящее время 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 и расскажите, где и как вы его используете.
Полезные ссылки
- Популяризатор;
- Ошибка отслеживания
- Запись на сайте ChromeStatus.com
- Изменения по сравнению с API эксперимента с источником
- Завершенный эксперимент с источником
Благодарности
Мы благодарим Доменика Дениколу (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.