diff options
Diffstat (limited to 'src')
| -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 | ||||
| -rw-r--r-- | src/zenhorde/hordeclient.cpp | 5 | ||||
| -rw-r--r-- | src/zenserver/hub/hydration.cpp | 6 | ||||
| -rw-r--r-- | src/zenserver/main.cpp | 24 | ||||
| -rw-r--r-- | src/zenserver/zenserver.cpp | 2 | ||||
| -rw-r--r-- | src/zenutil/include/zenutil/processmetricstracker.h | 105 | ||||
| -rw-r--r-- | src/zenutil/processmetricstracker.cpp | 392 | ||||
| -rw-r--r-- | src/zenutil/zenutil.cpp | 2 |
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(); |