Skip to content

perf: optimize Display.update for minimal allocation and output bytes#1785

Merged
gnodet merged 1 commit into
masterfrom
ripe-deal
Apr 15, 2026
Merged

perf: optimize Display.update for minimal allocation and output bytes#1785
gnodet merged 1 commit into
masterfrom
ripe-deal

Conversation

@gnodet
Copy link
Copy Markdown
Member

@gnodet gnodet commented Apr 14, 2026

Summary

Optimizes Display.update() to minimize both heap allocation and terminal output bytes.

Display.update() optimizations

  • Intra-line skip: when a row has a long unchanged gap between two changed regions, emit a cursor-movement sequence (CUF / CHA) instead of re-sending the unchanged characters
  • AttributedCharSequence input: accept AttributedCharSequence (not just AttributedString) to avoid per-frame snapshot copies
  • Fast-path for List<AttributedString>: skip snapshotLines overhead when input is already a list of AttributedString
  • Cell-grid update API: new update(char[], long[], int[], ...) overload with flat row-major arrays; reuses oldLines entries for unchanged rows via rowEquals check

AttributedCharSequence allocation reduction

  • State consolidation: toAnsiBytes uses long[2] (style + altCharset) instead of long[4]
  • Color fast-path: emitStyleChange() skips color extraction when (diff & COLOR_BITS) == 0 (only text decoration changed)
  • columnLength delegation: overloads delegate to a single 5-parameter method using bare BreakIterator + CharSequenceCharacterIterator, avoiding toString() allocation

DiffHelper

  • Deduplication: public diff(text1, text2) delegates to the allocation-free diff(text1, s1, e1, text2, s2, e2, result) variant
  • Suffix rollback fix: commonSuffixLength hidden-range rollback was unconditional; now conditional on whether the next character is actually hidden, matching the prefix logic

Other

  • CharSequenceCharacterIterator and resetGraphemeBreakIterator in WCWidth for allocation-free BreakIterator usage
  • @since 4.1.0 tags on new public API methods
  • DisplayBenchmarkTest for update throughput measurement
  • Tests for intra-line skip optimization, cell-grid API, and AttributedCharSequence input

Test plan

  • mvn test -pl terminal — all 267 tests pass
  • testIntraLineSkipOptimization — verifies cursor movement skips unchanged gaps
  • testCellGridUpdate / testCellGridWithWidths / testCellGridParityWithListUpdate — cell-grid API correctness
  • testUpdateWithAttributedCharSequence — AttributedCharSequence input path
  • Existing i737 test — regression check for full-screen update

Summary by CodeRabbit

  • New Features

    • Added support for flexible sequence types in display updates
    • Optimized cursor movement for diagonal repositioning
  • Improvements & Optimizations

    • Enhanced terminal display rendering performance through capability caching
    • Reduced memory allocations in diff computation and ANSI sequence generation
    • Improved grapheme cluster handling for accurate column width calculations
    • Streamlined style state tracking to minimize redundant resets
  • Tests

    • Added performance benchmarking for display updates
    • Expanded display update validation test coverage

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactored ANSI rendering to range- and state-aware APIs, exposed reusable style-buffer access, added allocation-free diff primitives and grapheme-break iterator reuse, overhauled Display.update to reduce allocations and add intra-line skip/overwrite optimizations, and added tests plus a micro-benchmark.

Changes

Cohort / File(s) Summary
Range-based rendering & style buffer
terminal/src/main/java/org/jline/utils/AttributedCharSequence.java, terminal/src/main/java/org/jline/utils/AttributedString.java, terminal/src/main/java/org/jline/utils/AttributedStringBuilder.java
Introduced range-capable toAnsiBytes(..., rangeStart, rangeEnd, ..., state) and updated toAnsi(...) to use it; added abstract styleBuffer() and exposed backing long[] style arrays (constructor visibility widened).
Display update & rendering refactor
terminal/src/main/java/org/jline/utils/Display.java
Cached terminal capabilities/state, replaced stream-based conversions with explicit loops, added updateSequences(...) and update(char[], long[], ...) grid overload, range-aware diff/rendering using preallocated result arrays, intra-line overwrite/skip optimization, improved cursor addressing, and extended byte-mode rawPrint with style-state tracking.
Range diff primitive
terminal/src/main/java/org/jline/utils/DiffHelper.java
Added allocation-free package-private diff(..., s1,e1, s2,e2, result) and internal prefix/suffix scanners using styleCodeAt(...); public diff(text1,text2) delegates to new helper.
Grapheme-break iterator reuse & width API
terminal/src/main/java/org/jline/utils/WCWidth.java
Added CharSequenceCharacterIterator and resetGraphemeBreakIterator(...) to bind/reuse BreakIterator without String allocations; added range-aware column-length helpers.
Tests & benchmark
terminal/src/test/java/org/jline/utils/DisplayBenchmarkTest.java, terminal/src/test/java/org/jline/utils/DisplayTest.java, terminal/src/test/java/org/jline/utils/ByteArrayBuilderTest.java
Added Display micro-benchmark and multiple unit tests for intra-line-skip, sequence and grid updates; added output-capture helpers; updated tests to exercise range/stateful toAnsiBytes and state-driven reset handling.

