Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions src/cascadia/PublicTerminalCore/HwndTerminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ HwndTerminal::HwndTerminal(HWND parentHwnd) :
_desiredFont{ L"Consolas", 0, DEFAULT_FONT_WEIGHT, { 0, 14 }, CP_UTF8 },
_actualFont{ L"Consolas", 0, DEFAULT_FONT_WEIGHT, { 0, 14 }, CP_UTF8, false },
_uiaProvider{ nullptr },
_uiaProviderInitialized{ false },
_currentDpi{ USER_DEFAULT_SCREEN_DPI },
_pfnWriteCallback{ nullptr },
_multiClickTime{ 500 } // this will be overwritten by the windows system double-click time
Expand Down Expand Up @@ -324,26 +323,18 @@ void HwndTerminal::_UpdateFont(int newDpi)

IRawElementProviderSimple* HwndTerminal::_GetUiaProvider() noexcept
{
if (nullptr == _uiaProvider && !_uiaProviderInitialized)
if (!_uiaProvider)
{
std::unique_lock<std::shared_mutex> lock;
try
{
#pragma warning(suppress : 26441) // The lock is named, this appears to be a false positive
lock = _terminal->LockForWriting();
if (_uiaProviderInitialized)
{
return _uiaProvider.Get();
}

auto lock = _terminal->LockForWriting();
LOG_IF_FAILED(::Microsoft::WRL::MakeAndInitialize<::Microsoft::Terminal::TermControlUiaProvider>(&_uiaProvider, this->GetUiaData(), this));
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
_uiaProvider = nullptr;
}
_uiaProviderInitialized = true;
}

return _uiaProvider.Get();
Expand Down
1 change: 0 additions & 1 deletion src/cascadia/PublicTerminalCore/HwndTerminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo
FontInfoDesired _desiredFont;
FontInfo _actualFont;
int _currentDpi;
bool _uiaProviderInitialized;
std::function<void(wchar_t*)> _pfnWriteCallback;
::Microsoft::WRL::ComPtr<::Microsoft::Terminal::TermControlUiaProvider> _uiaProvider;

Expand Down
8 changes: 4 additions & 4 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -866,19 +866,19 @@ WORD Terminal::_TakeVirtualKeyFromLastKeyEvent(const WORD scanCode) noexcept
// Return Value:
// - a shared_lock which can be used to unlock the terminal. The shared_lock
// will release this lock when it's destructed.
[[nodiscard]] std::shared_lock<std::shared_mutex> Terminal::LockForReading()
[[nodiscard]] std::unique_lock<til::ticket_lock> Terminal::LockForReading()
{
return std::shared_lock<std::shared_mutex>(_readWriteLock);
return std::unique_lock{ _readWriteLock };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no difference any more between Locking for Writing and Reading thanks to the mechanics of the ticket lock... wouldn't it make sense to have one call the other to make that super clear until the future revision where you build that more complicated fair read/write lock for funsies?

}

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

Viewport Terminal::_GetMutableViewport() const noexcept
Expand Down
17 changes: 13 additions & 4 deletions src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
#include "../../cascadia/terminalcore/ITerminalApi.hpp"
#include "../../cascadia/terminalcore/ITerminalInput.hpp"

#include <til/ticket_lock.h>

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

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

[[nodiscard]] std::shared_lock<std::shared_mutex> LockForReading();
[[nodiscard]] std::unique_lock<std::shared_mutex> LockForWriting();
[[nodiscard]] std::unique_lock<til::ticket_lock> LockForReading();
[[nodiscard]] std::unique_lock<til::ticket_lock> LockForWriting();

short GetBufferHeight() const noexcept;

Expand Down Expand Up @@ -244,6 +246,15 @@ class Microsoft::Terminal::Core::Terminal final :
std::function<void()> _pfnWarningBell;
std::function<void(std::wstring_view)> _pfnTitleChanged;
std::function<void(std::wstring_view)> _pfnCopyToClipboard;

// I've specifically put this instance here as it requires
// alignas(std::hardware_destructive_interference_size)
// for best performance.
//
// But we can abuse the fact that the surrounding members rarely change and are huge
// (std::function is like 64 bytes) to create some natural padding without wasting space.
til::ticket_lock _readWriteLock;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add the alignas now, would that make it safer (in case somebody changes the order) and not compromise the layout by adding wasted padding space?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ask because... this is a micro-optimization somebody could easily accidentally remove or not fully understand and fail to maintain.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alignas would add 56 bytes of padding space and force _readWriteLock to be 64 bytes large.
I had hoped that the comment I added there would be enough to prevent others from accidentally changing the order. It someone would move it to be next to the later members in this class it would double the overhead of the ticket lock (from 0.03% to 0.06% CPU usage), but I think the comment should be an okay-ish reminder to not do that. I hope?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the comment is OK. I wonder if we'd ever be possessed to remove all the std::functions though and then the trick wouldn't work anymore.


std::function<void(const int, const int, const int)> _pfnScrollPositionChanged;
std::function<void(const til::color)> _pfnBackgroundColorChanged;
std::function<void()> _pfnCursorPositionChanged;
Expand Down Expand Up @@ -299,8 +310,6 @@ class Microsoft::Terminal::Core::Terminal final :
SelectionExpansionMode _multiClickSelectionMode;
#pragma endregion

std::shared_mutex _readWriteLock;

// TODO: These members are not shared by an alt-buffer. They should be
// encapsulated, such that a Terminal can have both a main and alt buffer.
std::unique_ptr<TextBuffer> _buffer;
Expand Down
4 changes: 2 additions & 2 deletions src/cascadia/TerminalCore/terminalrenderdata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,14 @@ catch (...)
// they're done with any querying they need to do.
void Terminal::LockConsole() noexcept
{
_readWriteLock.lock_shared();
_readWriteLock.lock();
}

// Method Description:
// - Unlocks the terminal after a call to Terminal::LockConsole.
void Terminal::UnlockConsole() noexcept
{
_readWriteLock.unlock_shared();
_readWriteLock.unlock();
}

// Method Description:
Expand Down
29 changes: 29 additions & 0 deletions src/inc/til/atomic.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#pragma once

namespace til
{
template<typename T>
inline void atomic_wait(const std::atomic<T>& atomic, const T& current, DWORD waitMilliseconds = INFINITE) noexcept
{
static_assert(sizeof(atomic) == sizeof(current));
#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile
WaitOnAddress(const_cast<std::atomic<T>*>(&atomic), const_cast<T*>(&current), sizeof(current), waitMilliseconds);
}

template<typename T>
inline void atomic_notify_one(const std::atomic<T>& atomic) noexcept
{
#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile
WakeByAddressSingle(const_cast<std::atomic<T>*>(&atomic));
}

template<typename T>
inline void atomic_notify_all(const std::atomic<T>& atomic) noexcept
{
#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile
WakeByAddressAll(const_cast<std::atomic<T>*>(&atomic));
}
}
44 changes: 44 additions & 0 deletions src/inc/til/ticket_lock.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#pragma once

#include "atomic.h"

namespace til
{
// The use of alignas(std::hardware_destructive_interference_size) with this class is highly suggested.
struct ticket_lock
{
void lock() noexcept
{
const auto ticket = _next_ticket.fetch_add(1, std::memory_order_relaxed);

for (;;)
{
const auto current = _now_serving.load(std::memory_order_acquire);
if (current == ticket)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is provably no case where now_serving will jump multiple steps above the expected ticket value, correct?

Copy link
Member Author

@lhecker lhecker Jul 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible, if someone where to call unlock() multiple times.
But if you don't, it works exactly like the Wikipedia article describes the algorithm.

{
break;
}

til::atomic_wait(_now_serving, current);
}
}

void unlock() noexcept
{
_now_serving.fetch_add(1, std::memory_order_release);
til::atomic_notify_all(_now_serving);
}

private:
// You may be inclined to add alignas(std::hardware_destructive_interference_size)
// here to force the two atomics on separate cache lines, but I suggest to carefully
// benchmark such a change. Since this ticket_lock is primarily used to synchronize
// exactly 2 threads, it actually helps us that these atomic are on the same cache line
// as any change by one thread is flushed to the other, which will then read it anyways.
std::atomic<uint32_t> _next_ticket{ 0 };
std::atomic<uint32_t> _now_serving{ 0 };
};
}