Как не потратить час на починку того, что не сломано. Число, которое печатает твой же инструмент, — ещё не измерение; метрика, посчитанная удобным способом, может мерить не то явление. Проверка метрики возмущением входа, честный замер FPS секундомером вместо показаний харнесса, локальные метрики против глобальных средних. Используй перед тем, как оптимизировать по замеру, когда «производительность упала», когда цифра скачет на одинаковой работе, когда исполнитель спорит с вердиктом приёмки, и когда формулируешь порог приёмки числом.
Resources
1Install
npx skillscat add sashkosoft/godot-skilled/measurement-sanity Install via the SkillsCat registry.
Прежде чем верить числу
Два случая из одного проекта, оба стоили времени, оба ловятся одной проверкой
(см. ORIGIN.md).
Главное правило
Число становится измерением только когда ты знаешь, что именно оно меряет,
и проверил, что оно движется вместе с входом.
Проверка называется возмущением: измени вход в известную сторону и посмотри,
пошла ли метрика туда же.
- сделал строго меньше работы — число обязано улучшиться или не измениться;
- убрал причину дефекта — метрика дефекта обязана упасть;
- скачет туда-сюда на почти одинаковой работе — это шум, а не измерение.
Один замер не значит ничего. Ряд замеров, который не монотонен, значит, что
мерится не то.
Случай 1: харнесс печатает FPS, а это не FPS
Скриншот-харнесс проекта печатал FPS N в строке отчёта о снятом кадре. После
богатого шейдера покрытия он показал 4 против 83 до него. Вывод
напрашивался: двадцатикратная регрессия, надо оптимизировать.
Оптимизировали. Харнесс показал 15. Ещё оптимизировали — показал 1.
Вот здесь и надо было остановиться: работы стало строго меньше, а число упало.
Значит меряется не работа. Так и оказалось — харнесс снимает кадр в
суперсэмплированный офскрин-буфер, и FPS относится к этому рендеру, а не к
игре.
Честный замер — фиксированное число кадров, обычный запуск, часы снаружи:
$t = Measure-Command {
& .\engine.exe --path game --resolution 1600x900 --quit-after 600 scene.tscn *> $null
}
"{0:N1} FPS" -f (600 / $t.TotalSeconds)Результат: с новым покрытием 82 FPS, без него 73. Регрессии не было вовсе.
Час оптимизации ушёл на починку того, что не ломалось.
Как надо было: до оптимизации снять A/B — с фичей и без — одним и тем же
честным способом. Разница между двумя числами, полученными одинаково, — это
измерение. Одно число из незнакомого источника — нет.
Случай 2: метрика мерит не то явление
Принимали текстуры с альфа-вырезом. Ореол подложки на кромке мерили удобным
способом: средняя яркость полупрозрачных пикселей минус средняя яркость
непрозрачного ядра по всему файлу. Файл получил +0.217 и ушёл на переделку.
Исполнитель возразил и принёс собственный контр-замер: если сузить пороги
кейера, глобальный показатель ухудшается (0.198 → 0.293), хотя ореола от
этого не прибавляется ни капли.
Это и есть возмущение входа — и оно доказывало, что метрика ловит не ореол.
Глобальное среднее смешивало его с настоящей структурой: тонкая светлая
метёлка объективно светлее затенённой глубины куста.
Перемер локальной метрикой (каждый кромочный пиксель против ядра в своей
окрестности 9×9) дал −0.0013: ореола нет. Возражение проигнорировали, и
годный файл простоял забракованным двенадцать дней.
Правило: локальный эффект не ловится глобальным средним. Если явление
живёт в окрестности пикселя — и метрика должна жить там же.
Случай 3: скорость шага по анимации
Персонаж ходит на месте, тело двигает код — скорость надо взять из клипа,
иначе ноги скользят. Замер шёл четыре круга, и каждый круг учил своему:
- Первое число было запасным значением, а не замером. 1.20 м/с — ровно
то, что код подставляет при неудаче. Совпадение с «дефолтом» — повод
заподозрить, а не радоваться. Причина:mixer.setTimeдвигает только
запущенные действия; клип не был запущен, ступни стояли, замер дал 0,
и сработал запасной путь. Лог должен прямо писать, измерено или по умолчанию. - Два способа разошлись вдвое (0.48 против 1.24) — значит, как минимум
один мерит не то. «Какая ступня ниже, та опорная» ловила переносимую ногу
на отрыве пятки. Вывод траектории ступни по кадрам показал: в том клипе
опоры вообще не было — стопа разгонялась 0.7 → 2.0 м/с. Никакая скорость
тела такое не уберёт; честное число — размах / время хода назад, а
проскальзывание надо выводить числом и отдавать аниматору. - Когда аниматор поставил ноги на IK, стопа в опоре встала — и старый
способ стал занижать (0.75 против заявленных 0.78, 2.47 против 2.68).
Правильный способ зависит от качества клипа: чистая опора — медиана
скорости стопы на земле; плывущая — размах. - Мягкая постановка стопы добавила медленные отсчёты у касания, и
классификатор «опора чистая / плывёт» снова ошибся. Устойчиво — искать
плато: отсчёты не ниже 85 % максимума, если их заметная доля.
Правило: если замер расходится с тем, что заявляет автор, первым делом
подозревай метрику и выводи сырой ряд (траекторию), а не подбирай порог.
Случай 4: замер цены кадра в браузере (веб-квартал, three.js)
Кнопка «⏱ замер» / #bench=N (perf.js runBench) — таблица «без слоя X»
и экономия в мс. Первый её вариант дал экономию со знаком минус у половины
слоёв: без камешков кадр «дороже» на 3.8 мс. Это и есть признак из списка ниже —
число скачет сильнее эффекта. Причины, по порядку находок:
- Headless с
--virtual-time-budget— время виртуальное.performance.now()
стоит, замер печатает 0.00. Для цены кадра — только реальное время: Chrome--headless=new --use-angle=d3d11 --enable-gpuживым процессом (Popen, ждать,
убить), консоль из--enable-logging=stderr. Проверка, что рисует настоящая
карта:WEBGL_debug_renderer_info→RTX 4070 Ti SUPER … D3D11, а не SwiftShader. - Замер стартовал, пока сцена догружалась (8 с после загрузки, а слои идут
до ~40 с): сравнивались разные сцены.#bench=45— старт через 45 с. render + gl.finishмерит и CPU. Обход сцены three.js шумит на
миллисекунды. Таймер GPU (EXT_disjoint_timer_query_webgl2) мерит только
видеокарту. Под ANGLE результат приходит не сразу — ждать последний запрос
кадрами (requestAnimationFrame), а не опрашивать каждый: иначе замер
«висит» десятки минут.- Видеокарта в простое спит (nvidia-smi: 210 МГц) и разгоняется секундами:
общий кадр за один прогон уплыл с 34 до 10 мс. Нужен прогрев (3 с сплошного
рендера) и попеременный A/B: «с / без» пятью парами подряд, медианы. - На этой машине GPU делят Houdini и Blender соседних сессий — абсолютные мс
между прогонами несравнимы; сравнимы только пары внутри одного прогона и
счёт работы: вызовы и треугольники (разбивка PERF) не шумят вовсе.
Правило: до оптимизации — какой слой дорогой, по разнице пар; после —
«было → стало» флагом того же прогона (#inst=0, #cull=0), и в отчёт
вместе с мс — вызовы/треугольники, которые от шума не зависят.
Признаки, что мерится не то
- значения скачут на почти одинаковом входе;
- метрика не реагирует на устранение причины;
- метрика реагирует на то, что к явлению отношения не имеет;
- источник числа — «инструмент напечатал», и ты не знаешь, из чего он его взял;
- число красивое, круглое и подтверждает то, что ты и так думал.
Когда исполнитель спорит с вердиктом
Самый дешёвый момент разобраться — и самый частый момент проехать мимо.
Спор с числом почти всегда содержит контр-замер; проверь метрику, а не
настаивай на вердикте. В обоих случаях выше спорящая сторона была права.
Рабочие привычки
- На каждую фичу, которую подозреваешь в цене, — флаг, который её снимает.
Без A/B замер не с чем сравнить. - Замер в коммит кладётся парой «было → стало», одинаковым способом и с
железом. Одиночное число в коммите бесполезно. - Порог приёмки формулируется числом и способом замера. «Кромка должна
мягко сходить на нет» — это не требование; «альфа = 0 по рамке в 4 px» —
требование. - Прежде чем оптимизировать — воспроизведи проблему дважды.