Sequence Diagram

sequenceDiagram
    participant Display as "Display (renderer)"
    participant Diff as "DiffHelper"
    participant ACSeq as "AttributedCharSequence"
    participant WC as "WCWidth"
    participant Terminal as "Terminal/TTY"

    Display->>Diff: diff(oldLinesRange, newLinesRange)
    Diff-->>Display: prefix/suffix lengths (range result)
    Display->>WC: columnLength(rangeStart, rangeEnd)
    WC-->>Display: visible width
    Display->>ACSeq: toAnsiBytes(rangeStart, rangeEnd, colors, state)
    ACSeq-->>Display: ANSI bytes + updated state
    Display->>Terminal: move cursor & write bytes (rawPrint)
    Terminal-->>Display: acknowledge / cursor pos
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

chore, java

Poem

🐰 I hop through bytes and carry state,
I tuck stray graphemes into place,
I skip long runs with nimble gait,
Benchmarks hum — the screen finds grace.
A little rabbit, leaving no trace.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the primary change: optimizing Display.update() to minimize memory allocation and terminal output bytes through multiple performance improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ripe-deal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
terminal/src/main/java/org/jline/utils/Display.java (1)

667-693: Consider returning an empty array sentinel instead of null.

SonarCloud flags returning null at line 692. The current pattern requires callers to null-check. While this is existing behavior, a sentinel value (e.g., {-1, -1, 0}) could simplify caller code.

However, this would require updating the caller at line 337 and potentially other callers. Given this is a performance-focused PR, preserving the existing null-check pattern is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terminal/src/main/java/org/jline/utils/Display.java` around lines 667 - 693,
The method longestCommon currently returns null when no common substring is
found; change it to return a sentinel int[] (e.g., new int[]{-1, -1, 0}) instead
of null, assign that sentinel to lcsResult before returning, and update all
callers (including the caller referenced at line 337 and any other places that
check for null) to check for the sentinel (start indices == -1 or length == 0)
rather than a null reference to avoid NPEs and simplify control flow.
terminal/src/main/java/org/jline/utils/WCWidth.java (1)

533-541: Consider using super.clone() or a copy factory method.

The clone() implementation manually copies fields rather than calling super.clone(). While this works correctly since the class is final and package-private, it doesn't follow the standard Cloneable contract. SonarCloud flags this as a potential issue.

Since BreakIterator.setText(CharacterIterator) calls clone() internally, this implementation is functionally correct. However, a copy factory or using super.clone() would be more conventional.

♻️ Alternative using super.clone()
         `@Override`
         public Object clone() {
-            CharSequenceCharacterIterator copy = new CharSequenceCharacterIterator();
-            copy.cs = this.cs;
-            copy.begin = this.begin;
-            copy.end = this.end;
-            copy.pos = this.pos;
-            return copy;
+            try {
+                return super.clone();
+            } catch (CloneNotSupportedException e) {
+                throw new AssertionError(e); // Cannot happen for Cloneable
+            }
         }

Note: The class would need to implement Cloneable:

-    static final class CharSequenceCharacterIterator implements CharacterIterator {
+    static final class CharSequenceCharacterIterator implements CharacterIterator, Cloneable {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terminal/src/main/java/org/jline/utils/WCWidth.java` around lines 533 - 541,
The clone() method in CharSequenceCharacterIterator manually constructs a copy
by copying fields instead of following the standard Cloneable contract; replace
this manual copying with a conventional approach: either implement Cloneable and
call super.clone() inside clone() (ensuring CharSequenceCharacterIterator is
cloneable and returns a proper CharSequenceCharacterIterator) or add a copy
factory method (e.g., a private copy constructor or a public static
copyOf(CharSequenceCharacterIterator)) and have clone() delegate to that
factory; update the clone() implementation in CharSequenceCharacterIterator
accordingly and ensure the returned object is the correct concrete type.
terminal/src/test/java/org/jline/utils/DisplayBenchmarkTest.java (1)

