Spring Boot startup time: bean-by-bean diagnosis, and one directory per post

Adds spring-boot-startup-time/, the companion project for BLOG-618: a runnable
Spring Boot 4.1.1 application on JDK 25 that installs BufferingApplicationStartup
and FlightRecorderApplicationStartup behind a system property, and a /diag/startup
endpoint that computes step self time -- the number /actuator/startup does not give
you and the one that names the actual culprits.

Captured under docs/output/: the step tree sorted both ways, the same startup as JFR
events, a +5000-class experiment putting 0.11 ms per scanned class on the classpath
scan tax, the silent truncation a 2048-step buffer performs, and JDK 25 AOT cache
timings (6.93 s to 4.82 s). Post body and metadata live in post/.

Moves the existing Actuator project into actuator-in-production/ so the repository
holds one directory per article; the root README is now an index.
This commit is contained in:
2026-09-04 23:58:38 +05:30
parent 4b6cefa60a
commit 958b401f0f
112 changed files with 2744 additions and 154 deletions

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 740 300" role="img" aria-label="Two rankings of the same four hundred startup steps. Sorted by total duration the top entries are spring.context.refresh at 5830 milliseconds and reportTemplateRegistry at 514 milliseconds. Sorted by self time those fall to 114 and 10 milliseconds respectively, and the real costs are the entity manager factory at 1414 milliseconds, configuration class parsing at 1015, and tariffCacheWarmer at 504." xmlns="http://www.w3.org/2000/svg">
<style>
.t{font:600 13px system-ui,sans-serif;fill:#2b3138}
.h{font:600 11px system-ui,sans-serif;fill:#57606b}
.m{font:11px ui-monospace,Menlo,monospace;fill:#2b3138}
.c{font:11px system-ui,sans-serif;fill:#57606b}
</style>
<rect width="100%" height="100%" fill="#ffffff"/>
<text class="t" x="12" y="18">Same 400 steps, sorted two ways</text>
<text class="h" x="12" y="44">SORTED BY TOTAL DURATION — ranks containers</text>
<rect x="12" y="52" width="330" height="22" fill="#f7d9d3" stroke="#c56a54"/>
<text class="m" x="18" y="67">spring.context.refresh</text><text class="m" x="266" y="67">5829 ms</text>
<rect x="12" y="78" width="112" height="22" fill="#fdeccf" stroke="#c9973f"/>
<text class="m" x="18" y="93">&amp;entityManagerFactory</text><text class="m" x="266" y="93">1804 ms</text>
<rect x="12" y="104" width="40" height="22" fill="#f7d9d3" stroke="#c56a54"/>
<text class="m" x="58" y="119">reportTemplateRegistry</text><text class="m" x="266" y="119">514 ms</text>
<rect x="12" y="130" width="38" height="22" fill="#e7f4ea" stroke="#4a9d63"/>
<text class="m" x="56" y="145">tariffCacheWarmer</text><text class="m" x="266" y="145">504 ms</text>
<text class="h" x="392" y="44">SORTED BY SELF TIME — ranks culprits</text>
<rect x="392" y="52" width="88" height="22" fill="#e7f4ea" stroke="#4a9d63"/>
<text class="m" x="398" y="67">&amp;entityManagerFactory</text><text class="m" x="646" y="67">1414 ms</text>
<rect x="392" y="78" width="63" height="22" fill="#e7f4ea" stroke="#4a9d63"/>
<text class="m" x="461" y="93">config-classes.parse</text><text class="m" x="646" y="93">1015 ms</text>
<rect x="392" y="104" width="31" height="22" fill="#e7f4ea" stroke="#4a9d63"/>
<text class="m" x="429" y="119">tariffCacheWarmer</text><text class="m" x="646" y="119">504 ms</text>
<rect x="392" y="130" width="7" height="22" fill="#e8eefc" stroke="#5b7fc7" stroke-dasharray="3 3"/>
<text class="m" x="405" y="145">spring.context.refresh</text><text class="m" x="646" y="145">114 ms</text>
<rect x="392" y="156" width="1" height="22" fill="#e8eefc" stroke="#5b7fc7" stroke-dasharray="3 3"/>
<text class="m" x="399" y="171">reportTemplateRegistry</text><text class="m" x="646" y="171">10 ms</text>
<text class="c" x="12" y="212">reportTemplateRegistry looks like the fifth-worst bean in the application. It costs 10 ms.</text>
<text class="c" x="12" y="230">The other 504 ms belong to tariffCacheWarmer, which it happens to depend on and therefore triggers first.</text>
<text class="c" x="12" y="248">Whichever bean is constructed first in a dependency chain inherits the whole chain in the total column.</text>
<text class="c" x="12" y="266">That is an artefact of construction order. It is not a cost.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 740 250" role="img" aria-label="Diagram of a truncated startup buffer. The full timeline nests spring.context.refresh around configuration parsing and five thousand four hundred bean instantiations, ending with application started and ready. With a capacity of two thousand and forty-eight the buffer keeps the first two thousand and forty-eight steps to finish, all inner leaves, and silently drops spring.context.refresh, application started and application ready, because those steps end last." xmlns="http://www.w3.org/2000/svg">
<style>
.t{font:600 13px system-ui,sans-serif;fill:#2b3138}
.h{font:600 11px system-ui,sans-serif;fill:#57606b}
.m{font:11px ui-monospace,Menlo,monospace;fill:#2b3138}
.c{font:11px system-ui,sans-serif;fill:#57606b}
</style>
<rect width="100%" height="100%" fill="#ffffff"/>
<text class="t" x="12" y="18">What a 2048-step buffer keeps, and what it throws away</text>
<text class="h" x="12" y="42">STEPS, IN THE ORDER THEY END →</text>
<rect x="12" y="52" width="470" height="24" fill="#e7f4ea" stroke="#4a9d63"/>
<text class="m" x="20" y="68">2048 leaf steps: spring.beans.instantiate bulk1 … bulk2718</text>
<rect x="482" y="52" width="246" height="24" fill="#f4f5f7" stroke="#b7bec9" stroke-dasharray="3 3"/>
<text class="m" x="490" y="68">3352 more instantiate steps — dropped</text>
<rect x="482" y="84" width="246" height="24" fill="#f7d9d3" stroke="#c56a54" stroke-dasharray="3 3"/>
<text class="m" x="490" y="100">spring.context.refresh — dropped</text>
<rect x="482" y="116" width="246" height="24" fill="#f7d9d3" stroke="#c56a54" stroke-dasharray="3 3"/>
<text class="m" x="490" y="132">application.started / ready — dropped</text>
<line x1="482" y1="46" x2="482" y2="146" stroke="#c56a54" stroke-width="2"/>
<text class="h" x="488" y="44" fill="#c56a54">capacity reached</text>
<text class="c" x="12" y="176">The outermost steps end last, so they are the first casualties. Self-time arithmetic is now missing its outer frame,</text>
<text class="c" x="12" y="194">and the “whole refresh took N” figure is simply not in the data — while the endpoint still answers 200.</text>
<text class="c" x="12" y="218">The capacity counts steps, not beans. 16384 costs a few megabytes that are freed on the first drain. Pick the large number.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB