Иллюзия скорости: двухуровневый кэш, который не может ускорить то, что не умеет Doxygen
· 3 min read
Полная пересборка документации каждый раз безжалостно гоняла doxygen.exe и заново парсила гигантский XML-вывод. Инструменту было абсолютно плевать, поменяли вы одну запятую в комментариях или переписали половину SDK. На реальных проектах это выливалось в минуты бессмысленного ожидания сборки после каждого пуша, хотя львиную долю работы можно было смело пропустить.
Два отпечатка для контроля кэша

Мы построили схему кэширования на базе двух независимых отпечатков, разделив зоны ответственности. Первый, xml_fingerprint, определяет целесообразность запуска самого Doxygen. Мы считаем его контент-хешем по бинарнику, эффективному Doxyfile и дереву исходников. Проверку по времени модификации файлов выбросили сразу, чтобы каждый новый чекаут на CI не сбрасывал кэш ложными триггерами. Параметр OUTPUT_DIRECTORY тоже пришлось исключить из хеширования, чтобы избежать циклической зависимости отпечатка от генерируемых файлов.
Второй отпечаток, ir_fingerprint, решает судьбу парсинга готового XML в структуру ProjectCatalog. Он включает в себя первый хеш, но добавляет к нему конфигурацию парсера вроде exclude_swig_internals и хеш кода самой логики разбора. Любая мелкая правка в правилах парсинга мгновенно инвалидирует сохранённое внутреннее представление (IR). Нам удалось поймать потенциальный баг с рассинхронизацией состояний еще до начала написания кода благодаря паре независимых сессий ИИ-ревью.
Границы оптимизации вычислений

Кэш процессорного времени жестко обрывается на границе формирования IR. Само превращение объекта ProjectCatalog в финальный Markdown вычисляется с нуля каждый раз. Внимательные читатели могут найти в storage.py класс BuildCacheManager, управляющий кэшем рендеринга. Здесь он работает исключительно с вводом-выводом, защищая локальный livereload в Hugo от ложных срабатываний.
Мы отказались от кэширования самого рендера по вполне прагматичным соображениям. Рендер одной страницы плотно завязан на глобальное состояние, вроде навигационного дерева sidebar.toml и перекрестных ссылок. Изменение структуры таксономии ломает верстку десятков файлов одновременно, и ловить такую инвалидацию невероятно сложно. К тому же работа Doxygen занимает долгие минуты, тогда как проход по готовому дереву IR длится жалкие секунды.
Почему скорость остается иллюзией

Наш двухуровневый кэш умеет только одно — полностью пропускать шаг сборки при совпадении отпечатков. Doxygen физически лишен режима инкрементального парсинга и не умеет работать быстрее. Любая измененная буква в исходниках заставит нас терпеть полный прогон парсера. Настоящая инкрементальность, при которой пересчитывается только измененный файл, абсолютно несовместима с монолитным XML-дампом.
В архитектурных планах на мажорную версию мы уже прописали отказ от Doxygen в пользу прямого построения AST через libclang или tree-sitter… Но настоящая расплата за использование Doxygen ждала нас не в скорости парсинга.
Ускорять то, что медленно работает — это полбеды. Куда хуже, когда инструмент начинает методично пожирать диск сервера, пока не останется 0 байт свободного места. О том, как мы трижды ловили самую живучую утечку в tempfile.mkdtemp() и почему даже сам фикс оказался утечкой — читайте в следующем эпизоде.