Skip to content

Commit 32ec9a1

Browse files
committed
seems to work
0 parents  commit 32ec9a1

4 files changed

Lines changed: 335 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
venv

README.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Thread CPU usage
2+
Allows you to calculate the CPU usage of processes and threads based on `psutil`.
3+
Allows for simple configuration changes on the CLI.
4+
Advanced usage should use post-processing or modify the code.
5+
6+
## Examples
7+
8+
### Help
9+
```
10+
$ python main.py -h
11+
usage: main.py [-h] [--interval INTERVAL] [-n LINES] [-p PID]
12+
[--sort-key SORT_KEY]
13+
14+
Get CPU percentages for a process and children. Emits a JSON line for each
15+
process. `jq` will be helpful for post processing.
16+
17+
optional arguments:
18+
-h, --help show this help message and exit
19+
--interval INTERVAL Time between timing measurements in seconds
20+
-n LINES, --lines LINES
21+
Number of lines to emit
22+
-p PID, --pid PID PID of target process
23+
--sort-key SORT_KEY Sort by this key inside the "cpu_times" field
24+
```
25+
26+
### Introspection
27+
```
28+
$ python main.py -n 1 | jq keys
29+
[
30+
"cmdline",
31+
"cpu_times",
32+
"create_time",
33+
"exe",
34+
"name",
35+
"num_threads",
36+
"pid",
37+
"ppid",
38+
"threads"
39+
]
40+
```
41+
```
42+
$ python main.py -n 1 | jq '.cpu_times | keys'
43+
[
44+
"children_cpu_pct",
45+
"children_delta",
46+
"children_system_delta",
47+
"children_user_delta",
48+
"io_delta",
49+
"process_cpu_pct",
50+
"process_delta",
51+
"system_delta",
52+
"total_cpu_delta",
53+
"total_cpu_pct",
54+
"user_delta"
55+
]
56+
```
57+
```
58+
$ python main.py -n 1 | jq '.threads[0] | keys'
59+
[
60+
"system_time_delta",
61+
"tid",
62+
"total_delta",
63+
"total_pct",
64+
"user_time_delta"
65+
]
66+
```
67+
68+
### Calculate the CPU percentage used by the top 5 processes
69+
```
70+
$ python main.py -n 5 | jq '.cpu_times.total_cpu_pct' | jq -s '. | add'
71+
13.922
72+
```
73+
74+
### Filter all results with no cpu usage
75+
```
76+
$ python main.py --pid $TARGET_PID | jq -c 'select(.cpu_times.total_cpu_delta > 0) | {name: .name, pct: .cpu_times.total_cpu_pct, pid: .pid, ppid: .ppid}'
77+
{"name":"Web Content","pct":9.322,"pid":8420,"ppid":14547}
78+
{"name":"firefox-bin","pct":1.332,"pid":14547,"ppid":1}
79+
{"name":"Web Content","pct":0.121,"pid":12595,"ppid":14547}
80+
```
81+
82+
### Get thread info
83+
```
84+
$ python main.py --pid $TARGET_PID | jq -c '
85+
select(.cpu_times.total_cpu_delta > 0)
86+
| {
87+
name: .name,
88+
pct: .cpu_times.total_cpu_pct,
89+
pid: .pid,
90+
ppid: .ppid,
91+
threads: [
92+
.threads[]
93+
| select(.total_delta > 0)
94+
| {tid: .tid, pct: .total_pct}
95+
]
96+
}'
97+
```
98+
```
99+
{"name":"Web Content","pct":10.169,"pid":8420,"ppid":14547,"threads":[{"tid":28958,"pct":0.242},{"tid":8420,"pct":8.596},{"tid":8426,"pct":0.847},{"tid":8431,"pct":0.121},{"tid":8433,"pct":0.121},{"tid":8435,"pct":0.484},{"tid":8443,"pct":0.121}]}
100+
{"name":"firefox-bin","pct":3.511,"pid":14547,"ppid":1,"threads":[{"tid":14547,"pct":0.969},{"tid":14552,"pct":0.121},{"tid":14577,"pct":1.332},{"tid":14585,"pct":0.121},{"tid":14658,"pct":0.121},{"tid":14696,"pct":0.847},{"tid":4703,"pct":0.121}]}
101+
{"name":"WebExtensions","pct":0.121,"pid":14701,"ppid":14547,"threads":[{"tid":14701,"pct":0.121}]}
102+
{"name":"Web Content","pct":0.121,"pid":27924,"ppid":14547,"threads":[{"tid":27924,"pct":0.121}]}
103+
```
104+
105+
## Setup
106+
107+
```
108+
python3 -m venv venv
109+
source venv/bin/activate
110+
pip install -r requirements.txt
111+
```
112+
113+
## Useful tools
114+
115+
Browser based load generator
116+
1. [Doom](https://playclassic.games/games/first-person-shooter-dos-games-online/play-doom-online/play)
117+
118+
119+
Post-Processing
120+
1. [jq](https://stedolan.github.io/jq/)
121+
2. [jq play](https://jqplay.org/)
122+
123+
124+
## Similar things
125+
```
126+
ps -Lo ppid,pid,tid,pcpu,cmd --pid $TARGET_PID
127+
```
128+
129+
```
130+
top -b n1 -H -p $TARGET_PID | sed 1,6d
131+
```

main.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
from decimal import Decimal
2+
from json import encoder
3+
from time import sleep
4+
import decimal
5+
import simplejson as json
6+
import psutil
7+
8+
attrs = [
9+
'pid',
10+
'ppid',
11+
'name',
12+
'cpu_times',
13+
'num_threads',
14+
'threads',
15+
'cmdline',
16+
'create_time',
17+
'exe',
18+
]
19+
# EXAMPLE OUTPUT
20+
# {
21+
# "pid": 816,
22+
# "threads": [
23+
# {
24+
# "id": 816,
25+
# "user_time": 0.02,
26+
# "system_time": 0.01
27+
# },
28+
# {
29+
# "id": 823,
30+
# "user_time": 2.54,
31+
# "system_time": 3.04
32+
# },
33+
# {
34+
# "id": 864,
35+
# "user_time": 0.02,
36+
# "system_time": 0.0
37+
# }
38+
# ],
39+
# "cpu_times": {
40+
# "user": 2.59,
41+
# "system": 3.05,
42+
# "children_user": 0.0,
43+
# "children_system": 0.0,
44+
# "iowait": 0.0
45+
# },
46+
# "cmdline": [
47+
# "/usr/lib/accountsservice/accounts-daemon"
48+
# ],
49+
# "num_threads": 3,
50+
# "ppid": 1,
51+
# "exe": "/usr/lib/accountsservice/accounts-daemon",
52+
# "name": "accounts-daemon",
53+
# "create_time": 1581216030.43
54+
# }
55+
56+
57+
def floatfmt(f, precision=3):
58+
""" Creates a normalized Decimal with the given precision from a number.
59+
Useful for serializing floating point numbers with simplejson with a given precision.
60+
"""
61+
q = Decimal(10) ** -precision
62+
return Decimal(f).quantize(q).normalize()
63+
64+
def collect_processes(pid=None):
65+
""" Gets a list of processes
66+
If pid is None or 0 then it will return all processes
67+
"""
68+
if pid:
69+
p = psutil.Process(pid)
70+
result = {p.pid: p}
71+
result.update({c.pid: c for c in p.children(recursive=True)})
72+
else:
73+
result = {p.pid: p for p in psutil.process_iter(attrs=attrs)}
74+
return result
75+
76+
def main(pid=None, interval=1, lines=None, sort_key='total_cpu_pct'):
77+
children = collect_processes(pid)
78+
79+
# START OF CRITICAL SECTION
80+
# This begins the critical section until the second set of timing data is collected
81+
# Too much processing inside the critical section will skew CPU usage percentage results
82+
83+
# get cpu times
84+
cpu_time1 = psutil.cpu_times(percpu=False)
85+
86+
# track data in dictionaries keyed by PID
87+
data = {}
88+
garbage = []
89+
for pid, child in children.items():
90+
try:
91+
data[pid] = child.as_dict(attrs=attrs)
92+
except psutil.NoSuchProcess:
93+
garbage.append(pid)
94+
for pid in garbage:
95+
del data[pid]
96+
97+
# sleep for a bit so the CPU actually does things
98+
sleep(interval)
99+
100+
# second timing data is used to calculate deltas/percentages
101+
cpu_time2 = psutil.cpu_times(percpu=False)
102+
for pid, child in children.items():
103+
value = data[pid]
104+
try:
105+
value.update({
106+
'cpu_times2': child.cpu_times(),
107+
'threads2': child.threads(),
108+
})
109+
except psutil.NoSuchProcess:
110+
# Just give terminated processes duplicate timings.
111+
# They can be filtered out by sorting later
112+
value.update({
113+
'cpu_times2': value['cpu_times'] or None,
114+
'threads2': value['threads'] or None,
115+
})
116+
# END OF CRITICAL SECTION
117+
118+
# post processing to calculate cpu percentages
119+
cpu_delta = sum(cpu_time2) - sum(cpu_time1)
120+
for tid, item in data.items():
121+
# calculate cpu times of process and children
122+
t1 = item.pop('cpu_times')
123+
t2 = item.pop('cpu_times2')
124+
p_user_delta = t2.user - t1.user
125+
p_system_delta = t2.system - t1.system
126+
c_user_delta = t2.children_user - t1.children_user
127+
c_system_delta = t2.children_system - t1.children_system
128+
io_delta = t2.iowait - t1.iowait
129+
cpu_times = {
130+
"user_delta": floatfmt(p_user_delta),
131+
"system_delta": floatfmt(p_system_delta),
132+
"process_delta": floatfmt(p_user_delta + p_system_delta),
133+
"children_user_delta": floatfmt(c_user_delta),
134+
"children_system_delta": floatfmt(c_system_delta),
135+
"children_delta": floatfmt(c_user_delta + c_system_delta),
136+
"process_cpu_pct": floatfmt(100.0 * (p_user_delta + p_system_delta) / cpu_delta),
137+
"children_cpu_pct": floatfmt(100.0 * (c_user_delta + c_system_delta) / cpu_delta),
138+
"total_cpu_delta": floatfmt((p_user_delta + p_system_delta + c_user_delta + c_system_delta)),
139+
"total_cpu_pct": floatfmt(100.0*(p_user_delta + p_system_delta + c_user_delta + c_system_delta) / cpu_delta),
140+
"io_delta": floatfmt(io_delta),
141+
}
142+
# calculate thread cpu percentages
143+
threads1 = {t.id: t for t in item.pop('threads')}
144+
threads2 = {t.id: t for t in item.pop('threads2')}
145+
thread_deltas = {}
146+
for tid, t1 in threads1.items():
147+
t2 = threads2.get(tid)
148+
if t2:
149+
user_delta = t2.user_time - t1.user_time
150+
system_delta = t2.system_time - t1.system_time
151+
thread_deltas[tid] = {
152+
'tid': tid,
153+
'user_time_delta': floatfmt(user_delta),
154+
'system_time_delta': floatfmt(system_delta),
155+
'total_delta': floatfmt(user_delta+system_delta),
156+
'total_pct': floatfmt(100.0 * (user_delta + system_delta) / cpu_delta),
157+
}
158+
item.update({
159+
'cpu_times': cpu_times,
160+
'threads': list(thread_deltas.values()),
161+
})
162+
163+
data = list(data.values())
164+
data = sorted(data, key=lambda x: x['cpu_times'][sort_key], reverse=True)
165+
166+
if lines:
167+
data = data[:lines]
168+
169+
try:
170+
for item in data:
171+
print(json.dumps(item, sort_keys=True))
172+
except BrokenPipeError:
173+
pass # ignore error caused by piping to head
174+
175+
def get_default_args(func):
176+
""" Helper for argparse to pass default values to main
177+
"""
178+
import inspect
179+
signature = inspect.signature(func)
180+
return {
181+
k: v.default
182+
for k, v in signature.parameters.items()
183+
if v.default is not inspect.Parameter.empty
184+
}
185+
186+
if __name__ == '__main__':
187+
import argparse
188+
default = get_default_args(main)
189+
parser = argparse.ArgumentParser(
190+
description='Get CPU percentages for a process and children. Emits a JSON line for each process. `jq` will be helpful for post processing.')
191+
parser.add_argument('--interval', type=float, dest='interval', default=default.get('interval'),
192+
help='Time between timing measurements in seconds')
193+
parser.add_argument('-n','--lines', type=int, dest='lines', default=default.get('lines'),
194+
help='Number of lines to emit')
195+
parser.add_argument('-p', '--pid', type=int, dest='pid', default=default.get('pid'),
196+
help='PID of target process')
197+
parser.add_argument('--sort-key', type=str, dest='sort_key', default=default.get('sort_key'),
198+
help='Sort by this key inside the "cpu_times" field'
199+
)
200+
args = parser.parse_args()
201+
main(**vars(args))

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
psutil==5.6.7
2+
simplejson==3.17.0

0 commit comments

Comments
 (0)