Перейти к содержанию
V1 V2

Запуск против времени сборки: почему Kora переносит работу влево

28 августа 2026

Производительность фреймворков обычно обсуждается во время выполнения. Насколько быстро запускается приложение? Сколько запросов в секунду оно может обработать? Сколько памяти потребляет? Какую задержку фреймворк добавляет к запросу?

Это важные вопросы, но они скрывают более фундаментальный архитектурный выбор: когда должна происходить работа фреймворка?

Фреймворк может откладывать большой объём работы до запуска приложения. Он может сканировать классы, изучать аннотации, обнаруживать компоненты, разрешать отношения зависимостей, строить метаданные, конструировать структуры прокси, обнаруживать маршруты, интерпретировать маппинги и собирать модель выполнения после того, как JVM уже запущена.

Или он может выполнять большую часть этой работы раньше, во время компиляции.

Фреймворк Kora намеренно выбирает вторую модель.

Её центральный компромисс можно резюмировать одной строкой:

работа выполнения → работа этапа компиляции

Граф приложения анализируется и генерируется во время компиляции. Отношения зависимостей проверяются до запуска сервиса. HTTP-обработчики, репозитории, мапперы, AOP-обёртки, реализации конфигурации и другая инфраструктура могут генерироваться как обычный исходный код Java или Kotlin. Во время выполнения Kora стартует из структур, которые уже были обнаружены и проверены тулчейном компилятора, вместо пересборки тех же архитектурных знаний с нуля.

У этого дизайна очевидная стоимость: компиляции предстоит больше работы.

Процессоры аннотаций и KSP-процессоры должны выполняться. Сгенерированный исходный код должен создаваться и компилироваться. Изменения, затрагивающие граф приложения, могут инвалидировать выводы процессоров. Поэтому полностью чистая сборка может проводить больше времени в компиляторе, чем фреймворк, чья архитектура собирается динамически при запуске.

Kora не скрывает этот компромисс. Её собственное позиционирование производительности различает запуск и готовность от чистых сборок и кешированных сборок и явно описывает генерацию графа на этапе компиляции как стоимость, уплаченную во время сборки в обмен на избегание сканирования, рефлексии и связывания во время выполнения, пока сервис загружается.

Поэтому интересный вопрос — не:

Бесплатна ли генерация на этапе компиляции?

Нет.

Полезный вопрос:

Где должна жить стоимость, и сколько раз мы её заплатим?

Вот где меняется экономика.

Сборка обычно происходит один раз для артефакта.

Этот артефакт может затем запускаться много раз.

Он может запускаться в интеграционных тестах, black-box тестах, staging, canary, производственных развёртываниях, горизонтальном автоскейлинге, перезапусках Pod, заменах узлов, восстановлении spot, аварийном восстановлении и средах разработчиков.

Если немного дополнительной работы сборки убирает повторяющуюся работу запуска, обмен может стать выгодным очень быстро.

Поэтому модель Kora — не в первую очередь о том, чтобы сделать компиляцию медленнее ради ускорения бенчмарка. Она о переносе детерминированной работы фреймворка в фазу, где её можно проверить один раз, материализовать как исходный код, кешировать, где возможно, и переиспользовать каждым экземпляром выполнения, созданным из результирующего артефакта.

Именно это означает «перенести работу влево».


Модель с тяжёлым выполнением

Чтобы понять компромисс, рассмотрим, что фреймворку нужно знать до выполнения обычного запроса.

Ему нужно знать такие вещи, как:

  • какие компоненты существуют;
  • какой компонент зависит от какого интерфейса;
  • какая реализация должна удовлетворить эту зависимость;
  • какой HTTP-маршрут сопоставляется с каким методом;
  • какой маппер преобразует тело запроса;
  • какой маппер сериализует ответ;
  • какая реализация репозитория выполняет запрос;
  • какие аспекты оборачивают метод;
  • какой объект конфигурации должен быть создан;
  • какие компоненты телеметрии и отказоустойчивости применяются;
  • в каком порядке должны запускаться компоненты жизненного цикла.

Есть два широких способа получить эту информацию.

Первый — обнаружить её во время выполнения.

Упрощённый путь запуска с тяжёлым выполнением выглядит так:

JVM запускается
    ↓
начинается загрузка фреймворка
    ↓
обнаружение classpath / метаданных
    ↓
изучение аннотаций
    ↓
обнаружение компонентов
    ↓
разрешение зависимостей
    ↓
создание прокси / метаданных
    ↓
сборка маршрутов и мапперов
    ↓
инициализация приложения
    ↓
готово

Точная механика различается между фреймворками. Некоторые сильно используют рефлексию, некоторые — сгенерированные индексы, некоторые — улучшение байткода, некоторые кешируют метаданные, а современные фреймворки часто переносят выбранные операции на этап сборки.

Архитектурный момент остаётся: любая информация, не финализированная до развёртывания, должна реконструироваться, интерпретироваться или проверяться позже.

Эта работа сидит на критическом пути запуска.

Она платится каждый раз, когда запускается новый процесс.


Модель Kora: вычисли структуру до выполнения

Kora переносит большую часть этой структурной работы в конвейер компилятора.

Упрощённый поток становится:

исходный код
    ↓
обработка аннотаций Java / KSP Kotlin
    ↓
изучение объявлений Kora
    ↓
разрешение графа и контрактов
    ↓
генерация исходного кода
    ↓
компиляция сгенерированного исходного кода
    ↓
упаковка артефакта

Выполнение затем становится ближе к:

JVM запускается
    ↓
загрузка кода предсобранного графа приложения
    ↓
конструирование и инициализация компонентов
    ↓
готово

У приложения всё ещё есть реальная работа инициализации.

Пулу базы данных может понадобиться запуститься.

Kafka-консьюмеру может понадобиться инициализироваться.

HTTP-сервер должен привязать сокет.

Экспортёры телеметрии могут создавать ресурсы.

Пользовательские компоненты могут выполнять дорогую инициализацию.

Перенос работы в компиляцию не устраняет запуск приложения.

Что он убирает — класс обнаружения фреймворка и структурной сборки, который можно знать заранее.

Это различие важно.

Kora не утверждает, что компиляция может заранее открыть завтрашние соединения PostgreSQL.

Она говорит, что компиляция уже может знать, какой компонент создаст клиент PostgreSQL, какая конфигурация ему нужна, какой репозиторий от него зависит, какой контроллер зависит от репозитория и как эти объекты соединяются.

Эта архитектура не должна заново обнаруживаться при каждом запуске процесса.


Время сборки и время запуска не симметричны

На первый взгляд перенос одной секунды из запуска в компиляцию может выглядеть как обмен с нулевой суммой:

+1 секунда сборки
-1 секунда запуска
= нет разницы

Это рассуждение работает, только если каждая сборка соответствует ровно одному запуску процесса.

Современные бэкенд-системы редко ведут себя так.

Один артефакт может запускаться многократно:

1 сборка
    ↓
5 интеграционных сред
    ↓
10 запусков тестов CI
    ↓
3 запуска staging
    ↓
20 производственных реплик
    ↓
замены при rolling-развёртывании
    ↓
события автоскейлинга
    ↓
перезапуски и перепланирования

Поэтому арифметика ближе к:

дополнительная стоимость компиляции × сборки
против
экономия запуска × запуски

Если:

Δсборка = +2 секунды
Δзапуск = -1 секунда

и артефакт запускается один раз:

нетто = +1 секунда

Подход этапа компиляции проигрывает.

Если тот же артефакт запускается 20 раз:

штраф сборки = 2 секунды
экономия запуска = 20 секунд
чистая экономия = 18 секунд

Если он запускается 200 раз:

чистая экономия = 198 секунд

Это фундаментальная асимметрия.

Стоимость сборки обычно платится с частотой производства артефактов.

Стоимость запуска платится с частотой создания экземпляров.

Облачно-нативные системы создают экземпляры часто.


Общее уравнение

Обмен можно описать простой моделью.

Пусть:

B = дополнительная стоимость сборки, вызванная обработкой на этапе компиляции
S = сэкономленное время запуска на процесс
N = число запусков процесса из собранного артефакта

Тогда чистый временной эффект приблизительно:

чистая выгода = (S × N) - B

Работа этапа компиляции побеждает, когда:

S × N > B

или:

N > B / S

Предположим, обработка аннотаций добавляет:

B = 3 секунды

тогда как запуск улучшается на:

S = 0.75 секунды

Точка безубыточности:

N > 3 / 0.75
N > 4

Примерно после четырёх запусков экономия выполнения окупила стоимость сборки.

Эта модель намеренно упрощена. Кеши сборки, инкрементальная компиляция, параллельный запуск, топология CI, рабочие процессы разработчиков, создание образов и прогрев выполнения усложняют реальные системы.

Но уравнение раскрывает правильный множитель.

Важная переменная — не только время сборки или время запуска.

Это как часто повторяется каждая стоимость.


Обработка аннотаций — механизм, а не цель

Для Java-приложений Kora использует обработку аннотаций.

Процессор аннотаций видит структуру программы во время компиляции и генерирует дополнительный Java-исходник на основе объявлений в приложении.

Для Kotlin Kora использует KSP, API обработки символов Kotlin, для выполнения эквивалентной роли в тулчейне Kotlin.

Эти механизмы иногда обсуждаются так, будто сама обработка аннотаций — архитектурная цель.

Это не так.

Цель — получить надёжную модель структуры приложения до выполнения.

Обработка аннотаций и KSP — инструменты, которые позволяют Kora делать это, оставаясь внутри обычных систем сборки Java и Kotlin.

Вместо написания:

рантайм фреймворка:
«Дай мне изучить всё приложение и определить, что оно означает».

Kora фактически спрашивает:

фаза компилятора:
«Скажи мне, что приложение означает сейчас,
пока типы и объявления уже доступны».

Этот тайминг даёт два преимущества сразу.

Во-первых, можно генерировать исходный код.

Во-вторых, недопустимая архитектура может провалить сборку.


Внедрение зависимостей на этапе компиляции — больше, чем генерация кода

Внедрение зависимостей — самый ясный пример.

Предположим, приложение содержит:

Контроллер
    ↓
Сервис
    ↓
Репозиторий
    ↓
База данных

Контейнер времени выполнения может обнаружить этот граф после запуска.

Он может найти контроллер, изучить конструкторы, найти совместимый сервис, затем рекурсивно разрешить зависимость репозитория и базы данных.

Kora разрешает эту архитектуру во время компиляции.

Это значит, что фаза компилятора может обнаруживать:

  • отсутствующие зависимости;
  • неоднозначные зависимости;
  • недопустимые отношения графа;
  • циклы;
  • несовпадения тегов;
  • недопустимые комбинации аспектов;
  • другие структурные ошибки.

Сгенерированный граф затем содержит явный код, описывающий, как эти компоненты конструируются и соединяются.

Выполнению больше не нужно решать проблему зависимостей.

Оно исполняет решение, которое уже произвела компиляция.

Это важно, потому что разрешение зависимостей детерминировано для данного дерева исходного кода и конфигурации модулей.

Повторение его при каждом запуске процесса даёт мало ценности, если результат не может легитимно измениться между запусками.

Kora относится к этому как к знанию этапа сборки.


Сгенерированный исходный код — материализованный результат анализа

Вывод модели этапа компиляции Kora — не только скрытые метаданные.

Это исходный код.

Это различие важно.

Сгенерированный исходный код превращает решения фреймворка в то, что обычный языковой тулчейн может компилировать, оптимизировать, изучать, отлаживать и обдумывать.

Примеры сгенерированной инфраструктуры могут включать:

реализации графа приложения
модули HTTP-маршрутов
JSON-ридеры/райтеры
реализации репозиториев
AOP-обёртки
реализации конфигурации
мапперы
клиентскую инфраструктуру

Конкретные сгенерированные артефакты зависят от того, какие модули использует приложение.

Концептуально конвейер компилятора делает это:

декларативный исходный код приложения
    ↓
анализ фреймворка
    ↓
явная сгенерированная реализация
    ↓
обычный байткод JVM

Выполнение затем исполняет обычный скомпилированный код, а не интерпретирует большой объём метаданных фреймворка.

Вот почему сгенерированный исходный код централен для обмена запуск/сборка.

Сборка делает больше, потому что материализует будущие решения выполнения.


Сгенерированный код — форма предвычисления

Ещё один полезный способ думать о Kora — как о предвычислении.

Предположим, фреймворк времени выполнения должен ответить:

Какой объект предоставляет FooService?
Какие параметры конструктора ему нужны?
Какой маппер обрабатывает UserRequest?
Какой обработчик вызывает GET /users/{id}?
Какие аспекты оборачивают метод X?

