diff options
| author | Stefan Boberg <[email protected]> | 2026-03-23 19:22:08 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-23 19:22:08 +0100 |
| commit | 440ef03df8d8bba4432126f36168c1f7631c18dc (patch) | |
| tree | 07d4bd4446a11589c9a842255bf37c25aaded74b /src/zencore | |
| parent | Merge branch 'de/v5.7.25-hotpatch' (#880) (diff) | |
| download | zen-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.h | 86 | ||||
| -rw-r--r-- | src/zencore/include/zencore/testing.h | 7 | ||||
| -rw-r--r-- | src/zencore/process.cpp | 152 |
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"}; |