Skip to content

Commit 903f3c6

Browse files
committed
Fix output stuttering using a ticket lock
1 parent d13c37c commit 903f3c6

7 files changed

Lines changed: 91 additions & 22 deletions

File tree

src/cascadia/PublicTerminalCore/HwndTerminal.cpp

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ HwndTerminal::HwndTerminal(HWND parentHwnd) :
172172
_desiredFont{ L"Consolas", 0, DEFAULT_FONT_WEIGHT, { 0, 14 }, CP_UTF8 },
173173
_actualFont{ L"Consolas", 0, DEFAULT_FONT_WEIGHT, { 0, 14 }, CP_UTF8, false },
174174
_uiaProvider{ nullptr },
175-
_uiaProviderInitialized{ false },
176175
_currentDpi{ USER_DEFAULT_SCREEN_DPI },
177176
_pfnWriteCallback{ nullptr },
178177
_multiClickTime{ 500 } // this will be overwritten by the windows system double-click time
@@ -324,26 +323,18 @@ void HwndTerminal::_UpdateFont(int newDpi)
324323

325324
IRawElementProviderSimple* HwndTerminal::_GetUiaProvider() noexcept
326325
{
327-
if (nullptr == _uiaProvider && !_uiaProviderInitialized)
326+
if (!_uiaProvider)
328327
{
329-
std::unique_lock<std::shared_mutex> lock;
330328
try
331329
{
332-
#pragma warning(suppress : 26441) // The lock is named, this appears to be a false positive
333-
lock = _terminal->LockForWriting();
334-
if (_uiaProviderInitialized)
335-
{
336-
return _uiaProvider.Get();
337-
}
338-
330+
auto lock = _terminal->LockForWriting();
339331
LOG_IF_FAILED(::Microsoft::WRL::MakeAndInitialize<::Microsoft::Terminal::TermControlUiaProvider>(&_uiaProvider, this->GetUiaData(), this));
340332
}
341333
catch (...)
342334
{
343335
LOG_HR(wil::ResultFromCaughtException());
344336
_uiaProvider = nullptr;
345337
}
346-
_uiaProviderInitialized = true;
347338
}
348339

349340
return _uiaProvider.Get();

src/cascadia/PublicTerminalCore/HwndTerminal.hpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo
7373
FontInfoDesired _desiredFont;
7474
FontInfo _actualFont;
7575
int _currentDpi;
76-
bool _uiaProviderInitialized;
7776
std::function<void(wchar_t*)> _pfnWriteCallback;
7877
::Microsoft::WRL::ComPtr<::Microsoft::Terminal::TermControlUiaProvider> _uiaProvider;
7978

src/cascadia/TerminalCore/Terminal.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -866,19 +866,19 @@ WORD Terminal::_TakeVirtualKeyFromLastKeyEvent(const WORD scanCode) noexcept
866866
// Return Value:
867867
// - a shared_lock which can be used to unlock the terminal. The shared_lock
868868
// will release this lock when it's destructed.
869-
[[nodiscard]] std::shared_lock<std::shared_mutex> Terminal::LockForReading()
869+
[[nodiscard]] std::unique_lock<til::ticket_lock> Terminal::LockForReading()
870870
{
871-
return std::shared_lock<std::shared_mutex>(_readWriteLock);
871+
return std::unique_lock{ _readWriteLock };
872872
}
873873

874874
// Method Description:
875875
// - Acquire a write lock on the terminal.
876876
// Return Value:
877877
// - a unique_lock which can be used to unlock the terminal. The unique_lock
878878
// will release this lock when it's destructed.
879-
[[nodiscard]] std::unique_lock<std::shared_mutex> Terminal::LockForWriting()
879+
[[nodiscard]] std::unique_lock<til::ticket_lock> Terminal::LockForWriting()
880880
{
881-
return std::unique_lock<std::shared_mutex>(_readWriteLock);
881+
return std::unique_lock{ _readWriteLock };
882882
}
883883

884884
Viewport Terminal::_GetMutableViewport() const noexcept

src/cascadia/TerminalCore/Terminal.hpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
#include "../../cascadia/terminalcore/ITerminalApi.hpp"
1919
#include "../../cascadia/terminalcore/ITerminalInput.hpp"
2020

21+
#include <til/ticket_lock.h>
22+
2123
static constexpr std::wstring_view linkPattern{ LR"(\b(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|$!:,.;]*[A-Za-z0-9+&@#/%=~_|$])" };
2224
static constexpr size_t TaskbarMinProgress{ 10 };
2325

@@ -77,8 +79,8 @@ class Microsoft::Terminal::Core::Terminal final :
7779
// WritePastedText goes directly to the connection
7880
void WritePastedText(std::wstring_view stringView);
7981

80-
[[nodiscard]] std::shared_lock<std::shared_mutex> LockForReading();
81-
[[nodiscard]] std::unique_lock<std::shared_mutex> LockForWriting();
82+
[[nodiscard]] std::unique_lock<til::ticket_lock> LockForReading();
83+
[[nodiscard]] std::unique_lock<til::ticket_lock> LockForWriting();
8284

8385
short GetBufferHeight() const noexcept;
8486

@@ -244,6 +246,15 @@ class Microsoft::Terminal::Core::Terminal final :
244246
std::function<void()> _pfnWarningBell;
245247
std::function<void(std::wstring_view)> _pfnTitleChanged;
246248
std::function<void(std::wstring_view)> _pfnCopyToClipboard;
249+
250+
// I've specifically put this instance here as it requires
251+
// alignas(std::hardware_destructive_interference_size)
252+
// for best performance.
253+
//
254+
// But we can abuse the fact that the surrounding members rarely change and are huge
255+
// (std::function is like 64 bytes) to create some natural padding without wasting space.
256+
til::ticket_lock _readWriteLock;
257+
247258
std::function<void(const int, const int, const int)> _pfnScrollPositionChanged;
248259
std::function<void(const til::color)> _pfnBackgroundColorChanged;
249260
std::function<void()> _pfnCursorPositionChanged;
@@ -299,8 +310,6 @@ class Microsoft::Terminal::Core::Terminal final :
299310
SelectionExpansionMode _multiClickSelectionMode;
300311
#pragma endregion
301312

302-
std::shared_mutex _readWriteLock;
303-
304313
// TODO: These members are not shared by an alt-buffer. They should be
305314
// encapsulated, such that a Terminal can have both a main and alt buffer.
306315
std::unique_ptr<TextBuffer> _buffer;

src/cascadia/TerminalCore/terminalrenderdata.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,14 @@ catch (...)
230230
// they're done with any querying they need to do.
231231
void Terminal::LockConsole() noexcept
232232
{
233-
_readWriteLock.lock_shared();
233+
_readWriteLock.lock();
234234
}
235235

236236
// Method Description:
237237
// - Unlocks the terminal after a call to Terminal::LockConsole.
238238
void Terminal::UnlockConsole() noexcept
239239
{
240-
_readWriteLock.unlock_shared();
240+
_readWriteLock.unlock();
241241
}
242242

243243
// Method Description:

src/inc/til/atomic.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
#pragma once
5+
6+
namespace til
7+
{
8+
template<typename T>
9+
inline void atomic_wait(const std::atomic<T>& atomic, const T& current, DWORD waitMilliseconds = INFINITE)
10+
{
11+
static_assert(sizeof(atomic) == sizeof(current));
12+
WaitOnAddress(const_cast<std::atomic<T>*>(&atomic), const_cast<T*>(&current), sizeof(current), waitMilliseconds);
13+
}
14+
15+
template<typename T>
16+
inline void atomic_notify_one(const std::atomic<T>& atomic)
17+
{
18+
WakeByAddressSingle(const_cast<std::atomic<T>*>(&atomic));
19+
}
20+
21+
template<typename T>
22+
inline void atomic_notify_all(const std::atomic<T>& atomic)
23+
{
24+
WakeByAddressAll(const_cast<std::atomic<T>*>(&atomic));
25+
}
26+
}

src/inc/til/ticket_lock.h

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
#pragma once
5+
6+
#include "atomic.h"
7+
8+
namespace til
9+
{
10+
// The use of alignas(std::hardware_destructive_interference_size) with this class is highly suggested.
11+
struct ticket_lock
12+
{
13+
void lock()
14+
{
15+
const auto ticket = _next_ticket.fetch_add(1, std::memory_order_relaxed);
16+
17+
for (;;)
18+
{
19+
const auto current = _now_serving.load(std::memory_order_acquire);
20+
if (current == ticket)
21+
{
22+
break;
23+
}
24+
25+
til::atomic_wait(_now_serving, current);
26+
}
27+
}
28+
29+
void unlock()
30+
{
31+
_now_serving.fetch_add(1, std::memory_order_release);
32+
til::atomic_notify_all(_now_serving);
33+
}
34+
35+
private:
36+
// You may be inclined to add alignas(std::hardware_destructive_interference_size)
37+
// here to force the two atomics on separate cache lines, but I suggest to carefully
38+
// benchmark such a change. Since this ticket_lock is primarily used to synchronize
39+
// exactly 2 threads, it actually helps us that these atomic are on the same cache line
40+
// as any change by one thread is flushed to the other, which will then read it anyways.
41+
std::atomic<uint32_t> _next_ticket{ 0 };
42+
std::atomic<uint32_t> _now_serving{ 0 };
43+
};
44+
}

0 commit comments

Comments
 (0)