Add an automated conhost benchmark tool (#16453)

`ConsoleBench` is capable of launching conhost instances and measuring
their performance characteristics. It writes these out as an HTML file
with violin graphs (using plotly.js) for easy comparison.
Currently, only a small number of tests have been added, but the code
is structured in such a way that more tests can be added easily.
This commit is contained in:
Leonard Hecker 2024-02-22 13:31:31 +01:00 committed by GitHub
parent 9c8058c326
commit ec434e3fba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1415 additions and 0 deletions

View File

@ -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$

View File

@ -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}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>{be92101c-04f8-48da-99f0-e1f4f1d2dc48}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>ConsoleBench</RootNamespace>
<ProjectName>ConsoleBench</ProjectName>
<TargetName>ConsoleBench</TargetName>
<ConfigurationType>Application</ConfigurationType>
</PropertyGroup>
<Import Project="$(SolutionDir)src\common.build.pre.props" />
<Import Project="$(SolutionDir)src\common.nugetversions.props" />
<ItemGroup>
<ClCompile Include="arena.cpp" />
<ClCompile Include="conhost.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="utils.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="arena.h" />
<ClInclude Include="conhost.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="utils.h" />
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>
<ControlFlowGuard>false</ControlFlowGuard>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<Import Project="$(SolutionDir)src\common.build.post.props" />
<Import Project="$(SolutionDir)src\common.nugetversions.targets" />
</Project>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="arena.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="conhost.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="utils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="arena.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="conhost.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="utils.h">
<Filter>Source Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

View File

