Трассировка оптимизации и деопта
Инструментарий практика для наблюдения, как V8 оптимизирует и откатывается: --trace-opt, --trace-deopt, --trace-ic, --print-bytecode, --allow-natives-syntax (%OptimizeFunctionOnNextCall, %GetOptimizationStatus) и node --prof / --cpu-prof.
Функция на пути рендера быстра 200 мс, затем еле ползёт. CPU-профиль (flame graph — диаграмма горячих стеков во времени) показывает, что она каждые несколько секунд снова всплывает на вершину. Вы подозреваете цикл деопта — но подозрение это не диагноз. V8 точно скажет, что случилось, на каком смещении байт-кода и почему, если спросить его правильными флагами. Этот урок — инструментарий для такого вопроса. К концу вы будете знать, какой флаг взять первым, как прочитать строку деопта до названной причины и как управлять оптимизатором вручную в контролируемом эксперименте.
Мышление: воспроизвести, трассировать, прочитать, исправить
Работа над производительностью в V8 — это не гадание. Движок инструментирован изнутри, и Node/d8 раскрывают эту инструментацию через флаги. Цикл всегда один и тот же: детерминированно воспроизвести медленный путь, запустить его под нужным флагом трассировки, прочитать события, которые флаг выпускает, затем исправить данные или поток управления, вызвавшие их — но не движок.
Эти флаги работают с node (он встраивает V8) и с d8 (отдельная оболочка V8). Для разовых экспериментов d8 чище, потому что в нём нет шума Node. Для вопроса «что делает моё настоящее приложение» нужен node --prof или --cpu-prof.
Трассировка ярусов: —trace-opt и —trace-deopt
--trace-opt печатает строку каждый раз, когда функция выбрана для оптимизации, и каким ярусом компилятора. Вы узнаёте, когда функция стала достаточно горячей, чтобы скомпилироваться, и дошла ли она до Maglev или TurboFan:
$ node --trace-opt app.js
[marking 0x... <JSFunction parseRow> for optimization to MAGLEV, ...]
[completed optimizing 0x... <JSFunction parseRow> (target MAGLEV)]
[marking 0x... <JSFunction parseRow> for optimization to TURBOFAN, ...]--trace-deopt — флаг, к которому тянешься чаще всего. Он печатает строку каждый раз, когда оптимизированная функция откатывается обратно в интерпретатор — событие, стоящее почти за каждой регрессией «быстро, потом медленно»:
[deoptimizing (DEOPT eager): begin 0x... <JSFunction sum> (opt #3) @5, FP to SP ...
;;; deoptimize at <app.js:12:14>, reason: Smi, ...Читайте слева направо: вид (eager — guard упал на входе в проверяемую операцию; lazy — функцию инвалидировали, пока она висела приостановленной на стеке; soft — откат из-за бедности обратной связи, который не выбрасывает код), функция и id оптимизации, смещение байт-кода (@5), место в исходнике и причина — здесь Smi, означающая, что значение, которое оптимизированный код считал малым целым, им не было. Это одно слово и есть диагноз: число вышло за диапазон Smi или пришло не целое.
- --trace-opt
- когда + каким ярусом оптимизировано
- --trace-deopt
- вид деопта, смещение, причина
- --trace-ic
- переходы состояния IC по точкам
- --print-bytecode
- дамп байт-кода Ignition
- --print-opt-code --code-comments
- маш. код TurboFan
- node --prof / --cpu-prof
- сэмпл. / DevTools профиль
Трассировка IC и байт-кода
--trace-ic логирует каждый переход состояния inline cache в каждой точке доступа к свойству и вызова: продвижение от неинициализированного (0) к premonomorphic, monomorphic (1), polymorphic (P) и megamorphic (N). Когда горячая точка, которую вы ждали monomorphic, показывает N, вы нашли расхождение форм — тема юнита 03. В логе есть указатели map (hidden class), которые она видела, так что можно посчитать число различных форм, попавших в точку.
--print-bytecode выгружает байт-код Ignition для каждой скомпилированной функции — полезно подтвердить, что интерпретатор реально исполняет (например, что загрузка свойства стала общим LdaNamedProperty, а не чем-то дешевле). --print-opt-code с --code-comments выгружает сгенерированный TurboFan машинный код с аннотациями, привязывающими инструкции к исходнику; к нему тянутся только когда нужно подтвердить предположение на уровне инструкций, не для повседневной работы.
Принуждаем к делу через —allow-natives-syntax
Для детерминированных экспериментов --allow-natives-syntax разблокирует интринзики с префиксом %, чтобы можно было управлять оптимизатором вручную, не дожидаясь естественного разогрева функции:
function sum(a, b) { return a + b; }
sum(1, 2); // разогреть обратную связь (движок увидел Smi)
%OptimizeFunctionOnNextCall(sum); // запросить оптимизацию
sum(3, 4); // этот вызов исполняется оптимизированным кодом
console.log(%GetOptimizationStatus(sum));%GetOptimizationStatus(fn) возвращает битовую маску, которую декодируешь бит за битом. Биты, которые важны: оптимизирована, turbofanned, maglevved, интерпретируется, помечена на деоптимизацию, является функцией и никогда не оптимизировать (функция содержит конструкцию, которую V8 отказывается оптимизировать). Значение со взведёнными битами «оптимизирована» и «turbofanned» означает, что исполняется код TurboFan; если взведён бит «помечена на деоптимизацию», следующий вызов откатится. Прочие интринзики дополняют набор: %HasFastProperties(obj) говорит, остаётся ли объект в быстром (не словарном) режиме, а %DebugPrint(obj) выгружает его полную внутреннюю раскладку — указатель map, значения свойств и element kind массива (PACKED_SMI против HOLEY и так далее).
Профилируем настоящее приложение
Флаги трассировки отвечают на вопрос «почему медленна вот эта функция». Чтобы сначала найти, какая функция медленна, профилируйте:
node --profпишетisolate-*.logиз сэмплированных стеков;node --prof-process isolate-*.logпревращает его в человекочитаемый отчёт с разбивкой тиков по функциям и секцией, выделяющей время Ignition против оптимизированного кода и время GC.node --cpu-profпишет.cpuprofile, который грузишь прямо в панель Performance Chrome DevTools (или VS Code) ради flame graph — читать его гораздо легче, чем текстовый профиль.
Дисциплина: сначала профиль, чтобы найти доминирующую стоимость, затем нацелить --trace-deopt / --trace-ic на функцию, которую обвинил профиль. Слепая трассировка всего приложения топит вас в строках.
▸Почему это работает
Чем отличается soft-деопт: eager/lazy-деопт выбрасывает оптимизированный код, и функция должна заново разогреться и перекомпилироваться. Soft-деопт случается, когда оптимизированный код попадает на путь, для которого у него нет обратной связи (причина insufficient type feedback); V8 сохраняет код и просто собирает обратную связь. Горстка soft-деоптов на старте — норма. Повторяющийся eager-деопт на той же функции в том же смещении — это патология, цикл деопта, и слово-причина говорит о сломанном предположении.
Строка `--trace-deopt` гласит `deoptimizing (DEOPT eager) ... reason: Smi` на функции, суммирующей массив. Какова наиболее вероятная причина?
Вы пока не представляете, какая функция горячая. Какой инструмент берёте ПЕРВЫМ?
Расставьте по порядку шаги грамотной диагностики производительности V8 с нуля.
- 1 Собрать детерминированное репро, прогоняющее медленный путь
- 2 Профилировать через node --prof / --cpu-prof, чтобы найти доминирующую функцию
- 3 Запустить эту функцию под --trace-deopt / --trace-ic, чтобы назвать причину
- 4 Исправить данные или поток управления, сломавшие предположение, затем перепроверить
- 01Разберите чтение одной строки --trace-deopt. Какие у неё поля и какое из них диагноз?
- 02Как декодировать %GetOptimizationStatus и что делать с %HasFastProperties и %DebugPrint?
- 03В чём разница между node --prof и node --cpu-prof и где каждый в рабочем процессе?
Диагностика производительности V8 — это цикл из четырёх шагов: детерминированно воспроизвести, трассировать, прочитать события, исправить данные или поток управления. Флаги — это инструментарий. --trace-opt сообщает, когда функция оптимизирована и каким ярусом (Maglev/TurboFan); --trace-deopt сообщает каждый откат с его видом (eager/lazy/soft), смещением байт-кода, местом в исходнике и словом-причиной — и эта причина (Smi, wrong map, lost precision) и есть диагноз. --trace-ic раскрывает переходы состояния inline cache, чтобы заметить расхождение форм; --print-bytecode и --print-opt-code --code-comments выгружают байт-код Ignition и машинный код TurboFan для подтверждения. Под --allow-natives-syntax управляешь оптимизатором вручную через %OptimizeFunctionOnNextCall, декодируешь битовую маску %GetOptimizationStatus и осматриваешь раскладку через %HasFastProperties и %DebugPrint. Чтобы найти, какую функцию трассировать, сначала профилируй через node --prof (затем --prof-process) или --cpu-prof ради flame graph в DevTools. Профиль чтобы локализовать, трассировка чтобы диагностировать, фикс выше движка. Теперь, когда увидишь картину «быстро — потом медленно» на одной и той же функции, знаешь первый ход: --cpu-prof, найти виновника, затем --trace-deopt — слово-причина назовёт сломанное предположение.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.