Throttle scrollbar updates in TermControl to ~one per 8ms (#4608)

In addition to the below (original) description, this commit introduces
a ThrottledFunc template that can throttle _any_ function. It applies
that type to muffle updates to the scrollbar.

---

Redo #3531 but without the bug that it caused (#3622) which is why it
was reverted.

I'm sorry if I explain this badly. If you don't understand a part, make
sure to let me know and I will explain it better.

### Explanation

How it worked before: `Terminal` signals that viewport changed ->
`TermControl::_TerminalScrollPositionChanged` gets called on the
terminal thread -> it dispatches work for later to be ran the UI thread
to updates the scrollbar's values

Why it's bad:
* If we have many viewport changes, it will create a long stack of
  operations to run. Instead, we should just update the scroll bar with
  the most recent information that we know.
* Imagine if the rate that the work gets pushed on the UI thread is
  greater than the rate that it can handle: it might freeze?
* No need to be real time, we can wait just a little bit (8ms) to
  accumulate viewport changes before we actually change the scroll bar's
  value because it appears to be expensive (see perf below).

Now: `Terminal` signals that viewport changed ->
`TermControl::_TerminalScrollPositionChanged` gets called on the
terminal thread -> it tells the `ScrollBarUpdater` about a new update ->
the `ScrollBarUpdater` only runs one job (I don't know if that's the
right term) on the UI thread at a time. If a job is already running but
hasn't updated the scroll bar yet, it changes the setting in the already
existing job to update the scroll bar with the new values. A job "waits"
some time before doing the update to throttle updates because we don't
need real time scroll bar updates. -> eventually, it updates the scroll
bar If the user scrolls when a scroll bar update is pending, we keep the
scroll bar's Maximum and Minimum but let the user choose its new Value
with the `CancelPendingValueChange` method.

### Note

Also I changed a little bit the code from the Terminal to notify the
TermControl less often when possible.

I tried to scroll with the scroll bar, with the mouse wheel. I tried to
scroll while content is being outputted.

I tried to reproduce the crash from #2248 without success (good).

Co-authored-by: Leonard Hecker <leonard@hecker.io>

Closes #3622
This commit is contained in:
greg904 2020-06-12 21:51:37 +02:00 committed by GitHub
parent e8ece1645c
commit 25df527743
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 453 additions and 250 deletions

View file

@ -7,6 +7,7 @@ EXPCMDFLAGS
EXPCMDSTATE
fullkbd
href
IAsync
IBox
IBind
IClass

View file

@ -27,6 +27,10 @@ using namespace winrt::Windows::System;
using namespace winrt::Microsoft::Terminal::Settings;
using namespace winrt::Windows::ApplicationModel::DataTransfer;
// The minimum delay between updates to the scroll bar's values.
// The updates are throttled to limit power usage.
constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8);
namespace winrt::Microsoft::Terminal::TerminalControl::implementation
{
// Helper static function to ensure that all ambiguous-width glyphs are reported as narrow.
@ -58,7 +62,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_initializedTerminal{ false },
_settings{ settings },
_closing{ false },
_isTerminalInitiatedScroll{ false },
_isInternalScrollBarUpdate{ false },
_autoScrollVelocity{ 0 },
_autoScrollingPointerPoint{ std::nullopt },
_autoScrollTimer{},
@ -120,6 +124,32 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
}
});
_updateScrollBar = std::make_shared<ThrottledFunc<ScrollBarUpdate>>(
[weakThis = get_weak()](const auto& update) {
if (auto control{ weakThis.get() })
{
control->Dispatcher()
.RunAsync(CoreDispatcherPriority::Normal, [=]() {
if (auto control2{ weakThis.get() })
{
control2->_isInternalScrollBarUpdate = true;
auto scrollBar = control2->ScrollBar();
if (update.newValue.has_value())
{
scrollBar.Value(update.newValue.value());
}
scrollBar.Maximum(update.newMaximum);
scrollBar.Minimum(update.newMinimum);
scrollBar.ViewportSize(update.newViewportSize);
control2->_isInternalScrollBarUpdate = false;
}
});
}
},
ScrollBarUpdateInterval);
static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast<int>(1.0 / 30.0 * 1000000));
_autoScrollTimer.Interval(AutoScrollUpdateInterval);
_autoScrollTimer.Tick({ this, &TermControl::_UpdateAutoScroll });
@ -214,7 +244,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
{
if (_closing)
{
return;
co_return;
}
// Update our control settings
@ -1437,8 +1467,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/,
Controls::Primitives::RangeBaseValueChangedEventArgs const& args)
{
if (_isTerminalInitiatedScroll || _closing)
if (_isInternalScrollBarUpdate || _closing)
{
// The update comes from ourselves, more specifically from the
// terminal. So we don't have to update the terminal because it
// already knows.
return;
}
@ -1448,9 +1481,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// itself - it was initiated by the mouse wheel, or the scrollbar.
_terminal->UserScrollViewport(newValue);
// We've just told the terminal to update its viewport to reflect the
// new scroll value so the scroll bar matches the viewport now.
_willUpdateScrollBarToMatchViewport.store(false);
// User input takes priority over terminal events so cancel
// any pending scroll bar update if the user scrolls.
_updateScrollBar->ModifyPending([](auto& update) {
update.newValue.reset();
});
}
// Method Description:
@ -1949,35 +1984,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_titleChangedHandlers(winrt::hstring{ wstr });
}
// Method Description:
// - Update the position and size of the scrollbar to match the given
// viewport top, viewport height, and buffer size.
// The change will be actually handled in _ScrollbarChangeHandler.
// This should be done on the UI thread. Make sure the caller is calling
// us in a RunAsync block.
// Arguments:
// - viewTop: the top of the visible viewport, in rows. 0 indicates the top
// of the buffer.
// - viewHeight: the height of the viewport in rows.
// - bufferSize: the length of the buffer, in rows
void TermControl::_ScrollbarUpdater(Controls::Primitives::ScrollBar scrollBar,
const int viewTop,
const int viewHeight,
const int bufferSize)
{
// The terminal is already in the scroll position it wants, so no need
// to tell it to scroll.
_isTerminalInitiatedScroll = true;
const auto hiddenContent = bufferSize - viewHeight;
scrollBar.Maximum(hiddenContent);
scrollBar.Minimum(0);
scrollBar.ViewportSize(viewHeight);
scrollBar.Value(viewTop);
_isTerminalInitiatedScroll = false;
}
// Method Description:
// - Update the position and size of the scrollbar to match the given
// viewport top, viewport height, and buffer size.
@ -1988,9 +1994,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// of the buffer.
// - viewHeight: the height of the viewport in rows.
// - bufferSize: the length of the buffer, in rows
winrt::fire_and_forget TermControl::_TerminalScrollPositionChanged(const int viewTop,
const int viewHeight,
const int bufferSize)
void TermControl::_TerminalScrollPositionChanged(const int viewTop,
const int viewHeight,
const int bufferSize)
{
// Since this callback fires from non-UI thread, we might be already
// closed/closing.
@ -2001,21 +2007,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_scrollPositionChangedHandlers(viewTop, viewHeight, bufferSize);
auto weakThis{ get_weak() };
ScrollBarUpdate update;
const auto hiddenContent = bufferSize - viewHeight;
update.newMaximum = hiddenContent;
update.newMinimum = 0;
update.newViewportSize = viewHeight;
update.newValue = viewTop;
co_await winrt::resume_foreground(Dispatcher());
// Even if we weren't closed/closing few lines above, we might be
// while waiting for this block of code to be dispatched.
// If 'weakThis' is locked, then we can safely work with 'this'
if (auto control{ weakThis.get() })
{
if (!_closing.load())
{
// Update our scrollbar
_ScrollbarUpdater(ScrollBar(), viewTop, viewHeight, bufferSize);
}
}
_updateScrollBar->Run(update);
}
// Method Description:

