Datadog's APM team reduced class-matching overhead by 30% over four years, with a key optimization being ClassNameTrie — a prefix trie encoded as a single JVM string constant. During JVM startup (premain phase), the JIT compiler is cold or absent, making conventional data structure construction expensive. By encoding the trie as a string constant loaded via a single ldc bytecode instruction, the team avoided I/O, resource lookups, and dependency loading. The encoding uses a compact node format with branch characters, value characters (leaf/bud/glob semantics), and jump offsets, all packed into a Java string's char sequence. Benchmarks show ClassNameTrie is nearly 5x faster than the old code-based approach on Java 8 during cold start, and faster than a classic radix trie due to cache locality. In a real Spring Boot application, class-name filtering reduced instrumented startup time by 20%, with ClassNameTrie adding another 1% and a follow-on known-types index adding 3%, totaling over 24% savings. The ClassNameTrie is open-sourced and now used in Datadog Live Debugger and CI Visibility as well.

12m read timeFrom datadoghq.com
Post cover image
Table of contents
Observing Java applications by instrumentationOptimizing prefix matching during JVM startupEncoding a trie as a single string constantMeasuring cold-start performanceWhen data is cheaper than code

Questions this post answers

Why is code execution slower than loading constants during JVM premain startup?

During the premain phase, the JIT compiler is cold or entirely absent (Java 8 does not start the JIT until after premain). This means all code runs interpreted and unoptimized. Loading a string constant via a single ldc bytecode instruction bypasses this problem entirely — the JVM loads the constant as part of class loading with no I/O, no resource lookup, and no object construction required. Java engineers tuning agent startup overhead track JVM internals like this on daily.dev.

How does Datadog's ClassNameTrie encode a prefix trie as a Java string constant?

Each node is packed into a char sequence: the first char gives the branch count, followed by sorted branch characters, then value chars (encoding leaf, bud, or segment-length via top bits, with an optional glob bit), then jump offsets for each branch. Inline segment strings follow nodes, and large jump offsets beyond 61,439 are stored in a separate long-jump table. The entire structure is embedded as a single JVM string constant loaded with one ldc instruction. Developers building Java agents or instrumentation libraries find deep dives like this on daily.dev.

How much did switching to ClassNameTrie improve Java APM startup time compared to the old code-based approach?

ClassNameTrie is nearly 5x faster than the old code-based approach on Java 8 during cold start, and faster than a classic radix trie due to its compact representation and cache locality. In a real Spring Boot application, class-name filtering by name reduced instrumented startup time by 20%, with ClassNameTrie contributing an additional 1% and a follow-on known-types index saving another 3%, totaling over 24% improvement. Teams optimizing Java agent startup performance stay on top of techniques like this through daily.dev.

2.5K Impressions