From 15cb2196bf31d654778fbc0172a8d6170c98ab06 Mon Sep 17 00:00:00 2001 From: Vincent Hengel Date: Wed, 15 Jul 2026 10:28:34 +0200 Subject: [PATCH 1/3] base: add current process memory metrics --- CMakeLists.txt | 1 + base/premake5.lua | 4 ++- base/process/process_metrics.h | 20 +++++++++++++ base/process/process_metrics_linux.cc | 42 +++++++++++++++++++++++++++ base/process/process_metrics_mac.cc | 33 +++++++++++++++++++++ base/process/process_metrics_test.cc | 25 ++++++++++++++++ base/process/process_metrics_win.cc | 27 +++++++++++++++++ 7 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 base/process/process_metrics.h create mode 100644 base/process/process_metrics_linux.cc create mode 100644 base/process/process_metrics_mac.cc create mode 100644 base/process/process_metrics_test.cc create mode 100644 base/process/process_metrics_win.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 67fd9de3..6389cffb 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..38fcd26d 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..86b5ecb2 --- /dev/null +++ b/base/process/process_metrics.h @@ -0,0 +1,20 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. +#pragma once + +#include + +#include + +namespace base { + +struct ProcessMemoryUsage { + std::size_t resident_set_bytes = 0; + std::size_t peak_resident_set_bytes = 0; +}; + +// Queries physical memory currently resident for this process. The peak may be +// zero when the platform cannot provide it. Clears |usage| on failure. +BASE_EXPORT bool QueryCurrentProcessMemoryUsage(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..69dfb420 --- /dev/null +++ b/base/process/process_metrics_linux.cc @@ -0,0 +1,42 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include "base/process/process_metrics.h" + +#include +#include + +#include + +namespace base { + +bool QueryCurrentProcessMemoryUsage(ProcessMemoryUsage* usage) { + if (!usage) + return false; + *usage = {}; + + unsigned long resident_pages = 0; + std::FILE* statm = std::fopen("/proc/self/statm", "r"); + if (!statm) + return false; + const int fields = std::fscanf(statm, "%*s %lu", &resident_pages); + std::fclose(statm); + if (fields != 1) + return false; + + const long page_size = sysconf(_SC_PAGESIZE); + if (page_size <= 0) + return false; + usage->resident_set_bytes = + static_cast(resident_pages) * static_cast(page_size); + + rusage resources{}; + if (getrusage(RUSAGE_SELF, &resources) == 0 && resources.ru_maxrss > 0) { + // Linux and Android report ru_maxrss in KiB. + usage->peak_resident_set_bytes = + static_cast(resources.ru_maxrss) * 1024u; + } + return true; +} + +} // namespace base diff --git a/base/process/process_metrics_mac.cc b/base/process/process_metrics_mac.cc new file mode 100644 index 00000000..4f66ea92 --- /dev/null +++ b/base/process/process_metrics_mac.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 { + +bool QueryCurrentProcessMemoryUsage(ProcessMemoryUsage* usage) { + if (!usage) + return false; + *usage = {}; + + 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(mach_task_self(), 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); + + rusage resources{}; + if (getrusage(RUSAGE_SELF, &resources) == 0 && resources.ru_maxrss > 0) { + // macOS reports ru_maxrss in bytes. + usage->peak_resident_set_bytes = static_cast(resources.ru_maxrss); + } + 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..4dd86d53 --- /dev/null +++ b/base/process/process_metrics_test.cc @@ -0,0 +1,25 @@ +// 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 { +namespace { + +TEST(ProcessMetricsTest, ReportsCurrentProcessResidentSet) { + ProcessMemoryUsage usage; + ASSERT_TRUE(QueryCurrentProcessMemoryUsage(&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, RejectsNullOutput) { + EXPECT_FALSE(QueryCurrentProcessMemoryUsage(nullptr)); +} + +} // 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..7dd6ad0e --- /dev/null +++ b/base/process/process_metrics_win.cc @@ -0,0 +1,27 @@ +// 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 { + +bool QueryCurrentProcessMemoryUsage(ProcessMemoryUsage* usage) { + if (!usage) + return false; + *usage = {}; + + PROCESS_MEMORY_COUNTERS counters{}; + counters.cb = static_cast(sizeof(counters)); + if (!GetProcessMemoryInfo(GetCurrentProcess(), &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 From 17661c0f9f28547a25e4f886f0e6cb57e8ab2c1b Mon Sep 17 00:00:00 2001 From: Vincent Hengel Date: Wed, 15 Jul 2026 11:01:35 +0200 Subject: [PATCH 2/3] base: query memory for arbitrary processes --- base/process/process_metrics.h | 28 +++++++++++---- base/process/process_metrics_linux.cc | 49 ++++++++++++++------------- base/process/process_metrics_mac.cc | 28 +++++++-------- base/process/process_metrics_test.cc | 34 +++++++++++++++++-- base/process/process_metrics_win.cc | 20 +++++++---- build/build_config.lua | 5 ++- 6 files changed, 108 insertions(+), 56 deletions(-) diff --git a/base/process/process_metrics.h b/base/process/process_metrics.h index 86b5ecb2..e3dc0846 100644 --- a/base/process/process_metrics.h +++ b/base/process/process_metrics.h @@ -2,19 +2,33 @@ // For licensing information see LICENSE at the root of this distribution. #pragma once -#include - +#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 { - std::size_t resident_set_bytes = 0; - std::size_t peak_resident_set_bytes = 0; + mem_size resident_set_bytes = 0; + mem_size peak_resident_set_bytes = 0; }; -// Queries physical memory currently resident for this process. The peak may be -// zero when the platform cannot provide it. Clears |usage| on failure. -BASE_EXPORT bool QueryCurrentProcessMemoryUsage(ProcessMemoryUsage* usage); +// 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 index 69dfb420..37985f90 100644 --- a/base/process/process_metrics_linux.cc +++ b/base/process/process_metrics_linux.cc @@ -3,40 +3,43 @@ #include "base/process/process_metrics.h" -#include #include #include namespace base { -bool QueryCurrentProcessMemoryUsage(ProcessMemoryUsage* usage) { - if (!usage) - return false; - *usage = {}; +ProcessHandle GetCurrentProcessHandle() { + return static_cast(getpid()); +} - unsigned long resident_pages = 0; - std::FILE* statm = std::fopen("/proc/self/statm", "r"); - if (!statm) - return false; - const int fields = std::fscanf(statm, "%*s %lu", &resident_pages); - std::fclose(statm); - if (fields != 1) +bool QueryProcessMemoryUsage(ProcessHandle process, ProcessMemoryUsage& usage) { + usage = {}; + if (process <= 0) return false; - const long page_size = sysconf(_SC_PAGESIZE); - if (page_size <= 0) + char path[64]; + std::snprintf(path, sizeof(path), "/proc/%d/status", process); + std::FILE* status = std::fopen(path, "r"); + if (!status) return false; - usage->resident_set_bytes = - static_cast(resident_pages) * static_cast(page_size); - - rusage resources{}; - if (getrusage(RUSAGE_SELF, &resources) == 0 && resources.ru_maxrss > 0) { - // Linux and Android report ru_maxrss in KiB. - usage->peak_resident_set_bytes = - static_cast(resources.ru_maxrss) * 1024u; + + 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; + } } - return true; + 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 index 4f66ea92..d2510fee 100644 --- a/base/process/process_metrics_mac.cc +++ b/base/process/process_metrics_mac.cc @@ -4,29 +4,27 @@ #include "base/process/process_metrics.h" #include -#include - namespace base { -bool QueryCurrentProcessMemoryUsage(ProcessMemoryUsage* usage) { - if (!usage) +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; - *usage = {}; 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(mach_task_self(), MACH_TASK_BASIC_INFO, - reinterpret_cast(&task_memory), &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); - - rusage resources{}; - if (getrusage(RUSAGE_SELF, &resources) == 0 && resources.ru_maxrss > 0) { - // macOS reports ru_maxrss in bytes. - usage->peak_resident_set_bytes = static_cast(resources.ru_maxrss); - } + usage.resident_set_bytes = static_cast(task_memory.resident_size); + usage.peak_resident_set_bytes = static_cast(task_memory.resident_size_max); return true; } diff --git a/base/process/process_metrics_test.cc b/base/process/process_metrics_test.cc index 4dd86d53..a08d9f01 100644 --- a/base/process/process_metrics_test.cc +++ b/base/process/process_metrics_test.cc @@ -3,6 +3,12 @@ #include "base/process/process_metrics.h" +#if defined(OS_LINUX) +#include +#include +#include +#endif + #include namespace base { @@ -10,16 +16,38 @@ namespace { TEST(ProcessMetricsTest, ReportsCurrentProcessResidentSet) { ProcessMemoryUsage usage; - ASSERT_TRUE(QueryCurrentProcessMemoryUsage(&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, RejectsNullOutput) { - EXPECT_FALSE(QueryCurrentProcessMemoryUsage(nullptr)); +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 index 7dd6ad0e..e02a7860 100644 --- a/base/process/process_metrics_win.cc +++ b/base/process/process_metrics_win.cc @@ -8,19 +8,25 @@ namespace base { -bool QueryCurrentProcessMemoryUsage(ProcessMemoryUsage* usage) { - if (!usage) +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; - *usage = {}; PROCESS_MEMORY_COUNTERS counters{}; counters.cb = static_cast(sizeof(counters)); - if (!GetProcessMemoryInfo(GetCurrentProcess(), &counters, - 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); + usage.resident_set_bytes = static_cast(counters.WorkingSetSize); + usage.peak_resident_set_bytes = static_cast(counters.PeakWorkingSetSize); return true; } 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 From b59fb00070ab8a2c3a9ea2c018dfbf904ace6242 Mon Sep 17 00:00:00 2001 From: Vincent Hengel Date: Wed, 15 Jul 2026 12:48:41 +0200 Subject: [PATCH 3/3] base: fix mingw psapi casing --- CMakeLists.txt | 2 +- base/premake5.lua | 2 +- base/process/process_metrics_win.cc | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6389cffb..7eb4b86c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +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) + 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 38fcd26d..ae051c84 100644 --- a/base/premake5.lua +++ b/base/premake5.lua @@ -20,7 +20,7 @@ local function base_project() filter("configurations:Profile") dependencies("tracysdk") filter("system:windows") - links("Psapi") + links("psapi") filter{} warnings("High") diff --git a/base/process/process_metrics_win.cc b/base/process/process_metrics_win.cc index e02a7860..5e2d63ea 100644 --- a/base/process/process_metrics_win.cc +++ b/base/process/process_metrics_win.cc @@ -3,8 +3,8 @@ #include "base/process/process_metrics.h" -#include -#include +#include +#include namespace base {