9
9
from collections import defaultdict
10
10
import csv
11
11
import functools
12
+ import json
12
13
from operator import itemgetter
13
14
from pathlib import Path
14
15
import re
25
26
26
27
SANITY_CHECK = True
27
28
29
+ # Must match the value in _benchmark.src.yml
30
+ PERF_PERIOD = 1000000
31
+
28
32
29
33
# Categories of functions, where each value is a list of regular expressions.
30
34
# These are matched in-order.
40
44
"_PyPegen_.+" ,
41
45
"_PyStack_.+" ,
42
46
"_PyVectorcall_.+" ,
47
+ "_TAIL_CALL_.+" ,
43
48
"advance" ,
44
49
"call_instrumentation_vector.*" ,
45
50
"initialize_locals" ,
@@ -289,7 +294,7 @@ def handle_benchmark(
289
294
md : IO [str ],
290
295
results : defaultdict [str , defaultdict [str , float ]],
291
296
categories : defaultdict [str , defaultdict [tuple [str , str ], float ]],
292
- ):
297
+ ) -> float :
293
298
csv_path = Path (csv_path )
294
299
295
300
stem = csv_path .stem .split ("." , 1 )[0 ]
@@ -314,6 +319,7 @@ def handle_benchmark(
314
319
tainted_pids .add (pid )
315
320
316
321
times = defaultdict (float )
322
+ total = 0.0
317
323
with csv_path .open (newline = "" ) as fd :
318
324
csvreader = csv .reader (fd )
319
325
for _ in csvreader :
@@ -327,18 +333,14 @@ def handle_benchmark(
327
333
continue
328
334
329
335
self_time = float (self_time )
330
- if self_time > 1.0 :
331
- print (f"{ stem } Invalid data" )
332
336
if obj == "[JIT]" :
333
337
times [("[JIT]" , "jit" )] += self_time
334
338
else :
335
339
times [(obj , sym )] += self_time
340
+ total += self_time
336
341
337
- total = sum (times .values ())
338
- assert total <= 1.0
339
342
scale = 1.0 / total
340
- rows = [(v * scale , k [0 ], k [1 ]) for k , v in times .items ()]
341
- rows .sort (reverse = True )
343
+ rows = sorted (((v , * k ) for k , v in times .items ()), reverse = True )
342
344
343
345
for self_time , obj , sym in rows :
344
346
if self_time <= 0.0 :
@@ -349,8 +351,11 @@ def handle_benchmark(
349
351
350
352
results [stem ][category ] += self_time
351
353
352
- if self_time >= 0.0025 :
353
- md .write (f"| { self_time :.2%} | `{ obj } ` | `{ sym } ` | { category } |\n " )
354
+ scaled_time = self_time * scale
355
+ if scaled_time >= 0.0025 :
356
+ md .write (f"| { scaled_time :.2%} | `{ obj } ` | `{ sym } ` | { category } |\n " )
357
+
358
+ return total
354
359
355
360
356
361
def plot_bargraph (
@@ -414,6 +419,62 @@ def plot_pie(categories: list[tuple[float, str]], output_filename: PathLike):
414
419
fig .savefig (output_filename , dpi = 200 )
415
420
416
421
422
+ def handle_tail_call_stats (
423
+ input_dir : PathLike ,
424
+ categories : defaultdict [str , defaultdict [tuple [str , str ], float ]],
425
+ output_prefix : PathLike ,
426
+ ):
427
+ input_dir = Path (input_dir )
428
+ output_prefix = Path (output_prefix )
429
+
430
+ tail_call_stats = defaultdict (float )
431
+ total_time = 0.0
432
+ for (_ , sym ), self_time in categories ["interpreter" ].items ():
433
+ if (bytecode := sym .removeprefix ("_TAIL_CALL_" )) != sym :
434
+ tail_call_stats [bytecode ] += self_time
435
+ total_time += self_time
436
+
437
+ if len (tail_call_stats ) == 0 :
438
+ return
439
+
440
+ pystats_file = input_dir / "pystats.json"
441
+
442
+ if not pystats_file .is_file ():
443
+ print ("No pystats.json file found. Skipping tail call stats." )
444
+ return
445
+
446
+ with pystats_file .open () as fd :
447
+ pystats = json .load (fd )
448
+
449
+ pystats_bytecodes = defaultdict (int )
450
+ total_count = 0
451
+ for key , val in pystats .items ():
452
+ if match := re .match (r"opcode\[(.+)\]\.execution_count" , key ):
453
+ pystats_bytecodes [match .group (1 )] += val
454
+ total_count += val
455
+
456
+ with open (output_prefix .with_suffix (".tail_calls.csv" ), "w" ) as csvfile :
457
+ writer = csv .writer (csvfile , dialect = "unix" )
458
+ writer .writerow (
459
+ ["Bytecode" , "% time" , "count" , "% count" , "time per count (μs)" ]
460
+ )
461
+ for bytecode , periods in sorted (
462
+ tail_call_stats .items (), key = itemgetter (1 ), reverse = True
463
+ ):
464
+ count = pystats_bytecodes [bytecode ]
465
+ if count == 0 :
466
+ continue
467
+ writer .writerow (
468
+ [
469
+ bytecode ,
470
+ f"{ periods / total_time :.02%} " ,
471
+ count ,
472
+ f"{ count / total_count :.02%} " ,
473
+ f"{ ((periods / PERF_PERIOD ) / count ) * 1e6 :03f} " ,
474
+ ]
475
+ )
476
+
477
+
417
478
def _main (input_dir : PathLike , output_prefix : PathLike ):
418
479
input_dir = Path (input_dir )
419
480
output_prefix = Path (output_prefix )
@@ -425,35 +486,38 @@ def _main(input_dir: PathLike, output_prefix: PathLike):
425
486
print ("No profiling data. Skipping." )
426
487
return
427
488
489
+ total = 0.0
428
490
with output_prefix .with_suffix (".md" ).open ("w" ) as md :
429
491
for csv_path in sorted (input_dir .glob ("*.csv" )):
430
- handle_benchmark (csv_path , md , results , categories )
492
+ if ".tail_calls.csv" in csv_path .name :
493
+ continue
494
+ total += handle_benchmark (csv_path , md , results , categories )
431
495
432
496
sorted_categories = sorted (
433
- [
434
- (sum (val .values ()) / len (results ), key )
435
- for (key , val ) in categories .items ()
436
- ],
497
+ [(sum (val .values ()), key ) for (key , val ) in categories .items ()],
437
498
reverse = True ,
438
499
)
439
500
440
501
md .write ("\n \n ## Categories\n " )
441
- for total , category in sorted_categories :
502
+ for category_total , category in sorted_categories :
442
503
matches = categories [category ]
443
504
md .write (f"\n ### { category } \n \n " )
444
- md .write (f"{ total :.2%} total\n \n " )
505
+ md .write (f"{ category_total / total :.2%} total\n \n " )
445
506
md .write ("| percentage | object | symbol |\n " )
446
507
md .write ("| ---: | :--- | :--- |\n " )
447
508
for (obj , sym ), self_time in sorted (
448
509
matches .items (), key = itemgetter (1 ), reverse = True
449
510
):
450
- if self_time < 0.0025 :
511
+ self_fraction = self_time / total
512
+ if self_fraction < 0.000025 :
451
513
break
452
- md .write (f"| { self_time / len ( results ) :.2%} | { obj } | { sym } |\n " )
514
+ md .write (f"| { self_fraction :.2%} | { obj } | { sym } |\n " )
453
515
454
516
plot_bargraph (results , sorted_categories , output_prefix .with_suffix (".svg" ))
455
517
plot_pie (sorted_categories , output_prefix .with_suffix (".pie.svg" ))
456
518
519
+ handle_tail_call_stats (input_dir , categories , output_prefix )
520
+
457
521
458
522
def main ():
459
523
parser = argparse .ArgumentParser (
0 commit comments