docs
This documentation has been enhanced for AI coding agents with comprehensive examples, complete API signatures, thread safety notes, error handling patterns, and production-ready usage patterns.
Package: org.springframework.boot.context.metrics.buffering
Module: org.springframework.boot:spring-boot
Since: 2.4.0
Application startup performance monitoring through buffered recording of startup steps. This allows developers to analyze, profile, and optimize application initialization by tracking the timeline and duration of each startup phase with nanosecond precision.
Spring Boot's startup metrics system provides detailed instrumentation of application startup through BufferingApplicationStartup. It records each initialization step with precise timing information, enabling performance analysis, regression detection, and bottleneck identification.
ApplicationStartup implementation that buffers startup steps and records their timestamps and processing times for performance analysis.
package org.springframework.boot.context.metrics.buffering;
import org.springframework.core.metrics.ApplicationStartup;
import org.springframework.core.metrics.StartupStep;
import java.util.function.Predicate;
/**
* ApplicationStartup implementation that buffers steps and records their
* timestamp and processing time. Once recording has been started, steps
* are buffered up until the configured capacity.
*
* Capacity: Once buffer is full, new steps are silently dropped.
* Thread Safety: Thread-safe. Multiple threads can record steps concurrently.
*
* @since 2.4.0
*/
public class BufferingApplicationStartup implements ApplicationStartup {
/**
* Create a new buffered ApplicationStartup with a limited capacity
* and start recording steps immediately.
*
* @param capacity the configured capacity; once reached, new steps not recorded
* @throws IllegalArgumentException if capacity <= 0
*/
public BufferingApplicationStartup(int capacity) {
// Implementation creates circular buffer with given capacity
}
/**
* Start the recording of steps and mark the beginning of the StartupTimeline.
* The constructor already implicitly calls this, but it can be reset
* as long as steps have not been recorded already.
*
* @throws IllegalStateException if called and steps have been recorded
*/
public void startRecording() {
// Resets buffer and timeline start time
}
/**
* Add a predicate filter to the list of existing ones.
* A StartupStep that doesn't match all filters will not be recorded.
* Filters are evaluated in the order they were added.
*
* @param filter the predicate filter to add (must not be null)
* @throws IllegalArgumentException if filter is null
*/
public void addFilter(Predicate<StartupStep> filter) {
// Adds filter to evaluation chain
}
/**
* Return the timeline as a snapshot of currently buffered steps.
* This will not remove steps from the buffer, allowing multiple reads.
*
* @return a snapshot of currently buffered steps (never null)
*/
public StartupTimeline getBufferedTimeline() {
// Returns immutable snapshot
}
/**
* Return the timeline by pulling steps from the buffer.
* This removes steps from the buffer, freeing memory.
* Subsequent calls return empty timeline.
*
* @return buffered steps drained from the buffer (never null)
*/
public StartupTimeline drainBufferedTimeline() {
// Clears buffer and returns all steps
}
@Override
public StartupStep start(String name) {
// Creates and records new startup step
}
}Representation of the timeline of steps recorded by BufferingApplicationStartup with nanosecond-precision timing.
package org.springframework.boot.context.metrics.buffering;
import org.springframework.core.metrics.StartupStep;
import java.time.Instant;
import java.time.Duration;
import java.util.List;
/**
* Represents the timeline of steps recorded by BufferingApplicationStartup.
* Each TimelineEvent has start and end time as well as duration with
* nanosecond precision.
*
* Timeline is immutable once created.
*
* @since 2.4.0
*/
public class StartupTimeline {
/**
* Return the start time of this timeline.
* This is when recording began (constructor or startRecording() call).
*
* @return the start time (never null)
*/
public Instant getStartTime() {
// Returns timeline start instant
}
/**
* Return the recorded events in chronological order.
* List is immutable.
*
* @return the events (never null, may be empty)
*/
public List<TimelineEvent> getEvents() {
// Returns immutable list of events
}
/**
* Event on the current StartupTimeline.
* Each event has start/end time, precise duration and complete
* StartupStep information associated with it.
*/
public static class TimelineEvent {
/**
* Return the start time of this event.
* When the step was created.
*
* @return the start time (never null)
*/
public Instant getStartTime() {
// Returns event start time
}
/**
* Return the end time of this event.
* When the step was completed.
*
* @return the end time (never null)
*/
public Instant getEndTime() {
// Returns event end time
}
/**
* Return the duration of this event.
* The processing time of the associated StartupStep with nanoseconds precision.
*
* @return the event duration (never null, never negative)
*/
public Duration getDuration() {
// Returns precise duration (nanosecond resolution)
}
/**
* Return the StartupStep information for this event.
* Includes step name and all tags/attributes.
*
* @return the step information (never null)
*/
public StartupStep getStartupStep() {
// Returns associated step
}
}
}Configure startup metrics to monitor application initialization:
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
/**
* Application with startup metrics enabled.
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
// Enable startup metrics with buffer of 10,000 steps
BufferingApplicationStartup startup = new BufferingApplicationStartup(10000);
app.setApplicationStartup(startup);
// Run application
app.run(args);
// After startup, analyze the timeline
StartupTimeline timeline = startup.drainBufferedTimeline();
analyzeStartup(timeline);
}
private static void analyzeStartup(StartupTimeline timeline) {
System.out.println("=== Application Startup Analysis ===");
System.out.println("Started at: " + timeline.getStartTime());
System.out.println("Total steps recorded: " + timeline.getEvents().size());
// Calculate total startup time
if (!timeline.getEvents().isEmpty()) {
StartupTimeline.TimelineEvent lastEvent = timeline.getEvents()
.get(timeline.getEvents().size() - 1);
Duration totalTime = Duration.between(
timeline.getStartTime(),
lastEvent.getEndTime()
);
System.out.printf("Total startup time: %d ms%n", totalTime.toMillis());
}
// Find slowest steps
System.out.println("\nTop 10 slowest steps:");
timeline.getEvents().stream()
.sorted((e1, e2) -> e2.getDuration().compareTo(e1.getDuration()))
.limit(10)
.forEach(event -> {
System.out.printf(" %5d ms - %s%n",
event.getDuration().toMillis(),
event.getStartupStep().getName());
});
}
}Filter which steps to record to reduce memory usage:
package com.example.monitoring;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.core.metrics.StartupStep;
/**
* Startup monitoring with step filtering.
*/
public class FilteredStartupExample {
public BufferingApplicationStartup createFilteredStartup() {
BufferingApplicationStartup startup = new BufferingApplicationStartup(5000);
// Only record steps from Spring framework
startup.addFilter(step -> step.getName().startsWith("spring."));
// Only record steps that take more than 10ms
startup.addFilter(step -> {
// Note: This is a simplified example
// In reality, you'd need to check duration after step completes
return true;
});
// Only record specific categories
startup.addFilter(step -> {
String name = step.getName();
return name.startsWith("spring.beans.") ||
name.startsWith("spring.context.") ||
name.startsWith("spring.data.");
});
return startup;
}
}Monitor startup performance without draining the buffer:
package com.example.monitoring;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* Analyzes startup performance after application starts.
*/
@Component
public class StartupMonitor implements ApplicationRunner {
private final BufferingApplicationStartup applicationStartup;
public StartupMonitor(BufferingApplicationStartup applicationStartup) {
this.applicationStartup = applicationStartup;
}
@Override
public void run(ApplicationArguments args) {
// Get snapshot without draining
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
System.out.println("\n=== Startup Performance Report ===");
printSummary(timeline);
printSlowSteps(timeline);
printCategoryBreakdown(timeline);
}
private void printSummary(StartupTimeline timeline) {
Duration totalDuration = timeline.getEvents().stream()
.map(StartupTimeline.TimelineEvent::getDuration)
.reduce(Duration.ZERO, Duration::plus);
System.out.printf("Total startup time: %d ms%n", totalDuration.toMillis());
System.out.printf("Number of steps: %d%n", timeline.getEvents().size());
}
private void printSlowSteps(StartupTimeline timeline) {
System.out.println("\nSteps taking > 100ms:");
timeline.getEvents().stream()
.filter(event -> event.getDuration().toMillis() > 100)
.sorted((e1, e2) -> e2.getDuration().compareTo(e1.getDuration()))
.forEach(event -> {
System.out.printf(" %5d ms - %s%n",
event.getDuration().toMillis(),
event.getStartupStep().getName());
});
}
private void printCategoryBreakdown(StartupTimeline timeline) {
System.out.println("\nBreakdown by category:");
// Group by category prefix
var categories = timeline.getEvents().stream()
.collect(java.util.stream.Collectors.groupingBy(
event -> extractCategory(event.getStartupStep().getName()),
java.util.stream.Collectors.summingLong(
event -> event.getDuration().toMillis()
)
));
categories.forEach((category, totalMs) ->
System.out.printf(" %s: %d ms%n", category, totalMs)
);
}
private String extractCategory(String stepName) {
String[] parts = stepName.split("\\.");
return parts.length > 1 ? parts[0] + "." + parts[1] : "other";
}
}Export startup metrics to monitoring systems:
package com.example.monitoring;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* Exports startup metrics to Micrometer for monitoring.
*/
@Component
public class StartupMetricsExporter implements ApplicationRunner {
private final BufferingApplicationStartup applicationStartup;
private final MeterRegistry meterRegistry;
public StartupMetricsExporter(
BufferingApplicationStartup applicationStartup,
MeterRegistry meterRegistry) {
this.applicationStartup = applicationStartup;
this.meterRegistry = meterRegistry;
}
@Override
public void run(ApplicationArguments args) {
StartupTimeline timeline = applicationStartup.drainBufferedTimeline();
// Record overall startup time
Duration totalStartup = calculateTotalStartupTime(timeline);
Timer.builder("application.startup.time")
.description("Total application startup time")
.tag("environment", System.getenv("ENV"))
.register(meterRegistry)
.record(totalStartup);
// Record individual step metrics
timeline.getEvents().forEach(event -> {
String stepName = event.getStartupStep().getName();
Timer.builder("application.startup.step")
.tag("step", stepName)
.tag("category", extractCategory(stepName))
.description("Individual startup step duration")
.register(meterRegistry)
.record(event.getDuration());
});
// Record count of slow steps
long slowSteps = timeline.getEvents().stream()
.filter(event -> event.getDuration().toMillis() > 100)
.count();
meterRegistry.gauge("application.startup.slow.steps", slowSteps);
}
private Duration calculateTotalStartupTime(StartupTimeline timeline) {
if (timeline.getEvents().isEmpty()) {
return Duration.ZERO;
}
StartupTimeline.TimelineEvent lastEvent = timeline.getEvents()
.get(timeline.getEvents().size() - 1);
return Duration.between(timeline.getStartTime(), lastEvent.getEndTime());
}
private String extractCategory(String stepName) {
String[] parts = stepName.split("\\.");
return parts.length > 1 ? parts[1] : "other";
}
}# application.yml
spring:
application:
startup:
enabled: true
capacity: 10000package com.example.config;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration for startup metrics.
*/
@Configuration
public class StartupMetricsConfig {
@Bean
@ConfigurationProperties(prefix = "spring.application.startup")
public StartupMetricsProperties startupMetricsProperties() {
return new StartupMetricsProperties();
}
@Bean
public BufferingApplicationStartup bufferingApplicationStartup(
StartupMetricsProperties properties) {
if (!properties.isEnabled()) {
return null;
}
return new BufferingApplicationStartup(properties.getCapacity());
}
public static class StartupMetricsProperties {
private boolean enabled = false;
private int capacity = 10000;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
}
}package com.example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.Duration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test to detect startup performance regressions.
*/
@SpringBootTest
class StartupPerformanceTest {
@Autowired
private BufferingApplicationStartup applicationStartup;
@Test
void startupTimeShouldBeWithinThreshold() {
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
Duration maxAllowedStartup = Duration.ofSeconds(5);
Duration actualStartup = calculateTotalStartupTime(timeline);
assertThat(actualStartup)
.as("Startup time should be under %s", maxAllowedStartup)
.isLessThan(maxAllowedStartup);
}
@Test
void noStepShouldTakeLongerThan1Second() {
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
Duration maxStepDuration = Duration.ofSeconds(1);
timeline.getEvents().forEach(event -> {
assertThat(event.getDuration())
.as("Step %s took too long", event.getStartupStep().getName())
.isLessThan(maxStepDuration);
});
}
@Test
void beanInstantiationShouldBeFast() {
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
long slowBeans = timeline.getEvents().stream()
.filter(e -> e.getStartupStep().getName().startsWith("spring.beans.instantiate"))
.filter(e -> e.getDuration().toMillis() > 100)
.count();
assertThat(slowBeans)
.as("Too many beans taking > 100ms to instantiate")
.isLessThan(5);
}
private Duration calculateTotalStartupTime(StartupTimeline timeline) {
if (timeline.getEvents().isEmpty()) {
return Duration.ZERO;
}
var lastEvent = timeline.getEvents().get(timeline.getEvents().size() - 1);
return Duration.between(timeline.getStartTime(), lastEvent.getEndTime());
}
}package com.example.diagnostics;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.List;
/**
* Detects and reports startup bottlenecks.
*/
@Component
public class StartupBottleneckDetector {
public void detectBottlenecks(StartupTimeline timeline) {
Duration totalTime = calculateTotalTime(timeline);
Duration threshold = totalTime.dividedBy(10); // 10% of total
List<StartupTimeline.TimelineEvent> bottlenecks = timeline.getEvents().stream()
.filter(event -> event.getDuration().compareTo(threshold) > 0)
.sorted((e1, e2) -> e2.getDuration().compareTo(e1.getDuration()))
.toList();
if (!bottlenecks.isEmpty()) {
System.err.println("=== Startup Bottlenecks Detected ===");
bottlenecks.forEach(event -> {
double percentage = (event.getDuration().toMillis() * 100.0) /
totalTime.toMillis();
System.err.printf("WARNING: %s: %dms (%.1f%%)%n",
event.getStartupStep().getName(),
event.getDuration().toMillis(),
percentage);
});
}
}
private Duration calculateTotalTime(StartupTimeline timeline) {
return timeline.getEvents().stream()
.map(StartupTimeline.TimelineEvent::getDuration)
.reduce(Duration.ZERO, Duration::plus);
}
}Complete startup analysis with categorization and performance reporting:
package com.example.startup;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
/**
* Comprehensive startup performance analysis and reporting.
*/
@Component
public class StartupPerformanceDashboard implements ApplicationRunner {
private final BufferingApplicationStartup applicationStartup;
public StartupPerformanceDashboard(BufferingApplicationStartup applicationStartup) {
this.applicationStartup = applicationStartup;
}
@Override
public void run(ApplicationArguments args) {
StartupTimeline timeline = applicationStartup.drainBufferedTimeline();
System.out.println("\n" + "=".repeat(80));
System.out.println("STARTUP PERFORMANCE ANALYSIS");
System.out.println("=".repeat(80));
printSummary(timeline);
printCategoryBreakdown(timeline);
printSlowestSteps(timeline);
printBottlenecks(timeline);
printRecommendations(timeline);
System.out.println("=".repeat(80) + "\n");
}
private void printSummary(StartupTimeline timeline) {
List<StartupTimeline.TimelineEvent> events = timeline.getEvents();
if (events.isEmpty()) {
System.out.println("No startup events recorded");
return;
}
Duration totalDuration = calculateTotalDuration(timeline);
long totalSteps = events.size();
Duration avgStepDuration = totalDuration.dividedBy(Math.max(1, totalSteps));
System.out.println("\nSUMMARY:");
System.out.printf(" Total Startup Time: %,d ms%n", totalDuration.toMillis());
System.out.printf(" Total Steps: %,d%n", totalSteps);
System.out.printf(" Average Step Duration: %,d ms%n", avgStepDuration.toMillis());
System.out.printf(" Started At: %s%n", timeline.getStartTime());
}
private void printCategoryBreakdown(StartupTimeline timeline) {
System.out.println("\nBREAKDOWN BY CATEGORY:");
Map<String, CategoryStats> categoryStats = timeline.getEvents().stream()
.collect(Collectors.groupingBy(
event -> extractCategory(event.getStartupStep().getName()),
Collectors.collectingAndThen(
Collectors.toList(),
this::calculateCategoryStats
)
));
// Sort by total duration descending
categoryStats.entrySet().stream()
.sorted((e1, e2) -> Long.compare(
e2.getValue().totalDuration.toMillis(),
e1.getValue().totalDuration.toMillis()
))
.forEach(entry -> {
CategoryStats stats = entry.getValue();
System.out.printf(" %-30s %,8d ms (%3d steps, avg: %,5d ms)%n",
entry.getKey() + ":",
stats.totalDuration.toMillis(),
stats.count,
stats.avgDuration.toMillis());
});
}
private void printSlowestSteps(StartupTimeline timeline) {
System.out.println("\nSLOWEST STEPS (Top 10):");
timeline.getEvents().stream()
.sorted((e1, e2) -> e2.getDuration().compareTo(e1.getDuration()))
.limit(10)
.forEach(event -> {
Duration duration = event.getDuration();
String name = event.getStartupStep().getName();
System.out.printf(" %,6d ms %s%n", duration.toMillis(), name);
});
}
private void printBottlenecks(StartupTimeline timeline) {
Duration totalDuration = calculateTotalDuration(timeline);
Duration threshold = totalDuration.dividedBy(20); // 5% of total
List<StartupTimeline.TimelineEvent> bottlenecks = timeline.getEvents().stream()
.filter(event -> event.getDuration().compareTo(threshold) > 0)
.sorted((e1, e2) -> e2.getDuration().compareTo(e1.getDuration()))
.toList();
if (bottlenecks.isEmpty()) {
return;
}
System.out.println("\nBOTTLENECKS (>5% of total time):");
bottlenecks.forEach(event -> {
double percentage = (event.getDuration().toMillis() * 100.0) /
totalDuration.toMillis();
System.out.printf(" %,6d ms (%5.1f%%) %s%n",
event.getDuration().toMillis(),
percentage,
event.getStartupStep().getName());
});
}
private void printRecommendations(StartupTimeline timeline) {
List<String> recommendations = new ArrayList<>();
Duration totalDuration = calculateTotalDuration(timeline);
if (totalDuration.toMillis() > 10000) {
recommendations.add("Startup time exceeds 10 seconds - consider optimization");
}
// Check for slow bean initialization
long slowBeans = timeline.getEvents().stream()
.filter(e -> e.getStartupStep().getName().contains("bean"))
.filter(e -> e.getDuration().toMillis() > 500)
.count();
if (slowBeans > 5) {
recommendations.add(slowBeans + " beans take >500ms to initialize - review bean creation logic");
}
if (!recommendations.isEmpty()) {
System.out.println("\nRECOMMENDATIONS:");
recommendations.forEach(rec -> System.out.println(" - " + rec));
}
}
private Duration calculateTotalDuration(StartupTimeline timeline) {
List<StartupTimeline.TimelineEvent> events = timeline.getEvents();
if (events.isEmpty()) {
return Duration.ZERO;
}
StartupTimeline.TimelineEvent lastEvent = events.get(events.size() - 1);
return Duration.between(timeline.getStartTime(), lastEvent.getEndTime());
}
private String extractCategory(String stepName) {
if (stepName.startsWith("spring.beans.")) return "Bean Initialization";
if (stepName.startsWith("spring.data.")) return "Data Access";
if (stepName.startsWith("spring.jpa.")) return "JPA/Hibernate";
if (stepName.startsWith("spring.context.")) return "Context Setup";
if (stepName.startsWith("spring.boot.")) return "Boot Framework";
if (stepName.startsWith("spring.security.")) return "Security";
String[] parts = stepName.split("\\.");
return parts.length > 1 ? parts[0] + "." + parts[1] : "Other";
}
private CategoryStats calculateCategoryStats(List<StartupTimeline.TimelineEvent> events) {
Duration total = events.stream()
.map(StartupTimeline.TimelineEvent::getDuration)
.reduce(Duration.ZERO, Duration::plus);
Duration avg = total.dividedBy(Math.max(1, events.size()));
return new CategoryStats(total, avg, events.size());
}
record CategoryStats(Duration totalDuration, Duration avgDuration, int count) {}
}Automated tests to detect startup performance regressions:
package com.example.startup;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.Duration;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests to prevent startup performance regressions.
*/
@SpringBootTest
class StartupPerformanceRegressionTest {
@Autowired
private BufferingApplicationStartup applicationStartup;
// Define performance SLAs
private static final Duration MAX_TOTAL_STARTUP = Duration.ofSeconds(10);
private static final Duration MAX_STEP_DURATION = Duration.ofSeconds(2);
private static final int MAX_SLOW_STEPS = 5;
private static final long SLOW_STEP_THRESHOLD_MS = 500;
@Test
void startupTimeShouldBeWithinSLA() {
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
Duration actualStartup = calculateTotalStartupTime(timeline);
assertThat(actualStartup)
.as("Total startup time should be under %s", MAX_TOTAL_STARTUP)
.isLessThan(MAX_TOTAL_STARTUP);
}
@Test
void noSingleStepShouldTakeTooLong() {
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
List<StartupTimeline.TimelineEvent> slowSteps = timeline.getEvents().stream()
.filter(e -> e.getDuration().compareTo(MAX_STEP_DURATION) > 0)
.toList();
assertThat(slowSteps)
.as("No step should take longer than %s", MAX_STEP_DURATION)
.isEmpty();
}
@Test
void limitNumberOfSlowSteps() {
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
long slowStepCount = timeline.getEvents().stream()
.filter(e -> e.getDuration().toMillis() > SLOW_STEP_THRESHOLD_MS)
.count();
assertThat(slowStepCount)
.as("Too many steps take >%dms", SLOW_STEP_THRESHOLD_MS)
.isLessThan(MAX_SLOW_STEPS);
}
@Test
void beanInstantiationShouldBeFast() {
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
Duration beanInstantiationTime = timeline.getEvents().stream()
.filter(e -> e.getStartupStep().getName().contains("spring.beans.instantiate"))
.map(StartupTimeline.TimelineEvent::getDuration)
.reduce(Duration.ZERO, Duration::plus);
assertThat(beanInstantiationTime.toMillis())
.as("Bean instantiation should complete quickly")
.isLessThan(5000);
}
@Test
void databaseInitializationShouldBeReasonable() {
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
Duration dbInitTime = timeline.getEvents().stream()
.filter(e -> e.getStartupStep().getName().contains("jpa") ||
e.getStartupStep().getName().contains("datasource"))
.map(StartupTimeline.TimelineEvent::getDuration)
.reduce(Duration.ZERO, Duration::plus);
if (dbInitTime.toMillis() > 0) {
assertThat(dbInitTime.toMillis())
.as("Database initialization time")
.isLessThan(3000);
}
}
private Duration calculateTotalStartupTime(StartupTimeline timeline) {
List<StartupTimeline.TimelineEvent> events = timeline.getEvents();
if (events.isEmpty()) {
return Duration.ZERO;
}
StartupTimeline.TimelineEvent lastEvent = events.get(events.size() - 1);
return Duration.between(timeline.getStartTime(), lastEvent.getEndTime());
}
}Export startup metrics to monitoring systems for trend analysis:
package com.example.monitoring;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Timer;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Exports startup metrics to Micrometer for monitoring and alerting.
*/
@Component
public class StartupMetricsExporter implements ApplicationRunner {
private final BufferingApplicationStartup applicationStartup;
private final MeterRegistry meterRegistry;
public StartupMetricsExporter(BufferingApplicationStartup applicationStartup,
MeterRegistry meterRegistry) {
this.applicationStartup = applicationStartup;
this.meterRegistry = meterRegistry;
}
@Override
public void run(ApplicationArguments args) {
StartupTimeline timeline = applicationStartup.drainBufferedTimeline();
exportOverallMetrics(timeline);
exportCategoryMetrics(timeline);
exportPercentileMetrics(timeline);
exportBottleneckMetrics(timeline);
}
private void exportOverallMetrics(StartupTimeline timeline) {
Duration totalStartup = calculateTotalStartupTime(timeline);
Timer.builder("application.startup.time")
.description("Total application startup time")
.tag("environment", System.getenv().getOrDefault("ENV", "unknown"))
.tag("version", System.getenv().getOrDefault("APP_VERSION", "unknown"))
.register(meterRegistry)
.record(totalStartup);
meterRegistry.gauge("application.startup.steps.count",
timeline.getEvents().size());
}
private void exportCategoryMetrics(StartupTimeline timeline) {
Map<String, Duration> categoryDurations = timeline.getEvents().stream()
.collect(Collectors.groupingBy(
event -> extractCategory(event.getStartupStep().getName()),
Collectors.mapping(
StartupTimeline.TimelineEvent::getDuration,
Collectors.reducing(Duration.ZERO, Duration::plus)
)
));
categoryDurations.forEach((category, duration) -> {
Timer.builder("application.startup.category")
.description("Startup time by category")
.tag("category", category)
.register(meterRegistry)
.record(duration);
});
}
private void exportPercentileMetrics(StartupTimeline timeline) {
List<Long> durations = timeline.getEvents().stream()
.map(e -> e.getDuration().toMillis())
.sorted()
.toList();
if (!durations.isEmpty()) {
long p50 = getPercentile(durations, 0.50);
long p90 = getPercentile(durations, 0.90);
long p95 = getPercentile(durations, 0.95);
long p99 = getPercentile(durations, 0.99);
meterRegistry.gauge("application.startup.step.duration.p50", p50);
meterRegistry.gauge("application.startup.step.duration.p90", p90);
meterRegistry.gauge("application.startup.step.duration.p95", p95);
meterRegistry.gauge("application.startup.step.duration.p99", p99);
}
}
private void exportBottleneckMetrics(StartupTimeline timeline) {
Duration totalDuration = calculateTotalStartupTime(timeline);
Duration threshold = totalDuration.dividedBy(20); // 5% threshold
long bottleneckCount = timeline.getEvents().stream()
.filter(e -> e.getDuration().compareTo(threshold) > 0)
.count();
meterRegistry.gauge("application.startup.bottlenecks.count", bottleneckCount);
}
private Duration calculateTotalStartupTime(StartupTimeline timeline) {
List<StartupTimeline.TimelineEvent> events = timeline.getEvents();
if (events.isEmpty()) {
return Duration.ZERO;
}
StartupTimeline.TimelineEvent lastEvent = events.get(events.size() - 1);
return Duration.between(timeline.getStartTime(), lastEvent.getEndTime());
}
private String extractCategory(String stepName) {
String[] parts = stepName.split("\\.");
return parts.length > 1 ? parts[0] + "." + parts[1] : "other";
}
private long getPercentile(List<Long> sortedValues, double percentile) {
int index = (int) Math.ceil(percentile * sortedValues.size()) - 1;
return sortedValues.get(Math.max(0, index));
}
}Compare startup performance across versions or configurations:
package com.example.comparison;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import java.io.*;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
/**
* Tool for comparing startup performance between different runs.
*/
public class StartupComparisonTool {
/**
* Save startup timeline to file for later comparison.
*
* @param timeline the timeline to save
* @param outputFile output file path
* @throws IOException if save fails
*/
public void saveTimeline(StartupTimeline timeline, String outputFile) throws IOException {
try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) {
writer.println("# Startup Timeline Export");
writer.println("# Started at: " + timeline.getStartTime());
writer.println("# Step Name,Duration (ms),Start Time,End Time");
for (StartupTimeline.TimelineEvent event : timeline.getEvents()) {
writer.printf("%s,%d,%s,%s%n",
event.getStartupStep().getName(),
event.getDuration().toMillis(),
event.getStartTime(),
event.getEndTime());
}
}
}
/**
* Compare two startup timelines and generate report.
*
* @param baseline baseline timeline
* @param current current timeline
* @return comparison report
*/
public ComparisonReport compare(StartupTimeline baseline, StartupTimeline current) {
Duration baselineDuration = calculateTotal(baseline);
Duration currentDuration = calculateTotal(current);
Duration difference = currentDuration.minus(baselineDuration);
double percentChange = (difference.toMillis() * 100.0) / baselineDuration.toMillis();
Map<String, StepComparison> stepComparisons = compareSteps(baseline, current);
return new ComparisonReport(
baselineDuration,
currentDuration,
difference,
percentChange,
stepComparisons
);
}
/**
* Print comparison report to console.
*
* @param report the comparison report
*/
public void printReport(ComparisonReport report) {
System.out.println("\n" + "=".repeat(80));
System.out.println("STARTUP PERFORMANCE COMPARISON");
System.out.println("=".repeat(80));
System.out.println("\nOVERALL:");
System.out.printf(" Baseline: %,8d ms%n", report.baselineDuration.toMillis());
System.out.printf(" Current: %,8d ms%n", report.currentDuration.toMillis());
System.out.printf(" Difference: %,+8d ms (%+.1f%%)%n",
report.difference.toMillis(),
report.percentChange);
if (Math.abs(report.percentChange) > 10) {
System.out.println(" WARNING: Significant performance change detected!");
}
// Show steps with biggest changes
System.out.println("\nBIGGEST CHANGES:");
report.stepComparisons.values().stream()
.filter(sc -> Math.abs(sc.percentChange) > 20)
.sorted((s1, s2) -> Long.compare(
Math.abs(s2.difference.toMillis()),
Math.abs(s1.difference.toMillis())
))
.limit(10)
.forEach(sc -> {
System.out.printf(" %+,6d ms (%+6.1f%%) %s%n",
sc.difference.toMillis(),
sc.percentChange,
sc.stepName);
});
// Show new steps
List<String> newSteps = report.stepComparisons.values().stream()
.filter(sc -> sc.baselineDuration.equals(Duration.ZERO))
.map(sc -> sc.stepName)
.toList();
if (!newSteps.isEmpty()) {
System.out.println("\nNEW STEPS:");
newSteps.forEach(step -> System.out.println(" + " + step));
}
System.out.println("=".repeat(80) + "\n");
}
private Duration calculateTotal(StartupTimeline timeline) {
List<StartupTimeline.TimelineEvent> events = timeline.getEvents();
if (events.isEmpty()) {
return Duration.ZERO;
}
StartupTimeline.TimelineEvent lastEvent = events.get(events.size() - 1);
return Duration.between(timeline.getStartTime(), lastEvent.getEndTime());
}
private Map<String, StepComparison> compareSteps(StartupTimeline baseline,
StartupTimeline current) {
Map<String, Duration> baselineSteps = baseline.getEvents().stream()
.collect(Collectors.toMap(
e -> e.getStartupStep().getName(),
StartupTimeline.TimelineEvent::getDuration,
Duration::plus
));
Map<String, Duration> currentSteps = current.getEvents().stream()
.collect(Collectors.toMap(
e -> e.getStartupStep().getName(),
StartupTimeline.TimelineEvent::getDuration,
Duration::plus
));
Set<String> allSteps = new HashSet<>();
allSteps.addAll(baselineSteps.keySet());
allSteps.addAll(currentSteps.keySet());
return allSteps.stream()
.collect(Collectors.toMap(
stepName -> stepName,
stepName -> {
Duration baselineDur = baselineSteps.getOrDefault(stepName, Duration.ZERO);
Duration currentDur = currentSteps.getOrDefault(stepName, Duration.ZERO);
Duration diff = currentDur.minus(baselineDur);
double percentChange = baselineDur.toMillis() > 0
? (diff.toMillis() * 100.0) / baselineDur.toMillis()
: 100.0;
return new StepComparison(stepName, baselineDur, currentDur, diff, percentChange);
}
));
}
record ComparisonReport(
Duration baselineDuration,
Duration currentDuration,
Duration difference,
double percentChange,
Map<String, StepComparison> stepComparisons
) {}
record StepComparison(
String stepName,
Duration baselineDuration,
Duration currentDuration,
Duration difference,
double percentChange
) {}
}Dynamically adjust buffer size based on application complexity:
package com.example.config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* Configures startup metrics with adaptive buffer sizing.
*/
public class AdaptiveStartupMetricsConfig implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
if (isStartupMetricsEnabled(environment)) {
int bufferSize = calculateOptimalBufferSize(environment);
BufferingApplicationStartup startup = new BufferingApplicationStartup(bufferSize);
// Add filters to reduce noise
startup.addFilter(step -> !step.getName().contains("internal"));
// Filter fast steps in production
if (isProduction(environment)) {
startup.addFilter(step -> {
// Only record steps that might be interesting
String name = step.getName();
return name.contains("bean") ||
name.contains("data") ||
name.contains("jpa") ||
name.contains("context");
});
}
application.setApplicationStartup(startup);
}
}
private boolean isStartupMetricsEnabled(ConfigurableEnvironment environment) {
return environment.getProperty("management.metrics.startup.enabled",
Boolean.class, false);
}
private boolean isProduction(ConfigurableEnvironment environment) {
String[] profiles = environment.getActiveProfiles();
for (String profile : profiles) {
if (profile.equals("prod") || profile.equals("production")) {
return true;
}
}
return false;
}
private int calculateOptimalBufferSize(ConfigurableEnvironment environment) {
// Base size
int baseSize = 5000;
// Increase for data-heavy applications
if (environment.containsProperty("spring.datasource.url")) {
baseSize += 2000;
}
// Increase for JPA applications
if (environment.containsProperty("spring.jpa.properties")) {
baseSize += 2000;
}
// Increase for microservices with many beans
if (environment.containsProperty("spring.cloud")) {
baseSize += 3000;
}
return Math.min(baseSize, 20000); // Cap at 20k
}
}drainBufferedTimeline() to free memory after analysisProblem: Buffer fills up and new steps are silently dropped
Error: Missing steps in timeline, incomplete performance data
Solution:
// Calculate appropriate buffer size
int beanCount = context.getBeanDefinitionCount();
int estimatedSteps = beanCount * 5; // Rough estimate
int bufferSize = Math.max(5000, estimatedSteps);
BufferingApplicationStartup startup = new BufferingApplicationStartup(bufferSize);
// Monitor buffer usage
StartupTimeline timeline = startup.getBufferedTimeline();
System.out.printf("Recorded %d steps (buffer capacity: %d)%n",
timeline.getEvents().size(), bufferSize);Rationale: Each bean initialization creates multiple steps. Applications with many beans need larger buffers. Too small buffer causes data loss, too large wastes memory.
Problem: Keeping BufferingApplicationStartup reference without draining
Error: Memory leak, increased heap usage over application lifetime
Solution:
@Component
public class StartupAnalyzer implements ApplicationRunner {
private final BufferingApplicationStartup applicationStartup;
@Override
public void run(ApplicationArguments args) {
// Drain buffer to free memory
StartupTimeline timeline = applicationStartup.drainBufferedTimeline();
analyzeTimeline(timeline);
// timeline is now available for garbage collection
// No lingering references to startup steps
}
}Rationale: getBufferedTimeline() returns snapshot without clearing buffer. drainBufferedTimeline() clears buffer, allowing garbage collection of step data. Always drain after startup analysis completes.
Problem: Startup metrics enabled in production builds
Error: Increased memory usage, slightly slower startup
Solution:
# Only enable in specific profiles
spring.profiles.active=dev
# In application-dev.properties
management.metrics.startup.enabled=true
# In application-prod.properties (explicitly disabled)
management.metrics.startup.enabled=false@Configuration
@Profile("dev")
public class StartupMetricsConfig {
@Bean
public BufferingApplicationStartup bufferingApplicationStartup() {
return new BufferingApplicationStartup(10000);
}
}Rationale: Startup metrics add overhead and consume memory. Only needed during development and performance troubleshooting. Production applications should disable unless actively investigating performance issues.
Problem: Summing all step durations instead of using timeline span
Error: Reported startup time much higher than actual
Solution:
// WRONG: Summing step durations (counts overlapping time multiple times)
Duration wrongTotal = timeline.getEvents().stream()
.map(StartupTimeline.TimelineEvent::getDuration)
.reduce(Duration.ZERO, Duration::plus);
// CORRECT: Use timeline start and end
Duration correctTotal = Duration.between(
timeline.getStartTime(),
timeline.getEvents().get(timeline.getEvents().size() - 1).getEndTime()
);Rationale: Steps can overlap or run in parallel. Summing individual durations counts concurrent work multiple times. Use timeline span for accurate total time.
Problem: Accessing timeline without checking if events exist
Error:
IndexOutOfBoundsException: Index 0 out of bounds for length 0Solution:
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
if (timeline.getEvents().isEmpty()) {
System.out.println("No startup steps recorded");
return;
}
// Now safe to access events
StartupTimeline.TimelineEvent lastEvent =
timeline.getEvents().get(timeline.getEvents().size() - 1);Rationale: Buffer may be empty if capacity is 0, if recording hasn't started, or if all steps were filtered out. Always check before accessing events.
Problem: Complex logic in step filters
Error: Filter overhead slows down startup significantly
Solution:
// WRONG: Expensive operation in filter
startup.addFilter(step -> {
String name = step.getName();
// Database lookup or complex regex
return someService.shouldRecord(name); // Slow!
});
// CORRECT: Simple, fast filter logic
startup.addFilter(step -> {
String name = step.getName();
// Simple string operations only
return name.startsWith("spring.") && !name.contains("internal");
});Rationale: Filters are evaluated for every step during startup. Expensive filters add significant overhead. Use only simple, fast operations in filters.
Problem: Startup analysis code throws exception
Error: Application fails to start due to analysis error
Solution:
@Override
public void run(ApplicationArguments args) {
try {
StartupTimeline timeline = applicationStartup.drainBufferedTimeline();
analyzeAndReport(timeline);
} catch (Exception e) {
// Log error but don't fail application startup
log.error("Startup analysis failed", e);
}
}Rationale: Analysis code should never prevent application from starting. Wrap in try-catch to ensure errors in analysis don't affect application availability.
Problem: Comparing event times without considering time zones
Error: Incorrect duration calculations
Solution:
// Use Duration.between for time calculations
Duration duration = Duration.between(
event.getStartTime(),
event.getEndTime()
);
// Or use provided getDuration()
Duration duration = event.getDuration();
// Don't try to compare Instants with < or >
// if (event.getStartTime() < event.getEndTime()) // Won't compileRationale: Instant represents UTC time. Use Duration.between() for proper time interval calculations. TimelineEvent provides getDuration() for convenience.
Problem: Creating BufferingApplicationStartup but not setting it on SpringApplication
Error: No steps recorded, timeline is empty
Solution:
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
// IMPORTANT: Set the ApplicationStartup
BufferingApplicationStartup startup = new BufferingApplicationStartup(10000);
app.setApplicationStartup(startup);
app.run(args);
}
// Or in SpringApplicationBuilder
new SpringApplicationBuilder(Application.class)
.applicationStartup(new BufferingApplicationStartup(10000))
.run(args);Rationale: BufferingApplicationStartup must be explicitly set on SpringApplication before run() is called. Simply creating the object doesn't enable recording.
Problem: Accessing timeline from multiple threads simultaneously
Error: ConcurrentModificationException or data corruption
Solution:
// getBufferedTimeline() returns immutable snapshot - thread-safe
StartupTimeline timeline = applicationStartup.getBufferedTimeline();
// Can safely share across threads
CompletableFuture.runAsync(() -> analyzeTimeline(timeline));
CompletableFuture.runAsync(() -> exportMetrics(timeline));
// drainBufferedTimeline() clears buffer - not thread-safe
// Only call from single thread after startup
synchronized (applicationStartup) {
StartupTimeline timeline = applicationStartup.drainBufferedTimeline();
processTimeline(timeline);
}Rationale: getBufferedTimeline() returns immutable snapshot safe for concurrent access. drainBufferedTimeline() modifies buffer and should only be called once from a single thread.
BufferingApplicationStartup:
getBufferedTimeline() provides lock-free snapshotdrainBufferedTimeline() is synchronizedStartupTimeline:
Memory Usage:
Performance Impact:
| Step Name | Description |
|---|---|
spring.context.refresh | Overall context refresh |
spring.beans.instantiate | Bean instantiation |
spring.beans.post-process | Bean post-processing |
spring.data.repository.scanning | Repository scanning |
spring.jpa.hibernate.init | Hibernate initialization |
spring.boot.application.starting | Application starting event |
spring.boot.application.ready | Application ready event |
// Startup metrics
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
import org.springframework.boot.context.metrics.buffering.StartupTimeline.TimelineEvent;
// Core metrics
import org.springframework.core.metrics.ApplicationStartup;
import org.springframework.core.metrics.StartupStep;
// Spring Boot
import org.springframework.boot.SpringApplication;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.ApplicationArguments;
// Java time
import java.time.Instant;
import java.time.Duration;
// Collections
import java.util.List;
import java.util.function.Predicate;