aboutsummaryrefslogtreecommitdiff
path: root/src/zencore
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-03-23 19:22:08 +0100
committerGitHub Enterprise <[email protected]>2026-03-23 19:22:08 +0100
commit440ef03df8d8bba4432126f36168c1f7631c18dc (patch)
tree07d4bd4446a11589c9a842255bf37c25aaded74b /src/zencore
parentMerge branch 'de/v5.7.25-hotpatch' (#880) (diff)
downloadzen-440ef03df8d8bba4432126f36168c1f7631c18dc.tar.xz
zen-440ef03df8d8bba4432126f36168c1f7631c18dc.zip
Cross-platform process metrics support (#887)
- **Cross-platform `GetProcessMetrics`**: Implement Linux (`/proc/{pid}/stat`, `/proc/{pid}/statm`, `/proc/{pid}/status`) and macOS (`proc_pidinfo(PROC_PIDTASKINFO)`) support for CPU times and memory metrics. Fix Windows to populate the `MemoryBytes` field (was always 0). All platforms now set `MemoryBytes = WorkingSetSize`. - **`ProcessMetricsTracker`**: Experimental utility class (`zenutil`) that periodically samples resource usage for a set of tracked child processes. Supports both a dedicated background thread and an ASIO steady_timer mode. Computes delta-based CPU usage percentage across samples, with batched sampling (8 processes per tick) to limit per-cycle overhead. - **`ProcessHandle` documentation**: Add Doxygen comments to all public methods describing platform-specific behavior. - **Cleanup**: Remove unused `ZEN_RUN_TESTS` macro (inlined at its single call site in `zenserver/main.cpp`), remove dead `#if 0` thread-shutdown workaround block. - **Minor fixes**: Use `HttpClientAccessToken` constructor in hordeclient instead of setting private members directly. Log ASIO version at startup and include it in the server settings list.
Diffstat (limited to 'src/zencore')
-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
3 files changed, 220 insertions, 25 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"};