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

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

9 августа 2026

В JVM-бэкенд-разработке существует стойкая интуиция, которая звучит достаточно разумно, чтобы стать эмпирическим правилом: если фреймворк выполняет внедрение зависимостей, генерацию репозиториев, HTTP-маршрутизацию, маппинг, валидацию, AOP или другую инфраструктурную работу во время компиляции, то компиляция должна стать медленнее. Отсюда вывод часто расширяют на один шаг дальше: фреймворк, который сильно полагается на обработку аннотаций или плагины компилятора, должен также давать более медленный опыт разработки, чем фреймворк, откладывающий больше работы до запуска приложения.

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

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

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

Это различие особенно важно для фреймворка Kora, потому что Kora намеренно переносит большой объём работы фреймворка из времени выполнения в компиляцию. Его граф зависимостей строится и проверяется во время компиляции. Реализации репозиториев генерируются. HTTP-обработчики и маппинги могут генерироваться. Аспекты реализуются как сгенерированный код, а не как динамические прокси времени выполнения. Маппинги конфигурации, поддержка сериализации, код валидации и другие адаптеры также создаются до выполнения.

Посмотрите только на этот список — и легко предположить, что Kora должен платить за всё это медленными сборками.

Более интересный вопрос — не выполняет ли Kora работу во время компиляции. Он явно выполняет. Интересный вопрос в том, делает ли выполнение этой работы там цикл обратной связи разработки медленнее на практике.

Для этого нужно посмотреть на весь цикл.

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

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

Измерение только этого:

время, проведённое внутри annotationProcessor

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

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

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


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

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

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

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

Разработчики, испытавшие одну из таких сборок, обоснованно запоминают обработку аннотаций как виновника.

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

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

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

Это похоже на утверждение, что приложение «использует рефлексию», и попытку вывести его пропускную способность из одного этого факта. Рефлексия может происходить один раз при запуске или миллионы раз в секунду. Механизм важен, но частота, область, реализация и размещение важнее.

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


Генерация кода — это не одна вещь

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

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

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

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

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

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

Какая реализация удовлетворяет эту зависимость?
Какой конструктор следует вызвать?
Какие аннотации применяются к этому методу?
Какая цепочка перехватчиков требуется?
Как следует преобразовать этот HTTP-параметр?
Какой маппер должен обработать это значение?
Как следует преобразовать этот результат репозитория?

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

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

сконструировать A
сконструировать B, используя A
сконструировать C, используя B
вызвать маппер X
вызвать обработчик Y
вызвать перехватчик Z

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

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

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


Время компиляции — это бюджет, а не моральная категория

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

Обе позиции слишком упрощены.

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

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

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

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

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

У этого два следствия.

Первое — видимая работа сборки.

Второе — отсутствующая работа времени выполнения.

Любое серьёзное сравнение должно учитывать оба.


За что Kora на самом деле платит во время компиляции

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

Граф приложения — один из самых важных примеров. Kora использует такие объявления, как @KoraApp, @Component и @Module, чтобы описать компоненты и их связи. Компилятор разрешает зависимости, проверяет граф, обнаруживает отсутствующие или неоднозначные привязки и генерирует связывание, необходимое для конструирования приложения.

Эта операция не бесплатна. Анализ графа имеет стоимость.

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

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

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

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

С узкой точки зрения компиляции каждая из этих функций добавляет работу.

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

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

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

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


Первая ошибка бенчмаркинга: измерение только чистых сборок

Чистые сборки полезны. Их также необычайно легко использовать неправильно.

Чистая сборка отвечает на конкретный вопрос:

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

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

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

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

И всё же сравнения фреймворков часто приблизительно повторяют именно этот рабочий процесс.

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

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

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

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

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

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


Чистая сборка артефакта и итерация разработчика — разные продукты

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

Бенчмарк лендинга Kora использует distTar для Kora и bootJar для Spring в сценарии чистого артефакта. Эти задачи создают развёртываемые выходы. Они значимы для конвейеров CI и релизов, но локальная разработка может выполнять более узкие графы задач в зависимости от того, как организованы тесты и конфигурации запуска.

Разработчик, меняющий бизнес-логику, может нуждаться только в компиляции плюс одной задаче теста. Разработчик, работающий над HTTP-конечной точкой, может компилировать затронутые модули и запускать приложение из классов. IDE может делегировать компиляцию способом, отличным от финального конвейера упаковки. Компонентный тест может инициализировать только релевантный граф.

