-
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
Add logic for reflowing lines #644
Conversation
Looks very good, I could only find one last bug, which seems to happen when the input line is wrapped after a resize. It is a little hard to reproduce, but I've been able to capture a video that demonstrates the bug. You can see that I enter lots of text into the input line, then i resize and the characters are shown incorrectly. I then resize back and everything is back to normal. I then run Here's the full video that demonstrates my way down to the exception: |
Here's a much simpler way to reproduce (hopefully) the same bug as @mofux mentioned: Platform: macOS 10.12.4, stable Chrome 57, node v7.7.1 Steps:
Expected: Actual: Stack trace:
|
Opening in vim and fiddling with the size seems to break the normal buffer when you exit: Notice the scrollbar, scrolling it does nothing. Looks like the same exception:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@LucianBuzzo here's a pretty large review. It would be helpful to get a small high-level write up of the approach you're taking, in particular the following things:
- How
wrappedLines
works - How
WrappableList.transform
works - What the changes in
InputHandler
do
src/Interfaces.ts
Outdated
@@ -74,6 +74,10 @@ interface ICircularList<T> { | |||
shiftElements(start: number, count: number, offset: number): void; | |||
} | |||
|
|||
export interface IWrappableList extends ICircularList<any[]> { | |||
transform(width: number): void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a comment as to its purpose.
@@ -43,8 +43,13 @@ export class CircularList<T> { | |||
this._length = newLength; | |||
} | |||
|
|||
public get forEach(): (callbackfn: (value: T, index: number, array: T[]) => void) => void { | |||
return this._array.forEach; | |||
public forEach(callbackfn: (value: T, index?: number, array?: T[]) => void): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did this need to change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it stood, the forEach implementation didn't take into account the cyclic index, so as soon as the list started to wrap, the behaviour became unpredictable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point, this may be causing other obscure bugs if it's used anywhere important.
If this is the case though, I think the new forEach needs to take into account the _startIndex since a buffer can start from any index and not necessarily wrap all the way around. https://github.com/sourcelair/xterm.js/blob/944da2808906aa86b6ad096fd0b179e8cf87e0b8/src/utils/CircularList.ts#L140
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the get
method uses _getCyclicIndex
which uses _startIndex
though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point 😄
Is there any reason you use .bind
? This has a big downside in TypeScript as you lose typing information.
Also I think you should use .length
instead of .maxLength
for the reason mentioned above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch on using .length
! I think the .bind
is left over from earlier experiments and can be removed.
src/utils/WrappableList.ts
Outdated
return value !== null; | ||
} | ||
|
||
export class WrappableList<T> extends CircularList<any[]> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WrappableList
shouldn't be generic:
export class WrappableList extends CircularList<any[]> {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember having issues with the inherited methods when I was typing this, though it was probably down to my bad TS skills! I'll take another look at it
src/utils/WrappableList.ts
Outdated
if (this._length + 1 === this.maxLength) { | ||
this.wrappedLines = this.wrappedLines.map(x => x - 1); | ||
} | ||
this._array[this._getCyclicIndex(this._length)] = value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defer implementation here to super class:
super.push(value);
src/utils/WrappableList.ts
Outdated
// Need to make sure wrappedlines move when CircularList wraps around | ||
public push(value: any[]): void { | ||
if (this._length + 1 === this.maxLength) { | ||
this.wrappedLines = this.wrappedLines.map(x => x - 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Array.prototype.map
will create a new array of the same size, initializing it to the result of the function (ie. linear). We need WrappableList.push
to remain at a constant time complexity as with a large buffer this happens on every since row added to the list (which can be a lot depending on the command).
src/utils/WrappableList.ts
Outdated
} | ||
}; | ||
|
||
this.forEach((line, index) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also won't scale particularly well and will likely cause xterm.js embedders to lock up with a large scrollback buffer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you think of any good real world examples of embedded software that I can test performance with? I think there's quite a few gains that can be made in terms of efficiency
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I was making performance improvements I would up the scrollback buffer and run ls -lR
in a reasonably large directory and measure the time/lock ups.
Note that normally time
is useful here but it's not accurate in the demo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing you can do is attach window resize to the resize function, this is what many program do including VS Code as right now resize is a relatively lightweight operation and it would be good to keep it that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the srollback buffer in vscode?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1000 by default, it's not uncommon for people to want much more than that though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made some improvements and I'm seeing good performance up to a scrollback size of 5000 lines. Above 5000 lines the performance starts to decrease but I think it's still acceptable.
As an example, after setting the scrollback buffer to 10000 and running the command grep -Rl ' ' .
in the root of the xterm.js project, running the resize command at 20 cols takes approximately 270ms. This is an extreme example, using the same method above but running resize at 40 cols takes approximately 80ms - I think this is a more likely use case.
To help prevent locking I've wrapped the resize
method in a throttle
function based on underscore's implementation - this helps a lot when binding the resize
method to the window resize event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think forcing throttling is a particularly good solution here, the lib consumer could always do that if absolutely necessary. Throttling will also potentially cause layout issues for consumers as the terminal will be too large for the container (until the time is hit).
I'd really like to get perf improved much more than this though, 80ms is more than a frame already and remember that when xterm.js is hosted in an application like VS Code everything slows down a whole lot more as xterm.js is just a small portion of the code. As a real example, I use a scrollback of 5000 personally so that the build/test output from VS Code will fit in the buffer.
I think we need some more fundamental changes to the algorithms to get their time complexities down. I'll look through your descriptions below and study the code again and get back to you 😃
src/utils/WrappableList.ts
Outdated
|
||
let cachedStartIndex = this._startIndex; | ||
const scrollback = temp.length > this.maxLength ? temp.length : this.maxLength; | ||
this.maxLength = scrollback; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should be able to override the maxLength
getter which would be preferable to assigning over the getter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah; I definitely don't want to assign over the getter!
src/xterm.js
Outdated
let cachedLines = this.lines.length; | ||
this.lines.transform(x); | ||
|
||
let ymove = this.lines.length - cachedLines; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this will move based on the difference in rows, ideally we would move based on the line index.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you write some pseudo code to explain this? I don't quite understand what you mean, sorry!
src/xterm.js
Outdated
|
||
if (ymove) { | ||
if (this.ydisp === 0) { | ||
this.y += ymove; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe changing the cursor position like this is dangerous.
src/InputHandler.ts
Outdated
@@ -92,6 +94,11 @@ export class InputHandler implements IInputHandler { | |||
this._terminal.lines.get(row)[this._terminal.x] = [this._terminal.curAttr, '', 0]; | |||
this._terminal.x++; | |||
} | |||
|
|||
let wi = this._terminal.lines.wrappedLines.indexOf(row); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear what's happening here. It also looks like InputHandler
knows a lot more about the WrappableList
implementation than it should.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed this needs a higher level of abstraction and comments! 😊
src/xterm.js
Outdated
this.scrollTop = 0; | ||
this.scrollBottom = y - 1; | ||
|
||
this.charMeasure.measure(); | ||
|
||
this.refresh(0, this.rows - 1); | ||
|
||
this.normal = null; | ||
// this.normal = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been here since the beginning, see #510 (comment) for more info.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little skeptical of this, it looked like a lot of work to get it working properly when I looked at it. When you resize you need to resize both buffers, otherwise the other one will be out of date?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could check by empty/full buffer on the normal buffer, switch to alt buffer, resize rows then switch back and check the state of the scroll bar. Not sure if that was the only issue though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran through the repro steps mentioned in the issue and it appears to work fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have to check it out then 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since the normal buffer is in a pristine state we can just run the resize logic (with the new force flag), when we switch back to the normal biffer and get the correct output
Connects to #622
writing lines to an unscrollable area.
@Tyriar How WrappableList.transform works What the changes in InputHandler do An additional change that has been made is to change blank character cells from |
@Tyriar I've made some good performance improvements. 👍
Running the same tests, but resizing between 40 and 41 cols:
|
@LucianBuzzo I sat down yesterday, studied the code and looked at how we could scale the code a little better by first breaking down the problem again now that we have a better understanding and looking at how we could solve it by doing minimal work on get/push and resize operations. FYI I checked after your recent perf improvements and am still seeing 300+ms resizes sometimes Repro steps:
Even with these improvements while they may be fine for most users, I think it's still an unacceptable performance regression in VS Code as the team and our users are particularly sensitive to performance issues. Another aspect of VS Code and likely other apps is that multiple terminals will be resized at the same time. Therefore I'm a little reluctant in pushing as it would prevent us from pulling the latest xterm.js. The community work on this issue has been invaluable so far though as we have a much clearer view of the problem now. What we need
Issues with current implementation
ProposalWrappableBuffer's stateI'm proposing we change
With this we have a basic model of the wrapping with minimal information. For fast access of the actual rows we will also want
These two arrays are all that will be evaluated when the terminal is resized, so the resize of resize(width: number) {
// Lots of optimization potential here
this.normalToWrappedIndex = new Array(this.circularList.length);
this.wrappedToNormalIndex = new Array(this.circularList.length);
let currentNormal = 0;
let currentWrappedCount = 0;
for (let i = 0; i < this.circularList.length; i++) {
const line = this.circularList.get(i);
const wrappedCount = Math.ceil(line.length / width);
this.normalToWrappedIndex[i] = currentWrappedCount;
for (let j = 0; j < wrappedCount; j++) {
this.wrappedToNormalIndex[currentWrappedCount + j] = [i, j];
}
currentWrappedCount += wrappedCount;
}
} Retrieving rowsSince we're only doing the above on a resize we need to actually serve the wrapped rows somehow. This can be done at the time the wrapped row is requested with When the line's wrapped state changes we can discard the cached chunked row. Composition over inheritanceIn order to prevent future misuse let's use a member variable instead of extending and have both CircularList`s internal changesTo deal If there are any other internal movement that Exposing both wrapped and unwrapped buffersI believe we want to expose both wrapped ( ScrollbackCurrently it's a little ambiguous what scrollback means, in the new solution scrollback would strictly define the number of unwrapped rows. ResultHere's a once over of the
@LucianBuzzo let me know what you think. You're of course under no obligation to implement this but I'm very interested to hear you thoughts. |
@Tyriar The issue that I think you will run into with this approach is that there are a number of behaviours where characters are expected to wrap and then written over with a carriage return or an escape sequence. |
@LucianBuzzo that case where a character is duplicated before the \r, how did you end up handling that in your approach? |
@Tyriar in my previous PR I never resolved the issue. I couldn't think of a robust way of handling these cases whilst taking the approach of ignoring the terminal width and storing the line data in an unwrapped state (I believe that is what you are proposing?). In this PR it's a non issue as we wrap lines when they go over the terminal width. |
@Tyriar I've just tried using the latest branch in vscode with a 10000 line scrollback (full as well) and it works really well. |
@LucianBuzzo I definitely see the difference (2 terminals, 10000 scrollback): Also 46 tests are failing. https://travis-ci.org/sourcelair/xterm.js/jobs/228226085 @parisk Travis isn't erroring out 😕 I'm going to have a look at perf improvements on top of your stuff now. |
Was trying to get a quick proof of concept together of my design. Was running into a bit of an issue when wrapping |
@LucianBuzzo I put out a PR against your branch to clean up some types https://github.com/LucianBuzzo/xterm.js/pull/2 |
@Tyriar Should I keep working on this and sort the tests out? |
@LucianBuzzo currently I don't think we can accept this until resize scales better and the problems with the design are fixed (breaking the private API of CircularList, scrollback increasing permanently on a resize) as they represent technical debt we would be taking on. |
@Tyriar WRT the resize functionality scaling better, what are the benchmarks that you want to hit? |
@LucianBuzzo I was testing 1-2 terminals with 10000 sized buffers within VS Code. The issues with the scaling are:
It's difficult to define an exact benchmark, basically I would like to see a single terminal resizing in as little time as possible. Something more like < 30ms to resize a 10000 buffer while being hosted within VS Code (on my apparently slow machine). But it's not the only issue with the implementation, the reaching into |
@Tyriar Your comment reads like a dead-end for this PR. I understand your concerns, but I feel a lot of work and ideas that went into this PR and its previous iterations could lay the ground for a re-implementation from your side. I believe you will have to take the lead on this, as this feature has a direct dependency to the virtual selection and the link matcher (multiple lines) features, and you're the only contributor who has an insight knowledge about all of them. |
@mofux unfortunately yeah. While this whole thing has been very valuable as we've learned a lot. I'm definitely going to do a better job in looking into how hard something is before encouraging PRs on it, I had no idea this would have been so hard and touching so many things until I started reviewing the PRs. |
…pulate circular list internals
@Tyriar I've given this some thought and updated this PR with an implementation that attempts to resolve the issues you've described here.
In theory, this means that there should be no conflict with the line selection work you are doing. |
ping @Tyriar ^^ |
Pretty flat out with work at the moment, will have to defer evaluating the changes for a bit. |
@Tyriar Of course, no problem! |
Closing this as it's not going to get merged for performance/design reasons and it's really out of sync now. It is very useful as a reference though so I've pulled some of the key findings out into #622 |
Connects to #622
I believe this approach will be more stable and easier to manage than the one previously proposed in #609
One thing I realised after working on the previous reflow PR was that you have a lot of instances where input intentionally writes over EOL and then expects to overwrite the wrapped characters using a carriage return or
CSI Ps ; Ps H
.Using the dynamic reflow I had proposed previously, it's very difficult to compensate for this behaviour without a lot of trickery.
As it stands this PR appears to work very well, though I'm sure that it needs more testing.
I'd greatly appreciate people trying to break the implementation and then posting their feedback here.