-
Notifications
You must be signed in to change notification settings - Fork 8.5k
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
Rewrite how marks are stored & add reflow #16937
Conversation
…d replace with a text attribute
I'm totally just going to hand spans back out of the text buffer. genius.
This comment has been minimized.
This comment has been minimized.
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.
10/22, but submitting to give you a chance to think about these before I continue
src/buffer/out/Marks.hpp
Outdated
std::pair<til::point, til::point> GetExtent() const | ||
{ | ||
til::point realEnd{ til::coalesce_value(outputEnd, commandEnd, end) }; | ||
return std::make_pair(til::point{ start }, realEnd); |
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.
return std::make_pair(til::point{ start }, realEnd); | |
return std::make_pair(start, realEnd); |
q: is this necessary? start
is already the right type
|
||
void ROW::EndOutput(std::optional<unsigned int> error) noexcept | ||
{ | ||
if (_promptData.has_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.
gut check me - EndOutput
may happen on a line without a prompt, right? Do we care about those instances?
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 mean, technically, yes. But I think the one caller of this (TextBuffer::EndOutput
) goes up and actually does find the right row
@@ -299,6 +305,8 @@ class ROW final | |||
bool _wrapForced = false; | |||
// Occurs when the user runs out of text to support a double byte character and we're forced to the next line | |||
bool _doubleBytePadded = false; | |||
|
|||
std::optional<ScrollbarData> _promptData = std::nullopt; |
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.
@lhecker this will grow ROW by 12+ bytes (considering padding for alignment after ScrollbarData::category
, and the size of the flag in an optional
), for a total of 108k for a full buffer. Is this OK?
@zadjii-msft if you want to save the cost of this in conhost, you can use the preprocessor version of TIL_FEATURE
for this and the implementation bodies (fwiw)
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.
Something else we discussed:
struct ScrollbarData
{
til::color color;
uint32_t exitCode = 0;
MarkCategory category{ MarkCategory::Default };
bool valid = false;
bool hasColor = false;
bool hasExitCode = false;
};
struct ROW {
// ...
ScrollbarData _promptData;
};
to avoid some overhead
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.
Honestly I think optional
is about as optimal as you can get; it's pretty much a struct { uninitialized<T> _value; bool valid; }
already, but with some packing magic to make it so that alignment such-and-such works out
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.
While I'd agree that std::optional
is optimal in keeping data uninitialized, I'd find it difficult to agree that it's optimal when it comes to its memory layout. point
, color
and so on for instance all double in size when wrapped in an optional
.
The current ScrollbarData
struct is 20 bytes large, whereas the one above is 12 bytes large. Due to ROW's necessary 16 byte alignment this effectively results in 20 bytes of padding per ROW.
MarkExtents
' struct size for instance is 60 bytes, compared to just 28 if we didn't use optional
. 54% of the struct is just empty space.
src/buffer/out/TextAttribute.hpp
Outdated
@@ -38,6 +38,16 @@ enum class UnderlineStyle | |||
Max = DashedUnderlined | |||
}; | |||
|
|||
// We only need a few bits, but uint8_t is two bytes anyways, and apparently |
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.
// We only need a few bits, but uint8_t is two bytes anyways, and apparently | |
// We only need a few bits, but due to padding uint8_t occupies two bytes anyways, and apparently |
fwiw since uint8_t is unequivocally one byte, we should explain why it's two ^
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 gonna leave it as a u16 unfortunately due to the std::has_unique_object_representations_v
thing
@@ -502,7 +502,7 @@ namespace ControlUnitTests | |||
const auto& start = core->_terminal->GetSelectionAnchor(); | |||
const auto& end = core->_terminal->GetSelectionEnd(); | |||
const til::point expectedStart{ 24, 0 }; // The character after the prompt | |||
const til::point expectedEnd{ 29, 3 }; // x = buffer.right | |||
const til::point expectedEnd{ 21, 3 }; // x = the end of the text |
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.
whoa, what happened to cause this change?
Looking at how we're storing marks now, it makes me wonder if we need the FTCS flushing mechanism at all - now that they're TextAttributes, can conpty emit them? Is that ~ ~ weird ~ ~ or wrong? I'm assuming Leonard wants to say "no, please stop rendering things with conpty" but it's only 2024 😄 |
{ | ||
// We want to return _no_ marks when we're in the alt buffer, to effectively | ||
// hide them. | ||
return _inAltBuffer() ? std::vector<ScrollMark>{} : std::move(_activeBuffer().GetMarkRows()); |
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.
nit: Don't (usually) std::move
a return value. Otherwise RVO becomes impossible. Same thing below.
src/buffer/out/textBuffer.cpp
Outdated
// Otherwise, we've changed from any state -> any state, and it doesn't really matter. | ||
if (markKind != MarkKind::None) |
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.
Due to the earlier check this will always be true.
|
||
if (markKind != MarkKind::None) | ||
{ | ||
lastMarkedText = { nextX, y }; |
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 seems to me as if we could move this down to the lastMarkKind = markKind;
which would IMO be easier to read.
We could also use a switch/case instead of a series of if conditions. An explicit MarkKind::None
could then ensure that we don't set the lastMarkKind
and lastMarkedText
by doing something like...
case MarkKind::None:
x = nextX;
continue;
} | ||
lastPromptY = promptY; | ||
} | ||
std::reverse(commands.begin(), commands.end()); |
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 can avoid reversing here if you iterate forwards through the buffer instead, right?
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 I'm comfortable enough with this!
} | ||
void ROW::SetScrollbarData(std::optional<ScrollbarData> data) noexcept | ||
{ | ||
_promptData = data; |
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 _promptData
doesn't get reset in Reset()
, which is called during buffer circling. Due to this I believe this code will start failing to work once the buffer is full.
If I'm correct, then simply adding it to Reset()
should fix it.
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 is my only remaining important concern. The memory layout is IMO not optimal for perf, but we can noodle on that later.)
src/buffer/out/Row.cpp
Outdated
_promptData = ScrollbarData{ | ||
.category = MarkCategory::Prompt, | ||
.color = std::nullopt, | ||
.exitCode = std::nullopt, | ||
}; |
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.
FYI, and I'm writing this just because I think it's interesting, as I mentioned elsewhere, optional
is really only good at keeping stuff uninitialized. It's not really consistently great at initializing things, however.
As Dustin said, an optional
is basically
template<typename T>
struct optional {
union { T data }; // union keeps it uninitialized even if it has a constructor
bool initialized = false;
};
The problem with optional
is that its constructor
template < class U = T >
constexpr optional( U&& value );
takes the initial value by reference, std::move
s it into data
and sets initialized
to true
. This only works great if the compiler happens to figure out how to optimize it. This doesn't work consistently in any compiler, but MSVC in particular also happens to be really REALLY bad at avoiding unnecessary struct copies.
This means that constructing a ScrollbarData
like you do looks like this:
mov BYTE PTR [rcx], 4 ; .category = 4
mov BYTE PTR [rcx+8], 0 ; .color.initialized = false
mov BYTE PTR [rcx+16], 0 ; .exitCode.initialized = false
However, constructing a std::optional<ScrollbarData>
for _promptData
looks like this:
; $T1 == the current function stack
mov BYTE PTR $T1[rsp], 4 ; stack: .category = 4
mov BYTE PTR $T1[rsp+8], 0 ; stack: .color.initialized = false
movups xmm0, XMMWORD PTR $T1[rsp] ; stack -> register
mov BYTE PTR $T1[rsp+16], 0 ; stack: .exitCode.initialized = false
mov eax, DWORD PTR $T1[rsp+16] ; stack -> register
movups XMMWORD PTR [rcx], xmm0 ; register -> ROW (.category/.color)
mov DWORD PTR [rcx+16], eax ; register -> ROW (.exitCode)
mov BYTE PTR [rcx+20], 1 ; ._promptData.initialized = true
In other words, MSVC constructs the ScrollbarData
on stack, then copies that into register, and then copies it again into ROW
. Other compilers don't do this exact thing, but they're doing similar dumb stuff under similar circumstances.
However, fret not, because the C++ consortium has solved this problem by making improvements to the language like introducing proper union types by adding another overload to std::optional
which can be used here:
template< class... Args >
constexpr explicit optional( std::in_place_t, Args&&... args );
std::optional<ScrollbarData>{
std::in_place,
MarkCategory::Prompt,
std::nullopt,
std::nullopt,
};
Just kidding:
mov BYTE PTR $T1[rsp], 4
mov BYTE PTR $T1[rsp+8], 0
movups xmm0, XMMWORD PTR $T1[rsp]
mov BYTE PTR $T1[rsp+16], 0
mov BYTE PTR $T1[rsp+20], 1
movsd xmm1, QWORD PTR $T1[rsp+16]
movups XMMWORD PTR [rcx], xmm0
movsd QWORD PTR [rcx+16], xmm1
This occurs because fixing the construction doesn't fix the existence of its move constructor. That one is still implemented in the STL and thus relies on the compiler optimizing it which no compiler does consistently correct.
The actual proper fix is to use this:
if (!_promptData.has_value())
{
_promptData.emplace(MarkCategory::Prompt);
}
which results in this:
cmp BYTE PTR [rcx+20], 0 ; _promptData.has_value()
jne SHORT $LN8@test ; if (!...) goto ret;
mov BYTE PTR [rcx], 4 ; .category = 4
mov BYTE PTR [rcx+8], 0 ; .color.initialized = false
mov BYTE PTR [rcx+16], 0 ; .exitCode.initialized = false
mov BYTE PTR [rcx+20], 1 ; ._promptData.initialized = true
$LN8@test:
ret 0
While using emplace
fixes the issue here, my point is mainly that optional
is not particularly great for performance nor memory usage. It may work, but only with careful use, as demonstrated above.
src/buffer/out/textBuffer.cpp
Outdated
auto attr = GetCurrentAttributes(); | ||
attr.SetMarkAttributes(MarkKind::Output); | ||
SetCurrentAttributes(attr); |
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.
nit: You can just do
_currentAttributes.SetMarkAttributes(MarkKind::Output);
here and similar above.
src/host/getset.cpp
Outdated
const auto inConpty{ g.getConsoleInformation().IsInVtIoMode() }; | ||
const auto resizeQuirk{ g.getConsoleInformation().GetVtIo()->IsResizeQuirkEnabled() }; | ||
if (!(inConpty && resizeQuirk)) |
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.
FWIW this change is not quite the same as it was before as it prevents the compiler from shortcutting the latter call. An alternative way to write this may be this:
if (!g.getConsoleInformation().IsInVtIoMode() ||
!g.getConsoleInformation().GetVtIo()->IsResizeQuirkEnabled())
{
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 sorry, this was for debugging.
const auto currentNewPos = _mainBuffer->GetCursor().GetPosition(); | ||
if (currentNewPos.y < proposedTop) | ||
{ | ||
_mainBuffer->GetCursor().SetPosition(til::point{ currentNewPos.x, proposedTop }); | ||
} |
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 I once read somewhere in our repository that adjusting the viewport to fit the cursor is much better than adjusting the cursor. I'm not sure what the reason was back then, but I can see how a later write should always continue where a previous one left off.
Instead I would suggest moving this up to line 390 and do this (untested):
proposedTop = std::min(proposedTop, newCursorPos.y);
This is fallout from #16937. * Typing a command then backspacing the chars then asking for suggestions would think the current commandline ended with spaces, making filtering very hard. * The currently typed command would _also_ appear in the command history, which isn't useful. I actually did TDD for this and wrote the test first, then confirmed again running through the build script, I wasn't hitting any of the earlier issues. Closes #17241 Closes #17243
This is fallout from #16937. * Typing a command then backspacing the chars then asking for suggestions would think the current commandline ended with spaces, making filtering very hard. * The currently typed command would _also_ appear in the command history, which isn't useful. I actually did TDD for this and wrote the test first, then confirmed again running through the build script, I wasn't hitting any of the earlier issues. Closes #17241 Closes #17243 (cherry picked from commit bf8a647) Service-Card-Id: 92638413 Service-Version: 1.21
This is pretty much a huge refactoring of how marks are stored in the buffer.
Gone is the list of
ScrollMark
s in the buffer that store regions of text as points marking the ends. Those would be nigh impossible to reflow nicely.Instead, we're going to use
TextAttribute
s to store the kind of output we've got -Prompt
,Command
,Output
, or, the default,None
. Those already reflow nicely!But we also need to store things like, the exit code for the command. That's why we've now added
ScrollbarData
toROW
s. There's really only going to be one prompt->output on a single row. So, we only need to store one ScrollbarData per-row. When a command ends, we can just go update the mark on the row that started that command.But iterating over the whole buffer to find the next/previous prompt/command/output region sounds complicated. So, to avoid everyone needing to do some variant of that, we've added
MarkExtents
(which is literally just the same mark structure as before). TextBuffer can figure out where all the mark regions are, and hand that back to callers. This allows ControlCore to be basically unchanged.But collecting up all the regions for all the marks sounds expensive! We need to update the scrollbar frequently, we can't just collect those up every time! No we can't! But we also don't need to. The scrollbar doesn't need to know where all the marks start and end and if they have commands and this and that - no. We only need to know the rows that have marks on them. So, we've now also got
ScrollMark
to represent just a mark on a scrollbar at a specific row on the buffer. We can get those quickly.