14
14
import typing
15
15
from collections import defaultdict , namedtuple
16
16
from contextlib import contextmanager
17
+ from pathlib import Path
17
18
from typing import Dict
18
19
19
20
import psutil
29
30
FLUSH_CMD = 'screen -S {session} -X colon "logfile flush 0^M"'
30
31
CommandReturn = namedtuple ("CommandReturn" , "returncode stdout stderr" )
31
32
CMDLOG = logging .getLogger ("commands" )
32
- GET_CPU_LOAD = "top -bn1 -H -p {} -w512 | tail -n+8"
33
33
34
34
35
35
def get_threads (pid : int ) -> dict :
@@ -56,30 +56,43 @@ def set_cpu_affinity(pid: int, cpulist: list) -> list:
56
56
return psutil .Process (pid ).cpu_affinity (real_cpulist )
57
57
58
58
59
- def get_cpu_utilization (pid : int ) -> Dict [ str , float ] :
60
- """Return current process per thread CPU utilization ."""
61
- _ , stdout , _ = check_output ( GET_CPU_LOAD . format ( pid ) )
62
- cpu_utilization = {}
59
+ def get_thread_name (pid : int , tid : int ) -> str :
60
+ """Return thread name from pid and tid pair ."""
61
+ return Path ( "/proc" , str ( pid ), "task" , str ( tid ), "comm" ). read_text ( "utf-8" ). strip ( )
62
+
63
63
64
- # Take all except the last line
65
- lines = stdout .strip ().split (sep = "\n " )
66
- for line in lines :
67
- # sometimes the firecracker process will have gone away, in which case top does not return anything
68
- if not line :
69
- continue
64
+ CpuTimes = namedtuple ("CpuTimes" , ["user" , "system" ])
70
65
71
- info = line .strip ().split ()
72
- # We need at least CPU utilization and threads names cols (which
73
- # might be two cols e.g `fc_vcpu 0`).
74
- info_len = len (info )
75
- assert info_len > 11 , line
76
66
77
- cpu_percent = float (info [8 ])
67
+ def get_cpu_times (pid : int ) -> Dict [str , CpuTimes ]:
68
+ """Return a dict mapping thread name to CPU usage (in seconds) since start."""
69
+ cpu_times = {}
70
+ for thread in psutil .Process (pid ).threads ():
71
+ thread_name = get_thread_name (pid , thread .id )
72
+ cpu_times [thread_name ] = CpuTimes (thread .user_time , thread .system_time )
73
+ return cpu_times
78
74
79
- # Handles `fc_vcpu 0` case as well.
80
- thread_name = info [11 ] + (" " + info [12 ] if info_len > 12 else "" )
81
- cpu_utilization [thread_name ] = cpu_percent
82
75
76
+ def get_cpu_utilization (
77
+ pid : int ,
78
+ interval : int = 1 ,
79
+ split_user_system : bool = False ,
80
+ ) -> Dict [str , float | CpuTimes ]:
81
+ """Return current process per thread CPU utilization over the interval (seconds)."""
82
+ cpu_utilization = {}
83
+ cpu_times_before = get_cpu_times (pid )
84
+ time .sleep (interval )
85
+ cpu_times_after = get_cpu_times (pid )
86
+ threads = set (cpu_times_before .keys ()) & set (cpu_times_after .keys ())
87
+ for thread_name in threads :
88
+ before = cpu_times_before [thread_name ]
89
+ after = cpu_times_after [thread_name ]
90
+ user = (after .user - before .user ) / interval * 100
91
+ system = (after .system - before .system ) / interval * 100
92
+ if split_user_system :
93
+ cpu_utilization [thread_name ] = CpuTimes (user , system )
94
+ else :
95
+ cpu_utilization [thread_name ] = user + system
83
96
return cpu_utilization
84
97
85
98
@@ -94,18 +107,13 @@ def track_cpu_utilization(
94
107
# Sleep first `omit` secconds
95
108
time .sleep (omit )
96
109
97
- cpu_utilization = {}
110
+ cpu_utilization = defaultdict ( list )
98
111
for _ in range (iterations ):
99
112
current_cpu_utilization = get_cpu_utilization (pid )
100
113
assert len (current_cpu_utilization ) > 0
101
114
102
115
for thread_name , value in current_cpu_utilization .items ():
103
- if not cpu_utilization .get (thread_name ):
104
- cpu_utilization [thread_name ] = []
105
116
cpu_utilization [thread_name ].append (value )
106
-
107
- # 1 second granularity
108
- time .sleep (1 )
109
117
return cpu_utilization
110
118
111
119
0 commit comments