Более широкий урок в том, что «время сборки» — это не одно число.

Как минимум, команды должны различать:

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

Эти сценарии задействуют разные части тулчейна.

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

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


Вторая ошибка бенчмаркинга: рассмотрение каждого изменения исходника как равного

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

Изменение внутри тела метода отличается от добавления зависимости к конструктору. Изменение контракта SQL-запроса отличается от переименования приватного хелпера. Добавление конечной точки контроллера отличается от изменения константы. Правка логики сборки отличается от правки кода приложения.

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

Представьте четыре правки.

Правка A: изменение бизнес-логики внутри существующего метода

Граф зависимостей не изменился. HTTP-объявления не изменились. Интерфейсы репозиториев не изменились. Большая часть сгенерированной инфраструктуры может оставаться корректной.

Правка B: добавление зависимости к конструктору

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

Правка C: изменение сигнатуры репозитория

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

Правка D: добавление нового маршрута контроллера

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

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

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

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

Это даёт распределение, а не одно число.


Gradle меняет экономику генерации на этапе компиляции

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

Несколько механизмов важны.

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

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

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

Демон Gradle амортизирует запуск JVM, загрузку классов, JIT-компиляцию и инициализацию инструментов сборки по повторным вызовам.

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

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

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

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

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

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

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


Демон Gradle — не чит для бенчмарка

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

Демон существует именно потому, что разработчики запускают сборки многократно.

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

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

Для анализа локального цикла обратной связи демон — это не оптимизация, которой разработчики как-то «обманывают», используя её. Это часть обычного продукта.

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

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


Кеш сборки и кеш конфигурации решают разные проблемы

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

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

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

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

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

Пользователю не важно, были ли 300 миллисекунд потрачены внутри javac, обработчика аннотаций, конфигурации Gradle, компиляции Kotlin, разрешения зависимостей или упаковки. Пользователь испытывает сумму.

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


Инкрементальная компиляция — настоящее поле битвы

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

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

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

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

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

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

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

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

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


Kotlin усложняет вопрос

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

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

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

Конвейеры компиляции Java и Kotlin имеют разные стоимости. Реализации процессоров могут иметь разные пути интеграции. Модули со смешанными языками ведут себя иначе, чем чисто Java-модули. Количество сгенерированных объявлений, общая сложность и версия компилятора — всё это может иметь значение.

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

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


Почему простой сгенерированный код важен

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

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

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

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

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

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

Фреймворк не нужно защищать абстрактно. Работу можно измерить.


Работа, которая исчезает во время выполнения

Центральная ошибка в аргументе «генерация на этапе компиляции медленнее» часто в том, что он считает добавленную работу компиляции, не считая удалённую работу времени выполнения.

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

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

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

Рассмотрим AOP.

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

Рассмотрим HTTP-маппинги.

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

Рассмотрим репозитории и маппинг.

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

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

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

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


Запуск приложения — это тоже время разработки

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

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

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

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

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

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

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

Полный локальный цикл примерно такой:

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

Оптимизация только одного члена может сделать общее медленнее.

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

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


Три графика лендинга следует читать вместе

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

Это важнее любого отдельного столбца.

Бенчмарк запуска использует десять сервисов PetClinic с производственными интеграциями внутри ограниченной среды Docker. Цель — измерить, как быстро приложения становятся готовыми обслуживать трафик.

Чистая сборка артефакта намеренно убирает оптимизации Gradle: нет кеша сборки, нет кеша конфигурации, нет параллельной сборки и нет постоянного демона Gradle. Kora производит свой дистрибутивный артефакт; Spring производит свой исполняемый артефакт приложения. Результат усредняется по повторным запускам.

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

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

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

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

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

Фреймворк с генерацией на этапе компиляции следует оценивать по всем трём.

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

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

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

Правильный вывод не в том, что один график «доказывает», что фреймворк быстрее универсально. Результаты бенчмарков всегда ограничены оборудованием, формой приложения, плагинами, версиями, JVM, конфигурацией Gradle, состоянием файловой системы и точными измеряемыми изменениями.

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


Производительность сборки артефакта может быть конкурентной даже с генерацией

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

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

