-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: more compact and flexible BufferLine data structure #4800
Comments
Looks good overall. Is it possible to split this into smaller chunks of work that can be reviewed and merged separately? The smaller a PR is the faster it is to understand/test/merge.
If we keep the text as raw text I have concerns that the JS engine may not be able to keep some optimizations compared to an This representation may also be more difficult to share memory between workers when we pursue #1515
Something I'm not clear on is how columns get mapped to indexes in
It would be much easier to merge/iterate on this if we can have both implementations side-by-side where you can enable the new one via
FYI we had some performance problems with iterators in the decoration code and ended up reverting to callbacks instead of iterators for hot code paths. |
Indexes in |
If you don't care about performance during a transition phase, it is probably feasible to implement the new data structure while still using the old API. It should also be possible to use two different implementation classes - old and new. However, things that are currently O(1) will be much slower if using the new implementation and the old API, due to the need for many functions to allocate a local One could mitigate this somewhat with conditional logic, at least in the most performance-critical places:
Supporting both data structures and both APIs will of course have some cost (performance and complexity), if nothing else testing |
@PerBothner we do care about performance during transition, if it's just a few if statements that shouldn't impact much. Ease of merging is a big part but probably the biggest is reducing risk when shipping this to vscode's users. If there are separate implementations we can toggle between, we can make the new one opt-in and when we think it's ready turn it opt-out in Insiders only (the beta build) to get a lot of low risk testing. |
I have really big concerns regarding the (I really dont get why JS has no easy string <--> typed array interface, but due to the lack of that, we have to go with one in the end internally, and I think strings are not the better way here for the reason above) |
Seems like the fundamental ideas can remain, just with _test transformed into a typed array managed by us where each character can take up multiple bytes. |
I edited the proposal to add a new "Different visible and logical row width" section. This isn't directly part of the proposal, but it's an example of a useful improvement enabled by the proposal. I also fixed a few minor typos I noticed. |
One possibility is to include the code units of the text directly in the I haven't really thought about how this would change code complexity or performance. |
If we remove the
|
@PerBothner wouldn't including the text data in |
We already (in my prototype) have to shift things around if text is changed in the middle of a line. The simple case where you replace some characters by some other characters (like updating an existing field in a form) can often be done in-place without shifting. The more complex cases (such as editing a Fish command line with syntax coloring) will require shifting, but it is not a performance-critical operation - and you're usually dealing with insertions or deletions anyway. |
An idea that seems to make sense for at least the transition period and perhaps permanently: Instead of extensively changing the API, we still use the "column number" as the key parameter to most of the functions, as opposed to using a CellData as a cursor/iterator. To avoid quadratic behavior (due to a linear search for every cell in a line) we keep a small (1-entry) cache in each When the most recent position is in the middle of a wide character or a |
I've started re-vamping the BufferLine re-implementation based on these comments. I just checked in the first batch of changes.
|
Update and status of my fork: Both the old I ripped out the code for using "cursors". Instead I reimplemented the old API using a cache in each For performance, I added an The biggest remaining piece is how to deal with line-wrapping. I would prefer to do that by storing "logical lines" (without wrapping) using a Some simple tests (output with wide characters, clusters, fg/bg changes) and applications (such as |
@PerBothner Sorry for being absent for a couple of weeks. Gonna try to catch up by tomorrow hopefully. |
It might make sense to update the the proposal to match the current implementation. On the other hand, it might be helpful to get some feedback that the current approach is reasonable before I spend too much time re-writing the proposal. Have you (@Tyriar or @jerch) been able to take a recent look at the implementation? I've been doing a lot of thinking about how to handle line-wrapping, without a firm conclusion. I'm pretty sure the goal should be that lines that are
to
This would improve flexibility. |
@PerBothner Edit: |
I did a merge from upstream/master.
|
I did just notice there is still some old crud in CellData: |
I committed a cleanup of |
This (or the logical equivalent) should work:
|
Thx, will use that one. 👍 |
I've started work on the new line-wrapping approach. There are new two concrete classes:
Buggy and incomplete but should indicate what I'm aiming for. If you have a check-out of my fork, you can do:
|
Things are coming together, and it may soon be time to create an actual Pull Request (even though there is still quite a bit more to be done). Right now I'm trying to clean up
However,
This seems wrong to me (as an old Java programmer): '_cachedBg' should be accessible in
I'm reluctant to whine "compiler bug" but this seems overly strict. Is this really how TypeScript is supposed to work? I can work around the problem by making |
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 aUint32Array
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 exampleSKIP_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 implicitSKIP_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 theBufferLine
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 eachprint
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 substringfrom 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-classBufferOverflowLine
for each wrapped (non-initial) row. EachBufferOverflowLine
points back to the parentBufferLine
. TheBufferOverflowLine
also contains the state needed to efficiently initialize aCellData
at the start of that row.A reflow operations needs to add or remove
BufferOverflowLine
children of a parentBufferLine
.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 a
BufferOverflowLine
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 implementsIBufferLine
and is the parent ofBufferLine
(andBufferOverflowLine
). 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,The text was updated successfully, but these errors were encountered: