or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

admin-jmx.mdansi-support.mdaot-native-image.mdapplication-info.mdavailability.mdbootstrap.mdbootstrapping.mdbuilder.mdcloud-platform.mdconfiguration-annotations.mdconfiguration-data.mdconfiguration-properties.mdconversion.mddiagnostics.mdenvironment-property-sources.mdindex.mdjson-support.mdlifecycle-events.mdlogging.mdorigin-tracking.mdresource-loading.mdretry-support.mdssl-tls.mdstartup-metrics.mdsupport.mdsystem-utilities.mdtask-execution.mdthreading.mdutilities.mdvalidation.mdweb-support.md
tile.json

startup-metrics.mddocs/

Startup Metrics and Timeline

Quick Reference

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.

Overview

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.

Core Components

BufferingApplicationStartup

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
    }
}

StartupTimeline

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
        }
    }
}

Usage Examples

Basic Startup Monitoring

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());
            });
    }
}

Filtering Startup Steps

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;
    }
}

Continuous Monitoring

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";
    }
}

Integration with Micrometer

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";
    }
}

Configuration via Properties

# application.yml
spring:
  application:
    startup:
      enabled: true
      capacity: 10000
package 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;
        }
    }
}

Production Use Cases

Startup Time Regression Testing

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());
    }
}

Bottleneck Detection

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);
    }
}

Common Patterns

Pattern 1: Startup Performance Dashboard

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) {}
}

Pattern 2: Startup Regression Testing

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());
    }
}

Pattern 3: Continuous Performance Monitoring

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));
    }
}

Pattern 4: Startup Comparison Tool

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
    ) {}
}

Pattern 5: Adaptive Buffer Sizing

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
    }
}

Best Practices

  1. Enable Selectively: Only enable in development and during performance troubleshooting
  2. Right-Size Buffer: Use 5,000-10,000 capacity for typical applications
  3. Filter Noise: Add filters to exclude internal framework steps
  4. Drain After Startup: Call drainBufferedTimeline() to free memory after analysis
  5. Monitor Trends: Export metrics to monitoring systems to track performance over time
  6. Set SLAs: Define acceptable startup time thresholds and alert on violations
  7. Compare Versions: Save timelines to compare performance between releases
  8. Focus on Outliers: Investigate steps taking >5% of total startup time
  9. Test Regression: Include startup performance tests in CI/CD pipeline
  10. Document Baseline: Record baseline performance metrics for future comparison

Common Pitfalls

1. Buffer Overflow

Problem: 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.

2. Memory Leak from Not Draining

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.

3. Leaving Enabled in Production

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.

4. Incorrect Total Time Calculation

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.

5. No Null Checks for Empty Timeline

Problem: Accessing timeline without checking if events exist

Error:

IndexOutOfBoundsException: Index 0 out of bounds for length 0

Solution:

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.

6. Expensive Operations in Filters

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.

7. Not Using Try-Catch for Analysis

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.

8. Comparing Instant Values Directly

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 compile

Rationale: Instant represents UTC time. Use Duration.between() for proper time interval calculations. TimelineEvent provides getDuration() for convenience.

9. Missing ApplicationStartup Configuration

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.

10. Thread Safety Issues with Shared Timeline

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.

Thread Safety

BufferingApplicationStartup:

  • Thread-safe: Multiple threads can record steps concurrently
  • Lock-free reads: getBufferedTimeline() provides lock-free snapshot
  • Synchronized drain: drainBufferedTimeline() is synchronized

StartupTimeline:

  • Immutable: Thread-safe after creation
  • Concurrent access: Safe to read from multiple threads

Performance Considerations

Memory Usage:

  • Each step consumes approximately 200-500 bytes
  • Buffer size of 10,000 = ~2-5 MB memory
  • Steps dropped silently when buffer is full

Performance Impact:

  • Minimal overhead: ~1-5ms per 1000 steps
  • Nanosecond-precision timing has negligible cost
  • Filtering reduces memory and CPU usage

Common Startup Steps

Step NameDescription
spring.context.refreshOverall context refresh
spring.beans.instantiateBean instantiation
spring.beans.post-processBean post-processing
spring.data.repository.scanningRepository scanning
spring.jpa.hibernate.initHibernate initialization
spring.boot.application.startingApplication starting event
spring.boot.application.readyApplication ready event

Import Statements

// 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;