From 182a60c9b00b462a68a2f1e6ad5628f9c88af6ee Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 3 Sep 2026 20:14:12 +0300 Subject: [PATCH] =?UTF-8?q?EDITORIAL-212:=20=D0=B2=D1=8B=D1=87=D0=B8=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BC=D0=B5=D1=85=D0=B0=D0=BD=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=20=D0=B1=D1=8E=D0=B4=D0=B6=D0=B5=D1=82=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B8=D0=B7=D0=B2=D0=BE=D0=B4=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- editorial/agent-rewrites/212.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/editorial/agent-rewrites/212.json b/editorial/agent-rewrites/212.json index 6ea1728..e77cb04 100644 --- a/editorial/agent-rewrites/212.json +++ b/editorial/agent-rewrites/212.json @@ -1,7 +1,7 @@ { "index": 212, "slug": "editorial-2022-02-mechanism-performance-budget", - "title": "Бюджет производительности маршрута: почему общий PASS скрывает регрессию", - "excerpt": "Практическая модель бюджета маршрута: фиксируем условия, сравниваем именованные компоненты с допуском и останавливаем проверку, если входы больше не сопоставимы.", - "contentHtml": "

После небольшого изменения команда запускает проверку и получает PASS. Общая сумма работы выросла с 85 до 93 условных единиц, но осталась ниже общего лимита 98. Через несколько дней пользователи замечают, что первый экран стал реагировать позже. В отчёте нет явного нарушения: зелёный итог скрыл рост в одном компоненте.

\n

Цена такой ошибки — не только лишние миллисекунды. Команда выбирает неверное действие. Она меняет кеш, сеть или весь маршрут, хотя регрессия появилась в одном участке. Потом сравнения начинают проводиться в разных условиях, а бюджет теряет смысл.

\n

Тезис: бюджет маршрута должен проверять именованные части до общей суммы. Общий итог нужен как дополнительный ограничитель. Он не отменяет нарушение отдельного компонента.

\n

Что именно защищает бюджет

\n

Бюджет — это не одно число в конфигурации. Он связывает маршрут, пользовательский сценарий, версию сборки, условия запуска и измеряемые части работы. Эти поля образуют measurement contract. Если контракт не записан, два числа «до» и «после» могут описывать разные события.

\n

В учебной модели ниже четыре поля: documentTicks, styleTicks, scriptTicks и renderTicks. Суффикс Ticks намеренный. Это условные значения локального примера. Они не являются LCP, TTFB, INP, DOMContentLoaded или результатом браузерного профиля.

\n

Настоящий браузер предоставляет другие записи. Например, Navigation Timing описывает навигацию документа, а Resource Timing — загрузку ресурсов. Сначала нужно получить такую запись из поддерживаемого API, затем явно сопоставить её поля с внутренней схемой. Нельзя переименовать произвольное число в LCP и получить от этого реальную метрику.

\n

Механизм сравнения

\n

Сравнение проходит четыре слоя.

\n
  1. Контракт. Проверяем одинаковые маршрут, сценарий, версию workload и условия. В условия входят, например, тип навигации, cache policy, viewport, браузер и сеть.
  2. Snapshot. Сохраняем baseline с той же схемой полей. Baseline — это не «последний удачный отчёт», а точка отсчёта для конкретного контракта.
  3. Компоненты. Для каждого именованного поля проверяем собственный limit и tolerance. Результат должен показывать имя, baseline, candidate, allowed и delta.
  4. Решение. Overall получает FAIL, если нарушен хотя бы один компонент. Aggregate читается после component checks и остаётся вторичным guard.
\n

Допуск — часть правила, а не скрытая скидка. При limit = 28 и tolerance = 2 порог равен 30. В отчёте нужно сохранить оба значения. Иначе нельзя отличить осознанный допуск от случайно изменённого лимита.

\n
const contract = {\n  route: '/checkout',\n  scenario: 'open-and-submit',\n  conditions: {\n    browser: 'chromium',\n    viewport: '390x844',\n    cache: 'cold',\n    network: 'fixed-4g'\n  }\n};\n\nconst baseline = {\n  documentTicks: 18,\n  styleTicks: 15,\n  scriptTicks: 26,\n  renderTicks: 26\n};\n\nconst candidate = {\n  documentTicks: 20,\n  styleTicks: 16,\n  scriptTicks: 34,\n  renderTicks: 23\n};\n\nconst rules = {\n  documentTicks: { limit: 20, tolerance: 2 },\n  styleTicks: { limit: 18, tolerance: 2 },\n  scriptTicks: { limit: 28, tolerance: 2 },\n  renderTicks: { limit: 30, tolerance: 2 }\n};
\n

В этом примере baseline равен 85, candidate — 93. Общий allowed равен 98, поэтому aggregate проходит. Но scriptTicks вырос с 26 до 34. Его allowed равен 30. Компонент нарушен, значит итоговая проверка должна вернуть FAIL. Разница между 34 и 28 равна 6. Разница между 34 и allowed равна 4. Обе величины полезны, если отчёт называет их однозначно.

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
Общий PASS, но один участок выросAggregate скрывает component failureСравнить каждый field с его allowedОстановить итог как FAIL и открыть только failed component
До и после нельзя честно сравнитьРазличаются cache, route, browser или scenarioСравнить все поля contractВернуть status measurement-contract-invalid и переснять candidate
Отчёт меняется от запуска к запускуУсловия не зафиксированы или шум выше toleranceПовторить сценарий и записать условияИзменить способ измерения или обосновать tolerance
Красный компонент приводит к глобальной оптимизацииДиагностика не ограничена именем поляПроверить input и границу failed componentСделать одну локальную проверку, не менять весь стек
Учебный отчёт называют browser metricВнутреннее поле не сопоставлено с APIНайти источник и mapping для значенияПереименовать поле или добавить реальный сбор
\n

Почему нужен отрицательный путь

\n

Представим, что baseline снят с cold cache, а candidate — с warm cache. Candidate может оказаться быстрее. Это не доказательство улучшения: изменился вход. То же происходит, если baseline относится к /checkout, а candidate — к /cart, или если один запуск включает авторизацию, а другой нет.

\n

Правильный результат в таком случае — не FAIL и не PASS. Сравнение нужно остановить с причиной measurement-contract-invalid. Component map остаётся пустым, aggregate не получает performance-смысл, а следующий шаг — восстановить один контракт и повторить измерение.

\n

Не стоит автоматически нормализовать несовпадение. Если система молча заменит warm на cold или возьмёт последний baseline, она создаст удобный, но ложный verdict. Лучше потерять один результат, чем принять несопоставимые числа за регрессию или улучшение.

\n
\"Учебное
Учебная иллюстрация: зелёный aggregate не отменяет красный named component. Значения показывают условные ticks, а не результаты production-профиля.
\n

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

\n

Модель полезна только после явной границы между сбором и решением. Сбор получает официальную запись API. Адаптер выбирает поля, нормализует единицы и сохраняет условия. Сравниватель работает уже с проверенной внутренней схемой.

\n
const navigation = performance.getEntriesByType('navigation')[0];\n\nconst observation = {\n  source: 'PerformanceNavigationTiming',\n  fields: {\n    responseEnd: navigation.responseEnd,\n    domInteractive: navigation.domInteractive,\n    loadEventEnd: navigation.loadEventEnd\n  },\n  conditions: {\n    route: location.pathname,\n    navigationType: navigation.type\n  }\n};\n\n// Учебный пример: здесь нет решения о PASS/FAIL.\n// Сначала нужен отдельный mapping в схему проекта.
\n

Этот код показывает границу, а не готовый production-сборщик. Он не учитывает отправку данных, sampling, privacy, доступность API и различия браузеров. Он также не превращает три timestamp в четыре условных компонента автоматически. Mapping должен описывать формулу, единицы, поддержку браузеров и условия применимости.

\n

Если проекту нужен ресурсный бюджет, следует получить Resource Timing и отдельно решить, какие ресурсы входят в контракт. Документная навигация и загрузка каждого ресурса отвечают на разные вопросы. Смешивать их в один total без правила агрегации нельзя.

\n

Порядок работы

\n
  1. Назвать маршрут и пользовательский сценарий. Не использовать «страница в целом».
  2. Записать условия: браузер, viewport, сеть, cache policy, версия сборки и тип навигации.
  3. Определить поля и единицы. Для каждого поля указать источник, limit и tolerance.
  4. Снять baseline и сохранить его вместе с contract. Не заменять его последним удачным запуском.
  5. Снять candidate в тех же условиях. При изменении условий завершить проверку на validation error.
  6. Сначала проверить наличие, набор и диапазон полей. Затем сравнить компоненты.
  7. Посчитать aggregate только для контекста. Он не должен маскировать component failure.
  8. Для первого нарушения назначить одну ограниченную проверку: конкретный input, ресурс или границу маршрута.
  9. После изменения повторить измерение с тем же контрактом и сравнить новый candidate с тем же baseline.
\n

Ограничения модели

\n

Учебные ticks не дают сведений о реальном количестве пользователей, SLA, полевых перцентилях или влиянии устройства. Даже настоящий browser trace не объясняет сам по себе причину регрессии. Он показывает наблюдение. Причину нужно искать в ресурсах, коде, серверном ответе, cache и сценарии.

\n

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

\n

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

\n

Проверяемый критерий готовности

\n

Изменение готово, если маршрут и сценарий названы; условия сохранены; baseline и candidate имеют одну схему; каждое поле имеет limit и tolerance; отчёт показывает component checks отдельно от aggregate; mismatch условий останавливает сравнение; failed component ведёт к одной ограниченной проверке; повторный candidate снят в том же contract.

\n

Зелёный total сам по себе этому критерию не соответствует. Проверка считается полезной только тогда, когда по её результату можно понять, что именно нарушено, что нужно проверить дальше и почему сравнение вообще допустимо.

\n

Проверяемые источники

\n" + "title": "Почему общий PASS не спасает бюджет маршрута: механизм проверок", + "excerpt": "Разбираем механизм бюджета маршрута: сопоставимый контракт, именованные проверки, допуск, вторичный aggregate и ограниченная диагностика без подмены учебных чисел браузерной метрикой.", + "contentHtml": "

В учебном отчёте после изменения маршрута общий результат остаётся зелёным: сумма выросла с 85 до 93 условных единиц и не превысила общий предел 98. Но в том же отчёте scriptTicks поднялся с 26 до 34 при допустимом значении 30. Если смотреть только на total, изменение проходит. Цена ошибки — команда принимает зелёный итог за доказательство здоровья всего пути и пропускает участок, который уже вышел за свой предел.

\n

Такой пример не описывает реальный запуск и не обещает пользователю измеренную скорость. Он показывает дефект правила: разные части маршрута нельзя складывать до того, как проверена каждая часть. Бюджет должен сначала установить сопоставимость входов, затем показать named component checks и только после этого посчитать aggregate. Иначе случайное уменьшение одного расхода может замаскировать рост другого.

\n

Главный вывод: общий PASS отвечает только на вопрос об общей сумме. Он не отменяет FAIL отдельного компонента. Чтобы результат можно было повторить, рядом с числами нужны маршрут, сценарий, версия рабочей нагрузки, условия, baseline, candidate, limit и tolerance.

\n

Что именно защищает бюджет маршрута

\n

Бюджет маршрута — это контракт между измерением и решением. Он связывает конкретный путь, пользовательский сценарий и набор измеряемых полей с правилом остановки. Формулировка «страница должна быть быстрой» таким контрактом не является: в ней нет ни входа, ни единицы, ни границы отказа.

\n

В учебной модели маршрут — /training/checkout/review, сценарий — anonymous-cart-with-one-item, а версия рабочей нагрузки — checkout-fixture-v1. Четыре поля имеют суффикс Ticks, потому что это условная единица примера. Она не равна миллисекундам и не переименовывает произвольное число в LCP, TTFB или другую браузерную метрику.

\n

Условия должны описывать то, что способно изменить результат: состояние кеша, viewport, профиль браузера, набор данных, сеть и тип навигации. Полный список зависит от проекта. Правило важнее списка: если baseline и candidate получены при разных обязательных условиях, сравнение надо остановить до вычисления performance verdict.

\n

Слой 1: сначала проверить сопоставимость

\n

Baseline — это не последний отчёт, который случайно оказался зелёным. Это снимок с той же схемой полей и тем же контрактом, что у candidate. Перед сравнением сверяются route, scenario, workloadVersion, unit и conditions. Если одно поле не совпало, система возвращает состояние measurement-contract-invalid, а не пытается угадать, какую разницу можно простить.

\n

Например, baseline снят с cold cache, а candidate — с warm cache. Более маленькое число candidate не доказывает улучшение: изменился вход. Та же проблема возникает при смене маршрута, состава корзины, viewport или типа навигации. Молчаливое «выравнивание» условий опаснее явной ошибки, потому что красивый отчёт перестаёт показывать границу применимости.

\n

Проверка контракта должна быть наблюдаемой. В отчёте полезно сохранить не только статус, но и первое расхождение: conditions.cache: cold → warm. Тогда следующий шаг понятен — переснять candidate в прежних условиях или создать новый baseline с объяснённой причиной.

\n

Слой 2: проверять именованные компоненты

\n

После validation каждый компонент проверяется отдельно. Для него нужны как минимум фактическое значение, baseline, limit, tolerance и вычисленный allowed. Правило задаётся формулой allowed = limit + tolerance; компонент проходит, если actual <= allowed.

\n

В нашем примере для scriptTicks limit равен 28, tolerance равен 2, поэтому allowed равен 30. Candidate со значением 34 превышает порог на 4. Он также отличается от baseline на 8, но это другая величина: delta описывает изменение относительно снимка, а allowed — границу принятия. Отчёт не должен смешивать эти значения под одним названием «превышение».

\n

Именованный компонент нужен не для того, чтобы заранее объявить виновную библиотеку. Он ограничивает место следующей проверки. Если красным стал scriptTicks, результат подтверждает нарушение правила этого поля. Он ещё не подтверждает причину: ею может оказаться bundle, обработка данных, dynamic import, состояние кеша или ошибка самого измерения.

\n

Слой 3: aggregate идёт после component checks

\n

Общая сумма полезна как отдельный guard. Она показывает, сколько места осталось у маршрута в целом, но теряет распределение расходов. В учебной записи baseline равен 18 + 15 + 26 + 26 = 85. Candidate равен 20 + 16 + 34 + 23 = 93. При aggregate limit 96 и tolerance 2 общий allowed равен 98.

\n

Candidate проходит aggregate, потому что 93 не больше 98. Но scriptTicks не проходит, потому что 34 больше 30. Поэтому overall должен быть FAIL. Порядок вычисления защищает от подмены: aggregate отвечает за total, а component отвечает за именованную часть. Зелёная сумма не имеет права превращать красную строку в зелёную.

\n
Роль каждого слоя в проверке бюджета
СлойЧто получаетЧто возвращаетГраница вывода
Contractroute, scenario, version и conditionscomparable или measurement-contract-invalidНесовпадение не является регрессией
Componentbaseline, candidate и правило имениactual, allowed, delta и pass/failFAIL указывает поле, но не корень причины
Aggregateсумму полей и общий limitконтекстный total pass/failНе отменяет failed component
Diagnosisпервое failed nameодну узкую следующую проверкуНе даёт глобального вывода о маршруте
\n

Воспроизводимый учебный пример

\n

Ниже — полностью локальная модель. Она не вызывает браузер, сеть, таймер или CI. Все числа заданы явно, поэтому читатель может вручную проверить арифметику. В production такой объект потребует отдельного источника каждого поля и протокола, по которому baseline и candidate были получены.

\n
const contract = {\n  route: '/training/checkout/review',\n  scenario: 'anonymous-cart-with-one-item',\n  workloadVersion: 'checkout-fixture-v1',\n  unit: 'teaching-ticks',\n  conditions: { browser: 'chromium', viewport: '390x844', cache: 'cold' },\n};\n\nconst rules = {\n  documentTicks: { limit: 20, tolerance: 2 },\n  styleTicks: { limit: 18, tolerance: 2 },\n  scriptTicks: { limit: 28, tolerance: 2 },\n  renderTicks: { limit: 30, tolerance: 2 },\n  aggregate: { limit: 96, tolerance: 2 },\n};\n\nconst baseline = { documentTicks: 18, styleTicks: 15, scriptTicks: 26, renderTicks: 26 };\nconst candidate = { documentTicks: 20, styleTicks: 16, scriptTicks: 34, renderTicks: 23 };\n\nconst components = Object.keys(rules).filter((name) => name !== 'aggregate');\nconst checks = components.map((name) => {\n  const rule = rules[name];\n  const allowed = rule.limit + rule.tolerance;\n  return {\n    name,\n    baseline: baseline[name],\n    actual: candidate[name],\n    limit: rule.limit,\n    tolerance: rule.tolerance,\n    allowed,\n    delta: candidate[name] - baseline[name],\n    pass: candidate[name] <= allowed,\n  };\n});\n\nconst total = Object.values(candidate).reduce((sum, value) => sum + value, 0);\nconst aggregateAllowed = rules.aggregate.limit + rules.aggregate.tolerance;\nconst overall = checks.every((item) => item.pass) && total <= aggregateAllowed\n  ? 'PASS'\n  : 'FAIL';\n\nconsole.log({ overall, checks, total, aggregateAllowed });
\n

Вывод этого объекта должен содержать overall: 'FAIL'. В списке checks только scriptTicks имеет pass: false; его actual равен 34, allowed — 30, delta — 8. Total равен 93 и проходит aggregate. Если код возвращает PASS из-за одной только суммы, в нём нарушено главное правило модели.

\n

Здесь есть ещё одна проверка качества примера: все четыре поля входят в total ровно один раз. Нельзя сначала считать total из всех свойств объекта, а затем добавить туда уже агрегированный результат. В реальной схеме список компонентов лучше хранить отдельно от aggregate, чтобы случайное новое поле не меняло формулу без ревью.

\n

Как устроить отрицательный путь

\n

Положительный путь показывает арифметику, но надёжность видна на отказе. Изменим только conditions.cache у candidate с cold на warm. Числа могут остаться прежними, однако результат уже нельзя трактовать как сравнение одного workload. Система должна вернуть measurement-contract-invalid, пустой список component checks и aggregate со статусом not-comparable.

\n

Нельзя исправлять такой отказ увеличением tolerance. Допуск относится к шуму внутри одного объявленного условия, а не к смене самого условия. Если команда сознательно переходит с cold cache на warm cache, это новый сценарий измерения: ему нужен новый контракт и понятный baseline.

\n

То же правило действует для отсутствующего поля, лишнего поля или отрицательного значения. До вычисления delta проверяются схема и диапазон. Иначе пропущенное значение превратится в undefined, а арифметика или сериализация скроет ошибку под числом, похожим на результат.

\n
\"Учебная
Учебные ticks показывают, почему aggregate PASS не отменяет FAIL именованного компонента. Диаграмма не является снимком браузерного или production-измерения.
\n

От измерения к решению: граница браузера

\n

Когда модель понадобится связать с браузером, сначала выбирается источник, а не придумывается mapping под удобную формулу. Navigation Timing предоставляет запись о навигации текущего документа. User Timing позволяет помечать и измерять участки работы приложения. Performance Timeline задаёт способы получить записи и наблюдать новые entries. Эти интерфейсы дают исходные наблюдения, но не определяют четыре учебных компонента и их лимиты.

\n
const [navigation] = performance.getEntriesByType('navigation');\n\nif (navigation) {\n  const observation = {\n    source: 'PerformanceNavigationTiming',\n    route: location.pathname,\n    navigationType: navigation.type,\n    fields: {\n      responseEnd: navigation.responseEnd,\n      domInteractive: navigation.domInteractive,\n      loadEventEnd: navigation.loadEventEnd,\n    },\n  };\n\n  console.table(observation);\n}\n\nperformance.mark('checkout-review-start');\n// Здесь выполняется заранее описанная работа приложения.\nperformance.mark('checkout-review-end');\nperformance.measure(\n  'checkout-review-work',\n  'checkout-review-start',\n  'checkout-review-end',\n);
\n

Этот фрагмент показывает два разных источника. Navigation entry описывает временные точки навигации; mark и measure описывают названный участок работы приложения. Ни один из них автоматически не становится scriptTicks. Для mapping нужно записать поле источника, единицу, момент получения, условия доступности, преобразование и владельца. Если связь не определена, оставьте поле вне production verdict.

\n

Есть и практическое ограничение: поддержка и поведение API зависят от браузера и сценария, а начало наблюдения может влиять на то, какие записи доступны. Поэтому пример не утверждает, что одной строки getEntriesByType достаточно для полевого профиля. Сначала проверяется наличие источника и повторяемость процедуры, затем решается, какие его поля подходят выбранной границе.

\n

Диагностика не должна расширять область поиска

\n

Component failure — это сигнал для следующего факта, а не готовая причинная цепочка. Если красным стал scriptTicks, выберите одну границу: размер bundle после изменения, список новых импортов или число обработанных элементов. Результат этой проверки должен различать хотя бы две гипотезы. Если ни одна граница не наблюдается, так и запишите: имеющихся данных недостаточно для выбора причины.

\n

Широкая оптимизация всего маршрута в этот момент создаёт новый риск. Она меняет несколько переменных и лишает команды возможности связать новый результат с исходным failure. Сначала исправляется одна известная причина или уточняется измерение, затем candidate переснимается при прежнем контракте и сравнивается с тем же baseline.

\n

Изменение limit тоже не является быстрым лечением. Оно допустимо, если владелец маршрута согласовал новую стоимость, обоснование и новый предел. Повышение aggregate, когда failed component остаётся без объяснения, убирает защитный сигнал, но не уменьшает работу маршрута.

\n

Порядок проверки для команды

\n
  1. Назовите путь. Запишите route и пользовательский сценарий. «Страница в целом» недостаточно для воспроизводимого входа.
  2. Зафиксируйте контракт. Сохраните workloadVersion, unit, browser, viewport, cache, набор данных и тип навигации — только те условия, которые действительно участвуют в измерении.
  3. Проверьте схему. Убедитесь, что baseline и candidate имеют одинаковые component names, числовые значения и допустимый диапазон.
  4. Проверьте компоненты. Для каждого имени выведите baseline, actual, limit, tolerance, allowed, delta и pass/fail.
  5. Посчитайте aggregate. Сведите только объявленные компоненты и покажите отдельный aggregate limit. Не используйте total как замену списку checks.
  6. Сформируйте verdict. Overall PASS допустим только при сопоставимом контракте, успешных компонентах и успешном aggregate. Иначе верните FAIL или точную ошибку сопоставимости.
  7. Сузьте диагностику. Для первой красной строки выберите один наблюдаемый input и одну границу маршрута. Не называйте причину, которой нет в данных.
  8. Повторите сравнение. После изменения сохраните контракт и baseline. Если контракт изменился, начните новый ряд измерений и объясните это в отчёте.
\n

Что означает готовый результат

\n

Проверка готова к использованию, если читатель может восстановить входы и получить тот же статус. В отчёте видны route, scenario, версия и conditions; mismatched contract останавливает сравнение; каждый component содержит actual и allowed; aggregate помечен как вторичный; failed name ведёт к одной следующей проверке.

\n

Для учебного объекта ожидается конкретный результат: baseline total равен 85, candidate total равен 93, aggregate allowed равен 98, scriptTicks имеет actual 34 и allowed 30, overall равен FAIL. При смене cache ожидается measurement-contract-invalid, а не вывод об ускорении или регрессии.

\n

Ограничения модели

\n

Условные ticks не сообщают latency реальных пользователей, перцентили, SLA, влияние устройства или стоимость передачи данных. Даже настоящий trace показывает наблюдение, но не доказывает сам по себе, какая библиотека вызвала изменение. Причину устанавливают отдельным экспериментом с сохранёнными входами.

\n

Один запуск также не описывает шум. Tolerance нужно выбирать по повторениям, среде, источнику и цене ложного срабатывания. Слишком большой допуск прячет постепенный рост, слишком маленький превращает бюджет в нестабильный сигнал. Эти решения принадлежат проекту и не следуют из арифметики учебного примера.

\n

Бюджет не заменяет функциональную проверку. Маршрут может уложиться в performance limits и при этом потерять действие, ошибку или доступность. Поэтому component checks должны жить рядом с проверками корректности сценария, а не подменять их.

\n

Проверяемые источники

\n" }