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