Compare commits
17 Commits
scene-grap
...
profiling
| Author | SHA1 | Date | |
|---|---|---|---|
| 897a76d8cf | |||
| 5799c800d3 | |||
| cc9ac43ffa | |||
| 93d53fcef6 | |||
| b0ec6c7014 | |||
| 3c913fefd3 | |||
| 263bcf36b7 | |||
| 8e060b27ca | |||
| 6613f1f746 | |||
| 5728733f5f | |||
| 6e3e88919e | |||
| 57103bb98b | |||
| cb95a7188d | |||
|
|
5ca192b7be | ||
|
|
e99a6ee33e | ||
|
|
4923ea5527 | ||
|
|
1f7394d75a |
18
.classpath
18
.classpath
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="bin/main" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="gradle_scope" value="main"/>
|
||||
<attribute name="gradle_used_by_scope" value="main,test"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="bin/main" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="gradle_scope" value="main"/>
|
||||
<attribute name="gradle_used_by_scope" value="main,test"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
|
||||
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
||||
<classpathentry kind="output" path="bin/default"/>
|
||||
</classpath>
|
||||
@@ -1 +0,0 @@
|
||||
019e6999-b7c8-7591-a8aa-ea51b89a7f7e
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,3 +14,7 @@ build
|
||||
build
|
||||
.gradle
|
||||
bin
|
||||
.worktrees
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
34
.project
34
.project
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>jprototerm</name>
|
||||
<comment>Project jprototerm created by Buildship.</comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1779917652126</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
||||
@@ -1,13 +0,0 @@
|
||||
arguments=--init-script /home/anon/Src/eclipse.jdt.ls/org.eclipse.jdt.ls.product/target/repository/configuration/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle
|
||||
auto.sync=false
|
||||
build.scans.enabled=false
|
||||
connection.gradle.distribution=GRADLE_DISTRIBUTION(LOCAL_INSTALLATION(/home/anon/.sdkman/candidates/gradle/current))
|
||||
connection.project.dir=
|
||||
eclipse.preferences.version=1
|
||||
gradle.user.home=
|
||||
java.home=/nix/store/c3pl7bqrx3d2rc3dh98z6yaj0mv1p52g-openjdk-21.0.10+7/lib/openjdk
|
||||
jvm.arguments=
|
||||
offline.mode=false
|
||||
override.workspace.settings=true
|
||||
show.console.view=true
|
||||
show.executions.view=true
|
||||
19
README.md
19
README.md
@@ -132,3 +132,22 @@ open_scrollback = "ALT+S"
|
||||
Each tab has its own stack of tiled and floating panes; only the active tab is rendered. A
|
||||
thin tab bar appears at the top when more than one tab is open. Closing the last tiled pane
|
||||
while floating panes exist promotes the most recently active floating pane to a tiled pane.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Two render-debugging flags are off by default and add no overhead unless enabled. Pass them
|
||||
as JVM system properties (each also has an environment-variable equivalent). With the
|
||||
packaged binary the JVM picks them up from `JDK_JAVA_OPTIONS`:
|
||||
|
||||
```sh
|
||||
JDK_JAVA_OPTIONS="-Djprototerm.profile=true" ./result/bin/jprototerm
|
||||
```
|
||||
|
||||
- `-Djprototerm.profile=true` (or `JPROTOTERM_PROFILE=1`): print a `[render-profile]` line to
|
||||
stderr every N renders with the per-frame cost of each render stage — `snapshot` (terminal
|
||||
state marshalling), `fingerprint` (change detection), `draw` (canvas painting), and the
|
||||
`frame-total`. Set `-Djprototerm.profile.frames=<N>` to change the dump interval (default
|
||||
120).
|
||||
- `-Djprototerm.debugRepaint=true` (or `JPROTOTERM_DEBUG_REPAINT=1`): paint each per-column
|
||||
repaint run's cleared span red instead of clearing it. Repainted regions flash red, so you
|
||||
can see exactly which cells are being redrawn each frame.
|
||||
|
||||
2
TODOS.md
2
TODOS.md
@@ -1,2 +0,0 @@
|
||||
jlibghostty - why downcall metadata not propagated ?
|
||||
jlibghostty - how need to change flake so consuming flakes dont have to depend on same ghostty flake ?
|
||||
8
flake.lock
generated
8
flake.lock
generated
@@ -70,11 +70,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780079529,
|
||||
"narHash": "sha256-AxlGTL8c5xSLcQHvWlm994IdOqxsN8iKrA02Cpv7vso=",
|
||||
"lastModified": 1780258814,
|
||||
"narHash": "sha256-8rxL7xaZ/loYg3zdt0w5+hfNyHFVknDZN360NzrtCsQ=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "68121d50b52fb56038871c97c97e7a12ffe987c2",
|
||||
"revCount": 20,
|
||||
"rev": "6a3d5aa0b0b1f738c958e2a2f0249574c07d9c4d",
|
||||
"revCount": 23,
|
||||
"type": "git",
|
||||
"url": "https://gitea.gregorlohaus.com/gregor/jlibghostty.git"
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ final class KeyEncoder {
|
||||
return switch (code) {
|
||||
case ENTER -> "\r";
|
||||
case BACK_SPACE -> "\u007f";
|
||||
case TAB -> "\t";
|
||||
case TAB -> event.isShiftDown() ? "\u001b[Z" : "\t";
|
||||
case ESCAPE -> "\u001b";
|
||||
case UP -> "\u001b[A";
|
||||
case DOWN -> "\u001b[B";
|
||||
|
||||
79
src/main/java/com/gregor/jprototerm/RenderProfiler.java
Normal file
79
src/main/java/com/gregor/jprototerm/RenderProfiler.java
Normal file
@@ -0,0 +1,79 @@
|
||||
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;
|
||||
static final int UPDATE = 4;
|
||||
static final int MARSHAL = 5;
|
||||
private static final int BUCKETS = 6;
|
||||
private static final String[] NAMES =
|
||||
{"snapshot", "fingerprint", "draw", "frame-total", "update", "marshal"};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -160,13 +160,21 @@ public final class TerminalPane implements AutoCloseable {
|
||||
synchronized (terminal) {
|
||||
long version = contentVersion.get();
|
||||
if (full) {
|
||||
long updateStart = RenderProfiler.start();
|
||||
renderState.update(terminal);
|
||||
RenderProfiler.stop(RenderProfiler.UPDATE, updateStart);
|
||||
long marshalStart = RenderProfiler.start();
|
||||
cachedSnapshot = renderState.snapshot();
|
||||
RenderProfiler.stop(RenderProfiler.MARSHAL, marshalStart);
|
||||
renderState.resetDirty();
|
||||
snapshotVersion = version;
|
||||
} else if (snapshotVersion != version) {
|
||||
long updateStart = RenderProfiler.start();
|
||||
renderState.update(terminal);
|
||||
RenderProfiler.stop(RenderProfiler.UPDATE, updateStart);
|
||||
long marshalStart = RenderProfiler.start();
|
||||
cachedSnapshot = renderState.snapshotIncremental();
|
||||
RenderProfiler.stop(RenderProfiler.MARSHAL, marshalStart);
|
||||
renderState.resetDirty();
|
||||
snapshotVersion = version;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,13 @@ final class TerminalPaneNode extends Region {
|
||||
private static final int DIRTY_PARTIAL = 1;
|
||||
private static final int DIRTY_FULL = 2;
|
||||
|
||||
// Debug toggle: paint each repaint run's cleared span red instead of clearing it to
|
||||
// transparent. If the black bars turn red, they are spans repaintColumns clears but never
|
||||
// refills; if they stay black, those pixels are never touched by repaintColumns at all.
|
||||
private static final boolean DEBUG_REPAINT =
|
||||
Boolean.getBoolean("jprototerm.debugRepaint")
|
||||
|| "1".equals(System.getenv("JPROTOTERM_DEBUG_REPAINT"));
|
||||
|
||||
private static final Color DEFAULT_FOREGROUND = Color.rgb(225, 229, 235);
|
||||
private static final Color SELECTED_BACKGROUND = Color.rgb(52, 92, 140);
|
||||
private static final Color PANE_BACKGROUND = Color.rgb(9, 10, 12);
|
||||
@@ -91,7 +98,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 +111,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 +129,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 +185,9 @@ final class TerminalPaneNode extends Region {
|
||||
Set<Integer> liveRows = new HashSet<>();
|
||||
for (RenderRow row : snapshot.renderRows()) {
|
||||
TerminalRowNode node = rowNode(row.row());
|
||||
long fpStart = RenderProfiler.start();
|
||||
long fingerprint = rowFingerprint(row);
|
||||
RenderProfiler.stop(RenderProfiler.FINGERPRINT, fpStart);
|
||||
node.render(row);
|
||||
rowFingerprints.put(row.row(), fingerprint);
|
||||
liveRows.add(row.row());
|
||||
@@ -197,7 +217,9 @@ final class TerminalPaneNode extends Region {
|
||||
continue;
|
||||
}
|
||||
TerminalRowNode node = rowNode(row.row());
|
||||
long fpStart = RenderProfiler.start();
|
||||
long fingerprint = rowFingerprint(row);
|
||||
RenderProfiler.stop(RenderProfiler.FINGERPRINT, fpStart);
|
||||
node.renderChanged(row);
|
||||
rowFingerprints.put(row.row(), fingerprint);
|
||||
}
|
||||
@@ -212,7 +234,9 @@ final class TerminalPaneNode extends Region {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
long shiftStart = RenderProfiler.start();
|
||||
ShiftPlan plan = detectShift(snapshot, changedRows);
|
||||
RenderProfiler.stop(RenderProfiler.FINGERPRINT, shiftStart);
|
||||
if (plan == null) {
|
||||
return Set.of();
|
||||
}
|
||||
@@ -236,21 +260,29 @@ final class TerminalPaneNode extends Region {
|
||||
}
|
||||
|
||||
private ShiftPlan detectShift(RenderStateSnapshot snapshot, List<RenderRow> changedRows) {
|
||||
int rowCount = snapshot.rows();
|
||||
int changedCount = changedRows.size();
|
||||
// The new-content hash of each changed row is invariant across the delta scan below,
|
||||
// so compute it once here instead of re-hashing the whole row for every candidate delta.
|
||||
long[] changedHashes = new long[changedCount];
|
||||
for (int i = 0; i < changedCount; i++) {
|
||||
changedHashes[i] = rowFingerprint(changedRows.get(i));
|
||||
}
|
||||
|
||||
int bestDelta = 0;
|
||||
int bestScore = 0;
|
||||
int rowCount = snapshot.rows();
|
||||
for (int delta = -rowCount + 1; delta < rowCount; delta++) {
|
||||
if (delta == 0) {
|
||||
continue;
|
||||
}
|
||||
int score = 0;
|
||||
for (RenderRow row : changedRows) {
|
||||
int sourceRow = row.row() + delta;
|
||||
for (int i = 0; i < changedCount; i++) {
|
||||
int sourceRow = changedRows.get(i).row() + delta;
|
||||
if (sourceRow < 0 || sourceRow >= rowCount || !rows.containsKey(sourceRow)) {
|
||||
continue;
|
||||
}
|
||||
Long previous = rowFingerprints.get(sourceRow);
|
||||
if (previous != null && previous == rowFingerprint(row)) {
|
||||
if (previous != null && previous == changedHashes[i]) {
|
||||
score++;
|
||||
}
|
||||
}
|
||||
@@ -267,13 +299,14 @@ final class TerminalPaneNode extends Region {
|
||||
|
||||
List<RowMove> moves = new ArrayList<>(bestScore);
|
||||
Set<Integer> targetRows = new HashSet<>();
|
||||
for (RenderRow row : changedRows) {
|
||||
for (int i = 0; i < changedCount; i++) {
|
||||
RenderRow row = changedRows.get(i);
|
||||
int sourceRow = row.row() + bestDelta;
|
||||
if (sourceRow < 0 || sourceRow >= rowCount || !rows.containsKey(sourceRow)) {
|
||||
continue;
|
||||
}
|
||||
Long previous = rowFingerprints.get(sourceRow);
|
||||
if (previous != null && previous == rowFingerprint(row)) {
|
||||
if (previous != null && previous == changedHashes[i]) {
|
||||
moves.add(new RowMove(sourceRow, row.row()));
|
||||
targetRows.add(row.row());
|
||||
}
|
||||
@@ -692,6 +725,7 @@ final class TerminalPaneNode extends Region {
|
||||
private void render(RenderRow row) {
|
||||
prepareCanvas(row);
|
||||
|
||||
long drawStart = RenderProfiler.start();
|
||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||
gc.clearRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight());
|
||||
gc.setFontSmoothingType(FontSmoothingType.LCD);
|
||||
@@ -699,14 +733,20 @@ final class TerminalPaneNode extends Region {
|
||||
|
||||
paintSidePadding(gc, row, canvas.getWidth(), canvas.getHeight());
|
||||
drawRow(gc, row, rowTop(row), metrics.cellWidth(), metrics.lineHeight());
|
||||
RenderProfiler.stop(RenderProfiler.DRAW, drawStart);
|
||||
|
||||
long fpStart = RenderProfiler.start();
|
||||
cellFingerprints = cellFingerprints(row);
|
||||
RenderProfiler.stop(RenderProfiler.FINGERPRINT, fpStart);
|
||||
}
|
||||
|
||||
private void renderChanged(RenderRow row) {
|
||||
double oldWidth = canvas.getWidth();
|
||||
double oldHeight = canvas.getHeight();
|
||||
prepareCanvas(row);
|
||||
long fpStart = RenderProfiler.start();
|
||||
long[] nextFingerprints = cellFingerprints(row);
|
||||
RenderProfiler.stop(RenderProfiler.FINGERPRINT, fpStart);
|
||||
if (cellFingerprints.length != nextFingerprints.length
|
||||
|| oldWidth != canvas.getWidth()
|
||||
|| oldHeight != canvas.getHeight()) {
|
||||
@@ -714,6 +754,7 @@ final class TerminalPaneNode extends Region {
|
||||
return;
|
||||
}
|
||||
|
||||
long drawStart = RenderProfiler.start();
|
||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||
gc.setFontSmoothingType(FontSmoothingType.LCD);
|
||||
gc.setFont(metrics.font());
|
||||
@@ -741,6 +782,7 @@ final class TerminalPaneNode extends Region {
|
||||
if (runStart >= 0) {
|
||||
repaintColumns(gc, row, runStart, runEnd);
|
||||
}
|
||||
RenderProfiler.stop(RenderProfiler.DRAW, drawStart);
|
||||
cellFingerprints = nextFingerprints;
|
||||
}
|
||||
|
||||
@@ -794,7 +836,15 @@ final class TerminalPaneNode extends Region {
|
||||
double x = TerminalMetrics.PADDING + startColumn * cellWidth;
|
||||
double width = (endColumn - startColumn + 1) * cellWidth;
|
||||
|
||||
gc.clearRect(x, 0.0, width, canvas.getHeight());
|
||||
// Opaque base fill rather than clearRect: a transparent clear leaves the run's
|
||||
// fractional edge pixels transparent, which show the near-black pane background
|
||||
// as a thin seam bar against the adjacent (un-repainted) line. Filling opaque
|
||||
// removes every transparent pixel; per-cell backgrounds then paint on top, and
|
||||
// default-background cells correctly show PANE_BACKGROUND. Safe because the
|
||||
// per-column path never runs while kitty graphics (which need a transparent row
|
||||
// canvas for below-text images) are present.
|
||||
gc.setFill(DEBUG_REPAINT ? Color.RED : PANE_BACKGROUND);
|
||||
gc.fillRect(x, 0.0, width, canvas.getHeight());
|
||||
if (startColumn == 0) {
|
||||
gc.setFill(rowEdgeBackground(row, true));
|
||||
gc.fillRect(0.0, 0.0, TerminalMetrics.PADDING, canvas.getHeight());
|
||||
|
||||
Reference in New Issue
Block a user