Если эти ответы стабильны для собранного артефакта, их можно вычислить один раз.

Это напоминает многие классические техники производительности:

вычислить один раз
сохранить результат
переиспользовать много раз

База данных создаёт индекс, потому что поиск с нуля при каждом запросе дорог.

Компилятор выполняет свёртку констант, потому что повторение выражения во время выполнения ненужно.

Система сборки кеширует выводы, потому что неизменённая работа не должна повторяться.

Kora применяет ту же философию к структуре фреймворка.

Сгенерированный исходный код — фактически скомпилированный кеш архитектурных решений.

Разница в том, что кеш — код, проверенный по типам.


Рефлексия времени выполнения гибка, но имеет модель стоимости

Рефлексия и динамическое обнаружение мощны, потому что откладывают решения.

Выполнение может изучать классы после развёртывания.

Библиотеки могут динамически регистрировать поведение.

Фреймворки могут интерпретировать аннотации без шага генерации.

Эта гибкость ценна в системах, которые действительно нуждаются в расширяемости времени выполнения.

Но большинство бизнес-сервисов имеют относительно статичную архитектуру.

Контроллер обычно не меняет зависимости после сборки JAR.

Репозиторий внезапно не получает новый конструктор во время выполнения.

Метод, аннотированный политикой отказоустойчивости, обычно сохраняет эту политику, пока не развёрнут новый артефакт.

В такой среде обнаружение во время выполнения многократно отвечает на вопросы, ответы на которые уже были известны на этапе сборки.

Kora выбирает пожертвовать частью этой динамической гибкости в пользу:

более ранней валидации
+
явного сгенерированного кода
+
меньшей работы запуска
+
меньшей механики выполнения

Это архитектурный обмен, а не универсальный закон.

Платформа плагинов, загружающая произвольные модули из неизвестных JAR во время выполнения, может предпочесть динамическое обнаружение.

Статически упакованный облачный сервис обычно имеет гораздо меньшую потребность в нём.


Время сборки — не одно число

При обсуждении стоимости обработки аннотаций люди часто говорят:

«Но Kora делает сборку медленнее».

Это утверждение слишком расплывчато, чтобы быть полезным.

Есть несколько разных путей сборки:

чистая сборка
инкрементальная сборка
кешированная сборка
сборка с кешем конфигурации
сборка CI
локальная сборка IDE
частичная пересборка мультимодуля

Их экономика существенно различается.

Чистая сборка начинается без скомпилированных выводов.

Инкрементальная сборка начинается с уже доступных большинства выводов и перекомпилирует только то, что изменилось и что должно быть инвалидировано.

Кешированная сборка может переиспользовать выводы задач, произведённые ранее.

Сборка разработчика может выигрывать от тёплого демона Gradle.

Сборка CI может использовать удалённые кеши.

Релизная сборка может намеренно отключать кеши для измерений воспроизводимости.

Отношение ко всем ним как к одному «времени сборки» скрывает путь, который разработчики фактически испытывают чаще всего.


Чистая сборка: наихудший путь компилятора

Чистая сборка — самый лёгкий сценарий для рассуждений.

Всё должно быть пересоздано:

компиляция исходного кода
обработка аннотаций / KSP
сгенерированный исходный код
компиляция сгенерированного исходного кода
обработка ресурсов
упаковка

Здесь фреймворки компиляции раскрывают свою наибольшую видимую стоимость сборки.

Если фреймворк генерирует существенную инфраструктуру, чистая сборка должна сгенерировать её всю снова.

Это реальная работа.

Для бенчмаркинга стоимости сборки фреймворка чистая сборка полезна, потому что даёт контролируемое сравнение, похожее на верхнюю границу, и избегает случайного измерения переиспользования кеша вместо работы компилятора.

Собственный лендинг Kora разделяет измерения чистой сборки артефакта и кешированной/инкрементальной именно по этой причине.

Но чистая сборка не обязательно доминирующий рабочий процесс разработчика.

Разработчики обычно не запускают:

./gradlew clean build

после каждого изменённого метода.

И не должны.


Почему clean часто искусственный бенчмарк разработки

Запуск clean намеренно уничтожает информацию, которую система сборки могла бы переиспользовать.

Он удаляет сгенерированные исходники, скомпилированные классы и выводы задач.

Это полезно, когда:

  • тестируется воспроизводимость сборки;
  • диагностируются проблемы устаревших выводов;
  • измеряется стоимость полной сборки;
  • создаются контролируемые условия бенчмарка.

Это плохая модель для обычной разработки правка-компиляция-тест.

Обычный цикл ближе к:

правка одного или нескольких файлов
    ↓
компиляция изменённых входов
    ↓
повторный запуск затронутых процессоров/задач
    ↓
переиспользование неизменённых выводов
    ↓
тест

Это различие особенно важно для фреймворков компиляции.

Если сгенерированный артефакт зависит только от малой части графа исходного кода, инкрементальный процессор может избежать пересборки несвязанных выводов.

Поэтому вопрос становится не только:

Насколько дорога обработка аннотаций?

но:

Сколько обработки аннотаций инвалидируется обычным изменением?

Это гораздо лучшая метрика опыта разработчика.


Инкрементальная компиляция делает обмен практичным

Генерация на этапе компиляции была бы гораздо менее привлекательной, если бы каждая крошечная правка исходного кода заставляла регенерировать всё приложение.

Современные инструменты сборки спроектированы, чтобы избегать этого.

Инкрементальная компиляция Gradle отслеживает отношения исходного кода и перекомпилирует затронутые классы вместо автоматической пересборки всего.

Процессоры аннотаций также можно проектировать с расчётом на инкрементальную обработку.

Kora добавила поддержку инкрементальной обработки для своей обработки аннотаций Java и обработки KSP Kotlin, чтобы генерация на этапе компиляции могла эффективнее участвовать в этой модели.

Поэтому идеальный путь правки выглядит так:

изменить локальный код
    ↓
инвалидировать ограниченный набор символов
    ↓
повторно запустить требуемые процессоры
    ↓
регенерировать затронутый вывод
    ↓
скомпилировать

а не:

изменить одну строку
    ↓
регенерировать весь мир

Не каждое изменение исходного кода локально.

Изменение графа приложения может иметь более широкие последствия.

Изменение контракта общего модуля может инвалидировать многих потребителей.

Изменение сгенерированных API-моделей может запускать широкую перекомпиляцию.

Но это концептуально уместно: структурное изменение должно стоить больше, чем локальное изменение тела метода, потому что фактически изменилось больше программы.


Инкрементальная компиляция вознаграждает стабильную архитектуру

Есть интересный вторичный эффект.

Фреймворки компиляции делают архитектурные границы видимыми для системы сборки.

Рассмотрим два изменения.

Изменение A

Разработчик модифицирует внутреннюю бизнес-логику:

public Money calculatePrice(Order order) {
    // изменённый алгоритм
}
fun calculatePrice(order: Order): Money {
    // изменённый алгоритм
}

Граф зависимостей не изменился.

Контракт репозитория не изменился.

HTTP-API не изменился.

Хорошо ведущий себя инкрементальный тулчейн должен уметь держать большую часть сгенерированной инфраструктуры нетронутой.

Изменение B

Разработчик меняет:

конструкторные зависимости
связывание модулей
тип компонента
сигнатуру HTTP-маршрута
метод репозитория
контракт OpenAPI

Теперь сгенерированные выводы фреймворка легитимно должны измениться.

Сборка становится дороже, потому что изменилась архитектура.

Это не обязательно недостаток.

Это значит, что стоимость компиляции коррелирует с объёмом изменения.

Структурная правка вызывает структурную работу.

Локальная правка должна оставаться сравнительно локальной.


KSP делает тот же обмен для Kotlin

Kotlin не может просто переиспользовать обработку аннотаций Java как свою идеальную модель обработки символов.

Поэтому Kora использует KSP для Kotlin-проектов.

Концептуально архитектура остаётся той же:

исходный код Kotlin
    ↓
KSP видит символы
    ↓
Kora анализирует объявления
    ↓
Kora генерирует инфраструктуру Kotlin/Java по необходимости
    ↓
компилятор компилирует результат

KSP был спроектирован специально для эффективного раскрытия структуры Kotlin-программы генераторам кода.

Для такого фреймворка, как Kora, это важно, потому что Java и Kotlin — оба первоклассные языки приложений, тогда как философия фреймворка остаётся той же:

анализировать рано
генерировать явно
исполнять напрямую

Инструмент различается.

Архитектурный принцип — нет.


Сгенерированный исходный код имеет стоимость сборки и дивиденд отладки

Сгенерированный исходный код требует времени на создание и компиляцию.

Это очевидная стоимость.

Менее очевидная выгода в том, что он остаётся проверяемым после генерации.

Предположим, разработчик хочет знать, как запрос достигает контроллера.

В системе с тяжёлым выполнением объяснение может включать:

  • правила сканирования;
  • рефлексию;
  • динамические прокси;
  • метаданные фреймворка;
  • перехватчики времени выполнения;
  • внутренности контейнера.

В Kora большая часть ответа может прослеживаться через сгенерированный код.

Компилятор уже преобразовал декларативное использование фреймворка в явную реализацию.

Это делает дополнительную работу сборки продуктивной в двух измерениях:

оптимизация выполнения
+
диагностический артефакт

Сгенерированный класс — не просто кеш оптимизации.

Это документация того, что решил фреймворк.


Ошибки этапа сборки — тоже удалённая работа выполнения

Перенос работы влево делает больше, чем перемещает циклы CPU.

Он перемещает обнаружение сбоев.

Предположим, зависимость отсутствует.

Последовательность, ориентированная на выполнение, могла бы быть:

правка кода
    ↓
успешная компиляция
    ↓
упаковка
    ↓
запуск приложения
    ↓
фреймворк строит контейнер
    ↓
разрешение зависимостей падает

Kora стремится к:

правка кода
    ↓
компиляция
    ↓
разрешение зависимостей падает

Второй конвейер короче.

Это значит, что часть дополнительной работы компилятора заменяет не только работу запуска, но и неудачные попытки выполнения.

Поэтому модель стоимости должна включать:

время, сэкономленное избеганием недопустимых запусков

Это особенно важно в CI.

Сбой, обнаруженный во время компиляции, может остановить конвейер до:

  • упаковки контейнера;
  • загрузки образа;
  • создания тестовой среды;
  • загрузки приложения;
  • интеграционных тестов;
  • развёртывания.

Чем раньше сбой, тем меньше его вычислительный радиус.


Сборка — лучшее место для детерминированного сбоя

Здесь есть более широкий инженерный принцип:

Если свойство можно доказать до развёртывания, выполнение обычно неверное место, чтобы обнаружить, что оно недопустимо.

Примеры включают:

отсутствующую зависимость
неоднозначного провайдера
недопустимый сгенерированный маппинг
плохую сигнатуру репозитория
неподдерживаемую комбинацию аспектов
сломанный контракт маршрута

Это свойства программы.

Они не становятся более значимыми после запуска Pod в Kubernetes.

Проверка их во время компиляции увеличивает работу сборки, но снижает неопределённость позже.

Вот почему «запуск против времени сборки» — не чисто обмен производительности.

Это также:

неопределённость выполнения
→ определённость этапа компиляции

Kora тратит усилия компилятора, чтобы снизить операционную неоднозначность.


Кешированные сборки снова меняют экономику

Кеши сборки добавляют ещё одну асимметрию.

Сгенерированные выводы — детерминированные функции входов.

Когда система сборки может безопасно определить, что входы не изменились, она может переиспользовать предыдущие выводы задач вместо их пересчёта.

Это значит, что часть стоимости этапа компиляции может полностью исчезнуть на последующих сборках.

Точное поведение зависит от конфигурации задач, версий Gradle, реализации процессора, структуры проекта, локальности кеша и того, настроено ли удалённое кеширование.

Но концептуально:

первая сборка:
заплатить за генерацию

поздняя совместимая сборка:
переиспользовать вывод генерации

Запуск не может так же легко использовать тот же вид организационного кеша.

Каждая новая JVM всё равно должна стать работающим процессом.

Каждый новый Pod всё равно должен инициализировать свои компоненты приложения.

Каждый тестовый контейнер всё равно должен достичь готовности.

Работа сборки имеет больше возможностей для переиспользования, потому что производит статические артефакты.

Запуск выполнения создаёт эфемерное состояние.

Это ещё одна причина, почему работа этапа сборки может быть экономически привлекательной.


Кеш конфигурации отличается от кеша сборки

Полезно разделять две часто путаемые концепции Gradle.

Кеш сборки переиспользует выводы задач.