Производственное приложение Spring может включать собственную обработку аннотаций, генерацию метаданных конфигурации, генерацию кода из схем OpenAPI или базы данных, улучшение байткода, инструментирование тестов, работу по упаковке, обработку ресурсов, создание многослойных архивов и плагины фреймворка. Проекты также обычно используют Lombok, MapStruct, QueryDSL, генерацию jOOQ, protobuf, gRPC, Avro, плагины компилятора Kotlin и другие системы времени сборки независимо от основного фреймворка.

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

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

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

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

Бенчмарк лендинга Kora лучше всего читать как одну такую точку данных, а не как теорему.

Команда, оценивающая Kora, должна повторить сравнение со своей собственной формой приложения.


Почему «время обработчика аннотаций» — неправильная первичная метрика

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

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

Предположим, Фреймворк A тратит:

1.2 с компиляция
0.0 с генерация
4.0 с запуск
2.0 с тест

а Фреймворк B тратит:

1.5 с компиляция
0.7 с генерация
0.5 с запуск
2.0 с тест

У Фреймворка B гораздо большая видимая стоимость генерации на этапе компиляции, но более короткий общий цикл.

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

0.8 с инкрементальная компиляция
0.1 с релевантная генерация
0.5 с запуск
2.0 с тест

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

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

Разработчик ждёт результат, а не процессор.

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


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

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

Рассмотрим отсутствующую зависимость.

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

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

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

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

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

Сбой времени выполнения часто даёт:

правка
↓
компиляция
↓
упаковка
↓
запуск
↓
инициализация фреймворка
↓
сбой

Сбой на этапе компиляции может дать:

правка
↓
компиляция
↓
сбой

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

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


Быстрый сбой — часть производительности сборки

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

Представьте две сборки.

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

Сборка B тратит шесть секунд на компиляцию и сообщает ту же проблему немедленно.

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

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

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

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


Компонентные и интеграционные тесты меняют уравнение

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

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

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

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

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

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

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

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

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


Обратная связь CI — тоже цикл разработки

Тот же аргумент выходит за пределы ноутбука.

Pull request часто запускает:

checkout
↓
конфигурация сборки
↓
компиляция
↓
генерация
↓
юнит-тесты
↓
запуск интеграционных сервисов
↓
запуск контекстов приложений
↓
интеграционные тесты
↓
упаковка
↓
публикация результата

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

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

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

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

Но при оценке генерации кода на этапе компиляции как архитектуры кешируемость — часть реальной операционной среды архитектуры.


Скрытое преимущество детерминированной генерации

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

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

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

Это не значит, что фреймворки времени выполнения не могут оптимизировать запуск. Они могут предварительно вычислять индексы, сохранять метаданные, вводить режимы AOT или кешировать внутренние структуры разными способами. На самом деле экосистема JVM-фреймворков всё больше движется к большему объёму обработки заранее именно потому, что повторное обнаружение времени выполнения имеет стоимости.

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

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


Чистые сборки всё ещё важны

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

Чистые сборки важны по нескольким причинам.

Новые среды разработчиков выполняют их. Свежие CI-агенты выполняют их, если удалённые кеши неэффективны. Обновления зависимостей и переключения веток могут инвалидировать кеши. Крупные рефакторинги могут вынуждать широкую перекомпиляцию. Релизные конвейеры могут намеренно использовать чистые рабочие пространства. Расследования воспроизводимости сборки часто начинаются с холодного состояния.

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

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

Она такова:

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

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

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


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

Есть условия, при которых критика становится корректной.

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

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

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

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

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

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

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

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

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

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

Ни одну из этих проблем нельзя отмахнуть фразой «время выполнения быстрее».

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

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


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

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

Производительность сборки заслуживает аналогичной строгости.

Фреймворк с компиляцией на этапе компиляции в идеале должен отслеживать:

  • чистую компиляцию Java;
  • чистую компиляцию Kotlin;
  • чистую сборку артефакта;
  • тёплую сборку артефакта;
  • инкрементальное изменение тела метода;
  • инкрементальное изменение графа зависимостей;
  • инкрементальное изменение контракта репозитория;
  • инкрементальное изменение HTTP-контракта;
  • инкрементальное изменение конфигурации;
  • количество сгенерированных исходников;
  • размер сгенерированных исходников;
  • время обработки аннотаций;
  • время компилятора;
  • время конфигурации Gradle;
  • поведение попаданий в кеш;
  • совместимость с кешем конфигурации;
  • запуск тестового контекста;
  • полную итерацию интеграционного теста.

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

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

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

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

Как только генерацию на этапе компиляции рассматривают как инфраструктуру, а не магию, её стоимость становится измеримой и оптимизируемой.

