diff --git a/src/main/java/com/gregor/jprototerm/Main.java b/src/main/java/com/gregor/jprototerm/Main.java index 569c8df..6330ce0 100644 --- a/src/main/java/com/gregor/jprototerm/Main.java +++ b/src/main/java/com/gregor/jprototerm/Main.java @@ -31,10 +31,17 @@ public final class Main extends Application { @Override public void start(Stage stage) { + // First mark: time from JVM start through JavaFX toolkit + GL pipeline init (start() is the + // first app code the toolkit runs). Usually the dominant slice of cold startup. + StartupTiming.mark("toolkit ready (start)"); config = AppConfig.load(); + StartupTiming.mark("config loaded"); metrics = new TerminalMetrics(config.fontFamily(), config.fontSize()); + StartupTiming.mark("fonts loaded"); compositor = new Compositor(config, metrics); + // Includes the first Ghostty.open (native dlopen) and the first pty spawn. + StartupTiming.mark("compositor ready"); // When the last pane closes — whether via the close-pane key or because a pane's process // exited on its own — tear down and quit. compositor.setOnEmpty(() -> { @@ -60,6 +67,7 @@ public final class Main extends Application { @Override public void handle(long now) { compositor.render(); + StartupTiming.firstFrame(); } }.start(); @@ -72,6 +80,7 @@ public final class Main extends Application { // to honour, so place it on the screen under the mouse pointer instead. centreOnActiveScreen(stage, config.windowWidth(), config.windowHeight()); stage.show(); + StartupTiming.mark("stage shown"); // Ask the window manager to raise and focus the new window so the user can type right // away; the canvas requestFocus() below only routes events within the scene. stage.toFront(); diff --git a/src/main/java/com/gregor/jprototerm/StartupTiming.java b/src/main/java/com/gregor/jprototerm/StartupTiming.java new file mode 100644 index 0000000..d5fe286 --- /dev/null +++ b/src/main/java/com/gregor/jprototerm/StartupTiming.java @@ -0,0 +1,48 @@ +package com.gregor.jprototerm; + +import java.lang.management.ManagementFactory; + +/** + * Opt-in startup phase timing, enabled with {@code -Djprototerm.timing=true} (e.g. via + * {@code JAVA_TOOL_OPTIONS}); otherwise every method is a cheap no-op and prints nothing. + * + *

Each {@link #mark(String)} prints one line to stderr with the time since the previous mark and + * the total since JVM start, so a cold launch breaks down into its phases — toolkit/GL init vs + * config load vs font loading vs first frame. The anchor is the JVM's own start time (the closest + * proxy we have to "process start"), so the first mark includes JVM bootstrap and JavaFX toolkit + * init, which is usually the dominant cost. + */ +final class StartupTiming { + private static final boolean ENABLED = Boolean.getBoolean("jprototerm.timing"); + // Epoch millis; getStartTime() is the JVM's start, the earliest timestamp we can anchor to. + private static final long JVM_START_MILLIS = ManagementFactory.getRuntimeMXBean().getStartTime(); + private static long lastMillis = -1; + private static boolean firstFrameSeen; + + private StartupTiming() { + } + + /** Records a phase boundary, printing the delta since the previous mark and since JVM start. */ + static void mark(String phase) { + if (!ENABLED) { + return; + } + long now = System.currentTimeMillis(); + long sinceStart = now - JVM_START_MILLIS; + long sinceLast = lastMillis < 0 ? sinceStart : now - lastMillis; + lastMillis = now; + System.err.printf("[timing] %-22s +%5d ms (%5d ms since JVM start)%n", phase, sinceLast, sinceStart); + } + + /** + * Records the first rendered frame exactly once, then becomes a no-op. Safe and cheap to call + * from the render loop every frame (it only ever touches FX-thread state). + */ + static void firstFrame() { + if (!ENABLED || firstFrameSeen) { + return; + } + firstFrameSeen = true; + mark("first frame"); + } +}