Mads
Multi-Agent Distributed System
Loading...
Searching...
No Matches
pm.hpp
Go to the documentation of this file.
1#pragma once
2/*
3 Header-only C++20 process metrics by PID (default: current process).
4 Platforms: Windows, Linux, macOS.
5
6 CPU percentages are computed from deltas, therefore they are meaningful only
7 after at least two calls to sample().
8*/
9
10#include <cstdint>
11#include <chrono>
12#include <string>
13#include <fstream>
14#include <sstream>
15#include <optional>
16#include <thread>
17#include <mutex>
18
19#if defined(_WIN32)
20 #ifndef NOMINMAX
21 #define NOMINMAX
22 #endif
23 #include <windows.h>
24 #include <tlhelp32.h>
25 #include <psapi.h>
26 #pragma comment(lib, "psapi.lib")
27#elif defined(__linux__)
28 #include <unistd.h>
29#elif defined(__APPLE__)
30 #include <unistd.h>
31 #include <libproc.h>
32 #include <sys/proc_info.h>
33 #include <mach/mach.h>
34 #include <mach/mach_time.h>
35 #include <mach/host_info.h>
36 #include <mach/mach_host.h>
37#else
38 #error "Unsupported platform"
39#endif
40
41namespace mads {
42
55public:
57 using pid_type =
58#if defined(_WIN32)
59 DWORD;
60#else
61 pid_t;
62#endif
63
65 using clock = std::chrono::steady_clock;
66
72 explicit process_metrics(std::optional<pid_type> pid = std::nullopt)
73 : pid_(pid.value_or(current_pid())),
74 ncpu_(std::max(1u, std::thread::hardware_concurrency()))
75 {}
76
81 pid_type pid() const noexcept { return pid_; }
82
91 void sample() {
92 std::scoped_lock lk(m_);
93 auto proc_ns = process_cpu_time_ns_(pid_);
94 auto sys_ns = system_cpu_time_ns_();
95
96 if (proc_ns && sys_ns) {
97 if (last_) {
98 auto dproc = (*proc_ns >= last_->proc_ns) ? (*proc_ns - last_->proc_ns) : 0ULL;
99 auto dsys = (*sys_ns >= last_->sys_ns) ? (*sys_ns - last_->sys_ns) : 0ULL;
100
101 cpu_total_capacity_ =
102 (dsys > 0)
103 ? (static_cast<double>(dproc) * 100.0) / static_cast<double>(dsys)
104 : 0.0;
105 }
106 last_ = sample_state{ *proc_ns, *sys_ns };
107 } else {
108 cpu_total_capacity_ = 0.0;
109 last_.reset();
110 }
111 }
112
123 std::scoped_lock lk(m_);
124 return cpu_total_capacity_ < 0.0 ? 0.0 : cpu_total_capacity_;
125 }
126
135 double cpu_percent_per_core() const {
136 std::scoped_lock lk(m_);
137 return (cpu_total_capacity_ < 0.0 ? 0.0 : cpu_total_capacity_)
138 * static_cast<double>(ncpu_);
139 }
140
145 std::uint32_t thread_count() const {
146#if defined(_WIN32)
147 HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
148 if (snap == INVALID_HANDLE_VALUE) return 0;
149
150 THREADENTRY32 te{};
151 te.dwSize = sizeof(te);
152
153 std::uint32_t count = 0;
154 if (Thread32First(snap, &te)) {
155 do {
156 if (te.th32OwnerProcessID == pid_) ++count;
157 } while (Thread32Next(snap, &te));
158 }
159 CloseHandle(snap);
160 return count;
161#elif defined(__linux__)
162 std::ifstream f("/proc/" + std::to_string(pid_) + "/status");
163 if (!f) return 0;
164 std::string line;
165 while (std::getline(f, line)) {
166 if (line.rfind("Threads:", 0) == 0) {
167 std::istringstream iss(line.substr(8));
168 std::uint32_t n = 0;
169 iss >> n;
170 return n;
171 }
172 }
173 return 0;
174#elif defined(__APPLE__)
175 proc_taskinfo info{};
176 if (read_task_info_(pid_, &info)) {
177 return static_cast<std::uint32_t>(info.pti_threadnum);
178 }
179
180 mach_port_t task = task_for_pid_port_(pid_);
181 if (task == MACH_PORT_NULL) return 0;
182
183 thread_act_array_t thread_list = nullptr;
184 mach_msg_type_number_t thread_count = 0;
185 if (task_threads(task, &thread_list, &thread_count) != KERN_SUCCESS) {
186 return 0;
187 }
188
189 for (mach_msg_type_number_t i = 0; i < thread_count; ++i) {
190 mach_port_deallocate(mach_task_self(), thread_list[i]);
191 }
192 vm_deallocate(mach_task_self(), reinterpret_cast<vm_address_t>(thread_list),
193 static_cast<vm_size_t>(thread_count * sizeof(thread_act_t)));
194
195 return static_cast<std::uint32_t>(thread_count);
196#endif
197 }
198
208 std::uint64_t ram_bytes() const {
209#if defined(_WIN32)
210 HANDLE h = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
211 FALSE, pid_);
212 if (!h) return 0;
213
214 PROCESS_MEMORY_COUNTERS_EX pmc{};
215 std::uint64_t ws = 0;
216 if (GetProcessMemoryInfo(h,
217 reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&pmc), sizeof(pmc))) {
218 ws = static_cast<std::uint64_t>(pmc.WorkingSetSize);
219 }
220 CloseHandle(h);
221 return ws;
222#elif defined(__linux__)
223 std::ifstream f("/proc/" + std::to_string(pid_) + "/statm");
224 if (!f) return 0;
225
226 std::uint64_t size=0, resident=0;
227 f >> size >> resident;
228 long page = ::sysconf(_SC_PAGESIZE);
229 if (page <= 0) return 0;
230
231 return resident * static_cast<std::uint64_t>(page);
232#elif defined(__APPLE__)
233 proc_taskinfo proc_info{};
234 if (read_task_info_(pid_, &proc_info)) {
235 return static_cast<std::uint64_t>(proc_info.pti_resident_size);
236 }
237
238 mach_port_t task = task_for_pid_port_(pid_);
239 if (task == MACH_PORT_NULL) return 0;
240
241 task_basic_info_data_t mach_info{};
242 mach_msg_type_number_t count = TASK_BASIC_INFO_COUNT;
243 if (task_info(task, TASK_BASIC_INFO,
244 reinterpret_cast<task_info_t>(&mach_info), &count) != KERN_SUCCESS)
245 return 0;
246
247 return static_cast<std::uint64_t>(mach_info.resident_size);
248#endif
249 }
250
255 std::uint32_t cpu_count() const { return ncpu_; }
256
257private:
258 struct sample_state {
259 std::uint64_t proc_ns;
260 std::uint64_t sys_ns;
261 };
262
267 static pid_type current_pid() {
268#if defined(_WIN32)
269 return GetCurrentProcessId();
270#else
271 return ::getpid();
272#endif
273 }
274
275#if defined(__APPLE__)
276 static std::uint64_t mach_absolute_to_ns_(std::uint64_t ticks) {
277 mach_timebase_info_data_t timebase{};
278 if (mach_timebase_info(&timebase) != KERN_SUCCESS || timebase.denom == 0) {
279 return ticks;
280 }
281 const long double ns = static_cast<long double>(ticks) *
282 static_cast<long double>(timebase.numer) /
283 static_cast<long double>(timebase.denom);
284 return static_cast<std::uint64_t>(ns);
285 }
286
287 static bool read_task_info_(pid_type pid, proc_taskinfo* out_info) {
288 if (out_info == nullptr) {
289 return false;
290 }
291
292 proc_taskinfo info{};
293 const int bytes = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &info, PROC_PIDTASKINFO_SIZE);
294 if (bytes != PROC_PIDTASKINFO_SIZE) {
295 return false;
296 }
297
298 *out_info = info;
299 return true;
300 }
301
309 static mach_port_t task_for_pid_port_(pid_type pid) {
310 if (pid == current_pid())
311 return mach_task_self();
312
313 mach_port_t task = MACH_PORT_NULL;
314 if (task_for_pid(mach_task_self(), pid, &task) != KERN_SUCCESS)
315 return MACH_PORT_NULL;
316 return task;
317 }
318#endif
319
326 static std::optional<std::uint64_t> process_cpu_time_ns_(pid_type pid) {
327#if defined(_WIN32)
328 HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
329 if (!h) return std::nullopt;
330
331 FILETIME ft_create{}, ft_exit{}, ft_kernel{}, ft_user{};
332 if (!GetProcessTimes(h, &ft_create, &ft_exit, &ft_kernel, &ft_user)) {
333 CloseHandle(h);
334 return std::nullopt;
335 }
336
337 auto to100 = [](const FILETIME& ft) -> std::uint64_t {
338 ULARGE_INTEGER u{};
339 u.LowPart = ft.dwLowDateTime;
340 u.HighPart = ft.dwHighDateTime;
341 return static_cast<std::uint64_t>(u.QuadPart);
342 };
343
344 std::uint64_t total_ns = (to100(ft_kernel) + to100(ft_user)) * 100ULL; /* 100ns -> ns */
345 CloseHandle(h);
346 return total_ns;
347
348#elif defined(__linux__)
349 std::ifstream f("/proc/" + std::to_string(pid) + "/stat");
350 if (!f) return std::nullopt;
351
352 int dummy = 0;
353 std::string comm;
354 char state = 0;
355
356 f >> dummy >> comm >> state;
357 if (!f) return std::nullopt;
358
359 /* Skip fields 4..13 (10 fields), then read utime (14) and stime (15). */
360 for (int i = 0; i < 10; ++i) { std::string tmp; f >> tmp; if (!f) return std::nullopt; }
361
362 std::uint64_t ut = 0, st = 0;
363 f >> ut >> st;
364 if (!f) return std::nullopt;
365
366 long hz = ::sysconf(_SC_CLK_TCK);
367 if (hz <= 0) return std::nullopt;
368
369 long double sec = static_cast<long double>(ut + st) / static_cast<long double>(hz);
370 return static_cast<std::uint64_t>(sec * 1.0e9L);
371
372#elif defined(__APPLE__)
373 proc_taskinfo proc_info{};
374 if (read_task_info_(pid, &proc_info)) {
375 const std::uint64_t total = static_cast<std::uint64_t>(proc_info.pti_total_user + proc_info.pti_total_system);
376 return mach_absolute_to_ns_(total);
377 }
378
379 mach_port_t task = task_for_pid_port_(pid);
380 if (task == MACH_PORT_NULL) return std::nullopt;
381
382 task_thread_times_info_data_t mach_info{};
383 mach_msg_type_number_t count = TASK_THREAD_TIMES_INFO_COUNT;
384 if (task_info(task, TASK_THREAD_TIMES_INFO,
385 reinterpret_cast<task_info_t>(&mach_info), &count) != KERN_SUCCESS)
386 return std::nullopt;
387
388 auto tv2ns = [](time_value_t tv) -> std::uint64_t {
389 return static_cast<std::uint64_t>(tv.seconds) * 1000000000ULL +
390 static_cast<std::uint64_t>(tv.microseconds) * 1000ULL;
391 };
392
393 return tv2ns(mach_info.user_time) + tv2ns(mach_info.system_time);
394#endif
395 }
396
404 static std::optional<std::uint64_t> system_cpu_time_ns_() {
405#if defined(_WIN32)
406 FILETIME ft_idle{}, ft_kernel{}, ft_user{};
407 if (!GetSystemTimes(&ft_idle, &ft_kernel, &ft_user))
408 return std::nullopt;
409
410 auto to100 = [](const FILETIME& ft) -> std::uint64_t {
411 ULARGE_INTEGER u{};
412 u.LowPart = ft.dwLowDateTime;
413 u.HighPart = ft.dwHighDateTime;
414 return static_cast<std::uint64_t>(u.QuadPart);
415 };
416
417 return (to100(ft_kernel) + to100(ft_user)) * 100ULL; /* 100ns -> ns */
418
419#elif defined(__linux__)
420 std::ifstream f("/proc/stat");
421 if (!f) return std::nullopt;
422
423 std::string cpu;
424 f >> cpu;
425 if (!f || cpu != "cpu") return std::nullopt;
426
427 std::uint64_t u=0,n=0,s=0,i=0,iw=0,irq=0,si=0,st=0;
428 f >> u >> n >> s >> i >> iw >> irq >> si >> st;
429 if (!f) return std::nullopt;
430
431 long hz = ::sysconf(_SC_CLK_TCK);
432 if (hz <= 0) return std::nullopt;
433
434 long double sec = static_cast<long double>(u + n + s + i + iw + irq + si + st)
435 / static_cast<long double>(hz);
436 return static_cast<std::uint64_t>(sec * 1.0e9L);
437
438#elif defined(__APPLE__)
439 host_cpu_load_info_data_t info{};
440 mach_msg_type_number_t count = HOST_CPU_LOAD_INFO_COUNT;
441 if (host_statistics64(mach_host_self(), HOST_CPU_LOAD_INFO,
442 reinterpret_cast<host_info64_t>(&info), &count) != KERN_SUCCESS)
443 return std::nullopt;
444
445 std::uint64_t ticks =
446 static_cast<std::uint64_t>(info.cpu_ticks[CPU_STATE_USER]) +
447 static_cast<std::uint64_t>(info.cpu_ticks[CPU_STATE_SYSTEM]) +
448 static_cast<std::uint64_t>(info.cpu_ticks[CPU_STATE_IDLE]) +
449 static_cast<std::uint64_t>(info.cpu_ticks[CPU_STATE_NICE]);
450
451 long hz = ::sysconf(_SC_CLK_TCK);
452 if (hz <= 0) hz = 100; /* fallback */
453
454 long double sec = static_cast<long double>(ticks) / static_cast<long double>(hz);
455 return static_cast<std::uint64_t>(sec * 1.0e9L);
456#endif
457 }
458
459private:
460 pid_type pid_;
461 std::uint32_t ncpu_;
462 mutable std::mutex m_;
463 std::optional<sample_state> last_;
464 double cpu_total_capacity_ = 0.0;
465};
466
467} // namespace mads
Read-only process resource metrics for a given PID.
Definition pm.hpp:54
std::uint32_t cpu_count() const
Get number of logical CPUs detected on the system.
Definition pm.hpp:255
pid_t pid_type
PID type used by the platform.
Definition pm.hpp:61
std::chrono::steady_clock clock
Steady clock type used internally.
Definition pm.hpp:65
double cpu_percent_total_capacity() const
CPU usage as percentage of total machine capacity.
Definition pm.hpp:122
process_metrics(std::optional< pid_type > pid=std::nullopt)
Construct metrics for a target process.
Definition pm.hpp:72
pid_type pid() const noexcept
Get the PID associated with this instance.
Definition pm.hpp:81
double cpu_percent_per_core() const
CPU usage scaled per logical core.
Definition pm.hpp:135
std::uint64_t ram_bytes() const
Get resident memory usage (RSS / working set) in bytes.
Definition pm.hpp:208
void sample()
Take a new CPU usage sample for the target PID.
Definition pm.hpp:91
std::uint32_t thread_count() const
Get instantaneous number of threads in the target process.
Definition pm.hpp:145
Definition pm.hpp:41