Skip to content

Proposal: more compact and flexible BufferLine data structure #4800

Open
@PerBothner

Description

@PerBothner

See this branch for a prototype of this proposal. The prototype is not usable: a lot of things work; a lot don't.

The BufferLine data structure contains a _data field, which is a Uint32Array with 3 elements per column. This makes for fast O(1) mapping from column number to character/cell information, but it has anumber of disadvantages:

  • It is not memory-effecient.

  • Convoluted encoding due to squeezing attribute values into available bits in the foreground/background elements.

  • Tied to a terminal model with an integral number of fixed-width cells. Supports double-width characters and grapheme clusters (somewhat clumsily), but no variable-width fonts, or any glyphs whose width isn't an integral number of cells. Many languages don't work well with fixed-width characters. For other languages being forced to use fixed-width character is unaestheic or unfriendly. (This includes to some extent English.) (Support for variable-width fonts is not part of the current proposal, but I do have some ideas for what can be done.)

  • Limited extensibility: There aren't a lot of available bits left, and there is no room for properties that require more than a few bits. Anything that doesn't fit has to be added the the extended-attribute object, which is more expensive.

  • Clumsy/expensive reflow when window width changes.

Proposal

Summary

The main fields of a BufferLine become:

  • A _text string that contains all the characters in the line. This is the concatenation of all the content code (for simple characters), and the _combined elements, and subsumes both.

  • The _data array is a combination of cell runs (represented as lengths in the _text string), and bg/fg/attribute values. Each element is a 4-bit "kind" (for example SKIP_COLUMNS for "null" columns), and 28 bits of data (such as a color value or the length of a text run).
    The _dataLength field contains the "current" (active) length of the _data array, to allow for future insertions.
    The _data array only needs as many elements as actually used, though pre-allocating extra space for "growth" is typically worthwhile.

In the prototype, a "run" is either one or more non-composed BMP characters of the same width; a run of "null" columns; or a single "other" glyph (a cluster or a non-BMP character). If the number columns spanned by the _data elements before _dataLength, an implicit SKIP_COLUMNS represents the rest of the line.

A CellData contains the current index into the _data and _text arrays. Since a single _data element can represent a run of multiple columns (when all characters are BMP and the same width) the CellData also maintains a column-offset relative to the start of the current _data element.

The CellData also contains the bg/fg/attribute state at the current position.

Space efficiency

The _data array contains elements for changes in bh/fg/attribute value, and for "runs" of text. This is much more efficient than the current _data array.

Each non-null character is just one or 2 16-bit code units in the _text string, which is as efficient as you can get.

InputHandler efficiency

In the prototype, most editing operations make use of a CellData object that represents the current position in the BufferLine to edit. Given an appropriate CellData, the actual editing operation is comparable in complexity to the existing implementation: The logic is sometimes slightly more complicated, but the amount of work is comparable: either fixed, or proportional to the number of following runs (if elements have to be inserted or deleted). Bulk operations (which are most performance-critical) tend to be at the end of a line, so usually you're modifying the last element or two in the _data array or appending to the end of it.

However, setting the fields of a CellData to the correct values representing an arbitrary column position is no longer O(1), but is proportional to the number of "runs" between the start of the line and the desired position. This is an obvious potential performance regression.

Luckily, most output is sequential, not random-access. This is especially the case non-interactive "bulk" output.

The InputHandler maintains a _workCell that when valid represents the current state of the current position. In the current implementation, the _workCell state is initialized at the start of each print call. A natural optimization is to assume if the _workCell is "valid" it can be used without initialization. Escape sequences that move the cursor will usually need to invalidate the _workCell, but plain text and attribute changes do not.

Another possible performance concern is updating the _text string. In the prototype is this done separately for each character, but it can obviously be "chunked" to larger units.

Rendering efficiency

Rendering does not require random access: Currently each renderer reads each line in sequential order, so using a CellData as an iterator is straightforward and efficient.

Efficiency of selection, serialization and other operations

Other operations generally work with with sequential access, or they are not performance-critical.

Possible refinements