View file

@ -15,6 +15,7 @@
#include "../buffer/out/search.h"
#include "cppwinrt_utils.h"
#include "SearchBoxControl.h"
#include "ThrottledFunc.h"
namespace winrt::Microsoft::Terminal::TerminalControl::implementation
{
@ -135,8 +136,15 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
FontInfoDesired _desiredFont;
FontInfo _actualFont;
bool _isTerminalInitiatedScroll;
std::atomic<bool> _willUpdateScrollBarToMatchViewport;
struct ScrollBarUpdate
{
std::optional<double> newValue;
double newMaximum;
double newMinimum;
double newViewportSize;
};
std::shared_ptr<ThrottledFunc<ScrollBarUpdate>> _updateScrollBar;
bool _isInternalScrollBarUpdate;
int _rowsToScroll;
@ -201,7 +209,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void _DoResizeUnderLock(const double newWidth, const double newHeight);
void _RefreshSizeUnderLock();
void _TerminalTitleChanged(const std::wstring_view& wstr);
winrt::fire_and_forget _TerminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize);
void _TerminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize);
winrt::fire_and_forget _TerminalCursorPositionChanged();
void _MouseScrollHandler(const double mouseDelta, const Windows::Foundation::Point point, const bool isLeftButtonPressed);
@ -216,7 +224,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void _TryStopAutoScroll(const uint32_t pointerId);
void _UpdateAutoScroll(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e);
void _ScrollbarUpdater(Windows::UI::Xaml::Controls::Primitives::ScrollBar scrollbar, const int viewTop, const int viewHeight, const int bufferSize);
static Windows::UI::Xaml::Thickness _ParseThicknessFromPadding(const hstring padding);
void _KeyHandler(Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e, const bool keyDown);

View file

@ -43,6 +43,8 @@
<ClInclude Include="TermControlAutomationPeer.h">
<DependentUpon>TermControlAutomationPeer.idl</DependentUpon>
</ClInclude>
<ClInclude Include="ThreadSafeOptional.h" />
<ClInclude Include="ThrottledFunc.h" />
<ClInclude Include="TSFInputControl.h">
<DependentUpon>TSFInputControl.xaml</DependentUpon>
</ClInclude>

View file

@ -26,6 +26,8 @@
<ClInclude Include="TermControlAutomationPeer.h" />
<ClInclude Include="XamlUiaTextRange.h" />
<ClInclude Include="TermControlUiaProvider.hpp" />
<ClInclude Include="ThreadSafeOptional.h" />
<ClInclude Include="ThrottledFunc.h" />
<ClInclude Include="UiaTextRange.hpp" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "pch.h"
template<typename T>
class ThreadSafeOptional
{
public:
template<class... Args>
bool Emplace(Args&&... args)
{
std::lock_guard guard{ _lock };
bool hadValue = _inner.has_value();
_inner.emplace(std::forward<Args>(args)...);
return !hadValue;
}
std::optional<T> Take()
{
std::lock_guard guard{ _lock };
std::optional<T> value;
_inner.swap(value);
return value;
}
// Method Description:
// - If the optional has a value, then call the specified function with a
// reference to the value.
// - This method is always thread-safe. It can be called multiple times on
// different threads.
// Arguments:
// - f: the function to call with a reference to the value
// Return Value:
// - <none>
template<typename F>
void ModifyValue(F f)
{
std::lock_guard guard{ _lock };
if (_inner.has_value())
{
f(_inner.value());
}
}
private:
std::mutex _lock;
std::optional<T> _inner;
};

View file

@ -0,0 +1,97 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- ThrottledFunc.h
Abstract:
- This module defines a class to throttle function calls.
- You create an instance of a `ThrottledFunc` with a function and the delay
between two function calls.
- The function takes an argument of type `T`, the template argument of
`ThrottledFunc`.
- Use the `Run` method to wait and then call the function.
--*/
#pragma once
#include "pch.h"
#include "ThreadSafeOptional.h"
template<typename T>
class ThrottledFunc : public std::enable_shared_from_this<ThrottledFunc<T>>
{
public:
using Func = std::function<void(T arg)>;
ThrottledFunc(Func func, winrt::Windows::Foundation::TimeSpan delay) :
_func{ func },
_delay{ delay }
{
}
// Method Description:
// - Runs the function later with the specified argument, except if `Run`
// is called again before with a new argument, in which case the new
// argument will be instead.
// - For more information, read the "Abstract" section in the header file.
// Arguments:
// - arg: the argument to pass to the function
// Return Value:
// - <none>
winrt::fire_and_forget Run(T arg)
{
if (!_pendingCallArg.Emplace(arg))
{
// already pending
return;
}
auto weakThis = this->weak_from_this();
co_await winrt::resume_after(_delay);
if (auto self{ weakThis.lock() })
{
auto arg = self->_pendingCallArg.Take();
if (arg.has_value())
{
self->_func(arg.value());
}
else
{
// should not happen
}
}
}
// Method Description:
// - Modifies the pending argument for the next function invocation, if
// there is one pending currently.
// - Let's say that you just called the `Run` method with argument A.
// After the delay specified in the constructor, the function R
// specified in the constructor will be called with argument A.
// - By using this method, you can modify argument A before the function R
// is called with argument A.
// - You pass a function to this method which will take a reference to
// argument A and will modify it.
// - When there is no pending invocation of function R, this method will
// not do anything.
// - This method is always thread-safe. It can be called multiple times on
// different threads.
// Arguments:
// - f: the function to call with a reference to the argument
// Return Value:
// - <none>
template<typename F>
void ModifyPending(F f)
{
_pendingCallArg.ModifyValue(f);
}
private:
Func _func;
winrt::Windows::Foundation::TimeSpan _delay;
ThreadSafeOptional<T> _pendingCallArg;
};

View file

@ -781,48 +781,49 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
auto proposedCursorPosition = proposedPosition;
auto& cursor = _buffer->GetCursor();
const Viewport bufferSize = _buffer->GetSize();
bool notifyScroll = false;
// If we're about to scroll past the bottom of the buffer, instead cycle the
// buffer.
// GH#5540 - Make sure this is a positive number. We can't create a
// negative number of new rows.
const auto newRows = std::max(0, proposedCursorPosition.Y - bufferSize.Height() + 1);
if (newRows > 0)
SHORT rowsPushedOffTopOfBuffer = 0;
if (proposedCursorPosition.Y >= bufferSize.Height())
{
const auto newRows = proposedCursorPosition.Y - bufferSize.Height() + 1;
for (auto dy = 0; dy < newRows; dy++)
{
_buffer->IncrementCircularBuffer();
proposedCursorPosition.Y--;
rowsPushedOffTopOfBuffer++;
}
notifyScroll = true;
}
// Update Cursor Position
cursor.SetPosition(proposedCursorPosition);
const COORD cursorPosAfter = cursor.GetPosition();
// Move the viewport down if the cursor moved below the viewport.
if (cursorPosAfter.Y > _mutableViewport.BottomInclusive())
bool updatedViewport = false;
if (proposedCursorPosition.Y > _mutableViewport.BottomInclusive())
{
const auto newViewTop = std::max(0, cursorPosAfter.Y - (_mutableViewport.Height() - 1));
const auto newViewTop = std::max(0, proposedCursorPosition.Y - (_mutableViewport.Height() - 1));
if (newViewTop != _mutableViewport.Top())
{
_mutableViewport = Viewport::FromDimensions({ 0, gsl::narrow<short>(newViewTop) },
_mutableViewport.Dimensions());
notifyScroll = true;
updatedViewport = true;
}
}
if (notifyScroll)
if (updatedViewport)
{
_NotifyScrollEvent();
}
if (rowsPushedOffTopOfBuffer != 0)
{
// We have to report the delta here because we might have circled the text buffer.
// That didn't change the viewport and therefore the TriggerScroll(void)
// method can't detect the delta on its own.
COORD delta{ 0, -gsl::narrow<SHORT>(newRows) };
COORD delta{ 0, -rowsPushedOffTopOfBuffer };
_buffer->GetRenderTarget().TriggerScroll(&delta);
_NotifyScrollEvent();
}
_NotifyTerminalCursorPositionChanged();

View file

@ -36,7 +36,7 @@ namespace TerminalCoreUnitTests
class TerminalBufferTests;
class TerminalApiTest;
class ConptyRoundtripTests;
class TerminalAndRendererTests;
class ScrollTest;
};
#endif
@ -301,6 +301,6 @@ private:
friend class TerminalCoreUnitTests::TerminalBufferTests;
friend class TerminalCoreUnitTests::TerminalApiTest;
friend class TerminalCoreUnitTests::ConptyRoundtripTests;
friend class TerminalCoreUnitTests::TerminalAndRendererTests;
friend class TerminalCoreUnitTests::ScrollTest;
#endif
};

View file

@ -0,0 +1,217 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include <WexTestClass.h>
#include <argb.h>
#include <DefaultSettings.h>
#include "../renderer/inc/DummyRenderTarget.hpp"
#include "../renderer/base/Renderer.hpp"
#include "../renderer/dx/DxRenderer.hpp"
#include "../cascadia/TerminalCore/Terminal.hpp"
#include "MockTermSettings.h"
#include "consoletaeftemplates.hpp"
#include "TestUtils.h"
using namespace winrt::Microsoft::Terminal::Settings;
using namespace Microsoft::Terminal::Core;
using namespace ::Microsoft::Console::Types;
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
namespace
{
class MockScrollRenderTarget final : public ::Microsoft::Console::Render::IRenderTarget
{
public:
~MockScrollRenderTarget() override{};
std::optional<COORD> TriggerScrollDelta() const
{
return _triggerScrollDelta;
}
void Reset()
{
_triggerScrollDelta.reset();
}
virtual void TriggerRedraw(const Microsoft::Console::Types::Viewport&){};
virtual void TriggerRedraw(const COORD* const){};
virtual void TriggerRedrawCursor(const COORD* const){};
virtual void TriggerRedrawAll(){};
virtual void TriggerTeardown(){};
virtual void TriggerSelection(){};
virtual void TriggerScroll(){};
virtual void TriggerScroll(const COORD* const delta)
{
_triggerScrollDelta = { *delta };
};
virtual void TriggerCircling(){};
void TriggerTitleChange(){};
private:
std::optional<COORD> _triggerScrollDelta;
};
struct ScrollBarNotification
{
int ViewportTop;
int ViewportHeight;
int BufferHeight;
};
}
namespace TerminalCoreUnitTests
{
class ScrollTest;
};
using namespace TerminalCoreUnitTests;
class TerminalCoreUnitTests::ScrollTest final
{
// !!! DANGER: Many tests in this class expect the Terminal buffer
// to be 80x32. If you change these, you'll probably inadvertently break a
// bunch of tests !!!
static const SHORT TerminalViewWidth = 80;
static const SHORT TerminalViewHeight = 32;
// For TestNotifyScrolling, it's important that this value is ~=9000.
// Something smaller like 100 won't cause the test to fail.
static const SHORT TerminalHistoryLength = 9001;
TEST_CLASS(ScrollTest);
TEST_METHOD(TestNotifyScrolling);
TEST_METHOD_SETUP(MethodSetup)
{
_term = std::make_unique<::Microsoft::Terminal::Core::Terminal>();
_scrollBarNotification = std::make_shared<std::optional<ScrollBarNotification>>();
_term->SetScrollPositionChangedCallback([scrollBarNotification = _scrollBarNotification](const int top, const int height, const int bottom) {
ScrollBarNotification tmp;
tmp.ViewportTop = top;
tmp.ViewportHeight = height;
tmp.BufferHeight = bottom;
*scrollBarNotification = { tmp };
});
_renderTarget = std::make_unique<MockScrollRenderTarget>();
_term->Create({ TerminalViewWidth, TerminalViewHeight }, TerminalHistoryLength, *_renderTarget);
return true;
}
TEST_METHOD_CLEANUP(MethodCleanup)
{
_term = nullptr;
return true;
}
private:
std::unique_ptr<Terminal> _term;
std::unique_ptr<MockScrollRenderTarget> _renderTarget;
std::shared_ptr<std::optional<ScrollBarNotification>> _scrollBarNotification;
};
void ScrollTest::TestNotifyScrolling()
{
// See https://github.com/microsoft/terminal/pull/5630
//
// This is a test for GH#5540, in the most bizarre way. The origin of that
// bug was that as newlines were emitted, we'd accumulate an enormous scroll
// delta into a selection region, to the point of overflowing a SHORT. When
// the overflow occurred, the Terminal would fail to send a NotifyScroll() to
// the TermControl hosting it.
//
// For this bug to repro, we need to:
// - Have a sufficiently large buffer, because each newline we'll accumulate
// a delta of (0, ~bufferHeight), so (bufferHeight^2 + bufferHeight) >
// SHRT_MAX
// - Have a selection
Log::Comment(L"Watch out - this test takes a while to run, and won't "
L"output anything unless in encounters an error. This is expected.");
auto& termTb = *_term->_buffer;
auto& termSm = *_term->_stateMachine;
const auto totalBufferSize = termTb.GetSize().Height();
auto currentRow = 0;
// We're outputting like 18000 lines of text here, so emitting 18000*4 lines
// of output to the console is actually quite unnecessary
WEX::TestExecution::SetVerifyOutput settings(WEX::TestExecution::VerifyOutputSettings::LogOnlyFailures);
// Emit a bunch of newlines to test scrolling.
for (; currentRow < totalBufferSize * 2; currentRow++)
{
*_scrollBarNotification = std::nullopt;
_renderTarget->Reset();
termSm.ProcessString(L"X\r\n");
// When we're on TerminalViewHeight-1, we'll emit the newline that
// causes the first scroll event
bool scrolled = currentRow >= TerminalViewHeight - 1;
// When we circle the buffer, the scroll bar's position does not
// change.
bool circledBuffer = currentRow >= totalBufferSize - 1;
bool expectScrollBarNotification = scrolled && !circledBuffer;
if (expectScrollBarNotification)
{
VERIFY_IS_TRUE(_scrollBarNotification->has_value(),
fmt::format(L"Expected a 'scroll bar position changed' notification for row {}", currentRow).c_str());
}
else
{
VERIFY_IS_FALSE(_scrollBarNotification->has_value(),
fmt::format(L"Expected to not see a 'scroll bar position changed' notification for row {}", currentRow).c_str());
}
// If we scrolled but it circled the buffer, then the terminal will
// call `TriggerScroll` with a delta to tell the renderer about it.
if (scrolled && circledBuffer)
{
VERIFY_IS_TRUE(_renderTarget->TriggerScrollDelta().has_value(),
fmt::format(L"Expected a 'trigger scroll' notification in RenderTarget for row {}", currentRow).c_str());
COORD expectedDelta;
expectedDelta.X = 0;
expectedDelta.Y = -1;
VERIFY_ARE_EQUAL(expectedDelta, _renderTarget->TriggerScrollDelta().value(), fmt::format(L"Wrong value in 'trigger scroll' notification in RenderTarget for row {}", currentRow).c_str());
}
else
{
VERIFY_IS_FALSE(_renderTarget->TriggerScrollDelta().has_value(),
fmt::format(L"Expected to not see a 'trigger scroll' notification in RenderTarget for row {}", currentRow).c_str());
}
if (_scrollBarNotification->has_value())
{
const auto tmp = _scrollBarNotification->value();
const int expectedTop = std::clamp<int>(currentRow - TerminalViewHeight + 2,
0,
TerminalHistoryLength);
const int expectedHeight = TerminalViewHeight;
const int expectedBottom = expectedTop + TerminalViewHeight;
if ((tmp.ViewportTop != expectedTop) ||
(tmp.ViewportHeight != expectedHeight) ||
(tmp.BufferHeight != expectedBottom))
{
Log::Comment(NoThrowString().Format(L"Expected viewport values did not match on line %d", currentRow));
}
VERIFY_ARE_EQUAL(tmp.ViewportTop, expectedTop);
VERIFY_ARE_EQUAL(tmp.ViewportHeight, expectedHeight);
VERIFY_ARE_EQUAL(tmp.BufferHeight, expectedBottom);
}
}
}

