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:
18
spring-boot-startup-time/docs/output/00-versions.txt
Normal file
18
spring-boot-startup-time/docs/output/00-versions.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
=== java -version ===
|
||||
openjdk version "25.0.4.1" 2026-08-18 LTS
|
||||
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
|
||||
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
|
||||
|
||||
=== mvn -version ===
|
||||
Apache Maven 3.9.11 (3e54c93a704957b63ee3494413a2b544fd3d825b)
|
||||
|
||||
=== mvn dependency:list (selected) ===
|
||||
com.h2database:h2:jar:2.4.240:runtime -- module com.h2database [auto]
|
||||
io.micrometer:micrometer-core:jar:1.17.1:compile -- module micrometer.core [auto]
|
||||
org.apache.tomcat.embed:tomcat-embed-core:jar:11.0.24:compile -- module org.apache.tomcat.embed.core
|
||||
org.hibernate.orm:hibernate-core:jar:7.4.5.Final:compile -- module org.hibernate.orm.core [auto]
|
||||
org.springframework.boot:spring-boot:jar:4.1.1:compile -- module spring.boot [auto]
|
||||
org.springframework.data:spring-data-jpa:jar:4.1.1:compile -- module spring.data.jpa [auto]
|
||||
org.springframework:spring-beans:jar:7.0.9:compile -- module spring.beans [auto]
|
||||
org.springframework:spring-context:jar:7.0.9:compile -- module spring.context [auto]
|
||||
org.springframework:spring-core:jar:7.0.9:compile -- module spring.core [auto]
|
||||
56
spring-boot-startup-time/docs/output/01-api-corrections.txt
Normal file
56
spring-boot-startup-time/docs/output/01-api-corrections.txt
Normal file
@@ -0,0 +1,56 @@
|
||||
=== Where getApplicationStartup() actually lives (Spring Framework 7.0.9) ===
|
||||
|
||||
Injecting ApplicationContext and calling getApplicationStartup() does not compile:
|
||||
|
||||
[ERROR] .../config/StartupBeans.java:[22,45] cannot find symbol
|
||||
symbol: method getApplicationStartup()
|
||||
location: variable context of type org.springframework.context.ApplicationContext
|
||||
|
||||
javap says the accessor is declared one interface down, and also on the bean factory:
|
||||
|
||||
$ javap -cp . org.springframework.context.ConfigurableApplicationContext | grep ApplicationStartup
|
||||
public abstract void setApplicationStartup(org.springframework.core.metrics.ApplicationStartup);
|
||||
public abstract org.springframework.core.metrics.ApplicationStartup getApplicationStartup();
|
||||
|
||||
$ javap -cp . org.springframework.beans.factory.config.ConfigurableBeanFactory | grep ApplicationStartup
|
||||
public abstract void setApplicationStartup(org.springframework.core.metrics.ApplicationStartup);
|
||||
public abstract org.springframework.core.metrics.ApplicationStartup getApplicationStartup();
|
||||
|
||||
=== The buffering API surface (spring-boot 4.1.1) ===
|
||||
|
||||
$ javap -cp . org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup
|
||||
public class BufferingApplicationStartup implements org.springframework.core.metrics.ApplicationStartup {
|
||||
public BufferingApplicationStartup(int);
|
||||
public void startRecording();
|
||||
public void addFilter(java.util.function.Predicate<org.springframework.core.metrics.StartupStep>);
|
||||
public org.springframework.core.metrics.StartupStep start(java.lang.String);
|
||||
public StartupTimeline getBufferedTimeline();
|
||||
public StartupTimeline drainBufferedTimeline();
|
||||
}
|
||||
|
||||
$ javap -cp . org.springframework.boot.context.metrics.buffering.StartupTimeline$TimelineEvent
|
||||
public class StartupTimeline$TimelineEvent {
|
||||
public java.time.Instant getStartTime();
|
||||
public java.time.Instant getEndTime();
|
||||
public java.time.Duration getDuration();
|
||||
public org.springframework.core.metrics.StartupStep getStartupStep();
|
||||
}
|
||||
|
||||
Note: there is no setter for the buffer size and no Spring property that installs this.
|
||||
The only constructor takes the capacity, and it must be handed to SpringApplication
|
||||
before run().
|
||||
|
||||
=== You do not need a @Bean method for it ===
|
||||
|
||||
Declaring one produces a startup failure, because Boot has already registered the
|
||||
instance as a singleton:
|
||||
|
||||
Parameter 0 of constructor in com.ankurm.startup.web.StartupDiagnosticsEndpoint
|
||||
required a single bean, but 2 were found:
|
||||
- bufferingApplicationStartup: defined by method 'bufferingApplicationStartup'
|
||||
in class path resource [com/ankurm/startup/config/StartupBeans.class]
|
||||
- applicationStartup: a programmatically registered singleton
|
||||
|
||||
So the correct move is to inject ApplicationStartup directly and narrow with instanceof.
|
||||
Injecting BufferingApplicationStartup by its concrete type compiles and works -- until
|
||||
somebody runs without tracking, at which point the application will not start at all.
|
||||
67
spring-boot-startup-time/docs/output/02-startup-tree.txt
Normal file
67
spring-boot-startup-time/docs/output/02-startup-tree.txt
Normal file
@@ -0,0 +1,67 @@
|
||||
=== Boot's own startup line ===
|
||||
Started StartupDiagnosisApplication in 6.6 seconds (process running for 7.4)
|
||||
|
||||
=== /diag/startup ===
|
||||
recorded steps: 400
|
||||
|
||||
-- self time by step name (self = duration minus direct children) --
|
||||
self ms count step
|
||||
4253.68 361 spring.beans.instantiate
|
||||
1014.51 1 spring.context.config-classes.parse
|
||||
402.81 1 spring.boot.application.environment-prepared
|
||||
160.39 1 spring.boot.webserver.create
|
||||
153.07 1 spring.boot.application.started
|
||||
113.74 1 spring.context.refresh
|
||||
95.72 1 spring.data.repository.proxy
|
||||
51.50 6 spring.beans.smart-initialize
|
||||
43.85 1 spring.data.repository.scanning
|
||||
35.02 1 spring.boot.application.starting
|
||||
31.10 9 spring.context.bean-factory.post-process
|
||||
22.48 1 spring.context.beandef-registry.post-process
|
||||
18.09 1 spring.context.beans.post-process
|
||||
13.78 1 spring.boot.application.context-loaded
|
||||
12.92 1 spring.data.repository.composition
|
||||
5.15 5 spring.data.repository.postprocessor
|
||||
4.70 1 spring.data.repository.init
|
||||
0.89 1 spring.data.repository.target
|
||||
0.68 1 spring.context.config-classes.enhance
|
||||
0.48 1 spring.boot.application.ready
|
||||
0.42 1 spring.data.repository.postprocessors
|
||||
0.26 1 spring.boot.application.context-prepared
|
||||
0.07 1 spring.data.repository.metadata
|
||||
|
||||
-- slowest individual steps by SELF time (the culprits) --
|
||||
self ms total ms step / tags
|
||||
1413.83 1803.62 spring.beans.instantiate &entityManagerFactory
|
||||
1014.51 1066.42 spring.context.config-classes.parse
|
||||
503.84 503.84 spring.beans.instantiate tariffCacheWarmer
|
||||
402.81 402.81 spring.boot.application.environment-prepared
|
||||
326.96 326.96 spring.beans.instantiate keystoreLoader
|
||||
268.41 312.88 spring.beans.instantiate dataSourceScriptDatabaseInitializer
|
||||
204.92 470.83 spring.beans.instantiate orderRepository
|
||||
160.39 460.45 spring.boot.webserver.create
|
||||
153.07 153.07 spring.boot.application.started
|
||||
113.74 5829.88 spring.context.refresh
|
||||
95.72 101.29 spring.data.repository.proxy
|
||||
64.25 64.25 spring.beans.instantiate jpaMappingContext
|
||||
|
||||
-- slowest individual steps by TOTAL time (the containers) --
|
||||
total ms self ms step / tags
|
||||
5829.88 113.74 spring.context.refresh
|
||||
1803.62 1413.83 spring.beans.instantiate &entityManagerFactory
|
||||
1344.76 18.09 spring.context.beans.post-process
|
||||
1088.90 22.48 spring.context.beandef-registry.post-process
|
||||
1066.42 1014.51 spring.context.config-classes.parse
|
||||
514.24 10.41 spring.beans.instantiate reportTemplateRegistry
|
||||
503.84 503.84 spring.beans.instantiate tariffCacheWarmer
|
||||
479.45 8.62 spring.beans.instantiate orderController
|
||||
470.83 204.92 spring.beans.instantiate orderRepository
|
||||
460.45 160.39 spring.boot.webserver.create
|
||||
402.81 402.81 spring.boot.application.environment-prepared
|
||||
326.96 326.96 spring.beans.instantiate keystoreLoader
|
||||
|
||||
=== actuator's own endpoint: GET peeks, POST drains ===
|
||||
GET /actuator/startup -> events in response: 400
|
||||
POST /actuator/startup -> events in response: 400
|
||||
POST /actuator/startup -> events in response: 0
|
||||
GET /actuator/startup -> events in response: 0
|
||||
60
spring-boot-startup-time/docs/output/03-jfr.txt
Normal file
60
spring-boot-startup-time/docs/output/03-jfr.txt
Normal file
@@ -0,0 +1,60 @@
|
||||
Started StartupDiagnosisApplication in 7.441 seconds (process running for 8.49)
|
||||
|
||||
=== jfr summary (Spring rows only) ===
|
||||
|
||||
Version: 2.1
|
||||
Chunks: 1
|
||||
=========================================================================================
|
||||
org.springframework.core.metrics.jfr.FlightRecorderStartupEvent 398 33261
|
||||
|
||||
=== the event type, in full ===
|
||||
@Name("org.springframework.core.metrics.jfr.FlightRecorderStartupEvent")
|
||||
@Category("Spring Application")
|
||||
@Label("Startup Step")
|
||||
@Description("Spring Application Startup")
|
||||
class FlightRecorderStartupEvent extends jdk.jfr.Event {
|
||||
@Label("Start Time")
|
||||
@Timestamp("TICKS")
|
||||
long startTime;
|
||||
|
||||
@Label("Duration")
|
||||
@Timespan("TICKS")
|
||||
long duration;
|
||||
|
||||
@Label("Event Thread")
|
||||
@Description("Thread in which event was committed in")
|
||||
Thread eventThread;
|
||||
|
||||
@Label("Stack Trace")
|
||||
@Description("Stack Trace starting from the method the event was committed in")
|
||||
StackTrace stackTrace;
|
||||
|
||||
long eventId;
|
||||
|
||||
|
||||
=== the selector matters: the event is named by its FQCN, not 'StartupEvent' ===
|
||||
--events StartupEvent -> 0 events
|
||||
--events 'org.springframework.core.*' -> 398 events
|
||||
|
||||
=== 8 slowest startup steps, straight out of the recording ===
|
||||
398 StartupEvent records in the recording
|
||||
|
||||
duration name / tags
|
||||
PT6.544849841S spring.context.refresh
|
||||
PT2.263845435S spring.beans.instantiate beanName=&entityManagerFactory,beanType=interface org.springframework.context.weaving.LoadTimeWeaverAware,
|
||||
PT1.500763631S spring.context.beans.post-process
|
||||
PT1.202591188S spring.context.beandef-registry.post-process postProcessor=org.springframework.context.annotation.ConfigurationClassPostProcessor@76b224cd,
|
||||
PT1.176668418S spring.context.config-classes.parse classCount=130,
|
||||
PT0.538651275S spring.beans.instantiate beanName=reportTemplateRegistry,
|
||||
PT0.53548451S spring.beans.instantiate beanName=tariffCacheWarmer,beanType=class com.ankurm.startup.slow.TariffCacheWarmer,
|
||||
PT0.515340537S spring.boot.webserver.create factory=class org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory,
|
||||
|
||||
=== what JFR gives you that the buffer does not: JVM context in the same file ===
|
||||
jdk.ExecutionSample 345 3787
|
||||
jdk.GCPhasePauseLevel1 132 5281
|
||||
jdk.GCPhasePauseLevel2 50 1744
|
||||
jdk.GCPhasePause 39 937
|
||||
jdk.Compilation 16 452
|
||||
jdk.ClassLoaderStatistics 10 260
|
||||
|
||||
total GC pause time during this startup: 39 events, 289.8 ms
|
||||
10
spring-boot-startup-time/docs/output/04-scan-tax.txt
Normal file
10
spring-boot-startup-time/docs/output/04-scan-tax.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
runs per variant: 3 (first run of each also queried for phase self times)
|
||||
|
||||
variant | Started in (s) | parse ms | instMs | steps
|
||||
---------------------------+--------------------+-----------+-----------+-------
|
||||
baseline | 7.293,6.685,6.86 | 1055.69 | 4854.73 | 400
|
||||
+5000 plain classes | 7.626,7.143,7.04 | 1617.38 | 4468.31 | 400
|
||||
+5000 @Component | 11.02,11.647,11.481 | 3221.82 | 6097.98 | 5400
|
||||
+5000 @Component, lazy | 9.688,9.374,9.609 | 3434.76 | 4982.61 | 324
|
||||
|
||||
bulk package removed; jar rebuilt at baseline.
|
||||
32
spring-boot-startup-time/docs/output/05-what-helps.txt
Normal file
32
spring-boot-startup-time/docs/output/05-what-helps.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
variant | Started in (s), each run | median
|
||||
-----------------------------------+----------------------------+---------
|
||||
no tracking | 6.871,6.456,7.473,6.992 | 6.9315
|
||||
BufferingApplicationStartup | 6.448,6.856,6.416,6.528 | 6.488
|
||||
FlightRecorder + recording | 6.496,6.739,6.343,6.475 | 6.4855
|
||||
lazy-initialization | 5.075,5.331,5.217,5.266 | 5.2415
|
||||
|
||||
=== JDK 25 AOT cache (Project Leyden) ===
|
||||
openjdk version "25.0.4.1" 2026-08-18 LTS
|
||||
-- training run (-XX:AOTMode=record) --
|
||||
-rw-r--r-- 1 eloquent-blissful-maxwell eloquent-blissful-maxwell 119484416 Sep 4 23:44 /tmp/app.aotconf
|
||||
-- assembly run (-XX:AOTMode=create) --
|
||||
Reading AOTConfiguration /tmp/app.aotconf and writing AOTCache /tmp/app.aot
|
||||
AOTCache creation is complete: /tmp/app.aot 118001664 bytes
|
||||
-rw-r--r-- 1 eloquent-blissful-maxwell eloquent-blissful-maxwell 118001664 Sep 4 23:44 /tmp/app.aot
|
||||
|
||||
AOT cache (-XX:AOTCache) | 5.028,4.729,4.708,4.905 | 4.817
|
||||
AOT cache + lazy | 3.649,3.488,3.354,3.384 | 3.436
|
||||
|
||||
=== where the AOT cache takes the time from (same tracker, same jar) ===
|
||||
run | parse ms | inst ms | webserver
|
||||
-----------------------+-----------+-----------+----------
|
||||
plain | 986.66 | 4344.21 | 150.69
|
||||
aotcache | 729.16 | 3155.96 | 100.96
|
||||
|
||||
Notes:
|
||||
* Timings are from one machine (Temurin 25.0.4.1+1, Linux x86_64, container) and are
|
||||
indicative. The ratios between rows are the point, not the absolute numbers.
|
||||
* "no tracking" and the two instrumented rows overlap. Recording startup steps is not
|
||||
measurably slower than not recording them on this application.
|
||||
* The AOT cache was trained on this exact jar with -XX:AOTMode=record and assembled with
|
||||
-XX:AOTMode=create. It is 118 MB and is invalidated by a classpath change.
|
||||
28
spring-boot-startup-time/docs/output/06-buffer-overflow.txt
Normal file
28
spring-boot-startup-time/docs/output/06-buffer-overflow.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
--- capacity 2048 ---
|
||||
started, recorded steps: 2048
|
||||
--- capacity 16384 ---
|
||||
started, recorded steps: 5400
|
||||
|
||||
=== which steps survive truncation (capacity 2048, 5400 available) ===
|
||||
|
||||
$ curl -s localhost:8080/actuator/startup | python3 -c '...'
|
||||
events: 2048
|
||||
first 3: ['spring.boot.application.starting',
|
||||
'spring.boot.application.environment-prepared',
|
||||
'spring.boot.application.context-prepared']
|
||||
last 3 : ['spring.beans.instantiate', 'spring.beans.instantiate', 'spring.beans.instantiate']
|
||||
last tags: [{'key': 'beanName', 'value': 'bulk2718'}]
|
||||
names: [('spring.beans.instantiate', 2029),
|
||||
('spring.context.bean-factory.post-process', 9),
|
||||
('spring.boot.application.starting', 1),
|
||||
('spring.boot.application.environment-prepared', 1)]
|
||||
|
||||
The buffer keeps the FIRST 2048 steps and drops everything after. The timeline therefore
|
||||
ends mid-instantiation, and 'spring.context.refresh', 'spring.boot.application.started'
|
||||
and 'spring.boot.application.ready' -- the steps that bracket everything else -- are
|
||||
absent, because a step is only recorded when it ENDS.
|
||||
|
||||
$ grep -ic buffer app.log
|
||||
0
|
||||
|
||||
No warning. No exception. The endpoint returns 200 with a well-formed, truncated answer.
|
||||
Reference in New Issue
Block a user