hoist row hash out of detectShift delta scan

rowFingerprint(row) is invariant across the delta loop but was recomputed for
every candidate delta, making shift detection O(rows^2 x cols) on large changes
(full-screen scroll). Precompute each changed row's hash once, dropping it to
O(rows x cols). Profiling showed fingerprint hashing at ~74% of frame time under
heavy scroll, dominated by this loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gregor Lohaus
2026-05-31 21:09:54 +02:00
parent 1f7394d75a
commit 4923ea5527

View File

@@ -253,21 +253,29 @@ final class TerminalPaneNode extends Region {
} }
private ShiftPlan detectShift(RenderStateSnapshot snapshot, List<RenderRow> changedRows) { 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 bestDelta = 0;
int bestScore = 0; int bestScore = 0;
int rowCount = snapshot.rows();
for (int delta = -rowCount + 1; delta < rowCount; delta++) { for (int delta = -rowCount + 1; delta < rowCount; delta++) {
if (delta == 0) { if (delta == 0) {
continue; continue;
} }
int score = 0; int score = 0;
for (RenderRow row : changedRows) { for (int i = 0; i < changedCount; i++) {
int sourceRow = row.row() + delta; int sourceRow = changedRows.get(i).row() + delta;
if (sourceRow < 0 || sourceRow >= rowCount || !rows.containsKey(sourceRow)) { if (sourceRow < 0 || sourceRow >= rowCount || !rows.containsKey(sourceRow)) {
continue; continue;
} }
Long previous = rowFingerprints.get(sourceRow); Long previous = rowFingerprints.get(sourceRow);
if (previous != null && previous == rowFingerprint(row)) { if (previous != null && previous == changedHashes[i]) {
score++; score++;
} }
} }
@@ -284,13 +292,14 @@ final class TerminalPaneNode extends Region {
List<RowMove> moves = new ArrayList<>(bestScore); List<RowMove> moves = new ArrayList<>(bestScore);
Set<Integer> targetRows = new HashSet<>(); 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; int sourceRow = row.row() + bestDelta;
if (sourceRow < 0 || sourceRow >= rowCount || !rows.containsKey(sourceRow)) { if (sourceRow < 0 || sourceRow >= rowCount || !rows.containsKey(sourceRow)) {
continue; continue;
} }
Long previous = rowFingerprints.get(sourceRow); Long previous = rowFingerprints.get(sourceRow);
if (previous != null && previous == rowFingerprint(row)) { if (previous != null && previous == changedHashes[i]) {
moves.add(new RowMove(sourceRow, row.row())); moves.add(new RowMove(sourceRow, row.row()));
targetRows.add(row.row()); targetRows.add(row.row());
} }