30-127: Consider adding a minimal assertion for test validity.

This benchmark test has no assertions, which SonarCloud flags. While benchmarks typically focus on measurement rather than verification, adding a minimal assertion ensures the test fails if something goes catastrophically wrong rather than silently producing invalid measurements.

♻️ Add minimal assertion
             System.out.printf("  Heap growth:   %,d bytes%n", memAfter - memBefore);
         }
+        // Minimal assertion to ensure the benchmark actually ran
+        org.junit.jupiter.api.Assertions.assertTrue(true, "Benchmark completed successfully");
     }

Or more meaningfully, verify the display state is valid after updates:

// After the benchmark loop
org.junit.jupiter.api.Assertions.assertNotNull(display, "Display should remain valid");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terminal/src/test/java/org/jline/utils/DisplayBenchmarkTest.java` around
lines 30 - 127, The test benchmarkDisplayUpdate contains no assertions—add a
minimal JUnit assertion at the end of the test to ensure the Display instance
and its state remain valid after updates (e.g., assert display is not null
and/or verify the current buffer equals the expected screen A/C), referencing
the Display variable named display and the benchmarkDisplayUpdate test method so
the test will fail on catastrophic errors rather than silently passing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@terminal/src/main/java/org/jline/utils/DiffHelper.java`:
- Around line 110-163: Remove the dangling/duplicate Javadoc that was left
before the new range-based diff method: delete the old comment block that
described "Compute a list of difference between two lines" (the Javadoc that no
longer precedes any declaration) so only the proper Javadoc remains for the new
static void diff(AttributedString, int, int, AttributedString, int, int, int[])
and the existing public static List<Diff> diff(AttributedString,
AttributedString) stays correctly documented.

---

Nitpick comments:
In `@terminal/src/main/java/org/jline/utils/Display.java`:
- Around line 667-693: The method longestCommon currently returns null when no
common substring is found; change it to return a sentinel int[] (e.g., new
int[]{-1, -1, 0}) instead of null, assign that sentinel to lcsResult before
returning, and update all callers (including the caller referenced at line 337
and any other places that check for null) to check for the sentinel (start
indices == -1 or length == 0) rather than a null reference to avoid NPEs and
simplify control flow.

In `@terminal/src/main/java/org/jline/utils/WCWidth.java`:
- Around line 533-541: The clone() method in CharSequenceCharacterIterator
manually constructs a copy by copying fields instead of following the standard
Cloneable contract; replace this manual copying with a conventional approach:
either implement Cloneable and call super.clone() inside clone() (ensuring
CharSequenceCharacterIterator is cloneable and returns a proper
CharSequenceCharacterIterator) or add a copy factory method (e.g., a private
copy constructor or a public static copyOf(CharSequenceCharacterIterator)) and
have clone() delegate to that factory; update the clone() implementation in
CharSequenceCharacterIterator accordingly and ensure the returned object is the
correct concrete type.

In `@terminal/src/test/java/org/jline/utils/DisplayBenchmarkTest.java`:
- Around line 30-127: The test benchmarkDisplayUpdate contains no assertions—add
a minimal JUnit assertion at the end of the test to ensure the Display instance
and its state remain valid after updates (e.g., assert display is not null
and/or verify the current buffer equals the expected screen A/C), referencing
the Display variable named display and the benchmarkDisplayUpdate test method so
the test will fail on catastrophic errors rather than silently passing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 727fc339-d267-47e2-a2eb-975549b3f142

📥 Commits

Reviewing files that changed from the base of the PR and between 7e8e2ed and 5556e6b.

📒 Files selected for processing (5)
  • terminal/src/main/java/org/jline/utils/AttributedCharSequence.java
  • terminal/src/main/java/org/jline/utils/DiffHelper.java
  • terminal/src/main/java/org/jline/utils/Display.java
  • terminal/src/main/java/org/jline/utils/WCWidth.java
  • terminal/src/test/java/org/jline/utils/DisplayBenchmarkTest.java

