diff --git a/src/main/java/com/gregor/jprototerm/RenderProfiler.java b/src/main/java/com/gregor/jprototerm/RenderProfiler.java new file mode 100644 index 0000000..28e5f64 --- /dev/null +++ b/src/main/java/com/gregor/jprototerm/RenderProfiler.java @@ -0,0 +1,76 @@ +package com.gregor.jprototerm; + +/** + * Lightweight render profiler, disabled unless {@code -Djprototerm.profile=true} (or the + * {@code JPROTOTERM_PROFILE=1} environment variable) is set. It accumulates wall-clock nanos + * into a handful of buckets and prints aggregate per-frame stats to stderr every + * {@code jprototerm.profile.frames} render invocations (default 120). + * + *
All render work runs on the JavaFX application thread, so the accumulators are plain + * fields with no synchronization. + * + *
Caveat: JavaFX canvas drawing is deferred to the QuantumRenderer thread, so the
+ * {@link #DRAW} bucket measures only the cost of recording draw commands, not the
+ * GPU paint. Pair this with {@code -Djavafx.pulseLogger=true} to see the render-thread side.
+ */
+final class RenderProfiler {
+ static final int SNAPSHOT = 0;
+ static final int FINGERPRINT = 1;
+ static final int DRAW = 2;
+ static final int FRAME = 3;
+ private static final int BUCKETS = 4;
+ private static final String[] NAMES = {"snapshot", "fingerprint", "draw", "frame-total"};
+
+ private static final boolean ENABLED =
+ Boolean.getBoolean("jprototerm.profile") || "1".equals(System.getenv("JPROTOTERM_PROFILE"));
+ private static final int DUMP_FRAMES = Integer.getInteger("jprototerm.profile.frames", 120);
+
+ private static final long[] totalNanos = new long[BUCKETS];
+ private static final long[] counts = new long[BUCKETS];
+ private static int frames;
+
+ private RenderProfiler() {
+ }
+
+ static boolean enabled() {
+ return ENABLED;
+ }
+
+ /** Returns a start timestamp, or 0 when profiling is disabled. */
+ static long start() {
+ return ENABLED ? System.nanoTime() : 0L;
+ }
+
+ /** Records the time elapsed since {@code startNanos} into {@code bucket}. */
+ static void stop(int bucket, long startNanos) {
+ if (!ENABLED) {
+ return;
+ }
+ totalNanos[bucket] += System.nanoTime() - startNanos;
+ counts[bucket]++;
+ }
+
+ /** Marks the end of one render invocation; dumps and resets every {@code DUMP_FRAMES}. */
+ static void frame() {
+ if (!ENABLED) {
+ return;
+ }
+ if (++frames < DUMP_FRAMES) {
+ return;
+ }
+ dump();
+ }
+
+ private static void dump() {
+ StringBuilder sb = new StringBuilder(192);
+ sb.append("[render-profile] ").append(frames).append(" renders");
+ for (int i = 0; i < BUCKETS; i++) {
+ double totalMs = totalNanos[i] / 1_000_000.0;
+ sb.append(String.format(" | %s %.3fms/f (n=%d)", NAMES[i], totalMs / frames, counts[i]));
+ totalNanos[i] = 0;
+ counts[i] = 0;
+ }
+ System.err.println(sb);
+ frames = 0;
+ }
+}
diff --git a/src/main/java/com/gregor/jprototerm/TerminalPaneNode.java b/src/main/java/com/gregor/jprototerm/TerminalPaneNode.java
index cfbc1ed..cb39616 100644
--- a/src/main/java/com/gregor/jprototerm/TerminalPaneNode.java
+++ b/src/main/java/com/gregor/jprototerm/TerminalPaneNode.java
@@ -91,7 +91,9 @@ final class TerminalPaneNode extends Region {
void renderFull(boolean active) {
prepareGeometry();
+ long snapshotStart = RenderProfiler.start();
RenderStateSnapshot snapshot = pane.snapshotFull();
+ RenderProfiler.stop(RenderProfiler.SNAPSHOT, snapshotStart);
long renderedVersion = pane.snapshotVersion();
boolean withKitty = pane.kittyEnabled() && hasKittyGraphics();
updateRowsFull(snapshot);
@@ -102,6 +104,13 @@ final class TerminalPaneNode extends Region {
}
void renderIncremental(boolean active) {
+ long frameStart = RenderProfiler.start();
+ renderIncrementalBody(active);
+ RenderProfiler.stop(RenderProfiler.FRAME, frameStart);
+ RenderProfiler.frame();
+ }
+
+ private void renderIncrementalBody(boolean active) {
boolean geometryChanged = prepareGeometry();
boolean withKitty = pane.kittyEnabled() && hasKittyGraphics();
if (drawnContentVersion == Long.MIN_VALUE || geometryChanged || withKitty) {
@@ -113,7 +122,9 @@ final class TerminalPaneNode extends Region {
return;
}
+ long snapshotStart = RenderProfiler.start();
RenderStateSnapshot snapshot = pane.snapshot();
+ RenderProfiler.stop(RenderProfiler.SNAPSHOT, snapshotStart);
long renderedVersion = pane.snapshotVersion();
int dirty = snapshot == null ? DIRTY_FULL : snapshot.dirty();
if (dirty == DIRTY_FULL) {
@@ -167,7 +178,9 @@ final class TerminalPaneNode extends Region {
Set