Именно так к ней и следует подходить.


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

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

Вместо этого постройте репрезентативный сервис.

Включите технологии, которые производственный сервис будет реально использовать: доступ к базе данных, HTTP-конечные точки, телеметрию, конфигурацию, отказоустойчивость, безопасность, миграции, обмен сообщениями и всё остальное, что материально влияет на поведение запуска и сборки.

Затем определите репрезентативные изменения.

Например:

Сценарий 1
Изменить строку внутри бизнес-логики.

Сценарий 2
Добавить зависимость к конструктору.

Сценарий 3
Добавить метод репозитория.

Сценарий 4
Добавить HTTP-конечную точку.

Сценарий 5
Изменить общий DTO.

Сценарий 6
Изменить схему конфигурации.

Сценарий 7
Запустить один компонентный тест.

Сценарий 8
Запустить один интеграционный тест на основе Testcontainers.

Сценарий 9
Произвести развёртываемый артефакт из чистого checkout.

Измеряйте каждый сценарий многократно.

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

Держите оборудование, JDK, версию Gradle, версию компилятора, состояние файловой системы и настройки JVM под контролем.

Запускайте и холодные, и тёплые сценарии.

Не отключайте молча оптимизации сборки, которые разработчики обычно используют, а затем называйте результат «опытом разработчика».

Не держите молча тёплые кеши, а затем называйте результат «производительностью чистой сборки».

Маркируйте каждое число честно.

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


Почему многомодульная архитектура важна

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

Границы модулей могут сильно влиять на стоимость генерации.

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

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

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

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

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

Хорошая архитектура и хорошее поведение сборки часто усиливают друг друга.


«Больше работы на этапе компиляции» может означать меньше работы везде остального

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

Модель с тяжёлым временем выполнения может концептуально выглядеть так:

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

Модель, ориентированная на этап компиляции, переносит несколько этих этапов раньше:

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

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

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

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

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


Это похоже на планирование запросов в базах данных

Полезная аналогия приходит из баз данных.

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

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

Задержка планирования важна, особенно для коротких запросов. Но это один компонент более крупного бюджета задержки.

Генерация фреймворка на этапе компиляции похожа.

Сборка выполняет планирование.

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

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

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


Цикл обратной связи — это продукт

Для разработчика фреймворк переживается не как набор внутренних техник реализации. Он переживается как цикл.

Вы редактируете.

Тулчейн отвечает.

Вы запускаете.

Приложение становится готовым.

Тест выполняется.

Вы видите, сработала ли ваша идея.

Затем вы повторяете.

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

Архитектура Kora на этапе компиляции может улучшить цикл несколькими способами одновременно:

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

Против этих преимуществ Kora платит стоимости обработки аннотаций и компиляции сгенерированных исходников.

Это фактический балансовый отчёт.

Всё меньшее неполно.


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

Современные бэкенд-тесты всё больше размывают границу между юнит- и интеграционными тестами. Testcontainers делает практичным запуск реальных баз данных, брокеров Kafka, Redis или другой инфраструктуры. HTTP-тесты могут задействовать почти весь стек приложения. Контрактные тесты могут запускать реальные серверные компоненты, а не моки.

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

Если контекст приложения инициализируется за десять секунд, а фактическое утверждение занимает 200 миллисекунд, усилия по оптимизации, очевидно, должны нацеливаться на инициализацию контекста, прежде чем зацикливаться на 100-миллисекундной разнице компилятора.

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

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

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


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

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

Ошибка компилятора обычно появляется рядом с кодом и связями типов, которые её вызвали. IDE её разбирает. Инструменты сборки выводят её напрямую. нейро-агент кодирования может скормить её в следующий цикл правки. Разработчикам не нужно проверять логи приложения после частичной инициализации контейнера.

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

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

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

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

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

Это часть качества обратной связи.


Тот же аргумент ещё важнее для нейро-агентов кодирования

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

Автономный агент кодирования часто работает примерно так:

проверка кода
↓
внесение правки
↓
компиляция/тест
↓
чтение диагностики
↓
внесение следующей правки

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

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

Но принцип фундаментально не о нейро-технологиях.

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

Разработчики-люди всегда зависели от того же цикла.


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

Самая важная идея в современной производительности сборки — не «сделать каждую отдельную операцию быстрее».

Это «делать меньше ненужной работы».

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

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

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

