startup timint

This commit is contained in:
2026-06-02 09:47:23 +02:00
parent c0ce81f125
commit 96674a3bf5
2 changed files with 57 additions and 0 deletions

View File

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

View File

@@ -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.
*
* <p>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");
}
}