The bg/fg/attribute could be the xor'd values of the corresponding previous values. This would make backwards traversal efficient.

While using a _text array to contain both simple characters and clusters is very compact, there is some costs in terms of mapping codeunits to strings, and appending the new strings to the _text string. (On the other hand, operations where string values are needed may be faster than currently, especially where runs of characters are desired (as in the dom renderer) because extracting a substring
from the _text string is probably relatively inexpensive.) An alternative is to store the codeunits in the _data array, as in the current implementation.

Line overflow and reflow

Terminology: A (visible) "row" is the text/data on a single row in the terminal. A (logical) "line" is one or more rows which "wrap" into each other.

A tempting follow-up change is for all the rows belonging to the same line to share the _data array and the _text array. This means neither _data or _text change when text is re-flowed. We just add or remove row objects.

A possible data structure is to use a plain BufferLine for a line (including the initial rows), and a sub-class BufferOverflowLine for each wrapped (non-initial) row. Each BufferOverflowLine points back to the parent BufferLine. The BufferOverflowLine also contains the state needed to efficiently initialize a CellData at the start of that row.

A reflow operations needs to add or remove BufferOverflowLine children of a parent BufferLine.

A bonus is cleaner separation between the content "model" (a table of lines with _data and _text properties) versus the "view" in a specific viewport (a table of rows).

Different visible and logical row width

Consider a REPL: While typing an input line, you change the line width, either by resizing the viewport or changing the zoom. The terminal send the new line width to the application, which sends a sequence to re-draw the input line with the new width. But by the time the terminal can update itself, there may be a further re-sizing. This can result in a garbled display.

A solution: The terminal does not send the window-resize request to the application. Instead it does a local reflow, just as it would do with old output lines. The tricky part is that the terminal must interpret escape sequences from the application using the old width (since that is what the application believes to be the current width).

The implementation isn't terribly difficult: create aBufferOverflowLine set based on the actual (visible) line width, and a different set based on the logical (application) line width. Basically you add a flag "use this BufferOverFlowLine during rendering but ignore it during input-handling" and a converse flag "use this during input-handling but ignore it during rendering".

This behavior can be controlled by shell-integration escape sequences: A prompt-end escape sequence turns off window-size change reporting and enables tracking separate visible and logical line width. An input-end (command-start) sequence returns to normal.

The same logic can be used to support variable-width fonts in prompts and command input: The application assumes normal fixed-width characters, and works with the logical width, while the renderer displays a user-preferred variable-width font, wrapping lines based on the actual width. This behavior should be opt-in: It can be enabled by a flag in the shell-integrate escape. This can be set in a user configuration file, but does not require modifying the actual application.

AbstractBufferLine

The prototype has an AbstractBufferLine class that implements IBufferLine and is the parent of BufferLine (and BufferOverflowLine). The intention is that we might add new sub-classes for "sections" that aren't normal lines. For example, images or raw (but safety-scrubbed) HTML <div> elements. We may also support lines that have different heights.

Esoteric line types are not part of part of this proposal, though they motivate some design decisions.

Rich output: HTML, SVG, and images

As an example the gnuplot graphing program has an interactive mode where the output of each command can display a graph in various formats, including SVG. If the gnuplot terminal type is "domterm", it generates SVG, wraps it an some simple escape sequences, and DomTerm displays below the input command.

Another example is a math program emitting MathML which is displayed in the same REPL console as terminal output.

Note this is similar to ipython/Jupyter, but directly in the terminal. This you can mix rich output with full terminal support.

CellData is concrete

Conceptually, the CellData implementation depends on the AbstractBufferLine implementation. However, we would like to pre-allocate CellData helper objects, and re-use the CellData instance for many lines, regardless of whether the line is a regular BufferLine or something else. Hence CellData has various fields with unspecified meaning: _stateM, _stateN, _stateA, _stateB. These are available for any AbstractBufferLine implementation to use as it sees fit. For example _stateM and _stateN are the current indexes in the _data and _text arrays,

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/proposalA proposal that needs some discussion before proceeding

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions