mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 00:48:23 -06:00
Extend lead/trail edge support for throttled_func (#19210)
You can now create throttled functions which trigger both on the leading and trailing edge. This was then also ported to `ThrottledFunc` for `DispatcherQueue`s and used for title/taskbar updates. Closes #19188 * In CMD run: ```batch FOR /L %N IN () DO @echo %time% ``` * Doesn't hang the UI ✅ SERVICE NOTES This replays part of #19192 to make it compatible with throttled_func. (cherry picked from commit dbf740cf2c113b9613b22c0895529e68d66e14db) Service-Card-Id: PVTI_lADOAF3p4s4AxadtzgdI2TQ PVTI_lADOAF3p4s4AxadtzgdSMv0 Service-Version: 1.23
This commit is contained in:
parent
f1dad37670
commit
f18abeb62e
@ -135,12 +135,19 @@ namespace winrt::TerminalApp::implementation
|
||||
_isElevated = ::Microsoft::Console::Utils::IsRunningElevated();
|
||||
_canDragDrop = ::Microsoft::Console::Utils::CanUwpDragDrop();
|
||||
|
||||
_reloadSettings = std::make_shared<ThrottledFuncTrailing<>>(winrt::Windows::System::DispatcherQueue::GetForCurrentThread(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() {
|
||||
if (auto self{ weakSelf.get() })
|
||||
{
|
||||
self->ReloadSettings();
|
||||
}
|
||||
});
|
||||
_reloadSettings = std::make_shared<ThrottledFunc<>>(
|
||||
DispatcherQueue::GetForCurrentThread(),
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::milliseconds{ 100 },
|
||||
.debounce = true,
|
||||
.trailing = true,
|
||||
},
|
||||
[weakSelf = get_weak()]() {
|
||||
if (auto self{ weakSelf.get() })
|
||||
{
|
||||
self->ReloadSettings();
|
||||
}
|
||||
});
|
||||
|
||||
_languageProfileNotifier = winrt::make_self<LanguageProfileNotifier>([this]() {
|
||||
_reloadSettings->Run();
|
||||
|
||||
@ -64,7 +64,7 @@ namespace winrt::TerminalApp::implementation
|
||||
bool _hasSettingsStartupActions{ false };
|
||||
::TerminalApp::AppCommandlineArgs _settingsAppArgs;
|
||||
|
||||
std::shared_ptr<ThrottledFuncTrailing<>> _reloadSettings;
|
||||
std::shared_ptr<ThrottledFunc<>> _reloadSettings;
|
||||
|
||||
std::vector<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings> _warnings{};
|
||||
|
||||
|
||||
@ -35,17 +35,23 @@ namespace winrt::TerminalApp::implementation
|
||||
// (which should be the default, see:
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-trackmouseevent#remarks)
|
||||
unsigned int hoverTimeoutMillis{ 400 };
|
||||
LOG_IF_WIN32_BOOL_FALSE(SystemParametersInfoW(SPI_GETMOUSEHOVERTIME, 0, &hoverTimeoutMillis, 0));
|
||||
const auto toolTipInterval = std::chrono::milliseconds(hoverTimeoutMillis);
|
||||
if (FAILED(SystemParametersInfoW(SPI_GETMOUSEHOVERTIME, 0, &hoverTimeoutMillis, 0)))
|
||||
{
|
||||
hoverTimeoutMillis = 400;
|
||||
}
|
||||
|
||||
// Create a ThrottledFunc for opening the tooltip after the hover
|
||||
// timeout. If we hover another button, we should make sure to call
|
||||
// Run() with the new button. Calling `_displayToolTip.Run(nullptr)`,
|
||||
// which will cause us to not display a tooltip, which is used when we
|
||||
// leave the control entirely.
|
||||
_displayToolTip = std::make_shared<ThrottledFuncTrailing<Controls::Button>>(
|
||||
_displayToolTip = std::make_shared<ThrottledFunc<Controls::Button>>(
|
||||
dispatcher,
|
||||
toolTipInterval,
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::milliseconds{ hoverTimeoutMillis },
|
||||
.debounce = true,
|
||||
.trailing = true,
|
||||
},
|
||||
[weakThis = get_weak()](Controls::Button button) {
|
||||
// If we provide a button, then open the tooltip on that button.
|
||||
// We can "dismiss" this throttled func by calling it with null,
|
||||
|
||||
@ -32,7 +32,7 @@ namespace winrt::TerminalApp::implementation
|
||||
til::typed_event<TerminalApp::MinMaxCloseControl, winrt::Windows::UI::Xaml::RoutedEventArgs> MaximizeClick;
|
||||
til::typed_event<TerminalApp::MinMaxCloseControl, winrt::Windows::UI::Xaml::RoutedEventArgs> CloseClick;
|
||||
|
||||
std::shared_ptr<ThrottledFuncTrailing<winrt::Windows::UI::Xaml::Controls::Button>> _displayToolTip{ nullptr };
|
||||
std::shared_ptr<ThrottledFunc<winrt::Windows::UI::Xaml::Controls::Button>> _displayToolTip{ nullptr };
|
||||
std::optional<CaptionButton> _lastPressedButton{ std::nullopt };
|
||||
};
|
||||
}
|
||||
|
||||
@ -104,9 +104,13 @@ namespace winrt::TerminalApp::implementation
|
||||
}
|
||||
}
|
||||
|
||||
_adjustProcessPriorityThrottled = std::make_shared<ThrottledFuncTrailing<>>(
|
||||
_adjustProcessPriorityThrottled = std::make_shared<ThrottledFunc<>>(
|
||||
DispatcherQueue::GetForCurrentThread(),
|
||||
std::chrono::milliseconds{ 100 },
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::milliseconds{ 100 },
|
||||
.debounce = true,
|
||||
.trailing = true,
|
||||
},
|
||||
[=]() {
|
||||
_adjustProcessPriority();
|
||||
});
|
||||
@ -5007,8 +5011,6 @@ namespace winrt::TerminalApp::implementation
|
||||
safe_void_coroutine TerminalPage::_ControlCompletionsChangedHandler(const IInspectable sender,
|
||||
const CompletionsChangedEventArgs args)
|
||||
{
|
||||
// This will come in on a background (not-UI, not output) thread.
|
||||
|
||||
// This won't even get hit if the velocity flag is disabled - we gate
|
||||
// registering for the event based off of
|
||||
// Feature_ShellCompletions::IsEnabled back in _RegisterTerminalEvents
|
||||
|
||||
@ -361,7 +361,7 @@ namespace winrt::TerminalApp::implementation
|
||||
bool _MovePane(const Microsoft::Terminal::Settings::Model::MovePaneArgs args);
|
||||
bool _MoveTab(winrt::com_ptr<TerminalTab> tab, const Microsoft::Terminal::Settings::Model::MoveTabArgs args);
|
||||
|
||||
std::shared_ptr<ThrottledFuncTrailing<>> _adjustProcessPriorityThrottled;
|
||||
std::shared_ptr<ThrottledFunc<>> _adjustProcessPriorityThrottled;
|
||||
void _adjustProcessPriority() const;
|
||||
|
||||
template<typename F>
|
||||
|
||||
@ -946,27 +946,49 @@ namespace winrt::TerminalApp::implementation
|
||||
void TerminalTab::_AttachEventHandlersToContent(const uint32_t paneId, const TerminalApp::IPaneContent& content)
|
||||
{
|
||||
auto weakThis{ get_weak() };
|
||||
auto dispatcher = TabViewItem().Dispatcher();
|
||||
auto dispatcher = DispatcherQueue::GetForCurrentThread();
|
||||
ContentEventTokens events{};
|
||||
|
||||
auto throttledTitleChanged = std::make_shared<ThrottledFunc<>>(
|
||||
dispatcher,
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::milliseconds{ 200 },
|
||||
.leading = true,
|
||||
.trailing = true,
|
||||
},
|
||||
[weakThis]() {
|
||||
if (const auto tab = weakThis.get())
|
||||
{
|
||||
tab->UpdateTitle();
|
||||
}
|
||||
});
|
||||
|
||||
events.TitleChanged = content.TitleChanged(
|
||||
winrt::auto_revoke,
|
||||
[dispatcher, weakThis](auto&&, auto&&) -> safe_void_coroutine {
|
||||
// The lambda lives in the `std::function`-style container owned by `control`. That is, when the
|
||||
// `control` gets destroyed the lambda struct also gets destroyed. In other words, we need to
|
||||
// copy `weakThis` onto the stack, because that's the only thing that gets captured in coroutines.
|
||||
// See: https://devblogs.microsoft.com/oldnewthing/20211103-00/?p=105870
|
||||
const auto weakThisCopy = weakThis;
|
||||
co_await wil::resume_foreground(dispatcher);
|
||||
// Check if Tab's lifetime has expired
|
||||
if (auto tab{ weakThisCopy.get() })
|
||||
[func = std::move(throttledTitleChanged)](auto&&, auto&&) {
|
||||
func->Run();
|
||||
});
|
||||
|
||||
auto throttledTaskbarProgressChanged = std::make_shared<ThrottledFunc<>>(
|
||||
dispatcher,
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::milliseconds{ 200 },
|
||||
.leading = true,
|
||||
.trailing = true,
|
||||
},
|
||||
[weakThis]() {
|
||||
if (const auto tab = weakThis.get())
|
||||
{
|
||||
// The title of the control changed, but not necessarily the title of the tab.
|
||||
// Set the tab's text to the active panes' text.
|
||||
tab->UpdateTitle();
|
||||
tab->_UpdateProgressState();
|
||||
}
|
||||
});
|
||||
|
||||
events.TaskbarProgressChanged = content.TaskbarProgressChanged(
|
||||
winrt::auto_revoke,
|
||||
[func = std::move(throttledTaskbarProgressChanged)](auto&&, auto&&) {
|
||||
func->Run();
|
||||
});
|
||||
|
||||
events.TabColorChanged = content.TabColorChanged(
|
||||
winrt::auto_revoke,
|
||||
[dispatcher, weakThis](auto&&, auto&&) -> safe_void_coroutine {
|
||||
@ -982,18 +1004,6 @@ namespace winrt::TerminalApp::implementation
|
||||
}
|
||||
});
|
||||
|
||||
events.TaskbarProgressChanged = content.TaskbarProgressChanged(
|
||||
winrt::auto_revoke,
|
||||
[dispatcher, weakThis](auto&&, auto&&) -> safe_void_coroutine {
|
||||
const auto weakThisCopy = weakThis;
|
||||
co_await wil::resume_foreground(dispatcher);
|
||||
// Check if Tab's lifetime has expired
|
||||
if (auto tab{ weakThisCopy.get() })
|
||||
{
|
||||
tab->_UpdateProgressState();
|
||||
}
|
||||
});
|
||||
|
||||
events.ConnectionStateChanged = content.ConnectionStateChanged(
|
||||
winrt::auto_revoke,
|
||||
[dispatcher, weakThis](auto&&, auto&&) -> safe_void_coroutine {
|
||||
|
||||
@ -174,8 +174,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
//
|
||||
// NOTE: Calling UpdatePatternLocations from a background
|
||||
// thread is a workaround for us to hit GH#12607 less often.
|
||||
shared->outputIdle = std::make_unique<til::debounced_func_trailing<>>(
|
||||
std::chrono::milliseconds{ 100 },
|
||||
shared->outputIdle = std::make_unique<til::throttled_func<>>(
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::milliseconds{ 100 },
|
||||
.debounce = true,
|
||||
.trailing = true,
|
||||
},
|
||||
[this, weakThis = get_weak(), dispatcher = _dispatcher]() {
|
||||
dispatcher.TryEnqueue(DispatcherQueuePriority::Normal, [weakThis]() {
|
||||
if (const auto self = weakThis.get(); self && !self->_IsClosing())
|
||||
@ -195,8 +199,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
|
||||
// If you rapidly show/hide Windows Terminal, something about GotFocus()/LostFocus() gets broken.
|
||||
// We'll then receive easily 10+ such calls from WinUI the next time the application is shown.
|
||||
shared->focusChanged = std::make_unique<til::debounced_func_trailing<bool>>(
|
||||
std::chrono::milliseconds{ 25 },
|
||||
shared->focusChanged = std::make_unique<til::throttled_func<bool>>(
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::milliseconds{ 25 },
|
||||
.debounce = true,
|
||||
.trailing = true,
|
||||
},
|
||||
[this](const bool focused) {
|
||||
// Theoretically `debounced_func_trailing` should call `WaitForThreadpoolTimerCallbacks()`
|
||||
// with cancel=true on destruction, which should ensure that our use of `this` here is safe.
|
||||
@ -204,9 +212,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
});
|
||||
|
||||
// Scrollbar updates are also expensive (XAML), so we'll throttle them as well.
|
||||
shared->updateScrollBar = std::make_shared<ThrottledFuncTrailing<Control::ScrollPositionChangedArgs>>(
|
||||
shared->updateScrollBar = std::make_shared<ThrottledFunc<Control::ScrollPositionChangedArgs>>(
|
||||
_dispatcher,
|
||||
std::chrono::milliseconds{ 8 },
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::milliseconds{ 8 },
|
||||
.trailing = true,
|
||||
},
|
||||
[weakThis = get_weak()](const auto& update) {
|
||||
if (auto core{ weakThis.get() }; core && !core->_IsClosing())
|
||||
{
|
||||
@ -2752,15 +2763,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
}
|
||||
}
|
||||
|
||||
safe_void_coroutine ControlCore::_terminalCompletionsChanged(std::wstring_view menuJson,
|
||||
unsigned int replaceLength)
|
||||
void ControlCore::_terminalCompletionsChanged(std::wstring_view menuJson, unsigned int replaceLength)
|
||||
{
|
||||
auto args = winrt::make_self<CompletionsChangedEventArgs>(winrt::hstring{ menuJson },
|
||||
replaceLength);
|
||||
|
||||
co_await winrt::resume_background();
|
||||
|
||||
CompletionsChanged.raise(*this, *args);
|
||||
CompletionsChanged.raise(*this, winrt::make<CompletionsChangedEventArgs>(winrt::hstring{ menuJson }, replaceLength));
|
||||
}
|
||||
|
||||
// Select the region of text between [s.start, s.end), in buffer space
|
||||
|
||||
@ -306,9 +306,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
private:
|
||||
struct SharedState
|
||||
{
|
||||
std::unique_ptr<til::debounced_func_trailing<>> outputIdle;
|
||||
std::unique_ptr<til::debounced_func_trailing<bool>> focusChanged;
|
||||
std::shared_ptr<ThrottledFuncTrailing<Control::ScrollPositionChangedArgs>> updateScrollBar;
|
||||
std::unique_ptr<til::throttled_func<>> outputIdle;
|
||||
std::unique_ptr<til::throttled_func<bool>> focusChanged;
|
||||
std::shared_ptr<ThrottledFunc<Control::ScrollPositionChangedArgs>> updateScrollBar;
|
||||
};
|
||||
|
||||
void _setupDispatcherAndCallbacks();
|
||||
@ -338,7 +338,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
void _terminalSearchMissingCommand(std::wstring_view missingCommand, const til::CoordType& bufferRow);
|
||||
void _terminalWindowSizeChanged(int32_t width, int32_t height);
|
||||
|
||||
safe_void_coroutine _terminalCompletionsChanged(std::wstring_view menuJson, unsigned int replaceLength);
|
||||
void _terminalCompletionsChanged(std::wstring_view menuJson, unsigned int replaceLength);
|
||||
#pragma endregion
|
||||
|
||||
#pragma region RendererCallbacks
|
||||
|
||||
@ -342,9 +342,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
// These three throttled functions are triggered by terminal output and interact with the UI.
|
||||
// Since Close() is the point after which we are removed from the UI, but before the
|
||||
// destructor has run, we MUST check control->_IsClosing() before actually doing anything.
|
||||
_playWarningBell = std::make_shared<ThrottledFuncLeading>(
|
||||
_playWarningBell = std::make_shared<ThrottledFunc<>>(
|
||||
dispatcher,
|
||||
TerminalWarningBellInterval,
|
||||
til::throttled_func_options{
|
||||
.delay = TerminalWarningBellInterval,
|
||||
.leading = true,
|
||||
},
|
||||
[weakThis = get_weak()]() {
|
||||
if (auto control{ weakThis.get() }; control && !control->_IsClosing())
|
||||
{
|
||||
@ -352,9 +355,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
}
|
||||
});
|
||||
|
||||
_updateScrollBar = std::make_shared<ThrottledFuncTrailing<ScrollBarUpdate>>(
|
||||
_updateScrollBar = std::make_shared<ThrottledFunc<ScrollBarUpdate>>(
|
||||
dispatcher,
|
||||
ScrollBarUpdateInterval,
|
||||
til::throttled_func_options{
|
||||
.delay = ScrollBarUpdateInterval,
|
||||
.trailing = true,
|
||||
},
|
||||
[weakThis = get_weak()](const auto& update) {
|
||||
if (auto control{ weakThis.get() }; control && !control->_IsClosing())
|
||||
{
|
||||
|
||||
@ -284,7 +284,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
bool _quickFixesAvailable{ false };
|
||||
til::CoordType _quickFixBufferPos{};
|
||||
|
||||
std::shared_ptr<ThrottledFuncLeading> _playWarningBell;
|
||||
std::shared_ptr<ThrottledFunc<>> _playWarningBell;
|
||||
|
||||
struct ScrollBarUpdate
|
||||
{
|
||||
@ -294,7 +294,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
double newViewportSize;
|
||||
};
|
||||
|
||||
std::shared_ptr<ThrottledFuncTrailing<ScrollBarUpdate>> _updateScrollBar;
|
||||
std::shared_ptr<ThrottledFunc<ScrollBarUpdate>> _updateScrollBar;
|
||||
|
||||
bool _isInternalScrollBarUpdate;
|
||||
|
||||
|
||||
@ -89,9 +89,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
{
|
||||
if (!_updatePreviewControl)
|
||||
{
|
||||
_updatePreviewControl = std::make_shared<ThrottledFuncTrailing<>>(
|
||||
_updatePreviewControl = std::make_shared<ThrottledFunc<>>(
|
||||
winrt::Windows::System::DispatcherQueue::GetForCurrentThread(),
|
||||
std::chrono::milliseconds{ 100 },
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::milliseconds{ 100 },
|
||||
.debounce = true,
|
||||
.trailing = true,
|
||||
},
|
||||
[this]() {
|
||||
const auto settings = _Profile.TermSettings();
|
||||
_previewConnection->DisplayPowerlineGlyphs(_Profile.DefaultAppearance().HasPowerlineCharacters());
|
||||
|
||||
@ -33,7 +33,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
|
||||
winrt::com_ptr<PreviewConnection> _previewConnection{ nullptr };
|
||||
Microsoft::Terminal::Control::TermControl _previewControl{ nullptr };
|
||||
std::shared_ptr<ThrottledFuncTrailing<>> _updatePreviewControl;
|
||||
std::shared_ptr<ThrottledFunc<>> _updatePreviewControl;
|
||||
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ViewModelChangedRevoker;
|
||||
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _AppearanceViewModelChangedRevoker;
|
||||
Editor::IHostedInWindow _windowRoot;
|
||||
|
||||
@ -96,7 +96,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
ApplicationState::ApplicationState(const std::filesystem::path& stateRoot) noexcept :
|
||||
_sharedPath{ stateRoot / stateFileName },
|
||||
_elevatedPath{ stateRoot / elevatedStateFileName },
|
||||
_throttler{ std::chrono::seconds(1), [this]() { _write(); } }
|
||||
_throttler{
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::seconds{ 1 },
|
||||
.debounce = true,
|
||||
.trailing = true,
|
||||
},
|
||||
[this]() { _write(); }
|
||||
}
|
||||
{
|
||||
_read();
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
til::shared_mutex<state_t> _state;
|
||||
std::filesystem::path _sharedPath;
|
||||
std::filesystem::path _elevatedPath;
|
||||
til::throttled_func_trailing<> _throttler;
|
||||
til::throttled_func<> _throttler;
|
||||
|
||||
void _write() const noexcept;
|
||||
void _read() const noexcept;
|
||||
|
||||
@ -3,40 +3,44 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "til/throttled_func.h"
|
||||
#include <til/throttled_func.h>
|
||||
|
||||
// ThrottledFunc is a copy of til::throttled_func,
|
||||
// specialized for the use with a WinRT Dispatcher.
|
||||
template<bool leading, typename... Args>
|
||||
class ThrottledFunc : public std::enable_shared_from_this<ThrottledFunc<leading, Args...>>
|
||||
template<typename... Args>
|
||||
class ThrottledFunc : public std::enable_shared_from_this<ThrottledFunc<Args...>>
|
||||
{
|
||||
public:
|
||||
using filetime_duration = std::chrono::duration<int64_t, std::ratio<1, 10000000>>;
|
||||
using filetime_duration = til::throttled_func_options::filetime_duration;
|
||||
using function = std::function<void(Args...)>;
|
||||
|
||||
// Throttles invocations to the given `func` to not occur more often than `delay`.
|
||||
// Throttles invocations to the given `func` to not occur more often than specified in options.
|
||||
//
|
||||
// If this is a:
|
||||
// * ThrottledFuncLeading: `func` will be invoked immediately and
|
||||
// further invocations prevented until `delay` time has passed.
|
||||
// * ThrottledFuncTrailing: On the first invocation a timer of `delay` time will
|
||||
// be started. After the timer has expired `func` will be invoked just once.
|
||||
// Options:
|
||||
// * delay: The minimum time between invocations
|
||||
// * leading: If true, `func` will be invoked immediately on first call
|
||||
// * trailing: If true, `func` will be invoked after the delay
|
||||
// * debounce: If true, resets the timer on each call
|
||||
//
|
||||
// After `func` was invoked the state is reset and this cycle is repeated again.
|
||||
ThrottledFunc(
|
||||
winrt::Windows::System::DispatcherQueue dispatcher,
|
||||
filetime_duration delay,
|
||||
function func) :
|
||||
// At least one of leading or trailing must be true.
|
||||
ThrottledFunc(winrt::Windows::System::DispatcherQueue dispatcher, til::throttled_func_options opts, function func) :
|
||||
_dispatcher{ std::move(dispatcher) },
|
||||
_func{ std::move(func) },
|
||||
_timer{ _create_timer() }
|
||||
_timer{ _create_timer() },
|
||||
_debounce{ opts.debounce },
|
||||
_leading{ opts.leading },
|
||||
_trailing{ opts.trailing }
|
||||
{
|
||||
const auto d = -delay.count();
|
||||
if (!_leading && !_trailing)
|
||||
{
|
||||
throw std::invalid_argument("neither leading nor trailing");
|
||||
}
|
||||
|
||||
const auto d = -opts.delay.count();
|
||||
if (d >= 0)
|
||||
{
|
||||
throw std::invalid_argument("non-positive delay specified");
|
||||
}
|
||||
|
||||
memcpy(&_delay, &d, sizeof(d));
|
||||
}
|
||||
|
||||
@ -54,88 +58,119 @@ public:
|
||||
template<typename... MakeArgs>
|
||||
void Run(MakeArgs&&... args)
|
||||
{
|
||||
if (!_storage.emplace(std::forward<MakeArgs>(args)...))
|
||||
{
|
||||
_leading_edge();
|
||||
}
|
||||
_lead(std::make_tuple(std::forward<MakeArgs>(args)...));
|
||||
}
|
||||
|
||||
// Modifies the pending arguments for the next function
|
||||
// invocation, if there is one pending currently.
|
||||
//
|
||||
// `func` will be invoked as func(Args...). Make sure to bind any
|
||||
// arguments in `func` by reference if you'd like to modify them.
|
||||
template<typename F>
|
||||
void ModifyPending(F func)
|
||||
{
|
||||
_storage.modify_pending(func);
|
||||
const auto guard = _lock.lock_exclusive();
|
||||
if (_pendingRunArgs)
|
||||
{
|
||||
std::apply(func, *_pendingRunArgs);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static void __stdcall _timer_callback(PTP_CALLBACK_INSTANCE /*instance*/, PVOID context, PTP_TIMER /*timer*/) noexcept
|
||||
try
|
||||
{
|
||||
static_cast<ThrottledFunc*>(context)->_trailing_edge();
|
||||
static_cast<ThrottledFunc*>(context)->_trail();
|
||||
}
|
||||
CATCH_LOG()
|
||||
|
||||
void _leading_edge()
|
||||
{
|
||||
if constexpr (leading)
|
||||
{
|
||||
_dispatcher.TryEnqueue(winrt::Windows::System::DispatcherQueuePriority::Normal, [weakSelf = this->weak_from_this()]() {
|
||||
if (auto self{ weakSelf.lock() })
|
||||
{
|
||||
try
|
||||
{
|
||||
self->_func();
|
||||
}
|
||||
CATCH_LOG();
|
||||
|
||||
SetThreadpoolTimerEx(self->_timer.get(), &self->_delay, 0, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void _trailing_edge()
|
||||
{
|
||||
if constexpr (leading)
|
||||
{
|
||||
_storage.reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
_dispatcher.TryEnqueue(winrt::Windows::System::DispatcherQueuePriority::Normal, [weakSelf = this->weak_from_this()]() {
|
||||
if (auto self{ weakSelf.lock() })
|
||||
{
|
||||
try
|
||||
{
|
||||
self->_storage.apply(self->_func);
|
||||
}
|
||||
CATCH_LOG();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
inline wil::unique_threadpool_timer _create_timer()
|
||||
wil::unique_threadpool_timer _create_timer()
|
||||
{
|
||||
wil::unique_threadpool_timer timer{ CreateThreadpoolTimer(&_timer_callback, this, nullptr) };
|
||||
THROW_LAST_ERROR_IF(!timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
FILETIME _delay;
|
||||
void _lead(std::tuple<Args...> args)
|
||||
{
|
||||
bool timerRunning = false;
|
||||
|
||||
{
|
||||
const auto guard = _lock.lock_exclusive();
|
||||
|
||||
timerRunning = _timerRunning;
|
||||
_timerRunning = true;
|
||||
|
||||
if (!timerRunning && _leading)
|
||||
{
|
||||
// Call the function immediately on the leading edge.
|
||||
// See below (out of lock).
|
||||
}
|
||||
else if (_trailing)
|
||||
{
|
||||
_pendingRunArgs.emplace(std::move(args));
|
||||
}
|
||||
}
|
||||
|
||||
const auto execute = !timerRunning && _leading;
|
||||
const auto schedule = !timerRunning || _debounce;
|
||||
|
||||
if (execute)
|
||||
{
|
||||
_execute(std::move(args), schedule);
|
||||
}
|
||||
else if (schedule)
|
||||
{
|
||||
SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void _trail()
|
||||
{
|
||||
decltype(_pendingRunArgs) args;
|
||||
|
||||
{
|
||||
const auto guard = _lock.lock_exclusive();
|
||||
|
||||
_timerRunning = false;
|
||||
args = std::exchange(_pendingRunArgs, std::nullopt);
|
||||
}
|
||||
|
||||
if (args)
|
||||
{
|
||||
_execute(std::move(*args), false);
|
||||
}
|
||||
}
|
||||
|
||||
void _execute(std::tuple<Args...> args, bool schedule)
|
||||
{
|
||||
_dispatcher.TryEnqueue(
|
||||
winrt::Windows::System::DispatcherQueuePriority::Normal,
|
||||
[weakSelf = this->weak_from_this(), args = std::move(args), schedule]() mutable {
|
||||
if (auto self{ weakSelf.lock() })
|
||||
{
|
||||
try
|
||||
{
|
||||
std::apply(self->_func, std::move(args));
|
||||
}
|
||||
CATCH_LOG();
|
||||
|
||||
if (schedule)
|
||||
{
|
||||
SetThreadpoolTimerEx(self->_timer.get(), &self->_delay, 0, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
winrt::Windows::System::DispatcherQueue _dispatcher;
|
||||
|
||||
// Everything below this point is just like til::throttled_func.
|
||||
|
||||
function _func;
|
||||
|
||||
wil::unique_threadpool_timer _timer;
|
||||
til::details::throttled_func_storage<Args...> _storage;
|
||||
};
|
||||
FILETIME _delay;
|
||||
|
||||
template<typename... Args>
|
||||
using ThrottledFuncTrailing = ThrottledFunc<false, Args...>;
|
||||
using ThrottledFuncLeading = ThrottledFunc<true>;
|
||||
wil::srwlock _lock;
|
||||
std::optional<std::tuple<Args...>> _pendingRunArgs;
|
||||
bool _timerRunning = false;
|
||||
|
||||
bool _debounce;
|
||||
bool _leading;
|
||||
bool _trailing;
|
||||
};
|
||||
|
||||
@ -288,9 +288,12 @@ void AppHost::Initialize()
|
||||
// the PTY requesting a change to the window state and the Terminal
|
||||
// realizing it, but should mitigate issues where the Terminal and PTY get
|
||||
// de-sync'd.
|
||||
_showHideWindowThrottler = std::make_shared<ThrottledFuncTrailing<bool>>(
|
||||
_showHideWindowThrottler = std::make_shared<ThrottledFunc<bool>>(
|
||||
winrt::Windows::System::DispatcherQueue::GetForCurrentThread(),
|
||||
std::chrono::milliseconds(200),
|
||||
til::throttled_func_options{
|
||||
.delay = std::chrono::milliseconds{ 200 },
|
||||
.trailing = true,
|
||||
},
|
||||
[this](const bool show) {
|
||||
_window->ShowWindowChanged(show);
|
||||
});
|
||||
|
||||
@ -39,7 +39,7 @@ private:
|
||||
std::unique_ptr<IslandWindow> _window;
|
||||
winrt::TerminalApp::AppLogic _appLogic{ nullptr };
|
||||
winrt::TerminalApp::TerminalWindow _windowLogic{ nullptr };
|
||||
std::shared_ptr<ThrottledFuncTrailing<bool>> _showHideWindowThrottler;
|
||||
std::shared_ptr<ThrottledFunc<bool>> _showHideWindowThrottler;
|
||||
SafeDispatcherTimer _frameTimer;
|
||||
LARGE_INTEGER _lastActivatedTime{};
|
||||
winrt::guid _virtualDesktopId{};
|
||||
|
||||
@ -5,120 +5,49 @@
|
||||
|
||||
namespace til
|
||||
{
|
||||
namespace details
|
||||
struct throttled_func_options
|
||||
{
|
||||
template<typename... Args>
|
||||
class throttled_func_storage
|
||||
{
|
||||
public:
|
||||
template<typename... MakeArgs>
|
||||
bool emplace(MakeArgs&&... args)
|
||||
{
|
||||
std::unique_lock guard{ _lock };
|
||||
const bool hadValue = _pendingRunArgs.has_value();
|
||||
_pendingRunArgs.emplace(std::forward<MakeArgs>(args)...);
|
||||
return hadValue;
|
||||
}
|
||||
using filetime_duration = std::chrono::duration<int64_t, std::ratio<1, 10000000>>;
|
||||
|
||||
template<typename F>
|
||||
void modify_pending(F f)
|
||||
{
|
||||
std::unique_lock guard{ _lock };
|
||||
if (_pendingRunArgs)
|
||||
{
|
||||
std::apply(f, *_pendingRunArgs);
|
||||
}
|
||||
}
|
||||
filetime_duration delay{};
|
||||
bool debounce = false;
|
||||
bool leading = false;
|
||||
bool trailing = false;
|
||||
};
|
||||
|
||||
void apply(const auto& func)
|
||||
{
|
||||
decltype(_pendingRunArgs) args;
|
||||
{
|
||||
std::unique_lock guard{ _lock };
|
||||
args = std::exchange(_pendingRunArgs, std::nullopt);
|
||||
}
|
||||
// Theoretically it should always have a value, because the throttled_func
|
||||
// should not call the callback without there being a reason.
|
||||
// But in practice a failure here was observed at least once.
|
||||
// It's unknown to me what caused it, so the best we can do is avoid a crash.
|
||||
assert(args.has_value());
|
||||
if (args)
|
||||
{
|
||||
std::apply(func, *args);
|
||||
}
|
||||
}
|
||||
|
||||
explicit operator bool() const
|
||||
{
|
||||
std::shared_lock guard{ _lock };
|
||||
return _pendingRunArgs.has_value();
|
||||
}
|
||||
|
||||
private:
|
||||
// std::mutex uses imperfect Critical Sections on Windows.
|
||||
// --> std::shared_mutex uses SRW locks that are small and fast.
|
||||
mutable std::shared_mutex _lock;
|
||||
std::optional<std::tuple<Args...>> _pendingRunArgs;
|
||||
};
|
||||
|
||||
template<>
|
||||
class throttled_func_storage<>
|
||||
{
|
||||
public:
|
||||
bool emplace()
|
||||
{
|
||||
return _isPending.exchange(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void apply(const auto& func)
|
||||
{
|
||||
if (_isPending.exchange(false, std::memory_order_relaxed))
|
||||
{
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
_isPending.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
explicit operator bool() const
|
||||
{
|
||||
return _isPending.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<bool> _isPending;
|
||||
};
|
||||
} // namespace details
|
||||
|
||||
template<bool Debounce, bool Leading, typename... Args>
|
||||
template<typename... Args>
|
||||
class throttled_func
|
||||
{
|
||||
public:
|
||||
using filetime_duration = std::chrono::duration<int64_t, std::ratio<1, 10000000>>;
|
||||
using filetime_duration = throttled_func_options::filetime_duration;
|
||||
using function = std::function<void(Args...)>;
|
||||
|
||||
// Throttles invocations to the given `func` to not occur more often than `delay`.
|
||||
// Throttles invocations to the given `func` to not occur more often than specified in options.
|
||||
//
|
||||
// If this is a:
|
||||
// * throttled_func_leading: `func` will be invoked immediately and
|
||||
// further invocations prevented until `delay` time has passed.
|
||||
// * throttled_func_trailing: On the first invocation a timer of `delay` time will
|
||||
// be started. After the timer has expired `func` will be invoked just once.
|
||||
// Options:
|
||||
// * delay: The minimum time between invocations
|
||||
// * debounce: If true, resets the timer on each call
|
||||
// * leading: If true, `func` will be invoked immediately on first call
|
||||
// * trailing: If true, `func` will be invoked after the delay
|
||||
//
|
||||
// After `func` was invoked the state is reset and this cycle is repeated again.
|
||||
throttled_func(filetime_duration delay, function func) :
|
||||
// At least one of leading or trailing must be true.
|
||||
throttled_func(throttled_func_options opts, function func) :
|
||||
_func{ std::move(func) },
|
||||
_timer{ _createTimer() }
|
||||
_timer{ _create_timer() },
|
||||
_debounce{ opts.debounce },
|
||||
_leading{ opts.leading },
|
||||
_trailing{ opts.trailing }
|
||||
{
|
||||
const auto d = -delay.count();
|
||||
if (!_leading && !_trailing)
|
||||
{
|
||||
throw std::invalid_argument("neither leading nor trailing");
|
||||
}
|
||||
|
||||
const auto d = -opts.delay.count();
|
||||
if (d >= 0)
|
||||
{
|
||||
throw std::invalid_argument("non-positive delay specified");
|
||||
}
|
||||
|
||||
memcpy(&_delay, &d, sizeof(d));
|
||||
}
|
||||
|
||||
@ -139,27 +68,7 @@ namespace til
|
||||
template<typename... MakeArgs>
|
||||
void operator()(MakeArgs&&... args)
|
||||
{
|
||||
const auto hadValue = _storage.emplace(std::forward<MakeArgs>(args)...);
|
||||
|
||||
if constexpr (Debounce)
|
||||
{
|
||||
SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!hadValue)
|
||||
{
|
||||
SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (Leading)
|
||||
{
|
||||
if (!hadValue)
|
||||
{
|
||||
_func();
|
||||
}
|
||||
}
|
||||
_lead(std::make_tuple(std::forward<MakeArgs>(args)...));
|
||||
}
|
||||
|
||||
// Modifies the pending arguments for the next function
|
||||
@ -170,7 +79,11 @@ namespace til
|
||||
template<typename F>
|
||||
void modify_pending(F func)
|
||||
{
|
||||
_storage.modify_pending(func);
|
||||
const auto guard = _lock.lock_exclusive();
|
||||
if (_pendingRunArgs)
|
||||
{
|
||||
std::apply(func, *_pendingRunArgs);
|
||||
}
|
||||
}
|
||||
|
||||
// Makes sure that the currently pending timer is executed
|
||||
@ -190,36 +103,76 @@ namespace til
|
||||
static void __stdcall _timer_callback(PTP_CALLBACK_INSTANCE /*instance*/, PVOID context, PTP_TIMER /*timer*/) noexcept
|
||||
try
|
||||
{
|
||||
const auto self = static_cast<throttled_func*>(context);
|
||||
if constexpr (Leading)
|
||||
{
|
||||
self->_storage.reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
self->_storage.apply(self->_func);
|
||||
}
|
||||
static_cast<throttled_func*>(context)->_trail();
|
||||
}
|
||||
CATCH_LOG()
|
||||
|
||||
wil::unique_threadpool_timer _createTimer()
|
||||
wil::unique_threadpool_timer _create_timer()
|
||||
{
|
||||
wil::unique_threadpool_timer timer{ CreateThreadpoolTimer(&_timer_callback, this, nullptr) };
|
||||
THROW_LAST_ERROR_IF(!timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
FILETIME _delay;
|
||||
void _lead(std::tuple<Args...> args)
|
||||
{
|
||||
bool timerRunning = false;
|
||||
|
||||
{
|
||||
const auto guard = _lock.lock_exclusive();
|
||||
|
||||
timerRunning = _timerRunning;
|
||||
_timerRunning = true;
|
||||
|
||||
if (!timerRunning && _leading)
|
||||
{
|
||||
// Call the function immediately on the leading edge.
|
||||
// See below (out of lock).
|
||||
}
|
||||
else if (_trailing)
|
||||
{
|
||||
_pendingRunArgs.emplace(std::move(args));
|
||||
}
|
||||
}
|
||||
|
||||
if (!timerRunning && _leading)
|
||||
{
|
||||
std::apply(_func, std::move(args));
|
||||
}
|
||||
|
||||
if (!timerRunning || _debounce)
|
||||
{
|
||||
SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void _trail()
|
||||
{
|
||||
decltype(_pendingRunArgs) args;
|
||||
|
||||
{
|
||||
const auto guard = _lock.lock_exclusive();
|
||||
|
||||
_timerRunning = false;
|
||||
args = std::exchange(_pendingRunArgs, std::nullopt);
|
||||
}
|
||||
|
||||
if (args)
|
||||
{
|
||||
std::apply(_func, *std::move(args));
|
||||
}
|
||||
}
|
||||
|
||||
function _func;
|
||||
wil::unique_threadpool_timer _timer;
|
||||
details::throttled_func_storage<Args...> _storage;
|
||||
FILETIME _delay;
|
||||
|
||||
wil::srwlock _lock;
|
||||
std::optional<std::tuple<Args...>> _pendingRunArgs;
|
||||
bool _timerRunning = false;
|
||||
|
||||
bool _debounce;
|
||||
bool _leading;
|
||||
bool _trailing;
|
||||
};
|
||||
|
||||
template<typename... Args>
|
||||
using throttled_func_trailing = throttled_func<false, false, Args...>;
|
||||
using throttled_func_leading = throttled_func<false, true>;
|
||||
|
||||
template<typename... Args>
|
||||
using debounced_func_trailing = throttled_func<true, false, Args...>;
|
||||
using debounced_func_leading = throttled_func<true, true>;
|
||||
} // namespace til
|
||||
|
||||
@ -19,21 +19,25 @@ class ThrottledFuncTests
|
||||
TEST_METHOD(Basic)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
using throttled_func = til::throttled_func_trailing<bool>;
|
||||
using throttled_func = til::throttled_func<bool>;
|
||||
|
||||
til::latch latch{ 2 };
|
||||
|
||||
std::unique_ptr<throttled_func> tf;
|
||||
tf = std::make_unique<throttled_func>(10ms, [&](bool reschedule) {
|
||||
latch.count_down();
|
||||
tf = std::make_unique<throttled_func>(
|
||||
til::throttled_func_options{
|
||||
.delay = 10ms,
|
||||
.trailing = true },
|
||||
[&](bool reschedule) {
|
||||
latch.count_down();
|
||||
|
||||
// This will ensure that the callback is called even if we
|
||||
// invoke the throttled_func from inside the callback itself.
|
||||
if (reschedule)
|
||||
{
|
||||
tf->operator()(false);
|
||||
}
|
||||
});
|
||||
// This will ensure that the callback is called even if we
|
||||
// invoke the throttled_func from inside the callback itself.
|
||||
if (reschedule)
|
||||
{
|
||||
tf->operator()(false);
|
||||
}
|
||||
});
|
||||
// This will ensure that the throttled_func invokes the callback in general.
|
||||
tf->operator()(true);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user