Skip to content

LAB-1803: Trim styled blanks in vt scrollback - #15

Open
cweill wants to merge 2 commits into
mainfrom
LAB-1803-trim-styled-scrollback
Open

LAB-1803: Trim styled blanks in vt scrollback#15
cweill wants to merge 2 commits into
mainfrom
LAB-1803-trim-styled-scrollback

Conversation

@cweill

@cweill cweill commented May 13, 2026

Copy link
Copy Markdown

Motivation

Amux LAB-1803 heap profiles showed vt.cloneLineInto retaining the majority of server heap when scrollback contains full-width styled blank padding. The retained bytes were under the vt scrollback ring push path, so trimming scrollback rows before cloning reduces retained cells without changing the public API.

Summary

  • Treat trailing non-continuation blank cells as scrollback-trimmable even when they carry style attributes.
  • Preserve width-0 continuation cells so wide-character layout state is not discarded.
  • Add a regression test for styled blank scrollback rows.

Testing

  • go test -run 'TestScrollback/trims_trailing_styled_blanks' -count=100
  • go test -run TestScrollback -count=1
  • go test ./...

Review focus

  • The scrollback-only blank definition for trailing cells with Content == "" or Content == " ".
  • Confirming wide-character continuation cells remain preserved.

Refs LAB-1803.

@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown

Claude finished @cweill's task in 2m 10s —— View job


PR Review: LAB-1803: Trim styled blanks in vt scrollback

  • Gather context (read changed files)
  • Review logic correctness and edge cases
  • Post review findings

Summary

The change extracts isTrailingScrollbackBlank from the inline condition in trimTrailingEmptyCells and extends it to treat cells with blank content ("" or " ") as trimmable even when they carry style attributes, while explicitly preserving width-0 continuation cells.

The logic is correct. A few observations below, none blocking.


Observations

1. Continuation cell test is missing (non-blocking)

The PR description calls out "Preserve width-0 continuation cells" as a stated invariant, but there is no test exercising it. A line like [wide_char(w=2), cont(w=0), styled_blank(w=1), styled_blank(w=1)] should trim to [wide_char(w=2), cont(w=0)]. Given the PR explicitly mentions this guarantee, a test covering it would match the stated scope.

Add this test →

2. Nil check in isTrailingScrollbackBlank is unreachable from trimTrailingEmptyCells (non-blocking)

trimTrailingEmptyCells always calls isTrailingScrollbackBlank(&line[i]) where line[i] is a value, so the pointer is never nil. The nil guard is harmless and defensive — no action needed, just noting it isn't exercised.

3. Styled-space trimming is broader than background-only (intentional, non-blocking)

The check cell.Content == "" || cell.Content == " " trims any styled trailing space — including those with foreground color, bold, etc. The PR description frames the motivating case as background-fill padding, but the trim is actually style-agnostic. This is almost certainly the right call (foreground-only trailing spaces have no practical use in terminal output), and it makes the function simpler. Worth being aware of if this is ever revisited.

4. Width > 1 trailing cells with blank content are trimmed (correct behavior)

A full-width space (e.g., Width=2, Content=" ") at the end of a line without a following continuation cell would be trimmed. This is intentional and correct.


LGTM

@greptile-apps

greptile-apps Bot commented May 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR narrows the definition of a "trimmable" trailing cell in the scrollback ring: previously only zero-value cells or EmptyCell were stripped; now any cell with Width > 0 and Content == \"\" or Content == \" \" (regardless of style) is also trimmed, while Width == 0 continuation cells are explicitly preserved.

  • isTrailingScrollbackBlank replaces the inline two-condition check in trimTrailingEmptyCells, adding content-based detection for styled blank cells and a guard for wide-character continuation cells.
  • A regression test is added that pushes a line of background-styled space cells with a "prompt" prefix and asserts both the trimmed length and the textual content.

Confidence Score: 4/5

Safe to merge; the change is tightly scoped to the scrollback trim path and does not touch the live screen buffer or any public API.

The core logic is correct and the regression test exercises the new path directly. The dead nil guard and the narrower-than-described Width==0 protection do not affect correctness or introduce regressions.

vt/scrollback.go — the ordering of guards in isTrailingScrollbackBlank is worth a second look to confirm zero-value continuation cells at trailing positions are handled as intended.

Important Files Changed

Filename Overview
vt/scrollback.go Refactors trimTrailingEmptyCells to delegate to a new isTrailingScrollbackBlank helper; adds content-based blank detection (Content=="" or ==" ") while protecting Width==0 continuation cells. The nil guard in the helper is unreachable dead code; the IsZero() check short-circuits before the Width==0 guard for zero-value continuation cells, matching pre-existing behaviour, while the Width==0 path correctly protects non-zero (e.g. styled) continuation cells.
vt/scrollback_test.go Adds a targeted regression test for the new styled-blank trimming path; correctly constructs a 12-cell line with background-coloured blanks, writes "prompt" into the leading cells, and asserts both the trimmed length and textual content. No issues found.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["trimTrailingEmptyCells(line)"] --> B["Scan from right to left"]
    B --> C{"isTrailingScrollbackBlank(cell)"}
    C --> D{"cell == nil?"}
    D -- yes --> E["return true (blank)"]
    D -- no --> F{"IsZero() or Equal(EmptyCell)?"}
    F -- yes --> G["return true (blank)"]
    F -- no --> H{"cell.Width == 0?"}
    H -- yes --> I["return false (preserve continuation)"]
    H -- no --> J{"Content == '' or ' '?"}
    J -- yes --> K["return true (styled blank — NEW)"]
    J -- no --> L["return false (non-blank content)"]
    C -- blank --> M["continue scan leftward"]
    C -- non-blank --> N["set lastNonEmpty = i, break"]
    N --> O["return line[:lastNonEmpty+1]"]
