-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathcpu_monitor.py
More file actions
209 lines (178 loc) · 8.36 KB
/
Copy pathcpu_monitor.py
File metadata and controls
209 lines (178 loc) · 8.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""CPU monitoring for performance tests."""
import logging
import psutil
import threading
import time
from typing import Dict
class CPUMonitor:
"""Monitor CPU usage during performance tests with per-thread tracking and CPU affinity."""
def __init__(self, server_type: str = "auto", enabled: bool = True):
"""Initialize CPU monitor with server type detection.
Args:
server_type: Type of server to monitor ("auto", "valkey", "redis", or custom process name)
enabled: Whether monitoring is enabled
"""
self.enabled = enabled
if not enabled:
return
# Resolve server type to target process name(s)
if server_type == "auto":
# Auto-detect: search for both valkey and redis
self.target_processes = ["valkey-server", "redis-server"]
elif server_type == "valkey":
self.target_processes = ["valkey-server"]
elif server_type == "redis":
self.target_processes = ["redis-server"]
else:
# Custom process name
self.target_processes = [server_type]
self.monitoring = False
self.monitor_thread = None
self.thread_cpu_samples = {} # {tid: [cpu_samples]}
self.thread_names = {} # {tid: name}
self.thread_cores = {} # {tid: most_recent_core}
self.thread_migrations = {} # {tid: migration_count}
self.peak_memory = 0
self.process = None
self.initial_thread_times = {} # {tid: (user_time, system_time)}
def start_monitoring(self, test_id: str) -> None:
"""Start CPU monitoring for a test."""
if not self.enabled:
return
try:
# Find target process
for proc in psutil.process_iter(["pid", "name", "cmdline"]):
try:
cmdline = " ".join(proc.info["cmdline"] or [])
if any(term in cmdline for term in self.target_processes):
self.process = psutil.Process(proc.info["pid"])
matched_term = [
t for t in self.target_processes if t in cmdline
][0]
logging.info(
f"Found server process: PID={proc.info['pid']}, matched '{matched_term}'"
)
break
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
if not self.process:
logging.warning(
f"Could not find server process (searched for: {', '.join(self.target_processes)})"
)
return
# Initialize tracking
self.thread_cpu_samples = {}
self.thread_names = {}
self.thread_cores = {}
self.thread_migrations = {}
self.peak_memory = 0
self.initial_thread_times = {}
self.monitoring = True
self.monitor_thread = threading.Thread(
target=self._monitor_loop, daemon=True
)
self.monitor_thread.start()
logging.info(
f"Started CPU monitoring for {test_id} (PID: {self.process.pid})"
)
except Exception as e:
logging.warning(f"Failed to start CPU monitoring: {e}")
def stop_monitoring(self, test_id: str) -> Dict:
"""Stop CPU monitoring and return essential statistics."""
if not self.enabled:
return {}
self.monitoring = False
if self.monitor_thread:
self.monitor_thread.join(timeout=2)
if not self.thread_cpu_samples:
logging.warning(f"No CPU samples collected for {test_id}")
return {}
# Build simplified thread stats
threads = {}
for tid, samples in self.thread_cpu_samples.items():
if samples:
thread_name = self.thread_names.get(tid, f"thread_{tid}")
threads[thread_name] = {
"avg_percent": round(sum(samples) / len(samples), 2),
"primary_cpu": self.thread_cores.get(tid),
"migrations": self.thread_migrations.get(tid, 0),
}
stats = {
"threads": threads,
"memory_max_mb": round(self.peak_memory / (1024 * 1024), 2),
}
logging.info(
f"Monitored {len(threads)} threads, peak memory: {stats['memory_max_mb']}MB"
)
for name, data in threads.items():
cpu_info = f"{data['avg_percent']}%"
if data["primary_cpu"] is not None:
cpu_info += f" (cpu{data['primary_cpu']})"
if data.get("migrations", 0) > 0:
cpu_info += f", {data['migrations']} migrations"
logging.info(f" {name}: {cpu_info}")
return stats
def _monitor_loop(self) -> None:
"""Monitoring loop - track CPU per thread and peak memory."""
try:
import os
while self.monitoring:
try:
# Track peak memory
mem_bytes = self.process.memory_info().rss
if mem_bytes > self.peak_memory:
self.peak_memory = mem_bytes
# Get per-thread CPU
threads = self.process.threads()
for thread in threads:
tid = thread.id
# Initialize new threads
if tid not in self.thread_cpu_samples:
self.thread_cpu_samples[tid] = []
self.thread_migrations[tid] = 0
self.initial_thread_times[tid] = (
thread.user_time,
thread.system_time,
)
try:
comm_path = f"/proc/{self.process.pid}/task/{tid}/comm"
if os.path.exists(comm_path):
with open(comm_path, "r") as f:
self.thread_names[tid] = f.read().strip()
except (IOError, OSError, PermissionError):
self.thread_names[tid] = f"thread_{tid}"
# Track core migrations
try:
stat_path = f"/proc/{self.process.pid}/task/{tid}/stat"
if os.path.exists(stat_path):
with open(stat_path, "r") as f:
stat_data = f.read().split()
if len(stat_data) > 38:
current_core = int(stat_data[38])
if (
tid in self.thread_cores
and self.thread_cores[tid] != current_core
):
self.thread_migrations[tid] = (
self.thread_migrations.get(tid, 0) + 1
)
self.thread_cores[tid] = current_core
except (IOError, OSError, ValueError, IndexError):
pass
# Calculate CPU % over 1 second interval
if tid in self.initial_thread_times:
prev_user, prev_sys = self.initial_thread_times[tid]
delta_user = thread.user_time - prev_user
delta_sys = thread.system_time - prev_sys
thread_cpu = (delta_user + delta_sys) * 100
self.thread_cpu_samples[tid].append(thread_cpu)
self.initial_thread_times[tid] = (
thread.user_time,
thread.system_time,
)
time.sleep(1)
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
logging.warning(f"Process monitoring error: {e}")
break
except Exception as e:
logging.warning(f"CPU monitoring loop failed: {e}")