Анализ задержек
Глава 7 показала, что процессы несут на себе архитектуру: стрелки между модулями выводятся из шагов, а не рисуются вручную. Процессы также несут время — каждый шаг является вызовом, а вызов занимает какое-то его количество. Дайте интерфейсам на критическом пути бюджет latency, и компилятор свернёт эти бюджеты вдоль графа процесса в оценку наихудшего случая для каждого процесса — запрашиваемую точно так же, как любой другой факт о модели.
Это модель требования, а не измерения. latency: 200ms — это обещание провайдера, то, под чем подписывается ревьюер, публикуя интерфейс, — а не число, добытое из трейсинга. Анализатор отвечает на вопрос «если каждый провайдер сдержит своё обещание, насколько медленным может стать этот поток?». Это вопрос времени проектирования, и это ровно тот вопрос, на который статическая модель может честно ответить.
Поле latency
Заголовок раздела «Поле latency»Интерфейс принимает бюджет latency как голую длительность — без кавычек, без нового синтаксиса:
module Orders { interface getOrder { latency: 200ms }}Диапазон задаёт наилучший и наихудший случай раздельно:
grpc_unary charge { latency: 50ms-150ms}Значение лексится как обычный идентификатор (последовательность, начинающаяся с цифры и несущая буквы, уже лексится так, Глава 15) и читается инструментальным слоем анализа времени, не получая нового типа значения — latency является хорошо известным полем (Глава 9): обычными структурированными данными, которые просто интерпретирует именованный инструмент. Единица измерения берётся из семейства единиц времени calc — ms s min h d wk mo y, плюс алиасы с полным написанием и множественным числом (200ms, 2 hours, 3d), — а по соглашению это синхронный интерфейс: асинхронные и стримовые виды latency не несут, потому что время доставки — не то обещание, которое обязан держать производитель.
Значение не из этого семейства или непарсящееся — голое число, единица за пределами семейства (2w) — выдаёт предупреждение:
LATENCY_MALFORMED значение latency '2w' не является допустимой длительностью —используйте единицу времени (ms/s/min/h/d/wk/mo/y), например '200ms' или '50ms-500ms'Отсутствующий latency — это тихо. Отсутствие — не долг, о котором компилятор ноет сам по себе: интерфейс без заявленного бюджета просто вносит пробел в любую оценку, которая его вызывает (ниже), а если ваша команда хочет, чтобы каждый синхронный интерфейс объявлял его, — это политика покрытия, в которую вы включаетесь сами, а не встроенное требование.
Заметочный шаг тоже может нести latency, в теле { … }, оценивающем его как человеческое/бизнес-время:
process Onboard { Ops "verify docs" { latency: 1d-3d }}Неаннотированный заметочный шаг ничего не стоит.
Свёртка оценки
Заголовок раздела «Свёртка оценки»Имея аннотации latency, компилятор обходит каждый процесс и сворачивает оценку для процесса — интервальную арифметику над тем же IR, который порождает проекции flow, sequence и BPMN. Каждый лист оценивается как пара интервалов (system, business); композиция следует форме конструкции, которая его содержит: последовательность суммирует, if/else берёт диапазон своей самой широкой ветви, parallel join N берёт N-ю по величине наименьшую ветвь, select веерно расширяется до [0, …], граница повтора умножает стоимость попытки. Собственный вклад вызова — это заявленный latency его вызываемой стороны: нет latency — нет вклада, кроме пробела (ниже).
Две вещи делают эту свёртку заслуживающей доверия, а не просто механической:
System и business — раздельные оси, которые никогда не суммируются. Системное время — это вычисление/ответ: то, чего вызывающий реально ждёт синхронно. Бизнес-время — это человеческое или плановое ожидание: очередь на согласование, окно расчётов. Фрод-проверка, которая разрешается за 300 мс автоматического скоринга либо откатывается к однодневному ревью аналитика, — это system 300ms · business ≤1d, а не «от 300 мс до 1 дня»: день — это человеческая подстраховка, принципиально другой вид времени, нежели SLA ответа, и свёртка их в одно число позволила бы медленному человеческому шагу незаметно взорвать бюджет ответа, которого он никогда не касался. Живой бейдж печатает оба измерения рядом, опуская то, что равно нулю.
Заявленное обещание ограничивает собственные внутренности, но только со стороны system. Когда шаг вызывает интерфейс с latency, это число подменяет собой всё, что вызываемая сторона делает под капотом, — её собственные вызовы, её собственные пробелы, — по оси system. «Ответить за 200 мс, рассчитаться за дни» — это валидная, распространённая модель, а не противоречие: обещание ответа поглощает внутреннее системное время вызываемой стороны, а любое бизнес-ожидание внутри неё (согласование, плановая задача) проходит нетронутым в бизнес-итог вызывающей стороны. Если собственные смоделированные внутренности вызываемой стороны уже заведомо превышают заявленное ею обещание, это предупреждение NESTING_CONTRADICTION, и показывается более широкое, правдивое значение.
Строчные глифы сворачиваются как то, во что они разрешаются. Прогон-змейка \ и есть цепочка, поэтому сворачивается как цепочка: обещание головы ограничивает системную свёртку хвоста (вложение, а не сумма). Шаг-веер | сворачивается на уровне того шага, от которого он веером отходит, — там он и висит.
Верхняя граница либо конечна, либо OPEN, и результат OPEN несёт причину — одну из двух:
- Структурная — поток по-настоящему неограничен, пока вы не смоделируете таймаут: повторный вход
go, рекурсивная вставка подпроцесса,awaitили входящее ожидание, гоняемое наперегонки ни с чем. - Пробел — модели просто не хватает данных: отсутствующий или некорректный
latency,eachнад коллекцией неизвестного размера, неразобранное cron-выражение.
process ManualReview { Ops > Review.start as checkpoint Ops "escalate to senior" go checkpoint // возврат по циклу — структурно OPEN, не пробел в данных}Это различие важно, потому что пробел — это данные анализа, никогда не диагностика — компилятор не предупреждает вас за то, что вы оставили latency интерфейса неаннотированным, точно так же, как не предупреждает за незаполненное необязательное поле. Настоящая проблема — только некорректное значение (LATENCY_MALFORMED, выше), потому что это не отсутствующая информация, а неверная информация. Если вы хотите, чтобы пробелы валили сборку — «каждый синхронный интерфейс на этом пути обязан объявить бюджет», — это политика покрытия, которую вы пишете сами, а не поведение компилятора, навязанное каждой модели.
Чтение оценки: @estimatedLatency
Заголовок раздела «Чтение оценки: @estimatedLatency»Результат свёртки запрашивается через @estimatedLatency — builtin на субъекте-процессе или субъекте-подпроцессе (Селекторы, Глава 31). Это первый точечный (dotted) builtin — зарезервированное поддерево, а не одиночный геттер:
| Геттер | Значение |
|---|---|
@estimatedLatency | наихудший случай по system — SLA времени ответа, естественная цель для бюджета |
@estimatedLatency.min | наилучший случай по system |
@estimatedLatency.system / .business | наихудший случай по каждой оси, бюджетируется раздельно |
@estimatedLatency.bounded | false тогда и только тогда, когда сохраняется структурная причина OPEN — один лишь пробел данных её никогда не вызывает |
@estimatedLatency.unestimated | количество неоценённых (пробельных) листьев, питающих оценку |
Каждый геттер значения разрешается в конечную верхнюю границу, когда свёртка замыкается, в наихудшую оценённую величину, которую всё ещё можно доказать при пробеле данных, либо в ∞, когда структурная причина держит её по-настоящему открытой. ∞ здесь — настоящее значение: оно сравнивается как большее любого конечного бюджета, так что неограниченный поток всегда проваливает проверку бюджета, а не тихо проходит её. На субъекте, не являющемся процессом, геттеры значений нейтральны (undefined), а .bounded читается как true — пусто-ограничено (vacuously bounded), так что случайный where (not @estimatedLatency.bounded) никогда не пометит остальную модель по ошибке.
Сравнения осведомлены о длительности (duration-aware): любая сторона = != < <= > >= может быть голым литералом длительности или геттером поля в форме latency, и вычислитель приводит её к секундам для сравнения:
view SlowFlows { show process and where (@estimatedLatency > 500ms)}Бюджеты — обычные политики
Заголовок раздела «Бюджеты — обычные политики»В языке нет конструкции бюджета — бюджет это поле-длительность, которое вы объявляете, и политика (Глава 32), которая его читает. Это оставляет вопрос «что считается слишком медленным» решением проекта, а не мнением компилятора, а также означает, что бюджет компонуется с любой другой уже знакомой вам идиомой политик: severity, waiver’ами except, областью видимости по модулю.
Собственный заголовок процесса несёт бюджет как обычное поле — в той же зоне заголовка, что уже хранит описание или aspect team:
process CheckOrder { latencyBudget: 500ms Customer > Orders.getOrder}latencyBudget не имеет для языка никакого значения сам по себе — это просто поле-длительность, которое процесс случайно несёт. Именно политика придаёт ему силу:
policy LatencyBudgets { severity: warning forbid process and where (@latencyBudget) and where (@estimatedLatency > @latencyBudget)}forbid подхватывает каждый процесс, который одновременно объявляет latencyBudget и чья оценённая наихудшая величина по system превышает его, — процесс без поля бюджета просто выходит за рамки правила, а не является нарушением. Поскольку @estimatedLatency и @latencyBudget сравниваются по одной и той же duration-aware оси, правая часть > может быть литералом (@estimatedLatency > 500ms) или другим геттером поля (> @latencyBudget) без какой-либо особой обработки.
Тот же паттерн покрывает два вопроса, которые команды обычно хотят задать вслед за правилом бюджета, — покрытие (объявил ли вообще каждый синхронный интерфейс на этом пути своё обещание?) и ограниченность (есть ли на этом пути что-то по-настоящему неограниченное?):
policy LatencyCoverage { severity: warning require rest_create or rest_read or grpc_unary: this and where (@latency) except Legacy.Mainframe.submit "vendor gives no SLA; tracked in JIRA-1234"}
policy BoundedFlows { forbid process and where (not @estimatedLatency.bounded)}LatencyCoverage — это правило формы над интерфейсами (каждый синхронный должен нести поле latency, с именованным, обоснованным waiver’ом там, где это по-настоящему невозможно); LatencyBudgets и BoundedFlows — правила формы над процессами, читающие вывод свёртки. Все три — обычные политики forbid/require/except: анализу времени не понадобилась новая форма политики.
Поверхности
Заголовок раздела «Поверхности»Оценка — не только цель запроса: проекция процесса/потока рендерит её напрямую — бейдж на заголовке процесса, линейка вдоль критического пути, подписи по каждому прыжку на шагах, потребляющих больше всего времени. Headless-экспорт (сервер, CLI, MCP) переносит тот же бейдж и подписи в экспортированные SVG/PNG, а дифф между двумя ревизиями процесса показывает Δ оценки — вызов, чей заявленный latency расширился, или интерфейс, потерявший свой бюджет, отображается как регрессия по времени, а не просто текстовое изменение. Всё это опирается на ту же свёртку оценки, которую описывает эта глава; поверхность языка — поле, builtin, бюджеты-как-политики — это то, что делает эти проекции возможными.
Собираем вместе
Заголовок раздела «Собираем вместе»Минимальный, но полный срез — интерфейс с бюджетом, процесс, который его вызывает и заявляет собственную цель, и политика, которая эту цель обеспечивает:
module Orders { interface getOrder { latency: 200ms }}
process CheckOrder { latencyBudget: 500ms Customer > Orders.getOrder}
policy LatencyBudgets { severity: warning forbid process and where (@latencyBudget) and where (@estimatedLatency > @latencyBudget)}Единственный вызов CheckOrder оценивается в 200 мс по system — с большим запасом внутри бюджета в 500 мс, так что политике нечего отмечать. Добавьте второй вызов, чей интерфейс не несёт latency, или такой, чья одна лишь заявленная стоимость превышает 500 мс, — и LatencyBudgets начнёт срабатывать.
latency: 200ms(или диапазон,50ms-500ms) — поле голой длительности на синхронном интерфейсе: обещание провайдера на наихудший случай, читаемое семейством единиц времени calc (ms s min h d wk mo y+ алиасы). Значение не из этого семейства выдаёт предупреждениеLATENCY_MALFORMED; отсутствующее — тихо.- Тело
{ latency: … }заметочного шага оценивает его как бизнес-время; неаннотированные заметки ничего не стоят. - Компилятор сворачивает
latencyвдоль IR каждого процесса в оценку(system, business)— две оси, которые никогда не суммируются. Заявленное обещание вызова ограничивает только смоделированные системные внутренности вызываемой стороны; бизнес-ожидания проходят насквозь. - Верхняя граница либо конечна, либо OPEN, и OPEN несёт причину: структурную (
go, рекурсивная вставка, ожидание без таймаута — по-настоящему неограничено) или пробел (отсутствующие/некорректные данные). Пробелы — это данные анализа, никогда не диагностики. @estimatedLatency(с.min/.system/.business/.bounded/.unestimated) — первый точечный builtin, запрашиваемый на субъекте-процессе/подпроцессе, осведомлённый о длительности в сравнениях.- Бюджеты — обычные политики над обычными полями:
latencyBudgetна процессе, правилоforbid, сравнивающее его с@estimatedLatency. В языке нет выделенной конструкции бюджета. - Та же оценка управляет бейджем, линейкой и подписями по прыжкам в проекции flow/BPMN — вживую и в headless-экспорте — и показывается как Δ в диффах.
Что дальше
Заголовок раздела «Что дальше»Разобранные проекты, начиная с Главы 25: SaaS-бэкенд, сводят процессы, проекции и политики вместе на целой системе — бюджеты задержки встраиваются в тот же набор инструментов везде, где у потока есть контракт времени ответа, который стоит обеспечивать.