Comment thread terminal/src/main/java/org/jline/utils/DiffHelper.java
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@terminal/src/main/java/org/jline/utils/Display.java`:
- Around line 513-516: The condition comparing currentPos to columns uses
absolute cursor position instead of the per-line remaining width, so
deleteChars(...) optimization is skipped for non-first lines; modify the check
in the block that computes suffixWidth (uses oldLine.columnLength(...)) to
compute the visual column within the current line (e.g., visualPos = currentPos
- startVisualOfCurrentLine or compute remaining = columns - visualPos) and use
remaining columns (not absolute currentPos) when deciding to call
moveVisualCursorTo(...) and deleteChars(...); update references in this block to
use the per-line visual offset rather than currentPos vs columns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a4ee91f2-9468-4f35-a298-fcbc0b39d48c

📥 Commits

Reviewing files that changed from the base of the PR and between 5556e6b and 35eaa96.

📒 Files selected for processing (2)
  • terminal/src/main/java/org/jline/utils/AttributedCharSequence.java
  • terminal/src/main/java/org/jline/utils/Display.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • terminal/src/main/java/org/jline/utils/AttributedCharSequence.java

Comment thread terminal/src/main/java/org/jline/utils/Display.java
@gnodet gnodet changed the title perf: make Display.update a zero-allocation operation perf: optimize Display.update for zero allocation, minimal output bytes Apr 14, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
terminal/src/main/java/org/jline/utils/Display.java (1)

525-529: ⚠️ Potential issue | 🟠 Major

Use a line-relative column when gating deleteChars(...).

At Line 528, currentPos is absolute, but columns is the width of the current row. On any row after the first this makes the condition almost always false, so the delete fast path is skipped even when the suffix fits. Downstream, that turns second-and-later-line deletes back into clear/repaint work.

Suggested fix
-                    if (currentPos + suffixWidth < columns) {
+                    if ((currentPos - curCol) + suffixWidth < columns) {
                         moveVisualCursorTo(currentPos);
                         ensureDefaultAnsiStyle();
                         if (deleteChars(deleteWidth)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terminal/src/main/java/org/jline/utils/Display.java` around lines 525 - 529,
The condition gating the fast delete path uses absolute currentPos against
columns (the per-row width), causing it to fail on rows beyond the first;
compute a line-relative column (e.g. int lineCol = currentPos % columns or
currentPos - rowStart) and use that when checking suffixWidth and moving the
visual cursor before calling deleteChars(...). Update references around
hasEqSuffix, oldLine.columnLength(...), moveVisualCursorTo(currentPos) to use
the computed lineCol (or compute the remaining space as columns - lineCol) so
the suffix-fit check and cursor move are relative to the current row.
🧹 Nitpick comments (1)
terminal/src/main/java/org/jline/utils/Display.java (1)

829-834: Either cost-check CUP here or relax the heuristic/comment.

