import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function paragraph(text) { return '
' + text + '
'; } function heading(text) { return '' + escapeHtml(Array.isArray(lines) ? lines.join('\n') : lines) + '';
}
function figure(src, alt, caption) {
return '@nogc; он ограничивает проверяемый 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: decode → validation → compute → encode. Рядом отметим две видимые границы — 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',
['Поле', 'Фиксируем в модели', 'Зачем это нужно', 'Чего это не доказывает'],
[
['Вход', 'sum(19, 23) и идентификатор request', 'оба варианта получают одинаковые данные', 'формат настоящего HTTP-запроса или его нагрузку'],
['Успех handler', '{"requestId":"runtime-d-training-42","total":42}', 'сравниваем оптимизацию при равном результате', 'корректность любого бизнес-правила'],
['Бюджет', '6 allocation units', 'есть явный порог для учебной развилки', 'байты памяти, latency или CPU'],
['GC boundary', 'пересечение порога 8 в trace', 'видно, где модель ставит вопрос о GC', 'реальную паузу, алгоритм или schedule сборщика'],
['FFI / I/O', 'intent записан как boundary, invocation = not-performed', 'внешняя граница не исчезает из диагноза', 'вызов библиотеки, сеть, файл или блокировку'],
],
),
paragraph('У карточки нужен владелец следующей проверки, но не обязательно «владелец всего runtime». Автор handler отвечает за равенство result и error route. Тот, кто знает внешний contract, отвечает за отдельную проверку FFI или I/O. Если владельца ещё нет, boundary так и записывают неизвестной, а не подменяют её словом «D». Это снимает ложный выбор между «оптимизировать всё» и «ничего не делать»: можно сохранить контекст одного request и передать только нужный вопрос следующему человеку.'),
heading('Выбираем наблюдаемый профиль, а не название оптимизации'),
paragraph('В модели есть два варианта. allocation-heavy добавляет временные единицы на decode, validation, compute и encode. reuse записывает меньше единиц там, где мы условно переиспользовали промежуточное состояние. У обоих одинаковые вход, 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('Результат таблицы должен быть простым: allocation-heavy даёт тот же body, но 12 условных allocation units и выход за budget 6; reuse даёт тот же body, 3 units и остаётся внутри. Work units равны. Это сделано специально: если у вариантов разный output или разная работа handler, обсуждать allocation рано. Сначала возвращаем одинаковый контракт, потом меняем гипотезу о промежуточном состоянии.'),
heading('Как связать такую карточку с D, не выдумывая свойства runtime'),
paragraph('Официальная спецификация D описывает automatic memory management и отдельно атрибут @nogc. Для инженерного разговора отсюда полезна граница: ограничение на D-код не делает безопасной неизвестную часть за вызовом. Точно так же D ABI описывает взаимодействие с C ABI целевой системы, но один факт пересечения ABI не говорит, выделяет ли память библиотека, блокирует ли она поток и какой контракт ownership у её параметров. Эти вопросы нужно записать в trace и проверить на выбранной версии, а не угадывать по названию языка.'),
paragraph('Для исторической точки practice-заметки от 7 октября использован выпуск D 2.097.2, опубликованный до этой даты. Release record и точный снимок core/memory.d нужны здесь только для проверки терминов. Это не попытка восстановить профиль старого процесса: без исходника request, compiler options, линковки, операционной системы и данных такое утверждение было бы выдумкой. Поэтому следующий шаг остаётся узким: взять один живой endpoint и добавить к нему такой же request trace, а фактическое измерение провести отдельным инструментом и отдельно сохранить его условия.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'Симптом. Назовите один endpoint и один видимый результат: какой body или error code он должен вернуть. Не начинайте со слова «runtime».',
'Причина-гипотеза. Выберите один слой: временные объекты на пути decode/compute/encode, условная GC boundary, FFI boundary или I/O boundary. У каждой версии должна быть отдельная карточка.',
'Проверка. Прогоните одинаковый in-memory input через два учебных варианта. Убедитесь, что body, status и work units совпали, а trace содержит все выбранные границы.',
'Действие. Если только allocation budget различается, подготовьте локальный change, который сохраняет output. Не меняйте глобальную GC-конфигурацию и не переписывайте foreign code до отдельного доказательства.',
'Повторная проверка. После change повторите исходный input, сохраните trace до/после и добавьте error input. Успех без error route не закрывает handler.',
'Следующий уровень. Лишь затем выбирайте реальный profiler и записывайте compiler, DRuntime, платформу, вход и окно наблюдения. Учебная fixture к этому готовит вопрос, но не заменяет ответ.',
]),
heading('Ограничения и следующий проверяемый шаг'),
paragraph('Эта модель не запускает D compiler, D runtime, GC, HTTP, сеть, файл, database, FFI, profiler или benchmark. Её gc-boundary — запись о пересечении заданного порога; она не подтверждает 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 как учебную цепочку decode → validation → compute → encode. У неё есть 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 мы требуем все шесть следов: decode, validation, compute, ffi-boundary, io-boundary, encode. Для invalid input требуем другой, но тоже явный путь: decode, validation-error, encode-error. Compute и внешние boundaries туда не входят. Это важнее счастливого ответа: если handler молча теряет error route, меньший allocation count не является улучшением.'),
dataTable(
'Наблюдаемые участки учебного request path',
['Участок', 'Что записывает fixture', 'Вопрос к реальной системе', 'Что нельзя заключить'],
[
['Decode', 'shape входа, 2 work units, вариант allocation', 'какие данные действительно преобразуются на входе?', 'что HTTP parser или framework работает именно так'],
['Validation', 'accepted либо invalid-operation', 'какая ветка завершает request до compute?', 'что validation не создаёт объектов в реальном запуске'],
['Compute', 'операция 19 + 23, 4 work units', 'какой промежуточный state необходим результату?', 'что арифметика является bottleneck'],
['GC boundary', 'порог 8 units пересечён только heavy-веткой', 'где начать фактическую проверку allocation/GC?', 'наличие, длину или причину pause'],
['FFI / I/O', 'invocation: not-performed', 'где нужен отдельный 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 модель добавляет gc-boundary с полем measurement: "not-a-runtime-pause". Это предохранитель против самой частой подмены: «увидели границу — измерили 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 есть ffiIntent: "normalize-scalar" и ioIntent: "write-audit-record". Fixture пишет их как boundaries с invocation: "not-performed". Таким образом код не притворяется, что вызвал C-функцию, открыл файл, ушёл в сеть или увидел процесс. Но будущий интеграционный тест уже знает, где должен появиться owner, contract данных, ошибка и cancellation.'),
paragraph('D ABI описывает совместимость с C ABI целевой системы, однако ABI не заменяет договор вызова. У реальной границы отдельно проверяют lifetime аргументов, формат ownership, error code, поток, блокировку, allocator и возможность повторить операцию. Для I/O отдельно нужны protocol, timeout, idempotency, права и данные. Ни один из этих пунктов не выводится из @nogc, из названия 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 содержит gc-boundary, обе неисполняемые external boundaries и success encode. На reuse-ветке есть те же handler stages и boundaries, но нет пересечения порога 8. У обоих 9 work units. Это делает проверку строгой: если reuse «выигрывает» только потому, что пропустил validation или encode, assertion о полном пути станет ложным.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'Симптом. Зафиксируйте один request, result и error response, которые вызывают вопрос. Не объединяйте их с общими рассказами о нагрузке.',
'Причина-гипотеза. Привяжите подозрение к конкретному участку decode, validation, compute, encode, GC boundary, FFI boundary или I/O boundary. «Runtime» не является участком.',
'Проверка. Постройте trace valid и invalid inputs. У valid должны быть decode/validation/compute/encode, у invalid — явный error path без compute и external boundaries.',
'Проверка budget. Сравните варианты только после равенства body, status и work units. Отдельно покажите, какой из них пересёк выбранный порог.',
'Действие. Если изменился только model allocation, готовьте маленькую обратимую правку локального состояния. Если след указывает на FFI/I/O, сначала описывайте contract и тестируйте границу отдельно.',
'Следующий шаг. Для фактической производительности соберите реальный профиль с версией 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. @nogc в источнике не запускается и не используется как оправдание отключить 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 и получил отметку gc-boundary. Рядом есть FFI и I/O boundaries, а invalid input уходит в error response. Самая дорогая ошибка здесь — назвать отметку реальной паузой, обвинить внешнюю систему без вызова или сразу менять конфигурацию runtime. Тогда исчезают и причина, и возможность безопасно откатить change. Цена ошибки — потратить время на изменение runtime без доказанного источника паузы.'),
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 или память процесса. Если соседняя система говорит о паузе, это отдельный факт с отдельным источником, а не расшифровка записи gc-boundary.'),
paragraph('В valid варианте body остаётся {"requestId":"runtime-d-training-42","total":42}. 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'],
['есть gc-boundary', 'пересечён порог 8 units, поле not-a-runtime-pause', 'граница для будущего profiling', 'не объявлять pause; выбрать реальный метод измерения отдельно'],
['есть FFI/I/O boundary', 'invocation: not-performed', 'контракт внешнего вызова ещё не проверен', 'не обвинять зависимость; собрать owner, input/output и отдельный test'],
['validation-error', 'status 400 и encode-error', 'ошибка входа до compute', 'сохранить error input и не применять success-path оптимизацию как исправление'],
['result различается', 'success body не совпадает', 'нарушен contract handler', 'остановить локальную оптимизацию и вернуть равный результат прежде budget'],
],
),
heading('Читаем error route так же внимательно, как success'),
paragraph('Проверка только счастливого запроса всегда оставляет дыру. В fixture invalid operation divide декодируется, отвергается validation и проходит через encode-error. Он не доходит до 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('У записи gc-boundary ровно одно значение: заданный счётчик пересёк заданный порог. Эта запись полезна, потому что показывает место, где команда договорилась задать фактический вопрос. Она не говорит, был ли 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, @nogc и 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([
'Симптом. Сохраните один success или error request вместе с result/body. Не исправляйте runtime до появления конкретного input.',
'Причина-гипотеза. Разделите contract, model allocation, model GC boundary, FFI boundary и I/O boundary. Одно наблюдение не обязано объяснять остальные.',
'Проверка результата. Сравните success body и status two variants; затем прогоните invalid input и убедитесь, что validation-error/encode-error сохранились.',
'Проверка бюджета. Если result одинаков, проверьте allocation units против явно записанного budget. Смотрите на GC entry только как на пометку модели.',
'Действие. При локальном budget difference меняйте один обратимый temporary path. При FFI/I/O выбирайте отдельный contract test. При разном result или error route сначала восстановите handler contract.',
'Проверка после действия. Повторите те же 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');
}
}