From bb4981caae9bca01e0c42f8ed4cfc955925cea32 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Sat, 22 Jun 2024 13:30:18 +0200 Subject: [PATCH] Add more ConsoleBench tests (#17441) This now covers all major Console APIs. In the future we could add tests that cover VT sequences as well. --- .../ConsoleBench/ConsoleBench.exe.manifest | 26 + src/tools/ConsoleBench/ConsoleBench.vcxproj | 3 + .../ConsoleBench/ConsoleBench.vcxproj.filters | 5 + src/tools/ConsoleBench/arena.cpp | 20 +- src/tools/ConsoleBench/arena.h | 31 +- src/tools/ConsoleBench/conhost.cpp | 36 +- src/tools/ConsoleBench/conhost.h | 1 + src/tools/ConsoleBench/main.cpp | 616 ++++++++++++++---- src/tools/ConsoleBench/utils.h | 2 +- 9 files changed, 588 insertions(+), 152 deletions(-) create mode 100644 src/tools/ConsoleBench/ConsoleBench.exe.manifest diff --git a/src/tools/ConsoleBench/ConsoleBench.exe.manifest b/src/tools/ConsoleBench/ConsoleBench.exe.manifest new file mode 100644 index 00000000000..380e2e03a37 --- /dev/null +++ b/src/tools/ConsoleBench/ConsoleBench.exe.manifest @@ -0,0 +1,26 @@ + + + + + true + UTF-8 + + + + + + + + + + + + + diff --git a/src/tools/ConsoleBench/ConsoleBench.vcxproj b/src/tools/ConsoleBench/ConsoleBench.vcxproj index 1e66412cc6b..5ea84a7c56f 100644 --- a/src/tools/ConsoleBench/ConsoleBench.vcxproj +++ b/src/tools/ConsoleBench/ConsoleBench.vcxproj @@ -25,6 +25,9 @@ + + + false diff --git a/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters b/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters index 25a667f9907..ca5d01a8e55 100644 --- a/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters +++ b/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters @@ -49,4 +49,9 @@ Source Files + + + Source Files + + \ No newline at end of file diff --git a/src/tools/ConsoleBench/arena.cpp b/src/tools/ConsoleBench/arena.cpp index d91986c7276..8c96c341b4c 100644 --- a/src/tools/ConsoleBench/arena.cpp +++ b/src/tools/ConsoleBench/arena.cpp @@ -5,7 +5,7 @@ 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))); + m_alloc = static_cast(THROW_IF_NULL_ALLOC(VirtualAlloc(nullptr, bytes, MEM_RESERVE, PAGE_READWRITE))); } Arena::~Arena() @@ -41,8 +41,18 @@ void* Arena::_push_raw(size_t bytes, size_t alignment) { const auto mask = alignment - 1; const auto pos = (m_pos + mask) & ~mask; + const auto pos_new = pos + bytes; const auto ptr = m_alloc + pos; - m_pos = pos + bytes; + + if (pos_new > m_commit) + { + // Commit in 1MB chunks and pre-commit 1MiB in advance. + const auto commit_new = (pos_new + 0x1FFFFF) & ~0xFFFFF; + THROW_IF_NULL_ALLOC(VirtualAlloc(m_alloc + m_commit, commit_new - m_commit, MEM_COMMIT, PAGE_READWRITE)); + m_commit = commit_new; + } + + m_pos = pos_new; return ptr; } @@ -76,8 +86,8 @@ ScopedArena::~ScopedArena() static [[msvc::noinline]] std::array thread_arenas_init() { return { - Arena{ 64 * 1024 * 1024 }, - Arena{ 64 * 1024 * 1024 }, + Arena{ 1024 * 1024 * 1024 }, + Arena{ 1024 * 1024 * 1024 }, }; } @@ -166,7 +176,9 @@ std::wstring_view mem::format(Arena& arena, const wchar_t* fmt, va_list args) return {}; } + // Make space for a terminating \0 character. len++; + const auto buffer = arena.push_uninitialized(len); len = _vsnwprintf(buffer, len, fmt, args); diff --git a/src/tools/ConsoleBench/arena.h b/src/tools/ConsoleBench/arena.h index 1519f03377b..92409c32921 100644 --- a/src/tools/ConsoleBench/arena.h +++ b/src/tools/ConsoleBench/arena.h @@ -60,6 +60,7 @@ namespace mem void* _push_uninitialized(size_t bytes, size_t alignment = __STDCPP_DEFAULT_NEW_ALIGNMENT__); uint8_t* m_alloc = nullptr; + size_t m_commit = 0; size_t m_pos = 0; }; @@ -96,16 +97,32 @@ namespace mem } template - std::basic_string_view repeat_string(Arena& arena, std::basic_string_view in, size_t count) + auto repeat(Arena& arena, const T& in, size_t count) -> decltype(auto) { - const auto len = count * in.size(); - const auto buf = arena.push_uninitialized(len); - - for (size_t i = 0; i < count; ++i) + if constexpr (is_std_view::value) { - mem::copy(buf + i * in.size(), in.data(), in.size()); + const auto data = in.data(); + const auto size = in.size(); + const auto len = count * size; + const auto buf = arena.push_uninitialized(len); + + for (size_t i = 0; i < count; ++i) + { + mem::copy(buf + i * size, data, size); + } + + return T{ buf, len }; } + else + { + const auto buf = arena.push_uninitialized(count); + + for (size_t i = 0; i < count; ++i) + { + memcpy(buf + i, &in, sizeof(T)); + } - return { buf, len }; + return std::span{ buf, count }; + } } } diff --git a/src/tools/ConsoleBench/conhost.cpp b/src/tools/ConsoleBench/conhost.cpp index 95a0a3ea8a9..4db5352f99b 100644 --- a/src/tools/ConsoleBench/conhost.cpp +++ b/src/tools/ConsoleBench/conhost.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "arena.h" @@ -46,12 +47,27 @@ static void conhostCopyToStringBuffer(USHORT& length, auto& buffer, const wchar_ ConhostHandle spawn_conhost(mem::Arena& arena, const wchar_t* path) { + const auto pathLen = wcslen(path); + const auto isDLL = pathLen > 4 && wcscmp(&path[pathLen - 4], L".dll") == 0; + const auto scratch = mem::get_scratch_arena(arena); - const auto server = conhostCreateHandle(nullptr, L"\\Device\\ConDrv\\Server", true, false); + 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()); + const auto selfPath = scratch.arena.push_uninitialized(64 * 1024); + GetModuleFileNameW(nullptr, selfPath, 64 * 1024); + + std::wstring_view cmd; + + if (isDLL) + { + cmd = format(scratch.arena, LR"("%s" host %zx "%s")", selfPath, server.get(), path); + } + else + { + cmd = format(scratch.arena, LR"("%s" --server 0x%zx)", path, server.get()); + } uint8_t attrListBuffer[64]; @@ -154,6 +170,22 @@ ConhostHandle spawn_conhost(mem::Arena& arena, const wchar_t* path) }; } +// A continuation of spawn_conhost(). +void check_spawn_conhost_dll(int argc, const wchar_t* argv[]) +{ + if (argc == 4 && wcscmp(argv[1], L"host") == 0) + { + const auto serverHandle = reinterpret_cast(wcstoull(argv[2], nullptr, 16)); + const auto path = argv[3]; + + using Entrypoint = NTSTATUS(NTAPI*)(HANDLE); + const auto h = THROW_LAST_ERROR_IF_NULL(LoadLibraryExW(path, nullptr, 0)); + const auto f = THROW_LAST_ERROR_IF_NULL(reinterpret_cast(GetProcAddress(h, "ConsoleCreateIoThread"))); + THROW_IF_NTSTATUS_FAILED(f(serverHandle)); + ExitThread(S_OK); + } +} + HANDLE get_active_connection() { // (Not actually) FUN FACT! The handles don't mean anything and the cake is a lie! diff --git a/src/tools/ConsoleBench/conhost.h b/src/tools/ConsoleBench/conhost.h index e794eca555e..719a5bb87ad 100644 --- a/src/tools/ConsoleBench/conhost.h +++ b/src/tools/ConsoleBench/conhost.h @@ -16,5 +16,6 @@ struct ConhostHandle }; ConhostHandle spawn_conhost(mem::Arena& arena, const wchar_t* path); +void check_spawn_conhost_dll(int argc, const wchar_t* argv[]); HANDLE get_active_connection(); void set_active_connection(HANDLE connection); diff --git a/src/tools/ConsoleBench/main.cpp b/src/tools/ConsoleBench/main.cpp index 4e380474ebc..31b43e6b93d 100644 --- a/src/tools/ConsoleBench/main.cpp +++ b/src/tools/ConsoleBench/main.cpp @@ -7,26 +7,47 @@ #include "conhost.h" #include "utils.h" +#define ENABLE_TEST_OUTPUT_WRITE 1 +#define ENABLE_TEST_OUTPUT_SCROLL 1 +#define ENABLE_TEST_OUTPUT_FILL 1 +#define ENABLE_TEST_OUTPUT_READ 1 +#define ENABLE_TEST_INPUT 1 +#define ENABLE_TEST_CLIPBOARD 1 + using Measurements = std::span; using MeasurementsPerBenchmark = std::span; struct BenchmarkContext { - HWND hwnd; - HANDLE input; - HANDLE output; - int64_t time_limit; + bool wants_more() const; + void mark_beg(); + void mark_end(); + size_t rand(); + + HWND hwnd = nullptr; + HANDLE input = nullptr; + HANDLE output = nullptr; + mem::Arena& arena; std::string_view utf8_4Ki; std::string_view utf8_128Ki; std::wstring_view utf16_4Ki; std::wstring_view utf16_128Ki; + std::span attr_4Ki; + std::span char_4Ki; + std::span input_4Ki; + + Measurements m_measurements; + size_t m_measurements_off = 0; + int64_t m_time = 0; + int64_t m_time_limit = 0; + size_t m_rng_state = 0; }; struct Benchmark { const char* title; - void (*exec)(const BenchmarkContext& ctx, Measurements measurements); + void (*exec)(BenchmarkContext& ctx); }; struct AccumulatedResults @@ -37,158 +58,415 @@ struct AccumulatedResults MeasurementsPerBenchmark* measurments; }; -constexpr int32_t perf_delta(int64_t beg, int64_t end) -{ - return static_cast(end - beg); -} +static constexpr COORD s_buffer_size{ 120, 9001 }; +static constexpr COORD s_viewport_size{ 120, 30 }; -static constexpr Benchmark s_benchmarks[]{ +static constexpr Benchmark s_benchmarks[] = { +#if ENABLE_TEST_OUTPUT_WRITE Benchmark{ .title = "WriteConsoleA 4Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - for (auto& d : measurements) + .exec = [](BenchmarkContext& ctx) { + while (ctx.wants_more()) { - 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; - } + ctx.mark_beg(); + const auto res = WriteConsoleA(ctx.output, ctx.utf8_4Ki.data(), static_cast(ctx.utf8_4Ki.size()), nullptr, nullptr); + ctx.mark_end(); + debugAssert(res == TRUE); } }, }, Benchmark{ .title = "WriteConsoleW 4Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - for (auto& d : measurements) + .exec = [](BenchmarkContext& ctx) { + while (ctx.wants_more()) { - 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; - } + ctx.mark_beg(); + const auto res = WriteConsoleW(ctx.output, ctx.utf16_4Ki.data(), static_cast(ctx.utf16_4Ki.size()), nullptr, nullptr); + ctx.mark_end(); + debugAssert(res == TRUE); } }, }, Benchmark{ .title = "WriteConsoleA 128Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - for (auto& d : measurements) + .exec = [](BenchmarkContext& ctx) { + while (ctx.wants_more()) { - 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); + ctx.mark_beg(); + const auto res = WriteConsoleA(ctx.output, ctx.utf8_128Ki.data(), static_cast(ctx.utf8_128Ki.size()), nullptr, nullptr); + ctx.mark_end(); + debugAssert(res == TRUE); + } + }, + }, + Benchmark{ + .title = "WriteConsoleW 128Ki", + .exec = [](BenchmarkContext& ctx) { + while (ctx.wants_more()) + { + ctx.mark_beg(); + const auto res = WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); + ctx.mark_end(); + debugAssert(res == TRUE); + } + }, + }, + Benchmark{ + .title = "WriteConsoleOutputAttribute 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + DWORD written; + + while (ctx.wants_more()) + { + ctx.mark_beg(); + const auto res = WriteConsoleOutputAttribute(ctx.output, ctx.attr_4Ki.data(), static_cast(ctx.attr_4Ki.size()), pos, &written); + ctx.mark_end(); + debugAssert(res == TRUE); + } + }, + }, + Benchmark{ + .title = "WriteConsoleOutputCharacterW 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + DWORD written; + + while (ctx.wants_more()) + { + ctx.mark_beg(); + const auto res = WriteConsoleOutputCharacterW(ctx.output, ctx.utf16_4Ki.data(), static_cast(ctx.utf16_4Ki.size()), pos, &written); + ctx.mark_end(); + debugAssert(res == TRUE); + } + }, + }, + Benchmark{ + .title = "WriteConsoleOutputW 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + static constexpr COORD size{ 64, 64 }; + static constexpr SMALL_RECT rect{ 0, 0, 63, 63 }; + + while (ctx.wants_more()) + { + auto written = rect; + + ctx.mark_beg(); + const auto res = WriteConsoleOutputW(ctx.output, ctx.char_4Ki.data(), size, pos, &written); + ctx.mark_end(); + debugAssert(res == TRUE); + } + }, + }, +#endif +#if ENABLE_TEST_OUTPUT_SCROLL + Benchmark{ + .title = "ScrollConsoleScreenBufferW 4Ki", + .exec = [](BenchmarkContext& ctx) { + for (int i = 0; i < 10; i++) + { + WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); + } - if (end >= ctx.time_limit) + static constexpr CHAR_INFO fill{ L' ', FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED }; + static constexpr size_t w = 64; + static constexpr size_t h = 64; + + while (ctx.wants_more()) + { + auto r = ctx.rand(); + const auto srcLeft = (r >> 0) % (s_buffer_size.X - w); + const auto srcTop = (r >> 16) % (s_buffer_size.Y - h); + + size_t dstLeft; + size_t dstTop; + do { - break; - } + r = ctx.rand(); + dstLeft = (r >> 0) % (s_buffer_size.X - w); + dstTop = (r >> 16) % (s_buffer_size.Y - h); + } while (srcLeft == dstLeft && srcTop == dstTop); + + const SMALL_RECT scrollRect{ + .Left = static_cast(srcLeft), + .Top = static_cast(srcTop), + .Right = static_cast(srcLeft + w - 1), + .Bottom = static_cast(srcTop + h - 1), + }; + const COORD destOrigin{ + .X = static_cast(dstLeft), + .Y = static_cast(dstTop), + }; + + ctx.mark_beg(); + const auto res = ScrollConsoleScreenBufferW(ctx.output, &scrollRect, nullptr, destOrigin, &fill); + ctx.mark_end(); + debugAssert(res == TRUE); } }, }, Benchmark{ - .title = "WriteConsoleW 128Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - for (auto& d : measurements) + .title = "ScrollConsoleScreenBufferW vertical", + .exec = [](BenchmarkContext& ctx) { + for (int i = 0; i < 10; i++) { - 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) + static constexpr CHAR_INFO fill{ L' ', FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED }; + static constexpr size_t h = (4096 + s_buffer_size.X / 2) / s_buffer_size.X; + + while (ctx.wants_more()) + { + auto r = ctx.rand(); + const auto srcTop = r % (s_buffer_size.Y - h); + + size_t dstTop; + do { - break; - } + r = ctx.rand(); + dstTop = r % (s_buffer_size.Y - h); + } while (srcTop == dstTop); + + const SMALL_RECT scrollRect{ + .Left = 0, + .Top = static_cast(srcTop), + .Right = s_buffer_size.X - 1, + .Bottom = static_cast(srcTop + h - 1), + }; + const COORD destOrigin{ + .X = 0, + .Y = static_cast(dstTop), + }; + + ctx.mark_beg(); + const auto res = ScrollConsoleScreenBufferW(ctx.output, &scrollRect, nullptr, destOrigin, &fill); + ctx.mark_end(); + debugAssert(res == TRUE); } }, }, +#endif +#if ENABLE_TEST_OUTPUT_FILL 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); + .title = "FillConsoleOutputAttribute 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + DWORD written; - for (auto& d : measurements) + while (ctx.wants_more()) { - SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF5 /* ID_CONSOLE_SELECTALL */, 0); + ctx.mark_beg(); + FillConsoleOutputAttribute(ctx.output, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED, 4096, pos, &written); + ctx.mark_end(); + debugAssert(written == 4096); + } + }, + }, + Benchmark{ + .title = "FillConsoleOutputCharacterW 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + DWORD written; - 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); + while (ctx.wants_more()) + { + ctx.mark_beg(); + FillConsoleOutputCharacterW(ctx.output, L'A', 4096, pos, &written); + ctx.mark_end(); + debugAssert(written == 4096); + } + }, + }, +#endif +#if ENABLE_TEST_OUTPUT_READ + Benchmark{ + .title = "ReadConsoleOutputAttribute 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + const auto scratch = mem::get_scratch_arena(ctx.arena); + const auto buf = scratch.arena.push_uninitialized(4096); + DWORD read; - if (end >= ctx.time_limit) - { - break; - } + WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); + + while (ctx.wants_more()) + { + ctx.mark_beg(); + ReadConsoleOutputAttribute(ctx.output, buf, 4096, pos, &read); + ctx.mark_end(); + debugAssert(read == 4096); } }, }, Benchmark{ - .title = "Paste from clipboard 4Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - set_clipboard(ctx.hwnd, ctx.utf16_4Ki); + .title = "ReadConsoleOutputCharacterW 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + const auto scratch = mem::get_scratch_arena(ctx.arena); + const auto buf = scratch.arena.push_uninitialized(4096); + DWORD read; + + WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); + + while (ctx.wants_more()) + { + ctx.mark_beg(); + ReadConsoleOutputCharacterW(ctx.output, buf, 4096, pos, &read); + ctx.mark_end(); + debugAssert(read == 4096); + } + }, + }, + Benchmark{ + .title = "ReadConsoleOutputW 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + static constexpr COORD size{ 64, 64 }; + static constexpr SMALL_RECT rect{ 0, 0, 63, 63 }; + const auto scratch = mem::get_scratch_arena(ctx.arena); + const auto buf = scratch.arena.push_uninitialized(size.X * size.Y); + + WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); + + while (ctx.wants_more()) + { + auto read = rect; + + ctx.mark_beg(); + ReadConsoleOutputW(ctx.output, buf, size, pos, &read); + ctx.mark_end(); + debugAssert(read.Right == 63 && read.Bottom == 63); + } + }, + }, +#endif +#if ENABLE_TEST_INPUT + Benchmark{ + .title = "WriteConsoleInputW 4Ki", + .exec = [](BenchmarkContext& ctx) { + DWORD written; + FlushConsoleInputBuffer(ctx.input); - for (auto& d : measurements) + while (ctx.wants_more()) { - 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); + ctx.mark_beg(); + WriteConsoleInputW(ctx.input, ctx.input_4Ki.data(), static_cast(ctx.input_4Ki.size()), &written); + ctx.mark_end(); + debugAssert(written == ctx.input_4Ki.size()); 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; + .title = "ReadConsoleInputW 4Ki", + .exec = [](BenchmarkContext& ctx) { + const auto scratch = mem::get_scratch_arena(ctx.arena); + const auto buf = scratch.arena.push_uninitialized(ctx.input_4Ki.size()); + DWORD written, read; + FlushConsoleInputBuffer(ctx.input); + + while (ctx.wants_more()) + { + WriteConsoleInputW(ctx.input, ctx.input_4Ki.data(), static_cast(ctx.input_4Ki.size()), &written); + debugAssert(written == ctx.input_4Ki.size()); + + ctx.mark_beg(); + ReadConsoleInputW(ctx.input, buf, static_cast(ctx.input_4Ki.size()), &read); + ctx.mark_end(); + debugAssert(read == ctx.input_4Ki.size()); + } + }, + }, + Benchmark{ + .title = "ReadConsoleW 4Ki", + .exec = [](BenchmarkContext& ctx) { const auto scratch = mem::get_scratch_arena(ctx.arena); - const auto buf = scratch.arena.push_uninitialized(cap); - DWORD read; + const auto cap = static_cast(ctx.input_4Ki.size()) * 4; + const auto buf = scratch.arena.push_uninitialized(cap); + DWORD written, read; + + FlushConsoleInputBuffer(ctx.input); + + while (ctx.wants_more()) + { + WriteConsoleInputW(ctx.input, ctx.input_4Ki.data(), static_cast(ctx.input_4Ki.size()), &written); + debugAssert(written == ctx.input_4Ki.size()); + ctx.mark_beg(); + ReadConsoleW(ctx.input, buf, cap, &read, nullptr); + debugAssert(read == ctx.input_4Ki.size()); + ctx.mark_end(); + } + }, + }, +#endif +#if ENABLE_TEST_CLIPBOARD + Benchmark{ + .title = "Clipboard copy 4Ki", + .exec = [](BenchmarkContext& ctx) { + WriteConsoleW(ctx.output, ctx.utf16_4Ki.data(), static_cast(ctx.utf8_4Ki.size()), nullptr, nullptr); + + while (ctx.wants_more()) + { + SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF5 /* ID_CONSOLE_SELECTALL */, 0); + + ctx.mark_beg(); + SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF0 /* ID_CONSOLE_COPY */, 0); + ctx.mark_end(); + } + }, + }, + Benchmark{ + .title = "Clipboard paste 4Ki", + .exec = [](BenchmarkContext& ctx) { set_clipboard(ctx.hwnd, ctx.utf16_4Ki); FlushConsoleInputBuffer(ctx.input); - for (auto& d : measurements) + while (ctx.wants_more()) { + ctx.mark_beg(); SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF1 /* ID_CONSOLE_PASTE */, 0); + ctx.mark_end(); - 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; - } + FlushConsoleInputBuffer(ctx.input); } }, }, +#endif }; -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 constexpr size_t s_benchmarks_count = _countof(s_benchmarks); +static constexpr size_t s_samples_min = 20; +static constexpr size_t s_samples_max = 1000; + +// 128 characters and 124 columns. +static constexpr std::string_view s_payload_utf8{ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna alΑΒΓΔΕ" }; +// 128 characters and 128 columns. +static constexpr std::wstring_view s_payload_utf16{ L"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.ΑΒΓΔΕ" }; + +static constexpr WORD s_payload_attr = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; +static constexpr CHAR_INFO s_payload_char{ + .Char = { .UnicodeChar = L'A' }, + .Attributes = s_payload_attr, +}; +static constexpr INPUT_RECORD s_payload_record{ + .EventType = KEY_EVENT, + .Event = { + .KeyEvent = { + .bKeyDown = TRUE, + .wRepeatCount = 1, + .wVirtualKeyCode = 'A', + .wVirtualScanCode = 0, + .uChar = 'A', + .dwControlKeyState = 0, + }, + }, +}; static bool print_warning(); static AccumulatedResults* prepare_results(mem::Arena& arena, std::span paths); @@ -196,6 +474,7 @@ static std::span run_benchmarks_for_path(mem::Arena& arena, const static void generate_html(mem::Arena& arena, const AccumulatedResults* results); int wmain(int argc, const wchar_t* argv[]) +try { if (argc < 2) { @@ -203,6 +482,8 @@ int wmain(int argc, const wchar_t* argv[]) return 1; } + check_spawn_conhost_dll(argc, argv); + const auto cp = GetConsoleCP(); const auto output_cp = GetConsoleOutputCP(); const auto restore_cp = wil::scope_exit([&]() { @@ -230,12 +511,29 @@ int wmain(int argc, const wchar_t* argv[]) { const auto title = results->trace_names[trace_idx]; print_format(scratch.arena, "\r\n# %.*s\r\n", title.size(), title.data()); + + // I found that waiting between tests fixes weird bugs when launching very old conhost versions. + if (trace_idx != 0) + { + Sleep(5000); + } + results->measurments[trace_idx] = run_benchmarks_for_path(scratch.arena, paths[trace_idx]); } generate_html(scratch.arena, results); return 0; } +catch (const wil::ResultException& e) +{ + printf("Exception: %08x\n", e.GetErrorCode()); + return 1; +} +catch (...) +{ + printf("Unknown exception\n"); + return 1; +} static bool print_warning() { @@ -284,7 +582,7 @@ static AccumulatedResults* prepare_results(mem::Arena& arena, std::span(9001); - memset(buf, '\n', 9001); - WriteFile(ctx.output, buf, 9001, nullptr, nullptr); + const auto buf = scratch.arena.push_uninitialized(s_buffer_size.Y); + memset(buf, '\n', s_buffer_size.Y); + WriteFile(ctx.output, buf, s_buffer_size.Y, nullptr, nullptr); } static std::span run_benchmarks_for_path(mem::Arena& arena, const wchar_t* path) @@ -364,7 +663,7 @@ static std::span run_benchmarks_for_path(mem::Arena& arena, const const auto parent_hwnd = GetConsoleWindow(); const auto freq = query_perf_freq(); - const auto handle = spawn_conhost(scratch.arena, path); + auto handle = spawn_conhost(scratch.arena, path); set_active_connection(handle.connection.get()); const auto print_with_parent_connection = [&](auto&&... args) { @@ -377,45 +676,56 @@ static std::span run_benchmarks_for_path(mem::Arena& arena, const .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), + .utf8_4Ki = mem::repeat(scratch.arena, s_payload_utf8, 4 * 1024 / s_payload_utf8.size()), + .utf8_128Ki = mem::repeat(scratch.arena, s_payload_utf8, 128 * 1024 / s_payload_utf8.size()), + .utf16_4Ki = mem::repeat(scratch.arena, s_payload_utf16, 4 * 1024 / s_payload_utf16.size()), + .utf16_128Ki = mem::repeat(scratch.arena, s_payload_utf16, 128 * 1024 / s_payload_utf16.size()), + .attr_4Ki = mem::repeat(scratch.arena, s_payload_attr, 4 * 1024), + .char_4Ki = mem::repeat(scratch.arena, s_payload_char, 4 * 1024), + .input_4Ki = mem::repeat(scratch.arena, s_payload_record, 4 * 1024), + + .m_measurements = scratch.arena.push_uninitialized_span(4 * 1024 * 1024), }; 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. + // Warmup for 0.1s max. WriteConsoleW(ctx.output, L"\033c", 2, nullptr, nullptr); - ctx.time_limit = query_perf_counter() + freq / 10; - bench.exec(ctx, measurements); + ctx.m_measurements_off = 0; + ctx.m_time_limit = query_perf_counter() + freq / 10; + bench.exec(ctx); - // Actual run for 1s. + // Actual run for 3s max. WriteConsoleW(ctx.output, L"\033c", 2, nullptr, nullptr); - ctx.time_limit = query_perf_counter() + freq; - bench.exec(ctx, measurements); + ctx.m_measurements_off = 0; + ctx.m_time_limit = query_perf_counter() + freq * 3; + bench.exec(ctx); - // Trim off trailing 0s that resulted from the time_limit. - size_t len = measurements.size(); - for (; len > 0 && measurements[len - 1] == 0; --len) + const auto measurements = arena.push_uninitialized_span(std::min(ctx.m_measurements_off, s_samples_max)); + if (ctx.m_measurements_off <= s_samples_max) { + mem::copy(measurements.data(), ctx.m_measurements.data(), ctx.m_measurements_off); } - measurements = measurements.subspan(0, len); + else + { + const auto total = ctx.m_measurements_off; + for (size_t i = 0; i < s_samples_max; ++i) + { + measurements[i] = ctx.m_measurements[i * total / s_samples_max]; + } + } + results[bench_idx] = measurements; print_with_parent_connection(", done\r\n"); } @@ -463,7 +773,7 @@ static void generate_html(mem::Arena& arena, const AccumulatedResults* results) - +