Кеш конфигурации переиспользует сконфигурированный граф задач Gradle и состояние конфигурации проекта.

Они оптимизируют разные фазы.

Фреймворк компиляции выигрывает в первую очередь, когда его задачи обработки и выводы компиляции могут эффективно участвовать в инкрементальном выполнении и кешировании сборки.

Но кеш конфигурации всё ещё важен для обратной связи разработчика, потому что сам Gradle имеет меньше работы настройки для повторения перед началом задач.

Реалистичный быстрый локальный рабочий процесс поэтому сочетает несколько слоёв:

тёплый демон Gradle
+
кеш конфигурации
+
инкрементальную компиляцию
+
инкрементальную обработку аннотаций/KSP
+
кеш сборки

Результат может сильно отличаться от:

clean
без демона
без кеша
без параллелизма

Оба измерения валидны.

Они отвечают на разные вопросы.


Чистая сборка отвечает «Сколько стоит компилятор с нуля?»

Контролируемый бенчмарк чистой сборки отвечает:

Если ничего нельзя переиспользовать, сколько работы этот фреймворк добавляет к производству артефакта?

Это важно для:

  • свежих CI-раннеров;
  • релизных конвейеров;
  • начальной загрузки репозитория;
  • промахов кеша;
  • анализа воспроизводимости.

Фреймворк компиляции может легитимно работать здесь хуже, чем фреймворк с тяжёлым выполнением.

Нет причины скрывать этот результат.

Сборка делает больше.

Релевантный вопрос — пропорциональна ли дополнительная работа и окупает ли её результирующий артефакт позже.


Кешированная сборка отвечает «Что ощущается как разработка?»

Кешированная или инкрементальная сборка отвечает на другой вопрос:

После того как проект уже существует и разработчик меняет обычный код, насколько дорога следующая полезная итерация?

Для инженерной продуктивности это может значить больше, чем время чистой сборки.

Разработчики могут делать сотни инкрементальных итераций на каждую полную чистую сборку.

Поэтому идеальная комбинация фреймворк/сборка — не просто:

быстрая чистая сборка

а:

приемлемая чистая сборка
+
очень быстрый инкрементальный цикл

Архитектура этапа компиляции Kora зависит от этого различия.

Перенос работы влево гораздо привлекательнее, когда инструменты сборки могут избегать повторения неизменённой работы.


Локальная разработка — тоже проблема частоты

Рассмотрим разработчика, работающего один день.

Он может выполнять:

1 чистую сборку
60 инкрементальных компиляций
30 запусков тестов
20 запусков приложения

Общая стоимость обратной связи:

стоимость чистой сборки
+
стоимость инкрементальной компиляции × 60
+
стоимость теста × 30
+
стоимость запуска × 20

Сравнение фреймворков, измеряющее только первый член, может предсказать неверный опыт разработчика.

Аналогично, измерение только запуска может скрывать дорогое поведение этапа компиляции.

Правильный объект — весь цикл итераций:

правка
→ компиляция
→ обработка аннотаций/символов
→ генерация
→ тест/запуск
→ наблюдение результата

Это единица, которую разработчики фактически испытывают.


Стоимость сборки можно заплатить один раз, тогда как запуск платится на реплику

Теперь рассмотрим прод.

Конвейер CI производит один неизменяемый артефакт.

Этот артефакт продвигается в:

staging
регион A
регион B
canary
прод

Предположим, релиз в конце концов создаёт 100 JVM-процессов.

Процессор аннотаций выполнялся, когда собирался артефакт.

Он не выполняется снова в каждом контейнере.

Сгенерированный граф уже внутри артефакта.

Это даёт работе этапа компиляции исключительный рычаг.

Один проход анализа питает многие экземпляры выполнения.

Концептуально:

          скомпилировать один раз
              ↓
        сгенерированный артефакт
        ↙      ↓      ↘
   экземпляр экземпляр экземпляр
      ↓        ↓        ↓
   нет повторного обнаружения графа

Чем больше развёртывание, тем благоприятнее эта асимметрия.


Rolling-развёртывания умножают экономию запуска

Предположим, приложение запускает 50 реплик.

Новый релиз заставляет Kubernetes постепенно создать 50 замен.

Если Kora достигает готовности на две секунды быстрее на экземпляр, упрощённая агрегатная экономия запуска:

50 × 2 секунды = 100 инстанс-секунд

Развёртывание не обязательно становится на 100 секунд быстрее, потому что реплики могут запускаться параллельно.

Но флот тратит на 100 агрегатных инстанс-секунд меньше, ожидая готовности приложения.

Это влияет на:

  • длительность rollout;
  • занятость surge;
  • время под смешанными версиями;
  • запас восстановления;
  • перекрытие автоскейлинга.

Работа этапа сборки была заплачена один раз.

Преимущество запуска было собрано 50 раз.

Разверни дважды в тот же день:

100 запусков выполнения

без ещё одного чистого анализа фреймворка, если переразвёртывается тот же артефакт.

Множитель растёт.


Горизонтальное масштабирование снова умножает его

Производственный артефакт может жить дни, пока реплики приходят и уходят.

Предположим, трафик создаёт:

200 запусков scale-out

в течение жизни артефакта.

Если компиляция добавила:

3 секунды

для производства артефакта, а каждый экземпляр экономит:

1 секунду

запуска:

стоимость сборки:      +3 секунды
экономия выполнения: -200 секунд

Окупаемость выполнения доминирует.

Вот почему облачно-нативная инфраструктура усиливает аргумент этапа компиляции.

Эластичные системы спроектированы создавать и уничтожать процессы.

Всё, что платится на процесс, умножается эластичностью.

Всё, что платится один раз за артефакт, амортизируется.


Перезапуски и перепланирования — скрытые множители

Не все запуски запланированы.

Pods перезапускаются из-за:

  • обслуживания узлов;
  • давления памяти;
  • сбоя приложения;
  • потери узла;
  • обновлений кластера;
  • консолидации автоскейлера;
  • событий зон доступности;
  • spot-прерываний.

Каждый перезапуск воспроизводит запуск.

Он не воспроизводит исходную сборку приложения.

Это делает работу запуска повторяющимся операционным долгом.

Генерация на этапе компиляции конвертирует часть этого повторяющегося долга в одноразовую стоимость производства артефакта.

Это особенно ценно в нестабильных или высокоэластичных средах.


CI может содержать больше запусков, чем прод

Удивительный множитель появляется в тестировании.

Приложение может запускать 20 производственных реплик, но запускаться сотни раз в день в CI.

Рассмотрим:

50 пул-реквестов/день
× 4 тестовых шарда
× 2 запуска приложения/шард
= 400 запусков/день

Улучшение запуска на одну секунду экономит:

400 секунд/день

для одного репозитория.

По 100 репозиториям:

40,000 секунд/день
≈ 11 часов/день

агрегатного ожидания/вычислений CI.

Опять же, параллельное выполнение означает, что эффекты реального времени отличаются от агрегатных эффектов вычислений.

Но множитель реальный.

Генерация на этапе компиляции особенно привлекательна, когда сгенерированный артефакт или тестовые классы можно переиспользовать, пока контексты приложения многократно запускаются.


Полноконтекстные тесты выигрывают непропорционально

Модульные тесты мало заботятся о запуске приложения.

Они обычно инстанцируют горстку объектов.

Обмен становится важным в:

  • компонентных тестах;
  • интеграционных тестах;
  • black-box тестах;
  • сквозных тестах сервисов.

Эти тесты часто следуют:

компиляция
→ запуск контекста/приложения
→ выполнение сценария
→ остановка

Если запуск дёшев, реалистичные тесты становятся достаточно дешёвыми, чтобы запускать их чаще.

Это меняет инженерное поведение.

Команды могут предпочитать:

реальный граф приложения

вместо:

большой структуры моков

в большем числе случаев.

Сборка могла сделать больше работы, но эта работа позволяет много дешёвых проверок выполнения.


Собери один раз, тестируй много — мощный паттерн

Особенно благоприятная структура конвейера:

скомпилировать + сгенерировать один раз
    ↓
произвести артефакты приложения/тестов
    ↓
запустить много тестовых сценариев против них

Например:

сгенерированный граф
    ↓
тестовый шард A
тестовый шард B
тестовый шард C
тестовый шард D

Анализ этапа компиляции разделяется.

Запуск выполнения повторяется.

Это ровно тот паттерн, где компромисс Kora накапливается.

Чем больше постсборочных этапов переиспользуют одну и ту же скомпилированную архитектуру, тем ценнее перенос работы влево.


Обработка этапа сборки может улучшить и память выполнения

Выгода запуска очевидна, но перенос работы влево также может изменить след выполнения.

Контейнеру времени выполнения могут понадобиться структуры метаданных, описывающие:

  • компоненты;
  • отношения зависимостей;
  • аннотации;
  • поведение прокси;
  • определения маршрутов;
  • рефлексивные метаданные.

Если эти структуры преобразуются в обычный скомпилированный код, меньше метаданных выполнения может оставаться резидентными.

Точный исход памяти зависит от структуры приложения и деталей реализации, поэтому его следует измерять, а не предполагать.

Но архитектурно сгенерированный прямой код может снизить потребность в принадлежащих фреймворку представлениях выполнения.

Это значит, что работа этапа компиляции может окупаться в течение всего времени жизни процесса, а не только во время запуска.


Сгенерированные прямые вызовы могут помочь и горячему пути

Перенос решений в компиляцию также может устранить слои из обработки запросов.

Если фреймворк генерирует прямой обработчик или реализацию репозитория, выполнение часто может быть ближе к:

прямому вызову метода

чем к:

обобщённому поиску метаданных
→ рефлексивному вызову
→ диспетчеризации адаптера

Опять же, современные JVM удивительно хорошо оптимизируют многие динамические паттерны, и производственную производительность нужно измерять.

Но архитектура даёт JIT прямолинейный код для оптимизации.

Это важно, потому что тот же сгенерированный исходный код, который увеличивает работу сборки, может улучшить:

запуск
+
установившуюся пропускную способность
+
поведение выделения
+
отлаживаемость

Стоимость сборки платится более чем за одно свойство выполнения.


AOP на этапе компиляции ясно показывает обмен

Сквозное поведение — ещё один пример.

Система AOP времени выполнения может нуждаться в динамическом создании прокси и интерпретации метаданных перехвата.

Kora генерирует AOP-обёртки во время компиляции.

Для таких аннотаций, как:

  • отказоустойчивость;
  • валидация;
  • кеширование;
  • транзакции;
  • безопасность;
  • логирование;

приложение может получать сгенерированные реализации, которые явно содержат логику обёртки.

Это переносит конструирование прокси с:

запуска приложения

на:

генерацию исходного кода

и переносит часть обнаружения сбоев с выполнения на компиляцию.

Выполнение исполняет уже сгенерированное поведение.

Цена — дополнительная работа процессора.

Окупаемость повторяется каждый раз, когда этот артефакт запускается и каждый раз, когда метод выполняется.


Генерация репозиториев — ещё один пример

Абстракция репозитория имеет достаточно статической информации для генерации существенного кода реализации.

Компилятор знает:

  • сигнатуры методов;
  • типы возвращаемых значений;
  • аннотации запросов;
  • типы параметров;
  • контракты маппинга.

Kora может генерировать инфраструктуру репозитория из этой информации.

Фреймворк времени выполнения мог бы вместо этого изучать интерфейсы репозиториев и конструировать динамические реализации после запуска.

Обе модели могут представлять разработчику одинаково удобный API.

Разница в том, где происходит работа реализации.

Ответ Kora:

предпочитай генерацию до выполнения

когда необходимая информация уже известна.

Это повторяющаяся архитектурная тема.


HTTP-маршрутизацию тоже можно предвычислить

Маршруты обычно статичны для собранного сервиса.

Компилятор знает:

HTTP-метод
путь
контроллер
сигнатуру метода
маппер запроса
маппер ответа
перехватчики/аспекты

Редко есть бизнес-причина заново обнаруживать весь этот маппинг из аннотаций после каждого запуска JVM-процесса.

Сгенерированная инфраструктура маршрутов конвертирует этот статический контракт в исполняемый код заранее.

Выполнению всё ещё нужно регистрировать обработчики в сервере, но фреймворку не нужно переинтерпретировать столько структуры приложения.

Опять:

анализ этапа компиляции
→ сгенерированная реализация
→ более дешёвый запуск

Перенос работы влево — не то же самое, что компиляция в нативный образ

Генерацию фреймворка на этапе компиляции иногда путают с нативной компиляцией ahead-of-time.

Это разные слои.

Kora может генерировать инфраструктуру приложения на этапе сборки, всё ещё производя обычный байткод JVM и работая на HotSpot.

