From 6cb35fe48a3a2e2b0d5eb3943526ce832f18a7bb Mon Sep 17 00:00:00 2001 From: Chris Bielow Date: Mon, 27 Mar 2023 19:45:02 +0200 Subject: [PATCH 1/8] use UTF8 encoded filenames/paths and only widen for API calls --- WinTime/Arch.cpp | 17 +++-- WinTime/Arch.h | 6 +- WinTime/Process.cpp | 81 ++++++++++++++------ WinTime/Process.h | 21 ++++-- WinTime/WinTime.cpp | 175 +++++++++++++++++++++++--------------------- 5 files changed, 172 insertions(+), 128 deletions(-) diff --git a/WinTime/Arch.cpp b/WinTime/Arch.cpp index ae516dc..0fc1705 100644 --- a/WinTime/Arch.cpp +++ b/WinTime/Arch.cpp @@ -30,15 +30,16 @@ #include "Arch.h" +#include "Process.h" using namespace std; namespace WinTime { - Arch getArch(LPCWSTR path_to_exe) + Arch getArch(const std::string& path_to_exe) { DWORD result; - if (!GetBinaryTypeW(path_to_exe, &result)) + if (!GetBinaryTypeW(&widen(path_to_exe)[0], &result)) { // not an executable return Arch::NOT_EXECUTABLE; } @@ -55,26 +56,26 @@ namespace WinTime } } - std::wstring getArchMatchedExplanation(const ArchMatched what, LPCWSTR path_to_target_exe) + std::string getArchMatchedExplanation(const ArchMatched what, LPCSTR path_to_target_exe) { switch (what) { break; case ArchMatched::SAME: - return std::wstring(L"Architectures match") + (sizeof(void*) == 8 ? L" (64bit)" : L" (32bit)"); + return std::string("Architectures match") + (sizeof(void*) == 8 ? " (64bit)" : " (32bit)"); break; case ArchMatched::MIXED: if (hostIs64Bit()) - return std::wstring(L"WinTime is 64 bit, but target (") + path_to_target_exe + L") is 32 bit. Please use 32 bit version of WinTime."; + return std::string("WinTime is 64 bit, but target (") + path_to_target_exe + ") is 32 bit. Please use 32 bit version of WinTime."; else - return std::wstring(L"WinTime is 32 bit, but target (") + path_to_target_exe + L") is 64 bit. Please use 64 bit version of WinTime."; + return std::string("WinTime is 32 bit, but target (") + path_to_target_exe + ") is 64 bit. Please use 64 bit version of WinTime."; break; case ArchMatched::TARGET_UNKNOWN: - return std::wstring(L"Target Architecture (of ") + path_to_target_exe + L") is neither 32bit nor 64bit Windows, but something else."; + return std::string("Target Architecture (of ") + path_to_target_exe + ") is neither 32bit nor 64bit Windows, but something else."; default: throw std::logic_error("Missed a case? Please report a bug!"); }; } - ArchMatched checkMatchingArch(LPCWSTR target_exe) + ArchMatched checkMatchingArch(const std::string& target_exe) { const auto target_arch = getArch(target_exe); constexpr const Arch this_arch = hostIs64Bit() ? Arch::x64 : Arch::x86; diff --git a/WinTime/Arch.h b/WinTime/Arch.h index baa2c51..2019906 100644 --- a/WinTime/Arch.h +++ b/WinTime/Arch.h @@ -38,7 +38,7 @@ namespace WinTime /// Get architecture (32 or 64 bit) of executable /// Return Arch::NOT_EXECUTABLE on error - Arch getArch(LPCWSTR path_to_exe); + Arch getArch(const std::string& path_to_exe); constexpr bool hostIs64Bit() { @@ -52,9 +52,9 @@ namespace WinTime TARGET_UNKNOWN ///< we surely know the host target (that's us; but target might be Arch::OTHER etc) }; - std::wstring getArchMatchedExplanation(const ArchMatched what, LPCWSTR path_to_target_exe);; + std::string getArchMatchedExplanation(const ArchMatched what, LPCSTR path_to_target_exe);; /// Does the architecture of @p target_exe match the architecture of the current process? - ArchMatched checkMatchingArch(LPCWSTR target_exe); + ArchMatched checkMatchingArch(const std::string& target_exe); } // namespace diff --git a/WinTime/Process.cpp b/WinTime/Process.cpp index fca238c..6f81ac0 100644 --- a/WinTime/Process.cpp +++ b/WinTime/Process.cpp @@ -41,7 +41,7 @@ using namespace std; namespace WinTime { - int countBackslashesAtEnd(const std::wstring& arg) + int countBackslashesAtEnd(const std::string& arg) { int result{ 0 }; for (auto rit = arg.rbegin(); rit != arg.rend(); ++rit) @@ -52,8 +52,8 @@ namespace WinTime return result; } - void substitute(std::wstring& str, const std::wstring& search, - const std::wstring& replace) { + void substitute(std::string& str, const std::string& search, + const std::string& replace) { size_t pos = 0; while ((pos = str.find(search, pos)) != std::string::npos) { str.replace(pos, search.length(), replace); @@ -61,15 +61,38 @@ namespace WinTime } } - std::wstring Process::getPathToCurrentProcess() + std::string narrow(const std::wstring& wide_str) + { + char buffer[1000]; + WideCharToMultiByte(CP_UTF8, + 0, + wide_str.c_str(), + -1, + buffer, 1000, + NULL, NULL); + return std::string(buffer); + } + + std::wstring widen(const std::string uft8_str) + { + wchar_t buffer[1000]; + MultiByteToWideChar(CP_UTF8, + 0, + uft8_str.c_str(), + -1, + buffer, 1000); + return std::wstring(buffer); + } + + std::string Process::getPathToCurrentProcess() { wchar_t selfdir[MAX_PATH] = { 0 }; - GetModuleFileName(NULL, selfdir, MAX_PATH); - PathRemoveFileSpec(selfdir); - return std::wstring(selfdir); + GetModuleFileNameW(NULL, selfdir, MAX_PATH); + PathRemoveFileSpecW(selfdir); + return narrow(selfdir); } - std::wstring Process::concatArguments(const std::string& exe, int more_args_argc, char** more_args_argv) + std::string Process::concatArguments(const std::string& exe, int more_args_argc, const char** more_args_argv) { std::vector tmp; for (int i = 0; i < more_args_argc; ++i) @@ -77,18 +100,18 @@ namespace WinTime return concatArguments(exe, tmp); } - std::wstring Process::concatArguments(const std::string& exe, std::vector command_args) + std::string Process::concatArguments(const std::string& exe, std::vector command_args) { - std::wstring_convert> converter; + //std::string_convert> converter; command_args.insert(command_args.begin(), exe); // prepend the exe - std::wstring result; + std::string result; for (auto arga : command_args) { - std::wstring arg = converter.from_bytes(arga); + std::string arg = (arga); // Does arg have quotes? E.g. '-DQT_TESTCASE_BUILDDIR="C:/dev/openms_build_ninja"' if (arg.find('"') != std::string::npos) { // escape them, otherwise the commandline parser will wrongly interpret those quotes - substitute(arg, L"\"", L"\\\""); + substitute(arg, "\"", "\\\""); } if (arg.find(' ') != std::string::npos) @@ -111,30 +134,40 @@ namespace WinTime return result; } - std::wstring Process::searchPATH(const std::wstring& target_exe, bool verbose) + std::string Process::searchPATH(const std::string& target_exe, bool verbose) { - if (!std::filesystem::exists(target_exe)) + auto wt = widen(target_exe); + if (!std::filesystem::exists(wt)) { - TCHAR buffer[1000]; - SearchPath(NULL, &target_exe[0], TEXT(".exe"), sizeof(buffer) / sizeof(TCHAR), buffer, NULL); - std::wstring result = buffer; + const int BUF_SIZE = 3000; + wchar_t buffer[BUF_SIZE]; + auto ret = SearchPathW(NULL, &wt[0], (L".exe"), sizeof(buffer) / sizeof(wchar_t), buffer, NULL); + if (!ret) + { + throw std::runtime_error("Could not find executable '" + target_exe + "' (%PATH% was also checked)."); + } + if (ret > BUF_SIZE) + { + throw std::runtime_error("Path to '" + target_exe + "' is longer than " + std::to_string(BUF_SIZE) + ". Cannot store result. Fix the program or use a shorter path."); + } + std::string result = narrow(buffer); if (verbose) { - std::wcout << "Found '" << target_exe << "' in PATH as '" << result << "'.\n"; + std::wcout << "Found '" << wt << "' in PATH as '" << buffer << "'.\n"; } return result; } return target_exe; } - Process::Process(const std::wstring& target_exe, std::wstring& p_command_args, DWORD dwCreationFlags) + Process::Process(const std::string& target_exe, const std::string& p_command_args, DWORD dwCreationFlags) { process_information_ = new PROCESS_INFORMATION; - STARTUPINFO startupInfo; + STARTUPINFOW startupInfo; memset(&startupInfo, 0, sizeof(startupInfo)); - startupInfo.cb = sizeof(STARTUPINFO); - - was_created_ = CreateProcess(&target_exe[0], &p_command_args[0], NULL, NULL, FALSE, + startupInfo.cb = sizeof(STARTUPINFOA); + std::string pca = p_command_args; + was_created_ = CreateProcessW(&widen(target_exe)[0], &widen(pca)[0], NULL, NULL, FALSE, dwCreationFlags, NULL, NULL, &startupInfo, process_information_); } diff --git a/WinTime/Process.h b/WinTime/Process.h index a96ae2e..5d74134 100644 --- a/WinTime/Process.h +++ b/WinTime/Process.h @@ -29,28 +29,28 @@ namespace WinTime { - int countBackslashesAtEnd(const std::wstring& arg); + int countBackslashesAtEnd(const std::string& arg); - void substitute(std::wstring& str, const std::wstring& search, - const std::wstring& replace); + void substitute(std::string& str, const std::string& search, + const std::string& replace); class Process { public: - static std::wstring getPathToCurrentProcess(); + static std::string getPathToCurrentProcess(); - static std::wstring concatArguments(const std::string& exe, int more_args_argc, char** more_args_argv); + static std::string concatArguments(const std::string& exe, int more_args_argc, const char** more_args_argv); - static std::wstring concatArguments(const std::string& exe, std::vector command_args); + static std::string concatArguments(const std::string& exe, std::vector command_args); /// search for an executable on the %PATH% environment variable. - static std::wstring searchPATH(const std::wstring& exe, bool verbose = false); + static std::string searchPATH(const std::string& exe, bool verbose = false); /// Starts a process, with extra arguments (if not empty) /// The @p target_exe must be an absolute or relative path. The %PATH% environment variable is not used! (use @p searchPATH if you need that) - Process(const std::wstring& target_exe, std::wstring& p_command_args, DWORD dwCreationFlags = 0); + Process(const std::string& target_exe, const std::string& p_command_args, DWORD dwCreationFlags = 0); /// Was the process succesfully created in the C'tor? bool wasCreated() const; @@ -68,4 +68,9 @@ namespace WinTime bool was_created_; }; + std::string narrow(const std::wstring& wide_str); + + std::wstring widen(const std::string uft8_str); + + } // namespace \ No newline at end of file diff --git a/WinTime/WinTime.cpp b/WinTime/WinTime.cpp index 4ef7087..ddc12ba 100644 --- a/WinTime/WinTime.cpp +++ b/WinTime/WinTime.cpp @@ -95,12 +95,11 @@ namespace WinTime DWORD exit_code{1}; }; - void PrintError(LPTSTR lpszFunction) + void PrintError(std::string lpszFunction) { // Retrieve the system error message for the last-error code LPVOID lpMsgBuf; - LPVOID lpDisplayBuf; DWORD dw = GetLastError(); FormatMessage( @@ -115,51 +114,42 @@ namespace WinTime // Display the error message - lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, - (lstrlen((LPCTSTR)lpMsgBuf) + lstrlen((LPCTSTR)lpszFunction) + 40) * sizeof(TCHAR)); - StringCchPrintf((LPTSTR)lpDisplayBuf, - LocalSize(lpDisplayBuf) / sizeof(TCHAR), - TEXT("%s failed with error %d: %s"), - lpszFunction, dw, lpMsgBuf); - - wprintf(L"%s", (wchar_t*)lpDisplayBuf); - + std::cerr << lpszFunction << " failed with error :" << std::to_string(dw) << ' ' << std::string((char*)lpMsgBuf); LocalFree(lpMsgBuf); - LocalFree(lpDisplayBuf); } - std::optional InjectDll(__in LPCWSTR lpcwszDll, const std::wstring& target_path, std::wstring& p_command_args) + std::optional InjectDll(__in LPCSTR lpcwszDll, const std::string& target_path, std::string& p_command_args) { std::optional result; Process process(target_path, p_command_args, CREATE_SUSPENDED); if (!process.wasCreated()) { - PrintError(TEXT("CreateProcess")); + PrintError("CreateProcess"); return result; } - LPVOID lpLoadLibraryW = GetProcAddress(GetModuleHandle(L"KERNEL32.DLL"), "LoadLibraryW"); + LPVOID lpLoadLibraryW = GetProcAddress(GetModuleHandleW(L"KERNEL32.DLL"), "LoadLibraryW"); if (!lpLoadLibraryW) { - PrintError(TEXT("GetProcAddress")); + PrintError("GetProcAddress"); return result; } - SIZE_T nLength = wcslen(lpcwszDll) * sizeof(WCHAR); + SIZE_T nLength = strlen(lpcwszDll); // allocate mem for dll name LPVOID lpRemoteString = VirtualAllocEx(process.getPI().hProcess, NULL, nLength + 1, MEM_COMMIT, PAGE_READWRITE); if (!lpRemoteString) { - PrintError(TEXT("VirtualAllocEx")); + PrintError("VirtualAllocEx"); return result; } // write dll name if (!WriteProcessMemory(process.getPI().hProcess, lpRemoteString, lpcwszDll, nLength, NULL)) { - PrintError(TEXT("WriteProcessMemory")); + PrintError("WriteProcessMemory"); // free allocated memory VirtualFreeEx(process.getPI().hProcess, lpRemoteString, 0, MEM_RELEASE); @@ -170,7 +160,7 @@ namespace WinTime HANDLE hThread = CreateRemoteThread(process.getPI().hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)lpLoadLibraryW, lpRemoteString, NULL, NULL); if (!hThread) { - PrintError(TEXT("CreateRemoteThread")); + PrintError("CreateRemoteThread"); } else { WaitForSingleObject(hThread, 4000); @@ -188,7 +178,7 @@ namespace WinTime DWORD exit_code{ 1 }; if (!GetExitCodeProcess(process.getPI().hProcess, &exit_code)) { - PrintError(TEXT("Could not get return code of target process")); + PrintError("Could not get return code of target process"); } auto timings = getProcessTime(process.getPI().hProcess); @@ -199,8 +189,15 @@ namespace WinTime using namespace WinTime; -int main(int argc, char** argv) +int wmain(int argc, wchar_t** argv_wide) { + std::vector argv(argc); + std::vector argv_data(argc); + for (int i = 0; i < argc; ++i) + { + argv_data[i] = narrow(argv_wide[i]); + argv[i] = argv_data[i].c_str(); + } args::ArgumentParser p_parser("WinTime - measure time and memory usage of a process.", ""); p_parser.helpParams.width = 134; //args::ValueFlag integer(parser, "integer", "The integer flag", { 'i' }); @@ -216,7 +213,7 @@ int main(int argc, char** argv) args::PositionalList p_command_args(p_parser, "ARG", "arguments to COMMAND"); try { - p_parser.ParseCLI(argc, argv); + p_parser.ParseCLI(argc, &argv.front()); } catch (args::Help) { @@ -242,85 +239,93 @@ int main(int argc, char** argv) exit(0); } - std::wstring_convert> converter; - std::wstring wcommand = Process::searchPATH(converter.from_bytes(args::get(p_command)), p_verbose); - + try + { + std::string command = Process::searchPATH(args::get(p_command), p_verbose); - const auto self_dir = Process::getPathToCurrentProcess(); + const auto self_dir = Process::getPathToCurrentProcess(); - const auto arch_result = checkMatchingArch(&wcommand[0]); - if (p_verbose || arch_result != ArchMatched::SAME) - { - std::wcout << getArchMatchedExplanation(arch_result, &wcommand[0]) << '\n'; - } - if (arch_result == ArchMatched::TARGET_UNKNOWN) - { - std::cerr << "Cannot determine target architecture. Is it an executable?\n"; - exit(1); - } - // try auto switching to WINTIME_EXE_OTHERARCH - if (arch_result == ArchMatched::MIXED) - { - std::cerr << "Trying to find '" << WINTIME_EXE_OTHERARCH << "' automatically...\n"; - std::wstring wintime_other = self_dir + L"\\" + converter.from_bytes(std::string(WINTIME_EXE_OTHERARCH)); - if (!std::filesystem::exists(wintime_other)) + const auto arch_result = checkMatchingArch(&command[0]); + if (p_verbose || arch_result != ArchMatched::SAME) { - std::wcerr << "Cannot find '" << wintime_other << "'; Please make sure it is present or invoke it manually!\n"; - exit(1); + std::cout << getArchMatchedExplanation(arch_result, &command[0]) << '\n'; } - - // it exists... now invoke it and quit this process - Process process(wintime_other, Process::concatArguments(converter.to_bytes(wintime_other), argc - 1, argv + 1), NORMAL_PRIORITY_CLASS); - if (!process.wasCreated()) + if (arch_result == ArchMatched::TARGET_UNKNOWN) { + std::cerr << "Cannot determine target architecture. Is it an executable?\n"; exit(1); + } + // try auto switching to WINTIME_EXE_OTHERARCH + if (arch_result == ArchMatched::MIXED) + { + std::cerr << "Trying to find '" << WINTIME_EXE_OTHERARCH << "' automatically...\n"; + std::string wintime_other = self_dir + "\\" + std::string(WINTIME_EXE_OTHERARCH); + if (!std::filesystem::exists(wintime_other)) + { + std::cerr << "Cannot find '" << wintime_other << "'; Please make sure it is present or invoke it manually!\n"; + exit(1); + } + + // it exists... now invoke it and quit this process + Process process(wintime_other, Process::concatArguments(wintime_other, argc - 1, (&argv[0]) + 1), NORMAL_PRIORITY_CLASS); + if (!process.wasCreated()) + { + exit(1); + } + process.waitForFinish(); + exit(0); } - process.waitForFinish(); - exit(0); - } - std::wstring wcommand_args = Process::concatArguments(args::get(p_command), StringList(args::get(p_command_args))); + std::string wcommand_args = Process::concatArguments(args::get(p_command), StringList(args::get(p_command_args))); - if (p_verbose) - { - std::wcerr << "CMD {ARGS}:\n " << (wcommand_args) << '\n'; - } + if (p_verbose) + { + std::wcerr << "CMD {ARGS}:\n " << widen(wcommand_args) << '\n'; + } - std::wstring dll_path = self_dir + L"\\" + converter.from_bytes(std::string(WINTIME_DLL)); + std::string dll_path = self_dir + "\\" + (std::string(WINTIME_DLL)); - if (!std::filesystem::exists(dll_path)) - { - std::wcerr << "Could not find DLL '" << (dll_path) << "' for injection. Make sure it is present!\n"; - return(1); - } + if (!std::filesystem::exists(dll_path)) + { + std::cerr << "Could not find DLL '" << (dll_path) << "' for injection. Make sure it is present!\n"; + return(1); + } - // create pipe before injecting DLL - NamedPipeServer ps; + // create pipe before injecting DLL + NamedPipeServer ps; - auto inject_result = InjectDll(dll_path.c_str(), wcommand.c_str(), wcommand_args); - if (!inject_result) - { - std::cerr << "Injecting Dll failed. Aborting.\n"; - return 1; - } + auto inject_result = InjectDll(dll_path.c_str(), command.c_str(), wcommand_args); + if (!inject_result) + { + std::cerr << "Injecting Dll failed. Aborting.\n"; + return 1; + } - char buffer[sizeof(ClientProcessMemoryCounter)]; - DWORD read_bytes; - if (!ps.readFromPipe(buffer, sizeof(buffer), read_bytes)) - { - std::cerr << "Could not read client data from pipe!\n"; - } - ClientProcessMemoryCounter pmc(buffer); - pmc.print(); + char buffer[sizeof(ClientProcessMemoryCounter)]; + DWORD read_bytes; + if (!ps.readFromPipe(buffer, sizeof(buffer), read_bytes)) + { + std::cerr << "Could not read client data from pipe!\n"; + } + ClientProcessMemoryCounter pmc(buffer); + pmc.print(); + + inject_result->ptime.print(); - inject_result->ptime.print(); + if (p_output_file) + { + FileLog fl(p_output_file.Get(), p_append.Get() ? OpenMode::APPEND : OpenMode::OVERWRITE); + fl.log((wcommand_args), inject_result->ptime, pmc); + } + + // return the same exit code as target process + return inject_result->exit_code; - if (p_output_file) + } + catch (std::exception& ex) { - FileLog fl(p_output_file.Get(), p_append.Get() ? OpenMode::APPEND : OpenMode::OVERWRITE); - fl.log(converter.to_bytes(wcommand_args), inject_result->ptime, pmc); + std::cerr << "Exception occured: " << ex.what() << "\nAborting...\n"; + return 1; } - // return the same exit code as target process - return inject_result->exit_code; } From 0adc31d60a45fc4c93a6dc5cd9a3fb335c936fa7 Mon Sep 17 00:00:00 2001 From: Chris Bielow Date: Tue, 28 Mar 2023 08:40:12 +0200 Subject: [PATCH 2/8] abandon dll injection and query RAM usage directly --- CMakeLists.txt | 7 +-- README.md | 7 ++- WinTime/CMakeLists.txt | 7 +-- WinTime/FileLog.cpp | 5 +- WinTime/FileLog.h | 2 + WinTime/Memory.h | 124 +++++++++++++++++++++++++++++++++++++++++ WinTime/Process.cpp | 3 +- WinTime/WinTime.cpp | 94 ++++++------------------------- 8 files changed, 155 insertions(+), 94 deletions(-) create mode 100644 WinTime/Memory.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7edeb7b..d764ac4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.12) project(WinTimeAll VERSION 1.0) # specify the C++ standard -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) ## requires a 'project()' call first! if (CMAKE_SIZEOF_VOID_P EQUAL 4) @@ -17,12 +17,7 @@ endif() message(STATUS "Target Arch is ${ARCH}") ## the order needs to be exactly this (due to internal references) -add_subdirectory(NamedPipeLib) add_subdirectory(WinTime) -add_subdirectory(WinTimeDll) ## needs to go last, since it refs WinTime add_subdirectory(ExampleTarget) -## run that here again (already done in WinTime), but now WinTimeDll data is available -file(REMOVE "${WinTime_BINARY_DIR}/config.h") -configure_file("${WinTime_SOURCE_DIR}/config.h.in" "${WinTime_BINARY_DIR}/config.h" @ONLY) diff --git a/README.md b/README.md index 0272d67..89f9e75 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ WinTime64 -a -o log.txt Firefox.exe - PageFaultCount - PeakPagefileUsage - log file output + - supports Unicode program names and arguments via UTF-8 encoding - similar command line interface as /usr/bin/time ## Installation @@ -116,16 +117,16 @@ Open a [bug report, feature request](https://github.com/cbielow/wintime/issues) ## Technical details -WinTime makes heavy use of Windows API functions. It works by launching the target process and immediatedly injecting a Dll into it. Upon termination, the Dll will get notified that the target process is about to finish, and take the opportunity to measure peak ram usage and other metrics. This data will be send to WinTime using named pipes. WinTime itself will measure the CPU time of the target process, which can be done even after the process has exited iff you know the process ID (which we do since we spawned the process). +WinTime makes heavy use of Windows API functions. WinTime will invoke the target process and measure the CPU time and RAM usage of the target process. This can be done even after the process has exited iff you know the process ID (which we do since we spawned the process). The `-o` option allows to write/append the data to a log, which requires some file locking magic to ensure that concurrent access to the log does not mangle its content. ## How accurate is the data? ##### RAM -The RAM usage of the target process is slightly increased by the injected Dll and the Windows functions it calls -- about 1.3 MiB. Similarly, PageFaultCount increases by about 344 (which makes sense for a 4kb page, i.e. 344*4 Kb = 1376 Kb). Those are worst case numbers. If the target process natively calls GetProcessMemoryInfo(), these numbers will go down (due to reused pages). At a bare minimum, the RAM usage will increase by the size of the injected Dll, which is currently ~50 Kb. +Same as `GetProcessMemoryInfo()` (part of the Windows API). ##### CPU -The CPU time (wall time, kernel time, user time) are high resolution, but include some minor overhead of injecting the Dll and time of calling the RAM usage function inside the target process -- on my system that is at most 5 milliseconds (this is how long a process executes which does absolutely nothing except execution of an empty `main()` function plus the overhead of the injected WinTime.dll). +The CPU time (wall time, kernel time, user time) are high resolution. ## License MIT diff --git a/WinTime/CMakeLists.txt b/WinTime/CMakeLists.txt index 0531628..3b9766a 100644 --- a/WinTime/CMakeLists.txt +++ b/WinTime/CMakeLists.txt @@ -4,11 +4,8 @@ set(WINTIME_EXE "WinTime${ARCH}" CACHE STRING "Name of the exe" FORCE) set(WINTIME_EXE_OTHERARCH "WinTime${ARCHOTHER}" CACHE STRING "Name of the exe of other bitness" FORCE) -## run that here, so the file is present... (but run configure_file later, since we need WinTimeDLL in there as well) -file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/config.h" "") -add_executable(${WINTIME_EXE} WinTime.cpp args.hxx Time.h Time.cpp FileLog.h FileLog.cpp Console.h Console.cpp Arch.h Arch.cpp Process.h Process.cpp "${CMAKE_CURRENT_BINARY_DIR}/config.h") - -target_link_libraries(${WINTIME_EXE} PRIVATE ${NAMEDPIPELIB}) +configure_file("${WinTime_SOURCE_DIR}/config.h.in" "${WinTime_BINARY_DIR}/config.h" @ONLY) +add_executable(${WINTIME_EXE} WinTime.cpp args.hxx Time.h Time.cpp FileLog.h FileLog.cpp Console.h Console.cpp Arch.h Arch.cpp Memory.h Process.h Process.cpp "${CMAKE_CURRENT_BINARY_DIR}/config.h") ## include the binary dir to have access to config.h target_include_directories(${WINTIME_EXE} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/WinTime/FileLog.cpp b/WinTime/FileLog.cpp index 9ade206..ab11cb0 100644 --- a/WinTime/FileLog.cpp +++ b/WinTime/FileLog.cpp @@ -22,8 +22,11 @@ #include "FileLog.h" +#include "Memory.h" + #include // for _chsize_s() #include +#include #include using namespace std; @@ -131,7 +134,7 @@ namespace WinTime std::stringstream content; if (lockf.isFileEmpty()) { // at start of file .. write header - printLineToStream(content, '\t', [](auto type, const char sep) { return type.printHeader(sep); }, Command(), PTime(), ClientProcessMemoryCounter(PROCESS_MEMORY_COUNTERS())); + printLineToStream(content, '\t', [](auto type, const char sep) { return type.printHeader(sep); }, Command(), time, pmc); } printLineToStream(content, '\t', [](auto type, const char sep) { return type.print(sep); }, Command{ cmd }, time, pmc); lockf.write(content.str().c_str()); diff --git a/WinTime/FileLog.h b/WinTime/FileLog.h index 8649f28..9feaf94 100644 --- a/WinTime/FileLog.h +++ b/WinTime/FileLog.h @@ -104,6 +104,8 @@ namespace WinTime /// Truncates the file to the last write position and closes the stream ~LockedFile(); }; + + struct ClientProcessMemoryCounter; class FileLog { diff --git a/WinTime/Memory.h b/WinTime/Memory.h new file mode 100644 index 0000000..182315f --- /dev/null +++ b/WinTime/Memory.h @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2023-2023 Chris Bielow + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#pragma once + +#include +#include +#include +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#define _CRT_SECURE_NO_WARNINGS 1 +#include +#include + + // To ensure correct resolution of symbols, add Psapi.lib to TARGETLIBS + // and compile with -DPSAPI_VERSION=1 +#define PSAPI_VERSION 1 +#include // for PROCESS_MEMORY_COUNTERS +#pragma comment (lib, "Psapi.lib") + +namespace WinTime +{ + + /// convert bytes to a human readable unit (TiB, GiB, MiB, KiB) + inline std::string toHumanReadable(uint64_t bytes) + { + std::array units{ "byte", "KiB", "MiB", "GiB", "TiB", "PiB" }; + + const int divisor = 1024; + double db = double(bytes); + for (const auto u : units) + { + if (db < divisor) + { + std::stringstream ss; + ss << std::setprecision(4) /* 4 digits overall, i.e. 1000 or 1.456 */ << db << ' ' << u; + return ss.str(); + } + db /= divisor; + } + // wow ... you made it here... + return std::string("Congrats. That's a lot of bytes: ") + std::to_string(bytes); + } + + /// A serializable wrapper around a 'PROCESS_MEMORY_COUNTERS' struct + struct ClientProcessMemoryCounter + { + explicit ClientProcessMemoryCounter(HANDLE hProcess) + : data_{} + { + bool res = GetProcessMemoryInfo(hProcess, &data_, sizeof(data_)); + if (!res) + { + throw std::runtime_error("Could not get memory info!"); + } + } + + void print() const + { // only report data which does not depend on the time of measurement (which was when unloading the Dll). Only report maximia (peaks). + std::cerr << "PageFaultCount: " << (data_.PageFaultCount) << '\n'; + std::cerr << "PeakWorkingSetSize: " << toHumanReadable(data_.PeakWorkingSetSize) << '\n'; + //std::cerr << "WorkingSetSize: " << toHumanReadable(data_.WorkingSetSize) << '\n'; + std::cerr << "QuotaPeakPagedPoolUsage: " << toHumanReadable(data_.QuotaPeakPagedPoolUsage) << '\n'; + //std::cerr << "QuotaPagedPoolUsage: " << toHumanReadable(data_.QuotaPagedPoolUsage) << '\n'; + std::cerr << "QuotaPeakNonPagedPoolUsage: " << toHumanReadable(data_.QuotaPeakNonPagedPoolUsage) << '\n'; + //std::cerr << "QuotaNonPagedPoolUsage: " << toHumanReadable(data_.QuotaNonPagedPoolUsage) << '\n'; + //std::cerr << "PagefileUsage: " << toHumanReadable(data_.PagefileUsage) << '\n'; + std::cerr << "PeakPagefileUsage: " << toHumanReadable(data_.PeakPagefileUsage) << '\n'; + } + + std::string print(const char separator) const + { + std::stringstream where; + where << data_.PageFaultCount << separator + << (data_.PeakWorkingSetSize) << separator + << toHumanReadable(data_.PeakWorkingSetSize) << separator + //<< toHumanReadable(data_.WorkingSetSize) << separator + << toHumanReadable(data_.QuotaPeakPagedPoolUsage) << separator + //<< toHumanReadable(data_.QuotaPagedPoolUsage) << separator + << toHumanReadable(data_.QuotaPeakNonPagedPoolUsage) << separator + //<< toHumanReadable(data_.QuotaNonPagedPoolUsage) << separator + //<< toHumanReadable(data_.PagefileUsage) << separator + << toHumanReadable(data_.PeakPagefileUsage); + return where.str(); + } + + static std::string printHeader(const char separator) + { // only report data which does not depend on the time of measurement (which was when unloading the Dll). Only report maximia (peaks). + std::stringstream where; + where << "PageFaultCount" << separator + << "PeakWorkingSetSize (bytes)" << separator + << "PeakWorkingSetSize" << separator + //<< "WorkingSetSize" << separator + << "QuotaPeakPagedPoolUsage" << separator + //<< "QuotaPagedPoolUsage" << separator + << "QuotaPeakNonPagedPoolUsage" << separator + //<< "QuotaNonPagedPoolUsage" << separator + //<< "PagefileUsage" << separator + << "PeakPagefileUsage"; + return where.str(); + } + private: + PROCESS_MEMORY_COUNTERS data_; + }; + +} // namespace \ No newline at end of file diff --git a/WinTime/Process.cpp b/WinTime/Process.cpp index 6f81ac0..74e20be 100644 --- a/WinTime/Process.cpp +++ b/WinTime/Process.cpp @@ -105,9 +105,8 @@ namespace WinTime //std::string_convert> converter; command_args.insert(command_args.begin(), exe); // prepend the exe std::string result; - for (auto arga : command_args) + for (auto arg : command_args) { - std::string arg = (arga); // Does arg have quotes? E.g. '-DQT_TESTCASE_BUILDDIR="C:/dev/openms_build_ninja"' if (arg.find('"') != std::string::npos) { // escape them, otherwise the commandline parser will wrongly interpret those quotes diff --git a/WinTime/WinTime.cpp b/WinTime/WinTime.cpp index ddc12ba..291e8fa 100644 --- a/WinTime/WinTime.cpp +++ b/WinTime/WinTime.cpp @@ -36,13 +36,10 @@ #include "Arch.h" #include "config.h" -#include "Time.h" #include "FileLog.h" +#include "Memory.h" #include "Process.h" -// from NamedPipeLib -#include -#include -#include +#include "Time.h" #include "args.hxx" // arg parser @@ -92,6 +89,7 @@ namespace WinTime struct TargetInfo { PTime ptime{}; + ClientProcessMemoryCounter pmc; DWORD exit_code{1}; }; @@ -118,60 +116,16 @@ namespace WinTime LocalFree(lpMsgBuf); } - std::optional InjectDll(__in LPCSTR lpcwszDll, const std::string& target_path, std::string& p_command_args) + std::optional runExternalProcess(const std::string& target_path, std::string& p_command_args) { std::optional result; - Process process(target_path, p_command_args, CREATE_SUSPENDED); + Process process(target_path, p_command_args); if (!process.wasCreated()) { PrintError("CreateProcess"); return result; } - LPVOID lpLoadLibraryW = GetProcAddress(GetModuleHandleW(L"KERNEL32.DLL"), "LoadLibraryW"); - - if (!lpLoadLibraryW) - { - PrintError("GetProcAddress"); - return result; - } - - SIZE_T nLength = strlen(lpcwszDll); - - // allocate mem for dll name - LPVOID lpRemoteString = VirtualAllocEx(process.getPI().hProcess, NULL, nLength + 1, MEM_COMMIT, PAGE_READWRITE); - if (!lpRemoteString) - { - PrintError("VirtualAllocEx"); - return result; - } - - // write dll name - if (!WriteProcessMemory(process.getPI().hProcess, lpRemoteString, lpcwszDll, nLength, NULL)) { - - PrintError("WriteProcessMemory"); - // free allocated memory - VirtualFreeEx(process.getPI().hProcess, lpRemoteString, 0, MEM_RELEASE); - - return result; - } - - // call loadlibraryw - HANDLE hThread = CreateRemoteThread(process.getPI().hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)lpLoadLibraryW, lpRemoteString, NULL, NULL); - - if (!hThread) { - PrintError("CreateRemoteThread"); - } - else { - WaitForSingleObject(hThread, 4000); - - //resume suspended process - ResumeThread(process.getPI().hThread); - } - - // free allocated memory - VirtualFreeEx(process.getPI().hProcess, lpRemoteString, 0, MEM_RELEASE); - // wait for the child process to finish process.waitForFinish(); @@ -181,9 +135,10 @@ namespace WinTime PrintError("Could not get return code of target process"); } + ClientProcessMemoryCounter pmc(process.getPI().hProcess); auto timings = getProcessTime(process.getPI().hProcess); - return TargetInfo{ timings, exit_code }; + return TargetInfo{ timings, pmc, exit_code }; } } // namespace @@ -283,44 +238,29 @@ int wmain(int argc, wchar_t** argv_wide) std::wcerr << "CMD {ARGS}:\n " << widen(wcommand_args) << '\n'; } - std::string dll_path = self_dir + "\\" + (std::string(WINTIME_DLL)); - - if (!std::filesystem::exists(dll_path)) - { - std::cerr << "Could not find DLL '" << (dll_path) << "' for injection. Make sure it is present!\n"; - return(1); - } - - // create pipe before injecting DLL - NamedPipeServer ps; - - auto inject_result = InjectDll(dll_path.c_str(), command.c_str(), wcommand_args); - if (!inject_result) + + auto external_process_result = runExternalProcess(command.c_str(), wcommand_args); + if (!external_process_result) { - std::cerr << "Injecting Dll failed. Aborting.\n"; + std::cerr << "Running external process failed. Aborting.\n"; return 1; } - char buffer[sizeof(ClientProcessMemoryCounter)]; - DWORD read_bytes; - if (!ps.readFromPipe(buffer, sizeof(buffer), read_bytes)) + // only print to commandline if verbose or not writing to file + if (!p_output_file || p_verbose) { - std::cerr << "Could not read client data from pipe!\n"; + external_process_result->pmc.print(); + external_process_result->ptime.print(); } - ClientProcessMemoryCounter pmc(buffer); - pmc.print(); - - inject_result->ptime.print(); if (p_output_file) { FileLog fl(p_output_file.Get(), p_append.Get() ? OpenMode::APPEND : OpenMode::OVERWRITE); - fl.log((wcommand_args), inject_result->ptime, pmc); + fl.log(wcommand_args, external_process_result->ptime, external_process_result->pmc); } // return the same exit code as target process - return inject_result->exit_code; - + return external_process_result->exit_code; } catch (std::exception& ex) { From 19ba1593a1971c8a965116b352862e8e62969e93 Mon Sep 17 00:00:00 2001 From: Chris Bielow Date: Thu, 6 Apr 2023 10:28:05 +0200 Subject: [PATCH 3/8] allow creation of log file which contains non-ascii chars --- WinTime/FileLog.cpp | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/WinTime/FileLog.cpp b/WinTime/FileLog.cpp index ab11cb0..06165b1 100644 --- a/WinTime/FileLog.cpp +++ b/WinTime/FileLog.cpp @@ -23,10 +23,12 @@ #include "FileLog.h" #include "Memory.h" +#include "Process.h" #include // for _chsize_s() #include #include +#include #include using namespace std; @@ -42,27 +44,30 @@ namespace WinTime : filename_(filename), openmode_(mode) { + // First, make sure the file exists: + auto wfile = widen(filename_); + if (!std::filesystem::exists(wfile)) + { + std::wofstream of(wfile); + if (!of.is_open()) + { // this may print bogus characters if non-ascii letters are printed, but + // printing to wcerr << wfile will not show non-ascii either, unless fiddling with + // _setmode, which breaks ascii output... well done Microsoft! + std::cerr << "Could not open file " << filename_ << ". Do you have permission to create it in this directory?\n"; + throw std::runtime_error("Could not open file."); + } + } } bool LockedFile::tryLock() { - // First, make sure the file exists: - // Opens for reading and appending; creates the file first if it doesn't exist. - FILE* tmp = _fsopen(filename_.c_str(), "a", _SH_DENYWR); - - if (!tmp) - { // could not create file. Is it locked by someone else? - return false; - } - fclose(tmp); // close the lock, otherwise we cannot lock it again below - - // at this point, the file exists! + // at this point, the file exists! -- see C'tor // open file for reading and writing (so we can move to the end, depending on 'mode_'. // We cannot query for filesize before, because another process might write stuff to the file // immediately afterwards (before we _fsopen for just writing in case we found the file being empty; this // would overwrite the other process' data) - is_locked_ = ((stream_ = _fsopen(filename_.c_str(), "r+", _SH_DENYWR)) != NULL); + is_locked_ = ((stream_ = _wfsopen(&widen(filename_)[0], L"r+", _SH_DENYWR)) != NULL); if (!is_locked_) return false; From 806cc33b43552a091086624171b5bf5eb6e2d271 Mon Sep 17 00:00:00 2001 From: Chris Bielow Date: Thu, 6 Apr 2023 13:39:19 +0200 Subject: [PATCH 4/8] fix widen for arbitrary long arguments --- WinTime/Process.cpp | 26 +++++++++++++++----------- WinTime/Process.h | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/WinTime/Process.cpp b/WinTime/Process.cpp index 74e20be..d111289 100644 --- a/WinTime/Process.cpp +++ b/WinTime/Process.cpp @@ -63,25 +63,27 @@ namespace WinTime std::string narrow(const std::wstring& wide_str) { - char buffer[1000]; - WideCharToMultiByte(CP_UTF8, + std::string buffer(wide_str.size() * 2 + 2, '\0'); + auto bytes_written = WideCharToMultiByte(CP_UTF8, 0, wide_str.c_str(), -1, - buffer, 1000, + buffer.data(), 1000, NULL, NULL); - return std::string(buffer); + buffer.resize(bytes_written); + return buffer; } - std::wstring widen(const std::string uft8_str) + std::wstring widen(const std::string& uft8_str) { - wchar_t buffer[1000]; - MultiByteToWideChar(CP_UTF8, + std::wstring buffer(uft8_str.size() + 2, '\0'); + auto bytes_written = MultiByteToWideChar(CP_UTF8, 0, uft8_str.c_str(), -1, - buffer, 1000); - return std::wstring(buffer); + buffer.data(), buffer.size()); + buffer.resize(bytes_written); + return buffer; } std::string Process::getPathToCurrentProcess() @@ -164,9 +166,11 @@ namespace WinTime process_information_ = new PROCESS_INFORMATION; STARTUPINFOW startupInfo; memset(&startupInfo, 0, sizeof(startupInfo)); - startupInfo.cb = sizeof(STARTUPINFOA); + startupInfo.cb = sizeof(startupInfo); std::string pca = p_command_args; - was_created_ = CreateProcessW(&widen(target_exe)[0], &widen(pca)[0], NULL, NULL, FALSE, + auto texe = widen(target_exe); + auto pargs = widen(pca); + was_created_ = CreateProcessW(&texe[0], &pargs[0], NULL, NULL, FALSE, dwCreationFlags, NULL, NULL, &startupInfo, process_information_); } diff --git a/WinTime/Process.h b/WinTime/Process.h index 5d74134..141d111 100644 --- a/WinTime/Process.h +++ b/WinTime/Process.h @@ -70,7 +70,7 @@ namespace WinTime std::string narrow(const std::wstring& wide_str); - std::wstring widen(const std::string uft8_str); + std::wstring widen(const std::string& uft8_str); } // namespace \ No newline at end of file From c201906e92415e292ad558cdebd42520e43aebb2 Mon Sep 17 00:00:00 2001 From: Chris Bielow Date: Fri, 7 Apr 2023 16:49:46 +0200 Subject: [PATCH 5/8] fix allocationm (a simple malloc will not bring map all virtual allocations into physical pages, hence peak mem usage would remain low). --- ExampleTarget/ExampleTarget.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ExampleTarget/ExampleTarget.cpp b/ExampleTarget/ExampleTarget.cpp index 7fdba7c..fba559b 100644 --- a/ExampleTarget/ExampleTarget.cpp +++ b/ExampleTarget/ExampleTarget.cpp @@ -86,10 +86,9 @@ int main(int argc, char** argv) { size_t mb = std::abs(atoi(argv[1])); std::cerr << " -- Allocating " << mb << " Mb\n"; - volatile auto ptr = malloc(mb * 1024 * 1024 + 1); - ((char*)ptr)[0] = '\n'; - std::cerr << " -- Deallocating\n"; - free(ptr); + size_t bytes_to_allocate = mb * 1024 * 1024; + std::string large_data(bytes_to_allocate, 0); + volatile char* vd = large_data.data(); } if (argc >= 3) { @@ -103,4 +102,7 @@ int main(int argc, char** argv) { getMemoryInfo(GetCurrentProcessId()); } + std::cerr << "-- end of ExampleTarget\n\n"; } + + From ff85945d9d4082e3eb248f44e0942f010f5b4378 Mon Sep 17 00:00:00 2001 From: Chris Bielow Date: Fri, 7 Apr 2023 21:00:04 +0200 Subject: [PATCH 6/8] report local time instead of UTC --- WinTime/Time.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/WinTime/Time.cpp b/WinTime/Time.cpp index 15c31f0..23927a5 100644 --- a/WinTime/Time.cpp +++ b/WinTime/Time.cpp @@ -107,8 +107,11 @@ namespace WinTime PTime getProcessTime(HANDLE hProcess) { - FILETIME lpCreationTime{}, + FILETIME + lpCreationTime{}, + lpCreationTimeLocal{}, lpExitTime{}, + lpExitTimeLocal{}, lpKernelTime{}, lpUserTime{}; @@ -122,9 +125,12 @@ namespace WinTime std::cerr << "Could not query Process timings\n"; return PTime{}; } + FileTimeToLocalFileTime(&lpCreationTime, &lpCreationTimeLocal); + FileTimeToLocalFileTime(&lpExitTime, &lpExitTimeLocal); + SYSTEMTIME t_create, t_exit; - FileTimeToSystemTime(&lpCreationTime, &t_create); - FileTimeToSystemTime(&lpExitTime, &t_exit); + FileTimeToSystemTime(&lpCreationTimeLocal, &t_create); + FileTimeToSystemTime(&lpExitTimeLocal, &t_exit); return PTime{ toDateString(t_create), toDateString(t_exit), toSeconds(lpUserTime), From 5a0a4fcdad7e08b8244f01461ee3b811f806ff28 Mon Sep 17 00:00:00 2001 From: Chris Bielow Date: Fri, 7 Apr 2023 21:00:22 +0200 Subject: [PATCH 7/8] fix docs --- ExampleTarget/ExampleTarget.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ExampleTarget/ExampleTarget.cpp b/ExampleTarget/ExampleTarget.cpp index fba559b..8df6e2f 100644 --- a/ExampleTarget/ExampleTarget.cpp +++ b/ExampleTarget/ExampleTarget.cpp @@ -78,9 +78,9 @@ int main(int argc, char** argv) if (argc == 1) { std::cerr << "Usage " << argv[0] << " \n" - " MiB alloc: [number] MiB to allocate\n" - " ms sleep: [number, optional] Milliseconds to sleep\n" - " stats: [any value, optional] Print memory usage using internal functions (useful to estimate overhead of external WinTime)\n"; + " MiB alloc: [number] MiB to allocate (this may take some time, especially for larger allocations)\n" + " ms sleep: [number, optional] Milliseconds to sleep (in addition to the time it takes to allocate -- see above)\n" + " stats: [any value, optional] Print memory usage using internal functions (useful to compare against the results of an external WinTime call)\n"; } if (argc >= 2) { From a32e1cdb99e39f6603e1492c3b95eff58d1aaf94 Mon Sep 17 00:00:00 2001 From: Chris Bielow Date: Fri, 7 Apr 2023 21:00:48 +0200 Subject: [PATCH 8/8] added CHANGELOG; --- CHANGELOG | 9 +++++++++ README.md | 9 +++++---- 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..b54af38 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,9 @@ + +V1.1 - 2023/04/07 + - Unicode Support for command line arguments + - report local time for Creation/Exit time of process instead of UTC + - fix ExampleTarget RAM usage in Release mode (RAM allocation was not effective) + - switch from dll-injection to external querying of RAM usage + +V1.0 - 2023/02/13 + - initial release \ No newline at end of file diff --git a/README.md b/README.md index 89f9e75..4435ed9 100644 --- a/README.md +++ b/README.md @@ -59,10 +59,11 @@ WinTime64 -a -o log.txt Firefox.exe ## Features - - peak **RAM** usage - - total **CPU time** (wall, kernel, user) with high resolution - - PageFaultCount - - PeakPagefileUsage + - reports: + - peak **RAM** usage + - total **CPU time** (wall, kernel, user) with high resolution + - PageFaultCount + - PeakPagefileUsage - log file output - supports Unicode program names and arguments via UTF-8 encoding - similar command line interface as /usr/bin/time