This branch now takes cursor_address for every diagonal move, but the comment says “when it's shorter than relative”. For small deltas, relative cursor_* sequences can still be cheaper, so this can increase output bytes on local edits even though the PR is trying to minimize them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terminal/src/main/java/org/jline/utils/Display.java` around lines 829 - 834,
The current branch unconditionally uses absolute cursor_address
(puts(Capability.cursor_address, l1, c1)) for diagonal moves (when
hasCursorAddress && l0 != l1 && c0 != c1) despite the comment that it should
only be used "when it's shorter than relative"; modify this to compute and
compare the byte-cost of the absolute CUP sequence versus the combined relative
sequences using the existing capability outputs for cursor_up/down/left/right
(or their cached byte lengths) and only call puts(Capability.cursor_address, l1,
c1) and set cursorPos = i1 when the CUP cost is strictly less (otherwise fall
back to emitting the shorter relative moves), or if you prefer to keep the
heuristic, update the comment to remove the "when it's shorter than relative"
claim and explain the chosen heuristic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@terminal/src/main/java/org/jline/utils/Display.java`:
- Around line 525-529: The condition gating the fast delete path uses absolute
currentPos against columns (the per-row width), causing it to fail on rows
beyond the first; compute a line-relative column (e.g. int lineCol = currentPos
% columns or currentPos - rowStart) and use that when checking suffixWidth and
moving the visual cursor before calling deleteChars(...). Update references
around hasEqSuffix, oldLine.columnLength(...), moveVisualCursorTo(currentPos) to
use the computed lineCol (or compute the remaining space as columns - lineCol)
so the suffix-fit check and cursor move are relative to the current row.

---

Nitpick comments:
In `@terminal/src/main/java/org/jline/utils/Display.java`:
- Around line 829-834: The current branch unconditionally uses absolute
cursor_address (puts(Capability.cursor_address, l1, c1)) for diagonal moves
(when hasCursorAddress && l0 != l1 && c0 != c1) despite the comment that it
should only be used "when it's shorter than relative"; modify this to compute
and compare the byte-cost of the absolute CUP sequence versus the combined
relative sequences using the existing capability outputs for
cursor_up/down/left/right (or their cached byte lengths) and only call
puts(Capability.cursor_address, l1, c1) and set cursorPos = i1 when the CUP cost
is strictly less (otherwise fall back to emitting the shorter relative moves),
or if you prefer to keep the heuristic, update the comment to remove the "when
it's shorter than relative" claim and explain the chosen heuristic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e00fbaf0-6589-4b1d-89fb-d83904aea8c0

📥 Commits

Reviewing files that changed from the base of the PR and between 35eaa96 and f637d86.

📒 Files selected for processing (3)
  • terminal/src/main/java/org/jline/utils/DiffHelper.java
  • terminal/src/main/java/org/jline/utils/Display.java
  • terminal/src/test/java/org/jline/utils/DisplayTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • terminal/src/main/java/org/jline/utils/DiffHelper.java

@Elec332
Copy link
Copy Markdown
Contributor

Elec332 commented Apr 14, 2026

Is there a specific reason why AttributedCharSequence#toAnsiBytes()'s state of long[4] is defined as
foreground [0], background [1], style [2], and alt charset [3]?

Considering that color information is included in the style, it could be simplified to style [0], and alt charset [1].

To then avoid checking colors at all (bypassing most of that function) you could move all color attributes t0 the upper 13 nibbles of the state. By then only checking the upper 13 nibbles of the old and new state all the color checks could often be bypassed.

This would require the "hidden" state to be moved down a nibble, and the color "set" states to be moved up a nibble.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
terminal/src/main/java/org/jline/utils/AttributedCharSequence.java (1)

953-968: Minor Javadoc inaccuracy: method still allocates iterators.

The Javadoc states this is an "allocation-free alternative" but the method allocates a BreakIterator and CharSequenceCharacterIterator on each call. The allocation-free path is the package-private overload that accepts pre-allocated iterators.

Consider updating the Javadoc to clarify that this avoids substring allocation, not all allocation:

📝 Suggested Javadoc clarification
     /**
-     * Returns the display width in columns for a range of this attributed string.
-     *
-     * <p>This is an allocation-free alternative to creating a subSequence and calling
-     * {`@link` `#columnLength`(Terminal)} on it.</p>
+     * Returns the display width in columns for a range of this attributed string
+     * without creating a subSequence.
      *
      * `@param` terminal   the terminal to query for grapheme cluster mode, or {`@code` null}
      * `@param` rangeStart start index in this sequence (inclusive)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terminal/src/main/java/org/jline/utils/AttributedCharSequence.java` around
lines 953 - 968, Update the Javadoc for
AttributedCharSequence.columnLength(Terminal, int, int) to remove the claim that
it is "allocation-free" and instead state that it avoids creating a substring
but still allocates helper iterators (e.g., BreakIterator and
WCWidth.CharSequenceCharacterIterator); mention that the truly allocation-free
path is the package-private overload columnLength(Terminal, BreakIterator,
CharSequenceCharacterIterator, int, int) which accepts pre-allocated iterators.
Ensure the text references BreakIterator and
WCWidth.CharSequenceCharacterIterator so readers know which allocations occur
and which overload to use to avoid them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@terminal/src/main/java/org/jline/utils/AttributedCharSequence.java`:
- Around line 953-968: Update the Javadoc for
AttributedCharSequence.columnLength(Terminal, int, int) to remove the claim that
it is "allocation-free" and instead state that it avoids creating a substring
but still allocates helper iterators (e.g., BreakIterator and
WCWidth.CharSequenceCharacterIterator); mention that the truly allocation-free
path is the package-private overload columnLength(Terminal, BreakIterator,
CharSequenceCharacterIterator, int, int) which accepts pre-allocated iterators.
Ensure the text references BreakIterator and
WCWidth.CharSequenceCharacterIterator so readers know which allocations occur
and which overload to use to avoid them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a8d4b34-61fd-4122-ae0c-310989b1b34a

