Update _TerminalCursorPositionChanged to use ThrottledFunc (#6492)

* Update _TerminalCursorPositionChanged to use ThrottledFunc.
* Rename previous ThrottledFunc to ThrottledArgFunc because now
  ThrottledFunc is for functions that do not take an argument.
* Update ThrottledFunc and ThrottledArgFunc to accept a CoreDispatcher
  on which the function should be called for convenience.
* Don't use coroutines/winrt::fire_and_forget in
  ThrottledFunc/ThrottledArgFunc because they are too slow (see PR).

_AdjustCursorPosition went from 17% of samples to 3% in performance
testing.
This commit is contained in:
greg904 2020-06-23 23:05:40 +02:00 committed by GitHub
parent b24dbf7c77
commit 58f5d7c72e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 179 additions and 163 deletions

View file

@ -31,6 +31,9 @@ using namespace winrt::Windows::ApplicationModel::DataTransfer;
// The updates are throttled to limit power usage. // The updates are throttled to limit power usage.
constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8); constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8);
// The minimum delay between updating the TSF input control.
constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100);
namespace winrt::Microsoft::Terminal::TerminalControl::implementation namespace winrt::Microsoft::Terminal::TerminalControl::implementation
{ {
// Helper static function to ensure that all ambiguous-width glyphs are reported as narrow. // Helper static function to ensure that all ambiguous-width glyphs are reported as narrow.
@ -124,31 +127,36 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
} }
}); });
_tsfTryRedrawCanvas = std::make_shared<ThrottledFunc<>>(
[weakThis = get_weak()]() {
if (auto control{ weakThis.get() })
{
control->TSFInputControl().TryRedrawCanvas();
}
},
TsfRedrawInterval,
Dispatcher());
_updateScrollBar = std::make_shared<ThrottledFunc<ScrollBarUpdate>>( _updateScrollBar = std::make_shared<ThrottledFunc<ScrollBarUpdate>>(
[weakThis = get_weak()](const auto& update) { [weakThis = get_weak()](const auto& update) {
if (auto control{ weakThis.get() }) if (auto control{ weakThis.get() })
{ {
control->Dispatcher() control->_isInternalScrollBarUpdate = true;
.RunAsync(CoreDispatcherPriority::Normal, [=]() {
if (auto control2{ weakThis.get() })
{
control2->_isInternalScrollBarUpdate = true;
auto scrollBar = control2->ScrollBar(); auto scrollBar = control->ScrollBar();
if (update.newValue.has_value()) if (update.newValue.has_value())
{ {
scrollBar.Value(update.newValue.value()); scrollBar.Value(update.newValue.value());
} }
scrollBar.Maximum(update.newMaximum); scrollBar.Maximum(update.newMaximum);
scrollBar.Minimum(update.newMinimum); scrollBar.Minimum(update.newMinimum);
scrollBar.ViewportSize(update.newViewportSize); scrollBar.ViewportSize(update.newViewportSize);
control2->_isInternalScrollBarUpdate = false; control->_isInternalScrollBarUpdate = false;
}
});
} }
}, },
ScrollBarUpdateInterval); ScrollBarUpdateInterval,
Dispatcher());
static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast<int>(1.0 / 30.0 * 1000000)); static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast<int>(1.0 / 30.0 * 1000000));
_autoScrollTimer.Interval(AutoScrollUpdateInterval); _autoScrollTimer.Interval(AutoScrollUpdateInterval);
@ -2047,42 +2055,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// to be where the current cursor position is. // to be where the current cursor position is.
// Arguments: // Arguments:
// - N/A // - N/A
winrt::fire_and_forget TermControl::_TerminalCursorPositionChanged() void TermControl::_TerminalCursorPositionChanged()
{ {
bool expectedFalse{ false }; _tsfTryRedrawCanvas->Run();
if (!_coroutineDispatchStateUpdateInProgress.compare_exchange_weak(expectedFalse, true))
{
// somebody's already in here.
return;
}
if (_closing.load())
{
return;
}
auto dispatcher{ Dispatcher() }; // cache a strong ref to this in case TermControl dies
auto weakThis{ get_weak() };
// Muffle 2: Muffle Harder
// If we're the lucky coroutine who gets through, we'll still wait 100ms to clog
// the atomic above so we don't service the cursor update too fast. If we get through
// and finish processing the update quickly but similar requests are still beating
// down the door above in the atomic, we may still update the cursor way more than
// is visible to anyone's eye, which is a waste of effort.
static constexpr auto CursorUpdateQuiesceTime{ std::chrono::milliseconds(100) };
co_await winrt::resume_after(CursorUpdateQuiesceTime);
co_await winrt::resume_foreground(dispatcher);
if (auto control{ weakThis.get() })
{
if (!_closing.load())
{
TSFInputControl().TryRedrawCanvas();
}
_coroutineDispatchStateUpdateInProgress.store(false);
}
} }
hstring TermControl::Title() hstring TermControl::Title()