Сгенерированные выходы должны иметь точные входы.

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

Процессоры должны избегать чтения необъявленного состояния окружения.

Несвязанные изменения исходников должны избегать инвалидации несвязанных выходов.

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

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

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


Реальная модель стоимости — частота × задержка

Полезный способ думать о производительности сборки фреймворка:

стоимость для разработчика
≈
Σ (частота рабочего процесса × задержка рабочего процесса)

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

Например:

чистая сборка:
+10 с × 2 в день = +20 с

инкрементальный тест:
-1 с × 150 в день = -150 с

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

Опять же, эти числа иллюстративны. Важно уравнение.

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

Организации с огромными релизными конвейерами могут взвешивать чистые CI-сборки сильнее. Команды с удалёнными кешами сборки могут почти устранить эту озабоченность. Сервисы с интенсивным локальным интеграционным тестированием могут взвешивать запуск гораздо сильнее. Большие Kotlin-монолиты могут непропорционально заботиться о поведении инкрементального компилятора.

Нет универсального взвешивания.

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


Числа чистой сборки всё ещё ценны для инженерии фреймворка

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

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

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

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

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

Это ещё одна причина, почему структура графиков лендинга Kora разумна: она избегает схлопывания проблемы в одну заголовочную метрику.


Запуск следует чаще включать в бенчмарки сборки фреймворков

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

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

Реалистичный бенчмарк может быть:

1. Изменить реализацию контроллера.
2. Запустить сборку, требуемую локальным рабочим процессом.
3. Запустить приложение.
4. Дождаться готовности.
5. Выполнить один верификационный запрос.
6. Записать общее время по настенным часам.

Другой может быть:

1. Изменить контракт репозитория.
2. Запустить один интеграционный тест.
3. Записать время до результата.

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

Они также естественно раскрывают компромиссы фреймворка. Генерация на этапе компиляции появляется на шаге два. Сканирование времени выполнения и инициализация контейнера появляются на шаге три. Настройка базы данных и логика теста появляются позже.

Ни один фреймворк не может спрятать свою стоимость, переместив её в другую фазу.

Это гораздо более здоровое сравнение.


Лучшая матрица бенчмарков

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

Рабочий процесс Что он измеряет
Чистая компиляция Сырая стоимость компилятора + генерации
Чистый артефакт Компилятор + генерация + упаковка
Тёплый артефакт Поведение повторной полной сборки
Правка тела метода Лучший случай обычной инкрементальной работы
Изменение DI Стоимость инкрементальной генерации графа
Изменение репозитория Инвалидация генерации кода данных
Изменение контроллера Инвалидация HTTP-генерации
Изменение DTO Влияние на нижестоящую компиляцию и маппинг
Юнит-тест Базовый уровень, независимый от фреймворка
Компонентный тест Стоимость конструирования графа/контекста
Интеграционный тест Сборка + контекст + инфраструктура
Перезапуск после правки Задержка цикла запуска разработчика
Свежий CI Конвейер в худшем случае
CI с удалённым кешем Реальный оптимизированный конвейер

Фреймворк затем можно описывать честно.

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

Это полезные выводы.

«Обработка аннотаций медленная» — нет.


Что бенчмарк Kora доказывает и не доказывает

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

Они используют конкретную рабочую нагрузку в стиле PetClinic, конкретные интеграции, конкретные задачи сборки, конкретные настройки Gradle, конкретное оборудование и повторные запуски в определённой среде.

Измените форму приложения — и результат может измениться.

Приложение Kora с большим количеством генерации и сотнями репозиториев может вести себя иначе, чем крошечный HTTP-сервис. Приложение Spring, тщательно оптимизированное для AOT и запуска, может вести себя иначе, чем стандартное приложение. Kotlin может изменить баланс. Удалённый кеш сборки может трансформировать поведение CI. Большая корпоративная сборка Gradle с пользовательскими плагинами может доминироваться конфигурацией или разрешением зависимостей, а не обработкой фреймворка.

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

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

Следующий шаг — измерить вашу рабочую нагрузку.


Правильный вопрос для Kora не «генерирует ли он код?»

Он, очевидно, генерирует.

Лучшие вопросы:

Сколько кода генерируется?

Насколько он сложен?

Сколько генерации локально, а сколько на уровне приложения?

Какие изменения инвалидируют какие сгенерированные выходы?

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

Может ли Gradle переиспользовать выходы из кеша?

Как Java сравнивается с Kotlin?

