add opt-in render profiler instrumentation
Gated behind -Djprototerm.profile=true (or JPROTOTERM_PROFILE=1), accumulates per-frame nanos into snapshot/fingerprint/draw/frame-total buckets and dumps to stderr every N renders. Splits the three suspected render costs: native snapshot marshaling, fingerprint hashing, and canvas draw recording. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
76
src/main/java/com/gregor/jprototerm/RenderProfiler.java
Normal file
76
src/main/java/com/gregor/jprototerm/RenderProfiler.java
Normal file
@@ -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).
|
||||||
|
*
|
||||||
|
* <p>All render work runs on the JavaFX application thread, so the accumulators are plain
|
||||||
|
* fields with no synchronization.
|
||||||
|
*
|
||||||
|
* <p>Caveat: JavaFX canvas drawing is deferred to the QuantumRenderer thread, so the
|
||||||
|
* {@link #DRAW} bucket measures only the cost of <em>recording</em> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,7 +91,9 @@ final class TerminalPaneNode extends Region {
|
|||||||
|
|
||||||
void renderFull(boolean active) {
|
void renderFull(boolean active) {
|
||||||
prepareGeometry();
|
prepareGeometry();
|
||||||
|
long snapshotStart = RenderProfiler.start();
|
||||||
RenderStateSnapshot snapshot = pane.snapshotFull();
|
RenderStateSnapshot snapshot = pane.snapshotFull();
|
||||||
|
RenderProfiler.stop(RenderProfiler.SNAPSHOT, snapshotStart);
|
||||||
long renderedVersion = pane.snapshotVersion();
|
long renderedVersion = pane.snapshotVersion();
|
||||||
boolean withKitty = pane.kittyEnabled() && hasKittyGraphics();
|
boolean withKitty = pane.kittyEnabled() && hasKittyGraphics();
|
||||||
updateRowsFull(snapshot);
|
updateRowsFull(snapshot);
|
||||||
@@ -102,6 +104,13 @@ final class TerminalPaneNode extends Region {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void renderIncremental(boolean active) {
|
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 geometryChanged = prepareGeometry();
|
||||||
boolean withKitty = pane.kittyEnabled() && hasKittyGraphics();
|
boolean withKitty = pane.kittyEnabled() && hasKittyGraphics();
|
||||||
if (drawnContentVersion == Long.MIN_VALUE || geometryChanged || withKitty) {
|
if (drawnContentVersion == Long.MIN_VALUE || geometryChanged || withKitty) {
|
||||||
@@ -113,7 +122,9 @@ final class TerminalPaneNode extends Region {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long snapshotStart = RenderProfiler.start();
|
||||||
RenderStateSnapshot snapshot = pane.snapshot();
|
RenderStateSnapshot snapshot = pane.snapshot();
|
||||||
|
RenderProfiler.stop(RenderProfiler.SNAPSHOT, snapshotStart);
|
||||||
long renderedVersion = pane.snapshotVersion();
|
long renderedVersion = pane.snapshotVersion();
|
||||||
int dirty = snapshot == null ? DIRTY_FULL : snapshot.dirty();
|
int dirty = snapshot == null ? DIRTY_FULL : snapshot.dirty();
|
||||||
if (dirty == DIRTY_FULL) {
|
if (dirty == DIRTY_FULL) {
|
||||||
@@ -167,7 +178,9 @@ final class TerminalPaneNode extends Region {
|
|||||||
Set<Integer> liveRows = new HashSet<>();
|
Set<Integer> liveRows = new HashSet<>();
|
||||||
for (RenderRow row : snapshot.renderRows()) {
|
for (RenderRow row : snapshot.renderRows()) {
|
||||||
TerminalRowNode node = rowNode(row.row());
|
TerminalRowNode node = rowNode(row.row());
|
||||||
|
long fpStart = RenderProfiler.start();
|
||||||
long fingerprint = rowFingerprint(row);
|
long fingerprint = rowFingerprint(row);
|
||||||
|
RenderProfiler.stop(RenderProfiler.FINGERPRINT, fpStart);
|
||||||
node.render(row);
|
node.render(row);
|
||||||
rowFingerprints.put(row.row(), fingerprint);
|
rowFingerprints.put(row.row(), fingerprint);
|
||||||
liveRows.add(row.row());
|
liveRows.add(row.row());
|
||||||
@@ -197,7 +210,9 @@ final class TerminalPaneNode extends Region {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
TerminalRowNode node = rowNode(row.row());
|
TerminalRowNode node = rowNode(row.row());
|
||||||
|
long fpStart = RenderProfiler.start();
|
||||||
long fingerprint = rowFingerprint(row);
|
long fingerprint = rowFingerprint(row);
|
||||||
|
RenderProfiler.stop(RenderProfiler.FINGERPRINT, fpStart);
|
||||||
node.renderChanged(row);
|
node.renderChanged(row);
|
||||||
rowFingerprints.put(row.row(), fingerprint);
|
rowFingerprints.put(row.row(), fingerprint);
|
||||||
}
|
}
|
||||||
@@ -212,7 +227,9 @@ final class TerminalPaneNode extends Region {
|
|||||||
return Set.of();
|
return Set.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long shiftStart = RenderProfiler.start();
|
||||||
ShiftPlan plan = detectShift(snapshot, changedRows);
|
ShiftPlan plan = detectShift(snapshot, changedRows);
|
||||||
|
RenderProfiler.stop(RenderProfiler.FINGERPRINT, shiftStart);
|
||||||
if (plan == null) {
|
if (plan == null) {
|
||||||
return Set.of();
|
return Set.of();
|
||||||
}
|
}
|
||||||
@@ -692,6 +709,7 @@ final class TerminalPaneNode extends Region {
|
|||||||
private void render(RenderRow row) {
|
private void render(RenderRow row) {
|
||||||
prepareCanvas(row);
|
prepareCanvas(row);
|
||||||
|
|
||||||
|
long drawStart = RenderProfiler.start();
|
||||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||||
gc.clearRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight());
|
gc.clearRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight());
|
||||||
gc.setFontSmoothingType(FontSmoothingType.LCD);
|
gc.setFontSmoothingType(FontSmoothingType.LCD);
|
||||||
@@ -699,14 +717,20 @@ final class TerminalPaneNode extends Region {
|
|||||||
|
|
||||||
paintSidePadding(gc, row, canvas.getWidth(), canvas.getHeight());
|
paintSidePadding(gc, row, canvas.getWidth(), canvas.getHeight());
|
||||||
drawRow(gc, row, rowTop(row), metrics.cellWidth(), metrics.lineHeight());
|
drawRow(gc, row, rowTop(row), metrics.cellWidth(), metrics.lineHeight());
|
||||||
|
RenderProfiler.stop(RenderProfiler.DRAW, drawStart);
|
||||||
|
|
||||||
|
long fpStart = RenderProfiler.start();
|
||||||
cellFingerprints = cellFingerprints(row);
|
cellFingerprints = cellFingerprints(row);
|
||||||
|
RenderProfiler.stop(RenderProfiler.FINGERPRINT, fpStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderChanged(RenderRow row) {
|
private void renderChanged(RenderRow row) {
|
||||||
double oldWidth = canvas.getWidth();
|
double oldWidth = canvas.getWidth();
|
||||||
double oldHeight = canvas.getHeight();
|
double oldHeight = canvas.getHeight();
|
||||||
prepareCanvas(row);
|
prepareCanvas(row);
|
||||||
|
long fpStart = RenderProfiler.start();
|
||||||
long[] nextFingerprints = cellFingerprints(row);
|
long[] nextFingerprints = cellFingerprints(row);
|
||||||
|
RenderProfiler.stop(RenderProfiler.FINGERPRINT, fpStart);
|
||||||
if (cellFingerprints.length != nextFingerprints.length
|
if (cellFingerprints.length != nextFingerprints.length
|
||||||
|| oldWidth != canvas.getWidth()
|
|| oldWidth != canvas.getWidth()
|
||||||
|| oldHeight != canvas.getHeight()) {
|
|| oldHeight != canvas.getHeight()) {
|
||||||
@@ -714,6 +738,7 @@ final class TerminalPaneNode extends Region {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long drawStart = RenderProfiler.start();
|
||||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||||
gc.setFontSmoothingType(FontSmoothingType.LCD);
|
gc.setFontSmoothingType(FontSmoothingType.LCD);
|
||||||
gc.setFont(metrics.font());
|
gc.setFont(metrics.font());
|
||||||
@@ -741,6 +766,7 @@ final class TerminalPaneNode extends Region {
|
|||||||
if (runStart >= 0) {
|
if (runStart >= 0) {
|
||||||
repaintColumns(gc, row, runStart, runEnd);
|
repaintColumns(gc, row, runStart, runEnd);
|
||||||
}
|
}
|
||||||
|
RenderProfiler.stop(RenderProfiler.DRAW, drawStart);
|
||||||
cellFingerprints = nextFingerprints;
|
cellFingerprints = nextFingerprints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user