View file

@ -136,6 +136,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
FontInfoDesired _desiredFont; FontInfoDesired _desiredFont;
FontInfo _actualFont; FontInfo _actualFont;
std::shared_ptr<ThrottledFunc<>> _tsfTryRedrawCanvas;
struct ScrollBarUpdate struct ScrollBarUpdate
{ {
std::optional<double> newValue; std::optional<double> newValue;
@ -210,7 +212,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void _RefreshSizeUnderLock(); void _RefreshSizeUnderLock();
void _TerminalTitleChanged(const std::wstring_view& wstr); void _TerminalTitleChanged(const std::wstring_view& wstr);
void _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 _TerminalCursorPositionChanged();
void _MouseScrollHandler(const double mouseDelta, const Windows::Foundation::Point point, const bool isLeftButtonPressed); void _MouseScrollHandler(const double mouseDelta, const Windows::Foundation::Point point, const bool isLeftButtonPressed);
void _MouseZoomHandler(const double delta); void _MouseZoomHandler(const double delta);
@ -246,12 +248,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs); void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs);
winrt::fire_and_forget _AsyncCloseConnection(); winrt::fire_and_forget _AsyncCloseConnection();
// this atomic is to be used as a guard against dispatching billions of coroutines for
// routine state changes that might happen millions of times a second.
// Unbounded main dispatcher use leads to massive memory leaks and intense slowdowns
// on the UI thread.
std::atomic<bool> _coroutineDispatchStateUpdateInProgress{ false };
}; };
} }

View file

@ -43,7 +43,6 @@
<ClInclude Include="TermControlAutomationPeer.h"> <ClInclude Include="TermControlAutomationPeer.h">
<DependentUpon>TermControlAutomationPeer.idl</DependentUpon> <DependentUpon>TermControlAutomationPeer.idl</DependentUpon>
</ClInclude> </ClInclude>
<ClInclude Include="ThreadSafeOptional.h" />
<ClInclude Include="ThrottledFunc.h" /> <ClInclude Include="ThrottledFunc.h" />
<ClInclude Include="TSFInputControl.h"> <ClInclude Include="TSFInputControl.h">
<DependentUpon>TSFInputControl.xaml</DependentUpon> <DependentUpon>TSFInputControl.xaml</DependentUpon>
@ -61,6 +60,7 @@
<ClCompile Include="TermControl.cpp"> <ClCompile Include="TermControl.cpp">
<DependentUpon>TermControl.xaml</DependentUpon> <DependentUpon>TermControl.xaml</DependentUpon>
</ClCompile> </ClCompile>
<ClCompile Include="ThrottledFunc.cpp" />
<ClCompile Include="TSFInputControl.cpp"> <ClCompile Include="TSFInputControl.cpp">
<DependentUpon>TSFInputControl.xaml</DependentUpon> <DependentUpon>TSFInputControl.xaml</DependentUpon>
</ClCompile> </ClCompile>

View file

