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