Loading

Reviews (1): Last reviewed commit: "LAB-1803: Trim trailing styled blanks in..." | Re-trigger Greptile

Comment thread vt/scrollback.go
Comment on lines +127 to +131
func isTrailingScrollbackBlank(cell *uv.Cell) bool {
if cell == nil {
return true
}
if cell.IsZero() || cell.Equal(&uv.EmptyCell) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The nil check here is dead code. isTrailingScrollbackBlank is only ever called with &line[i] inside trimTrailingEmptyCells, where i is a valid slice index, so the pointer is always non-nil. The branch can be removed without any behavioural change.

Suggested change
func isTrailingScrollbackBlank(cell *uv.Cell) bool {
if cell == nil {
return true
}
if cell.IsZero() || cell.Equal(&uv.EmptyCell) {
func isTrailingScrollbackBlank(cell *uv.Cell) bool {
if cell.IsZero() || cell.Equal(&uv.EmptyCell) {

Comment thread vt/scrollback.go
Comment on lines +126 to +133

func isTrailingScrollbackBlank(cell *uv.Cell) bool {
if cell == nil {
return true
}
if cell.IsZero() || cell.Equal(&uv.EmptyCell) {
return true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Width == 0 guard silently unreachable for zero-value continuation cells

The PR description states "Preserve width-0 continuation cells so wide-character layout state is not discarded," but a zero-value continuation cell (Width: 0, Content: "", Style: {}) satisfies IsZero() and returns true before the Width == 0 guard is ever reached. Only a non-zero continuation cell (e.g. one carrying inherited style) is actually protected. A trailing zero-value continuation cell was already trimmed before this PR, so there is no regression, but the protection is narrower than the PR description implies. Worth a clarifying code comment if zero-value continuations at trailing positions are expected to be preserved too.

cweill added a commit to weill-labs/amux that referenced this pull request May 13, 2026
## Motivation

LAB-1803's live heap profile reproduced the reported post-LAB-1750
shape. During verification on 2026-05-13, a fresh server heap profile
for the installed `d266004` build showed 676.69 MB in-use with
`vt.cloneLineInto` retaining 411.63 MB / 60.83%; the issue artifact from
2026-05-13 showed the same post-LAB-1750 shape at 507.70 MB with
`cloneLineInto` at 298.41 MB / 58.78%. A pre-LAB-1750 heap artifact from
2026-05-12 showed 300.18 MB total with `cloneLineInto` at 91.16 MB /
30.37%.

`go tool pprof -peek cloneLineInto` pointed at
`vt.(*scrollbackRing).push`, not an amux snapshot cache, so the fix
targets the vt scrollback clone input instead of adding amux-side
memoization.

## Summary

- Add a resize-preservation benchmark that writes full-width styled
scrollback rows before shrinking and widening the emulator.
- Pin `github.com/charmbracelet/x/vt` to weill-labs/x commit
`fc372e8574ca`, which trims trailing styled blank cells before cloning
rows into scrollback.
- Keep `cloneLineInto`'s API unchanged; the fork-side change is tracked
in weill-labs/x#15.
- LAB-1794 is already fixed on fork `main` by `6adbf8184605`, so this PR
pins a later fork commit rather than cherry-picking the soft-wrap API
restoration here.

## Baseline numbers

Hardware: AMD EPYC-Milan Processor, Linux amd64.

| Measurement | Before | After |
| --- | ---: | ---: |
| `BenchmarkVTEmulatorResizePreservationStyledScrollback` bytes/op |
~14.00 MB/op | ~10.16 MB/op |
| `BenchmarkVTEmulatorResizePreservationStyledScrollback` allocs/op |
1518-1519 | 1506-1507 |
| Styled workload heap in-use | 112.13 MB | 48.10 MB |
| Styled workload `vt.cloneLineInto` in-use | 67.62 MB / 60.30% | 5.00
MB / 10.41% |

## Testing

- `go test ./internal/mux -run '^$' -bench
BenchmarkVTEmulatorResizePreservationStyledScrollback -benchmem
-count=5`
- `go test ./internal/mux -run
'TestRenderWithCursorRoundTripPreserves(SoftWrapForResize|WrappedTrailingSpaces|BlankWrappedSpacesAtCursor|BlankWrappedRowsBeforeHardNewline|PhantomCursorForResize)|TestVTEmulatorResizeShrink'
-count=100`
- `go test -race ./internal/mux -timeout 120s -count=10`
- `go test ./... -timeout 180s`
- `go test ./test -run '^TestSwapForward$' -timeout 120s -count=1` after
one `go test ./... -timeout 120s` run hit that integration timing flake.
- In `weill-labs/x/vt`: `go test -run
'TestScrollback/trims_trailing_styled_blanks' -count=100`
- In `weill-labs/x/vt`: `go test -run TestScrollback -count=1`
- In `weill-labs/x/vt`: `go test ./...`
- `go test -race ./... -timeout 120s -count=10` was attempted and
currently fails outside the touched package set in timing-heavy
tests/packages (`internal/cli`, `internal/client`, `internal/dialutil`,
`internal/remote`, `internal/server`, and `test`). `internal/mux` passed
the same race/count loop.

## Review focus

- Whether the fork-side scrollback trim semantics are the right layer
versus amux memoization; profiles showed retained bytes in vt scrollback
ring storage.
- Confirming LAB-1750 preserve-output behavior stays intact; the
preserve-output regression tests re-pass with `-count=100`.
- Whether to merge weill-labs/x#15 first and
then update this PR to the merged fork commit.

Closes LAB-1803.

---------

Co-authored-by: Orca worker-02 <worker-02@orca.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant