Cut CLI startup roughly in half - #5
Merged
Merged
Conversation
Measurement first: the existing numbers were cold CLI wall time, which says
nothing about where the time goes. A new :benchmark module times the pipeline
in-process, and it turns out conversion was never the problem — every fixture
but Wikipedia parses and renders in under 5 ms.
What was slow:
- Tika's MIME registry cost ~90 ms to build on first use, more than converting
most documents. SignatureMimeDetector reads the leading bytes and consults a
fixed table instead, so content still beats a wrong extension. TikaMimeDetector
moves out of utils and becomes public for callers who want full sniffing.
- converters built their heavy fields eagerly, so constructing the registry
loaded POI and Jackson even to convert a text file
- HtmlToDocument compiled a whitespace Regex per text node and rebuilt a parent
list per table row, on a document with tens of thousands of both
- JsonConverter allocated a second ObjectMapper on every call
The CLI now also optimizes for short runs: installDist records a class-data-sharing
archive into the distribution, and the JVM compiles with C1 only. The start script
skips the archive when it is absent, so distZip and MIKROMARKDOWN_NO_CDS=1 both work.
CLI wall time, best of eight:
json 298 -> 95 ms docx 446 -> 190 ms
epub 329 -> 100 ms pdf 437 -> 201 ms
blog 352 -> 109 ms pptx 506 -> 240 ms
wiki 362 -> 159 ms xlsx 446 -> 183 ms
That is about 2x faster than Python markitdown and still 4-8x slower than Rust
anydoc, which is process startup: java -version alone is 41 ms here. Closing that
would need ahead-of-time compilation, not a faster pipeline.
Converted output is byte-identical across all eight fixtures.
Also scopes the Konsist rules to the library module, so the new benchmark module's
println and Main.kt do not trip rules describing library architecture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Goal was matching anydoc. We got about 2x faster; the remaining gap is process startup, not the pipeline. Details below, including what I could not close.
Measure first
The only numbers we had were cold CLI wall time, which cannot distinguish conversion from JVM startup. A new
:benchmarkmodule times the pipeline in-process:Conversion was never the problem — best of 50 after warmup:
For everything but Wikipedia, our in-process conversion is already faster than anydoc's entire process.
What was actually slow
SignatureMimeDetectorreads the leading bytes and consults a fixed table, so a mislabelled.txtthat is really a PDF or OOXML package is still identified correctly.TikaMimeDetectormoves out ofutils, becomes public, and stays available for callers who want content sniffing of text formats too.HtmlToDocumentcompiled aRegexper text node and rebuilt a parent list per table row — on a document with tens of thousands of both.JsonConverterallocated a secondObjectMapperper call.Startup
installDistnow records a class-data-sharing archive into the distribution, which roughly halves startup. It uses a static two-step dump (DumpLoadedClassListthen-Xshare:dump) rather than-XX:ArchiveClassesAtExit, because the dynamic form needs the JDK's own base archive and JetBrains Runtime does not ship one. The start script only passes the archive when it exists, sodistZipandMIKROMARKDOWN_NO_CDS=1both keep working.-XX:TieredStopAtLevel=1). C2 never pays for itself in a 150 ms process. Library embedders are unaffected — this is a start-script flag.Result
CLI wall time, best of eight:
Three-way, whole process: MikroMarkdown 100–240 ms, markitdown 410–540 ms, anydoc 22–27 ms.
The gap I did not close
We are still 4–8x anydoc on CLI wall time, and I don't think that closes with more tuning.
java -versionalone is 41 ms on this machine, and a JSON conversion — 0.04 ms of actual work — still takes 95 ms. What remains is class loading for Kotlin's stdlib, clikt, kotlinx-io and the format libraries.Matching a native binary needs ahead-of-time compilation. GraalVM
native-imageis the real option, but POI and XMLBeans lean on reflection heavily enough that it is its own project, plausibly viable for a PDF/HTML/CSV/JSON-only build first. Happy to spike it if you want the number.Verification
./gradlew checkpasses. Output is byte-identical across all eight fixtures — this changes when work happens, not what is produced.The Konsist rules are now scoped to the
librarymodule, so the benchmark module'sprintlnand itsMain.ktdon't trip rules that describe library architecture.🤖 Generated with Claude Code