@@ -7,6 +7,9 @@ Usage: topotop [options]
7
7
Options:
8
8
-h, --help print help.
9
9
-d, --delay SECONDS delay between updates. The default is "1.0".
10
+ -l, --largest-share show largest share of each busy CPU that a
11
+ single process got. The top dot stands for
12
+ at least 99 % CPU. Requires "bpftrace".
10
13
-m, --memory include memory bandwidth, requires "pcm-memory".
11
14
-u, --upi include UPI traffic, requires "pcm".
12
15
"""
@@ -145,6 +148,57 @@ class Bitmap:
145
148
braille [y4 ].append (color + brbits2chr (braille_char_bits ) + nocolor )
146
149
return '\n ' .join ('' .join (c for c in line ) for line in braille )
147
150
151
+ _bpftrace_cpu_pid_process = None
152
+ def cpu_pid_stats ():
153
+ global _bpftrace_cpu_pid_process
154
+ if _bpftrace_cpu_pid_process is None :
155
+ bpfprog = (
156
+ "tracepoint:sched:sched_stat_runtime{ if (@active[0]==0) { @run0[cpu,args->pid]+=args->runtime; } else { @run1[cpu,args->pid]+=args->runtime; } }" + # @run: {(cpu, pid): runtime}
157
+ "interval:ms:%d{ @active[0]=1-@active[0]; if (@active[0]==1) { print(@run0); clear(@run0); } else { print(@run1); clear(@run1); } print(\" ---\" ); }" % (int (opt_delay * 1000 ),)
158
+ )
159
+ _bpftrace_cpu_pid_process = subprocess .Popen (
160
+ ["bpftrace" , "-e" , bpfprog ],
161
+ bufsize = 0 ,
162
+ universal_newlines = True ,
163
+ stdout = subprocess .PIPE )
164
+ # @MAP0[cpu,pid]: count
165
+ stat_cpu_pid = {
166
+ "run" : {},
167
+ }
168
+ while True :
169
+ line = _bpftrace_cpu_pid_process .stdout .readline ().strip ()
170
+ if line == "" :
171
+ raise Exception ("bpftrace exited" )
172
+ if line == "---" :
173
+ break
174
+ if line .startswith ("@" ):
175
+ stat = line [1 :4 ]
176
+ cpu_pid_value = stat_cpu_pid [stat ]
177
+ cpu_pid , count = line .split (":" )
178
+ # cpu_pid: "@XXX0[cpu, pid]"
179
+ cpu = int (cpu_pid .split ("," )[0 ][6 :]) # cut "@XXX0["
180
+ pid = int (cpu_pid .split ("," )[1 ][:- 1 ]) # cut "]"
181
+ if not cpu in cpu_pid_value :
182
+ cpu_pid_value [cpu ] = {}
183
+ cpu_pid_value [cpu ][pid ] = int (count .strip ())
184
+ return stat_cpu_pid
185
+
186
+ def cpu_lshare (stat_cpu_pid , cpu_usage ):
187
+ """Return largest share (lshare) that the most running process got from a CPU"""
188
+ cpu_lshare = {}
189
+ for cpu , pid_runtime in stat_cpu_pid ["run" ].items ():
190
+ # show largest share only on CPUs with at least 25 % usage
191
+ if cpu_usage .get (cpu , 0 ) < 0.25 :
192
+ cpu_lshare [cpu ] = 0
193
+ continue
194
+ max_time = max (pid_runtime .values ())
195
+ sum_time = sum (pid_runtime .values ())
196
+ if sum_time == 0 :
197
+ cpu_lshare [cpu ] = 0
198
+ continue
199
+ cpu_lshare [cpu ] = max_time / sum_time
200
+ return cpu_lshare
201
+
148
202
_cpu_load_total , _cpu_load_idle = {}, {}
149
203
def cpu_usage_from_proc ():
150
204
"""Read CPU usage statistics from /proc/stat"""
@@ -439,12 +493,11 @@ def print_sysmbw_usage(data):
439
493
bm .set_line (- 1 , write_bar )
440
494
print (f"sysR+W { rgbps + wgbps :6.1f} GB/s{ bm .to_braille ()} { _max_sys_bw :6.1f} GB/s" )
441
495
442
- def print_cpu_load2 (pkg , cpus , cpu_usage ):
496
+ def print_cpu_load2 (pkg , cpus , cpu_usage , cpu_ls ):
443
497
try :
444
498
width = os .get_terminal_size ().columns
445
499
except :
446
500
width = 72
447
-
448
501
height_pts = 8 # resolution in braille points
449
502
lines = []
450
503
prev_pkg = None
@@ -453,6 +506,7 @@ def print_cpu_load2(pkg, cpus, cpu_usage):
453
506
line1 = []
454
507
line2 = []
455
508
usage = []
509
+ lshare = [] # largest share that any process got on a CPU
456
510
for cpu in cpus :
457
511
if cpu [0 ] != node or cpu [1 ] != pkg :
458
512
continue
@@ -466,12 +520,29 @@ def print_cpu_load2(pkg, cpus, cpu_usage):
466
520
line2 .append (f"coreid" )
467
521
vcpu = cpu [- 1 ]
468
522
usage .append (round (height_pts * cpu_usage [vcpu ]))
523
+ if cpu_ls :
524
+ # 99 % CPU usage is shown as 100 % in the braille,
525
+ # anything lower than that lacks the last braille dot.
526
+ lshare .append (int (height_pts * min (1.0 , .01 + cpu_ls .get (vcpu , 0 ))))
469
527
if len (usage ) == 2 :
470
528
bm = Bitmap ()
471
- bm .add_col ([0 ]* height_pts )
529
+ if cpu_ls and lshare :
530
+ lshare_col0 = [0 ]* height_pts
531
+ if lshare [0 ] > 0 :
532
+ lshare_col0 [height_pts - lshare [0 ]] = 1
533
+ bm .add_col (lshare_col0 )
534
+ else :
535
+ bm .add_col ([0 ]* height_pts )
472
536
for u in usage :
473
537
coldata = [0 ]* (height_pts - u ) + [1 ]* u
474
538
bm .add_col (coldata )
539
+ if cpu_ls and len (lshare ) == 2 :
540
+ lshare_col1 = [0 ]* height_pts
541
+ if lshare [1 ] > 0 :
542
+ lshare_col1 [height_pts - lshare [1 ]] = 1
543
+ bm .add_col (lshare_col1 )
544
+ else :
545
+ bm .add_col ([0 ]* height_pts )
475
546
s = bm .to_braille ()
476
547
if cpu [- 2 ] % 4 == 0 :
477
548
bgon = "\x1b [48;5;14m"
@@ -483,6 +554,7 @@ def print_cpu_load2(pkg, cpus, cpu_usage):
483
554
line1 .append (bgon + (s .split ("\n " )[1 ]) + bgoff )
484
555
line2 .append (bgon + (" " + str (cpu [- 2 ]))[- 2 :] + bgoff )
485
556
usage = []
557
+ lshare = []
486
558
if line0 :
487
559
lines .append (line0 )
488
560
lines .append (line1 )
@@ -494,11 +566,13 @@ if __name__ == "__main__":
494
566
opt_delay = 1.0
495
567
opt_memory = False
496
568
opt_upi = False
569
+ opt_largest_share = False
497
570
571
+ # get stats on cpu pid events
498
572
try :
499
573
opts , remainder = getopt .gnu_getopt (
500
- sys .argv [1 :], 'hd:mu ' ,
501
- ['help' , 'delay=' , 'memory' , 'upi' ])
574
+ sys .argv [1 :], 'hd:lmu ' ,
575
+ ['help' , 'delay=' , 'largest-share' , ' memory' , 'upi' ])
502
576
except Exception as e :
503
577
error (str (e ))
504
578
for opt , arg in opts :
@@ -510,19 +584,28 @@ if __name__ == "__main__":
510
584
opt_delay = float (arg )
511
585
except :
512
586
error ("invalid delay: %s" % (arg ,))
587
+ elif opt in ["-l" , "--largest-share" ]:
588
+ opt_largest_share = True
513
589
elif opt in ["-m" , "--memory" ]:
514
590
opt_memory = True
515
591
elif opt in ["-u" , "--upi" ]:
516
592
opt_upi = True
517
593
else :
518
- error ("unknown option: %s" % (opt ,))
594
+ error ("internal error: unhandled option: %s" % (opt ,))
519
595
520
596
cpus = read_topology ()
521
597
add_numa_info_to_pkg (cpus )
522
598
max_pkg = max ([cpu [1 ] for cpu in cpus ])
523
599
membw_usage , upi_usage = None , None
524
600
cpu_usage_from_proc () # read initial values
525
601
602
+ if opt_largest_share :
603
+ print ("starting bpftrace" )
604
+ try :
605
+ cpu_pid_stats ()
606
+ except Exception as err :
607
+ error ("cannot run bpftrace to show -l/--largest-share data: %s" % (err ,))
608
+
526
609
if opt_upi :
527
610
print ("starting pcm" )
528
611
try :
@@ -545,6 +628,7 @@ if __name__ == "__main__":
545
628
else :
546
629
membw_from_pcm = lambda : time .sleep (opt_delay )
547
630
631
+ cpu_ls = {}
548
632
# clear terminal
549
633
print ("\x1b [2J" , end = "" )
550
634
while True :
@@ -560,8 +644,11 @@ if __name__ == "__main__":
560
644
print ()
561
645
562
646
cpu_usage = cpu_usage_from_proc ()
647
+ if opt_largest_share :
648
+ cps = cpu_pid_stats ()
649
+ cpu_ls = cpu_lshare (cps , cpu_usage )
563
650
564
- print_cpu_load2 (0 , cpus , cpu_usage )
651
+ print_cpu_load2 (0 , cpus , cpu_usage , cpu_ls )
565
652
566
653
if max_pkg > 0 :
567
654
if opt_upi :
@@ -570,7 +657,7 @@ if __name__ == "__main__":
570
657
print ()
571
658
if opt_upi :
572
659
print_upi_usage (pkg , upi_usage , "\u2191 " )
573
- print_cpu_load2 (pkg , cpus , cpu_usage )
660
+ print_cpu_load2 (pkg , cpus , cpu_usage , cpu_ls )
574
661
575
662
if membw_usage :
576
663
for pkg in range (1 , max_pkg + 1 ):
0 commit comments