📥 Commits

Reviewing files that changed from the base of the PR and between d4a85c8 and 18a421f.

📒 Files selected for processing (2)
  • terminal/src/main/java/org/jline/utils/AttributedCharSequence.java
  • terminal/src/main/java/org/jline/utils/Display.java

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@terminal/src/main/java/org/jline/utils/DiffHelper.java`:
- Around line 155-170: In commonSuffixLength(AttributedString text1, int e1,
AttributedString text2, int e2, int n) the unconditional rollback to
startHiddenRange when the scan ends in a hidden run is too aggressive; change
the final rollback so it only happens if the mismatch that terminated the loop
is itself inside a hidden region in both strings (e.g., check text1.isHidden(e1
- commonEnd - 1) && text2.isHidden(e2 - commonEnd - 1) before assigning
commonEnd = startHiddenRange), leaving bona fide equal visible suffixes intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f6c1d3a-6cc5-45dc-92b8-50f4806ba831

📥 Commits

Reviewing files that changed from the base of the PR and between 8b6a4de and fc1b79a.

📒 Files selected for processing (1)
  • terminal/src/main/java/org/jline/utils/DiffHelper.java

Comment thread terminal/src/main/java/org/jline/utils/DiffHelper.java
@gnodet gnodet changed the title perf: optimize Display.update for zero allocation, minimal output bytes perf: optimize Display.update for minimal allocation and output bytes Apr 15, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@terminal/src/test/java/org/jline/utils/DisplayBenchmarkTest.java`:
- Around line 19-22: The documentation comment in DisplayBenchmarkTest (class
DisplayBenchmarkTest) has a typo in the Maven run command; update the comment
text to replace "./mvx mvn test -B -pl terminal -Dtest=DisplayBenchmarkTest"
with the correct "./mvnw mvn test -B -pl terminal -Dtest=DisplayBenchmarkTest"
(i.e., change "./mvx" to "./mvnw") so the run instructions are accurate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4bdf71a8-d22b-412c-8416-e3093b1233a8

📥 Commits

Reviewing files that changed from the base of the PR and between 10862ef and 809e2b0.

📒 Files selected for processing (9)
  • terminal/src/main/java/org/jline/utils/AttributedCharSequence.java
  • terminal/src/main/java/org/jline/utils/AttributedString.java
  • terminal/src/main/java/org/jline/utils/AttributedStringBuilder.java
  • terminal/src/main/java/org/jline/utils/DiffHelper.java
  • terminal/src/main/java/org/jline/utils/Display.java
  • terminal/src/main/java/org/jline/utils/WCWidth.java
  • terminal/src/test/java/org/jline/utils/ByteArrayBuilderTest.java
  • terminal/src/test/java/org/jline/utils/DisplayBenchmarkTest.java
  • terminal/src/test/java/org/jline/utils/DisplayTest.java
✅ Files skipped from review due to trivial changes (1)
  • terminal/src/test/java/org/jline/utils/ByteArrayBuilderTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • terminal/src/main/java/org/jline/utils/AttributedString.java
  • terminal/src/main/java/org/jline/utils/DiffHelper.java
  • terminal/src/main/java/org/jline/utils/AttributedCharSequence.java

