revise October 2021 D runtime articles
Build and deploy / deploy (push) Successful in 14s

This commit is contained in:
2026-07-31 13:15:12 +03:00
parent 8e078b92ea
commit f6058fbbca
7 changed files with 1053 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Производство редакционных партий
На 31 июля 2026 года строгий аудит проходит 133 из 358 созданных материалов. Остальные 225 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
На 31 июля 2026 года строгий аудит проходит 136 из 358 созданных материалов. Остальные 222 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия
+208
View File
@@ -0,0 +1,208 @@
# Автономное тройное ревью П44 · октябрь 2021 · «Runtime D в сервисе»
Статус: **готово к независимой интеграции**. Это автономный пакет для трёх
существующих slug. Он не подключён к registry: README, <code>articles.json</code>,
общие audit-скрипты, очередь, правила и Git не менялись.
Созданы ровно пять файлов:
- <code>web/scripts/upgrade-2021-10.mjs</code>;
- <code>web/public/assets/editorial/2021/d-runtime-request-path-2021.svg</code>;
- <code>web/public/assets/editorial/2021/d-runtime-allocation-profile-2021.svg</code>;
- <code>web/public/assets/editorial/2021/d-runtime-diagnosis-2021.svg</code>;
- этот документ.
Revision-модуль экспортирует только overlay-поля
<code>slug</code>, <code>title</code>, <code>categories</code>,
<code>cover</code>, <code>excerpt</code>, <code>readingMinutes</code> и
<code>contentHtml</code>. Отдельная проверка подтвердила отсутствие
<code>date</code> и <code>author</code> во всех трёх revision. Базовый архив
сохраняет их исходными.
## Проход 1. Тон, структура и объём — пройдено
| Revision | Проблема и цена в первых двух абзацах | M4 и практическая граница | Body |
| --- | --- | --- | --- |
| Practice | Endpoint объявляют «тормозящим из-за runtime D» без входа, result и границы; цена — случайная глобальная правка | Один request, явный result, budget и owner следующей проверки | **8 109** знаков |
| Mechanism | Слово «быстро» скрывает decode, validation, compute, encode и внешние границы; цена — оптимизация ломает error route или объясняет FFI GC-ом | Trace одного handler, одинаковый result двух вариантов, структура до измерения | **8 544** знака |
| Field | Heavy-ветка пересекает учебный budget и оставляет GC boundary; цена — назвать его паузой или обвинить dependency без вызова | Evidence → contract → units → отдельная граница → обратимый change | **9 633** знака |
- Во всех текстах есть ситуация и стоимость ошибки в начале, таблица,
содержательный <code>figure</code> с <code>alt</code>/<code>figcaption</code>,
исполнимый JS-фрагмент, нумерованный маршрут «симптом → причина → проверка
→ действие», ограничения и отдельный раздел источников.
- Голос М4 октября 2021: короткая техническая речь, одна граница на абзац,
условные выводы вместо выдуманного опыта. Новое умение логично продолжает
предыдущие материалы о данных и событиях: автор изолирует request path и
его контракт, прежде чем рассуждать о runtime.
- На первом автоматическом проходе draft gate указал, что practice и mechanism
не называли проблему буквальным словом в первых 800 знаках. Формулировки
исправлены. В том же self-review обнаружено расхождение: в prose и графике
было написано 11 work units при фактических 9. Все упоминания исправлены,
затем полный draft audit повторён с итогом PASS.
Вердикт: **пройдено**. Нет SLA, production-метрик, benchmark results,
реального профиля или заявления, что runtime сам по себе делает endpoint
быстрее.
## Проход 2. Техника, историческая точность и fixture — пройдено
Сквозная <code>runDRuntimeServiceFixture()</code> выполняется в одном
Node-процессе. Один valid in-memory request проходит
<code>decode → validation → compute → encode</code>; между compute и encode
trace явно сохраняет FFI/I/O boundaries c
<code>invocation: "not-performed"</code>. Invalid request проходит
<code>decode → validation-error → encode-error</code> и не доходит до
compute, FFI или I/O.
| Инвариант | Фактический результат fixture |
| --- | --- |
| Handler result одинаков | обе valid-ветки вернули status 200 и один body с <code>total:42</code> |
| Allocation budget видим | heavy: 12 units при budget 6, вне budget; reuse: 3 units, внутри budget |
| Работа handler не подменена оптимизацией | у valid-веток одинаковые 9 work units |
| GC не выдан за замер | heavy пересекает порог 8 и получает <code>gc-boundary</code> с <code>not-a-runtime-pause</code> |
| FFI/I/O boundary видимы | обе success-ветки содержат две boundaries, но ни одна не выполняет внешний вызов |
| Error route не скрыт | invalid input возвращает status 400 и имеет <code>validation-error</code>/<code>encode-error</code> |
Финальный запуск подтвердил **12 из 12** истинных assertions. Fixture не
запускает D compiler, D runtime, GC, HTTP, file, network, FFI, foreign code,
profiler, benchmark или внешний процесс. Его units не называются bytes,
CPU, latency, временем или паузой.
Исторические и технические claims ограничены первичными/официальными
источниками:
- [D 2.097.2: Automatic Memory Management source snapshot](https://github.com/dlang/dlang.org/blob/d5798c666479e8c4c918221e10ebf997b1f5f89f/spec/garbage.dd) —
неизменяемая терминология automatic memory management и ограничений
collector; в статьях это не превращается в профиль request;
- [D 2.097.2: @nogc function source snapshot](https://github.com/dlang/dlang.org/blob/d5798c666479e8c4c918221e10ebf997b1f5f89f/spec/function.dd) —
историческая граница проверяемого D-кода; атрибут не объявлен
доказательством свойств неизвестной внешней операции;
- [D 2.097.2: ABI source snapshot](https://github.com/dlang/dlang.org/blob/d5798c666479e8c4c918221e10ebf997b1f5f89f/spec/abi.dd) —
историческая граница D/C ABI целевой системы, не договор ownership или
стоимость particular foreign call;
- [D 2.097.2 release record](https://dlang.org/changelog/2.097.2.html) —
официальный выпуск от 9 августа 2021 года, существовавший до всех
октябрьских дат партии;
- [DRuntime core/memory.d at the 2.097.2 snapshot](https://github.com/dlang/druntime/blob/f978df34a48613492240e8398227419b14002bef/src/core/memory.d) —
immutable исходник для исторической проверки терминов.
Все пять ссылок получили HTTP 200 в link preflight. Первые три зафиксированы
на commit <code>d5798c666479e8c4c918221e10ebf997b1f5f89f</code>, на который
указывает тег D 2.097.2; снимок DRuntime отдельно зафиксирован на commit
<code>f978df34a48613492240e8398227419b14002bef</code>. Ни release number, ни
документация не используются как источник несуществующего benchmark,
прошлой pause или поведения воображаемого сервиса.
Вердикт: **пройдено**. Технические результаты относятся ровно к
детерминированной модели, а исторические формулировки не переносят текущую
документацию на незафиксированный запуск 2021 года.
## Проход 3. SVG, ссылки и preflight — пройдено
- В каждом SVG есть <code>title</code>, <code>desc</code> и
<code>role="img"</code>. Visuals поясняют разные вопросы: путь request,
сравнение units и маршрут rollback-safe диагностики.
- Первый mobile render выявил неясные боковые стрелки в request/diagnosis,
переполнение длинных footer-подписей и пересечение колонок в allocation
profile. Стрелки заменены последовательным маршрутом, подписи разбиты на
строки, столбцы разведены; финальный 375 px render просмотрен повторно.
- Static safety scan не нашёл <code>script</code>, <code>foreignObject</code>,
внешних <code>href/src</code> или raster <code>data:image</code>. Все три
файла проходят XML-проверку.
### Фактически выполненные команды
Проверки выполнены 31 июля 2026 года:
<pre><code>cd web &amp;&amp; node --check scripts/upgrade-2021-10.mjs
cd web &amp;&amp; npm run audit:draft -- scripts/upgrade-2021-10.mjs
cd web &amp;&amp; node scripts/upgrade-2021-10.mjs --verify-fixture
cd web &amp;&amp; xmllint --noout \
public/assets/editorial/2021/d-runtime-request-path-2021.svg \
public/assets/editorial/2021/d-runtime-allocation-profile-2021.svg \
public/assets/editorial/2021/d-runtime-diagnosis-2021.svg
cd web &amp;&amp; rg -n -i '&lt;script|&lt;foreignObject|(?:href|src)="https?://|data:image' \
public/assets/editorial/2021/d-runtime-request-path-2021.svg \
public/assets/editorial/2021/d-runtime-allocation-profile-2021.svg \
public/assets/editorial/2021/d-runtime-diagnosis-2021.svg</code></pre>
| Проверка | Реальный результат |
| --- | --- |
| <code>node --check</code> | PASS, code 0 |
| <code>audit:draft</code> | PASS: **8 109 / 8 544 / 9 633** знака; у всех трёх revision есть sections, table, figure, code, route, sources и локальный asset |
| Fixture | PASS, 12 из 12 assertions истинны |
| <code>xmllint --noout</code> | PASS, code 0 для всех трёх SVG |
| SVG safety scan | PASS: совпадений нет; <code>rg</code> завершился code 1 именно потому, что опасные конструкции отсутствуют |
| Link preflight | PASS: пять официальных/первичных ссылок вернули HTTP 200 |
| Sharp 375 px | PASS: три SVG отрендерены в PNG шириной 375 px и просмотрены вручную после исправления overflow/стрелок/пересечения |
| Overlay scope | PASS: у revision только разрешённые изменяемые поля; <code>date</code> и <code>author</code> отсутствуют |
<code>npm run audit:draft</code> завершилась с code 0. npm вывел
существующие предупреждения о пользовательских <code>store-dir</code>,
<code>cache-dir</code> и <code>public-hoist-pattern</code>; пакет их не
меняет.
Намеренно не запускались strict registry audit, production build, browser,
screen reader, D compiler/runtime, реальный profiler, HTTP, I/O, FFI, CI,
deployment, Git add/commit/push или публикация. Эти действия вне автономной
П44. Mascot PNG не добавлялись и не изменялись.
Выпусковой вердикт автономной партии:
**READY FOR INDEPENDENT INTEGRATION**.
## Независимое редакторское ревью и интеграция
Статус: **принято и подключено в editorial overlay 31 июля 2026 года**.
### 1. Факты и техника
- Независимая проверка выявила один исторический риск: первые три source URL
в исходной партии вели на текущую, изменяемую документацию D. Они заменены
на точные исходники официального репозитория <code>dlang/dlang.org</code>
в commit <code>d5798c666479e8c4c918221e10ebf997b1f5f89f</code>. Команда
<code>git ls-remote</code> подтвердила, что на этот commit указывает тег
<code>v2.097.2</code>. Это сохраняет доступное до октября 2021 состояние
терминов automatic memory management, <code>@nogc</code> и ABI.
- Отдельно подтверждено, что commit
<code>f978df34a48613492240e8398227419b14002bef</code> — dereferenced тег
<code>v2.097.2</code> репозитория DRuntime. Таким образом версия и оба
снимка не подменяются текущими исходниками.
- Повторный <code>node --check</code> прошёл. Draft audit подтвердил основной
объём: 8 109, 8 544 и 9 633 знака. Fixture выдал 12 из 12 истинных
assertions: оба valid-варианта вернули status 200 и один body; heavy имеет
12 units при budget 6, reuse — 3; у обоих 9 work units; invalid input
возвращает status 400 и не доходит до compute/FFI/I/O.
- После подключения overlay строгий адресный audit прошёл для всех трёх slug:
practice — 1 figure / 1 table / 1 code, mechanism — 1 / 1 / 1, field —
1 / 2 / 1. В registry 127 revision, в том числе ровно три октябрьские;
дубликатов slug нет. Overlay по-прежнему не содержит полей
<code>date</code> или <code>author</code>.
### 2. Редактура и голос
- Прочитаны все три текста после source correction. Они начинают с конкретной
ошибочной формулировки и стоимости, сохраняют различие между учебной
моделью и фактическим profile, дают отдельные error-path и rollback-safe
шаги. Нет заявлений о production latency, pause, benchmark, реальном D
runtime или выполненном внешнем вызове.
- Язык соответствует октябрю 2021: автор уже работает с границами handler,
конфигурации и внешнего договора, но не присваивает себе зрелый опыт
platform/incident leadership. Английские технические слова привязаны к
trace, коду или таблице; следующий шаг остаётся воспроизводимым.
### 3. Визуал и выпуск
- Три SVG повторно проверены через <code>xmllint --noout</code>, safety scan
не нашёл <code>script</code>, <code>foreignObject</code>, внешних URL или
<code>data:image</code>. Каждый SVG отрендерен Sharp в ширину 375 px и
просмотрен вручную: линии, подписи, контраст, последовательность блоков и
нижние предупреждения остаются читаемыми.
- <code>npm run build</code> завершилась с code 0: Next.js сгенерировал 374
статические страницы. <code>git diff --check</code> также прошёл. Обычные
предупреждения npm о пользовательских <code>store-dir</code>,
<code>cache-dir</code> и <code>public-hoist-pattern</code> не относятся к
этой партии и не повлияли на exit code.
Итог: октябрьские три статьи заменяют исходные архивные тексты через overlay;
после этой интеграции строгий аудит проходит **136 из 358**, остаётся **222**.
+2
View File
@@ -40,6 +40,7 @@ import { revisions as june2021Revisions } from '../scripts/upgrade-2021-06.mjs';
import { revisions as july2021Revisions } from '../scripts/upgrade-2021-07.mjs';
import { revisions as august2021Revisions } from '../scripts/upgrade-2021-08.mjs';
import { revisions as september2021Revisions } from '../scripts/upgrade-2021-09.mjs';
import { revisions as october2021Revisions } from '../scripts/upgrade-2021-10.mjs';
// This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [
@@ -85,4 +86,5 @@ export const editorialRevisions = [
...july2021Revisions,
...august2021Revisions,
...september2021Revisions,
...october2021Revisions,
];
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="1320" viewBox="0 0 720 1320" role="img" aria-labelledby="title desc">
<title id="title">Сравнение учебных allocation units для двух вариантов одного request</title>
<desc id="desc">Две колонки сравнивают allocation-heavy и reuse для decode, validation, compute и encode. Heavy получает 12 units и пересекает условный порог 8, reuse получает 3 units. У обоих одинаковый result и 9 work units. Числа не являются байтами, временем или benchmark.</desc>
<defs>
<style>
.title { font: 700 30px system-ui, sans-serif; fill: #f8fafc; }
.subtitle { font: 400 19px system-ui, sans-serif; fill: #cbd5e1; }
.head { font: 700 24px system-ui, sans-serif; fill: #f8fafc; }
.label { font: 600 20px system-ui, sans-serif; fill: #dbeafe; }
.value { font: 700 22px ui-monospace, SFMono-Regular, Menlo, monospace; fill: #f8fafc; }
.small { font: 400 18px system-ui, sans-serif; fill: #cbd5e1; }
.good { font: 700 18px system-ui, sans-serif; fill: #86efac; }
.warn { font: 700 18px system-ui, sans-serif; fill: #fde68a; }
.danger { font: 700 18px system-ui, sans-serif; fill: #fda4af; }
</style>
</defs>
<rect width="720" height="1320" fill="#0b1220"/>
<rect x="24" y="24" width="672" height="110" rx="20" fill="#172842" stroke="#818cf8" stroke-width="3"/>
<text x="52" y="68" class="title">Allocation profile учебной модели</text>
<text x="52" y="100" class="subtitle">один result, одинаковая работа, разные заданные units</text>
<rect x="36" y="174" width="304" height="84" rx="18" fill="#4a2734" stroke="#fb7185" stroke-width="3"/>
<text x="64" y="210" class="head">allocation-heavy</text>
<text x="64" y="239" class="subtitle">только учебный вариант</text>
<rect x="380" y="174" width="304" height="84" rx="18" fill="#1f4d43" stroke="#5eead4" stroke-width="3"/>
<text x="408" y="210" class="head">reuse</text>
<text x="408" y="239" class="subtitle">тот же handler contract</text>
<text x="54" y="316" class="label">decode</text>
<rect x="54" y="338" width="140" height="44" rx="12" fill="#be425d"/>
<rect x="398" y="338" width="47" height="44" rx="12" fill="#2da98d"/>
<text x="205" y="369" class="value">3</text>
<text x="456" y="369" class="value">1</text>
<text x="54" y="448" class="label">validation</text>
<rect x="54" y="470" width="47" height="44" rx="12" fill="#be425d"/>
<rect x="398" y="470" width="2" height="44" rx="1" fill="#2da98d"/>
<text x="112" y="501" class="value">1</text>
<text x="416" y="501" class="value">0</text>
<text x="54" y="580" class="label">compute</text>
<rect x="54" y="602" width="234" height="44" rx="12" fill="#be425d"/>
<rect x="398" y="602" width="47" height="44" rx="12" fill="#2da98d"/>
<text x="299" y="633" class="value">5</text>
<text x="456" y="633" class="value">1</text>
<text x="54" y="712" class="label">encode</text>
<rect x="54" y="734" width="140" height="44" rx="12" fill="#be425d"/>
<rect x="398" y="734" width="47" height="44" rx="12" fill="#2da98d"/>
<text x="205" y="765" class="value">3</text>
<text x="456" y="765" class="value">1</text>
<line x1="54" y1="836" x2="666" y2="836" stroke="#475569" stroke-width="2"/>
<text x="54" y="882" class="head">Итог allocation units</text>
<rect x="54" y="908" width="234" height="60" rx="14" fill="#7f1d3b"/>
<text x="80" y="948" class="value">12</text>
<text x="132" y="946" class="danger">выше budget 6</text>
<rect x="398" y="908" width="234" height="60" rx="14" fill="#166b5b"/>
<text x="424" y="948" class="value">3</text>
<text x="478" y="946" class="good">внутри budget 6</text>
<rect x="54" y="1010" width="612" height="82" rx="16" fill="#3b3520" stroke="#facc15" stroke-width="3"/>
<text x="82" y="1045" class="head">Порог GC = 8 units</text>
<text x="82" y="1074" class="warn">heavy пересекает model boundary; это не измеренная pause</text>
<rect x="54" y="1130" width="612" height="112" rx="16" fill="#183550" stroke="#60a5fa" stroke-width="3"/>
<text x="82" y="1168" class="head">Общее у двух вариантов</text>
<text x="82" y="1199" class="small">work units: 9 · response status: 200 · body: total 42</text>
<text x="82" y="1227" class="small">FFI и I/O boundaries видимы, но invocation = not-performed</text>
<text x="54" y="1265" class="small">units — детерминированные счётчики fixture, не bytes / CPU</text>
<text x="54" y="1293" class="small">и не latency / benchmark</text>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="1600" viewBox="0 0 720 1600" role="img" aria-labelledby="title desc">
<title id="title">Безопасная диагностика учебного request в D-сервисе</title>
<desc id="desc">Вертикальный маршрут диагностики начинает с сохранения результата, error route и trace. Затем он проверяет contract handler, allocation budget, условную границу GC и неисполняемые FFI/I/O boundaries. Каждый шаг задаёт обратимое действие и запрещает менять runtime глобально без отдельного evidence.</desc>
<defs>
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
<path d="M0 0 L12 6 L0 12 Z" fill="#7dd3fc"/>
</marker>
<style>
.title { font: 700 30px system-ui, sans-serif; fill: #f8fafc; }
.subtitle { font: 400 19px system-ui, sans-serif; fill: #bfdbfe; }
.question { font: 700 24px system-ui, sans-serif; fill: #f8fafc; }
.body { font: 400 20px system-ui, sans-serif; fill: #dbeafe; }
.small { font: 600 18px system-ui, sans-serif; fill: #cbd5e1; }
.good { font: 700 18px system-ui, sans-serif; fill: #86efac; }
.warn { font: 700 18px system-ui, sans-serif; fill: #fde68a; }
.danger { font: 700 18px system-ui, sans-serif; fill: #fda4af; }
</style>
</defs>
<rect width="720" height="1600" fill="#0b1220"/>
<rect x="24" y="24" width="672" height="112" rx="20" fill="#172842" stroke="#60a5fa" stroke-width="3"/>
<text x="52" y="69" class="title">Диагностика одного request</text>
<text x="52" y="101" class="subtitle">сначала evidence, затем обратимое действие</text>
<rect x="54" y="178" width="612" height="112" rx="18" fill="#193450" stroke="#60a5fa" stroke-width="3"/>
<text x="86" y="221" class="question">0. Сохранить evidence</text>
<text x="86" y="253" class="body">input · success/error body · stages · budget</text>
<text x="86" y="280" class="small">units модели не записываются как latency</text>
<path d="M360 302 L360 344" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="358" width="612" height="124" rx="18" fill="#1d3b50" stroke="#a5b4fc" stroke-width="3"/>
<text x="86" y="401" class="question">1. Contract handler сохранён?</text>
<text x="86" y="433" class="body">success body равен; invalid идёт в encode-error?</text>
<text x="86" y="462" class="danger">нет → остановить правку, восстановить validation/encode</text>
<path d="M360 494 L360 536" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="550" width="612" height="124" rx="18" fill="#3b3520" stroke="#facc15" stroke-width="3"/>
<text x="86" y="593" class="question">2. Allocation выше budget?</text>
<text x="86" y="625" class="body">heavy 12 &gt; 6; reuse 3 ≤ 6</text>
<text x="86" y="654" class="warn">сравнивать только при равном result и work</text>
<path d="M360 686 L360 728" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="742" width="612" height="124" rx="18" fill="#3b3520" stroke="#facc15" stroke-width="3"/>
<text x="86" y="785" class="question">3. Что означает gc-boundary?</text>
<text x="86" y="817" class="body">только threshold 8 units в модели</text>
<text x="86" y="846" class="warn">это не pause: нужен отдельный profile и условия</text>
<path d="M360 878 L360 920" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="934" width="612" height="124" rx="18" fill="#3b3520" stroke="#facc15" stroke-width="3"/>
<text x="86" y="977" class="question">4. FFI / I/O выполнялись?</text>
<text x="86" y="1009" class="body">trace: invocation = not-performed</text>
<text x="86" y="1038" class="warn">нет → не обвинять dependency; описать отдельный contract test</text>
<path d="M360 1070 L360 1112" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="1126" width="612" height="130" rx="18" fill="#193450" stroke="#60a5fa" stroke-width="3"/>
<text x="86" y="1169" class="question">5. Есть фактический профиль границы?</text>
<text x="86" y="1201" class="body">версия tool · вход · среда · один метод</text>
<text x="86" y="1230" class="small">нет → сначала собрать evidence, не менять runtime глобально</text>
<path d="M360 1268 L360 1310" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="1324" width="612" height="130" rx="18" fill="#1f4d43" stroke="#5eead4" stroke-width="3"/>
<text x="86" y="1367" class="question">Rollback-safe действие</text>
<text x="86" y="1399" class="body">один temporary-state change; valid + invalid traces</text>
<text x="86" y="1428" class="good">внешняя граница станет реальной → отдельный contract test</text>
<rect x="54" y="1484" width="612" height="100" rx="16" fill="#442b37" stroke="#fb7185" stroke-width="3"/>
<text x="82" y="1518" class="question">Стоп-критерий</text>
<text x="82" y="1546" class="small">исчез success/error contract → сначала восстановить его</text>
<text x="82" y="1573" class="small">и только затем снова сравнивать units</text>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="1532" viewBox="0 0 720 1532" role="img" aria-labelledby="title desc">
<title id="title">Один учебный request в D-сервисе: основной путь, модельные allocations и внешние границы</title>
<desc id="desc">Вертикальная схема показывает путь decode, validation, compute и encode для одного учебного request. На пути видны model allocation units, условная граница GC и неисполняемые FFI и I/O boundaries. Ошибка validation уходит в encode-error и не доходит до compute.</desc>
<defs>
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
<path d="M0 0 L12 6 L0 12 Z" fill="#7dd3fc"/>
</marker>
<style>
.title { font: 700 30px system-ui, sans-serif; fill: #f8fafc; }
.subtitle { font: 400 19px system-ui, sans-serif; fill: #bfdbfe; }
.step { font: 700 26px system-ui, sans-serif; fill: #f8fafc; }
.body { font: 400 20px system-ui, sans-serif; fill: #dbeafe; }
.small { font: 600 18px system-ui, sans-serif; fill: #cbd5e1; }
.mono { font: 600 17px ui-monospace, SFMono-Regular, Menlo, monospace; fill: #e0f2fe; }
.good { font: 700 18px system-ui, sans-serif; fill: #86efac; }
.warn { font: 700 18px system-ui, sans-serif; fill: #fde68a; }
.danger { font: 700 18px system-ui, sans-serif; fill: #fda4af; }
</style>
</defs>
<rect width="720" height="1510" fill="#0b1220"/>
<rect x="24" y="24" width="672" height="114" rx="20" fill="#172842" stroke="#60a5fa" stroke-width="3"/>
<text x="54" y="69" class="title">Один request: видимые границы</text>
<text x="54" y="101" class="subtitle">модель units, а не профиль D runtime</text>
<rect x="54" y="178" width="612" height="112" rx="18" fill="#193450" stroke="#60a5fa" stroke-width="3"/>
<text x="86" y="221" class="step">Вход: sum(19, 23)</text>
<text x="86" y="254" class="body">requestId · operation · ffiIntent · ioIntent</text>
<path d="M360 302 L360 344" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="358" width="612" height="126" rx="18" fill="#163c58" stroke="#67e8f9" stroke-width="3"/>
<text x="86" y="402" class="step">1. decode</text>
<text x="86" y="434" class="body">shape request → normalized object</text>
<text x="86" y="463" class="mono">heavy +3 units · reuse +1 unit</text>
<path d="M360 496 L360 538" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="552" width="612" height="126" rx="18" fill="#1d3b50" stroke="#a5b4fc" stroke-width="3"/>
<text x="86" y="596" class="step">2. validation</text>
<text x="86" y="628" class="body">operation = sum? operands are integers?</text>
<text x="86" y="657" class="mono">heavy +1 unit · reuse +0 units</text>
<path d="M360 690 L360 732" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<text x="382" y="718" class="good">valid</text>
<text x="522" y="592" class="danger">invalid</text>
<rect x="54" y="746" width="612" height="144" rx="18" fill="#244132" stroke="#86efac" stroke-width="3"/>
<text x="86" y="790" class="step">3. compute</text>
<text x="86" y="822" class="body">19 + 23 = 42; result ещё локальный</text>
<text x="86" y="851" class="mono">heavy +5 units · reuse +1 unit</text>
<text x="442" y="851" class="warn">8 units → model GC boundary</text>
<path d="M360 902 L360 944" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="958" width="612" height="108" rx="18" fill="#3b3520" stroke="#facc15" stroke-width="3"/>
<text x="86" y="1001" class="step">FFI boundary</text>
<text x="86" y="1034" class="body">normalize-scalar · invocation: not-performed</text>
<path d="M360 1078 L360 1120" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="1134" width="612" height="108" rx="18" fill="#3b3520" stroke="#facc15" stroke-width="3"/>
<text x="86" y="1177" class="step">I/O boundary</text>
<text x="86" y="1210" class="body">write-audit-record · invocation: not-performed</text>
<path d="M360 1254 L360 1296" stroke="#7dd3fc" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="54" y="1310" width="612" height="118" rx="18" fill="#1f4d43" stroke="#5eead4" stroke-width="3"/>
<text x="86" y="1354" class="step">4. encode</text>
<text x="86" y="1386" class="mono">{"requestId":"runtime-d-training-42","total":42}</text>
<text x="86" y="1415" class="body">heavy +3 units · reuse +1 unit</text>
<rect x="54" y="1450" width="612" height="66" rx="14" fill="#442b37" stroke="#fb7185" stroke-width="2"/>
<text x="78" y="1477" class="small">invalid path: validation-error → encode-error</text>
<text x="78" y="1502" class="small">compute, FFI и I/O не выполняются</text>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

+626
View File
@@ -0,0 +1,626 @@
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function paragraph(text) {
return '<p>' + text + '</p>';
}
function heading(text) {
return '<h2>' + text + '</h2>';
}
function codeBlock(lines) {
return '<pre><code>' + escapeHtml(Array.isArray(lines) ? lines.join('\n') : lines) + '</code></pre>';
}
function figure(src, alt, caption) {
return '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
}
function orderedList(items) {
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
}
function dataTable(caption, headers, rows) {
const head = '<thead><tr>' + headers.map((header) => '<th scope="col">' + header + '</th>').join('') + '</tr></thead>';
const body = '<tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody>';
return '<div class="table-scroll"><table><caption>' + caption + '</caption>' + head + body + '</table></div>';
}
function sourceList(items) {
return '<ul>' + items.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
}
function plainText(content) {
return content
.replace(/<[^>]+>/g, ' ')
.replaceAll('&nbsp;', ' ')
.replaceAll('&quot;', '"')
.replaceAll('&#039;', "'")
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&amp;', '&')
.replace(/\s+/g, ' ')
.trim();
}
function bodyText(content) {
return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
}
function createRevision(meta, bodyParts, sources) {
if (sources.length < 2) {
throw new Error(meta.slug + ': нужно минимум два первичных или официальных источника');
}
const contentHtml = bodyParts.join('\n') + '\n' + heading('Проверяемые источники') + '\n' + sourceList(sources);
const proseLength = bodyText(contentHtml).length;
if (proseLength < 5000 || proseLength > 15000) {
throw new Error(meta.slug + ': основной текст вне 5 000–15 000 знаков: ' + proseLength);
}
return { ...meta, contentHtml, proseLength };
}
const dAutomaticMemoryManagement = {
title: 'D 2.097.2: source snapshot of Automatic Memory Management specification',
url: 'https://github.com/dlang/dlang.org/blob/d5798c666479e8c4c918221e10ebf997b1f5f89f/spec/garbage.dd',
note: 'неизменяемый исходник официальной спецификации из тега v2.097.2; он задаёт терминологию automatic memory management, но не профиль конкретного запущенного сервиса',
};
const dNoGcFunctions = {
title: 'D 2.097.2: source snapshot of @nogc function specification',
url: 'https://github.com/dlang/dlang.org/blob/d5798c666479e8c4c918221e10ebf997b1f5f89f/spec/function.dd',
note: 'неизменяемый исходник официальной спецификации атрибута <code>@nogc</code>; он ограничивает проверяемый D-код и не доказывает свойства внешней библиотеки или операции ввода-вывода',
};
const dApplicationBinaryInterface = {
title: 'D 2.097.2: source snapshot of Application Binary Interface specification',
url: 'https://github.com/dlang/dlang.org/blob/d5798c666479e8c4c918221e10ebf997b1f5f89f/spec/abi.dd',
note: 'неизменяемый исходник официальной границы ABI для взаимодействия с C ABI целевой системы; наличие границы не говорит о стоимости или безопасности конкретного foreign call',
};
const dRelease0972 = {
title: 'D 2.097.2: официальный release record',
url: 'https://dlang.org/changelog/2.097.2.html',
note: 'выпуск опубликован 9 августа 2021 года и служит проверяемой дооктябрьской точкой для терминов; статья не выводит из номера версии benchmark или runtime profile',
};
const dRuntimeMemorySnapshot0972 = {
title: 'DRuntime 2.097.2: immutable snapshot core/memory.d',
url: 'https://github.com/dlang/druntime/blob/f978df34a48613492240e8398227419b14002bef/src/core/memory.d',
note: 'точная фиксация исходника DRuntime, на которую указывал тег v2.097.2; используется для исторической проверки терминов, не как замена измерения приложения',
};
const fixtureRunExample = [
'const report = runDRuntimeServiceFixture();',
'const rows = report.variants.map((variant) => ({',
' variant: variant.variant,',
' result: variant.response.body,',
' allocationUnits: variant.model.allocationUnits,',
' withinBudget: variant.model.withinAllocationBudget,',
'}));',
'',
'console.table(rows);',
'if (!Object.values(report.assertions).every(Boolean)) {',
' throw new Error("training fixture invariant failed");',
'}',
].join('\n');
const traceExample = [
'const report = runDRuntimeServiceFixture();',
'const heavy = report.variants.find((item) => item.variant === "allocation-heavy");',
'',
'console.log(heavy.trace.map((entry) => entry.stage));',
'// decode, validation, compute, ffi-boundary, io-boundary, encode ...',
'console.log(heavy.model);',
'// { allocationUnits: 12, allocationBudget: 6, withinAllocationBudget: false, ... }',
].join('\n');
const diagnosisExample = [
'const report = runDRuntimeServiceFixture();',
'const failed = report.invalidRequest;',
'',
'console.log(failed.response);',
'// { ok: false, status: 400, body: \'{"error":"invalid-operation"}\' }',
'console.log(failed.trace.map((entry) => entry.stage));',
'// decode, validation-error, encode-error',
'',
'if (!failed.model.errorRouteVisible) {',
' throw new Error("error route disappeared from the training trace");',
'}',
].join('\n');
/**
* Детерминированная учебная модель одного сервисного запроса.
*
* Она не запускает D compiler, D runtime, HTTP server, файл, сокет, FFI,
* профилировщик или внешний процесс. allocationUnits и workUnits — заданные
* счётчики модели, а не bytes, CPU, latency, duration или pause. Trace
* показывает выбранные границы, чтобы обсуждать один запрос до реального
* профилирования.
*/
export function runDRuntimeServiceFixture() {
const allocationBudget = 6;
const gcObservationThreshold = 8;
const validRequest = Object.freeze({
requestId: 'runtime-d-training-42',
operation: 'sum',
left: 19,
right: 23,
ffiIntent: 'normalize-scalar',
ioIntent: 'write-audit-record',
});
const invalidRequest = Object.freeze({
requestId: 'runtime-d-training-invalid',
operation: 'divide',
left: 19,
right: 23,
ffiIntent: 'normalize-scalar',
ioIntent: 'write-audit-record',
});
function makeContext(variant) {
return {
variant,
allocationBudget,
gcObservationThreshold,
allocationUnits: 0,
workUnits: 0,
gcBoundaryObserved: false,
trace: [],
};
}
function record(context, stage, detail = {}) {
context.trace.push(Object.freeze({
index: context.trace.length + 1,
stage,
...detail,
}));
}
function addWork(context, units, label) {
context.workUnits += units;
record(context, 'work', {
label,
units,
totalWorkUnits: context.workUnits,
});
}
function addAllocation(context, units, label) {
context.allocationUnits += units;
record(context, 'allocation', {
label,
units,
totalAllocationUnits: context.allocationUnits,
measurement: 'training-model-only',
});
if (!context.gcBoundaryObserved && context.allocationUnits >= context.gcObservationThreshold) {
context.gcBoundaryObserved = true;
record(context, 'gc-boundary', {
label: 'allocation-units-crossed-training-threshold',
threshold: context.gcObservationThreshold,
measurement: 'not-a-runtime-pause',
});
}
}
function addVariantAllocation(context, heavyUnits, reuseUnits, label) {
const units = context.variant === 'allocation-heavy' ? heavyUnits : reuseUnits;
addAllocation(context, units, label);
}
function observeBoundary(context, kind, intent) {
record(context, kind + '-boundary', {
intent,
invocation: 'not-performed',
meaning: 'request-boundary-visible-in-training-trace',
});
}
function decode(context, raw) {
record(context, 'decode', { requestId: raw?.requestId ?? null });
addWork(context, 2, 'decode-shape');
addVariantAllocation(context, 3, 1, 'decode-request-shape');
if (!raw || typeof raw !== 'object') {
return { ok: false, error: 'invalid-request-shape' };
}
return {
ok: true,
value: Object.freeze({
requestId: raw.requestId,
operation: raw.operation,
left: raw.left,
right: raw.right,
ffiIntent: raw.ffiIntent,
ioIntent: raw.ioIntent,
}),
};
}
function validate(context, request) {
addWork(context, 1, 'validation-rules');
addVariantAllocation(context, 1, 0, 'validation-temporary-state');
const valid = typeof request.requestId === 'string'
&& request.requestId.length > 0
&& request.operation === 'sum'
&& Number.isInteger(request.left)
&& Number.isInteger(request.right);
if (!valid) {
record(context, 'validation-error', {
code: 'invalid-operation',
operation: request.operation,
});
return { ok: false, error: 'invalid-operation' };
}
record(context, 'validation', {
operation: request.operation,
result: 'accepted',
});
return { ok: true, value: request };
}
function compute(context, request) {
record(context, 'compute', {
operation: request.operation,
operands: [request.left, request.right],
});
addWork(context, 4, 'sum-operands');
addVariantAllocation(context, 5, 1, 'compute-intermediate-state');
observeBoundary(context, 'ffi', request.ffiIntent);
observeBoundary(context, 'io', request.ioIntent);
return Object.freeze({
requestId: request.requestId,
total: request.left + request.right,
});
}
function encodeSuccess(context, value) {
record(context, 'encode', { result: 'success' });
addWork(context, 2, 'encode-response');
addVariantAllocation(context, 3, 1, 'encode-response-buffer');
return Object.freeze({
ok: true,
status: 200,
body: JSON.stringify(value),
});
}
function encodeError(context, error) {
record(context, 'encode-error', { error });
addWork(context, 1, 'encode-error-response');
addVariantAllocation(context, 1, 1, 'encode-error-response-buffer');
return Object.freeze({
ok: false,
status: 400,
body: JSON.stringify({ error }),
});
}
function finalize(context, response) {
const stages = context.trace.map((entry) => entry.stage);
const model = Object.freeze({
allocationUnits: context.allocationUnits,
workUnits: context.workUnits,
allocationBudget: context.allocationBudget,
withinAllocationBudget: context.allocationUnits <= context.allocationBudget,
gcBoundaryObserved: context.gcBoundaryObserved,
ffiBoundaryVisible: stages.includes('ffi-boundary'),
ioBoundaryVisible: stages.includes('io-boundary'),
errorRouteVisible: stages.includes('validation-error') && stages.includes('encode-error'),
externalInvocationPerformed: context.trace.some((entry) => entry.invocation === 'performed'),
measurement: 'training-units-only',
});
return Object.freeze({
variant: context.variant,
response,
trace: Object.freeze([...context.trace]),
model,
});
}
function handle(raw, variant) {
const context = makeContext(variant);
const decoded = decode(context, raw);
if (!decoded.ok) return finalize(context, encodeError(context, decoded.error));
const validated = validate(context, decoded.value);
if (!validated.ok) return finalize(context, encodeError(context, validated.error));
const computed = compute(context, validated.value);
return finalize(context, encodeSuccess(context, computed));
}
const allocationHeavy = handle(validRequest, 'allocation-heavy');
const reuse = handle(validRequest, 'reuse');
const invalid = handle(invalidRequest, 'reuse');
const heavyStages = allocationHeavy.trace.map((entry) => entry.stage);
const reuseStages = reuse.trace.map((entry) => entry.stage);
const invalidStages = invalid.trace.map((entry) => entry.stage);
const requiredSuccessStages = ['decode', 'validation', 'compute', 'ffi-boundary', 'io-boundary', 'encode'];
const assertions = Object.freeze({
sameHandlerResult: allocationHeavy.response.ok === true
&& reuse.response.ok === true
&& allocationHeavy.response.status === reuse.response.status
&& allocationHeavy.response.body === reuse.response.body,
allocationHeavyExceedsBudget: allocationHeavy.model.allocationUnits > allocationBudget
&& allocationHeavy.model.withinAllocationBudget === false,
reuseFitsBudget: reuse.model.allocationUnits <= allocationBudget
&& reuse.model.withinAllocationBudget === true,
sameWorkUnitsForVariants: allocationHeavy.model.workUnits === reuse.model.workUnits,
successPathVisibleForAllocationHeavy: requiredSuccessStages.every((stage) => heavyStages.includes(stage)),
successPathVisibleForReuse: requiredSuccessStages.every((stage) => reuseStages.includes(stage)),
gcBoundaryIsModelledNotMeasured: allocationHeavy.model.gcBoundaryObserved === true
&& reuse.model.gcBoundaryObserved === false
&& allocationHeavy.trace.some((entry) => entry.stage === 'gc-boundary' && entry.measurement === 'not-a-runtime-pause'),
ffiBoundaryVisible: allocationHeavy.model.ffiBoundaryVisible === true
&& allocationHeavy.trace.some((entry) => entry.stage === 'ffi-boundary' && entry.invocation === 'not-performed'),
ioBoundaryVisible: allocationHeavy.model.ioBoundaryVisible === true
&& allocationHeavy.trace.some((entry) => entry.stage === 'io-boundary' && entry.invocation === 'not-performed'),
externalBoundaryIsNotExecuted: allocationHeavy.model.externalInvocationPerformed === false
&& reuse.model.externalInvocationPerformed === false,
errorRouteIsExplicit: invalid.response.ok === false
&& invalid.response.status === 400
&& invalid.response.body === '{"error":"invalid-operation"}'
&& invalid.model.errorRouteVisible === true,
invalidRequestDoesNotReachComputeOrBoundary: !invalidStages.includes('compute')
&& !invalidStages.includes('ffi-boundary')
&& !invalidStages.includes('io-boundary')
&& invalidStages.includes('validation-error')
&& invalidStages.includes('encode-error'),
});
return Object.freeze({
scenario: Object.freeze({
description: 'one in-memory request: decode -> validation -> compute -> encode',
allocationBudget,
gcObservationThreshold,
measurements: 'deterministic training units, not runtime performance',
}),
variants: Object.freeze([allocationHeavy, reuse]),
invalidRequest: invalid,
assertions,
});
}
const commonSources = [
dAutomaticMemoryManagement,
dNoGcFunctions,
dApplicationBinaryInterface,
dRelease0972,
dRuntimeMemorySnapshot0972,
];
const practiceArticle = createRevision(
{
slug: 'editorial-2021-10-practice-d-runtime-service',
title: 'D в сервисе: как ограничить вопрос о runtime одним запросом',
categories: ['DLang', 'Backend', 'Отладка'],
cover: '/assets/editorial/2021/d-runtime-request-path-2021.svg',
excerpt: 'Практический способ разобрать один сервисный запрос в D: отделить результат handler от учебного бюджета allocation units, отметить границы GC, FFI и I/O и подготовить следующий проверяемый шаг.',
readingMinutes: 14,
},
[
paragraph('В сервисе появляется фраза «этот endpoint тормозит из-за runtime D». Проблема не в том, что в ней упомянут GC. В ней нет входа, результата, границы и доказательства. Цена такой формулировки — случайная правка: отключить что-то глобально, переписать FFI-вызов или добавить кеш, а затем не суметь объяснить, какой участок запроса вообще изменили.'),
paragraph('Для начала я бы не пытался измерить весь сервис и не переносил ожидание «быстро» из PHP или JavaScript. Возьмём один учебный request: <code>decode → validation → compute → encode</code>. Рядом отметим две видимые границы — FFI и I/O — но не выполним ни foreign call, ни ввод-вывод. В этой статье allocation и work считаются детерминированными единицами модели. Это не байты, не время CPU, не latency и не профиль настоящего runtime.'),
heading('Сначала формулируем вопрос, который можно закрыть'),
paragraph('У одного endpoint должна быть короткая карточка. Что приходит на вход? Какой успешный результат остаётся неизменным? Где request может закончиться ошибкой? Какая граница интересует сейчас: построение временных объектов, наблюдаемая точка GC, вызов за ABI или выход в I/O? Пока в карточке смешаны все эти вопросы, команда сравнивает несравнимое: правило validation, стоимость encoding и поведение внешней библиотеки.'),
paragraph('Такое сужение не обедняет расследование. Оно отделяет контракт handler от гипотезы о ресурсе. Если оба учебных варианта возвращают один body, но один превышает выбранный allocation budget, мы получили повод посмотреть на создание промежуточного состояния. Мы ещё не получили право назвать вариант медленнее, включать настройку DRuntime или обещать паузу в продуктивном процессе.'),
dataTable(
'Карточка одного учебного request',
['Поле', 'Фиксируем в модели', 'Зачем это нужно', 'Чего это не доказывает'],
[
['Вход', '<code>sum(19, 23)</code> и идентификатор request', 'оба варианта получают одинаковые данные', 'формат настоящего HTTP-запроса или его нагрузку'],
['Успех handler', '<code>{"requestId":"runtime-d-training-42","total":42}</code>', 'сравниваем оптимизацию при равном результате', 'корректность любого бизнес-правила'],
['Бюджет', '<code>6 allocation units</code>', 'есть явный порог для учебной развилки', 'байты памяти, latency или CPU'],
['GC boundary', 'пересечение порога <code>8</code> в trace', 'видно, где модель ставит вопрос о GC', 'реальную паузу, алгоритм или schedule сборщика'],
['FFI / I/O', 'intent записан как boundary, invocation = <code>not-performed</code>', 'внешняя граница не исчезает из диагноза', 'вызов библиотеки, сеть, файл или блокировку'],
],
),
paragraph('У карточки нужен владелец следующей проверки, но не обязательно «владелец всего runtime». Автор handler отвечает за равенство result и error route. Тот, кто знает внешний contract, отвечает за отдельную проверку FFI или I/O. Если владельца ещё нет, boundary так и записывают неизвестной, а не подменяют её словом «D». Это снимает ложный выбор между «оптимизировать всё» и «ничего не делать»: можно сохранить контекст одного request и передать только нужный вопрос следующему человеку.'),
heading('Выбираем наблюдаемый профиль, а не название оптимизации'),
paragraph('В модели есть два варианта. <code>allocation-heavy</code> добавляет временные единицы на decode, validation, compute и encode. <code>reuse</code> записывает меньше единиц там, где мы условно переиспользовали промежуточное состояние. У обоих одинаковые вход, work units и ответ. Значит, сравнение не прячет изменение результата за словом «оптимизация».'),
paragraph('Бюджет здесь намеренно маленький и проектный. Его задача — сделать проверку бинарной: первый путь за границей, второй внутри. В реальном проекте такой порог сначала выбирают из цели конкретного endpoint и затем подтверждают инструментом, который подходит версии compiler, DRuntime и окружению. Не надо брать учебное число 6 как настройку heap, лимит процесса или триггер GC. Это номер строки в договоре fixture, а не команда для запуска.'),
figure(
'/assets/editorial/2021/d-runtime-request-path-2021.svg',
'Схема одного учебного сервисного запроса: decode, validation, compute и encode образуют основной путь; рядом явно отмечены model allocation, условная граница GC и неисполняемые FFI/I/O boundaries. Ошибка validation идёт в encode-error и не доходит до compute.',
'Схема фиксирует маршрут и ответственность шага. Цветная отметка GC — порог учебных allocation units, не запись о паузе настоящего D runtime.',
),
heading('Фиксируем результат и trace одним JS-примером'),
paragraph('Fixture находится в этом же revision-модуле, поэтому пример можно исполнить без компилятора D и без инфраструктуры. Он не измеряет собственные JavaScript allocations. Он читает уже собранную модель и проверяет её assertions. Если кто-то поменяет путь так, что reuse начнёт давать другой body, тест остановится даже при красивом меньшем числе units.'),
codeBlock(fixtureRunExample),
paragraph('Результат таблицы должен быть простым: <code>allocation-heavy</code> даёт тот же body, но 12 условных allocation units и выход за budget 6; <code>reuse</code> даёт тот же body, 3 units и остаётся внутри. Work units равны. Это сделано специально: если у вариантов разный output или разная работа handler, обсуждать allocation рано. Сначала возвращаем одинаковый контракт, потом меняем гипотезу о промежуточном состоянии.'),
heading('Как связать такую карточку с D, не выдумывая свойства runtime'),
paragraph('Официальная спецификация D описывает automatic memory management и отдельно атрибут <code>@nogc</code>. Для инженерного разговора отсюда полезна граница: ограничение на D-код не делает безопасной неизвестную часть за вызовом. Точно так же D ABI описывает взаимодействие с C ABI целевой системы, но один факт пересечения ABI не говорит, выделяет ли память библиотека, блокирует ли она поток и какой контракт ownership у её параметров. Эти вопросы нужно записать в trace и проверить на выбранной версии, а не угадывать по названию языка.'),
paragraph('Для исторической точки practice-заметки от 7 октября использован выпуск D 2.097.2, опубликованный до этой даты. Release record и точный снимок <code>core/memory.d</code> нужны здесь только для проверки терминов. Это не попытка восстановить профиль старого процесса: без исходника request, compiler options, линковки, операционной системы и данных такое утверждение было бы выдумкой. Поэтому следующий шаг остаётся узким: взять один живой endpoint и добавить к нему такой же request trace, а фактическое измерение провести отдельным инструментом и отдельно сохранить его условия.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'<strong>Симптом.</strong> Назовите один endpoint и один видимый результат: какой body или error code он должен вернуть. Не начинайте со слова «runtime».',
'<strong>Причина-гипотеза.</strong> Выберите один слой: временные объекты на пути decode/compute/encode, условная GC boundary, FFI boundary или I/O boundary. У каждой версии должна быть отдельная карточка.',
'<strong>Проверка.</strong> Прогоните одинаковый in-memory input через два учебных варианта. Убедитесь, что body, status и work units совпали, а trace содержит все выбранные границы.',
'<strong>Действие.</strong> Если только allocation budget различается, подготовьте локальный change, который сохраняет output. Не меняйте глобальную GC-конфигурацию и не переписывайте foreign code до отдельного доказательства.',
'<strong>Повторная проверка.</strong> После change повторите исходный input, сохраните trace до/после и добавьте error input. Успех без error route не закрывает handler.',
'<strong>Следующий уровень.</strong> Лишь затем выбирайте реальный profiler и записывайте compiler, DRuntime, платформу, вход и окно наблюдения. Учебная fixture к этому готовит вопрос, но не заменяет ответ.',
]),
heading('Ограничения и следующий проверяемый шаг'),
paragraph('Эта модель не запускает D compiler, D runtime, GC, HTTP, сеть, файл, database, FFI, profiler или benchmark. Её <code>gc-boundary</code> — запись о пересечении заданного порога; она не подтверждает pause. Её FFI и I/O entries — неисполненные границы; они не подтверждают вызов, ownership, retry или блокировку. Она также не моделирует concurrency, backpressure, scheduler, исключения D, сериализацию реального протокола и лимиты процесса.'),
paragraph('Для своего проекта возьмите один нечувствительный test input и выпишите результат, error path, owner boundary и версию инструмента. Если после этого вопрос всё ещё звучит как «runtime D медленный», карточка недостаточно узкая. Если он звучит как «при том же result в encode создаётся лишнее промежуточное состояние», есть безопасный следующий эксперимент. Такой переход от ярлыка к проверке полезнее любой заранее выбранной настройки.'),
],
commonSources,
);
const mechanismArticle = createRevision(
{
slug: 'editorial-2021-10-mechanism-d-runtime-service',
title: 'Под капотом D-сервиса: request path, allocation и внешние границы',
categories: ['DLang', 'Backend', 'Архитектура'],
cover: '/assets/editorial/2021/d-runtime-allocation-profile-2021.svg',
excerpt: 'Разбираем один request path в D как наблюдаемую структуру: где в учебной модели появляются allocation units, условная GC boundary, FFI/I/O boundaries и почему слово «быстро» не переносится между runtime.',
readingMinutes: 15,
},
[
paragraph('Когда один handler называют «быстрым на D», обычно пропускают механизм. Проблема в том, что в ответе могли быть decode, проверка входа, вычисление, кодирование, вызов за ABI и выход в I/O, но в разговор попало одно слово. Цена такой потери структуры — неверная оптимизация: уменьшить allocation в одном месте и не заметить, что error route изменился, или объяснить внешний вызов свойствами GC.'),
paragraph('Разберём один request как учебную цепочку <code>decode → validation → compute → encode</code>. У неё есть trace, result, work units и allocation units. Два варианта меняют только модель временных объектов: один получает 12 units, другой 3, но оба возвращают тот же response и те же 9 work units. Это не benchmark D, не профиль DRuntime и не сравнение языков. Это способ увидеть, какой вопрос нужно отдать измерению позже.'),
heading('Путь request — это контракт, а не полоса времени'),
paragraph('Decode принимает in-memory object и собирает нормализованный request. Validation отделяет неверную операцию от вычисления. Compute складывает два целых числа и оставляет в trace намерения FFI и I/O без их исполнения. Encode превращает успешный result или error в одинаково определённый body. В таком порядке можно спросить, где выросла модель allocation, не смешивая ошибку входа с работой внешней зависимости.'),
paragraph('Для valid input мы требуем все шесть следов: <code>decode</code>, <code>validation</code>, <code>compute</code>, <code>ffi-boundary</code>, <code>io-boundary</code>, <code>encode</code>. Для invalid input требуем другой, но тоже явный путь: <code>decode</code>, <code>validation-error</code>, <code>encode-error</code>. Compute и внешние boundaries туда не входят. Это важнее счастливого ответа: если handler молча теряет error route, меньший allocation count не является улучшением.'),
dataTable(
'Наблюдаемые участки учебного request path',
['Участок', 'Что записывает fixture', 'Вопрос к реальной системе', 'Что нельзя заключить'],
[
['Decode', 'shape входа, 2 work units, вариант allocation', 'какие данные действительно преобразуются на входе?', 'что HTTP parser или framework работает именно так'],
['Validation', 'accepted либо <code>invalid-operation</code>', 'какая ветка завершает request до compute?', 'что validation не создаёт объектов в реальном запуске'],
['Compute', 'операция <code>19 + 23</code>, 4 work units', 'какой промежуточный state необходим результату?', 'что арифметика является bottleneck'],
['GC boundary', 'порог 8 units пересечён только heavy-веткой', 'где начать фактическую проверку allocation/GC?', 'наличие, длину или причину pause'],
['FFI / I/O', '<code>invocation: not-performed</code>', 'где нужен отдельный contract внешнего вызова?', 'вызов, скорость, ownership или retry политики'],
['Encode', 'стабильный success/error body', 'сохранился ли внешний contract endpoint?', 'поведение реального serializer'],
],
),
heading('Allocation и GC: отделяем структуру от измерения'),
paragraph('В fixture allocation units добавляются не потому, что JavaScript нашёл реальные объекты, а потому, что модель помечает четыре проектных места: decode request shape, temporary state validation, intermediate state compute и response buffer encode. Heavy-вариант записывает 3 + 1 + 5 + 3, reuse-вариант — 1 + 0 + 1 + 1. Сумма задана явно, поэтому review видит, какая правка должна поменять budget.'),
paragraph('После пересечения 8 units модель добавляет <code>gc-boundary</code> с полем <code>measurement: "not-a-runtime-pause"</code>. Это предохранитель против самой частой подмены: «увидели границу — измерили GC». Официальная документация D действительно обсуждает автоматическое управление памятью и ограничения collector, но из этого не следует наблюдаемая история конкретного request. Нужны фактические условия и отдельный профиль. Наш trace только делает место вопроса видимым.'),
figure(
'/assets/editorial/2021/d-runtime-allocation-profile-2021.svg',
'Сравнение двух учебных вариантов request path: allocation-heavy набирает 12 allocation units и пересекает условный порог GC 8, reuse набирает 3 units; у обоих 9 work units и одинаковый handler result. Подпись подчёркивает, что units не являются байтами или временем.',
'Диаграмма сравнивает только заданные счётчики fixture. Зелёная ветка не названа быстрее: она лишь проходит выбранный учебный budget при неизменном результате.',
),
heading('FFI и I/O должны быть в trace, даже если вызов не выполняется'),
paragraph('Внешняя граница часто пропадает из разговора, пока не появится проблема. В учебном request есть <code>ffiIntent: "normalize-scalar"</code> и <code>ioIntent: "write-audit-record"</code>. Fixture пишет их как boundaries с <code>invocation: "not-performed"</code>. Таким образом код не притворяется, что вызвал C-функцию, открыл файл, ушёл в сеть или увидел процесс. Но будущий интеграционный тест уже знает, где должен появиться owner, contract данных, ошибка и cancellation.'),
paragraph('D ABI описывает совместимость с C ABI целевой системы, однако ABI не заменяет договор вызова. У реальной границы отдельно проверяют lifetime аргументов, формат ownership, error code, поток, блокировку, allocator и возможность повторить операцию. Для I/O отдельно нужны protocol, timeout, idempotency, права и данные. Ни один из этих пунктов не выводится из <code>@nogc</code>, из названия DRuntime или из меньшего allocation budget.'),
heading('Почему «быстро» не переносится между runtime'),
paragraph('Слово «быстро» содержит больше переменных, чем кажется. Меняется вход, compiler, версия runtime, link mode, target ABI, операционная система, библиотека, allocator, параллельность и метод измерения. Даже одинаковый source не гарантирует одинаковый путь в другом окружении. Поэтому локальный вывод «reuse имеет 3 units» нельзя превращать в фразу «D быстрее другого runtime». У модели нет секунд, CPU cycles, памяти процесса и внешнего вызова, а значит сравнивать ей нечего.'),
paragraph('Историческая рамка здесь тоже нужна. К октябрю 2021 существовал release D 2.097.2; его record и фиксированный source snapshot дают проверяемую точку для терминов DRuntime. Но название версии не заменяет command line, код application или collected evidence. Если нужно сопоставить две среды, сначала фиксируют по одному request и один ожидаемый output, затем одинаково описывают compiler/runtime/platform и только после этого читают реальные результаты.'),
heading('Минимальный JS-пример: читаем trace, а не придумываем профиль'),
paragraph('Следующий фрагмент использует экспортированную fixture. Он не вызывает D API, не открывает сокет и не строит benchmark. Он показывает те trace entries, которые нужны, чтобы отделить успешный path от учебной границы allocation. Если порядок исчезнет, это будет видно до обсуждения реального profiler.'),
codeBlock(traceExample),
paragraph('На heavy-ветке trace содержит <code>gc-boundary</code>, обе неисполняемые external boundaries и success encode. На reuse-ветке есть те же handler stages и boundaries, но нет пересечения порога 8. У обоих 9 work units. Это делает проверку строгой: если reuse «выигрывает» только потому, что пропустил validation или encode, assertion о полном пути станет ложным.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'<strong>Симптом.</strong> Зафиксируйте один request, result и error response, которые вызывают вопрос. Не объединяйте их с общими рассказами о нагрузке.',
'<strong>Причина-гипотеза.</strong> Привяжите подозрение к конкретному участку decode, validation, compute, encode, GC boundary, FFI boundary или I/O boundary. «Runtime» не является участком.',
'<strong>Проверка.</strong> Постройте trace valid и invalid inputs. У valid должны быть decode/validation/compute/encode, у invalid — явный error path без compute и external boundaries.',
'<strong>Проверка budget.</strong> Сравните варианты только после равенства body, status и work units. Отдельно покажите, какой из них пересёк выбранный порог.',
'<strong>Действие.</strong> Если изменился только model allocation, готовьте маленькую обратимую правку локального состояния. Если след указывает на FFI/I/O, сначала описывайте contract и тестируйте границу отдельно.',
'<strong>Следующий шаг.</strong> Для фактической производительности соберите реальный профиль с версией compiler/DRuntime, входом, окружением и методом. Не переносите учебный verdict между runtime.',
]),
heading('Ограничения и следующий проверяемый шаг'),
paragraph('Модель не подтверждает скорость, pause, GC algorithm, масштабирование, throughput, latency, cache behavior, memory footprint, thread scheduling, HTTP semantics, ABI compatibility конкретной библиотеки или эффект compiler flag. В ней нет D compiler, runtime, file, network, foreign code, profiler, benchmark или process-level telemetry. <code>@nogc</code> в источнике не запускается и не используется как оправдание отключить collector.'),
paragraph('Практический следующий шаг — перенести названия stages на один реальный handler, не меняя его поведения: сначала получить trace input/result/error, затем выбрать одну границу для независимого измерения. Если граница FFI, добавить отдельный contract test. Если граница I/O, добавить отдельный test timeout/error. Если остался вопрос о allocations, записать точную версию инструмента и повторяемый вход. Так система получает доказательство, а не переносимый ярлык «быстро».'),
],
commonSources,
);
const fieldArticle = createRevision(
{
slug: 'editorial-2021-10-field-d-runtime-service',
title: 'Разбор request в D-сервисе: allocation, условная GC-граница и безопасный откат',
categories: ['DLang', 'Backend', 'Надёжность'],
cover: '/assets/editorial/2021/d-runtime-diagnosis-2021.svg',
excerpt: 'Полевой маршрут для одного request: как не спутать рост учебных allocation units с паузой, сохранить evidence о FFI/I/O boundary, увидеть error route и выбрать rollback-safe действие.',
readingMinutes: 15,
},
[
paragraph('В trace учебного request heavy-вариант пересёк allocation budget 6 и получил отметку <code>gc-boundary</code>. Рядом есть FFI и I/O boundaries, а invalid input уходит в error response. Самая дорогая ошибка здесь — назвать отметку реальной паузой, обвинить внешнюю систему без вызова или сразу менять конфигурацию runtime. Тогда исчезают и причина, и возможность безопасно откатить change.'),
paragraph('Это не отчёт о production-инциденте. В нём нет реального профиля, сервера, D compiler, DRuntime, HTTP, foreign code или I/O. Есть один детерминированный in-memory request, два варианта одной обработки и error input. Его цель — собрать evidence в правильном порядке: result, stage trace, заданные units, выбранная boundary и только потом действие. Такая дисциплина полезна до того, как появятся цифры настоящего инструмента.'),
heading('Собираем evidence до изменения runtime'),
paragraph('Первый набор evidence небольшой: request id, нормализованный вход, success body либо error body, последовательность stages, allocation/work units модели, budget и список external boundaries. Значения, похожие на время, здесь запрещены: fixture не показывает миллисекунды, CPU или память процесса. Если соседняя система говорит о паузе, это отдельный факт с отдельным источником, а не расшифровка записи <code>gc-boundary</code>.'),
paragraph('В valid варианте body остаётся <code>{"requestId":"runtime-d-training-42","total":42}</code>. Heavy-вариант получает 12 allocation units и не проходит budget 6. Reuse-вариант получает 3 units и проходит budget. Оба имеют 9 work units. Такое доказательство ещё не отвечает, какой код нужно менять в D. Оно только запрещает одновременно менять contract handler и считать, что уменьшение model units уже стало оптимизацией процесса.'),
dataTable(
'Как классифицировать наблюдение до исправления',
['Наблюдение', 'Что фактически есть', 'Вероятная граница', 'Первое безопасное действие'],
[
['heavy > budget', '12 units при budget 6, body равен reuse', 'учебные временные состояния', 'сохранить два trace и проверить локальный reuse change при том же output'],
['есть <code>gc-boundary</code>', 'пересечён порог 8 units, поле <code>not-a-runtime-pause</code>', 'граница для будущего profiling', 'не объявлять pause; выбрать реальный метод измерения отдельно'],
['есть FFI/I/O boundary', '<code>invocation: not-performed</code>', 'контракт внешнего вызова ещё не проверен', 'не обвинять зависимость; собрать owner, input/output и отдельный test'],
['validation-error', 'status 400 и <code>encode-error</code>', 'ошибка входа до compute', 'сохранить error input и не применять success-path оптимизацию как исправление'],
['result различается', 'success body не совпадает', 'нарушен contract handler', 'остановить локальную оптимизацию и вернуть равный результат прежде budget'],
],
),
heading('Читаем error route так же внимательно, как success'),
paragraph('Проверка только счастливого запроса всегда оставляет дыру. В fixture invalid operation <code>divide</code> декодируется, отвергается validation и проходит через <code>encode-error</code>. Он не доходит до compute, FFI или I/O. Это даёт ясную отрицательную проверку: изменение buffer reuse не должно заставить ошибочный вход исчезнуть, стать успехом или неожиданно пересечь external boundary.'),
codeBlock(diagnosisExample),
paragraph('Такой error trace нужен и при разборе возможного allocation growth. Если heavy-вариант создаёт дополнительные units до validation, а invalid request больше не виден, нельзя сказать «мы сократили pressure». Возможно, мы просто перестали обрабатывать некорректный вход тем же контрактом. Поэтому fixture одновременно проверяет same handler result для success, явный status 400 для error и отсутствие compute/FFI/I/O в отрицательной ветке.'),
heading('Отделяем условную GC-границу от причины паузы'),
paragraph('У записи <code>gc-boundary</code> ровно одно значение: заданный счётчик пересёк заданный порог. Эта запись полезна, потому что показывает место, где команда договорилась задать фактический вопрос. Она не говорит, был ли GC, как он работал, остановил ли потоки, сколько длилась пауза и связана ли она с request. Документация D описывает automatic memory management, но переход от общего механизма к конкретному симптому всегда требует tool output и условий запуска.'),
paragraph('Безопасный отчёт поэтому выглядит скромно: «в учебной модели 12 units пересекают budget 6; boundary помечена; фактического profile нет». Небезопасный отчёт добавляет к этому «из-за GC endpoint зависает». Второй текст звучит увереннее, но у него нет входа, версия, метод, график или trace реального процесса. Практическая работа начинается с первого текста: он позволяет назначить следующий эксперимент и не переписать систему по впечатлению.'),
figure(
'/assets/editorial/2021/d-runtime-diagnosis-2021.svg',
'Диагностический маршрут для учебного request в D-сервисе: сначала сохранить result, error и trace; затем проверить равенство handler result, allocation budget, модельную GC-границу и неисполняемые FFI/I/O boundaries. При любом несоответствии выбрать обратимое локальное действие, а не менять runtime глобально.',
'Маршрут разделяет contract handler, учебный allocation budget и внешнюю границу. Ни одна карточка не называет model unit профилем или pause.',
),
heading('Rollback-safe действие: откатываем гипотезу, не evidence'),
paragraph('Если heavy и reuse возвращают один body, а отличается только model allocation, допустима маленькая обратимая правка: временное состояние в одном участке заменяется на повторное использование, а valid и invalid traces остаются рядом с change. Откат в таком случае — вернуть локальный путь и снова получить прежний trace. Нельзя называть rollback-safe глобальное отключение GC, смену allocator или переписывание foreign code без теста внешнего договора: такие действия расширяют границу и могут скрыть исходный симптом.'),
paragraph('Если body различается, error route исчезает или boundary стала выполнять внешний вызов, действие другое: остановить оптимизацию, сохранить samples и восстановить прежний contract handler. Здесь важнее не сделать «быстрый» patch, а не потерять смысл ошибки. После этого можно отдельно решать, нужна ли новая схема данных, другой API или реальное измерение. Смешивать этот разбор с одной настройкой runtime нельзя: разная причина требует разного owner и обратимости.'),
dataTable(
'Выбор обратимого действия',
['Подтверждённая ситуация', 'Что меняем', 'Что сохраняем', 'Чего не делаем'],
[
['равный output, heavy выше budget, reuse внутри', 'один локальный temporary path', 'оба valid trace, budget и same-result assertion', 'не объявляем оптимизацию production без profile'],
['условная GC boundary без real evidence', 'ничего в runtime', 'input, trace, версия будущего инструмента', 'не включаем/выключаем GC по модели'],
['FFI/I/O boundary подозрительна', 'отдельный contract test границы', 'intent, owner, expected error/result', 'не приписываем внешнему коду вызов, которого не было'],
['invalid input изменил маршрут', 'возвращаем validation/error contract', 'invalid sample и error trace', 'не сравниваем allocation до восстановления error route'],
['успешный body изменился', 'останавливаем локальную правку', 'expected body и diff result', 'не прячем разницу за новой версией response'],
],
),
heading('Источники обозначают границы, а не готовый диагноз'),
paragraph('Для терминов D я сверяю official language specification: automatic memory management, <code>@nogc</code> и ABI. Для исторической точки использован опубликованный до октября 2021 release D 2.097.2 и exact DRuntime source snapshot его тега. Ни один из этих источников не содержит profile данного request, потому что такого request не существовало. Поэтому они поддерживают только аккуратные утверждения о языке и границе ответственности.'),
paragraph('Такой подход особенно важен для FFI. ABI объясняет, почему граница не исчезает из архитектуры, но не описывает particular foreign function, её ownership или возможный blocking. Аналогично источник о GC помогает назвать механизм, но не превращает 12 учебных units в измеренную pause. Чем выше соблазн сделать вывод о реальной среде, тем важнее отдельно сохранить command, версию, вход, платформу и исходные results.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'<strong>Симптом.</strong> Сохраните один success или error request вместе с result/body. Не исправляйте runtime до появления конкретного input.',
'<strong>Причина-гипотеза.</strong> Разделите contract, model allocation, model GC boundary, FFI boundary и I/O boundary. Одно наблюдение не обязано объяснять остальные.',
'<strong>Проверка результата.</strong> Сравните success body и status two variants; затем прогоните invalid input и убедитесь, что validation-error/encode-error сохранились.',
'<strong>Проверка бюджета.</strong> Если result одинаков, проверьте allocation units против явно записанного budget. Смотрите на GC entry только как на пометку модели.',
'<strong>Действие.</strong> При локальном budget difference меняйте один обратимый temporary path. При FFI/I/O выбирайте отдельный contract test. При разном result или error route сначала восстановите handler contract.',
'<strong>Проверка после действия.</strong> Повторите те же valid и invalid samples, сохраните traces и только затем назначьте реальный profiler с описанным окружением.',
]),
heading('Ограничения и следующий проверяемый шаг'),
paragraph('Полевой маршрут не даёт production SLA, benchmark result, замер паузы, масштабирование, rate, memory footprint, сведения о compiler flags или DRuntime configuration. Он не исполняет D, FFI, file, network, HTTP, database, process, GC или profiler. Условные units не имеют единицы времени и не должны попадать в dashboard как metric. Откат в таблице — образец безопасного порядка, а не команда для чужой инфраструктуры.'),
paragraph('Следующий проверяемый шаг для своего сервиса — написать один integration-level trace вокруг выбранного handler без чувствительных данных, сохранить valid и invalid inputs, а затем выбрать ровно одну реальную границу для измерения. Если trace показывает внешний вызов, сначала тестируем его договор. Если одновременно меняются result и units, возвращаем contract. Если остаётся один вопрос про allocation, тогда можно сравнить фактические данные в зафиксированной среде. Так расследование не обещает лишнего и оставляет путь к следующему доказательству.'),
],
commonSources,
);
export const revisions = [practiceArticle, mechanismArticle, fieldArticle]
.map(({ proseLength, ...revision }) => revision);
const isMainModule = process.argv[1]
&& resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMainModule) {
if (process.argv.includes('--print-revisions')) {
process.stdout.write(JSON.stringify(revisions));
} else if (process.argv.includes('--verify-fixture')) {
const fixture = runDRuntimeServiceFixture();
if (!Object.values(fixture.assertions).every(Boolean)) {
throw new Error('fixture assertions must all be true');
}
process.stdout.write(JSON.stringify(fixture, null, 2) + '\n');
} else {
process.stderr.write('Usage: node web/scripts/upgrade-2021-10.mjs --print-revisions | --verify-fixture\n');
}
}