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:
parent
9c8058c326
commit
ec434e3fba
|
@ -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$
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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) };
|
||||
}
|
|
@ -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 };
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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>
|
||||
)");
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "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 <Windows.h>
|
||||
|
||||
#include <wil/result.h>
|
||||
#include <wil/resource.h>
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <span>
|
||||
#include <string_view>
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
Loading…
Reference in New Issue