Поток:

обработка этапа компиляции Kora
    ↓
сгенерированный исходный код Java/Kotlin
    ↓
байткод JVM
    ↓
рантайм HotSpot

Это сохраняет знакомые характеристики развёртывания JVM:

  • оптимизацию JIT;
  • обычные JVM-инструменты;
  • стандартное профилирование;
  • стандартные библиотеки;
  • виртуальные потоки;
  • зрелые опции GC.

Архитектура этапа сборки не требует отказа от модели выполнения JVM.

Это различие важно, потому что обмен сравнительно скромный: делать больше семантической работы фреймворка во время обычной компиляции, а не обязательно компилировать всё приложение в нативный исполняемый файл.


Компилятор становится частью архитектуры выполнения фреймворка

В фреймворке с тяжёлым выполнением движок фреймворка в первую очередь компонент времени выполнения.

В Kora часть фреймворка фактически живёт в тулчейне сборки.

Концептуально:

Фреймворк Kora
├── процессоры этапа компиляции
│   ├── анализ графа
│   ├── генерация кода
│   ├── валидация
│   └── проверка контрактов
│
└── модули выполнения
    ├── HTTP-сервер
    ├── интеграции данных
    ├── Kafka
    ├── телеметрия
    ├── отказоустойчивость
    └── жизненный цикл

Это разделение важно.

Более крупная система этапа компиляции позволяет более малую систему выполнения.

Это архитектурный обмен.


Сложность этапа сборки нужно инженерить тщательно

Перенос работы влево не автоматически хорош.

Плохой процессор аннотаций может разрушить продуктивность разработки.

Проблемы могут включать:

  • обработку всего проекта после каждой правки;
  • генерацию нестабильных выводов;
  • разрушение инкрементальной компиляции;
  • чрезмерный объём сгенерированного исходного кода;
  • плохую диагностику;
  • длинную инициализацию процессора;
  • ненужную межмодульную инвалидацию.

Если генерация на этапе компиляции часть архитектуры фреймворка, производительность процессора становится первоклассной продуктовой задачей.

Поэтому Kora должна оптимизировать два рантайма:

рантайм приложения
+
рантайм компилятора/этапа сборки

Разделение лендингом чистых и кешированных измерений сборки важно, потому что признаёт это.

Фреймворк компиляции следует судить не только по тому, как быстро запускается результирующий сервис, но и по тому, остаются ли обычные сборки разработки эффективными.


Инкрементальная обработка не опциональна для зрелого фреймворка компиляции

Как только генерация кода становится широкой — DI, репозитории, HTTP, JSON, AOP, конфигурация и многое другое — инкрементальная обработка становится стратегически важной.

Без неё обмен мог бы стать:

быстрый производственный запуск
за
медленную итерацию разработчика

Это просто перенесло бы боль с операций на инженерию.

Желаемый обмен:

немного больше работы компилятора
+
эффективная инкрементальная инвалидация
→
быстрый цикл разработки
+
очень быстрый запуск выполнения

Вот почему поддержка инкрементальной обработки аннотаций Gradle и инкрементальная обработка KSP важнее, чем может казаться из списка функций.

Именно они делают перенос работы влево масштабируемым для больших кодовых баз.


Мультимодульные сборки делают границы важнее

Крупные сервисы часто разделяются на Gradle-модули.

Это может улучшить экономику этапа компиляции, потому что неизменённые модули не всегда нужно пересобирать.

Представь:

:domain
:service
:database
:http
:application

Локальное изменение в:

:domain

может затрагивать только подмножество нижестоящих модулей.

Изменение в корне графа приложения может затрагивать более широкую генерацию.

Поэтому хорошие границы модулей помогают и архитектуре, и производительности сборки.

Это создаёт полезное взаимодействие:

модульная архитектура исходного кода
→ меньший объём инвалидации
→ более быстрая инкрементальная компиляция

Фреймворки компиляции вознаграждают чистую структуру зависимостей.

Плохо спроектированные модули с широкой связью API могут замедлить любую систему сборки.

Kora не убирает эту реальность.


Изменения ABI дороже изменений реализации

Система сборки часто может изолировать изменения, которые не меняют публичный бинарный интерфейс модуля.

Изменение внутренностей метода может требовать перекомпиляции одного класса.

Изменение сигнатуры конструктора может инвалидировать вызывающих.

Изменение контракта компонента может изменить сгенерированное связывание графа.

Это значит, что разработчики должны различать:

изменение реализации

и:

структурное/ABI изменение

Обработка этапа компиляции делает это различие особенно видимым.

Это не произвольный штраф процессора.

Структурные изменения действительно затрагивают больше программы.


Размер сгенерированного исходного кода — реальная стоимость

Ещё один компромисс — объём исходного кода и байткода.

Если фреймворк генерирует явные реализации вместо использования меньшего обобщённого интерпретатора выполнения, артефакт может содержать больше сгенерированных классов.

Это может влиять на:

  • компиляцию;
  • размер артефакта;
  • загрузку классов;
  • индексацию IDE;
  • навигацию отладки.

Эти эффекты следует измерять.

Но объём кода и механика выполнения обмениваются друг против друга.

Малый обобщённый интерпретатор может требовать больше метаданных и ветвления выполнения.

Сгенерированный явный код может производить больше классов, но меньше интерпретации.

Kora выбирает явность.

Лучший баланс зависит от качества процессора и масштаба приложения.


Чистые CI-раннеры раскрывают полную стоимость

Некоторые системы CI используют одноразовые воркеры с пустыми локальными кешами.

Для них каждый конвейер может больше напоминать чистую сборку, чем машина разработчика.

Это может сделать стоимость обработки этапа компиляции высоковидимой.

Есть несколько способов решить это операционно:

удалённый кеш сборки Gradle
кеш зависимостей
постоянные воркеры
многослойные артефакты конвейера
скомпилировать один раз / тестировать много

Правильная архитектура зависит от инфраструктуры CI.

Важно то, что перенос работы влево меняет, куда принадлежат усилия оптимизации.

Фреймворку с тяжёлым выполнением может понадобиться оптимизация запуска.

Фреймворк компиляции может больше выигрывать от архитектуры кеша сборки.

Обе — инженерные проблемы.

Вопрос в том, какая лучше масштабируется для организации.


Удалённые кеши сборки усиливают модель этапа компиляции

Удалённый кеш сборки позволяет выводам сборки одной машины переиспользоваться другой машиной, когда входы совпадают.

Это особенно интересно для сгенерированного кода, потому что вывод статичен.

Если задачи генерации кешируемы и детерминированы, CI-агент может не нуждаться в регенерации того же результата, который уже произвела другая сборка.

У запуска выполнения нет точного эквивалента.

Pod в другом регионе не может просто переиспользовать уже работающий граф объектов из другого процесса.

Ему всё ещё нужны собственные живые объекты.

Поэтому перенос детерминированной работы в кешируемую фазу создаёт возможности оптимизации, которых не существует, когда та же работа откладывается до запуска процесса.

Это сильный системный аргумент за вычисление на этапе сборки.


Продвижение артефакта делает обмен ещё лучше

Хорошие конвейеры развёртывания собирают артефакт один раз и продвигают тот же артефакт через среды.

Например:

коммит
    ↓
сборка артефакта
    ↓
интеграция
    ↓
staging
    ↓
canary
    ↓
прод

Процессор аннотаций выполняется вверху.

Сгенерированный граф приложения переиспользуется везде ниже.

Это значит, что стоимость этапа компиляции амортизируется по:

всем запускам тестов
+
всем экземплярам staging
+
всем экземплярам canary
+
всем производственным экземплярам

Пересборка отдельно для каждой среды ослабила бы это преимущество и обычно нежелательна для воспроизводимости в любом случае.

Продвижение неизменяемого артефакта и фреймворки компиляции естественно сочетаются.


«N раз при каждом развёртывании» — основной производственный аргумент

Предположим, один артефакт приложения развёртывается на:

N реплик

Если запуск Kora экономит:

S секунд/реплику

то каждый полный цикл замены экономит примерно:

N × S инстанс-секунд

занятости запуска.

Разверни:

D раз

и повторяющийся эффект:

N × S × D

По:

M сервисам

агрегат становится:

N × S × D × M

Стоимость этапа компиляции связана в первую очередь с:

сборками × B

Вот почему обмен нужно оценивать на частоте развёртывания, а не только на частоте компиляции разработчика.

Для облачно-нативных сервисов:

запуск экономится N раз при каждом развёртывании

часто доминирующее прозрение.


Автоскейлинг добавляет запуски без добавления сборок

Автоскейлинг делает асимметрию ещё резче.

Сервис может масштабироваться:

10 → 30 → 10 → 40 → 10

много раз, запуская ровно тот же артефакт.

Никакой обработки аннотаций не происходит во время этих событий.

Никакой сгенерированный исходный код не пересобирается.

Выгода выполнения собирается многократно бесплатно с точки зрения сборки.

Это значит, чем эластичнее сервис, тем ценнее быстрый запуск относительно накладных расходов сборки.

Статические сервисы имеют меньший множитель.

Эластичные сервисы — больший.


Spot и текучесть узлов добавляют больше переиспользования выполнения

Spot-узлы и агрессивный автоскейлинг кластера создают дополнительную текучесть процессов.

Артефакт остаётся неизменным.

Экземпляры исчезают и появляются.

Каждая замена платит запуск.

Ни одна не окупает компиляцию.

Это делает предвычисление на этапе компиляции особенно хорошо согласованным с одноразовой инфраструктурой.

Традиционные долгоживущие серверы приложений запускались редко.

Современные контейнерные платформы запускают процессы как обычные операции control plane.

Экономическая среда изменилась.

Архитектура фреймворка должна это отражать.


Scale-to-zero — крайний случай

Scale-to-zero доводит концепцию до логического конца.

Когда не остаётся ни одного экземпляра, следующее рабочее событие требует, чтобы холодная ёмкость была создана снова.

Тот же артефакт может многократно переходить:

0 → N → 0 → N → 0

Стоимость сборки неизменна.

Стоимость запуска повторяется при каждой активации.

Это делает задержку запуска частью модели обслуживания нагрузки.

Перенос детерминированной работы инициализации в компиляцию поэтому напрямую повышает практичность scale-to-zero.


Цикл разработчика имеет другую точку безубыточности

Прод сильно благоприятствует одной сборке, питающей многие запуски.

Разработка может быть другой.

Разработчик часто меняет исходный код и перекомпилирует перед каждым запуском:

правка
→ сборка
→ запуск
→ правка
→ сборка
→ запуск

Теперь стоимость компиляции и экономия запуска могут происходить с похожей частотой.

Вот почему инкрементальная компиляция так важна.

Если каждая правка заставляет полный дорогой проход процессора, выгода выполнения может не компенсировать в локальном цикле.

Если большинство правок дают дешёвые инкрементальные сборки, разработчик может получать и:

быструю компиляцию
+
быстрый запуск

Качество конвейера сборки определяет локальную точку безубыточности.


Правильная метрика разработчика — время-до-проверенного-изменения

Вместо отдельной оптимизации:

времени компиляции
времени запуска
времени теста

измеряй:

время-до-проверенного-изменения

То есть:

правка
    ↓
компиляция
    ↓
обработка/генерация
    ↓
запуск контекста
    ↓
запуск релевантного теста
    ↓
результат

Фреймворк, который собирается на секунду медленнее, но запускает тесты на пять секунд быстрее, всё равно может давать лучший цикл.

Фреймворк, который запускается мгновенно, но добавляет 30 секунд к каждой инкрементальной компиляции, может быть хуже.

Решает весь цикл.

Философия Kora сильнее всего, когда обработка аннотаций/KSP остаётся достаточно инкрементальной, чтобы экономия запуска доминировала в обычных рабочих процессах.


Время сборки также покупает лучшую диагностику

Есть ещё одна причина не сравнивать секунды механически.

Ошибка компилятора обычно полезнее исключения запуска.

Сборка может тратить дополнительное время на анализ графа, но вывод может быть:

точная структурная диагностика

вместо:

сбой выполнения после начала инициализации

Лучшая диагностика снижает время расследования человеком.

Предположим, обработка этапа компиляции добавляет:

500 мс

к итерации, но избегает одной 15-минутной сессии отладки в месяц.

Человеческая экономика затмевает стоимость CPU.

Не каждая сгенерированная ошибка совершенна, конечно.

Но перенос структурных проверок в компиляцию создаёт возможность точной диагностики в месте, где отношения исходного кода уже известны.


Нейро-агенты выигрывают от того же сдвига

Разработка при поддержке нейро-технологий делает этот обмен ещё интереснее.

