diff options
| author | Stefan Boberg <[email protected]> | 2026-03-24 15:47:23 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-24 15:47:23 +0100 |
| commit | 21c2abb1bde697c31bee562465cb986a0429a299 (patch) | |
| tree | 3734d235e79a8fbed307ae5c248936d356553b61 /src/zenutil/processmetricstracker.cpp | |
| parent | v5.7.25 hotpatch (#874) (diff) | |
| download | zen-21c2abb1bde697c31bee562465cb986a0429a299.tar.xz zen-21c2abb1bde697c31bee562465cb986a0429a299.zip | |
Subprocess Manager (#889)
Adds a `SubprocessManager` for managing child processes with ASIO-integrated async exit detection, stdout/stderr pipe capture, and periodic metrics sampling. Also introduces `ProcessGroup` for OS-backed process grouping (Windows JobObjects / POSIX process groups).
### SubprocessManager
- Async process exit detection using platform-native mechanisms (Windows `object_handle`, Linux `pidfd_open`, macOS `kqueue EVFILT_PROC`) — no polling
- Stdout/stderr capture via async pipe readers with per-process or default callbacks
- Periodic round-robin metrics sampling (CPU, memory) across managed processes
- Spawn, adopt, remove, kill, and enumerate managed processes
### ProcessGroup
- OS-level process grouping: Windows JobObject (kill-on-close guarantee), POSIX `setpgid` (bulk signal delivery)
- Atomic group kill via `TerminateJobObject` (Windows) or `kill(-pgid, sig)` (POSIX)
- Per-group aggregate metrics and enumeration
### ProcessHandle improvements
- Added explicit constructors from `int` (pid) and `void*` (native handle)
- Added move constructor and move assignment operator
### ProcessMetricsTracker
- Cross-platform process metrics (CPU time, working set, page faults) via `QueryProcessMetrics()`
- ASIO timer-driven periodic sampling with configurable interval and batch size
- Aggregate metrics across tracked processes
### Other changes
- Fixed `zentest-appstub` writing a spurious `Versions` file to cwd on every invocation
Diffstat (limited to 'src/zenutil/processmetricstracker.cpp')
| -rw-r--r-- | src/zenutil/processmetricstracker.cpp | 392 |
1 files changed, 0 insertions, 392 deletions
diff --git a/src/zenutil/processmetricstracker.cpp b/src/zenutil/processmetricstracker.cpp deleted file mode 100644 index 555d0ae1a..000000000 --- a/src/zenutil/processmetricstracker.cpp +++ /dev/null @@ -1,392 +0,0 @@ -// 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 |