Что происходит при изменении зависимости конструктора?

Что происходит при изменении только бизнес-логики?

Как быстро приложение запускается после этого?

Как быстро может запуститься компонентный контекст?

Как быстро интеграционный тест возвращает результат?

Как часто валидация на этапе компиляции предотвращает потраченный цикл времени выполнения?

Эти вопросы дают инженерные ответы.


Внедрение зависимостей на этапе компиляции следует оценивать по общей стоимости жизненного цикла графа

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

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

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

Контейнер времени выполнения разрешает больше её при инициализации процесса.

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

Что дешевле, зависит от частот.

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

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

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


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

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

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

Артефакт может запускаться много раз: машины разработчиков, тесты, задания CI, staging, катящиеся развёртывания, события автоскейлинга, замены узлов, отработка отказа, восстановление scale-to-zero, замена spot-инстансов и восстановление после сбоев.

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

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

Но она объясняет, почему архитекторы фреймворков могут рационально тратить немного CPU сборки, чтобы уменьшить CPU запуска.

Релевантная цель оптимизации — жизненный цикл артефакта, а не один вызов javac.


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

Есть ещё одна операционная асимметрия.

Инфраструктура сборки часто централизована, параллельна, кешируема и предсказуема. CI-машины можно обеспечивать значительным CPU. Выходы сборки можно разделять. Компиляция происходит вне пути запроса.

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

Секунда времени сборки и секунда задержки готовности поэтому имеют разную операционную ценность.

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

Дизайн фреймворка должен оценивать их по-разному.


Почему это важно для небольших сервисов

Микросервисы усиливают обе стороны компромисса.

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

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

Пятисекундный запуск контекста относительно неважен в 40-минутном тестовом наборе. Он доминирует в 700-миллисекундном теле теста.

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

Архитектура Kora нацелена на эту область: относительно небольшие, явные графы приложений; сгенерированная инфраструктура; тонкие интеграции; быстрый запуск; прямой JVM-код.

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


Что разработчики должны отслеживать в реальном проекте Kora

Если проект Kora начинает ощущаться медленным в сборке, не начинайте с предположения, что «обработка аннотаций — это цена Kora».

Измеряйте.

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

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

Сравните чистую сборку с тёплой.

Сравните правку бизнес-логики с правкой графа.

Проверьте, включён ли кеш конфигурации и совместим ли он.

Проверьте, полезен ли кеш сборки.

Исследуйте, не связаны ли модули без необходимости.

Посмотрите на объём сгенерированных исходников.

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

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

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

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

Если доминирует конфигурация Gradle, изменение стратегии DI не решит проблему.

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

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

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


Избегайте бенчмарк-театра

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

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

Не сравнивайте один фреймворк с тёплым демоном, а другой — из холодного процесса.

Не включайте кеш сборки для одного и отключайте для другого.

Не сравнивайте тонкое приложение в одном фреймворке с производственно-интегрированным приложением в другом.

Не запускайте разные задачи упаковки и не описывайте их как идентичные без объяснения разницы.

Не сравнивайте запуск до готовности в одном фреймворке с полной готовностью в другом.

Не игнорируйте версию JVM.

Не игнорируйте Kotlin против Java.

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

Не скрывайте, тёплые ли кеши файловой системы.

Не сообщайте только сценарий, который даёт желаемый вывод.

Вопрос Kora достаточно интересен и без манипуляций.


Фреймворк может сместить стоимость без увеличения общей стоимости

Это ключевой концептуальный момент.

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

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

Этап запуска становится дешевле.

Это смещение стоимости.

Поднимается ли общая стоимость, зависит от:

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

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

Для CI включите запуски конвейеров.

Для производства включите развёртывания и масштабирование.

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

Поэтому даже нетривиальная стоимость времени сборки может быть хорошим обменом.

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


Миф не в том, что генерация на этапе компиляции не имеет стоимости

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

Ложное утверждение не таково:

Генерация кода на этапе компиляции добавляет работу сборки.

Это утверждение истинно.

Ложное утверждение:

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

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

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

Его архитектура заставляет нас использовать правильную модель учёта.


Метрика, которая на самом деле важна

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

T_feedback =
T_setup_сборки
+ T_инкрементальная_компиляция
+ T_генерация
+ T_запуск_контекста
+ T_тест

Для недействительных структурных правок она может вместо этого быть:

T_feedback =
T_setup_сборки
+ T_компиляция_до_диагностики