@ -19,6 +19,7 @@
<ClCompile Include="UiaTextRange.cpp" /> <ClCompile Include="UiaTextRange.cpp" />
<ClCompile Include="SearchBoxControl.cpp" /> <ClCompile Include="SearchBoxControl.cpp" />
<ClCompile Include="init.cpp" /> <ClCompile Include="init.cpp" />
<ClCompile Include="ThrottledFunc.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
@ -26,7 +27,6 @@
<ClInclude Include="TermControlAutomationPeer.h" /> <ClInclude Include="TermControlAutomationPeer.h" />
<ClInclude Include="XamlUiaTextRange.h" /> <ClInclude Include="XamlUiaTextRange.h" />
<ClInclude Include="TermControlUiaProvider.hpp" /> <ClInclude Include="TermControlUiaProvider.hpp" />
<ClInclude Include="ThreadSafeOptional.h" />
<ClInclude Include="ThrottledFunc.h" /> <ClInclude Include="ThrottledFunc.h" />
<ClInclude Include="UiaTextRange.hpp" /> <ClInclude Include="UiaTextRange.hpp" />
</ItemGroup> </ItemGroup>

View file

@ -1,54 +0,0 @@
// 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,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "ThrottledFunc.h"
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::UI::Xaml;
ThrottledFunc<>::ThrottledFunc(ThrottledFunc::Func func, TimeSpan delay, CoreDispatcher dispatcher) :
_func{ func },
_delay{ delay },
_dispatcher{ dispatcher },
_isRunPending{}
{
}
// Method Description:
// - Runs the function later, except if `Run` is called again before
// with a new argument, in which case the request will be ignored.
// - For more information, read the class' documentation.
// - This method is always thread-safe. It can be called multiple times on
// different threads.
// Arguments:
// - <none>
// Return Value:
// - <none>
void ThrottledFunc<>::Run()
{
if (_isRunPending.test_and_set())
{
// already pending
return;
}
_dispatcher.RunAsync(CoreDispatcherPriority::Low, [weakThis = this->weak_from_this()]() {
if (auto self{ weakThis.lock() })
{
DispatcherTimer timer;
timer.Interval(self->_delay);
timer.Tick([=](auto&&...) {
if (auto self{ weakThis.lock() })
{
timer.Stop();
self->_isRunPending.clear();
self->_func();
}
});
timer.Start();
}
});
}

View file

