Skip to content

replace _combined map and string cache with _chars variable in LogicalLine - #6160

Open
PerBothner wants to merge 52 commits into
xtermjs:masterfrom
PerBothner:LineChars
Open

PerBothner wants to merge 52 commits into
xtermjs:masterfrom
PerBothner:LineChars

Conversation

@PerBothner

Copy link
Copy Markdown
Contributor

[This is a work-in-progress - there are a number of testsuite failures that I haven't debugged yet.]

This is an unfinished implementation of the _chars proposal from this comment, which is copied/edited below.

This builds on the LogicalLine PR #5797.

Quick summary: add a new field _chars to LogicalLine which replaces both the _combined map and the string cache.

The idea is that each cell is in one of two modes (see the comment before enum Content in common/buffer/Constants.ts): Either the existing way (a codepoint as a 21-bit value in the _data array), or as substring of the _chars string. See getString in LogicalLine. All combined characters would use the latter variant. But so do all other characters after translateToString is called, which also sets the _charsIsTextValue of the LogicalLine. If that property is true, all cells use the substring-of-_chars representations and all the substrings are in order. Thus translateToString can trivially and safely return the _chars string. If translateToString is called when _charsIsTextValue is false, we do basically the same computation as the current implementation (using a StringBuilder). So _chars functions as a cache for translateToString, and _charsIsTextValue indicates if the cache is valid. There is no need to trim the string cache because the amount of space for the _chars string is quite modest and I believe more than made up by removing the _combined map and StringCacheEntry, simpler data structures, simpler gc (no need for a bunch of expensive WeakRefs), and better memory locality.

@jerch mentioned earlier a concern that using a string for cell character data is very expensive in the presense of updates. But this hybrid approach avoids that issue: We generally don't update the _chars string during normal writes. (A combined characher is handled by just appending at the end of _chars.) However, the _chars array gets normalized when the line is rendered or translateToString is called for other reasons.

Instead to change isWrapped state use a Buffer.setWrapped method.
This allows for potential flexibility in how BufferLine
and line-wrapping are implemented.
A BufferLine is now the sub-range of a LogicalLine for a specific visible line,
while LogicalLine is independent of window width.
This is used by InputHandler.print to "batch" multiple characters.
This typically reduces memory usage, allocation, and copying.
Also restored CircularList.recycle, pending possible future use -
though the initial attempt actually slowed things down.
Seems slightly faster to recycle BufferLine but not LogicalLine.
The image addon now builds but has some problems still.
I think this is slightly cleaner and possibly faster, since no cloning needed.
(Still a good chunk to go.)
The CellData_string field is now only valid if STORED_IN_CHARS_MASK is set.
We don't cache the result of getChars, since then we would need to
invalidate the cache, which would require more code changes.
… addon-search

The only time the argument is non-true is in one test-case.
This removes one place where translateToString is called with trimRight false/
Added new offsetToString function in LogicalLine.
Also, by-pass public API to avoid allocating BufferLineApiView.
Instead, have BufferLine implement IBufferApi by adding a getLine method.
Also, add startColumn to IBufferLine.
It is no longer needed: We just use the asString method from ILogicalLine.
That method now caches the result inexpensively.
@PerBothner

Copy link
Copy Markdown
Contributor Author

Using this change I was able to optimize and simplify the search addon. The asString method in LogicalLine avoids the need for concatenating the results of translateToString on multiple wrapped line fragments. And the _chars variable in LogicalLine provides a simple and efficient cache for the asString result. Therefore there is no longer any value in having the search addon manage its own SearchLineCache.

Further improvements are probably feasible. For example, I suspect the _stringLengthToBufferSize function can be opimized easily. However, it is only called on a sucessful match, so isn't as critical.

This change has been checked into the LineChars branch.

This fixes a testcase with a wide char shifted right.
Combine reflowLargerCreateNewLayout and reflowLargerApplyNewLayout
into a single _reflowLargerNewLayout function. This avoids an
extra pass over the lines and an extra array allocation.

Also, merge in the rest of BufferReflow.ts into Buffer.ts - it was
weird having some of the reflow logic in Buffer.ts and some in
a separate module.
The LogicalLine implementation does not support trimmable (final)
null characters with attributes (except background), since
that can never be created using escape sequences,
Add optional attrs parameter.
Tweak handling of no pre-existing character in cell.
This reduces risk of confusion with BufferLine length property.
@PerBothner PerBothner changed the title WIP: replace _combined map and string cache with _chars variable replace _combined map and string cache with _chars variable in LogicalLine Oct 7, 2026
@PerBothner

Copy link
Copy Markdown
Contributor Author

I removed the "WIP" (work-in-progress) marker. All the tests pass and the functionality appears to be complete. The search addon has been simplified and optimized greatly.

Performance-wise (with the simple Terminal.benchmark.js) there doesn't seem to be any definite regression compared to the LogicalLine branch (PR #5797) on which this is based on. (Though LogicalLine does show some slowdown compared to master - more on that later.) The simplification and speedup of translateToString and especially the search addon makes that worthwhile.

@PerBothner

Copy link
Copy Markdown
Contributor Author

Sorry for creating a such a large change-set to review, but given the lack of reviews and feedback I have no choice but to combine all my "recommended changes" into a single large PR: I cannot juggle and keep updated a large number of change-sets, some of which are independent and some of which depend on each other. I do have some smaller PRs that have been merged into this PR. I will keep them open, but I will not be keeping them updated or resolve conflicts unless explicitly requested.

@PerBothner

Copy link
Copy Markdown
Contributor Author

Summary of status of changes:

  • A new LogicalLine class which owns the cell data, which is not modified on line resizing. Each BufferLine is basically a slice (sub-set) of a LogicalLine.

  • As an optimization, the "logical" _data array of a LogicalLine is actually a slice. (This may not be worth the extra complexity.)

  • The LogicalLine trimmedLength property is the count of "active" character cells, ignoring trimmed nulls at end of the (logical) line. Only that number of cell are valid and stored in _data. (Conceptionally, it is followed by an infinite number of nulls.) Cells after trimmedLength have no foreground or other attributes, except a background color. This is set by "background color erase" (BCE); using an end-of-line background color make BCE work on window re-size.

  • Reflow is much simpler and faster, since it doesn't need to modify or copy _data. The BufferReflow.ts file is gone - the functionality is either merged into Buffer.ts or no longer needed.

  • A LogicalLine has a new _chars string property which does double duty for the combinedData and the translateToString cache. A cell may contain either a code-point or specify a sub-string of the _chars_ array. The LogicalLine asString function returns the _chars string if it valid, or re-calculates it; translateToString returns a sub-string of the asString result. The _chars string never needs to be invalidated unless the _data is modified (which it never is for lines above ybase).

  • The search addon does not need its own cache - the LogicalLine asString value gives as what we need cheaply. (The search addon could probably be optimized even more.)

  • The isWrapped property is read-only. To modify it you need to call the Buffer.setWrapped method. This is because of the need to split/join LogicalLine objects. See PR Refactor so isWrapped isn't public writeable #5788 and also PR #

  • InputHandler.print merges multiple setCellFromCodepoint calls with a single setCellsFromCodepoints.

  • Some tests had to be modified because they depended on combinations of state that the library used to handle but could not arise in real use. For example invalid character-width values.

  • The BufferApiView class is optimized - `BufferLineApiView is removed. See PR Speed up BufferApiView.getLine and remove BufferLineApiView #6193.

  • This PR currently does not change the Marker implementation as in PR attach markers and decorations to LogicalLine #5853; that may be added later.

I will write up some performance notes later.

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