В сервисе появляется фраза «этот endpoint тормозит из-за runtime D». Проблема не в том, что в ней упомянут GC. В ней нет входа, результата, границы и доказательства. Цена такой формулировки — случайная правка: отключить что-то глобально, переписать FFI-вызов или добавить кеш, а затем не суметь объяснить, какой участок запроса вообще изменили.
Для начала я бы не пытался измерить весь сервис и не переносил ожидание «быстро» из PHP или JavaScript. Возьмём один учебный request: decode → validation → compute → encode. Рядом отметим две видимые границы — FFI и I/O — но не выполним ни foreign call, ни ввод-вывод. В этой статье allocation и work считаются детерминированными единицами модели. Это не байты, не время CPU, не latency и не профиль настоящего runtime.
Сначала формулируем вопрос, который можно закрыть
У одного endpoint должна быть короткая карточка. Что приходит на вход? Какой успешный результат остаётся неизменным? Где request может закончиться ошибкой? Какая граница интересует сейчас: построение временных объектов, наблюдаемая точка GC, вызов за ABI или выход в I/O? Пока в карточке смешаны все эти вопросы, команда сравнивает несравнимое: правило validation, стоимость encoding и поведение внешней библиотеки.
Такое сужение не обедняет расследование. Оно отделяет контракт handler от гипотезы о ресурсе. Если оба учебных варианта возвращают один body, но один превышает выбранный allocation budget, мы получили повод посмотреть на создание промежуточного состояния. Мы ещё не получили право назвать вариант медленнее, включать настройку DRuntime или обещать паузу в продуктивном процессе.
| Поле | Фиксируем в модели | Зачем это нужно | Чего это не доказывает |
|---|---|---|---|
| Вход | 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 | внешняя граница не исчезает из диагноза | вызов библиотеки, сеть, файл или блокировку |
У карточки нужен владелец следующей проверки, но не обязательно «владелец всего runtime». Автор handler отвечает за равенство result и error route. Тот, кто знает внешний contract, отвечает за отдельную проверку FFI или I/O. Если владельца ещё нет, boundary так и записывают неизвестной, а не подменяют её словом «D». Это снимает ложный выбор между «оптимизировать всё» и «ничего не делать»: можно сохранить контекст одного request и передать только нужный вопрос следующему человеку.
Выбираем наблюдаемый профиль, а не название оптимизации
В модели есть два варианта. allocation-heavy добавляет временные единицы на decode, validation, compute и encode. reuse записывает меньше единиц там, где мы условно переиспользовали промежуточное состояние. У обоих одинаковые вход, work units и ответ. Значит, сравнение не прячет изменение результата за словом «оптимизация».
Бюджет здесь намеренно маленький и проектный. Его задача — сделать проверку бинарной: первый путь за границей, второй внутри. В реальном проекте такой порог сначала выбирают из цели конкретного endpoint и затем подтверждают инструментом, который подходит версии compiler, DRuntime и окружению. Не надо брать учебное число 6 как настройку heap, лимит процесса или триггер GC. Это номер строки в договоре fixture, а не команда для запуска.
Фиксируем результат и trace одним JS-примером
Fixture находится в этом же revision-модуле, поэтому пример можно исполнить без компилятора D и без инфраструктуры. Он не измеряет собственные JavaScript allocations. Он читает уже собранную модель и проверяет её assertions. Если кто-то поменяет путь так, что reuse начнёт давать другой body, тест остановится даже при красивом меньшем числе units.
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");
}
Результат таблицы должен быть простым: allocation-heavy даёт тот же body, но 12 условных allocation units и выход за budget 6; reuse даёт тот же body, 3 units и остаётся внутри. Work units равны. Это сделано специально: если у вариантов разный output или разная работа handler, обсуждать allocation рано. Сначала возвращаем одинаковый контракт, потом меняем гипотезу о промежуточном состоянии.
Как связать такую карточку с D, не выдумывая свойства runtime
Официальная спецификация D описывает automatic memory management и отдельно атрибут @nogc. Для инженерного разговора отсюда полезна граница: ограничение на D-код не делает безопасной неизвестную часть за вызовом. Точно так же D ABI описывает взаимодействие с C ABI целевой системы, но один факт пересечения ABI не говорит, выделяет ли память библиотека, блокирует ли она поток и какой контракт ownership у её параметров. Эти вопросы нужно записать в trace и проверить на выбранной версии, а не угадывать по названию языка.
Для исторической точки practice-заметки от 7 октября использован выпуск D 2.097.2, опубликованный до этой даты. Release record и точный снимок core/memory.d нужны здесь только для проверки терминов. Это не попытка восстановить профиль старого процесса: без исходника request, compiler options, линковки, операционной системы и данных такое утверждение было бы выдумкой. Поэтому следующий шаг остаётся узким: взять один живой endpoint и добавить к нему такой же request trace, а фактическое измерение провести отдельным инструментом и отдельно сохранить его условия.
Маршрут: симптом → причина → проверка → действие
- Симптом. Назовите один 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 к этому готовит вопрос, но не заменяет ответ.
Ограничения и следующий проверяемый шаг
Эта модель не запускает D compiler, D runtime, GC, HTTP, сеть, файл, database, FFI, profiler или benchmark. Её gc-boundary — запись о пересечении заданного порога; она не подтверждает pause. Её FFI и I/O entries — неисполненные границы; они не подтверждают вызов, ownership, retry или блокировку. Она также не моделирует concurrency, backpressure, scheduler, исключения D, сериализацию реального протокола и лимиты процесса.
Для своего проекта возьмите один нечувствительный test input и выпишите результат, error path, owner boundary и версию инструмента. Если после этого вопрос всё ещё звучит как «runtime D медленный», карточка недостаточно узкая. Если он звучит как «при том же result в encode создаётся лишнее промежуточное состояние», есть безопасный следующий эксперимент. Такой переход от ярлыка к проверке полезнее любой заранее выбранной настройки.
Проверяемые источники
- D 2.097.2: source snapshot of Automatic Memory Management specification — неизменяемый исходник официальной спецификации из тега v2.097.2; он задаёт терминологию automatic memory management, но не профиль конкретного запущенного сервиса
- D 2.097.2: source snapshot of @nogc function specification — неизменяемый исходник официальной спецификации атрибута
@nogc; он ограничивает проверяемый D-код и не доказывает свойства внешней библиотеки или операции ввода-вывода - D 2.097.2: source snapshot of Application Binary Interface specification — неизменяемый исходник официальной границы ABI для взаимодействия с C ABI целевой системы; наличие границы не говорит о стоимости или безопасности конкретного foreign call
- D 2.097.2: официальный release record — выпуск опубликован 9 августа 2021 года и служит проверяемой дооктябрьской точкой для терминов; статья не выводит из номера версии benchmark или runtime profile
- DRuntime 2.097.2: immutable snapshot core/memory.d — точная фиксация исходника DRuntime, на которую указывал тег v2.097.2; используется для исторической проверки терминов, не как замена измерения приложения