aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/zencore/include/zencore/process.h86
-rw-r--r--src/zencore/include/zencore/testing.h7
-rw-r--r--src/zencore/process.cpp152
-rw-r--r--src/zenhorde/hordeclient.cpp5
-rw-r--r--src/zenserver/hub/hydration.cpp6
-rw-r--r--src/zenserver/main.cpp24
-rw-r--r--src/zenserver/zenserver.cpp2
-rw-r--r--src/zenutil/include/zenutil/processmetricstracker.h105
-rw-r--r--src/zenutil/processmetricstracker.cpp392
-rw-r--r--src/zenutil/zenutil.cpp2
10 files changed, 728 insertions, 53 deletions
diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h
index 75fd7b25a..d115bf11f 100644
--- a/src/zencore/include/zencore/process.h
+++ b/src/zencore/include/zencore/process.h
@@ -16,7 +16,13 @@ namespace zen {
class JobObject;
#endif
-/** Basic process abstraction
+/** Non-copyable handle to an OS process.
+ *
+ * On Windows, wraps a HANDLE opened with PROCESS_QUERY_INFORMATION | SYNCHRONIZE.
+ * On Linux/macOS, stores the pid directly (no kernel handle).
+ *
+ * Must be Initialize()'d before use. The destructor releases the underlying
+ * OS handle (Windows) or reaps a zombie child if it has already exited (POSIX).
*/
class ProcessHandle
{
@@ -28,19 +34,71 @@ public:
~ProcessHandle();
- void Initialize(int Pid);
- void Initialize(int Pid, std::error_code& OutEc);
- void Initialize(void* ProcessHandle); /// Initialize with an existing handle - takes ownership of the handle
- [[nodiscard]] bool IsRunning() const;
- [[nodiscard]] bool IsValid() const;
- bool Wait(int TimeoutMs = -1);
- bool Wait(int TimeoutMs, std::error_code& OutEc);
- int WaitExitCode();
- int GetExitCode();
- bool Kill();
- bool Terminate(int ExitCode);
- void Reset();
- [[nodiscard]] inline int Pid() const { return m_Pid; }
+ /// Open a handle to the process identified by @p Pid.
+ /// On Windows this calls OpenProcess(); on POSIX it simply stores the pid.
+ /// Throws std::system_error on failure.
+ void Initialize(int Pid);
+
+ /// Same as Initialize(int) but reports errors via @p OutEc instead of throwing.
+ void Initialize(int Pid, std::error_code& OutEc);
+
+ /// Initialize from an existing native process handle. Takes ownership —
+ /// the caller must not close the handle afterwards. Windows only.
+ void Initialize(void* ProcessHandle);
+
+ /// Returns true if the process is still alive.
+ /// On Windows, queries the exit code (STILL_ACTIVE check).
+ /// On POSIX, probes via kill(pid, 0) or equivalent.
+ [[nodiscard]] bool IsRunning() const;
+
+ /// Returns true if the handle has been successfully initialized.
+ [[nodiscard]] bool IsValid() const;
+
+ /// Block until the process exits or @p TimeoutMs elapses (-1 = infinite).
+ /// Returns true if the process exited, false on timeout.
+ /// Throws std::system_error on OS-level failure.
+ bool Wait(int TimeoutMs = -1);
+
+ /// Same as Wait(int) but reports errors via @p OutEc instead of throwing.
+ bool Wait(int TimeoutMs, std::error_code& OutEc);
+
+ /// Block until the process exits (indefinite wait), then return its exit code.
+ int WaitExitCode();
+
+ /// Return the process exit code. The process must have already exited
+ /// (asserts on Windows if still active). On POSIX the exit code is
+ /// captured during Wait().
+ int GetExitCode();
+
+ /// Attempt a graceful shutdown, falling back to a forced kill.
+ ///
+ /// On Windows: sends CTRL_BREAK_EVENT and waits up to 5 seconds; if the
+ /// process is still alive, calls TerminateProcess().
+ /// On POSIX: sends SIGTERM and waits up to 5 seconds; if the process is
+ /// still alive, sends SIGKILL.
+ ///
+ /// Calls Reset() before returning. Returns true on success.
+ bool Kill();
+
+ /// Immediately and unconditionally terminate the process.
+ ///
+ /// On Windows: calls TerminateProcess() with the given @p ExitCode and
+ /// waits for the process to fully exit.
+ /// On POSIX: sends SIGKILL (ExitCode is ignored) and waits up to 5 seconds
+ /// for the child to be reaped.
+ ///
+ /// Unlike Kill(), this does not attempt a graceful shutdown first.
+ /// Calls Reset() before returning. Returns true on success.
+ bool Terminate(int ExitCode);
+
+ /// Release the OS handle (Windows) or reap a zombie child (POSIX).
+ /// After this call, IsValid() returns false.
+ void Reset();
+
+ /// Return the process id.
+ [[nodiscard]] inline int Pid() const { return m_Pid; }
+
+ /// Return the native OS handle. HANDLE on Windows, pid cast to void* on POSIX.
[[nodiscard]] inline void* Handle() const { return m_ProcessHandle; }
private:
diff --git a/src/zencore/include/zencore/testing.h b/src/zencore/include/zencore/testing.h
index 01356fa00..6b37cd6da 100644
--- a/src/zencore/include/zencore/testing.h
+++ b/src/zencore/include/zencore/testing.h
@@ -52,13 +52,6 @@ private:
std::unique_ptr<Impl> m_Impl;
};
-# define ZEN_RUN_TESTS(argC, argV) \
- [&] { \
- zen::testing::TestRunner Runner; \
- Runner.ApplyCommandLine(argC, argV); \
- return Runner.Run(); \
- }()
-
int RunTestMain(int Argc, char* Argv[], const char* ExecutableName, void (*ForceLink)());
} // namespace zen::testing
diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp
index 47289a37b..dcb8b2422 100644
--- a/src/zencore/process.cpp
+++ b/src/zencore/process.cpp
@@ -1839,11 +1839,137 @@ GetProcessMetrics(const ProcessHandle& Handle, ProcessMetrics& OutMetrics)
OutMetrics.PeakWorkingSetSize = MemCounters.PeakWorkingSetSize;
OutMetrics.PagefileUsage = MemCounters.PagefileUsage;
OutMetrics.PeakPagefileUsage = MemCounters.PeakPagefileUsage;
+ OutMetrics.MemoryBytes = MemCounters.WorkingSetSize;
}
-#else
- // TODO: implement for Linux and Mac
- ZEN_UNUSED(Handle);
- ZEN_UNUSED(OutMetrics);
+#elif ZEN_PLATFORM_LINUX
+
+ const pid_t Pid = static_cast<pid_t>(Handle.Pid());
+
+ // Read CPU times from /proc/{pid}/stat
+ {
+ char Path[64];
+ snprintf(Path, sizeof(Path), "/proc/%d/stat", static_cast<int>(Pid));
+
+ char Buf[256];
+ int Fd = open(Path, O_RDONLY);
+ if (Fd >= 0)
+ {
+ ssize_t Len = read(Fd, Buf, sizeof(Buf) - 1);
+ close(Fd);
+
+ if (Len > 0)
+ {
+ Buf[Len] = '\0';
+
+ // Skip past "pid (name) " — find last ')' to handle names containing spaces or parens
+ const char* P = strrchr(Buf, ')');
+ if (P)
+ {
+ P += 2; // skip ') '
+
+ // Fields after (name): 0:state 1:ppid ... 11:utime 12:stime
+ unsigned long UTime = 0;
+ unsigned long STime = 0;
+ if (sscanf(P, "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lu %lu", &UTime, &STime) == 2)
+ {
+ static const long ClkTck = std::max(sysconf(_SC_CLK_TCK), 1L);
+ OutMetrics.KernelTimeMs = STime * 1000 / ClkTck;
+ OutMetrics.UserTimeMs = UTime * 1000 / ClkTck;
+ }
+ }
+ }
+ }
+ }
+
+ // Read memory metrics from /proc/{pid}/statm (values in pages)
+ {
+ char Path[64];
+ snprintf(Path, sizeof(Path), "/proc/%d/statm", static_cast<int>(Pid));
+
+ char Buf[128];
+ int Fd = open(Path, O_RDONLY);
+ if (Fd >= 0)
+ {
+ ssize_t Len = read(Fd, Buf, sizeof(Buf) - 1);
+ close(Fd);
+
+ if (Len > 0)
+ {
+ Buf[Len] = '\0';
+
+ // Fields: size resident shared text lib data dt
+ unsigned long VmSize = 0;
+ unsigned long Resident = 0;
+ if (sscanf(Buf, "%lu %lu", &VmSize, &Resident) == 2)
+ {
+ static const long PageSize = sysconf(_SC_PAGESIZE);
+ OutMetrics.WorkingSetSize = Resident * PageSize;
+ OutMetrics.PagefileUsage = VmSize * PageSize;
+ }
+ }
+ }
+ }
+
+ // Read peak RSS from /proc/{pid}/status (VmHWM line)
+ {
+ char Path[64];
+ snprintf(Path, sizeof(Path), "/proc/%d/status", static_cast<int>(Pid));
+
+ char Buf[2048];
+ int Fd = open(Path, O_RDONLY);
+ if (Fd >= 0)
+ {
+ ssize_t Len = read(Fd, Buf, sizeof(Buf) - 1);
+ close(Fd);
+
+ if (Len > 0)
+ {
+ Buf[Len] = '\0';
+
+ const char* VmHWM = strstr(Buf, "VmHWM:");
+ if (VmHWM)
+ {
+ unsigned long PeakRssKb = 0;
+ if (sscanf(VmHWM + 6, "%lu", &PeakRssKb) == 1)
+ {
+ OutMetrics.PeakWorkingSetSize = PeakRssKb * 1024;
+ }
+ }
+
+ const char* VmPeak = strstr(Buf, "VmPeak:");
+ if (VmPeak)
+ {
+ unsigned long PeakVmKb = 0;
+ if (sscanf(VmPeak + 7, "%lu", &PeakVmKb) == 1)
+ {
+ OutMetrics.PeakPagefileUsage = PeakVmKb * 1024;
+ }
+ }
+ }
+ }
+ }
+
+ OutMetrics.MemoryBytes = OutMetrics.WorkingSetSize;
+
+#elif ZEN_PLATFORM_MAC
+
+ const pid_t Pid = static_cast<pid_t>(Handle.Pid());
+
+ struct proc_taskinfo Info;
+ if (proc_pidinfo(Pid, PROC_PIDTASKINFO, 0, &Info, sizeof(Info)) > 0)
+ {
+ // pti_total_user and pti_total_system are in nanoseconds
+ OutMetrics.UserTimeMs = Info.pti_total_user / 1'000'000;
+ OutMetrics.KernelTimeMs = Info.pti_total_system / 1'000'000;
+
+ OutMetrics.WorkingSetSize = Info.pti_resident_size;
+ OutMetrics.PeakWorkingSetSize = Info.pti_resident_size; // macOS doesn't track peak RSS directly
+ OutMetrics.PagefileUsage = Info.pti_virtual_size;
+ OutMetrics.PeakPagefileUsage = Info.pti_virtual_size;
+ }
+
+ OutMetrics.MemoryBytes = OutMetrics.WorkingSetSize;
+
#endif
}
@@ -1885,6 +2011,24 @@ TEST_CASE("FindProcess")
}
}
+TEST_CASE("GetProcessMetrics")
+{
+ ProcessHandle Handle;
+ Handle.Initialize(GetCurrentProcessId());
+ REQUIRE(Handle.IsValid());
+
+ ProcessMetrics Metrics;
+ GetProcessMetrics(Handle, Metrics);
+
+ // The current process should have non-zero memory usage
+ CHECK(Metrics.WorkingSetSize > 0);
+ CHECK(Metrics.MemoryBytes > 0);
+ CHECK(Metrics.MemoryBytes == Metrics.WorkingSetSize);
+
+ // CPU time should be non-zero for a running test process
+ CHECK((Metrics.UserTimeMs + Metrics.KernelTimeMs) > 0);
+}
+
TEST_CASE("BuildArgV")
{
const char* Words[] = {"one", "two", "three", "four", "five"};
diff --git a/src/zenhorde/hordeclient.cpp b/src/zenhorde/hordeclient.cpp
index fb981f0ba..0eefc57c6 100644
--- a/src/zenhorde/hordeclient.cpp
+++ b/src/zenhorde/hordeclient.cpp
@@ -35,10 +35,7 @@ HordeClient::Initialize()
if (!m_Config.AuthToken.empty())
{
Settings.AccessTokenProvider = [token = m_Config.AuthToken]() -> HttpClientAccessToken {
- HttpClientAccessToken Token;
- Token.Value = token;
- Token.ExpireTime = HttpClientAccessToken::Clock::now() + std::chrono::hours{24};
- return Token;
+ return HttpClientAccessToken(token, HttpClientAccessToken::Clock::now() + std::chrono::hours{24});
};
}
diff --git a/src/zenserver/hub/hydration.cpp b/src/zenserver/hub/hydration.cpp
index 4a44da052..541127590 100644
--- a/src/zenserver/hub/hydration.cpp
+++ b/src/zenserver/hub/hydration.cpp
@@ -916,9 +916,9 @@ TEST_CASE("hydration.s3.current_state_json_selects_latest_folder")
Hydrator->Dehydrate();
}
- // ServerStateDir is now empty. Wait briefly so the v2 timestamp folder name is strictly later
- // (timestamp resolution is 1 ms).
- Sleep(2);
+ // ServerStateDir is now empty. Wait so the v2 timestamp folder name is strictly later
+ // (timestamp resolution is 1 ms, but macOS scheduler granularity requires a larger margin).
+ Sleep(100);
// v2: dehydrate WITH a marker file that only v2 has
CreateTestTree(ServerStateDir);
diff --git a/src/zenserver/main.cpp b/src/zenserver/main.cpp
index dff162b1c..00b7a67d7 100644
--- a/src/zenserver/main.cpp
+++ b/src/zenserver/main.cpp
@@ -249,7 +249,9 @@ test_main(int argc, char** argv)
zen::MaximizeOpenFileCount();
- return ZEN_RUN_TESTS(argc, argv);
+ zen::testing::TestRunner Runner;
+ Runner.ApplyCommandLine(argc, argv);
+ return Runner.Run();
}
#endif
@@ -267,26 +269,6 @@ main(int argc, char* argv[])
using namespace zen;
using namespace std::literals;
- // note: doctest has locally (in thirdparty) been fixed to not cause shutdown
- // crashes due to TLS destructors
- //
- // mimalloc on the other hand might still be causing issues, in which case
- // we should work out either how to eliminate the mimalloc dependency or how
- // to configure it in a way that doesn't cause shutdown issues
-
-#if 0
- auto _ = zen::MakeGuard([] {
- // Allow some time for worker threads to unravel, in an effort
- // to prevent shutdown races in TLS object destruction, mainly due to
- // threads which we don't directly control (Windows thread pool) and
- // therefore can't join.
- //
- // This isn't a great solution, but for now it seems to help reduce
- // shutdown crashes observed in some situations.
- WaitForThreads(1000);
- });
-#endif
-
enum
{
kHub,
diff --git a/src/zenserver/zenserver.cpp b/src/zenserver/zenserver.cpp
index de770156a..6aa02eb87 100644
--- a/src/zenserver/zenserver.cpp
+++ b/src/zenserver/zenserver.cpp
@@ -489,6 +489,7 @@ ZenServerBase::BuildSettingsList(const ZenServerConfig& ServerConfig)
{"BasePort"sv, fmt::to_string(ServerConfig.BasePort)},
{"CoreLimit"sv, fmt::to_string(ServerConfig.CoreLimit)},
{"MemoryAllocator"sv, std::string(GMalloc->GetName())},
+ {"AsioVersion"sv, fmt::format("{}.{}.{}", ASIO_VERSION / 100000, (ASIO_VERSION / 100) % 1000, ASIO_VERSION % 100)},
{"IsDebug"sv, fmt::to_string(ServerConfig.IsDebug)},
{"IsCleanStart"sv, fmt::to_string(ServerConfig.IsCleanStart)},
{"IsTest"sv, fmt::to_string(ServerConfig.IsTest)},
@@ -771,6 +772,7 @@ ZenServerMain::Run()
ZEN_INFO(ZEN_APP_NAME " - starting on port {}, version '{}'", m_ServerOptions.BasePort, ZEN_CFG_VERSION_BUILD_STRING_FULL);
ZEN_INFO(ZEN_APP_NAME " - memory allocator: {}", GMalloc->GetName());
+ ZEN_INFO(ZEN_APP_NAME " - asio: {}.{}.{}", ASIO_VERSION / 100000, (ASIO_VERSION / 100) % 1000, ASIO_VERSION % 100);
Entry = ServerState.Register(m_ServerOptions.BasePort);
diff --git a/src/zenutil/include/zenutil/processmetricstracker.h b/src/zenutil/include/zenutil/processmetricstracker.h
new file mode 100644
index 000000000..fdeae2bfa
--- /dev/null
+++ b/src/zenutil/include/zenutil/processmetricstracker.h
@@ -0,0 +1,105 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/process.h>
+#include <zencore/zencore.h>
+
+#include <memory>
+#include <vector>
+
+namespace asio {
+class io_context;
+}
+
+namespace zen {
+
+/** Tracked process entry with latest metrics snapshot.
+ */
+struct TrackedProcessEntry
+{
+ int Pid = 0;
+ ProcessMetrics Metrics;
+
+ // Derived CPU usage percentage (delta-based, requires two samples).
+ // -1.0 means not yet sampled.
+ float CpuUsagePercent = -1.0f;
+};
+
+/** Aggregate metrics across all tracked processes.
+ */
+struct AggregateProcessMetrics
+{
+ uint64_t TotalWorkingSetSize = 0;
+ uint64_t TotalPeakWorkingSetSize = 0;
+ uint64_t TotalUserTimeMs = 0;
+ uint64_t TotalKernelTimeMs = 0;
+ uint32_t ProcessCount = 0;
+};
+
+/** Background process metrics tracker.
+ *
+ * Maintains a set of child processes keyed by pid and periodically samples
+ * their resource usage (CPU times, memory) in a background thread or via
+ * an ASIO timer on an external io_context.
+ *
+ * The tracker does not take ownership of process handles. On Windows it
+ * duplicates the handle internally; on other platforms it uses the pid
+ * directly.
+ *
+ * Usage (dedicated thread):
+ * ProcessMetricsTracker Tracker;
+ * Tracker.Start();
+ * Tracker.Add(ChildHandle);
+ *
+ * Usage (ASIO timer):
+ * ProcessMetricsTracker Tracker(IoContext);
+ * Tracker.Start();
+ * Tracker.Add(ChildHandle);
+ */
+class ProcessMetricsTracker
+{
+public:
+ /// Construct with a dedicated background thread for sampling.
+ explicit ProcessMetricsTracker(uint64_t SampleIntervalMs = 5000);
+
+ /// Construct with an external io_context — uses an asio::steady_timer
+ /// instead of a dedicated thread. The caller must ensure the io_context
+ /// outlives this tracker and that its run loop is active.
+ ProcessMetricsTracker(asio::io_context& IoContext, uint64_t SampleIntervalMs = 5000);
+
+ ~ProcessMetricsTracker();
+
+ ProcessMetricsTracker(const ProcessMetricsTracker&) = delete;
+ ProcessMetricsTracker& operator=(const ProcessMetricsTracker&) = delete;
+
+ /// Start sampling. Spawns the background thread or enqueues the first timer.
+ void Start();
+
+ /// Stop sampling. Safe to call multiple times.
+ void Stop();
+
+ /// Add a process to track. Internally clones the handle (Windows) or
+ /// copies the pid (Linux/macOS). If the pid is already tracked, replaces it.
+ void Add(const ProcessHandle& Handle);
+
+ /// Remove a tracked process by pid.
+ void Remove(int Pid);
+
+ /// Remove all tracked processes.
+ void Clear();
+
+ /// Returns a snapshot of metrics for all tracked processes.
+ std::vector<TrackedProcessEntry> GetSnapshot() const;
+
+ /// Returns aggregate metrics across all tracked processes.
+ AggregateProcessMetrics GetAggregate() const;
+
+private:
+ struct Impl;
+ std::unique_ptr<Impl> m_Impl;
+};
+
+void processmetricstracker_forcelink(); // internal
+
+} // namespace zen
diff --git a/src/zenutil/processmetricstracker.cpp b/src/zenutil/processmetricstracker.cpp
new file mode 100644
index 000000000..555d0ae1a
--- /dev/null
+++ b/src/zenutil/processmetricstracker.cpp
@@ -0,0 +1,392 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zenutil/processmetricstracker.h>
+
+#include <zencore/thread.h>
+#include <zencore/timer.h>
+
+#include <algorithm>
+#include <thread>
+#include <unordered_map>
+#include <vector>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <asio/io_context.hpp>
+#include <asio/steady_timer.hpp>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen {
+
+struct ProcessMetricsTracker::Impl
+{
+ static constexpr size_t kBatchSize = 8;
+
+ struct Entry
+ {
+ ProcessHandle Handle;
+ ProcessMetrics LastMetrics;
+ float CpuUsagePercent = -1.0f;
+
+ uint64_t PrevUserTimeMs = 0;
+ uint64_t PrevKernelTimeMs = 0;
+ uint64_t PrevSampleTicks = 0;
+ };
+
+ uint64_t m_SampleIntervalMs;
+
+ mutable RwLock m_Lock;
+ std::unordered_map<int, Entry> m_Entries;
+ size_t m_NextSampleIndex = 0;
+ std::vector<int> m_KeyOrder;
+
+ std::atomic<bool> m_Running{false};
+
+ // Thread-based sampling
+ std::thread m_Thread;
+ Event m_StopEvent;
+
+ // Timer-based sampling
+ std::unique_ptr<asio::steady_timer> m_Timer;
+
+ explicit Impl(uint64_t SampleIntervalMs) : m_SampleIntervalMs(SampleIntervalMs) {}
+
+ Impl(asio::io_context& IoContext, uint64_t SampleIntervalMs)
+ : m_SampleIntervalMs(SampleIntervalMs)
+ , m_Timer(std::make_unique<asio::steady_timer>(IoContext))
+ {
+ }
+
+ ~Impl() { Stop(); }
+
+ void Start()
+ {
+ if (m_Running.exchange(true))
+ {
+ return;
+ }
+
+ if (m_Timer)
+ {
+ EnqueueTimer();
+ }
+ else
+ {
+ m_Thread = std::thread([this] { SamplingLoop(); });
+ }
+ }
+
+ void Stop()
+ {
+ if (!m_Running.exchange(false))
+ {
+ return;
+ }
+
+ if (m_Timer)
+ {
+ m_Timer->cancel();
+ }
+
+ if (m_Thread.joinable())
+ {
+ m_StopEvent.Set();
+ m_Thread.join();
+ }
+ }
+
+ void Add(const ProcessHandle& Handle)
+ {
+ int Pid = Handle.Pid();
+
+ RwLock::ExclusiveLockScope $(m_Lock);
+
+ auto It = m_Entries.find(Pid);
+ if (It != m_Entries.end())
+ {
+ m_Entries.erase(It);
+ }
+ else
+ {
+ m_KeyOrder.push_back(Pid);
+ }
+
+ auto [NewIt, Inserted] = m_Entries.try_emplace(Pid);
+ NewIt->second.Handle.Initialize(Pid);
+ }
+
+ void Remove(int Pid)
+ {
+ RwLock::ExclusiveLockScope $(m_Lock);
+ m_Entries.erase(Pid);
+ m_KeyOrder.erase(std::remove(m_KeyOrder.begin(), m_KeyOrder.end(), Pid), m_KeyOrder.end());
+
+ if (m_NextSampleIndex >= m_KeyOrder.size())
+ {
+ m_NextSampleIndex = 0;
+ }
+ }
+
+ void Clear()
+ {
+ RwLock::ExclusiveLockScope $(m_Lock);
+ m_Entries.clear();
+ m_KeyOrder.clear();
+ m_NextSampleIndex = 0;
+ }
+
+ std::vector<TrackedProcessEntry> GetSnapshot() const
+ {
+ std::vector<TrackedProcessEntry> Result;
+
+ RwLock::SharedLockScope $(m_Lock);
+ Result.reserve(m_Entries.size());
+
+ for (const auto& [Pid, E] : m_Entries)
+ {
+ TrackedProcessEntry Out;
+ Out.Pid = Pid;
+ Out.Metrics = E.LastMetrics;
+ Out.CpuUsagePercent = E.CpuUsagePercent;
+ Result.push_back(std::move(Out));
+ }
+
+ return Result;
+ }
+
+ AggregateProcessMetrics GetAggregate() const
+ {
+ AggregateProcessMetrics Agg;
+
+ RwLock::SharedLockScope $(m_Lock);
+
+ for (const auto& [Pid, E] : m_Entries)
+ {
+ Agg.TotalWorkingSetSize += E.LastMetrics.WorkingSetSize;
+ Agg.TotalPeakWorkingSetSize += E.LastMetrics.PeakWorkingSetSize;
+ Agg.TotalUserTimeMs += E.LastMetrics.UserTimeMs;
+ Agg.TotalKernelTimeMs += E.LastMetrics.KernelTimeMs;
+ Agg.ProcessCount++;
+ }
+
+ return Agg;
+ }
+
+ void SampleBatch()
+ {
+ RwLock::SharedLockScope $(m_Lock);
+
+ if (m_KeyOrder.empty())
+ {
+ return;
+ }
+
+ const uint64_t NowTicks = GetHifreqTimerValue();
+ size_t Remaining = std::min(kBatchSize, m_KeyOrder.size());
+
+ while (Remaining > 0)
+ {
+ if (m_NextSampleIndex >= m_KeyOrder.size())
+ {
+ m_NextSampleIndex = 0;
+ }
+
+ int Pid = m_KeyOrder[m_NextSampleIndex];
+ auto It = m_Entries.find(Pid);
+
+ if (It == m_Entries.end())
+ {
+ m_NextSampleIndex++;
+ Remaining--;
+ continue;
+ }
+
+ Entry& E = It->second;
+
+ ProcessMetrics Metrics;
+ GetProcessMetrics(E.Handle, Metrics);
+
+ if (E.PrevSampleTicks > 0)
+ {
+ uint64_t ElapsedMs = Stopwatch::GetElapsedTimeMs(NowTicks - E.PrevSampleTicks);
+ uint64_t DeltaCpuTimeMs = (Metrics.UserTimeMs + Metrics.KernelTimeMs) - (E.PrevUserTimeMs + E.PrevKernelTimeMs);
+ if (ElapsedMs > 0)
+ {
+ E.CpuUsagePercent = static_cast<float>(static_cast<double>(DeltaCpuTimeMs) / ElapsedMs * 100.0);
+ }
+ }
+
+ E.PrevUserTimeMs = Metrics.UserTimeMs;
+ E.PrevKernelTimeMs = Metrics.KernelTimeMs;
+ E.PrevSampleTicks = NowTicks;
+ E.LastMetrics = Metrics;
+
+ m_NextSampleIndex++;
+ Remaining--;
+ }
+ }
+
+ void SamplingLoop()
+ {
+ while (!m_StopEvent.Wait(static_cast<int>(m_SampleIntervalMs)))
+ {
+ if (!m_Running.load())
+ {
+ return;
+ }
+
+ SampleBatch();
+ }
+ }
+
+ void EnqueueTimer()
+ {
+ if (!m_Timer || !m_Running.load())
+ {
+ return;
+ }
+
+ m_Timer->expires_after(std::chrono::milliseconds(m_SampleIntervalMs));
+ m_Timer->async_wait([this](const asio::error_code& Ec) {
+ if (Ec || !m_Running.load())
+ {
+ return;
+ }
+
+ SampleBatch();
+ EnqueueTimer();
+ });
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////
+
+ProcessMetricsTracker::ProcessMetricsTracker(uint64_t SampleIntervalMs) : m_Impl(std::make_unique<Impl>(SampleIntervalMs))
+{
+}
+
+ProcessMetricsTracker::ProcessMetricsTracker(asio::io_context& IoContext, uint64_t SampleIntervalMs)
+: m_Impl(std::make_unique<Impl>(IoContext, SampleIntervalMs))
+{
+}
+
+ProcessMetricsTracker::~ProcessMetricsTracker() = default;
+
+void
+ProcessMetricsTracker::Start()
+{
+ m_Impl->Start();
+}
+
+void
+ProcessMetricsTracker::Stop()
+{
+ m_Impl->Stop();
+}
+
+void
+ProcessMetricsTracker::Add(const ProcessHandle& Handle)
+{
+ m_Impl->Add(Handle);
+}
+
+void
+ProcessMetricsTracker::Remove(int Pid)
+{
+ m_Impl->Remove(Pid);
+}
+
+void
+ProcessMetricsTracker::Clear()
+{
+ m_Impl->Clear();
+}
+
+std::vector<TrackedProcessEntry>
+ProcessMetricsTracker::GetSnapshot() const
+{
+ return m_Impl->GetSnapshot();
+}
+
+AggregateProcessMetrics
+ProcessMetricsTracker::GetAggregate() const
+{
+ return m_Impl->GetAggregate();
+}
+
+} // namespace zen
+
+#if ZEN_WITH_TESTS
+
+# include <zencore/testing.h>
+
+using namespace zen;
+
+void
+zen::processmetricstracker_forcelink()
+{
+}
+
+TEST_SUITE_BEGIN("util.processmetricstracker");
+
+TEST_CASE("ProcessMetricsTracker.SelfProcess")
+{
+ ProcessMetricsTracker Tracker(100);
+ Tracker.Start();
+
+ ProcessHandle Handle;
+ Handle.Initialize(zen::GetCurrentProcessId());
+ REQUIRE(Handle.IsValid());
+
+ int Pid = Handle.Pid();
+ Tracker.Add(Handle);
+
+ // Wait for at least two samples so CPU% is computed
+ std::this_thread::sleep_for(std::chrono::milliseconds(350));
+
+ auto Snapshot = Tracker.GetSnapshot();
+ REQUIRE(Snapshot.size() == 1);
+ CHECK(Snapshot[0].Pid == Pid);
+ CHECK(Snapshot[0].Metrics.WorkingSetSize > 0);
+ CHECK(Snapshot[0].Metrics.MemoryBytes > 0);
+ CHECK((Snapshot[0].Metrics.UserTimeMs + Snapshot[0].Metrics.KernelTimeMs) > 0);
+ CHECK(Snapshot[0].CpuUsagePercent >= 0.0f);
+
+ auto Agg = Tracker.GetAggregate();
+ CHECK(Agg.ProcessCount == 1);
+ CHECK(Agg.TotalWorkingSetSize > 0);
+
+ Tracker.Remove(Pid);
+
+ Snapshot = Tracker.GetSnapshot();
+ CHECK(Snapshot.empty());
+
+ Tracker.Stop();
+}
+
+TEST_CASE("ProcessMetricsTracker.AsioTimer")
+{
+ asio::io_context IoContext;
+
+ ProcessMetricsTracker Tracker(IoContext, 100);
+ Tracker.Start();
+
+ ProcessHandle Handle;
+ Handle.Initialize(zen::GetCurrentProcessId());
+ REQUIRE(Handle.IsValid());
+
+ Tracker.Add(Handle);
+
+ // Run the io_context for enough time to get two samples
+ IoContext.run_for(std::chrono::milliseconds(350));
+
+ auto Snapshot = Tracker.GetSnapshot();
+ REQUIRE(Snapshot.size() == 1);
+ CHECK(Snapshot[0].Metrics.WorkingSetSize > 0);
+ CHECK(Snapshot[0].CpuUsagePercent >= 0.0f);
+
+ Tracker.Stop();
+}
+
+TEST_SUITE_END();
+
+#endif
diff --git a/src/zenutil/zenutil.cpp b/src/zenutil/zenutil.cpp
index c4d01554d..032f21c9b 100644
--- a/src/zenutil/zenutil.cpp
+++ b/src/zenutil/zenutil.cpp
@@ -10,6 +10,7 @@
# include <zenutil/config/commandlineoptions.h>
# include <zenutil/rpcrecording.h>
# include <zenutil/splitconsole/logstreamlistener.h>
+# include <zenutil/processmetricstracker.h>
# include <zenutil/wildcard.h>
namespace zen {
@@ -21,6 +22,7 @@ zenutil_forcelinktests()
commandlineoptions_forcelink();
imdscredentials_forcelink();
logstreamlistener_forcelink();
+ processmetricstracker_forcelink();
s3client_forcelink();
sigv4_forcelink();
wildcard_forcelink();