DEV Community

Cover image for The Illusion of Speed: A Two-Level Cache Can't Speed Up What Doxygen Can't Skip
Flude team
Flude team

Posted on Originally published at blog.flude.guide

The Illusion of Speed: A Two-Level Cache Can't Speed Up What Doxygen Can't Skip

Every single full documentation rebuild ruthlessly executed doxygen.exe and parsed its massive XML output. The tool simply didn't care if you fixed a single typo in the comments or rewrote half the SDK. On real projects this translated into minutes of agonizing waiting after every push, even though the vast majority of the work could have been safely skipped.

Two Fingerprints for Cache Control

Two cache levels: XML and IR

We built the caching scheme around two independent fingerprints to strictly divide responsibilities. The xml_fingerprint determines whether the Doxygen run can be skipped entirely. We compute it as a content hash spanning the binary, the effective Doxyfile, and the source tree. Modification time checks were thrown out immediately to prevent fresh CI checkouts from falsely invalidating the cache. The OUTPUT_DIRECTORY parameter also had to be excluded from the hash to avoid a circular dependency between the fingerprint and the generated files.

The second fingerprint, ir_fingerprint, decides the fate of parsing the generated XML into a ProjectCatalog structure. It incorporates the first hash but adds parser configuration like exclude_swig_internals and the logic hash. Any minor tweak to the parsing rules instantly invalidates the saved internal representation (IR). We managed to catch a potential state desync bug before writing any code thanks to a couple of independent AI review sessions.

Boundaries of Compute Optimization

Cache invalidation risk

The compute time cache abruptly stops at the IR boundary. The actual transformation of the ProjectCatalog object into final Markdown is calculated from scratch every time. Observant readers might spot a BuildCacheManager class in storage.py that handles a rendering cache. It operates purely on file I/O to protect local Hugo livereload from triggering falsely.

We dropped the idea of caching the render computation for very pragmatic reasons. Rendering a single page is heavily tied to global state, such as the sidebar.toml navigation tree and cross-links. Altering the taxonomy structure breaks the layout of dozens of files simultaneously, making invalidation tracking incredibly complex. Furthermore, Doxygen processing takes long minutes, while traversing the ready IR tree takes mere seconds.

Why the Speed Remains an Illusion

Monolithic rebuild

Our two-level cache only knows how to do one thing—completely skip a build step when fingerprints match. Doxygen physically lacks an incremental parsing mode and cannot run any faster. Any altered character in the sources forces us to endure a full parser run. True incrementality, where only the modified file gets recomputed, is fundamentally incompatible with a monolithic XML dump.

In the architectural plans for the next major version we have already documented dropping Doxygen in favor of direct AST construction via libclang or tree-sitter—the choice between the two hasn't been made yet; the requirements treat them as equal candidates of the same technical direction. Incrementality becomes a natural property of the tool there, instead of something bolted on top with fingerprints—but that same shift will force us to throw away and rewrite the existing caching logic entirely. For now, we had to face the harsh limits of the free GitHub Actions cloud, which pushed us to build our own self-hosted runner. In the next episode we cover how we escaped to our own hardware.


Originally published on our blog: https://blog.flude.guide/blog/illusion-of-speed-caching

Also read us:

Top comments (0)