Агент работает лучше всего с короткой, детерминированной обратной связью:

сгенерировать код
→ компилятор отвечает
→ изучить ошибку
→ патч
→ повторить

Если архитектурные ошибки появляются только во время выполнения, каждая итерация агента должна:

скомпилировать
→ упаковать
→ запустить
→ провалиться
→ разобрать логи выполнения

Этот цикл медленнее и более неоднозначен.

Сгенерированный исходный код также даёт агенту что-то конкретное для изучения.

Для фреймворка, всё чаще используемого вместе с агентами кодирования, работа этапа компиляции может снизить число дорогих спекулятивных циклов выполнения.

Стоимость переходит в детерминированный машино-проверяемый этап.


Перенос работы влево — стратегия снижения риска

Аргумент производительности — только половина истории.

Каждое действие, перенесённое с выполнения на этап сборки, снижает число вещей, которые могут неожиданно выйти из строя после развёртывания.

Если граф зависимостей уже проверен, прод не должен обнаруживать базовый сбой связывания.

Если маппер уже сгенерирован и проверен по типам, выполнению не нужно синтезировать его динамически.

Если контракт репозитория проверен во время генерации, некоторые классы неправильного использования устраняются до упаковки.

Артефакт развёртывания содержит больше разрешённых решений.

Операционно это значит, что артефакт ближе к:

известной исполняемой архитектуре

и дальше от:

инструкций по конструированию архитектуры позже

Это значимое свойство надёжности.


Полезная аналогия: время линковки

Традиционные компилируемые системы уже принимают подобные компромиссы.

Линкер тратит время на разрешение символов и конструирование конечного исполняемого файла.

Никто не утверждает, что программа должна разрешать все обычные отношения символов с нуля при каждом запуске просто потому, что линковка занимает время.

Фаза сборки существует именно потому, что некоторую работу лучше сделать один раз до выполнения.

Kora применяет подобную философию на более высоком уровне фреймворка приложений.

Внедрение зависимостей, связывание маршрутов, реализация репозиториев и аспекты становятся ближе к задачам компиляции/линковки.

Они часть производства исполняемой архитектуры.


Ещё одна аналогия: планирование запросов базы данных

Базы данных различают работу планирования и работу выполнения.

В некоторых ситуациях подготовка или кеширование плана оправданы, потому что одна и та же структура выполняется многократно.

Генерация фреймворка следует похожей идее:

проанализировать структуру один раз
→ выполнять много раз

Если структура постоянно меняется во время выполнения, предвычисление менее полезно.

Если структура стабильна в течение жизни артефакта, предвычисление естественно.

Архитектура бэкенд-сервисов обычно очень стабильна между развёртываниями.

Это делает её хорошим кандидатом.


Стоимость сборки видима; стоимость выполнения диффузна

Одна психологическая причина, почему команды сопротивляются обработке этапа компиляции, — стоимость сборки мучительно видима.

Разработчик запускает команду и смотрит, как она занимает больше времени.

Структурная стоимость выполнения диффузна.

Она распределена по:

каждому Pod
каждому тестовому контексту
каждому развёртыванию
каждому перезапуску
каждому запуску разработчика

Ни одно отдельное событие не выглядит большим.

Это может смещать решения в сторону минимизации времени сборки, даже когда общее время системы увеличивается.

Та же предвзятость появляется во многих инженерных системах:

централизованная видимая стоимость
против
распределённая невидимая стоимость

Мышление в масштабе флота корректирует её.


Одна дополнительная секунда в сборке не равна одной дополнительной секунде запуска

Даже когда частота равна, контекст различается.

Сборки часто происходят на выделенном железе CI, где задачи могут выполняться параллельно и выводы могут кешироваться.

Запуск часто происходит во время чувствительного к ёмкости события:

  • развёртывания;
  • всплеска трафика;
  • перезапуска;
  • отказоустойчивости.

Поэтому одна секунда запуска может иметь более высокую операционную ценность, чем одна секунда сборки.

Во время всплеска задержанная готовность означает отсутствующую обслуживающую ёмкость.

Во время развёртывания задержанная готовность удлиняет окно rollout.

Во время восстановления после сбоя задержанная готовность продлевает сниженную избыточность.

Время сборки обычно влияет на задержку доставки.

Время запуска может влиять на доступность.

Секунды экономически не взаимозаменяемы.


Но время сборки всё равно важно

Это не значит, что производительность сборки можно игнорировать.

Фреймворк, делающий чистые и инкрементальные сборки неприемлемо медленными, вредит:

  • продуктивности разработчиков;
  • пропускной способности CI;
  • скорости merge;
  • частоте релизов.

Правильная философия — не:

перенести неограниченную работу в компиляцию

а:

перенести детерминированную работу выполнения влево,
когда её можно обрабатывать эффективно,
инкрементально, прозрачно и переиспользуемо.

Архитектура сборки Kora должна заработать выгоду запуска.

Обмен всегда следует измерять.


Что следует переносить влево?

У хороших кандидатов есть несколько свойств.

Они:

детерминированы
известны из исходного кода/контрактов
стабильны для собранного артефакта
дороги или ненужны для повторного обнаружения

Примеры включают:

  • топологию зависимостей;
  • объявления маршрутов;
  • код маппинга;
  • структуру репозиториев;
  • AOP-обёртку;
  • интерфейсы конфигурации;
  • логику сериализации;
  • контракты, выведенные из OpenAPI.

Плохие кандидаты — значения, действительно зависящие от выполнения:

  • живое состояние базы данных;
  • текущее членство в обнаружении сервисов;
  • динамические учётные данные;
  • текущие feature flags;
  • значения удалённой конфигурации;
  • активные сетевые соединения.

Kora не пытается компилировать живой мир.

Она компилирует статическую архитектуру, которая взаимодействует с этим миром.


Граница между статическим и динамическим — это архитектура

Хорошо спроектированному фреймворку компиляции нужна чёткая граница:

этап компиляции:
Какова структура приложения?

выполнение:
Какова текущая среда и состояние?

Этап компиляции может решить:

Этому репозиторию нужен этот клиент базы данных.

Выполнение решает:

Какой хост базы данных настроен сегодня?

Этап компиляции может решить:

Этот маршрут вызывает этот метод контроллера.

Выполнение решает:

Какой запрос только что пришёл?

Этап компиляции может решить:

У этого метода есть аспект повтора.

Выполнение решает:

Провалился ли этот конкретный вызов?

Это разделение позволяет Kora переносить существенную работу влево, не превращая приложение в статическую исполняемую конфигурацию.


Генерация этапа сборки улучшает видимость архитектуры

Другой побочный эффект в том, что архитектура становится проверяемой до выполнения.

Сгенерированный код графа может показывать:

создание компонентов
рёбра зависимостей
вызовы модулей
сборку обработчиков

Это полезно во время ревью кода и отладки.

Контейнер времени выполнения часто требует инструментов или знаний фреймворка, чтобы изучить своё разрешённое состояние после запуска.

Kora может выносить большую часть этого состояния в сгенерированный исходный код.

Поэтому фаза компилятора производит не только исполняемый код, но и архитектурный след.

Эта прозрачность трудно выражается как число бенчмарка, но она снижает операционную и обслуживающую стоимость.


Чистую сборку и кешированную сборку всегда следует сообщать отдельно

Обсуждения производительности фреймворков часто публикуют одно число сборки.

Это неадекватно для фреймворков компиляции.

Как минимум различай:

чистую сборку
кешированную/инкрементальную сборку

Чистая сборка раскрывает:

полную стоимость генерации и компиляции

Кешированная сборка раскрывает:

эффективность повседневной итерации

Обе важны.

Фреймворк может иметь более медленную чистую сборку, но отличное инкрементальное поведение.

Другой может иметь быструю чистую сборку, но плохую инвалидацию.

Без обоих чисел платформенные команды не могут судить о реальном обмене разработчика.

Лендинг Kora, явно представляющий запуск/готовность, чистую сборку и кешированную сборку как отдельные представления, — правильная методология.


CI тоже должен измерять пути попадания и промаха кеша

Крупные инженерные организации должны идти дальше.

Отслеживай:

холодную сборку CI
сборку с тёплым кешем зависимостей
попадание в удалённый кеш сборки
частичное попадание в кеш
инкрементальную сборку PR

Эти пути могут иметь резко разные стоимости.

Если большинство производственных конвейеров получают высокие показатели попадания в кеш, производительность холодной сборки важна меньше.

Если политика безопасности заставляет каждый CI-раннер начинать с нуля, производительность чистой сборки важна больше.

Архитектура фреймворка не существует независимо от инфраструктуры сборки.

Платформе нужно оценивать комбинацию.


Кеши сборки превращают вычисления в хранилище

Есть ещё один системный обмен.

Кеширование сгенерированных выводов означает обмен:

времени CPU

на:

хранилище кеша + передачу

Это обычно благоприятно, когда выводы переиспользуются часто, но не всегда.

Удалённый кеш с очень большими сгенерированными артефактами и медленной сетевой передачей может стать контрпродуктивным.

Поэтому размер сгенерированного вывода и кешируемость важны.

Общий принцип остаётся:

предвычисляй, если переиспользование дешевле пересчёта

Архитектура Kora создаёт переиспользуемые статические выводы, но платформа сборки всё равно должна управлять ими разумно.


Сгенерированный исходный код должен быть детерминированным

Кеширование работает лучше всего, когда одни и те же входы дают одни и те же выводы.

Генераторы кода должны избегать ненужной недетерминированности, такой как:

  • временные метки в сгенерированном исходном коде;
  • нестабильный порядок;
  • специфичные для среды пути;
  • случайные идентификаторы.

Детерминированная генерация улучшает:

  • попадания в кеш;
  • воспроизводимые сборки;
  • инспекцию кода;
  • стабильность тестов.

Это ещё одна причина, почему фреймворкам этапа сборки нужна зрелая инженерия компилятора.

Генератор фактически часть компилятора сборки.

Качество его вывода важно.


Параллелизм существует на обеих сторонах обмена

Сравнение — не просто последовательная сборка против последовательного запуска.

Системы сборки могут параллелизовать независимые модули и задачи.

Инициализация выполнения Kora также может инициализировать части графа параллельно, где позволяют зависимости.

Это создаёт два графа:

граф зависимостей сборки
граф компонентов выполнения

Эффективные системы используют параллелизм в обоих.

На этапе сборки:

независимые модули компилируются параллельно

При запуске:

независимые компоненты приложения инициализируются параллельно

Перенос работы влево не означает, что вся задержка напрямую переносится с одного критического пути на другой.

Фактический эффект реального времени зависит от параллелизма графа.


Критический путь важнее суммарной работы CPU

Предположим, обработка аннотаций выполняет две секунды работы CPU, но частично параллельно с другой активностью сборки.

Штраф реального времени сборки может быть меньше двух секунд.

Аналогично, удаление двух секунд агрегатной работы запуска может сократить критический путь готовности только на одну секунду, если компоненты уже инициализировались параллельно.

Вот почему измерения должны различать:

работу CPU

и:

критический путь реального времени

Для развёртывания и опыта разработчика важнее всего реальное время.

Для эффективности инфраструктуры суммарный CPU тоже может иметь значение.


Запуск, сэкономленный N раз, — про критическую ёмкость, а не только CPU

Когда новая реплика запускается во время scale-out HPA, релевантный исход — не сколько CPU потреблял запуск.

Это:

когда реплика становится полезной?

Генерация на этапе компиляции помогает, потому что структурная фаза фреймворка уже завершена.

Это может сократить:

запуск процесса → готовность

Ценность операционная.

Одна секунда, сэкономленная на 50 репликах, запускающихся во время всплеска трафика, может означать, что ёмкость прибывает на секунду раньше по всему флоту.

Это нельзя оценивать чисто как сэкономленное время вычислений.

Это сэкономленное время реакции самой инфраструктуры.


Работа этапа сборки имеет лучшую локализацию сбоев

Сбой процессора происходит близко к изменению исходного кода, которое его вызвало.

Сбой запуска выполнения может происходить:

  • на машине разработчика;
  • в CI;
  • в staging;
  • во время развёртывания;
  • после того как автоскейлер создал редкий путь кода.

Перенос проверок раньше сужает локализацию сбоев.

Чем раньше сбой:

меньше задействованных систем
меньше логов
меньше инфраструктурных слоёв
меньше пространство поиска отладки

Это выгода инженерной продуктивности.


Генерация этапа компиляции вписывается в неизменяемую инфраструктуру

Неизменяемая инфраструктура предполагает:

артефакт собран
артефакт протестирован
артефакт развёрнут без изменений

