Иллюзия скорости: двухуровневый кэш, который не может ускорить то, что не умеет Doxygen

 · 3 min read

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

Два отпечатка для контроля кэша

Два уровня кэша: XML и IR

Мы построили схему кэширования на базе двух независимых отпечатков, разделив зоны ответственности. Первый, 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() и почему даже сам фикс оказался утечкой — читайте в следующем эпизоде.