diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt
index 57d475dc87..dc0f987c29 100644
--- a/.github/actions/spelling/excludes.txt
+++ b/.github/actions/spelling/excludes.txt
@@ -115,6 +115,7 @@
^src/terminal/parser/ut_parser/Base64Test.cpp$
^src/terminal/parser/ut_parser/run\.bat$
^src/tools/benchcat
+^src/tools/ConsoleBench
^src/tools/integrity/dirs$
^src/tools/integrity/packageuwp/ConsoleUWP\.appxSources$
^src/tools/RenderingTests/main\.cpp$
diff --git a/OpenConsole.sln b/OpenConsole.sln
index 1717f4cc8a..e699c77cec 100644
--- a/OpenConsole.sln
+++ b/OpenConsole.sln
@@ -417,6 +417,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "benchcat", "src\tools\bench
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ConsoleMonitor", "src\tools\ConsoleMonitor\ConsoleMonitor.vcxproj", "{328729E9-6723-416E-9C98-951F1473BBE1}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ConsoleBench", "src\tools\ConsoleBench\ConsoleBench.vcxproj", "{BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
AuditMode|Any CPU = AuditMode|Any CPU
@@ -2387,6 +2389,26 @@ Global
{328729E9-6723-416E-9C98-951F1473BBE1}.Release|ARM64.ActiveCfg = Release|ARM64
{328729E9-6723-416E-9C98-951F1473BBE1}.Release|x64.ActiveCfg = Release|x64
{328729E9-6723-416E-9C98-951F1473BBE1}.Release|x86.ActiveCfg = Release|Win32
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.AuditMode|Any CPU.ActiveCfg = Debug|Win32
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.AuditMode|ARM64.ActiveCfg = Debug|ARM64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.AuditMode|x64.ActiveCfg = Debug|x64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.AuditMode|x86.ActiveCfg = Debug|Win32
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Debug|Any CPU.ActiveCfg = Debug|Win32
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Debug|ARM64.Build.0 = Debug|ARM64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Debug|x64.ActiveCfg = Debug|x64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Debug|x64.Build.0 = Debug|x64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Debug|x86.ActiveCfg = Debug|Win32
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Fuzzing|Any CPU.ActiveCfg = Debug|Win32
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Fuzzing|x64.ActiveCfg = Debug|x64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Fuzzing|x86.ActiveCfg = Debug|Win32
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Release|Any CPU.ActiveCfg = Release|Win32
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Release|ARM64.ActiveCfg = Release|ARM64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Release|ARM64.Build.0 = Release|ARM64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Release|x64.ActiveCfg = Release|x64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Release|x64.Build.0 = Release|x64
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48}.Release|x86.ActiveCfg = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2492,6 +2514,7 @@ Global
{37C995E0-2349-4154-8E77-4A52C0C7F46D} = {A10C4720-DCA4-4640-9749-67F4314F527C}
{2C836962-9543-4CE5-B834-D28E1F124B66} = {A10C4720-DCA4-4640-9749-67F4314F527C}
{328729E9-6723-416E-9C98-951F1473BBE1} = {A10C4720-DCA4-4640-9749-67F4314F527C}
+ {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48} = {A10C4720-DCA4-4640-9749-67F4314F527C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3140B1B7-C8EE-43D1-A772-D82A7061A271}
diff --git a/src/tools/ConsoleBench/ConsoleBench.vcxproj b/src/tools/ConsoleBench/ConsoleBench.vcxproj
new file mode 100644
index 0000000000..1e66412cc6
--- /dev/null
+++ b/src/tools/ConsoleBench/ConsoleBench.vcxproj
@@ -0,0 +1,39 @@
+
+
+
+ {be92101c-04f8-48da-99f0-e1f4f1d2dc48}
+ Win32Proj
+ ConsoleBench
+ ConsoleBench
+ ConsoleBench
+ Application
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+
+
+
+
+
+
+ false
+ pch.h
+
+
+ Console
+
+
+
+
+
\ No newline at end of file
diff --git a/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters b/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters
new file mode 100644
index 0000000000..25a667f990
--- /dev/null
+++ b/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters
@@ -0,0 +1,52 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
\ No newline at end of file
diff --git a/src/tools/ConsoleBench/arena.cpp b/src/tools/ConsoleBench/arena.cpp
new file mode 100644
index 0000000000..d91986c727
--- /dev/null
+++ b/src/tools/ConsoleBench/arena.cpp
@@ -0,0 +1,268 @@
+#include "pch.h"
+#include "arena.h"
+
+using namespace mem;
+
+Arena::Arena(size_t bytes)
+{
+ m_alloc = static_cast(THROW_IF_NULL_ALLOC(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)));
+}
+
+Arena::~Arena()
+{
+ VirtualFree(m_alloc, 0, MEM_RELEASE);
+}
+
+void Arena::clear()
+{
+ m_pos = 0;
+}
+
+size_t Arena::get_pos() const
+{
+ return m_pos;
+}
+
+void Arena::pop_to(size_t pos)
+{
+ if (m_pos <= pos)
+ {
+ return;
+ }
+
+#ifndef NDEBUG
+ memset(m_alloc + pos, 0xDD, m_pos - pos);
+#endif
+
+ m_pos = pos;
+}
+
+void* Arena::_push_raw(size_t bytes, size_t alignment)
+{
+ const auto mask = alignment - 1;
+ const auto pos = (m_pos + mask) & ~mask;
+ const auto ptr = m_alloc + pos;
+ m_pos = pos + bytes;
+ return ptr;
+}
+
+void* Arena::_push_zeroed(size_t bytes, size_t alignment)
+{
+ const auto ptr = _push_raw(bytes, alignment);
+ memset(ptr, 0, bytes);
+ return ptr;
+}
+
+void* Arena::_push_uninitialized(size_t bytes, size_t alignment)
+{
+ const auto ptr = _push_raw(bytes, alignment);
+#ifndef NDEBUG
+ memset(ptr, 0xCD, bytes);
+#endif
+ return ptr;
+}
+
+ScopedArena::ScopedArena(Arena& arena) :
+ arena{ arena },
+ m_pos_backup{ arena.get_pos() }
+{
+}
+
+ScopedArena::~ScopedArena()
+{
+ arena.pop_to(m_pos_backup);
+}
+
+static [[msvc::noinline]] std::array thread_arenas_init()
+{
+ return {
+ Arena{ 64 * 1024 * 1024 },
+ Arena{ 64 * 1024 * 1024 },
+ };
+}
+
+// This is based on an idea publicly described by Ryan Fleury as "scratch arena".
+// Assuming you have "persistent" data and "scratch" data, where the former is data that is returned to
+// the caller (= upwards) and the latter is data that is used locally, including calls (= downwards).
+// The fundamental realisation now is that regular, linear function calls (not coroutines) are sufficiently
+// covered with just N+1 arenas, where N is the number of in-flight "persistent" arenas across a call stack.
+// Often N is 1, because in most code, there's only 1 arena being passed as a parameter at a time.
+// This is also what this code specializes for.
+//
+// For instance, imagine you call A, which calls B, which calls C, and all 3 of those want to
+// return data and also allocate data for themselves, and that you have 2 arenas: 1 and 2.
+// Down in C the two arenas now look like this:
+// 1: [A (return)][B (local) ][C (return)]
+// 2: [A (local) ][B (return)][C (local) ]
+//
+// Now when each call returns and the arena's position is popped to the state before the call, this
+// interleaving ensures that you neither pop local data from, nor return data intended for a parent call.
+// After C returns:
+// 1: [A (return)][B (local) ][C (return)]
+// 2: [A (local) ][B (return)]
+// After B returns:
+// 1: [A (return)]
+// 2: [A (local) ][B (return)]
+// If this step confused you: B() got passed arena 2 from A() and uses arena 1 for local data.
+// When B() returns it restores this local arena to how it was before it used it, which means
+// that both, B's local data and C's return data are deallocated simultaneously. Neat!
+// After A returns:
+// 1: [A (return)]
+// 2:
+static ScopedArena get_scratch_arena_impl(const Arena* conflict)
+{
+ thread_local auto thread_arenas = thread_arenas_init();
+ auto& ta = thread_arenas;
+ return ScopedArena{ ta[conflict == &ta[0]] };
+}
+
+ScopedArena mem::get_scratch_arena()
+{
+ return get_scratch_arena_impl(nullptr);
+}
+
+ScopedArena mem::get_scratch_arena(const Arena& conflict)
+{
+ return get_scratch_arena_impl(&conflict);
+}
+
+#pragma warning(push)
+#pragma warning(disable : 4996)
+
+std::string_view mem::format(Arena& arena, const char* fmt, va_list args)
+{
+ auto len = _vsnprintf(nullptr, 0, fmt, args);
+ if (len < 0)
+ {
+ return {};
+ }
+
+ len++;
+ const auto buffer = arena.push_uninitialized(len);
+
+ len = _vsnprintf(buffer, len, fmt, args);
+ if (len < 0)
+ {
+ return {};
+ }
+
+ return { buffer, static_cast(len) };
+}
+
+std::string_view mem::format(Arena& arena, const char* fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ const auto str = format(arena, fmt, args);
+ va_end(args);
+ return str;
+}
+
+std::wstring_view mem::format(Arena& arena, const wchar_t* fmt, va_list args)
+{
+ auto len = _vsnwprintf(nullptr, 0, fmt, args);
+ if (len < 0)
+ {
+ return {};
+ }
+
+ len++;
+ const auto buffer = arena.push_uninitialized(len);
+
+ len = _vsnwprintf(buffer, len, fmt, args);
+ if (len < 0)
+ {
+ return {};
+ }
+
+ return { buffer, static_cast(len) };
+}
+
+std::wstring_view mem::format(Arena& arena, const wchar_t* fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ const auto str = format(arena, fmt, args);
+ va_end(args);
+ return str;
+}
+
+#pragma warning(pop)
+
+void mem::print_literal(const char* str)
+{
+ WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), str, static_cast(strlen(str)), nullptr, nullptr);
+}
+
+// printf() in the uCRT prints every single char individually and thus breaks surrogate
+// pairs apart which Windows Terminal treats as invalid input and replaces it with U+FFFD.
+void mem::print_format(Arena& arena, const char* fmt, ...)
+{
+ const auto scratch = get_scratch_arena(arena);
+
+ va_list args;
+ va_start(args, fmt);
+ const auto str = format(scratch.arena, fmt, args);
+ va_end(args);
+
+ WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), str.data(), static_cast(str.size()), nullptr, nullptr);
+}
+
+std::string_view mem::read_line(Arena& arena, size_t max_bytes)
+{
+ auto read = static_cast(max_bytes);
+ const auto buffer = arena.push_uninitialized(max_bytes);
+ if (!ReadConsoleA(GetStdHandle(STD_INPUT_HANDLE), buffer, read, &read, nullptr))
+ {
+ read = 0;
+ }
+ return { buffer, read };
+}
+
+std::wstring_view mem::u8u16(Arena& arena, const char* ptr, size_t count)
+{
+ if (count == 0 || count > INT_MAX)
+ {
+ return {};
+ }
+
+ const auto int_count = static_cast(count);
+ auto length = MultiByteToWideChar(CP_UTF8, 0, ptr, int_count, nullptr, 0);
+ if (length <= 0)
+ {
+ return {};
+ }
+
+ const auto buffer = arena.push_uninitialized(length);
+ length = MultiByteToWideChar(CP_UTF8, 0, ptr, int_count, buffer, length);
+ if (length <= 0)
+ {
+ return {};
+ }
+
+ return { buffer, static_cast(length) };
+}
+
+std::string_view mem::u16u8(Arena& arena, const wchar_t* ptr, size_t count)
+{
+ if (count == 0 || count > INT_MAX)
+ {
+ return {};
+ }
+
+ const auto int_count = static_cast(count);
+ auto length = WideCharToMultiByte(CP_UTF8, 0, ptr, int_count, nullptr, 0, nullptr, nullptr);
+ if (length <= 0)
+ {
+ return {};
+ }
+
+ const auto buffer = arena.push_uninitialized(length);
+ length = WideCharToMultiByte(CP_UTF8, 0, ptr, int_count, buffer, length, nullptr, nullptr);
+ if (length <= 0)
+ {
+ return {};
+ }
+
+ return { buffer, static_cast(length) };
+}
diff --git a/src/tools/ConsoleBench/arena.h b/src/tools/ConsoleBench/arena.h
new file mode 100644
index 0000000000..1519f03377
--- /dev/null
+++ b/src/tools/ConsoleBench/arena.h
@@ -0,0 +1,111 @@
+#pragma once
+
+namespace mem
+{
+ template
+ struct is_std_view : std::false_type
+ {
+ };
+ template
+ struct is_std_view> : std::true_type
+ {
+ };
+ template
+ struct is_std_view> : std::true_type
+ {
+ };
+
+ template
+ concept is_trivially_constructible = std::is_trivially_constructible_v || is_std_view>::value;
+
+ template
+ concept is_trivially_copyable = std::is_trivially_copyable_v;
+
+ struct Arena
+ {
+ explicit Arena(size_t bytes);
+ ~Arena();
+
+ void clear();
+ size_t get_pos() const;
+ void pop_to(size_t pos);
+
+ template
+ T* push_zeroed(size_t count = 1)
+ {
+ return static_cast(_push_zeroed(sizeof(T) * count, alignof(T)));
+ }
+
+ template
+ std::span push_zeroed_span(size_t count)
+ {
+ return { static_cast(_push_zeroed(sizeof(T) * count, alignof(T))), count };
+ }
+
+ template
+ T* push_uninitialized(size_t count = 1)
+ {
+ return static_cast(_push_uninitialized(sizeof(T) * count, alignof(T)));
+ }
+
+ template
+ std::span push_uninitialized_span(size_t count)
+ {
+ return { static_cast(_push_uninitialized(sizeof(T) * count, alignof(T))), count };
+ }
+
+ private:
+ void* _push_raw(size_t bytes, size_t alignment = __STDCPP_DEFAULT_NEW_ALIGNMENT__);
+ void* _push_zeroed(size_t bytes, size_t alignment = __STDCPP_DEFAULT_NEW_ALIGNMENT__);
+ void* _push_uninitialized(size_t bytes, size_t alignment = __STDCPP_DEFAULT_NEW_ALIGNMENT__);
+
+ uint8_t* m_alloc = nullptr;
+ size_t m_pos = 0;
+ };
+
+ struct ScopedArena
+ {
+ Arena& arena;
+
+ ScopedArena(Arena& arena);
+ ~ScopedArena();
+
+ private:
+ size_t m_pos_backup;
+ };
+
+ [[nodiscard]] ScopedArena get_scratch_arena();
+ [[nodiscard]] ScopedArena get_scratch_arena(const Arena& conflict);
+
+ std::string_view format(Arena& arena, _Printf_format_string_ const char* fmt, va_list args);
+ std::string_view format(Arena& arena, _Printf_format_string_ const char* fmt, ...);
+ std::wstring_view format(Arena& arena, _Printf_format_string_ const wchar_t* fmt, va_list args);
+ std::wstring_view format(Arena& arena, _Printf_format_string_ const wchar_t* fmt, ...);
+
+ void print_literal(const char* str);
+ void print_format(Arena& arena, _Printf_format_string_ const char* fmt, ...);
+ std::string_view read_line(Arena& arena, size_t max_bytes);
+
+ std::wstring_view u8u16(Arena& arena, const char* ptr, size_t count);
+ std::string_view u16u8(Arena& arena, const wchar_t* ptr, size_t count);
+
+ template
+ void copy(T* dst, const T* src, size_t count)
+ {
+ memcpy(dst, src, count * sizeof(T));
+ }
+
+ template
+ std::basic_string_view repeat_string(Arena& arena, std::basic_string_view in, size_t count)
+ {
+ const auto len = count * in.size();
+ const auto buf = arena.push_uninitialized(len);
+
+ for (size_t i = 0; i < count; ++i)
+ {
+ mem::copy(buf + i * in.size(), in.data(), in.size());
+ }
+
+ return { buf, len };
+ }
+}
diff --git a/src/tools/ConsoleBench/conhost.cpp b/src/tools/ConsoleBench/conhost.cpp
new file mode 100644
index 0000000000..95a0a3ea8a
--- /dev/null
+++ b/src/tools/ConsoleBench/conhost.cpp
@@ -0,0 +1,170 @@
+#include "pch.h"
+#include "conhost.h"
+
+#include
+#include
+
+#include "arena.h"
+
+static unique_nthandle conhostCreateHandle(HANDLE parent, const wchar_t* typeName, bool inherit, bool synchronous)
+{
+ UNICODE_STRING name;
+ RtlInitUnicodeString(&name, typeName);
+
+ ULONG attrFlags = OBJ_CASE_INSENSITIVE;
+ WI_SetFlagIf(attrFlags, OBJ_INHERIT, inherit);
+
+ OBJECT_ATTRIBUTES attr;
+ InitializeObjectAttributes(&attr, &name, attrFlags, parent, nullptr);
+
+ ULONG options = 0;
+ WI_SetFlagIf(options, FILE_SYNCHRONOUS_IO_NONALERT, synchronous);
+
+ HANDLE handle;
+ IO_STATUS_BLOCK ioStatus;
+ THROW_IF_NTSTATUS_FAILED(NtCreateFile(
+ /* FileHandle */ &handle,
+ /* DesiredAccess */ FILE_GENERIC_READ | FILE_GENERIC_WRITE,
+ /* ObjectAttributes */ &attr,
+ /* IoStatusBlock */ &ioStatus,
+ /* AllocationSize */ nullptr,
+ /* FileAttributes */ 0,
+ /* ShareAccess */ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ /* CreateDisposition */ FILE_CREATE,
+ /* CreateOptions */ options,
+ /* EaBuffer */ nullptr,
+ /* EaLength */ 0));
+ return unique_nthandle{ handle };
+}
+
+static void conhostCopyToStringBuffer(USHORT& length, auto& buffer, const wchar_t* str)
+{
+ const auto len = wcsnlen(str, sizeof(buffer) / sizeof(wchar_t)) * sizeof(wchar_t);
+ length = static_cast(len);
+ memcpy(buffer, str, len);
+}
+
+ConhostHandle spawn_conhost(mem::Arena& arena, const wchar_t* path)
+{
+ const auto scratch = mem::get_scratch_arena(arena);
+ const auto server = conhostCreateHandle(nullptr, L"\\Device\\ConDrv\\Server", true, false);
+ auto reference = conhostCreateHandle(server.get(), L"\\Reference", false, true);
+
+ {
+ const auto cmd = format(scratch.arena, LR"("%s" --server 0x%zx)", path, server.get());
+
+ uint8_t attrListBuffer[64];
+
+ STARTUPINFOEXW siEx{};
+ siEx.StartupInfo.cb = sizeof(STARTUPINFOEXW);
+ siEx.lpAttributeList = reinterpret_cast(&attrListBuffer[0]);
+
+ HANDLE inheritedHandles[1];
+ inheritedHandles[0] = server.get();
+
+ auto listSize = sizeof(attrListBuffer);
+ THROW_IF_WIN32_BOOL_FALSE(InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &listSize));
+ const auto cleanupProcThreadAttribute = wil::scope_exit([&]() noexcept {
+ DeleteProcThreadAttributeList(siEx.lpAttributeList);
+ });
+
+ THROW_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(
+ siEx.lpAttributeList,
+ 0,
+ PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
+ &inheritedHandles[0],
+ sizeof(inheritedHandles),
+ nullptr,
+ nullptr));
+
+ wil::unique_process_information pi;
+ THROW_IF_WIN32_BOOL_FALSE(CreateProcessW(
+ /* lpApplicationName */ nullptr,
+ /* lpCommandLine */ const_cast(cmd.data()),
+ /* lpProcessAttributes */ nullptr,
+ /* lpThreadAttributes */ nullptr,
+ /* bInheritHandles */ TRUE,
+ /* dwCreationFlags */ EXTENDED_STARTUPINFO_PRESENT,
+ /* lpEnvironment */ nullptr,
+ /* lpCurrentDirectory */ nullptr,
+ /* lpStartupInfo */ &siEx.StartupInfo,
+ /* lpProcessInformation */ pi.addressof()));
+ }
+
+ unique_nthandle connection;
+ {
+ UNICODE_STRING name;
+ RtlInitUnicodeString(&name, L"\\Connect");
+
+ OBJECT_ATTRIBUTES attr;
+ InitializeObjectAttributes(&attr, &name, OBJ_CASE_INSENSITIVE, reference.get(), nullptr);
+
+ CONSOLE_SERVER_MSG msg{};
+ {
+ // This must be RTL_USER_PROCESS_PARAMETERS::ProcessGroupId unless it's 0,
+ // but winternl doesn't know about that field. ;)
+ msg.ProcessGroupId = GetCurrentProcessId();
+ msg.ConsoleApp = TRUE;
+ msg.WindowVisible = TRUE;
+
+ conhostCopyToStringBuffer(msg.TitleLength, msg.Title, L"ConsoleBench.exe");
+ conhostCopyToStringBuffer(msg.ApplicationNameLength, msg.ApplicationName, L"ConsoleBench.exe");
+ conhostCopyToStringBuffer(msg.CurrentDirectoryLength, msg.CurrentDirectory, L"C:\\Windows\\System32\\");
+ }
+
+ // From wdm.h, but without the trailing `CHAR EaName[1];` field, as this makes
+ // appending the string at the end of the messages unnecessarily difficult.
+ struct FILE_FULL_EA_INFORMATION
+ {
+ ULONG NextEntryOffset;
+ UCHAR Flags;
+ UCHAR EaNameLength;
+ USHORT EaValueLength;
+ };
+ static constexpr auto bufferAppend = [](uint8_t* out, const auto& in) {
+ return static_cast(memcpy(out, &in, sizeof(in))) + sizeof(in);
+ };
+
+ alignas(__STDCPP_DEFAULT_NEW_ALIGNMENT__) uint8_t eaBuffer[2048];
+ auto eaBufferEnd = &eaBuffer[0];
+ // Curiously, EaNameLength is the length without \0,
+ // but the data payload only starts after the name *with* \0.
+ eaBufferEnd = bufferAppend(eaBufferEnd, FILE_FULL_EA_INFORMATION{ .EaNameLength = 6, .EaValueLength = sizeof(msg) });
+ eaBufferEnd = bufferAppend(eaBufferEnd, "server");
+ eaBufferEnd = bufferAppend(eaBufferEnd, msg);
+
+ IO_STATUS_BLOCK ioStatus;
+ THROW_IF_NTSTATUS_FAILED(NtCreateFile(
+ /* FileHandle */ connection.addressof(),
+ /* DesiredAccess */ FILE_GENERIC_READ | FILE_GENERIC_WRITE,
+ /* ObjectAttributes */ &attr,
+ /* IoStatusBlock */ &ioStatus,
+ /* AllocationSize */ nullptr,
+ /* FileAttributes */ 0,
+ /* ShareAccess */ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ /* CreateDisposition */ FILE_CREATE,
+ /* CreateOptions */ FILE_SYNCHRONOUS_IO_NONALERT,
+ /* EaBuffer */ &eaBuffer[0],
+ /* EaLength */ static_cast(eaBufferEnd - &eaBuffer[0])));
+ }
+
+ return ConhostHandle{
+ .reference = std::move(reference),
+ .connection = std::move(connection),
+ };
+}
+
+HANDLE get_active_connection()
+{
+ // (Not actually) FUN FACT! The handles don't mean anything and the cake is a lie!
+ // Whenever you call any console API function, the handle you pass is completely and entirely ignored.
+ // Instead, condrv will probe the PEB, extract the ConsoleHandle field and use that to send
+ // the message to the server (conhost). ConsoleHandle happens to be at Reserved2[0].
+ // Unfortunately, the reason for this horrifying approach has been lost to time.
+ return NtCurrentTeb()->ProcessEnvironmentBlock->ProcessParameters->Reserved2[0];
+}
+
+void set_active_connection(HANDLE connection)
+{
+ NtCurrentTeb()->ProcessEnvironmentBlock->ProcessParameters->Reserved2[0] = connection;
+}
diff --git a/src/tools/ConsoleBench/conhost.h b/src/tools/ConsoleBench/conhost.h
new file mode 100644
index 0000000000..e794eca555
--- /dev/null
+++ b/src/tools/ConsoleBench/conhost.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include
+
+namespace mem
+{
+ struct Arena;
+}
+
+using unique_nthandle = wil::unique_any_handle_null;
+
+struct ConhostHandle
+{
+ unique_nthandle reference;
+ unique_nthandle connection;
+};
+
+ConhostHandle spawn_conhost(mem::Arena& arena, const wchar_t* path);
+HANDLE get_active_connection();
+void set_active_connection(HANDLE connection);
diff --git a/src/tools/ConsoleBench/main.cpp b/src/tools/ConsoleBench/main.cpp
new file mode 100644
index 0000000000..4e380474eb
--- /dev/null
+++ b/src/tools/ConsoleBench/main.cpp
@@ -0,0 +1,555 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+
+#include "arena.h"
+#include "conhost.h"
+#include "utils.h"
+
+using Measurements = std::span;
+using MeasurementsPerBenchmark = std::span;
+
+struct BenchmarkContext
+{
+ HWND hwnd;
+ HANDLE input;
+ HANDLE output;
+ int64_t time_limit;
+ mem::Arena& arena;
+ std::string_view utf8_4Ki;
+ std::string_view utf8_128Ki;
+ std::wstring_view utf16_4Ki;
+ std::wstring_view utf16_128Ki;
+};
+
+struct Benchmark
+{
+ const char* title;
+ void (*exec)(const BenchmarkContext& ctx, Measurements measurements);
+};
+
+struct AccumulatedResults
+{
+ size_t trace_count;
+ // These are arrays of size trace_count
+ std::string_view* trace_names;
+ MeasurementsPerBenchmark* measurments;
+};
+
+constexpr int32_t perf_delta(int64_t beg, int64_t end)
+{
+ return static_cast(end - beg);
+}
+
+static constexpr Benchmark s_benchmarks[]{
+ Benchmark{
+ .title = "WriteConsoleA 4Ki",
+ .exec = [](const BenchmarkContext& ctx, Measurements measurements) {
+ for (auto& d : measurements)
+ {
+ const auto beg = query_perf_counter();
+ WriteConsoleA(ctx.output, ctx.utf8_4Ki.data(), static_cast(ctx.utf8_4Ki.size()), nullptr, nullptr);
+ const auto end = query_perf_counter();
+ d = perf_delta(beg, end);
+
+ if (end >= ctx.time_limit)
+ {
+ break;
+ }
+ }
+ },
+ },
+ Benchmark{
+ .title = "WriteConsoleW 4Ki",
+ .exec = [](const BenchmarkContext& ctx, Measurements measurements) {
+ for (auto& d : measurements)
+ {
+ const auto beg = query_perf_counter();
+ WriteConsoleW(ctx.output, ctx.utf16_4Ki.data(), static_cast(ctx.utf16_4Ki.size()), nullptr, nullptr);
+ const auto end = query_perf_counter();
+ d = perf_delta(beg, end);
+
+ if (end >= ctx.time_limit)
+ {
+ break;
+ }
+ }
+ },
+ },
+ Benchmark{
+ .title = "WriteConsoleA 128Ki",
+ .exec = [](const BenchmarkContext& ctx, Measurements measurements) {
+ for (auto& d : measurements)
+ {
+ const auto beg = query_perf_counter();
+ WriteConsoleA(ctx.output, ctx.utf8_128Ki.data(), static_cast(ctx.utf8_128Ki.size()), nullptr, nullptr);
+ const auto end = query_perf_counter();
+ d = perf_delta(beg, end);
+
+ if (end >= ctx.time_limit)
+ {
+ break;
+ }
+ }
+ },
+ },
+ Benchmark{
+ .title = "WriteConsoleW 128Ki",
+ .exec = [](const BenchmarkContext& ctx, Measurements measurements) {
+ for (auto& d : measurements)
+ {
+ const auto beg = query_perf_counter();
+ WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr);
+ const auto end = query_perf_counter();
+ d = perf_delta(beg, end);
+
+ if (end >= ctx.time_limit)
+ {
+ break;
+ }
+ }
+ },
+ },
+ Benchmark{
+ .title = "Copy to clipboard 4Ki",
+ .exec = [](const BenchmarkContext& ctx, Measurements measurements) {
+ WriteConsoleW(ctx.output, ctx.utf16_4Ki.data(), static_cast(ctx.utf8_4Ki.size()), nullptr, nullptr);
+
+ for (auto& d : measurements)
+ {
+ SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF5 /* ID_CONSOLE_SELECTALL */, 0);
+
+ const auto beg = query_perf_counter();
+ SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF0 /* ID_CONSOLE_COPY */, 0);
+ const auto end = query_perf_counter();
+ d = perf_delta(beg, end);
+
+ if (end >= ctx.time_limit)
+ {
+ break;
+ }
+ }
+ },
+ },
+ Benchmark{
+ .title = "Paste from clipboard 4Ki",
+ .exec = [](const BenchmarkContext& ctx, Measurements measurements) {
+ set_clipboard(ctx.hwnd, ctx.utf16_4Ki);
+ FlushConsoleInputBuffer(ctx.input);
+
+ for (auto& d : measurements)
+ {
+ const auto beg = query_perf_counter();
+ SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF1 /* ID_CONSOLE_PASTE */, 0);
+ const auto end = query_perf_counter();
+ d = perf_delta(beg, end);
+
+ FlushConsoleInputBuffer(ctx.input);
+
+ if (end >= ctx.time_limit)
+ {
+ break;
+ }
+ }
+ },
+ },
+ Benchmark{
+ .title = "ReadConsoleInputW clipboard 4Ki",
+ .exec = [](const BenchmarkContext& ctx, Measurements measurements) {
+ static constexpr DWORD cap = 16 * 1024;
+
+ const auto scratch = mem::get_scratch_arena(ctx.arena);
+ const auto buf = scratch.arena.push_uninitialized(cap);
+ DWORD read;
+
+ set_clipboard(ctx.hwnd, ctx.utf16_4Ki);
+ FlushConsoleInputBuffer(ctx.input);
+
+ for (auto& d : measurements)
+ {
+ SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF1 /* ID_CONSOLE_PASTE */, 0);
+
+ const auto beg = query_perf_counter();
+ ReadConsoleInputW(ctx.input, buf, cap, &read);
+ debugAssert(read >= 1024 && read < cap);
+ const auto end = query_perf_counter();
+ d = perf_delta(beg, end);
+
+ if (end >= ctx.time_limit)
+ {
+ break;
+ }
+ }
+ },
+ },
+};
+static constexpr size_t s_benchmarks_count = _countof(s_benchmarks);
+
+// Each of these strings is 128 columns.
+static constexpr std::string_view payload_utf8{ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labor眠い子猫はマグロ狩りの夢を見る" };
+static constexpr std::wstring_view payload_utf16{ L"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labor眠い子猫はマグロ狩りの夢を見る" };
+
+static bool print_warning();
+static AccumulatedResults* prepare_results(mem::Arena& arena, std::span paths);
+static std::span run_benchmarks_for_path(mem::Arena& arena, const wchar_t* path);
+static void generate_html(mem::Arena& arena, const AccumulatedResults* results);
+
+int wmain(int argc, const wchar_t* argv[])
+{
+ if (argc < 2)
+ {
+ mem::print_literal("Usage: %s [paths to conhost.exe]...");
+ return 1;
+ }
+
+ const auto cp = GetConsoleCP();
+ const auto output_cp = GetConsoleOutputCP();
+ const auto restore_cp = wil::scope_exit([&]() {
+ SetConsoleCP(cp);
+ SetConsoleOutputCP(output_cp);
+ });
+ SetConsoleCP(CP_UTF8);
+ SetConsoleOutputCP(CP_UTF8);
+
+ const auto scratch = mem::get_scratch_arena();
+
+ const std::span paths{ &argv[1], static_cast(argc) - 1 };
+ const auto results = prepare_results(scratch.arena, paths);
+ if (!results)
+ {
+ return 1;
+ }
+
+ if (!print_warning())
+ {
+ return 0;
+ }
+
+ for (size_t trace_idx = 0; trace_idx < results->trace_count; ++trace_idx)
+ {
+ const auto title = results->trace_names[trace_idx];
+ print_format(scratch.arena, "\r\n# %.*s\r\n", title.size(), title.data());
+ results->measurments[trace_idx] = run_benchmarks_for_path(scratch.arena, paths[trace_idx]);
+ }
+
+ generate_html(scratch.arena, results);
+ return 0;
+}
+
+static bool print_warning()
+{
+ mem::print_literal(
+ "This will overwrite any existing measurements.html in your current working directory.\r\n"
+ "\r\n"
+ "For best test results:\r\n"
+ "* Make sure your system is fully idle and your CPU cool\r\n"
+ "* Move your cursor to a corner of your screen and don't move it over the conhost window(s)\r\n"
+ "* Exit or stop any background applications, including Windows Defender (if possible)\r\n"
+ "\r\n"
+ "Continue? [Yn] ");
+
+ for (;;)
+ {
+ INPUT_RECORD rec;
+ DWORD read;
+ if (!ReadConsoleInputW(GetStdHandle(STD_INPUT_HANDLE), &rec, 1, &read) || read == 0)
+ {
+ return false;
+ }
+
+ if (rec.EventType == KEY_EVENT && rec.Event.KeyEvent.bKeyDown)
+ {
+ // Transforms the character to uppercase if it's lowercase.
+ const auto ch = rec.Event.KeyEvent.uChar.UnicodeChar & 0xDF;
+ if (ch == L'N')
+ {
+ return false;
+ }
+ if (ch == L'\r' || ch == L'Y')
+ {
+ break;
+ }
+ }
+ }
+
+ mem::print_literal("\r\n");
+ return true;
+}
+
+static AccumulatedResults* prepare_results(mem::Arena& arena, std::span paths)
+{
+ for (const auto path : paths)
+ {
+ const auto attr = GetFileAttributesW(path);
+ if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY) != 0)
+ {
+ print_format(arena, "Invalid path: %s\r\n", path);
+ return nullptr;
+ }
+ }
+
+ const auto trace_count = paths.size();
+ const auto results = arena.push_zeroed();
+
+ results->trace_count = trace_count;
+ results->trace_names = arena.push_zeroed(trace_count);
+ results->measurments = arena.push_zeroed(trace_count);
+
+ for (size_t trace_idx = 0; trace_idx < trace_count; ++trace_idx)
+ {
+ const auto path = paths[trace_idx];
+ auto trace_name = get_file_version(arena, path);
+
+ if (trace_name.empty())
+ {
+ const auto end = path + wcsnlen(path, SIZE_MAX);
+ auto filename = end;
+ for (; filename > path && filename[-1] != L'\\' && filename[-1] != L'/'; --filename)
+ {
+ }
+ trace_name = u16u8(arena, filename, end - filename);
+ }
+
+ results->trace_names[trace_idx] = trace_name;
+ }
+
+ return results;
+}
+
+static void prepare_conhost(const BenchmarkContext& ctx, HWND parent_hwnd)
+{
+ const auto scratch = mem::get_scratch_arena(ctx.arena);
+
+ SetForegroundWindow(parent_hwnd);
+
+ // Ensure conhost is in a consistent state with identical fonts and window sizes,
+ SetConsoleCP(CP_UTF8);
+ SetConsoleOutputCP(CP_UTF8);
+ SetConsoleMode(ctx.output, ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
+ {
+ CONSOLE_SCREEN_BUFFER_INFOEX info{
+ .cbSize = sizeof(info),
+ .dwSize = { 120, 9001 },
+ .wAttributes = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED,
+ .srWindow = { 0, 0, 119, 29 },
+ .dwMaximumWindowSize = { 120, 30 },
+ .wPopupAttributes = FOREGROUND_BLUE | FOREGROUND_RED | BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY,
+ .ColorTable = { 0x0C0C0C, 0x1F0FC5, 0x0EA113, 0x009CC1, 0xDA3700, 0x981788, 0xDD963A, 0xCCCCCC, 0x767676, 0x5648E7, 0x0CC616, 0xA5F1F9, 0xFF783B, 0x9E00B4, 0xD6D661, 0xF2F2F2 },
+ };
+ SetConsoleScreenBufferInfoEx(ctx.output, &info);
+ }
+ {
+ CONSOLE_FONT_INFOEX info{
+ .cbSize = sizeof(info),
+ .dwFontSize = { 0, 16 },
+ .FontFamily = 54,
+ .FontWeight = 400,
+ .FaceName = L"Consolas",
+ };
+ SetCurrentConsoleFontEx(ctx.output, FALSE, &info);
+ }
+
+ // Ensure conhost's backing TextBuffer is fully committed and initialized. There's currently no way
+ // to un-commit it and so not committing it now would be unfair for the first test that runs.
+ const auto buf = scratch.arena.push_uninitialized(9001);
+ memset(buf, '\n', 9001);
+ WriteFile(ctx.output, buf, 9001, nullptr, nullptr);
+}
+
+static std::span run_benchmarks_for_path(mem::Arena& arena, const wchar_t* path)
+{
+ const auto scratch = mem::get_scratch_arena(arena);
+ const auto parent_connection = get_active_connection();
+ const auto parent_hwnd = GetConsoleWindow();
+ const auto freq = query_perf_freq();
+
+ const auto handle = spawn_conhost(scratch.arena, path);
+ set_active_connection(handle.connection.get());
+
+ const auto print_with_parent_connection = [&](auto&&... args) {
+ set_active_connection(parent_connection);
+ mem::print_format(scratch.arena, args...);
+ set_active_connection(handle.connection.get());
+ };
+
+ BenchmarkContext ctx{
+ .hwnd = GetConsoleWindow(),
+ .input = GetStdHandle(STD_INPUT_HANDLE),
+ .output = GetStdHandle(STD_OUTPUT_HANDLE),
+ .arena = scratch.arena,
+ .utf8_4Ki = mem::repeat_string(scratch.arena, payload_utf8, 4 * 1024 / 128),
+ .utf8_128Ki = mem::repeat_string(scratch.arena, payload_utf8, 128 * 1024 / 128),
+ .utf16_4Ki = mem::repeat_string(scratch.arena, payload_utf16, 4 * 1024 / 128),
+ .utf16_128Ki = mem::repeat_string(scratch.arena, payload_utf16, 128 * 1024 / 128),
+ };
+
+ prepare_conhost(ctx, parent_hwnd);
+ Sleep(1000);
+
+ const auto results = arena.push_uninitialized_span(s_benchmarks_count);
+ for (auto& measurements : results)
+ {
+ measurements = arena.push_zeroed_span(2048);
+ }
+
+ for (size_t bench_idx = 0; bench_idx < s_benchmarks_count; ++bench_idx)
+ {
+ const auto& bench = s_benchmarks[bench_idx];
+ auto& measurements = results[bench_idx];
+
+ print_with_parent_connection("- %s", bench.title);
+
+ // Warmup for 0.1s.
+ WriteConsoleW(ctx.output, L"\033c", 2, nullptr, nullptr);
+ ctx.time_limit = query_perf_counter() + freq / 10;
+ bench.exec(ctx, measurements);
+
+ // Actual run for 1s.
+ WriteConsoleW(ctx.output, L"\033c", 2, nullptr, nullptr);
+ ctx.time_limit = query_perf_counter() + freq;
+ bench.exec(ctx, measurements);
+
+ // Trim off trailing 0s that resulted from the time_limit.
+ size_t len = measurements.size();
+ for (; len > 0 && measurements[len - 1] == 0; --len)
+ {
+ }
+ measurements = measurements.subspan(0, len);
+
+ print_with_parent_connection(", done\r\n");
+ }
+
+ set_active_connection(parent_connection);
+ return results;
+}
+
+static void generate_html(mem::Arena& arena, const AccumulatedResults* results)
+{
+ const auto scratch = mem::get_scratch_arena(arena);
+
+ const wil::unique_hfile file{ THROW_LAST_ERROR_IF_NULL(CreateFileW(L"measurements.html", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)) };
+ const auto sec_per_tick = 1.0f / query_perf_freq();
+ BufferedWriter writer{ file.get(), scratch.arena.push_uninitialized_span(1024 * 1024) };
+
+ writer.write(R"(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)");
+}
diff --git a/src/tools/ConsoleBench/pch.cpp b/src/tools/ConsoleBench/pch.cpp
new file mode 100644
index 0000000000..398a99f665
--- /dev/null
+++ b/src/tools/ConsoleBench/pch.cpp
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
diff --git a/src/tools/ConsoleBench/pch.h b/src/tools/ConsoleBench/pch.h
new file mode 100644
index 0000000000..ce01975430
--- /dev/null
+++ b/src/tools/ConsoleBench/pch.h
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#define NOMINMAX
+#define WIN32_LEAN_AND_MEAN
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
diff --git a/src/tools/ConsoleBench/utils.cpp b/src/tools/ConsoleBench/utils.cpp
new file mode 100644
index 0000000000..03308addeb
--- /dev/null
+++ b/src/tools/ConsoleBench/utils.cpp
@@ -0,0 +1,108 @@
+#include "pch.h"
+#include "utils.h"
+
+#include "arena.h"
+
+BufferedWriter::BufferedWriter(HANDLE out, std::span buffer) :
+ m_out{ out },
+ m_buffer{ buffer }
+{
+}
+
+BufferedWriter::~BufferedWriter()
+{
+ flush();
+}
+
+void BufferedWriter::flush()
+{
+ _write(m_buffer.data(), m_buffer_usage);
+ m_buffer_usage = 0;
+}
+
+void BufferedWriter::write(std::string_view str)
+{
+ if (m_buffer_usage + str.size() > m_buffer.size())
+ {
+ flush();
+ }
+
+ if (str.size() >= m_buffer.size())
+ {
+ _write(str.data(), str.size());
+ }
+ else
+ {
+ mem::copy(m_buffer.data() + m_buffer_usage, str.data(), str.size());
+ m_buffer_usage += str.size();
+ }
+}
+
+void BufferedWriter::_write(const void* data, size_t bytes) const
+{
+ DWORD written;
+ THROW_IF_WIN32_BOOL_FALSE(WriteFile(m_out, data, static_cast(bytes), &written, nullptr));
+ THROW_HR_IF(E_FAIL, written != bytes);
+}
+
+std::string_view get_file_version(mem::Arena& arena, const wchar_t* path)
+{
+ const auto bytes = GetFileVersionInfoSizeExW(0, path, nullptr);
+ if (!bytes)
+ {
+ return {};
+ }
+
+ const auto scratch = mem::get_scratch_arena(arena);
+ const auto buffer = scratch.arena.push_uninitialized(bytes);
+ if (!GetFileVersionInfoExW(0, path, 0, bytes, buffer))
+ {
+ return {};
+ }
+
+ VS_FIXEDFILEINFO* info;
+ UINT varLen = 0;
+ if (!VerQueryValueW(buffer, L"\\", reinterpret_cast(&info), &varLen))
+ {
+ return {};
+ }
+
+ return mem::format(
+ arena,
+ "%d.%d.%d.%d",
+ HIWORD(info->dwFileVersionMS),
+ LOWORD(info->dwFileVersionMS),
+ HIWORD(info->dwFileVersionLS),
+ LOWORD(info->dwFileVersionLS));
+}
+
+void set_clipboard(HWND hwnd, std::wstring_view contents)
+{
+ const auto global = GlobalAlloc(GMEM_MOVEABLE, (contents.size() + 1) * sizeof(wchar_t));
+ {
+ const auto ptr = static_cast(GlobalLock(global));
+
+ mem::copy(ptr, contents.data(), contents.size());
+ ptr[contents.size()] = 0;
+
+ GlobalUnlock(global);
+ }
+
+ for (DWORD sleep = 10;; sleep *= 2)
+ {
+ if (OpenClipboard(hwnd))
+ {
+ break;
+ }
+ // 10 iterations
+ if (sleep > 10000)
+ {
+ THROW_LAST_ERROR();
+ }
+ Sleep(sleep);
+ }
+
+ EmptyClipboard();
+ SetClipboardData(CF_UNICODETEXT, global);
+ CloseClipboard();
+}
diff --git a/src/tools/ConsoleBench/utils.h b/src/tools/ConsoleBench/utils.h
new file mode 100644
index 0000000000..2c30d77f24
--- /dev/null
+++ b/src/tools/ConsoleBench/utils.h
@@ -0,0 +1,47 @@
+#pragma once
+
+// clang-format off
+#ifdef NDEBUG
+#define debugAssert(cond) ((void)0)
+#else
+#define debugAssert(cond) if (!(cond)) __debugbreak()
+#endif
+// clang-format on
+
+namespace mem
+{
+ struct Arena;
+}
+
+struct BufferedWriter
+{
+ BufferedWriter(HANDLE out, std::span buffer);
+ ~BufferedWriter();
+
+ void flush();
+ void write(std::string_view str);
+
+private:
+ void _write(const void* data, size_t bytes) const;
+
+ HANDLE m_out;
+ std::span m_buffer;
+ size_t m_buffer_usage = 0;
+};
+
+inline int64_t query_perf_counter()
+{
+ LARGE_INTEGER i;
+ QueryPerformanceCounter(&i);
+ return i.QuadPart;
+}
+
+inline int64_t query_perf_freq()
+{
+ LARGE_INTEGER i;
+ QueryPerformanceFrequency(&i);
+ return i.QuadPart;
+}
+
+std::string_view get_file_version(mem::Arena& arena, const wchar_t* path);
+void set_clipboard(HWND hwnd, std::wstring_view contents);