Модель Kora естественно согласуется с этим.

Сгенерированный граф становится частью неизменяемого артефакта.

Выполнение не ожидается, что переинтерпретирует структуру приложения на основе того, что случайно оказалось на classpath после развёртывания.

Это делает исполняемую архитектуру более детерминированной.

Для современных контейнерных платформ это часто ровно то, что модель развёртывания уже хочет.


Динамическая расширяемость выполнения — контробмен

Есть ситуации, где перенос работы влево менее привлекателен.

Представь платформу, которая должна:

  • загружать произвольные плагины после развёртывания;
  • обнаруживать реализации из внешне поставляемых JAR;
  • включать новые типы компонентов без пересборки;
  • конструировать топологию приложения динамически из состояния выполнения.

Разрешение графа на этапе компиляции может стать ограничительным в такой системе.

Контейнер времени выполнения имеет преимущество, потому что намеренно откладывает решения.

Kora оптимизирована под другую категорию:

статически собранные серверные приложения

где само развёртывание — механизм изменения архитектуры.

Для этой нагрузки разрешение на этапе компиляции обычно лучше подходит.

Вот почему ни один архитектурный обмен не универсально оптимален.


Микросервисы усиливают выбор Kora

Микросервисная архитектура обычно производит:

много малых артефактов
много реплик
частые развёртывания
частые перезапуски
много CI

Это ровно та среда, где:

собрать один раз
запускать много

становится обычным.

Монолитные системы с одним огромным приложением и редкими перезапусками могут иметь другой баланс стоимости.

Поэтому облачно-ориентированный дизайн Kora согласуется с её архитектурой этапа компиляции.

Инфраструктурная модель усиливает экономию выполнения.


Частота развёртывания меняет уравнение

Рассмотрим две организации.

Организация A

развёртывать каждый сервис раз в месяц
2 реплики
мало автоскейлинга

Скорость запуска важна, но множитель скромен.

Организация B

continuous delivery
20 реплик/сервис
несколько регионов
частый автоскейлинг
эфемерные тестовые среды
spot-узлы

Та же экономия на запуск стоит гораздо больше.

Это предполагает полезный принцип оценки фреймворка:

Чем чаще ваша инфраструктура создаёт JVM, тем ценнее предвычисление на этапе компиляции.

Обмен Kora наиболее убедителен в средах с высокой текучестью.


Количество разработчиков меняет сторону сборки

Другая сторона уравнения тоже имеет множители.

Более медленная инкрементальная сборка влияет на:

разработчиков × итераций/день

Более медленная чистая сборка CI влияет на:

репозитории × конвейеров/день

Поэтому стоимость сборки также нужно оценивать в масштабе флота.

Полная модель не смещена в сторону выполнения.

Она:

стоимость сборки × частота сборки
против
экономия запуска/выполнения × частота выполнения

Обе частоты могут быть огромными.

Вот почему кешированное/инкрементальное поведение критично.


Более полная экономическая формула

Для одного сервиса:

Годовые накладные расходы этапа компиляции =
    Δчистая_сборка × количество_чистых_сборок
  + Δинкрементальная_сборка × количество_инкрементальных_сборок

Выгода выполнения:

Годовая выгода выполнения =
    Δзапуск
  × (
        запуски_разработчика
      + запуски_CI
      + запуски_staging
      + запуски_производственных_развёртываний
      + запуски_автоскейлинга
      + запуски_перезапусков
    )

Затем добавь ценность:

более ранних сбоев компилятора
сниженного прогрева
меньших метаданных выполнения
эффективности горячего пути

Расчёт безубыточности становится более реалистичным.


Пример: один сервис

Предположим:

штраф чистой сборки Kora:        +4 с
штраф инкрементальной сборки Kora:  +0.2 с
экономия запуска:                  2 с

За рабочий день:

1 чистая сборка
40 инкрементальных сборок
20 локальных/контекстных запусков
30 запусков CI
5 запусков развёртывания/staging

Дополнительное время на стороне сборки:

4
+ 40 × 0.2
= 12 с

Экономия на стороне запуска:

(20 + 30 + 5) × 2
= 110 с

Нетто:

98 с/день

Это только иллюстрация.

Реальные значения нужно измерять.

Но это показывает, почему видимо более медленная чистая компиляция может сосуществовать с существенно более быстрым общим циклом системы.


Пример: производственный флот

Предположим, у сервиса:

30 реплик
4 развёртывания/день

Полный rollout создаёт примерно:

30 запусков

игнорируя canary и повторы сбоев.

Ежедневные запуски развёртывания:

30 × 4 = 120

При двухсекундном преимуществе запуска:

240 инстанс-секунд/день

Теперь добавь активность HPA:

80 запусков/день

и текучесть узлов/перезапусков:

20 запусков/день

Итого:

220 запусков/день
× 2 с
= 440 инстанс-секунд/день

Артефакт мог быть собран только четыре раза.

Множитель выполнения уже велик.


Пример: масштаб компании

Теперь представь:

300 сервисов
в среднем 50 запусков/сервис/день
экономия запуска 1.5 с

Агрегат:

300 × 50 × 1.5
= 22,500 инстанс-секунд/день
≈ 6.25 инстанс-часов/день

Прямая экономия вычислений может быть не главной частью.

Эти секунды происходят во время:

  • развёртывания;
  • масштабирования;
  • восстановления;
  • CI.

Их реальная ценность — более короткие циклы управления.

Это та же логика экономики флота, применённая ко времени.


Стоимость запуска имеет следствие для резервной ёмкости

Если новым экземплярам требуется больше времени для готовности, существующие экземпляры должны нести нагрузку дольше.

Это может требовать больше статического резерва.

Фреймворк компиляции, запускающийся быстрее, может поэтому позволять:

нижнее минимальное количество реплик
меньший запас на реплику
более быстрое активирование scale-to-zero

когда остальная часть платформы это поддерживает.

Теперь обмен этапа сборки может влиять на повторяющуюся стоимость инфраструктуры.

Несколько дополнительных секунд в компиляции могут снизить месяцы резервирования базовой ёмкости.

Это радикально асимметричный обмен.


Работу этапа сборки легче централизовать

Оптимизацию сборки можно решить один раз на уровне платформы:

  • удалённый кеш;
  • стандартная конфигурация Gradle;
  • оптимизированные образы CI;
  • стратегия демона;
  • соглашения модулей;
  • обновления процессоров.

Выгода распространяется по сервисам.

Работа запуска выполнения распределена по каждому процессу.

Это делает оптимизацию на стороне сборки организационно привлекательной.

Платформенная команда может инвестировать в один высококачественный конвейер сборки и амортизировать стоимость.


Обновления процессоров могут улучшить каждый сервис

Поскольку генерация на этапе компиляции централизована внутри фреймворка, улучшения производительности в процессорах могут снизить стоимость сборки по всему флоту.

Аналогично, улучшения генерации выполнения могут улучшить запуск каждого сервиса после обновления.

Это создаёт рычаг:

одна оптимизация фреймворка
× все приложения

Поэтому архитектура компилятора Kora даёт сопровождающим фреймворка место для концентрации усилий оптимизации, которые отдельные команды приложений наследуют автоматически.


Регрессию сборки легче наблюдать, чем регрессию запуска

Ещё одно преимущество границы этапа компиляции — измеримость.

CI может отслеживать:

длительность compileJava
длительность KSP
время процессора аннотаций
количество сгенерированного исходного кода
длительность сборки артефакта

и предупреждать при их регрессии.

Запуск тоже можно измерять, но он более чувствителен к среде:

  • троттлингу CPU;
  • DNS;
  • доступности базы данных;
  • вытягиванию образов;
  • конкуренции узлов;
  • удалённым сервисам.

Детерминированную работу этапа сборки часто легче изолировать.

Это не делает стоимость сборки неважной.

Это делает её более контролируемой.


Правильная панель имеет и сборку, и запуск

Платформенные команды, оценивающие Kora, должны отслеживать как минимум:

чистую сборку артефакта p50/p95
инкрементальную сборку p50/p95
долю попаданий в кеш
длительность обработки аннотаций/KSP
объём сгенерированного исходного кода
запуск приложения p50/p95
время-до-готовности p50/p95

Затем свяжи эти метрики с частотой:

сборки/день
запуски/день
развёртывания/день
запуски тестов/день

Без частоты сырые времена нельзя корректно оценить.


Не оптимизируй одну сторону вслепую

Есть два режима сбоя.

Режим сбоя один

Оптимизируй выполнение при любой стоимости сборки.

Результат:

быстрый сервис
медленная инженерная организация

Режим сбоя два

Оптимизируй сборку при любой стоимости выполнения.

Результат:

быстрая компиляция
медленное развёртывание/масштабирование/восстановление

Правильная цель — минимальная общая стоимость жизненного цикла.

Kora намеренно принимает некоторую работу сборки, потому что её архитектура ожидает, что выгода выполнения будет повторяться.

Обмен успешен, только если инкрементальные сборки остаются достаточно дешёвыми.


Работа по производительности должна следовать множественности

Это предполагает общее правило оптимизации:

Оптимизируй работу пропорционально тому, как часто она повторяется.

Если вычисление происходит:

один раз/сборку

и экономит работу, которая иначе происходила бы:

100 раз/артефакт

тратить больше усилий на первую фазу может быть рационально.

Если вычисление происходит:

100 раз во время разработки

и экономит работу:

один раз в проде

может и не быть.

Множественность важнее одного лишь местоположения работы.

Архитектура Kora основана на наблюдении, что многие решения фреймворка стабильны, а запусков выполнения много.


«Перенести влево» также означает перенести понимание влево

Фраза «shift left» часто используется для безопасности и тестирования.

Kora применяет похожую идею к пониманию фреймворка.

Вместо обнаружения архитектуры во время выполнения:

запустить приложение
→ изучить контейнер
→ вывести граф

разработчик получает:

исходный код
→ анализ компилятора
→ сгенерированный исходный код
→ явный граф

Понимание тоже переносится раньше.

Это улучшает:

  • ревью кода;
  • адаптацию;
  • отладку;
  • разработку при поддержке нейро-технологий.

Сборка становится местом, где фреймворк объясняет себя.


Артефакты сборки становятся более полными

Артефакт с тяжёлым выполнением можно представить как содержащий:

код приложения
+
инструкции для обнаружения фреймворка

Артефакт этапа компиляции содержит больше:

кода приложения
+
разрешённой реализации фреймворка

Это делает собранный артефакт семантически богаче.

Больше архитектурных решений уже финализировано.

Это отчасти причина, почему запуск может быть меньше.

Процесс исполняет более полный продукт.


Почему подход Kora лучше подходит современным CPU

Сгенерированный прямой код также даёт компилятору JVM обычную цель оптимизации.

JIT очень хорошо оптимизирует:

  • обычные вызовы методов;
  • предсказуемые ветвления;
  • стабильные типы;
  • инлайнинг.

Глубокая интерпретация выполнения может вводить обобщённые слои, которые JIT может или не может полностью устранить.

Сгенерированный исходный код делает модель выполнения явной.

Это не просто оптимизация запуска.

Это выравнивает инфраструктуру фреймворка с моделью оптимизации, в которой Java уже сильна.

Опять же, стоимость — генерация на этапе компиляции.

Выгода может сохраняться в течение всего времени жизни процесса.


Конвейер сборки становится частью инженерии производительности выполнения

Это, пожалуй, самый важный концептуальный сдвиг.

В Kora производительность выполнения нельзя отделить от конвейера сборки.

Фреймворк достигает свойств выполнения отчасти, делая работу до выполнения.

Поэтому:

инженерия сборки

часть:

инженерии производительности выполнения

Производительность процессора аннотаций, производительность KSP, инкрементальная инвалидация, качество сгенерированного кода, кешируемость и конструирование артефакта — всё влияет на конечную экономику сервиса.

Это более компилятор-ориентированный способ строить бэкенд-фреймворк.


Что должны спрашивать CTO

CTO или платформенная команда, оценивающая этот обмен, должна спрашивать больше, чем:

У какого фреймворка самый быстрый запуск?

Спроси:

Каков штраф чистой сборки?
Каков штраф инкрементальной сборки?
Сколько работы процессора инвалидируется обычными правками?
Хорошо ли работает удалённое кеширование?
Сколько запуска экономится?
Сколько раз запускается каждый артефакт?
Как часто мы развёртываем?
Насколько эластичны наши сервисы?
Сколько полноконтекстных тестов мы запускаем?
Насколько раньше появляются структурные сбои?

Затем моделируй организацию.

Правильный ответ зависит от факторов умножения.


Что должны спрашивать разработчики

На уровне проекта измеряй:

./gradlew clean distTar