@ -0,0 +1,268 @@
#include "pch.h"
#include "arena.h"
using namespace mem;
Arena::Arena(size_t bytes)
{
m_alloc = static_cast<uint8_t*>(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<Arena, 2> 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<char>(len);
len = _vsnprintf(buffer, len, fmt, args);
if (len < 0)
{
return {};
}
return { buffer, static_cast<size_t>(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<wchar_t>(len);
len = _vsnwprintf(buffer, len, fmt, args);
if (len < 0)
{
return {};
}
return { buffer, static_cast<size_t>(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<DWORD>(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<DWORD>(str.size()), nullptr, nullptr);
}
std::string_view mem::read_line(Arena& arena, size_t max_bytes)
{
auto read = static_cast<DWORD>(max_bytes);
const auto buffer = arena.push_uninitialized<char>(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<int>(count);
auto length = MultiByteToWideChar(CP_UTF8, 0, ptr, int_count, nullptr, 0);
if (length <= 0)
{
return {};
}
const auto buffer = arena.push_uninitialized<wchar_t>(length);
length = MultiByteToWideChar(CP_UTF8, 0, ptr, int_count, buffer, length);
if (length <= 0)
{
return {};
}
return { buffer, static_cast<size_t>(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<int>(count);
auto length = WideCharToMultiByte(CP_UTF8, 0, ptr, int_count, nullptr, 0, nullptr, nullptr);
if (length <= 0)
{
return {};
}
const auto buffer = arena.push_uninitialized<char>(length);
length = WideCharToMultiByte(CP_UTF8, 0, ptr, int_count, buffer, length, nullptr, nullptr);
if (length <= 0)
{
return {};
}
return { buffer, static_cast<size_t>(length) };
}

View File

@ -0,0 +1,111 @@
#pragma once
namespace mem
{
template<typename T>
struct is_std_view : std::false_type
{
};
template<typename U, std::size_t E>
struct is_std_view<std::span<U, E>> : std::true_type
{
};
template<typename U, typename V>
struct is_std_view<std::basic_string_view<U, V>> : std::true_type
{
};
template<typename T>
concept is_trivially_constructible = std::is_trivially_constructible_v<T> || is_std_view<std::remove_cv_t<T>>::value;
template<typename T>
concept is_trivially_copyable = std::is_trivially_copyable_v<T>;
struct Arena
{
explicit Arena(size_t bytes);
~Arena();
void clear();
size_t get_pos() const;
void pop_to(size_t pos);
template<is_trivially_copyable T>
T* push_zeroed(size_t count = 1)
{
return static_cast<T*>(_push_zeroed(sizeof(T) * count, alignof(T)));
}
template<is_trivially_copyable T>
std::span<T> push_zeroed_span(size_t count)
{
return { static_cast<T*>(_push_zeroed(sizeof(T) * count, alignof(T))), count };
}
template<is_trivially_copyable T>
T* push_uninitialized(size_t count = 1)
{
return static_cast<T*>(_push_uninitialized(sizeof(T) * count, alignof(T)));
}
template<is_trivially_copyable T>
std::span<T> push_uninitialized_span(size_t count)
{
return { static_cast<T*>(_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<is_trivially_copyable T>
void copy(T* dst, const T* src, size_t count)
{
memcpy(dst, src, count * sizeof(T));
}
template<typename T>
std::basic_string_view<T> repeat_string(Arena& arena, std::basic_string_view<T> in, size_t count)
{
const auto len = count * in.size();
const auto buf = arena.push_uninitialized<T>(len);
for (size_t i = 0; i < count; ++i)
{
mem::copy(buf + i * in.size(), in.data(), in.size());
}
return { buf, len };
}
}

View File

@ -0,0 +1,170 @@
#include "pch.h"
#include "conhost.h"
#include <conmsgl1.h>
#include <winternl.h>
#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<USHORT>(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<PPROC_THREAD_ATTRIBUTE_LIST>(&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<wchar_t*>(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<uint8_t*>(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<ULONG>(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;
}

View File

@ -0,0 +1,20 @@
#pragma once
#include <winternl.h>
namespace mem
{
struct Arena;
}
using unique_nthandle = wil::unique_any_handle_null<decltype(&::NtClose), ::NtClose>;
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);

View File

@ -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<int32_t>;
using MeasurementsPerBenchmark = std::span<Measurements>;
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<int32_t>(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<DWORD>(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<DWORD>(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<DWORD>(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<DWORD>(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<DWORD>(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<INPUT_RECORD>(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<const wchar_t*> paths);
static std::span<Measurements> 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<size_t>(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<const wchar_t*> 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<AccumulatedResults>();
results->trace_count = trace_count;
results->trace_names = arena.push_zeroed<std::string_view>(trace_count);
results->measurments = arena.push_zeroed<MeasurementsPerBenchmark>(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<char>(9001);
memset(buf, '\n', 9001);
WriteFile(ctx.output, buf, 9001, nullptr, nullptr);
}
static std::span<Measurements> 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<Measurements>(s_benchmarks_count);
for (auto& measurements : results)
{
measurements = arena.push_zeroed_span<int32_t>(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<char>(1024 * 1024) };
writer.write(R"(<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
html {
overflow-x: hidden;
}
html, body {
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.view {
width: 1024px;
height: 600px;
}
</style>
</head>
<body>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js" charset="utf-8"></script>
<script>
)");
{
writer.write(" const results = [");
for (size_t bench_idx = 0; bench_idx < s_benchmarks_count; ++bench_idx)
{
const auto& bench = s_benchmarks[bench_idx];
writer.write("{title:'");
writer.write(bench.title);
writer.write("',results:[");
for (size_t trace_idx = 0; trace_idx < results->trace_count; ++trace_idx)
{
writer.write("{basename:'");
writer.write(results->trace_names[trace_idx]);
writer.write("',measurements:[");
const auto measurements = results->measurments[trace_idx][bench_idx];
if (!measurements.empty())
{
std::sort(measurements.begin(), measurements.end());
// Console calls have a high tail latency. Whatever the reason is (it's probably scheduling latency)
// it's not particularly interesting at the moment when the median latency is intolerable high anyway.
const auto p25 = measurements[(measurements.size() * 25 + 50) / 100];
const auto p75 = measurements[(measurements.size() * 75 + 50) / 100];
const auto iqr3 = (p75 - p25) * 3;
const auto outlier_max = p75 + iqr3;
const auto beg = measurements.data();
auto end = beg + measurements.size();
for (; end[-1] > outlier_max; --end)
{
}
for (auto it = beg; it < end; ++it)
{
char buffer[32];
const auto res = std::to_chars(&buffer[0], &buffer[64], *it * sec_per_tick, std::chars_format::scientific, 3);
writer.write({ &buffer[0], res.ptr });
writer.write(",");
}
}
writer.write("]},");
}
writer.write("]},");
}
writer.write("];\n");
}
writer.write(R"(
for (const r of results) {
const div = document.createElement('div');
div.className = 'view';
document.body.insertAdjacentElement('beforeend', div)
Plotly.newPlot(div, r.results.map(tcr => ({
type: 'violin',
name: tcr.basename,
y: tcr.measurements,
meanline: { visible: true },
points: false,
spanmode : 'hard',
})), {
showlegend: false,
title: r.title,
yaxis: {
minexponent: 0,
showgrid: true,
showline: true,
ticksuffix: 's',
},
}, {
responsive: true,
});
}
</script>
</body>
</html>
)");
}

View File

@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"

View File

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <wil/result.h>
#include <wil/resource.h>
#include <array>
#include <algorithm>
#include <charconv>
#include <span>
#include <string_view>

View File

@ -0,0 +1,108 @@
#include "pch.h"
#include "utils.h"
#include "arena.h"
BufferedWriter::BufferedWriter(HANDLE out, std::span<char> 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<DWORD>(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<char>(bytes);
if (!GetFileVersionInfoExW(0, path, 0, bytes, buffer))
{
return {};
}
VS_FIXEDFILEINFO* info;
UINT varLen = 0;
if (!VerQueryValueW(buffer, L"\\", reinterpret_cast<void**>(&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<wchar_t*>(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();
}

View File

@ -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<char> 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<char> 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);