View file

@ -1,177 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
// This test class is useful for cases where we need a Terminal, a Renderer, and
// a DxEngine. Some bugs won't repro without all three actually being hooked up.
// Note however, that the DxEngine is not wired up to actually be PaintFrame'd
// in this test - it pretty heavily depends on being able to actually get a
// render target, and as we're running in a unit test, we don't have one of
// those. However, this class is good for testing how invalidation works across
// all three.
#include "precomp.h"
#include <WexTestClass.h>
#include <argb.h>
#include <DefaultSettings.h>
#include "../renderer/inc/DummyRenderTarget.hpp"
#include "../renderer/base/Renderer.hpp"
#include "../renderer/dx/DxRenderer.hpp"
#include "../cascadia/TerminalCore/Terminal.hpp"
#include "MockTermSettings.h"
#include "consoletaeftemplates.hpp"
#include "TestUtils.h"
using namespace winrt::Microsoft::Terminal::Settings;
using namespace Microsoft::Terminal::Core;
using namespace ::Microsoft::Console::Types;
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
namespace TerminalCoreUnitTests
{
class TerminalAndRendererTests;
};
using namespace TerminalCoreUnitTests;
class TerminalCoreUnitTests::TerminalAndRendererTests final
{
// !!! DANGER: Many tests in this class expect the Terminal buffer
// to be 80x32. If you change these, you'll probably inadvertently break a
// bunch of tests !!!
static const SHORT TerminalViewWidth = 80;
static const SHORT TerminalViewHeight = 32;
// For TestNotifyScrolling, it's important that this value is ~=9000.
// Something smaller like 100 won't cause the test to fail.
static const SHORT TerminalHistoryLength = 9001;
TEST_CLASS(TerminalAndRendererTests);
TEST_METHOD(TestNotifyScrolling);
TEST_METHOD_SETUP(MethodSetup)
{
_term = std::make_unique<::Microsoft::Terminal::Core::Terminal>();
// Create the renderer
_renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_term.get(), nullptr, 0, nullptr);
::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer;
// Create the engine
_dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
_renderer->AddRenderEngine(_dxEngine.get());
// Initialize the renderer, engine for a default font size
_renderer->TriggerFontChange(USER_DEFAULT_SCREEN_DPI, _desiredFont, _actualFont);
const til::size viewportSize{ TerminalViewWidth, TerminalViewHeight };
const til::size fontSize = _actualFont.GetSize();
const til::size windowSize = viewportSize * fontSize;
VERIFY_SUCCEEDED(_dxEngine->SetWindowSize(windowSize));
const auto vp = _dxEngine->GetViewportInCharacters(Viewport::FromDimensions({ 0, 0 }, windowSize));
VERIFY_ARE_EQUAL(viewportSize, til::size{ vp.Dimensions() });
// Set up the Terminal, using the Renderer (which has the engine in it)
_term->Create({ TerminalViewWidth, TerminalViewHeight }, TerminalHistoryLength, renderTarget);
return true;
}
TEST_METHOD_CLEANUP(MethodCleanup)
{
_term = nullptr;
return true;
}
private:
std::unique_ptr<Terminal> _term;
std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer;
std::unique_ptr<::Microsoft::Console::Render::DxEngine> _dxEngine;
FontInfoDesired _desiredFont{ DEFAULT_FONT_FACE, 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8 };
FontInfo _actualFont{ DEFAULT_FONT_FACE, 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false };
};
void TerminalAndRendererTests::TestNotifyScrolling()
{
// See https://github.com/microsoft/terminal/pull/5630
//
// This is a test for GH#5540, in the most bizarre way. The origin of that
// bug was that as newlines were emitted, we'd accumulate an enormous scroll
// delta into a selection region, to the point of overflowing a SHORT. When
// the overflow occurred, the Terminal would fail to send a NotifyScroll() to
// the TermControl hosting it.
//
// For this bug to repro, we need to:
// - Have a sufficiently large buffer, because each newline we'll accumulate
// a delta of (0, ~bufferHeight), so (bufferHeight^2 + bufferHeight) >
// SHRT_MAX
// - Have a selection
Log::Comment(L"Watch out - this test takes a while to run, and won't "
L"output anything unless in encounters an error. This is expected.");
auto& termTb = *_term->_buffer;
auto& termSm = *_term->_stateMachine;
const auto totalBufferSize = termTb.GetSize().Height();
auto currentRow = 0;
bool gotScrollingNotification = false;
// We're outputting like 18000 lines of text here, so emitting 18000*4 lines
// of output to the console is actually quite unnecessary
WEX::TestExecution::SetVerifyOutput settings(WEX::TestExecution::VerifyOutputSettings::LogOnlyFailures);
auto verifyScrolling = [&](const int top, const int height, const int bottom) {
const int expectedTop = std::clamp<int>(currentRow - TerminalViewHeight + 2,
0,
TerminalHistoryLength);
const int expectedHeight = TerminalViewHeight;
const int expectedBottom = expectedTop + TerminalViewHeight;
if ((expectedTop != top) ||
(expectedHeight != height) ||
(expectedBottom != bottom))
{
Log::Comment(NoThrowString().Format(L"Expected values did not match on line %d", currentRow));
}
VERIFY_ARE_EQUAL(expectedTop, top);
VERIFY_ARE_EQUAL(expectedHeight, height);
VERIFY_ARE_EQUAL(expectedBottom, bottom);
gotScrollingNotification = true;
};
// Hook up the scrolling callback
_term->SetScrollPositionChangedCallback(verifyScrolling);
// Create a selection - the actual bounds don't matter, we just need to have one.
_term->SetSelectionAnchor(COORD{ 0, 0 });
_term->SetSelectionEnd(COORD{ TerminalViewWidth - 1, 0 });
_renderer->TriggerSelection();
// Emit a bunch of newlines. Eventually, the accumulated scroll delta will
// cause an overflow, and cause us to miss a NotifyScroll.
for (; currentRow < totalBufferSize * 2; currentRow++)
{
gotScrollingNotification = false;
termSm.ProcessString(L"X\r\n");
// When we're on TerminalViewHeight-1, we'll emit the newline that
// causes the first scroll event
if (currentRow >= TerminalViewHeight - 1)
{
VERIFY_IS_TRUE(gotScrollingNotification,
fmt::format(L"Expected a scrolling notification for row {}", currentRow).c_str());
}
else
{
VERIFY_IS_FALSE(gotScrollingNotification,
fmt::format(L"Expected to not see scrolling notification for row {}", currentRow).c_str());
}
}
}

View file

@ -20,7 +20,7 @@
<ClCompile Include="TerminalApiTest.cpp" />
<ClCompile Include="ConptyRoundtripTests.cpp" />
<ClCompile Include="TerminalBufferTests.cpp" />
<ClCompile Include="TerminalAndRendererTests.cpp" />
<ClCompile Include="ScrollTest.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\buffer\out\lib\bufferout.vcxproj">