или фактическую задачу релизного артефакта.

Затем измерь обычную правку:

изменить тело метода
→ скомпилировать/протестировать

Затем структурную правку:

изменить зависимость компонента
→ скомпилировать/протестировать

Затем измерь:

запуск процесса → готовность

Наконец, измерь:

правка → проверенное поведение

Последнее число обычно самая важная метрика разработчика.


Лучший результат — не минимальное время сборки

Фреймворк, чья сборка заканчивается за одну секунду, но требует десять секунд запуска, может быть хуже для рабочего процесса, запускающего приложение 20 раз.

Фреймворк, чья чистая сборка занимает на пять секунд дольше, но чей инкрементальный путь быстр и чьё приложение запускается почти сразу, может давать лучший общий цикл.

Цель — не:

min(сборка)

или:

min(запуск)

Она ближе к:

min(
    частота_сборки × стоимость_сборки
  + частота_запуска × стоимость_запуска
  + частота_выполнения × накладные_расходы_выполнения
)

при ограничениях:

корректность
продуктивность разработчиков
операционная надёжность

Это фактическая задача оптимизации.


Обмен сильнее всего, когда сгенерированные знания переиспользуются

Чем больше раз переиспользуется одно и то же архитектурное знание, тем лучше выглядит генерация на этапе компиляции.

Переиспользование происходит по:

запускам процессов
запускам тестов
репликам развёртывания
регионам
запускам разработчиков
шардам CI
перезапускам

Сгенерированный исходный код не нужно каждый раз переинтерпретировать в архитектуру.

Он уже архитектура.

Это центральная ставка Kora.


Когда обмен становится слабым

Модель менее убедительна, когда:

  • приложение запускается один раз и работает годы;
  • сборки происходят чрезвычайно часто, но запуски редки;
  • процессоры инвалидируют почти весь проект после малых правок;
  • архитектура выполнения должна быть высокодинамичной;
  • сгенерированный код становится чрезмерно большим;
  • запуск полностью доминируется внешними системами.

Предположим:

разница запуска фреймворка = 300 мс
миграция базы данных = 60 с

Перенос работы фреймворка влево не меняет готовность осмысленно, пока не решена архитектура миграции.

Аналогично, если генератор кода добавляет 20 секунд к каждой инкрементальной компиляции, чтобы сэкономить 100 мс при запуске, обмен плох.

Архитектуру нужно судить с реальными измерениями.


Внешняя работа запуска всё ещё доминирует в некоторых приложениях

Процесс Kora всё ещё может запускаться медленно, если компоненты приложения выполняют дорогую работу.

Примеры:

  • миграция схемы;
  • большая предзагрузка кеша;
  • удалённое получение секретов;
  • медленный DNS;
  • медленное рукопожатие TLS;
  • недоступная база данных;
  • синхронные вызовы удалённых API во время запуска.

Генерация графа на этапе компиляции не может устранить эти стоимости.

Она убирает только работу, которую сам фреймворк может знать и предвычислить.

Это полезно, потому что делает оставшуюся стоимость запуска более честной.

Если накладные расходы фреймворка малы, профилирование указывает более прямо на фактические зависимости приложения.


Переноси миграции влево тоже — но обычно в развёртывание

Те же рассуждения часто применимы к миграции базы данных.

Если миграция блокирует каждую реплику приложения во время запуска, то:

работа миграции × реплики

может повторяться или сериализоваться неловко.

Многие организации вместо этого переносят миграцию в фазу развёртывания:

задача миграции
→ развернуть приложение

Это ещё одна форма переноса работы влево.

Общий паттерн:

делай одноразовую детерминированную работу один раз
не один раз на реплику

Архитектура этапа компиляции Kora вписывается в тот же системный принцип.


Более широкое правило: не заставляй каждую реплику решать одну и ту же проблему

Если каждая реплика независимо выполняет работу, которую можно было разрешить раньше, флот повторяет усилия.

Примеры включают:

  • обнаружение архитектуры;
  • статическую генерацию кода;
  • подготовку схемы;
  • компиляцию ресурсов;
  • индексацию зависимостей.

Облачно-нативный множитель наказывает повторяющуюся инициализацию.

Масштабируемая архитектура пытается сдвинуть стабильную работу в:

сборку
создание образа
подготовку развёртывания

и оставить выполнение для состояния, которое действительно меняется во время выполнения.

Kora применяет это агрессивно к структуре фреймворка.


Чистая сборка — это инвестиция

Чистую сборку Kora можно рассматривать как конструирование более специализированного артефакта.

Процессор тратит время на преобразование общих объявлений в специфичную для приложения реализацию.

Эта специализация имеет стоимость.

Но результат — артефакт, которому позже нужно меньше интерпретации.

Это напоминает инвестицию:

заплати один раз сейчас
→ снизь повторяющиеся стоимости позже

Хороша ли инвестиция, зависит от переиспользования.

В современных бэкенд-флотах переиспользование обычно высоко.


Кешированная сборка — амортизированная стоимость

Если чистая сборка — авансовая инвестиция, инкрементальные и кешированные сборки представляют амортизированную стоимость разработки.

Они отвечают:

Сколько этой инвестиции нужно повторить после этой правки?

Зрелый фреймворк компиляции должен делать ответ:

только то, что изменилось

как можно чаще.

Вот почему качество инкрементальной обработки — не второстепенная деталь реализации.

Это часть экономической модели фреймворка.


Запуск — налог на каждый экземпляр

И наоборот, обнаружение во время выполнения ведёт себя как налог на каждый процесс.

Независимо от того, сколько раз тот же артефакт уже запускался в другом месте, каждая JVM платит снова.

Это терпимо, когда налог крошечный.

Он становится дорогим, когда:

экземпляры × запуски

велико.

Облачные системы агрессивно умножают налоги на экземпляр.

Kora выбирает предоплатить многие из них.


Та же логика применима к прогреву

Сгенерированный прямой код также может снизить объём специфичной для фреймворка механики, с которой JVM должна столкнуться до достижения стабильной производительности.

Он не убирает прогрев JIT.

Он не убирает загрузку классов.

Он не убирает кеши приложения.

Но он может снизить долю фреймворка.

Это значит, что перенос работы влево может экономить не только:

время-до-запуска-процесса

но и часть:

времени-до-стабильного-сервиса

Это увеличивает окупаемость выполнения.


Та же логика применима к памяти

Если метаданные выполнения ненужны, потому что поведение закодировано в сгенерированных классах, процесс может держать меньше состояния фреймворка живым.

Любое снижение окупается непрерывно:

сэкономленная память
× экземпляры
× часы выполнения

Теперь дополнительная работа сборки обменивается не только на время запуска, но и на постоянный след флота.

Это может сделать обмен ещё более благоприятным.


Та же логика применима к отладке

Сгенерированный исходный код также амортизирует знания отладки.

После генерации его могут изучать:

  • каждый разработчик;
  • каждый ревьюер кода;
  • каждый реагирующий на инциденты;
  • каждый нейро-агент.

Работа компилятора произвела переиспользуемое объяснение поведения фреймворка.

Это трудно количественно оценить, но ценно в масштабе.


Полезная ментальная модель

Думай о сборке Kora как о частичном выполнении фреймворка заранее.

Не бизнес-логики приложения.

Не его состояния выполнения.

А вопросов фреймворка о структуре.

Во время компиляции Kora спрашивает:

Какие компоненты существуют?
Как они соединены?
Какой код должен адаптировать этот контракт?
Какая обёртка реализует этот аспект?
Какой маппер реализует это преобразование?

Она записывает ответы как исходный код.

Выполнение затем использует эти ответы.

Это суть модели.


Работа выполнения должна быть про факты выполнения

Чистое разделение:

Этап компиляции

типы
контракты
зависимости
маршруты
мапперы
формы репозиториев
аспекты
статическая архитектура

Выполнение

запросы
соединения
состояние базы данных
значения конфигурации
трафик
сбои
таймауты
живая топология

Чем меньше выполнение тратит на повторный вывод фактов этапа компиляции, тем больше выполнения посвящено фактической работе выполнения.

Это полезный принцип дизайна за пределами Kora.


Почему это важно для платформенных команд

Платформенные команды оптимизируют повторяющиеся паттерны.

Если каждый сервис независимо платит стоимость обнаружения при запуске, эта стоимость становится свойством флота.

Если каждый сервис вместо этого платит умеренную стоимость генерации на этапе сборки, платформа может централизованно оптимизировать сборку с помощью:

  • общих соглашений Gradle;
  • инкрементальной обработки;
  • удалённых кешей;
  • стандартизированных образов CI;
  • параллельного выполнения.

Результирующие артефакты могут затем запускаться дёшево везде.

Это сильное преимущество централизации.


Почему это важно для команд приложений

Команды приложений получают:

диагностику этапа компиляции
сгенерированный проверяемый код
быстрый запуск приложения
дешёвые контекстные тесты

без ручной реализации архитектуры этапа компиляции.

Их ответственность в основном сохранять хорошую гигиену сборки:

  • не запускать clean без необходимости;
  • держать кеши Gradle здоровыми;
  • использовать разумные границы модулей;
  • избегать комбинаций процессоров, разрушающих инкрементальные сборки;
  • измерять регрессии.

Фреймворк и тулчейн сборки делают остальное.


Почему это важно для операций

Операционные команды получают артефакты, которые уже разрешили больше своей структуры.

Это может означать:

  • более быструю готовность;
  • меньшую дисперсию запуска;
  • более короткие окна rollout;
  • более быструю замену;
  • более дешёвую эластичность;
  • меньше структурных сбоев после запуска.

Им не важно напрямую, как работает обработка аннотаций.

Им важно, что стоимость была заплачена до того, как артефакт дошёл до них.

Это полезное разделение задач.


Почему это важно для CI

CI — место, где встречаются обе стороны.

CI платит стоимость сборки.

Затем он часто собирает выгоду запуска многократно в:

  • тестовых шардах;
  • интеграционных средах;
  • black-box запусках;
  • валидации упаковки.

Хорошо спроектированный конвейер должен использовать это:

скомпилировать один раз
→ разветвить тесты

вместо независимой пересборки внутри каждого шарда.

Чем больше конвейер переиспользует сгенерированный артефакт, тем благоприятнее становится архитектура Kora.


Идеальная форма конвейера

Дружелюбный к Kora конвейер CI может концептуально выглядеть так:

          исходный код
             ↓
     компиляция + генерация
             ↓
        артефакт сборки
       ↙      ↓       ↘
модульные  интеграционные  статические
тесты      тесты          проверки
             ↓
        black-box тесты
             ↓
          контейнер
             ↓
           развёртывание

Дорогая структурная работа компилятора централизована вверху.

Всё ниже потребляет разрешённый артефакт.

Это максимизирует переиспользование.


Не перекомпилируй на каждую среду

Анти-паттерн:

собрать staging-артефакт
собрать canary-артефакт
собрать производственный артефакт

из одного коммита с генерацией исходного кода, специфичной для среды.

Это повторяет работу этапа компиляции и ослабляет воспроизводимость артефакта.

Предпочитай конфигурацию выполнения для значений среды, держа структурную архитектуру фиксированной.

Тогда:

одна сборка
→ много сред

сохраняет окупаемость этапа компиляции.


Запуск, сэкономленный при каждом развёртывании, повторяется вечно

Один из сильнейших аргументов за перенос работы влево — временной.

Штраф сборки фреймворка существует только пока собирается эта версия.

Его преимущество запуска собирается в течение жизни этой версии.

Каждый:

  • передеплой;
  • перезапуск;
  • перепланирование;
  • scale-out;
  • создание среды;

снова собирает выгоду.

В этом смысле сгенерированный исходный код ведёт себя как актив.

Сборка заплатила за его создание.

Организация продолжает его переиспользовать.


Накопительный эффект между версиями

Теперь умножь по релизам.

Предположим, сервис развёртывается:

3 раза/день
250 рабочих дней/год

Это:

750 релизов/год

При 20 репликах на релиз:

15,000 запусков развёртывания/год

Экономия запуска в одну секунду становится:

15,000 инстанс-секунд/год

только для развёртываний.

Добавь автоскейлинг, CI, перезапуски и staging.

Дополнительная работа компиляции происходит примерно на:

750 сборках

или меньше, если одна сборка питает несколько продвижений.

Соотношение частот — и есть история.


Стоимость этапа сборки предзагружена

Всю архитектуру можно резюмировать экономически:

Kora:
предзагрузить больше детерминированной работы фреймворка

модель с тяжёлым выполнением:
отложить больше работы фреймворка до выполнения процесса