@ -4,94 +4,139 @@ Licensed under the MIT license.
Module Name: Module Name:
- ThrottledFunc.h - 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 #pragma once
#include "pch.h" #include "pch.h"
#include "ThreadSafeOptional.h" // Class Description:
// - Represents a function that takes arguments and whose invocation is
template<typename T> // delayed by a specified duration and rate-limited such that if the code
class ThrottledFunc : public std::enable_shared_from_this<ThrottledFunc<T>> // tries to run the function while a call to the function is already
// pending, then the previous call with the previous arguments will be
// cancelled and the call will be made with the new arguments instead.
// - The function will be run on the the specified dispatcher.
template<typename... Args>
class ThrottledFunc : public std::enable_shared_from_this<ThrottledFunc<Args...>>
{ {
public: public:
using Func = std::function<void(T arg)>; using Func = std::function<void(Args...)>;
ThrottledFunc(Func func, winrt::Windows::Foundation::TimeSpan delay) : ThrottledFunc(Func func, winrt::Windows::Foundation::TimeSpan delay, winrt::Windows::UI::Core::CoreDispatcher dispatcher) :
_func{ func }, _func{ func },
_delay{ delay } _delay{ delay },
_dispatcher{ dispatcher }
{ {
} }
// Method Description: // Method Description:
// - Runs the function later with the specified argument, except if `Run` // - Runs the function later with the specified arguments, except if `Run`
// is called again before with a new argument, in which case the new // is called again before with new arguments, in which case the new
// argument will be instead. // arguments will be used instead.
// - For more information, read the "Abstract" section in the header file. // - For more information, read the class' documentation.
// - This method is always thread-safe. It can be called multiple times on
// different threads.
// Arguments: // Arguments:
// - arg: the argument to pass to the function // - arg: the argument to pass to the function
// Return Value: // Return Value:
// - <none> // - <none>
winrt::fire_and_forget Run(T arg) template<typename... MakeArgs>
void Run(MakeArgs&&... args)
{ {
if (!_pendingCallArg.Emplace(arg))
{ {
// already pending std::lock_guard guard{ _lock };
return;
}
auto weakThis = this->weak_from_this(); bool hadValue = _pendingRunArgs.has_value();
_pendingRunArgs.emplace(std::forward<MakeArgs>(args)...);
co_await winrt::resume_after(_delay); if (hadValue)
if (auto self{ weakThis.lock() })
{
auto arg = self->_pendingCallArg.Take();
if (arg.has_value())
{ {
self->_func(arg.value()); // already pending
} return;
else
{
// should not happen
} }
} }
_dispatcher.RunAsync(CoreDispatcherPriority::Low, [weakThis = this->weak_from_this()]() {
if (auto self{ weakThis.lock() })
{
DispatcherTimer timer;
timer.Interval(self->_delay);
timer.Tick([=](auto&&...) {
if (auto self{ weakThis.lock() })
{
timer.Stop();
std::optional<std::tuple<Args...>> args;
{
std::lock_guard guard{ self->_lock };
self->_pendingRunArgs.swap(args);
}
std::apply(self->_func, args.value());
}
});
timer.Start();
}
});
} }
// Method Description: // Method Description:
// - Modifies the pending argument for the next function invocation, if // - Modifies the pending arguments for the next function invocation, if
// there is one pending currently. // there is one pending currently.
// - Let's say that you just called the `Run` method with argument A. // - Let's say that you just called the `Run` method with some arguments.
// After the delay specified in the constructor, the function R // After the delay specified in the constructor, the function specified
// specified in the constructor will be called with argument A. // in the constructor will be called with these arguments.
// - By using this method, you can modify argument A before the function R // - By using this method, you can modify the arguments before the function
// is called with argument A. // is called.
// - You pass a function to this method which will take a reference to // - You pass a function to this method which will take references to
// argument A and will modify it. // the arguments (one argument corresponds to one reference to an
// - When there is no pending invocation of function R, this method will // argument) and will modify them.
// - When there is no pending invocation of the function, this method will
// not do anything. // not do anything.
// - This method is always thread-safe. It can be called multiple times on // - This method is always thread-safe. It can be called multiple times on
// different threads. // different threads.
// Arguments: // Arguments:
// - f: the function to call with a reference to the argument // - f: the function to call with references to the arguments
// Return Value: // Return Value:
// - <none> // - <none>
template<typename F> template<typename F>
void ModifyPending(F f) void ModifyPending(F f)
{ {
_pendingCallArg.ModifyValue(f); std::lock_guard guard{ _lock };
if (_pendingRunArgs.has_value())
{
std::apply(f, _pendingRunArgs.value());
}
} }
private: private:
Func _func; Func _func;
winrt::Windows::Foundation::TimeSpan _delay; winrt::Windows::Foundation::TimeSpan _delay;
ThreadSafeOptional<T> _pendingCallArg; winrt::Windows::UI::Core::CoreDispatcher _dispatcher;
std::optional<std::tuple<Args...>> _pendingRunArgs;
std::mutex _lock;
};
// Class Description:
// - Represents a function whose invocation is delayed by a specified duration
// and rate-limited such that if the code tries to run the function while a
// call to the function is already pending, the request will be ignored.
// - The function will be run on the the specified dispatcher.
template<>
class ThrottledFunc<> : public std::enable_shared_from_this<ThrottledFunc<>>
{
public:
using Func = std::function<void()>;
ThrottledFunc(Func func, winrt::Windows::Foundation::TimeSpan delay, winrt::Windows::UI::Core::CoreDispatcher dispatcher);
void Run();
private:
Func _func;
winrt::Windows::Foundation::TimeSpan _delay;
winrt::Windows::UI::Core::CoreDispatcher _dispatcher;
std::atomic_flag _isRunPending;
}; };