Для CI:

T_ci =
T_checkout
+ T_конфигурация
+ T_компиляция
+ T_генерация
+ T_тесты
+ T_запуски_контекстов
+ T_упаковка

Для чистых релизных сборок:

T_release =
T_чистая_сборка
+ T_тест
+ T_упаковка

Фреймворк может оптимизировать разные уравнения по-разному.

Ни один таймер обработчика аннотаций не отвечает на все из них.


Что «быстрая компиляция» должна означать в 2026 году

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

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

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

Значимое понятие поэтому — «время до действенной обратной связи».

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

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

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


Валидация на этапе компиляции — форма тестирования

Этот момент заслуживает акцента.

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

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

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

Конвейер становится:

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

а не:

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

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

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


Почему сгенерированные исходники Kora помогают диагностировать поведение сборки

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

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

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

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

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

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

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

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


Что опровергло бы аргумент?

Серьёзный инженерный тезис должен быть опровержимым.

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

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

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

Развенчание мифов должно защищать измерение, а не предопределённого победителя.


Лучшее утверждение

Самое сильное защитимое утверждение поэтому скромно, но важно:

Генерация кода на этапе компиляции не говорит вам, медленна ли разработка.

Вам нужна дополнительная информация.

Сколько работы генерируется?

Как часто она перезапускается?

Что её инвалидирует?

Можно ли переиспользовать выходы?

Как быстро приложение запускается после этого?

Как рано обнаруживаются сбои?

Насколько дороги компонентные и интеграционные тесты?

Какова сквозная задержка цикла правка-тест?

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


Компромисс Kora в одной диаграмме

Дизайн Kora можно резюмировать как перемещение работы фреймворка:

                         Сборка Kora
                            │
              ┌─────────────┼─────────────┐
              │             │             │
        разрешить граф   сгенерировать   проверить
              │             │             │
              └─────────────┼─────────────┘
                            ↓
                      явный артефакт
                            ↓
                   небольшой запуск времени выполнения
                            ↓
                     прямое выполнение

Стоимость концентрируется раньше.

Выгода появляется позже.

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

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


От стоимости компилятора к инженерии цикла обратной связи

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

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

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

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

Задачи должны быть дружелюбны к кешу.

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

API тестирования должны делать создание контекста дешёвым.

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

Эти свойства усиливают друг друга.

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

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


Более широкий JVM-тренд и так движется в этом направлении

Kora не одинок в признании ценности переноса детерминированной работы раньше.

По всей экосистеме JVM фреймворки всё чаще используют индексы, анализ заранее, сгенерированные метаданные, сгенерированные прокси, генерацию конфигурации native-image, расширение времени сборки, внедрение зависимостей на этапе компиляции и другие формы предвычисления.

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

Это делает старую бинарную классификацию — «фреймворк времени выполнения быстр в компиляции, фреймворк с компиляцией на этапе компиляции медлен в компиляции» — всё более устаревшей.

Современные фреймворки занимают разные точки на спектре AOT.

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


Производительность сборки — свойство системы

Это приводит к финальному общему уроку.

Скорость сборки — не свойство обработчика аннотаций.

Это свойство системы.

Эта система включает:

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

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

Всякий раз, когда кто-то говорит «обработка аннотаций делает сборки медленными», правильный ответ не «нет, не делает».

Правильный ответ:

Покажите рабочий процесс.


Заключение: измеряйте время до знания

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

Но время компиляции не синонимично времени разработки.

Разработчики заботятся о том, как быстро изменение становится знанием.

Скомпилировался ли код?

Действителен ли граф?

Может ли приложение запуститься?

Проходит ли тест?

Если что-то не так, как скоро мы узнаем?

Реальный цикл обратной связи:

изменение
  ↓
инкрементальная компиляция
  ↓
запуск приложения/контекста
  ↓
запуск теста
  ↓
получение результата

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

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

Архитектура Kora делает компромисс необычно видимым, потому что фреймворк явен с обеих сторон. Генерация графа добавляет работу сборки. Выплата в том, что большая часть фреймворка уже сконструирована, проверена и представлена как прямой код до того, как JVM начнёт обслуживать приложение.

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

Он более точен:

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

И для разработчиков финальная метрика должна быть ещё шире:

Не оптимизируйте время, проведённое внутри компилятора, изолированно. Оптимизируйте время от изменения кода до знания, работает ли изменение.

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