Предзагруженная стоимость имеет несколько полезных свойств:

  • легче кешировать;
  • легче параллелизовать;
  • легче падать рано;
  • переиспользуется многими экземплярами;
  • вне критического пути доступности.

Отложенная стоимость выполнения имеет одно главное преимущество:

  • больше динамической гибкости.

Kora выбирает предзагрузку, потому что нацелена на статически упакованные производственные сервисы.


Это философия компилятора

Kora часто описывается как бэкенд-фреймворк, но этот обмен делает её отчасти проектом компилятора.

Её работа — не просто предоставлять API выполнения.

Она преобразует:

высокоуровневые объявления приложения

в:

явную исполняемую инфраструктуру

Это преобразование — источник нескольких свойств Kora:

  • DI на этапе компиляции;
  • сгенерированные репозитории;
  • сгенерированные обработчики;
  • сгенерированные аспекты;
  • прозрачность;
  • быстрый запуск;
  • малая механика выполнения.

Это не независимые функции.

Это следствия одной и той же компилятор-ориентированной архитектуры.


Обмен архитектурный, а не трюк бенчмарка

Было бы легко сформулировать стратегию узко:

перенести работу в сборку
чтобы бенчмарк запуска выглядел лучше

Это упускает суть.

Перенос работы влево меняет:

когда появляются ошибки
где представлена архитектура
что выполнение должно помнить
сколько раз повторяется работа
что можно кешировать
что разработчики могут изучать
как быстро экземпляры становятся полезными

Запуск — только самый видимый исход.

Более глубокая выгода — эффективность жизненного цикла.


Практическое руководство: как измерить обмен

Для реального сервиса собери четыре измерения.

1. Чистая сборка артефакта

Измеряй с нуля:

./gradlew clean <задача-артефакта>

Используй контролируемые настройки.

Запиши:

  • общее время;
  • время компиляции Java/Kotlin;
  • время процессора/KSP;
  • размер сгенерированного вывода.

2. Инкрементальная локальная сборка

Сделай репрезентативные изменения:

изменение тела метода
изменение зависимости компонента
изменение репозитория
изменение сигнатуры контроллера

Измерь каждое отдельно.

3. Кешированная сборка CI

Измерь реалистичное поведение кеша:

кеш зависимостей
кеш сборки
кеш конфигурации
удалённый кеш

4. Запуск/готовность

Измерь:

запуск процесса → готовность

при реалистичных лимитах CPU и памяти.

Затем примени частоты.

Без частот сравнение неполно.


Включи обратную связь сбоев в измерение

Также измерь, как быстро падают недопустимые изменения.

Примеры:

отсутствующий компонент
неоднозначная зависимость
недопустимый контракт репозитория
сломанный маппер
недопустимая цель аспекта

Запиши:

время от правки → выполнимого сбоя

Это часто релевантнее времени успешной сборки.

Фреймворк, ловящий недопустимую архитектуру во время компиляции, может сэкономить целые попытки выполнения.


Включи полный цикл разработчика

Наконец измерь:

правка → зелёный тест

Это сильнейшая метрика разработчика.

Она включает:

  • инкрементальную обработку;
  • компиляцию;
  • запуск контекста;
  • выполнение теста.

Если дополнительная работа процессора Kora более чем компенсируется более быстрым запуском и точными сбоями, архитектура выполняет свой задуманный обмен.

Если нет, это проблема производительности сборки, которую стоит исправить.


Не используй clean в обычной разработке

Один практический вывод следует прямо из этой архитектуры.

Не делай clean частью рутинных команд разработчика, если это не необходимо.

Например, избегай рабочих процессов, таких как:

./gradlew clean test

на каждой итерации.

Предпочитай:

./gradlew test

и позволь Gradle решить, что изменилось.

Фреймворк компиляции получает большую часть своей эргономики разработчика из инкрементального переиспользования.

Уничтожение выводов перед каждой сборкой убирает это преимущество.

Используй чистые сборки намеренно, а не по привычке.


Сохраняй демон Gradle

Аналогично, тёплый демон Gradle избегает многократной оплаты накладных расходов инициализации JVM и инструментов сборки.

Для бенчмарков воспроизводимости CI можно его отключить.

Для локальной разработки делать так обычно мало смысла.

Цель — не имитировать стерильную среду бенчмарка.

Цель — самый быстрый корректный цикл обратной связи.


Используй кеш сборки, где он уместен

Если крупная организация использует Kora по многим репозиториям или модулям, кеширование сборки может быть стратегически ценным.

Сгенерированные и скомпилированные выводы — ровно тот вид детерминированных артефактов, которые могут выигрывать от переиспользования, когда конфигурация задач это поддерживает.

Платформенная команда должна отслеживать показатели попаданий в кеш, а не просто включать функцию и предполагать, что она работает.

Плохие ключи кеша или нестабильные входы могут стереть выгоду.


Избегай случайной инвалидации процессоров

Производительность сборки также может повреждаться структурой исходного кода.

Широкие общие модули и часто меняющиеся публичные API могут вызывать большие волны инвалидации.

Хорошая модульность помогает:

стабильные интерфейсы
локализованная реализация
ясные зависимости

Это хорошая архитектура независимо от Kora.

Генерация на этапе компиляции просто делает стоимость широкой связанности более видимой.


Держи сгенерированный исходный код проверяемым, но эфемерным

Сгенерированный исходный код обычно должен оставаться продуктом сборки, а не поддерживаемым вручную кодом приложения.

Разработчики должны иметь возможность изучать его при необходимости.

Но он должен регенерироваться из авторитетных объявлений исходного кода.

Концептуально:

исходный код приложения = источник истины
сгенерированный исходный код = продукт компилятора

Это сохраняет воспроизводимость и предотвращает расхождение.


Относись к производительности процессора как к метрике регрессии фреймворка

Если Kora — стандарт платформы, тайминг процессора следует отслеживать между обновлениями.

Отслеживай репрезентативные приложения.

Новый релиз, улучшающий выполнение на 3%, но удваивающий время инкрементальной сборки, может не быть чистым выигрышем.

Аналогично, оптимизация процессора, снижающая задержку сборки для тысяч разработчиков, может иметь огромную организационную ценность, даже если поведение выполнения неизменно.

Архитектура этапа компиляции делает производительность компилятора частью качества фреймворка.


Обмен меняется с масштабом команды

Для малой команды с одним сервисом:

производительность сборки

может доминировать в восприятии.

Для платформы с сотнями сервисов:

запуск выполнения × флот

становится гораздо больше.

Для организации с тысячами разработчиков:

инкрементальная сборка × разработчики

снова становится большим.

Нет универсального веса.

Правильное решение зависит от организационного масштаба с обеих сторон.

Вот почему платформенным командам нужны измерения, а не идеология.


Лучший исход Kora — и быстрая сборка, и быстрый рантайм

Важно не превращать обмен в ложный выбор.

Долгосрочная цель:

быстрая чистая сборка
быстрая инкрементальная сборка
быстрый запуск
быстрое выполнение

Перенос работы влево объясняет, почему существует некоторая стоимость сборки.

Он не оправдывает ненужную неэффективность сборки.

Процессоры аннотаций можно оптимизировать.

Обработку KSP можно сделать инкрементальной.

Сгенерированный вывод можно минимизировать.

Кеши Gradle можно использовать.

Архитектура компилятора должна непрерывно улучшаться.

Обмен объясняет где принадлежит работа, а не почему работа должна быть медленной.


Перенос работы влево — про оплату в самое дешёвое время

Самый глубокий способ описать выбор Kora:

Выполняй работу в самой дешёвой фазе, где вся требуемая информация уже доступна.

Если компилятор уже знает структуру приложения, решать её снова во время выполнения избыточно.

Если информация действительно зависит от выполнения, компиляция не может её заменить.

Архитектура должна размещать каждую проблему в самой ранней валидной фазе.

Для Kora:

топология приложения
→ этап компиляции

живое состояние инфраструктуры
→ выполнение

Эта граница — и решение производительности, и решение дизайна ПО.


Результат: выполнение делает меньше работы фреймворка

После сборки артефакта выполнение Kora может более прямо сосредоточиться на:

  • инициализации реальных компонентов;
  • привязке серверов;
  • подключении к инфраструктуре;
  • обработке запросов;
  • выполнении бизнес-логики;
  • порождении телеметрии;
  • обработке сбоев.

Оно тратит меньше времени на вопрос:

Что это за приложение?

потому что сборка уже ответила на этот вопрос.

Вот почему запуск быстр.


Результат: сборки становятся семантически ценнее

И наоборот, сборка делает больше, чем превращает файлы .java или .kt в байткод.

Она также:

  • проверяет архитектуру;
  • генерирует реализации;
  • материализует связывание зависимостей;
  • производит читаемый код фреймворка.

Поэтому успешная сборка доказывает больше о приложении, чем сборка, откладывающая эти вопросы до запуска.

Это делает время сборки более ценной работой, а не просто большей работой.


Результат: один артефакт может обслуживать многие события выполнения

Сгенерированный артефакт можно переиспользовать по:

локальным запускам
CI
интеграционным тестам
staging
canary
проду
автоскейлингу
перезапускам

Каждое событие выигрывает от уже выполненной работы.

Это переиспользование — где архитектура окупается.


Основное уравнение заново

Всю статью можно свести к:

стоимость этапа компиляции
против
экономия выполнения × повторение выполнения

или:

B
против
S × N

Но реальное уравнение богаче:

Стоимость на стороне сборки:
    обработка аннотаций
  + KSP
  + компиляция сгенерированного исходного кода
  + инвалидация

Выгода на стороне выполнения:
    более быстрый запуск
  + меньше обнаружения выполнения
  + более ранние сбои
  + более тонкая механика выполнения
  + возможно, меньше памяти/CPU
  + более лёгкая отладка

И множители различаются.

Вот почему Kora выбирает перенос работы влево.


Заключение

Время запуска и время сборки не следует оценивать как изолированные категории бенчмарка.

Это две точки в жизненном цикле одного и того же приложения.

Фреймворк может выполнять структурную работу при производстве артефакта, или он может откладывать эту работу до запуска каждого процесса. Kora намеренно выбирает предзагрузку большей её части.

Процессоры аннотаций Java и KSP Kotlin анализируют объявления приложения во время компиляции. Связывание зависимостей разрешается. Структурные ошибки могут падать рано. HTTP-адаптеры, репозитории, мапперы, аспекты, реализации конфигурации и код графа могут генерироваться как читаемый исходный код. Сборка становится несколько дороже, потому что производит более специализированный и более полно разрешённый артефакт.

Выполнение затем получает выгоду.

Меньше структуры фреймворка для обнаружения. Граф приложения уже известен. Сгенерированный код уже скомпилирован. Запуск может сосредоточиться на инициализации реальных ресурсов выполнения, а не на пересборке архитектурных знаний.

Обмен особенно благоприятен, потому что две стоимости имеют разную множественность.

Сборка может произойти один раз.

Результирующий артефакт может запускаться десятки, сотни или тысячи раз по CI, staging, репликам развёртывания, автоскейлингу, перезапускам, нескольким регионам, восстановлению spot и средам разработчиков.

Это даёт центральное уравнение:

дополнительная работа сборки один раз
<
работа запуска, сэкономленная N раз

Но сторона сборки всё ещё важна. Вот почему чистую сборку и кешированную/инкрементальную сборку нужно рассматривать отдельно. Чистые сборки раскрывают полную стоимость генерации кода. Инкрементальная компиляция, кеши Gradle, демон, границы мультимодулей и инкрементальная обработка аннотаций/KSP определяют, остаётся ли обычная разработка быстрой. Архитектура этапа компиляции успешна, только когда тулчейн сборки избегает повторения неизменённой работы.

Это также причина, почему clean — неверная линза по умолчанию для опыта разработчика. Фреймворк компиляции спроектирован сотрудничать с инкрементальными вычислениями. Уничтожение всех выводов перед каждой правкой намеренно отключает один из главных механизмов, делающих обмен экономичным.

Более широкий архитектурный принцип прост:

Делай детерминированную работу один раз,
в самой ранней фазе, где её можно доказать,
затем переиспользуй результат.

Kora применяет этот принцип к структуре фреймворка.

Она переносит архитектуру из интерпретации выполнения в генерацию на этапе компиляции.

Она обменивает некоторую работу сборки на меньшую работу запуска, более раннюю диагностику, явный сгенерированный код и меньший объём механики выполнения.

Для одного локального запуска разница может выглядеть как деталь бенчмарка.

По каждой сборке, каждому тестовому контексту, каждому развёртыванию, каждой реплике, каждому перезапуску и каждому региону она становится оптимизацией жизненного цикла.

Вот почему Kora переносит работу влево.