Comment on lines +19 to +22
/**
* Micro-benchmark for Display.update to measure throughput.
* Run with: ./mvx mvn test -B -pl terminal -Dtest=DisplayBenchmarkTest
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in documentation comment.

The run command contains a typo: ./mvx should be ./mvnw (Maven wrapper).

📝 Suggested fix
-* Run with: ./mvx mvn test -B -pl terminal -Dtest=DisplayBenchmarkTest
+* Run with: ./mvnw test -B -pl terminal -Dtest=DisplayBenchmarkTest
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Micro-benchmark for Display.update to measure throughput.
* Run with: ./mvx mvn test -B -pl terminal -Dtest=DisplayBenchmarkTest
*/
/**
* Micro-benchmark for Display.update to measure throughput.
* Run with: ./mvnw test -B -pl terminal -Dtest=DisplayBenchmarkTest
*/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@terminal/src/test/java/org/jline/utils/DisplayBenchmarkTest.java` around
lines 19 - 22, The documentation comment in DisplayBenchmarkTest (class
DisplayBenchmarkTest) has a typo in the Maven run command; update the comment
text to replace "./mvx mvn test -B -pl terminal -Dtest=DisplayBenchmarkTest"
with the correct "./mvnw mvn test -B -pl terminal -Dtest=DisplayBenchmarkTest"
(i.e., change "./mvx" to "./mvnw") so the run instructions are accurate.

@gnodet gnodet added this to the 4.1.0 milestone Apr 15, 2026
Key optimizations in Display.update():
- Intra-line skip: when a row has a long unchanged gap between two
  changed regions, emit a cursor-movement sequence instead of
  re-sending the unchanged characters, reducing output bytes
- Accept AttributedCharSequence (not just AttributedString) to avoid
  per-frame snapshot copies when the caller already holds an immutable
  sequence
- Fast-path for List<AttributedString> input to skip snapshotLines
  overhead
- Cell-grid update API: flat char[]/long[] arrays with row-equality
  check that reuses oldLines entries for unchanged rows

Allocation reduction in AttributedCharSequence:
- Consolidate toAnsiBytes state from long[4] to long[2] (style +
  altCharset flag)
- Color fast-path in emitStyleChange(): skip color extraction when
  only text decoration attributes changed
- Deduplicate columnLength overloads into a delegation chain using
  bare BreakIterator + CharSequenceCharacterIterator to avoid
  toString() allocation

DiffHelper improvements:
- Deduplicate public diff() to delegate to allocation-free variant
- Fix commonSuffixLength unconditional hidden-range rollback: make
  rollback conditional on whether the next character is actually
  hidden, matching the prefix logic

Other:
- Add CharSequenceCharacterIterator and resetGraphemeBreakIterator
  to WCWidth for allocation-free BreakIterator usage
- Add @SInCE 4.1.0 tags to new public API methods
- Add DisplayBenchmarkTest for update throughput measurement
- Add tests for intra-line skip, cell-grid API, and
  AttributedCharSequence input
@gnodet gnodet merged commit c4decc6 into master Apr 15, 2026
9 checks passed
@gnodet gnodet deleted the ripe-deal branch April 15, 2026 07:44
@sonarqubecloud
Copy link
Copy Markdown

gnodet added a commit to gnodet/jline3 that referenced this pull request Apr 16, 2026
Remove public methods and classes added in the recent optimization
PRs (jline#1746, jline#1785) that have no callers within JLine:

- Make ByteArrayBuilder package-private (only used by Display and
  AttributedCharSequence internally)
- Remove Display.updateSequences() overloads and
  Display.update(char[], long[], ...) cell-grid method
- Remove AttributedCharSequence.columnLength(Terminal, int, int)
  public 3-arg overload (internal 5-arg version is still used)
- Remove Size(Size) copy constructor (Size.copy() already exists)
- Remove spurious (short) casts in Size setters that silently
  truncated values while storing into int fields
gnodet added a commit to gnodet/jline3 that referenced this pull request Apr 16, 2026
Remove public methods and classes added in the recent optimization
PRs (jline#1746, jline#1785) that have no callers within JLine:

- Make ByteArrayBuilder package-private (only used by Display and
  AttributedCharSequence internally)
- Remove Display.updateSequences() overloads and
  Display.update(char[], long[], ...) cell-grid method
- Remove AttributedCharSequence.columnLength(Terminal, int, int)
  public 3-arg overload (internal 5-arg version is still used)
- Remove Size(Size) copy constructor (Size.copy() already exists)
gnodet added a commit that referenced this pull request Apr 16, 2026
Remove public methods and classes added in the recent optimization
PRs (#1746, #1785) that have no callers within JLine:

- Make ByteArrayBuilder package-private (only used by Display and
  AttributedCharSequence internally)
- Remove Display.updateSequences() overloads and
  Display.update(char[], long[], ...) cell-grid method
- Remove AttributedCharSequence.columnLength(Terminal, int, int)
  public 3-arg overload (internal 5-arg version is still used)
- Remove Size(Size) copy constructor (Size.copy() already exists)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants