Трассировка от JS к DOM и обратно
Отладка утечек памяти в Chrome 66 стала намного проще. В инструментах разработчика Chrome теперь можно трассировать и создавать снимки C++ DOM объектов, а также отображать все достижимые DOM объекты из JavaScript с их ссылками. Эта функция является одним из преимуществ нового механизма трассировки C++ сборщика мусора V8.
Фон
Утечка памяти в системе сборки мусора происходит, когда неиспользуемый объект не освобождается из-за непреднамеренных ссылок на другие объекты. Утечки памяти на веб-страницах часто связаны с взаимодействием между объектами JavaScript и элементами DOM.
Следующий упрощённый пример демонстрирует утечку памяти, которая происходит, когда программист забывает удалить регистрацию обработчика событий. Никакие из объектов, на которые ссылается обработчик событий, не могут быть освобождены сборщиком мусора. В частности, окно iframe утечёт вместе с обработчиком событий.
// Главное окно:
const iframe = document.createElement('iframe');
iframe.src = 'iframe.html';
document.body.appendChild(iframe);
iframe.addEventListener('load', function() {
const localVariable = iframe.contentWindow;
function leakingListener() {
// Делает что-то с `localVariable`.
if (localVariable) {}
}
document.body.addEventListener('my-debug-event', leakingListener);
document.body.removeChild(iframe);
// ОШИБКА: забыли удалить регистрацию `leakingListener`.
});
Утечка окна iframe также удерживает все его объекты JavaScript.
// iframe.html:
class Leak {};
window.globalVariable = new Leak();
Важно понимать концепцию удерживающих путей, чтобы найти основную причину утечки памяти. Удерживающий путь — это цепочка объектов, которая предотвращает сборку мусора для протекающего объекта. Цепочка начинается с корневого объекта, такого как глобальный объект главного окна. Цепочка заканчивается протекающим объектом. Каждый промежуточный объект в этой цепочке имеет прямую ссылку на следующий объект в цепочке. Например, удерживающий путь для объекта Leak
в iframe выглядит следующим образом:
Обратите внимание, что удерживающий путь пересекает границу JavaScript / DOM (выделено зелёным/красным, соответственно) дважды. Объекты JavaScript находятся в куче V8, а объекты DOM — это объекты C++ в Chrome.
Снимок кучи в DevTools
Мы можем исследовать удерживающий путь любого объекта, сделав снимок кучи в DevTools. Снимок кучи точно фиксирует все объекты в куче V8. До недавнего времени он содержал только приблизительную информацию о объектах C++ DOM. Например, Chrome 65 показывает неполный удерживающий путь для объекта Leak
из упрощённого примера:
Только первая строка точна: объект Leak
действительно хранится в global_variable
объекта окна iframe. Последующие строки приближают реальный удерживающий путь и усложняют отладку утечки памяти.
Начиная с Chrome 66, DevTools трассирует объекты C++ DOM и точно фиксирует объекты и ссылки между ними. Это основано на мощном механизме трассировки объектов C++, который ранее был введён для межкомпонентной сборки мусора. В результате удерживающий путь в DevTools теперь действительно правильный:
В деталях: межкомпонентная трассировка
Объекты DOM управляются Blink — движком рендеринга Chrome, который отвечает за перевод DOM в текст и изображения на экране. Blink и его представление DOM написаны на C++, что означает, что DOM не может быть напрямую представлен в JavaScript. Вместо этого объекты в DOM представлены двумя половинами: объект-обёртка V8, доступный для JavaScript, и объект C++, представляющий узел в DOM. Эти объекты имеют прямые ссылки друг на друга. Определение жизнеспособности и владения объектами через несколько компонентов, таких как Blink и V8, сложно, потому что все вовлечённые стороны должны согласиться, какие объекты ещё живы, а какие можно освободить.
В Chrome 56 и более ранних версиях (т.е. до марта 2017 года) Chrome использовал механизм под названием группировка объектов для определения живучести. Объекты группировались на основе их принадлежности к документам. Группа со всеми содержащимися в ней объектами сохранялась на время существования хотя бы одного объекта путем некоторого другого пути удержания. Это имело смысл в контексте DOM-узлов, которые всегда ссылаются на свой содержащий документ, образуя так называемые DOM-деревья. Однако эта абстракция удаляла все фактические пути удержания, что усложняло отладку, как показано на рисунке 2. В случае объектов, которые не подходили под этот сценарий, например, замыканий JavaScript, используемых как слушатели событий, этот подход также становился громоздким и приводил к различным ошибкам, когда JavaScript-обертки объектов преждевременно собирались, что приводило к их замене на пустые обертки JS, которые теряли все свои свойства.
Начиная с Chrome 57, этот подход был заменен трассировкой межкомпонентных связей, механизмом, который определяет живучесть путем трассировки от JavaScript к реализации DOM на C++ и обратно. Мы реализовали приращённую трассировку на стороне C++ с барьерами записи, чтобы избежать пауз на весь мир, о которых мы говорили в предыдущих публикациях в блоге. Трассировка межкомпонентных связей не только обеспечивает лучшую задержку, но также лучше определяет живучесть объектов за границами компонентов и исправляет несколько сценариев, которые ранее вызывали утечки. Кроме того, это позволяет DevTools предоставлять снимок, который действительно представляет DOM, как показано на рисунке 3.
Попробуйте это! Мы будем рады услышать ваши отзывы.