revise May 2026 systems performance articles
Build and deploy / deploy (push) Successful in 15s

This commit is contained in:
2026-07-31 19:18:32 +03:00
parent c5f7177b72
commit 134f826602
7 changed files with 289 additions and 1 deletions
+239
View File
@@ -0,0 +1,239 @@
function escapeHtml(value) {
return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;');
}
const p = (text) => '<p>' + text + '</p>';
const h2 = (text) => '<h2>' + text + '</h2>';
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function deepFreeze(value) {
if (value && typeof value === 'object' && !Object.isFrozen(value)) Object.values(value).forEach(deepFreeze);
return Object.freeze(value);
}
function cloneFixed(value) { return JSON.parse(JSON.stringify(value)); }
function plainText(content) { return content.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); }
function bodyText(content) { return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, '')); }
const REFERENCES = deepFreeze({
traceContext: { url: 'https://www.w3.org/TR/2021/REC-trace-context-1-20211123/' },
otel: { url: 'https://raw.githubusercontent.com/open-telemetry/opentelemetry-specification/c6520a73287040ca16499cba62cea1b3508dc4da/specification/trace/api.md' },
http: { url: 'https://www.rfc-editor.org/rfc/rfc9110.html' },
});
function sourceList(entries) {
return '<ul>' + entries.map((entry) => '<li><a href="' + entry.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(entry.title) + '</a> — версия: ' + escapeHtml(entry.version) + '. ' + escapeHtml(entry.use) + ' Граница: ' + escapeHtml(entry.boundary) + '</li>').join('') + '</ul>';
}
const FIXED_PERFORMANCE_INPUTS = deepFreeze({
'comparable-critical-path-v1': {
id: 'comparable-critical-path-v1', family: 'fixed-system-performance-v1',
trace: { traceId: 'fixed-trace-01', rootSpanId: 'root-01', complete: true, correlation: 'one named fixed synthetic trace' },
load: { cohort: 'fixed-load-a', logicalRequests: 12, concurrency: 3, inputShape: 'fixed-read-shape-a' },
baseline: { id: 'baseline-fixed-a', sameCohort: true, sameLoad: true, effectClaim: 'not-made' },
spans: [
{ id: 'root-01', parentId: null, kind: 'SERVER', name: 'fixed-gateway', start: 0, end: 1000, role: 'end-to-end' },
{ id: 'queue-01', parentId: 'root-01', kind: 'INTERNAL', name: 'fixed-admission-queue', start: 40, end: 560, role: 'queue-wait' },
{ id: 'db-01', parentId: 'root-01', kind: 'CLIENT', name: 'fixed-db-call', start: 570, end: 720, role: 'database-execution' },
{ id: 'ext-01', parentId: 'root-01', kind: 'CLIENT', name: 'fixed-catalog-call', start: 730, end: 930, role: 'external-dependency' },
],
decision: { proposedChange: 'none', controlBoundary: 'same fixed cohort, load and trace shape', observedOnly: true },
boundary: 'named fixed synthetic literals only; no network, filesystem, clock, profiler, benchmark, service, client, metrics, PII or production claim',
},
'incomplete-trace-v1': {
id: 'incomplete-trace-v1', family: 'fixed-system-performance-v1', trace: { traceId: 'fixed-trace-02', rootSpanId: 'root-02', complete: false, correlation: 'one named fixed synthetic trace' },
load: { cohort: 'fixed-load-a', logicalRequests: 12, concurrency: 3, inputShape: 'fixed-read-shape-a' }, baseline: { id: 'baseline-fixed-a', sameCohort: true, sameLoad: true, effectClaim: 'not-made' },
spans: [{ id: 'root-02', parentId: null, kind: 'SERVER', name: 'fixed-gateway', start: 0, end: 1000, role: 'end-to-end' }, { id: 'db-02', parentId: 'missing-01', kind: 'CLIENT', name: 'fixed-db-call', start: 570, end: 720, role: 'database-execution' }],
decision: { proposedChange: 'none', controlBoundary: 'same fixed cohort, load and trace shape', observedOnly: true }, boundary: 'negative fixed literal',
},
'incomparable-load-v1': {
id: 'incomparable-load-v1', family: 'fixed-system-performance-v1', trace: { traceId: 'fixed-trace-03', rootSpanId: 'root-03', complete: true, correlation: 'one named fixed synthetic trace' },
load: { cohort: 'fixed-load-b', logicalRequests: 24, concurrency: 6, inputShape: 'fixed-read-shape-b' }, baseline: { id: 'baseline-fixed-a', sameCohort: false, sameLoad: false, effectClaim: 'not-made' },
spans: [{ id: 'root-03', parentId: null, kind: 'SERVER', name: 'fixed-gateway', start: 0, end: 800, role: 'end-to-end' }, { id: 'queue-03', parentId: 'root-03', kind: 'INTERNAL', name: 'fixed-admission-queue', start: 20, end: 500, role: 'queue-wait' }, { id: 'db-03', parentId: 'root-03', kind: 'CLIENT', name: 'fixed-db-call', start: 520, end: 690, role: 'database-execution' }],
decision: { proposedChange: 'none', controlBoundary: 'different fixed cohort', observedOnly: true }, boundary: 'negative fixed literal',
},
'hidden-queue-v1': {
id: 'hidden-queue-v1', family: 'fixed-system-performance-v1', trace: { traceId: 'fixed-trace-04', rootSpanId: 'root-04', complete: true, correlation: 'one named fixed synthetic trace' },
load: { cohort: 'fixed-load-a', logicalRequests: 12, concurrency: 3, inputShape: 'fixed-read-shape-a' }, baseline: { id: 'baseline-fixed-a', sameCohort: true, sameLoad: true, effectClaim: 'not-made' },
spans: [{ id: 'root-04', parentId: null, kind: 'SERVER', name: 'fixed-gateway', start: 0, end: 1000, role: 'end-to-end' }, { id: 'opaque-04', parentId: 'root-04', kind: 'INTERNAL', name: 'fixed-unclassified-delay', start: 40, end: 560, role: 'unknown-delay' }, { id: 'ext-04', parentId: 'root-04', kind: 'CLIENT', name: 'fixed-catalog-call', start: 730, end: 930, role: 'external-dependency' }],
decision: { proposedChange: 'none', controlBoundary: 'same fixed cohort, load and trace shape', observedOnly: true }, boundary: 'negative fixed literal',
},
'unsupported-effect-v1': {
id: 'unsupported-effect-v1', family: 'fixed-system-performance-v1', trace: { traceId: 'fixed-trace-05', rootSpanId: 'root-05', complete: true, correlation: 'one named fixed synthetic trace' },
load: { cohort: 'fixed-load-a', logicalRequests: 12, concurrency: 3, inputShape: 'fixed-read-shape-a' }, baseline: { id: 'baseline-fixed-a', sameCohort: true, sameLoad: true, effectClaim: 'faster-after-change' },
spans: [{ id: 'root-05', parentId: null, kind: 'SERVER', name: 'fixed-gateway', start: 0, end: 900, role: 'end-to-end' }, { id: 'queue-05', parentId: 'root-05', kind: 'INTERNAL', name: 'fixed-admission-queue', start: 40, end: 510, role: 'queue-wait' }, { id: 'db-05', parentId: 'root-05', kind: 'CLIENT', name: 'fixed-db-call', start: 520, end: 670, role: 'database-execution' }],
decision: { proposedChange: 'reduce-fixed-db-step', controlBoundary: 'same fixed cohort, load and trace shape', observedOnly: false }, boundary: 'negative fixed literal',
},
});
function known(value) { return Object.values(FIXED_PERFORMANCE_INPUTS).some((candidate) => JSON.stringify(candidate) === JSON.stringify(value)); }
function stopped(status, reason, nextAction) { return deepFreeze({ accepted: false, status, reasons: [reason], nextAction, handoff: 'not-eligible-for-synthetic-performance-review', productionEffect: 'not-attempted' }); }
export function createFixedPerformanceReviewInput(id = 'comparable-critical-path-v1') { return Object.hasOwn(FIXED_PERFORMANCE_INPUTS, id) ? deepFreeze(cloneFixed(FIXED_PERFORMANCE_INPUTS[id])) : deepFreeze({ id, boundary: 'unknown input is not a fixture scenario' }); }
export function reviewFixedPerformanceInput(input) {
if (!known(input)) return stopped('stop-unknown-fixed-input', 'input-is-not-a-known-fixed-literal', 'select-a-named-fixture-input');
if (input.trace.complete !== true || !input.spans.every((span) => Number.isFinite(span.start) && Number.isFinite(span.end) && span.end >= span.start && (span.parentId === null || input.spans.some((parent) => parent.id === span.parentId)))) return stopped('stop-incomplete-trace', 'trace-is-not-a-complete-connected-timed-tree', 'restore-the-missing-fixed-span-or-parent');
if (!input.baseline.sameCohort || !input.baseline.sameLoad || input.load.cohort !== 'fixed-load-a' || input.load.logicalRequests !== 12 || input.load.concurrency !== 3 || input.load.inputShape !== 'fixed-read-shape-a') return stopped('stop-incomparable-load', 'baseline-and-candidate-do-not-share-the-fixed-load-boundary', 'align-the-named-fixed-cohort-load-and-shape');
if (!input.spans.some((span) => span.role === 'queue-wait') || input.spans.some((span) => span.role === 'unknown-delay')) return stopped('stop-hidden-queue', 'waiting-is-not-separated-from-execution-in-the-fixed-trace', 'name-a-queue-wait-span-before-attribution');
if (input.baseline.effectClaim !== 'not-made' || input.decision.observedOnly !== true || input.decision.proposedChange !== 'none') return stopped('stop-unsupported-effect', 'the-input-claims-an-effect-without-a-fixed-control-comparison', 'record-an-observation-and-keep-the-effect-unclaimed');
const root = input.spans.find((span) => span.id === input.trace.rootSpanId);
const ranked = input.spans.filter((span) => span.id !== root.id).map((span) => ({ name: span.name, role: span.role, durationUnits: span.end - span.start })).sort((a, b) => b.durationUnits - a.durationUnits);
return deepFreeze({ accepted: true, status: 'synthetic-performance-observation-ready', reasons: [], traceId: input.trace.traceId, endToEndUnits: root.end - root.start, rankedObservedSegments: ranked, conclusion: 'fixed queue-wait is the longest observed named segment; this is not an optimization claim', handoff: 'synthetic-performance-review', productionEffect: 'not-attempted' });
}
export function compareFixedPerformanceInputs(leftId, rightId) {
const left = createFixedPerformanceReviewInput(leftId); const right = createFixedPerformanceReviewInput(rightId);
const leftReview = reviewFixedPerformanceInput(left); const rightReview = reviewFixedPerformanceInput(right);
if (!leftReview.accepted || !rightReview.accepted) return deepFreeze({ accepted: false, status: 'stop-comparison-input', reasons: [...leftReview.reasons, ...rightReview.reasons], handoff: 'not-eligible-for-synthetic-performance-review', productionEffect: 'not-attempted' });
return deepFreeze({ accepted: true, status: 'synthetic-comparison-ready', reasons: [], comparison: 'same named fixed boundary only', handoff: 'synthetic-performance-review', productionEffect: 'not-attempted' });
}
export function runFixedPerformanceFixture() {
const expectations = [
['comparable-critical-path-v1', true, 'synthetic-performance-observation-ready'], ['incomplete-trace-v1', false, 'stop-incomplete-trace'], ['incomparable-load-v1', false, 'stop-incomparable-load'], ['hidden-queue-v1', false, 'stop-hidden-queue'], ['unsupported-effect-v1', false, 'stop-unsupported-effect'],
];
const checks = expectations.map(([id, accepted, status]) => { const result = reviewFixedPerformanceInput(createFixedPerformanceReviewInput(id)); return result.accepted === accepted && result.status === status; });
checks.push(reviewFixedPerformanceInput(createFixedPerformanceReviewInput('unknown-v1')).status === 'stop-unknown-fixed-input');
checks.push(compareFixedPerformanceInputs('comparable-critical-path-v1', 'comparable-critical-path-v1').accepted === true);
checks.push(compareFixedPerformanceInputs('comparable-critical-path-v1', 'incomplete-trace-v1').accepted === false);
return deepFreeze({ passed: checks.filter(Boolean).length, total: checks.length, accepted: checks.every(Boolean), checks });
}
const REFERENCES_LIST = [
{ ...REFERENCES.traceContext, title: 'W3C Trace Context', version: 'Recommendation, 23.11.2021, immutable publication', use: 'Фиксирует формат traceparent и смысл trace-id/parent-id для связности распределённой trace.', boundary: 'Опираемся только на идентификацию и связь; не выводим производительность из стандарта.' },
{ ...REFERENCES.otel, title: 'OpenTelemetry Specification Trace API', version: 'v1.29.0, commit c6520a7, 11.01.2024', use: 'Задаёт различение CLIENT, SERVER, PRODUCER и CONSUMER span как описательных ролей.', boundary: 'Роль span не доказывает причину задержки и не является метрикой.' },
{ ...REFERENCES.http, title: 'RFC 9110: HTTP Semantics', version: 'RFC 9110, June 2022', use: 'Определяет HTTP как stateless application-level protocol и отделяет протокольную семантику от измерения времени.', boundary: 'Не используем RFC для обещаний latency, capacity или реального поведения системы.' },
];
const practiceExample = "import { createFixedPerformanceReviewInput, reviewFixedPerformanceInput } from './upgrade-2026-05.mjs';\n\nconst review = reviewFixedPerformanceInput(\n createFixedPerformanceReviewInput('comparable-critical-path-v1'),\n);\n\nconsole.log({\n accepted: review.accepted,\n endToEndUnits: review.endToEndUnits,\n longestObserved: review.rankedObservedSegments[0],\n handoff: review.handoff,\n});";
const mechanismExample = "import { createFixedPerformanceReviewInput, reviewFixedPerformanceInput } from './upgrade-2026-05.mjs';\n\nfor (const id of ['hidden-queue-v1', 'incomparable-load-v1']) {\n const result = reviewFixedPerformanceInput(createFixedPerformanceReviewInput(id));\n console.log({ id, accepted: result.accepted, reason: result.reasons[0] });\n}";
const fieldExample = "import { createFixedPerformanceReviewInput, reviewFixedPerformanceInput } from './upgrade-2026-05.mjs';\n\nconst result = reviewFixedPerformanceInput(\n createFixedPerformanceReviewInput('unsupported-effect-v1'),\n);\n\nconsole.log({\n accepted: result.accepted,\n status: result.status,\n nextAction: result.nextAction,\n productionEffect: result.productionEffect,\n});";
const practiceContent = [
p('У сервиса можно уменьшить локальный шаг и не сдвинуть end-to-end latency. Цена ошибки — неделя оптимизации, после которой пользовательский путь остаётся прежним, а команда получает красивую, но бесполезную цифру. В этой статье не ищем «самый медленный сервис»: составляем критический путь одной именованной synthetic trace и останавливаемся до решения об оптимизации.'),
p('Точка входа — fixed literal <code>comparable-critical-path-v1</code>. В нём 1&nbsp;000 условных единиц от входа до ответа: очередь занимает 520, вызов БД — 150, внешний каталог — 200. Единицы намеренно не миллисекунды и не production metric. Они нужны только чтобы проверить дисциплину: сначала одинаковая граница, затем наблюдение, затем hand-off.'),
h2('Почему локальная скорость не равна скорости пути'),
p('Critical path — не сумма всех span и не рейтинг компонентов. Это цепь на одном пути от root span до ответа, где каждая задержка имеет родителя, начало, конец и роль. Если у задержки нет связи с root, она может быть соседним событием. Если у ожидания нет отдельного имени, его нельзя честно переложить на БД или внешний вызов.'),
table('Synthetic waterfall: что можно наблюдать в одном фиксированном пути', ['Сегмент', 'Роль', 'Интервал', 'Длительность', 'Допустимый вывод'], [['fixed-admission-queue', 'queue-wait', '40–560', '520', 'длиннейший названный наблюдаемый сегмент'], ['fixed-db-call', 'database-execution', '570–720', '150', 'исполнение БД в этой synthetic trace'], ['fixed-catalog-call', 'external-dependency', '730–930', '200', 'внешний вызов в этой synthetic trace'], ['fixed-gateway', 'end-to-end', '0–1 000', '1 000', 'граница пути, не причина задержки']]),
figure('/assets/editorial/2026/systems-performance-2026-critical-path-waterfall.svg', 'Waterfall одной синтетической trace: очередь, БД и внешний вызов внутри end-to-end границы', 'Рисунок 1. Не масштаб производительности, а порядок проверки: увидеть очередь отдельно от исполнения.'),
h2('Процедура без преждевременной оптимизации'),
ol(['Назвать одну synthetic trace и root span. Не смешивать несколько логических запросов в «средний путь».', 'Проверить дерево: у каждого дочернего span существует parent, у интервала есть начало и конец, root покрывает путь.', 'Разделить ожидание, исполнение БД и внешний вызов. <em>Неизвестная задержка</em> — это остановка, а не поле для догадки.', 'Зафиксировать нагрузочную границу: cohort, число логических запросов, concurrency и форма входа.', 'Ранжировать только названные сегменты; передать review как наблюдение. Не писать «ускорили», пока нет сопоставимого контроля.']),
h2('Буквально исполнимый synthetic пример'),
p('Экспорт не читает trace, не вызывает сеть и не измеряет сервис. Он возвращает только review по встроенному литералу. Результат намеренно говорит <code>observation-ready</code>, а не «готово к rollout».'),
code(practiceExample),
h2('Как читать результат'),
p('Первое место у <code>fixed-admission-queue</code> означает ровно одно: в этой заданной записи ожидание длиннее двух других именованных сегментов. Это не доказательство того, что очередь является системным bottleneck при другой нагрузке, и не инструкция переписать gateway. Следующий шаг — сформировать узкий performance review с границей и контрпримером.'),
h2('Что не включать в critical path'),
p('Не включайте в путь красивую панель, усреднённый показатель или span из соседнего trace-id. Они могут дать гипотезу, но не меняют порядок интервалов в <code>fixed-trace-01</code>. Особенно опасен «средний database time»: он может принадлежать другой операции, тогда как данный root уже ждёт admission queue. Водопад полезен ровно тем, что удерживает разговор возле одного логического запроса.'),
p('Не складывайте дочерние длительности, если они пересекаются. В synthetic input интервалы записаны так, что очередь, БД и каталог идут последовательно; поэтому их удобно читать как составные части пути. В иной модели параллельные вызовы требуют другой проверки: кандидат на critical path выбирается по временной зависимости, а не по арифметической сумме. Фикстура не делает этого выбора и не притворяется универсальным трассировщиком.'),
h2('Минимальная карточка hand-off'),
table('Поля, без которых наблюдение нельзя передать дальше', ['Поле', 'Почему требуется', 'Значение в упражнении'], [['trace identity', 'защищает от склейки разных запросов', 'fixed-trace-01'], ['root boundary', 'показывает, что именно означает end-to-end', '0–1 000 units'], ['named wait', 'не позволяет назвать ожидание исполнением', 'fixed-admission-queue'], ['load boundary', 'запрещает эффект без контроля', 'fixed-load-a / 12 / 3 / shape-a'], ['claim', 'отделяет факт от решения', 'effect claim: not-made']]),
p('Эта карточка сознательно не содержит владельца «виновного сервиса» и ожидаемой выгоды. Сначала она делает исходные данные проверяемыми. Если рецензент не видит parent, классификации или границы нагрузки, правильный ответ — вернуть запись на уточнение. Это не задержка работы; это защита от работы по неверной поверхности.'),
h2('Разбор fixed waterfall по порядку'),
p('Root <code>fixed-gateway</code> начинается в 0 и заканчивается в 1&nbsp;000. Это рамка, а не расходный элемент. Затем выделен <code>fixed-admission-queue</code>: 40–560. Внутри упражнения он означает ожидание до начала последующей работы. Такой span не обязан существовать во всякой реализации; именно поэтому отсутствие отдельного имени закрывает review. После него расположены <code>fixed-db-call</code> 570–720 и <code>fixed-catalog-call</code> 730–930. Промежутки 560–570 и 720–730 не объясняются и не превращаются в скрытую причину: они остаются промежутками литерала.'),
p('Из этой последовательности нельзя получить число «ускорения БД, которое улучшит путь на 15 процентов». Для него нет варианта input с изменённой БД и тем же контролем. Но можно заметить другое: уменьшение исполнения БД в изоляции имеет верхнюю видимую границу 150 условных единиц в данном пути, тогда как ожидание уже занимает 520. Это не прогноз, а способ не выбирать первую работу только потому, что она находится в знакомом сервисе.'),
p('В реальном обсуждении команда нередко начинает с самой доступной части: SQL, кеша или кода gateway. В synthetic review доступность изменения не заменяет evidence. Сначала автор выписывает самый длинный наблюдаемый сегмент и все условия, которые позволяют его так назвать. Затем владелец потенциального изменения может принести новый, отдельно проверяемый fixed input. Только после этого можно обсуждать, какую гипотезу он изолирует.'),
h2('Границы критического пути'),
p('Критический путь не обязан быть полным объяснением latency. Он отвечает на более узкий вопрос: какие именованные интервалы лежат в одном пути и какой из них длиннее в данном наблюдении. Если операция происходит параллельно внешнему вызову, её полная длительность может не добавляться к root. Если клиент повторил запрос вне известного root, это уже другой логический путь. Если очередь существует, но не имеет span, её нельзя восстановить по пустоте между отметками.'),
p('Поэтому таблица waterfall — не журнал «куда ушло всё время». Она — контракт на терминологию. Слово queue-wait используется только там, где литерал сам это указывает. Слово database-execution не расширяется до «БД виновата». Слово external-dependency означает границу вызова, а не качество внешнего поставщика. Такой словарь заметно сокращает спор: рецензенты сначала спорят о недостающем поле, а не о характере системы.'),
h2('Вопросы до любой задачи на ускорение'),
table('Вопрос', ['Проверка', 'Если ответ «нет»', 'Корректное действие'], [['Путь один и связан?', 'root и все parent существуют', 'нельзя назвать critical path', 'вернуть incomplete trace'], ['Ожидание отдельно?', 'есть queue-wait, а не opaque delay', 'невозможно отделить очередь от работы', 'запросить классификацию'], ['Нагрузка та же?', 'cohort, requests, concurrency, shape совпали', 'нет контрольной границы', 'не сравнивать интервалы'], ['Эффект не придуман?', 'claim = not-made', 'вывод сильнее evidence', 'снять claim и оставить observation']]),
p('Этот набор вопросов кажется медленным лишь до первой неверной оптимизации. Он не запрещает исследовать БД, внешний каталог или admission policy. Он запрещает выдавать исследование за доказанный end-to-end эффект. В пакете P99 положительным результатом считается не меньший root, а аккуратно оформленный hand-off для следующего review.'),
p('Практический результат waterfall не в том, что он называет один «самый важный» блок. Он делает цену следующего шага наблюдаемой: пока нет контрольной границы, любой рефакторинг остаётся отдельной гипотезой с неизвестным влиянием на путь. Поэтому в карточке полезно оставить место для вопроса, который ещё не решён, вместо того чтобы закрыть его словом «оптимизация». В нашем случае вопрос звучит: как сформировать следующий fixed input, не потеряв queue-wait и границу нагрузки?'),
p('До этого момента никакая команда не обязана соглашаться с причиной задержки. Она обязана согласиться только с тем, какие поля были проверены и какие поля ещё отсутствуют.'),
h2('Ограничения и следующий шаг'),
p('Synthetic trace не содержит распределения, хвостов, реального таймера, вариации входа или причинности. W3C Trace Context описывает перенос идентификаторов, а не полноту данных; OpenTelemetry задаёт роли, а не экономический эффект. Поэтому корректный hand-off: «в fixed input queue-wait — самый длинный названный сегмент; эффект изменения не заявлен». Следующая статья проверит, почему похожий waterfall может оказаться несопоставимым.'),
h2('Проверяемые источники'), sourceList(REFERENCES_LIST),
].join('');
const mechanismContent = [
p('Проблема начинается с фразы «БД медленная», когда trace показывает только длинный промежуток. Цена этой подмены — оптимизация исполнения при реальном ожидании в очереди либо при внешнем вызове. Механика performance review начинается не с percentiles и не с profiler: она требует разделить ожидание, работу и зависимость в одной сопоставимой synthetic trace.'),
p('Здесь три независимые оси: роль сегмента, нагрузочная граница и вывод. Роль отвечает, что именно названо в литерале; нагрузка — можно ли поставить рядом baseline; вывод — какое утверждение допускает доказательство. Если оси смешать, короткий root span при иной concurrency легко выдают за ускорение.'),
h2('Три вида времени, которые нельзя складывать в одну причину'),
p('Queue wait — время до начала работы в названной admission queue. Database execution — интервал вызова БД в synthetic trace. External dependency — интервал исходящего вызова каталога. Их длительности можно расположить рядом; причинный ярлык переносить нельзя. CLIENT и SERVER из OpenTelemetry — описательные kinds, а не автоматическая классификация bottleneck.'),
table('От наблюдения к допустимому высказыванию', ['Наблюдение в fixed literal', 'Чего этого недостаточно доказать', 'Разрешённая формулировка', 'Стоп-причина'], [['queue-wait 520 units', 'что очередь замедляет production', 'ожидание — длиннейший названный сегмент данного input', 'нет иной нагрузки'], ['db-call 150 units', 'что SQL надо оптимизировать', 'исполнение БД занимает 150 units в этой записи', 'нет причины'], ['root 800 вместо 1 000', 'что изменение ускорило путь', 'другой input имеет другой интервал', 'different cohort/load'], ['unclassified-delay 520', 'что это очередь', 'задержка не классифицирована', 'hidden queue']]),
figure('/assets/editorial/2026/systems-performance-2026-load-latency-curve.svg', 'Схема двух synthetic нагрузочных точек и latency: несопоставимое сравнение перечёркнуто', 'Рисунок 2. Разная нагрузка и форма входа не образуют контрольную границу, даже если второй интервал короче.'),
h2('Проверка сопоставимости'),
p('Для допуска к сравнению литерал хранит cohort <code>fixed-load-a</code>, 12 logical requests, concurrency 3 и <code>fixed-read-shape-a</code>. Это не универсальный набор метрик. Это минимальный контракт конкретного exercise. Смена хотя бы одного поля переводит результат в <code>stop-incomparable-load</code>, потому что разница может принадлежать нагрузке, а не предполагаемому изменению.'),
h2('Fail-closed вместо правдоподобной догадки'),
p('У <code>hidden-queue-v1</code> интервал 40–560 специально помечен <code>unknown-delay</code>. Он выглядит как очередь, но функция останавливается: <code>stop-hidden-queue</code>. У <code>incomplete-trace-v1</code> дочерний span ссылается на отсутствующего parent — функция возвращает <code>stop-incomplete-trace</code>. Так review фиксирует недостающий факт, а не превращает форму диаграммы в диагноз.'),
code(mechanismExample),
h2('Длительность — не причинность'),
p('Длинный span говорит, что интервал записан длинным в конкретном литерале. Он не говорит, почему интервал получился длинным и какое изменение его сократит. Очередь может быть следствием выбранной дисциплины, ограниченного ресурса или искусственно заданного сценария; БД может ждать сеть; внешний вызов может включать локальную подготовку. Даже в одинаковой trace роль и причинный механизм — разные утверждения. Функция намеренно не принимает поле «cause», потому что fixture его не доказывает.'),
p('Полезный разрыв выглядит так: наблюдение — <code>queue-wait = 520</code>; гипотеза — «admission rule создаёт ожидание»; проверка гипотезы — другой заранее определённый synthetic literal с тем же контролем; эффект — только сравнение разрешённых результатов. Нельзя перескочить с первого пункта к четвёртому. Чем ярче диаграмма, тем важнее сделать этот разрыв видимым в записи.'),
p('Тот же принцип работает для БД. <code>database-execution = 150</code> — не SQL diagnosis, не индекс и не бюджет. Это значение в условной шкале конкретного input. Оно может стать исходной точкой для отдельного вопроса, но в текущем пакете не получает рекомендаций. Такая неполнота является свойством качественного review: он не выдаёт отсутствующие причины за измеренные.'),
h2('Связность, вложенность и параллельность'),
p('Complete tree проверяет минимальную структурную вещь: у дочернего span есть существующий parent, а время не идёт назад. Этого недостаточно, чтобы автоматически вычислить все возможные критические ветви. Мы не строим общий алгоритм распределённой трассировки и не интерпретируем overlap. В fixture интервалы заданы последовательно, потому что задача учебного review — показать разделение типов задержки, а не спрятать причинную ошибку за сложным графом.'),
p('Если два CHILD span пересеклись во времени, их нельзя бездумно сложить. Если один дочерний интервал выходит за root, это нарушение границы, а не «дополнительная производительность». Если producer и consumer разделены очередью, идентификаторы могут связывать события, но сам факт связи ещё не определяет время ожидания. W3C и OpenTelemetry помогают назвать связь и role; содержательный вывод всё равно требует явного measurement contract.'),
h2('Почему короткий root не побеждает сам по себе'),
p('В <code>incomparable-load-v1</code> root равен 800, то есть визуально лучше 1&nbsp;000. Одновременно input меняет cohort, число logical requests, concurrency и input shape. Нельзя выбрать один из этих факторов как объяснение, потому что литерал не изолирует его. Возврат <code>stop-incomparable-load</code> не отрицает 800; он запрещает называть разницу эффектом предполагаемой оптимизации.'),
p('Контрольная граница не обязана быть большой статистической процедурой, чтобы быть полезной. Для данного пакета она предельно узка и проверяется равенством именованных полей. Это делает пример воспроизводимым и честным. В будущей системе граница может быть богаче, но принцип не меняется: сначала определить, какие условия должны совпасть, затем решить, допускает ли запись сравнительное высказывание.'),
p('Механика намеренно предпочитает отказ частичному ответу. Удобный, но неполный trace создаёт больше риска, чем отсутствие цифры: его легко включить в убедительный narrative. Named stop reason сохраняет контекст недостающего условия и делает повторный review дешевле. В этой модели отказ не является ошибкой функции; он и есть корректный результат для входа, который нельзя безопасно интерпретировать.'),
p('Поэтому `stop` должен быть виден и в коде, и в тексте, и в таблице, а не прятаться в комментарии к графику.'),
h2('Зачем нужна единая trace, а не три полезных фрагмента'),
p('Отдельный лог БД, внешний таймер и очередь из другой записи могут быть качественными артефактами, но не составляют критический путь. В W3C trace context trace-id предназначен для идентификации распределённой trace; parent-id связывает операцию с родителем. В нашем узком упражнении это переводится в проверяемое дерево литералов. Отсутствующий link — запрет на склейку, а не повод подобрать наиболее похожее число.'),
h2('Нагрузка не является фоном'),
p('Logical requests, concurrency и input shape здесь не «подписи к графику». Это условия, при которых одно synthetic наблюдение разрешено поставить рядом с baseline. Рост logical requests с 12 до 24 и concurrency с 3 до 6 меняет сам вопрос: короткий root в <code>incomparable-load-v1</code> не говорит, улучшился ли тот же путь. Он говорит лишь, что задан другой литерал.'),
p('Полезная привычка — писать контрольную границу раньше результата. Тогда фраза «root стал короче» сразу вызывает проверку: тот же cohort? те же logical requests? та же форма входа? Если хотя бы на один вопрос нет точного ответа, результат следует записать как отдельное наблюдение. Не нужно подбирать коэффициент, нормировку или убедительное объяснение, чтобы снять этот запрет.'),
ol(['Сначала проверить связность одного synthetic дерева и интервалы каждого named span.', 'Затем выписать load boundary до просмотра отличий root interval.', 'Потом отделить queue-wait от database execution и external dependency.', 'Только после этих проверок оформить наблюдение или вернуть точную stop reason.']),
h2('Что именно проверяет функция'),
p('Функция не вычисляет throughput и не моделирует scheduler. Она проверяет структуру аргумента: complete tree, названную queue-wait, фиксированную нагрузочную границу и отсутствие effect claim. После этого она ранжирует уже имеющиеся условные интервалы. Такой порядок полезен для review: сначала валидность материала, затем интерпретация, но никогда не обратная последовательность.'),
table('Fail-closed ветви механики', ['Условие', 'Статус', 'Почему не продолжаем'], [['дочерний span без parent', 'stop-incomplete-trace', 'путь нельзя собрать'], ['cohort или concurrency иные', 'stop-incomparable-load', 'нет общей границы'], ['длинный интервал unknown-delay', 'stop-hidden-queue', 'ожидание не отделено от работы'], ['заявлен faster-after-change', 'stop-unsupported-effect', 'нет доказанного эффекта']]),
h2('Граница утверждения'),
p('Даже complete и comparable input не утверждает, что найден bottleneck исправлен. Он разрешает один hand-off: «при заданных literals очередь — первая наблюдаемая кандидатная граница исследования». Рядом должны стоять условия сравнения и контрпример: при неизвестном ожидании или иной concurrency вывод отменяется. Это дешевле, чем спор о точности десятичного знака у несопоставимых наблюдений.'),
h2('Проверяемые источники'), sourceList(REFERENCES_LIST),
].join('');
const fieldContent = [
p('Проблема performance review появляется, когда короткий интервал объявляют эффектом изменения. Цена — принятие решения, которое невозможно воспроизвести: исчезла ли задержка, изменилась ли нагрузка, или сравнили разные пути? Полевая дисциплина здесь скромнее: собрать evidence, потребовать контрпример и передать решение только как synthetic hand-off.'),
p('В этой статье нет real benchmark и нет обещания улучшения. Есть один положительный исход — корректно оформленная запись для review. Она показывает, что наблюдали, какую границу контролировали, что могло опровергнуть вывод и почему production effect остаётся <code>not-attempted</code>.'),
h2('Цикл evidence → контрпример → решение'),
p('Evidence — complete timed tree с отдельным queue-wait. Контрпример — литерал, нарушающий одно из условий: отсутствующий parent, другая нагрузка, неразмеченное ожидание или готовая фраза «стало быстрее». Решение — либо принять наблюдение к review, либо назвать конкретную stop reason. Важная деталь: решение не выбирает оптимизацию; оно выбирает, достаточно ли материала для следующего вопроса.'),
figure('/assets/editorial/2026/systems-performance-2026-bottleneck-evidence-loop.svg', 'Цикл performance review: evidence, проверка control boundary, контрпример, hand-off или stop reason', 'Рисунок 3. Решение идёт не от яркого графика к оптимизации, а от наблюдения к проверяемой границе и только затем к hand-off.'),
table('Карточка решения для synthetic performance review', ['Поле', 'Содержимое в accepted input', 'Пример отказа', 'Следующее действие'], [['След trace', 'fixed-trace-01, complete tree', 'parent отсутствует', 'восстановить named span'], ['Контрольная граница', 'fixed-load-a; 12; 3; fixed-read-shape-a', '24; 6; fixed-read-shape-b', 'не сравнивать'], ['Кандидатный сегмент', 'fixed-admission-queue, 520 units', 'unclassified-delay', 'назвать очередь или оставить unknown'], ['Эффект', 'not-made', 'faster-after-change', 'снять claim и записать наблюдение']]),
h2('Как формулировать запись'),
p('Хорошая запись коротка: «В <code>fixed-trace-01</code> при fixed-load-a queue-wait — 520 из 1&nbsp;000 units и длиннее двух других названных сегментов. Сравнение и effect claim не выполнялись. Handoff: synthetic-performance-review». Она не обещает latency, не приписывает причину и не маскирует пробел словом «вероятно».'),
p('Плохая запись звучит убедительнее: «после уменьшения DB path система стала быстрее». В <code>unsupported-effect-v1</code> ей соответствует смена root interval и <code>faster-after-change</code>, но отсутствует допустимый контрольный результат. Функция обязана вернуть stop; иначе review легализует вывод по одному наблюдению.'),
h2('Буквально исполнимый отказ'),
code(fieldExample),
h2('Роли на review'),
p('Автор материала отвечает за именованные literals и отсутствие скрытой сети. Рецензент trace проверяет связность и классификацию ожидания. Рецензент решения проверяет, что baseline имеет ту же нагрузочную границу и что заголовок не сильнее evidence. Эти роли не делают текст тяжёлым: они отсекают три дешёвые ошибки до обсуждения архитектуры.'),
h2('Шаблон решения без ложной уверенности'),
p('Запись review удобно разбить на четыре строки. <em>Evidence</em>: какой fixed input прочитан и какие интервалы в нём названы. <em>Boundary</em>: cohort, requests, concurrency и shape, с которыми разрешено сравнение. <em>Counterexample</em>: какой named fixture отменит текущий вывод. <em>Decision</em>: hand-off или stop reason. В такой форме автор не может незаметно заменить факт на план, а рецензент может указать конкретное отсутствующее звено.'),
p('Для accepted input решение будет: evidence — complete <code>fixed-trace-01</code>; boundary — fixed-load-a/12/3/shape-a; counterexample — unknown-delay или иной load; decision — <code>synthetic-performance-review</code>. Здесь нет строки «оптимизировать очередь». Если участник review хочет добавить её, он должен принести отдельное evidence и явно сформулировать новый вопрос. Это удерживает текущую заметку в роли проверки, а не скрытого технического решения.'),
h2('Как рецензировать заголовок и вывод'),
p('Заголовок может обещать только то, что читатель найдёт в evidence. В P99 допустимы «как не оптимизировать до границы» и «performance review без обещаний», потому что текст описывает стоп-условия. Недопустимы «как ускорить систему» или «найти bottleneck за минуту»: у них сильнее модальность, чем у fixture. Эта проверка особенно важна для field article, где уверенная редакционная фраза легко воспринимается как production guidance.'),
p('Проверка вывода ещё проще: заменить все технические слова на поля литерала. Если остаётся предложение, которого нельзя указать пальцем в input или в явном status, его нужно ослабить либо удалить. «Queue-wait — длиннейший named segment» выживает. «Очередь является корнем проблемы» — нет. «Изменение БД ускорит путь» — нет. Такой self-review занимает минуты и часто обнаруживает главный дефект до публикации.'),
h2('Пределы безопасной эскалации'),
p('Handoff передаёт вопрос, а не право менять систему. Получатель может решить, что для следующего synthetic exercise нужно добавить литерал со строго одинаковым load boundary и одной изменённой характеристикой. Он также может отклонить задачу, если классификация ожидания недостаточна. Обе реакции корректны. Некорректно превратить accepted fixture в реальный benchmark, включить клиент или читать метрики: эти действия явно вне boundary пакета.'),
p('Именно поэтому в каждом результате присутствует <code>productionEffect: not-attempted</code>. Это не декоративное поле. Оно делает ограничение машинно видимым в примере и редакционно видимым в статье. Даже если future reader согласен с гипотезой, он не получит из этого кода разрешение на rollout, гарантию latency или утверждение о фактическом пользователе.'),
p('Хороший field review завершает спор там, где заканчиваются данные. Он не требует искусственного консенсуса о причине и не записывает отсутствие информации как риск «на потом». Вместо этого он оставляет проверяемый след: вход, проверенные условия, точную стоп-причину или узкий hand-off. Такой след можно перечитать через месяц без доступа к окружению, потому что он не опирается на память автора, скрытые панели или состояние реального сервиса.'),
p('Итоговый критерий прост: другой рецензент должен получить тот же status, запустив public export с тем же named literal. Если для понимания нужны устные пояснения, внешний дашборд или догадка о том, что автор имел в виду, пакет не готов. P99 оставляет ровно тот объём информации, который можно проверить локально и передать без расширения утверждения.'),
p('Это ограничение сохраняется и после публикации: текст объясняет процедуру, но не меняет природу evidence и не создаёт дополнительного доказательства.'),
h2('Чек перед hand-off'),
ol(['Есть один root и complete parent tree; длительности — условные значения фиксированного input.', 'Очередь отделена от БД и внешней зависимости или явно помечена unknown.', 'Baseline и candidate имеют одинаковые cohort, logicalRequests, concurrency и input shape.', 'В тексте нет «ускорили», «bottleneck устранён», «готово к rollout» или другого effect claim.', 'Выход функции — synthetic review hand-off либо именованный stop, никогда не production recommendation.']),
h2('Как использовать контрпример в разговоре'),
p('Контрпример не обязан опровергать всю систему. Достаточно разрушить одно звено конкретного вывода. Для <code>incomplete-trace-v1</code> отсутствующий parent не позволяет говорить о critical path. Для <code>hidden-queue-v1</code> неизвестная задержка не позволяет говорить «очередь». Для <code>incomparable-load-v1</code> смена cohort отменяет сравнение. Эти отказы точнее, чем общий комментарий «нужно больше данных».'),
p('Рецензенту полезно возвращать именно статус и следующее действие. Например, <code>stop-unsupported-effect</code> просит снять claim и оставить observation; он не доказывает обратное и не спорит с автором о реализации. Так контрпример становится частью рабочего протокола, а не риторическим препятствием. Когда условия восстановлены, тот же формат допускает повторный review.'),
h2('Почему hand-off остаётся узким'),
p('Даже accepted результат не назначает команду и не меняет конфигурацию. Он разрешает сохранить одну проверяемую мысль: где в фиксированном литерале расположен самый длинный названный сегмент. Всё остальное — причина, стоимость исправления, влияние на пользовательский путь — требует отдельной границы и отдельного материала. Узость результата делает его переносимым между рецензентами.'),
table('Результаты, которые нельзя подменять друг другом', ['Результат', 'Что означает', 'Чего не означает'], [['observation-ready', 'структура input пригодна для review', 'оптимизация доказана'], ['synthetic-performance-review', 'запись можно обсуждать', 'есть rollout'], ['stop reason', 'известен недостающий факт', 'система медленна'], ['not-attempted', 'production effect не рассматривался', 'эффекта нет в реальности']]),
h2('Ограничения и следующий шаг'),
p('Такая карточка не измеряет throughput, не отражает queue discipline, не оценивает хвост распределения и не заменяет эксплуатационную проверку. RFC 9110 описывает семантику HTTP, а не latency contract; стандарты trace описывают связь контекста, а не полноту наблюдений. Следующий безопасный шаг — расширить только fixed fixture новым контрпримером и снова проверить, что outcome остаётся review, не guarantee.'),
h2('Проверяемые источники'), sourceList(REFERENCES_LIST),
].join('');
export const revisions = [
{ slug: 'editorial-2026-05-practice-systems-performance', title: 'Критический путь: как не оптимизировать сервис до границы', excerpt: 'Synthetic waterfall, который отделяет очередь от БД и внешнего вызова — без обещания ускорения.', series: 'Практика системной инженерии', readingMinutes: 18, contentHtml: practiceContent },
{ slug: 'editorial-2026-05-mechanism-systems-performance', title: 'Время ожидания, время работы и ложное сравнение нагрузки', excerpt: 'Механика сопоставимой trace: какие задержки можно назвать и где review обязан остановиться.', series: 'Механика систем', readingMinutes: 19, contentHtml: mechanismContent },
{ slug: 'editorial-2026-05-field-systems-performance', title: 'Performance review без обещаний: evidence, контрпример, hand-off', excerpt: 'Полевая карточка решения, которая не превращает один короткий интервал в гарантию.', series: 'Полевые заметки', readingMinutes: 19, contentHtml: fieldContent },
];
export function verifyRevisionsAgainstFixture() {
const fixture = runFixedPerformanceFixture();
const articleChecks = revisions.map((revision) => { const text = bodyText(revision.contentHtml); return text.length >= 5000 && text.length <= 15000 && /(?:цена|Стоимость|ошибк)/i.test(text.slice(0, 900)) && /<table>/.test(revision.contentHtml) && /<figure>/.test(revision.contentHtml) && /<pre><code>/.test(revision.contentHtml); });
return deepFreeze({ passed: fixture.passed + articleChecks.filter(Boolean).length, total: fixture.total + articleChecks.length, accepted: fixture.accepted && articleChecks.every(Boolean), fixture, articleChecks, characters: Object.fromEntries(revisions.map((revision) => [revision.slug, bodyText(revision.contentHtml).length])) });
}
if (process.argv.includes('--verify-fixture')) {
const report = verifyRevisionsAgainstFixture(); console.log(JSON.stringify(report, null, 2)); if (!report.accepted) process.exitCode = 1;
}
if (process.argv.includes('--print-revisions')) console.log(JSON.stringify(revisions));