diff --git a/CMakeLists.txt b/CMakeLists.txt index 67fd9de3..7eb4b86c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,7 @@ if(WIN32) # resolve to the same *W types minwin.h forward-declares (otherwise the two # disagree and MSVC reports "redefinition; different basic types"). target_compile_definitions(eq_base PUBLIC OS_WIN NOMINMAX UNICODE _UNICODE) + target_link_libraries(eq_base PRIVATE psapi) if(MSVC) target_compile_options(eq_base PUBLIC "/utf-8") endif() diff --git a/base/premake5.lua b/base/premake5.lua index 71f2d27f..ae051c84 100644 --- a/base/premake5.lua +++ b/base/premake5.lua @@ -19,6 +19,8 @@ end local function base_project() filter("configurations:Profile") dependencies("tracysdk") + filter("system:windows") + links("psapi") filter{} warnings("High") @@ -93,4 +95,4 @@ project("base_memory_unittests") defines({ "BASE_STRIP_BUGCHECK", "BASE_MEM_CORE_DEBUG" -- enable additional debug verifications in the mem subsystem - }) \ No newline at end of file + }) diff --git a/base/process/process_metrics.h b/base/process/process_metrics.h new file mode 100644 index 00000000..e3dc0846 --- /dev/null +++ b/base/process/process_metrics.h @@ -0,0 +1,34 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. +#pragma once + +#include +#include + +namespace base { + +#if defined(OS_WIN) +using ProcessHandle = void*; +#elif defined(OS_MAC) || defined(OS_MACOS) +using ProcessHandle = u32; // mach_port_t +#else +using ProcessHandle = i32; // pid_t +#endif + +struct ProcessMemoryUsage { + mem_size resident_set_bytes = 0; + mem_size peak_resident_set_bytes = 0; +}; + +// Returns a borrowed native handle for the calling process. Callers must not +// close it. External handles remain owned by their creator. +BASE_EXPORT ProcessHandle GetCurrentProcessHandle(); + +// Queries physical memory resident for |process|. On POSIX the handle is a pid, +// on macOS a Mach task port, and on Windows a process HANDLE with query rights. +// The peak may be zero when the platform cannot provide it. Clears |usage| on +// failure. +BASE_EXPORT bool QueryProcessMemoryUsage(ProcessHandle process, + ProcessMemoryUsage& usage); + +} // namespace base diff --git a/base/process/process_metrics_linux.cc b/base/process/process_metrics_linux.cc new file mode 100644 index 00000000..37985f90 --- /dev/null +++ b/base/process/process_metrics_linux.cc @@ -0,0 +1,45 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include "base/process/process_metrics.h" + +#include + +#include + +namespace base { + +ProcessHandle GetCurrentProcessHandle() { + return static_cast(getpid()); +} + +bool QueryProcessMemoryUsage(ProcessHandle process, ProcessMemoryUsage& usage) { + usage = {}; + if (process <= 0) + return false; + + char path[64]; + std::snprintf(path, sizeof(path), "/proc/%d/status", process); + std::FILE* status = std::fopen(path, "r"); + if (!status) + return false; + + bool found_resident = false; + char line[256]; + while (std::fgets(line, sizeof(line), status)) { + unsigned long long kib = 0; + if (std::sscanf(line, "VmRSS: %llu kB", &kib) == 1) { + usage.resident_set_bytes = static_cast(kib) * 1024u; + found_resident = true; + } else if (std::sscanf(line, "VmHWM: %llu kB", &kib) == 1) { + usage.peak_resident_set_bytes = static_cast(kib) * 1024u; + } + } + std::fclose(status); + if (found_resident) + return true; + usage = {}; + return false; +} + +} // namespace base diff --git a/base/process/process_metrics_mac.cc b/base/process/process_metrics_mac.cc new file mode 100644 index 00000000..d2510fee --- /dev/null +++ b/base/process/process_metrics_mac.cc @@ -0,0 +1,31 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include "base/process/process_metrics.h" + +#include +namespace base { + +ProcessHandle GetCurrentProcessHandle() { + static_assert(sizeof(ProcessHandle) == sizeof(mach_port_t)); + return static_cast(mach_task_self()); +} + +bool QueryProcessMemoryUsage(ProcessHandle process, ProcessMemoryUsage& usage) { + usage = {}; + const mach_port_t task = static_cast(process); + if (!MACH_PORT_VALID(task)) + return false; + + mach_task_basic_info_data_t task_memory{}; + mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; + const kern_return_t result = task_info( + task, MACH_TASK_BASIC_INFO, reinterpret_cast(&task_memory), &count); + if (result != KERN_SUCCESS) + return false; + usage.resident_set_bytes = static_cast(task_memory.resident_size); + usage.peak_resident_set_bytes = static_cast(task_memory.resident_size_max); + return true; +} + +} // namespace base diff --git a/base/process/process_metrics_test.cc b/base/process/process_metrics_test.cc new file mode 100644 index 00000000..a08d9f01 --- /dev/null +++ b/base/process/process_metrics_test.cc @@ -0,0 +1,53 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include "base/process/process_metrics.h" + +#if defined(OS_LINUX) +#include +#include +#include +#endif + +#include + +namespace base { +namespace { + +TEST(ProcessMetricsTest, ReportsCurrentProcessResidentSet) { + ProcessMemoryUsage usage; + ASSERT_TRUE(QueryProcessMemoryUsage(GetCurrentProcessHandle(), usage)); + EXPECT_GT(usage.resident_set_bytes, 0u); + if (usage.peak_resident_set_bytes > 0) { + EXPECT_GE(usage.peak_resident_set_bytes, usage.resident_set_bytes); + } +} + +TEST(ProcessMetricsTest, RejectsInvalidHandleAndClearsOutput) { + ProcessMemoryUsage usage{1, 1}; + EXPECT_FALSE(QueryProcessMemoryUsage(ProcessHandle{}, usage)); + EXPECT_EQ(usage.resident_set_bytes, 0u); + EXPECT_EQ(usage.peak_resident_set_bytes, 0u); +} + +#if defined(OS_LINUX) +TEST(ProcessMetricsTest, ReportsAnotherProcessResidentSet) { + const pid_t child = fork(); + ASSERT_NE(child, -1); + if (child == 0) { + pause(); + _exit(0); + } + + ProcessMemoryUsage usage; + const bool queried = QueryProcessMemoryUsage(static_cast(child), usage); + kill(child, SIGTERM); + waitpid(child, nullptr, 0); + + ASSERT_TRUE(queried); + EXPECT_GT(usage.resident_set_bytes, 0u); +} +#endif + +} // namespace +} // namespace base diff --git a/base/process/process_metrics_win.cc b/base/process/process_metrics_win.cc new file mode 100644 index 00000000..5e2d63ea --- /dev/null +++ b/base/process/process_metrics_win.cc @@ -0,0 +1,33 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include "base/process/process_metrics.h" + +#include +#include + +namespace base { + +ProcessHandle GetCurrentProcessHandle() { + static_assert(sizeof(ProcessHandle) == sizeof(HANDLE)); + return GetCurrentProcess(); +} + +bool QueryProcessMemoryUsage(ProcessHandle process, ProcessMemoryUsage& usage) { + usage = {}; + // GetCurrentProcess() is the valid pseudo-handle (HANDLE)-1, the same bit + // pattern as INVALID_HANDLE_VALUE, and GetProcessMemoryInfo accepts it. + if (!process) + return false; + + PROCESS_MEMORY_COUNTERS counters{}; + counters.cb = static_cast(sizeof(counters)); + if (!GetProcessMemoryInfo(process, &counters, static_cast(sizeof(counters)))) { + return false; + } + usage.resident_set_bytes = static_cast(counters.WorkingSetSize); + usage.peak_resident_set_bytes = static_cast(counters.PeakWorkingSetSize); + return true; +} + +} // namespace base diff --git a/build/build_config.lua b/build/build_config.lua index e53e8827..06f1160d 100644 --- a/build/build_config.lua +++ b/build/build_config.lua @@ -14,7 +14,10 @@ defines({ "OS_LINUX", "OS_POSIX" }) -- we also define the POSIX alias here) buildoptions("-mwaitpkg -mrtm") -- Enable Intel(R) Transactional Synchronization Extensions (-mrtm) and WAITPKG instructions support (-mwaitpkg) on relevant processors -defines("OS_MACOS") +filter("system:macosx") +defines({ + "OS_MAC", + "OS_POSIX" }) filter {} -- -std=c++2b