-
-
Notifications
You must be signed in to change notification settings - Fork 628
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
Speed up generation of output #564
Conversation
This is really impressive! Would you be willing to push this PR to completion? |
Yeah, will do soon. |
Tests are passing too now. Just for fun, I ran
this PR
So roughly 32% faster rendering for even simple outputs. |
src/output.ts
Outdated
// Some characters take up more than one column. In that case, the following | ||
// pixels need to be cleared to avoid printing extra characters | ||
const charWidth = char.fullWidth ? 2 : char.value.length; | ||
for (let dx = 1; dx < charWidth; dx++) { |
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 does this skip the first character and starts from the second one?
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.
Because the first char is the char that's actually written, so width 1 is already accounted for.
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.
Not sure I understand, could you elaborate the "that's actually written" part?
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.
Line 201 writes the current character to the output array. Some characters are wider than a single column though, so we have to set the following columns (up to the character width) to "" so we don't end up with extra characters.
So if the char is a 2-column emoji, we write the emoji to column x and "" to column x+1. The character after the emoji will then correctly be at column x+2.
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.
Do we need a loop here then? Could it be simplified to this?
if (char.fullWidth) {
currentLine[offsetX + 1] = /* empty char */
}
offsetX += character.fullWidth ? 2 : 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.
Aren't there some emojis which have length > 4?
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.
Nevermind, they have a visual width of 2. We can't fully go by the "fullWidth" property though, since some emojis with a visual width of 2 are not detected by https://github.com/sindresorhus/is-fullwidth-code-point
For example 🍦 (code point 0x1f366) and 🎉 (code point 0x1f389)
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 pushed a commit to eliminate the loop.
Superb work 🔥 |
src/output.ts
Outdated
@@ -198,12 +198,16 @@ export default class Output { | |||
const chars = styledCharsFromTokens(tokenize(line)); | |||
let offsetX = x; | |||
for (const char of chars) { | |||
if (offsetX >= this.width) break; |
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.
How did you run into this issue where an out-of-bounds write would occur? Could you add a test for it to ensure it doesn't regress back?
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.
Done. In my case it was a pretty complicated nested layout, but I managed to reduce it to a <Text wrap="end">
that's longer than the stdout width.
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's no wrap="end"
value, but there's wrap="truncate-end"
. See https://github.com/vadimdemedes/ink#wrap. I updated it in the test you added, then commented out "prevent out-of-bounds writes" commit changes and it didn't crash for me. Looks like we don't need this code 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.
Hmm maybe the test case isn't correct then. I ended up with gaps in the output array, which crashed the function that converts tokens back to a string.
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.
Do you still see the same issue when using wrap="truncate-end"
instead of wrap="end"
(which is an invalid prop)?
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.
Will try tomorrow or so.
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.
Hey @AlCalzone, do you still have the capacity to finish this PR? Would really love to ship it. No worries if you're busy 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.
Sorry for the delay. I'll try to look at this soon. The problem is I'm actually not sure how to trigger the out-of-bounds write.
It did happen for me in a relatively complicated flexbox layout, which is why I added the checks.
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.
No worries, take your time 👍
@vadimdemedes I finally managed to reproduce this reliably. The issue is when trying to render boxes with borders that are at least 2 columns too wide for the terminal, e.g. this component on a terminal with width 10: <Box width={12} height={10} borderStyle={'round'} flexDirection="row"></Box> Not sure why this doesn't happen for width+1 though. This case causes the following writes to
As a result, the arrays for lines 1...9 have an entry at index 11 that should not exist and index 10 is empty, essentially this: This causes I added a better test case for this. |
Thanks @AlCalzone, that was really helpful! Here's what I've found. I took your import React from 'react';
import {Box, render} from 'ink';
function Test() {
return <Box width={12} height={10} borderStyle={'round'}></Box>;
}
process.stdout.columns = 10;
render(<Test />, {debug: true}); The right border is rendered in the wrong place, even though there are correct X and Y coordinates for all sides:
Here's where each line is written to output: Lines 187 to 190 in 8a04760
I added this console.log({
// X coordinate of the `line` to write to output
x,
// Existing output before X coordinate
before: sliceAnsi(currentLine, 0, x),
beforeWidth: sliceAnsi(currentLine, 0, x).length,
// New text to write to output
line,
lineWidth: line.length,
// Existing output after X coordinate + width of the `line`
after: sliceAnsi(currentLine, x + width),
afterWidth: sliceAnsi(currentLine, x + width).length
}); Here's the output for one of the lines for writing the right border:
Normally, Line 174 in 8a04760
Lines 97 to 99 in 8a04760
This creates a problem where It doesn't cause a crash in the current version of Ink, because To sum up, this is an existing issue of Ink with content larger than the terminal and should be fixed separately from this PR. To avoid regressions in this PR though, we need to skip |
Pushed a commit to keep existing behavior with out of bounds writes. |
CI is green, merging! Thank you for this incredible performance boost, @AlCalzone! |
This is a proof of concept for a different rendering approach that does not rely on
sliceAnsi
as heavily.I tried this with my test component from #560, but with 50 spinners. Without this change, 50 spinners put one core of my CPU at 100%, and rendering lags heavily. With this change, I'm looking at ~40% utilization of one core, which is still not ideal, but renders fluently.
The output for 10s of profiling now looks like this, is idle half the time and most of the work is done in
wasm
, which is probably fromyoga
internals:fixes: #560
This will need some more testing with emojis and clipping, but I wanted to get this out for some early feedback.