cpython/Tools/scripts/summarize_stats.py

946 lines
31 KiB
Python

"""Print a summary of specialization stats for all files in the
default stats folders.
"""
# NOTE: Bytecode introspection modules (opcode, dis, etc.) should only
# happen when loading a single dataset. When comparing datasets, it
# could get it wrong, leading to subtle errors.
import argparse
import collections
import json
import os.path
from datetime import date
import itertools
import sys
import re
if os.name == "nt":
DEFAULT_DIR = "c:\\temp\\py_stats\\"
else:
DEFAULT_DIR = "/tmp/py_stats/"
TOTAL = "specialization.hit", "specialization.miss", "execution_count"
def format_ratio(num, den):
"""
Format a ratio as a percentage. When the denominator is 0, returns the empty
string.
"""
if den == 0:
return ""
else:
return f"{num/den:.01%}"
def percentage_to_float(s):
"""
Converts a percentage string to a float. The empty string is returned as 0.0
"""
if s == "":
return 0.0
else:
assert s[-1] == "%"
return float(s[:-1])
def join_rows(a_rows, b_rows):
"""
Joins two tables together, side-by-side, where the first column in each is a
common key.
"""
if len(a_rows) == 0 and len(b_rows) == 0:
return []
if len(a_rows):
a_ncols = list(set(len(x) for x in a_rows))
if len(a_ncols) != 1:
raise ValueError("Table a is ragged")
if len(b_rows):
b_ncols = list(set(len(x) for x in b_rows))
if len(b_ncols) != 1:
raise ValueError("Table b is ragged")
if len(a_rows) and len(b_rows) and a_ncols[0] != b_ncols[0]:
raise ValueError("Tables have different widths")
if len(a_rows):
ncols = a_ncols[0]
else:
ncols = b_ncols[0]
default = [""] * (ncols - 1)
a_data = {x[0]: x[1:] for x in a_rows}
b_data = {x[0]: x[1:] for x in b_rows}
if len(a_data) != len(a_rows) or len(b_data) != len(b_rows):
raise ValueError("Duplicate keys")
# To preserve ordering, use A's keys as is and then add any in B that aren't
# in A
keys = list(a_data.keys()) + [k for k in b_data.keys() if k not in a_data]
return [(k, *a_data.get(k, default), *b_data.get(k, default)) for k in keys]
def calculate_specialization_stats(family_stats, total):
rows = []
for key in sorted(family_stats):
if key.startswith("specialization.failure_kinds"):
continue
if key in ("specialization.hit", "specialization.miss"):
label = key[len("specialization.") :]
elif key == "execution_count":
continue
elif key in (
"specialization.success",
"specialization.failure",
"specializable",
):
continue
elif key.startswith("pair"):
continue
else:
label = key
rows.append(
(
f"{label:>12}",
f"{family_stats[key]:>12}",
format_ratio(family_stats[key], total),
)
)
return rows
def calculate_specialization_success_failure(family_stats):
total_attempts = 0
for key in ("specialization.success", "specialization.failure"):
total_attempts += family_stats.get(key, 0)
rows = []
if total_attempts:
for key in ("specialization.success", "specialization.failure"):
label = key[len("specialization.") :]
label = label[0].upper() + label[1:]
val = family_stats.get(key, 0)
rows.append((label, val, format_ratio(val, total_attempts)))
return rows
def calculate_specialization_failure_kinds(name, family_stats, defines):
total_failures = family_stats.get("specialization.failure", 0)
failure_kinds = [0] * 40
for key in family_stats:
if not key.startswith("specialization.failure_kind"):
continue
_, index = key[:-1].split("[")
index = int(index)
failure_kinds[index] = family_stats[key]
failures = [(value, index) for (index, value) in enumerate(failure_kinds)]
failures.sort(reverse=True)
rows = []
for value, index in failures:
if not value:
continue
rows.append(
(
kind_to_text(index, defines, name),
value,
format_ratio(value, total_failures),
)
)
return rows
def print_specialization_stats(name, family_stats, defines):
if "specializable" not in family_stats:
return
total = sum(family_stats.get(kind, 0) for kind in TOTAL)
if total == 0:
return
with Section(name, 3, f"specialization stats for {name} family"):
rows = calculate_specialization_stats(family_stats, total)
emit_table(("Kind", "Count", "Ratio"), rows)
rows = calculate_specialization_success_failure(family_stats)
if rows:
print_title("Specialization attempts", 4)
emit_table(("", "Count:", "Ratio:"), rows)
rows = calculate_specialization_failure_kinds(name, family_stats, defines)
emit_table(("Failure kind", "Count:", "Ratio:"), rows)
def print_comparative_specialization_stats(
name, base_family_stats, head_family_stats, defines
):
if "specializable" not in base_family_stats:
return
base_total = sum(base_family_stats.get(kind, 0) for kind in TOTAL)
head_total = sum(head_family_stats.get(kind, 0) for kind in TOTAL)
if base_total + head_total == 0:
return
with Section(name, 3, f"specialization stats for {name} family"):
base_rows = calculate_specialization_stats(base_family_stats, base_total)
head_rows = calculate_specialization_stats(head_family_stats, head_total)
emit_table(
("Kind", "Base Count", "Base Ratio", "Head Count", "Head Ratio"),
join_rows(base_rows, head_rows),
)
base_rows = calculate_specialization_success_failure(base_family_stats)
head_rows = calculate_specialization_success_failure(head_family_stats)
rows = join_rows(base_rows, head_rows)
if rows:
print_title("Specialization attempts", 4)
emit_table(
("", "Base Count:", "Base Ratio:", "Head Count:", "Head Ratio:"), rows
)
base_rows = calculate_specialization_failure_kinds(
name, base_family_stats, defines
)
head_rows = calculate_specialization_failure_kinds(
name, head_family_stats, defines
)
emit_table(
(
"Failure kind",
"Base Count:",
"Base Ratio:",
"Head Count:",
"Head Ratio:",
),
join_rows(base_rows, head_rows),
)
def gather_stats(input):
# Note the output of this function must be JSON-serializable
if os.path.isfile(input):
with open(input, "r") as fd:
stats = json.load(fd)
stats["_stats_defines"] = {
int(k): v for k, v in stats["_stats_defines"].items()
}
stats["_defines"] = {int(k): v for k, v in stats["_defines"].items()}
return stats
elif os.path.isdir(input):
stats = collections.Counter()
for filename in os.listdir(input):
with open(os.path.join(input, filename)) as fd:
for line in fd:
try:
key, value = line.split(":")
except ValueError:
print(
f"Unparsable line: '{line.strip()}' in {filename}",
file=sys.stderr,
)
continue
key = key.strip()
value = int(value)
stats[key] += value
stats["__nfiles__"] += 1
import opcode
stats["_specialized_instructions"] = [
op for op in opcode._specialized_opmap.keys() if "__" not in op
]
stats["_stats_defines"] = get_stats_defines()
stats["_defines"] = get_defines()
return stats
else:
raise ValueError(f"{input:r} is not a file or directory path")
def extract_opcode_stats(stats, prefix):
opcode_stats = collections.defaultdict(dict)
for key, value in stats.items():
if not key.startswith(prefix):
continue
name, _, rest = key[len(prefix) + 1 :].partition("]")
opcode_stats[name][rest.strip(".")] = value
return opcode_stats
def parse_kinds(spec_src, prefix="SPEC_FAIL"):
defines = collections.defaultdict(list)
start = "#define " + prefix + "_"
for line in spec_src:
line = line.strip()
if not line.startswith(start):
continue
line = line[len(start) :]
name, val = line.split()
defines[int(val.strip())].append(name.strip())
return defines
def pretty(defname):
return defname.replace("_", " ").lower()
def kind_to_text(kind, defines, opname):
if kind <= 8:
return pretty(defines[kind][0])
if opname == "LOAD_SUPER_ATTR":
opname = "SUPER"
elif opname.endswith("ATTR"):
opname = "ATTR"
elif opname in ("FOR_ITER", "SEND"):
opname = "ITER"
elif opname.endswith("SUBSCR"):
opname = "SUBSCR"
for name in defines[kind]:
if name.startswith(opname):
return pretty(name[len(opname) + 1 :])
return "kind " + str(kind)
def categorized_counts(opcode_stats, specialized_instructions):
basic = 0
specialized = 0
not_specialized = 0
for name, opcode_stat in opcode_stats.items():
if "execution_count" not in opcode_stat:
continue
count = opcode_stat["execution_count"]
if "specializable" in opcode_stat:
not_specialized += count
elif name in specialized_instructions:
miss = opcode_stat.get("specialization.miss", 0)
not_specialized += miss
specialized += count - miss
else:
basic += count
return basic, not_specialized, specialized
def print_title(name, level=2):
print("#" * level, name)
print()
class Section:
def __init__(self, title, level=2, summary=None):
self.title = title
self.level = level
if summary is None:
self.summary = title.lower()
else:
self.summary = summary
def __enter__(self):
print_title(self.title, self.level)
print("<details>")
print("<summary>", self.summary, "</summary>")
print()
return self
def __exit__(*args):
print()
print("</details>")
print()
def to_str(x):
if isinstance(x, int):
return format(x, ",d")
else:
return str(x)
def emit_table(header, rows):
width = len(header)
header_line = "|"
under_line = "|"
for item in header:
under = "---"
if item.endswith(":"):
item = item[:-1]
under += ":"
header_line += item + " | "
under_line += under + "|"
print(header_line)
print(under_line)
for row in rows:
if width is not None and len(row) != width:
raise ValueError("Wrong number of elements in row '" + str(row) + "'")
print("|", " | ".join(to_str(i) for i in row), "|")
print()
def emit_histogram(title, stats, key, total):
rows = []
for k, v in stats.items():
if k.startswith(key):
entry = int(re.match(r".+\[([0-9]+)\]", k).groups()[0])
rows.append((f"<= {entry}", int(v), format_ratio(int(v), total)))
# Don't include larger buckets with 0 entries
for j in range(len(rows) - 1, -1, -1):
if rows[j][1] != 0:
break
rows = rows[: j + 1]
print(f"**{title}**\n")
emit_table(("Range", "Count:", "Ratio:"), rows)
def calculate_execution_counts(opcode_stats, total):
counts = []
for name, opcode_stat in opcode_stats.items():
if "execution_count" in opcode_stat:
count = opcode_stat["execution_count"]
miss = 0
if "specializable" not in opcode_stat:
miss = opcode_stat.get("specialization.miss")
counts.append((count, name, miss))
counts.sort(reverse=True)
cumulative = 0
rows = []
for count, name, miss in counts:
cumulative += count
if miss:
miss = format_ratio(miss, count)
else:
miss = ""
rows.append(
(
name,
count,
format_ratio(count, total),
format_ratio(cumulative, total),
miss,
)
)
return rows
def emit_execution_counts(opcode_stats, total):
with Section("Execution counts", summary="execution counts for all instructions"):
rows = calculate_execution_counts(opcode_stats, total)
emit_table(("Name", "Count:", "Self:", "Cumulative:", "Miss ratio:"), rows)
def _emit_comparative_execution_counts(base_rows, head_rows):
base_data = {x[0]: x[1:] for x in base_rows}
head_data = {x[0]: x[1:] for x in head_rows}
opcodes = base_data.keys() | head_data.keys()
rows = []
default = [0, "0.0%", "0.0%", 0]
for opcode in opcodes:
base_entry = base_data.get(opcode, default)
head_entry = head_data.get(opcode, default)
if base_entry[0] == 0:
change = 1
else:
change = (head_entry[0] - base_entry[0]) / base_entry[0]
rows.append((opcode, base_entry[0], head_entry[0], f"{change:0.1%}"))
rows.sort(key=lambda x: abs(percentage_to_float(x[-1])), reverse=True)
emit_table(("Name", "Base Count:", "Head Count:", "Change:"), rows)
def emit_comparative_execution_counts(
base_opcode_stats, base_total, head_opcode_stats, head_total, level=2
):
with Section(
"Execution counts", summary="execution counts for all instructions", level=level
):
base_rows = calculate_execution_counts(base_opcode_stats, base_total)
head_rows = calculate_execution_counts(head_opcode_stats, head_total)
_emit_comparative_execution_counts(base_rows, head_rows)
def get_defines():
spec_path = os.path.join(os.path.dirname(__file__), "../../Python/specialize.c")
with open(spec_path) as spec_src:
defines = parse_kinds(spec_src)
return defines
def emit_specialization_stats(opcode_stats, defines):
with Section("Specialization stats", summary="specialization stats by family"):
for name, opcode_stat in opcode_stats.items():
print_specialization_stats(name, opcode_stat, defines)
def emit_comparative_specialization_stats(
base_opcode_stats, head_opcode_stats, defines
):
with Section("Specialization stats", summary="specialization stats by family"):
opcodes = set(base_opcode_stats.keys()) & set(head_opcode_stats.keys())
for opcode in opcodes:
print_comparative_specialization_stats(
opcode, base_opcode_stats[opcode], head_opcode_stats[opcode], defines
)
def calculate_specialization_effectiveness(
opcode_stats, total, specialized_instructions
):
basic, not_specialized, specialized = categorized_counts(
opcode_stats, specialized_instructions
)
return [
("Basic", basic, format_ratio(basic, total)),
("Not specialized", not_specialized, format_ratio(not_specialized, total)),
("Specialized", specialized, format_ratio(specialized, total)),
]
def emit_specialization_overview(opcode_stats, total, specialized_instructions):
with Section("Specialization effectiveness"):
rows = calculate_specialization_effectiveness(
opcode_stats, total, specialized_instructions
)
emit_table(("Instructions", "Count:", "Ratio:"), rows)
for title, field in (
("Deferred", "specialization.deferred"),
("Misses", "specialization.miss"),
):
total = 0
counts = []
for name, opcode_stat in opcode_stats.items():
# Avoid double counting misses
if title == "Misses" and "specializable" in opcode_stat:
continue
value = opcode_stat.get(field, 0)
counts.append((value, name))
total += value
counts.sort(reverse=True)
if total:
with Section(f"{title} by instruction", 3):
rows = [
(name, count, format_ratio(count, total))
for (count, name) in counts[:10]
]
emit_table(("Name", "Count:", "Ratio:"), rows)
def emit_comparative_specialization_overview(
base_opcode_stats,
base_total,
head_opcode_stats,
head_total,
specialized_instructions,
):
with Section("Specialization effectiveness"):
base_rows = calculate_specialization_effectiveness(
base_opcode_stats, base_total, specialized_instructions
)
head_rows = calculate_specialization_effectiveness(
head_opcode_stats, head_total, specialized_instructions
)
emit_table(
(
"Instructions",
"Base Count:",
"Base Ratio:",
"Head Count:",
"Head Ratio:",
),
join_rows(base_rows, head_rows),
)
def get_stats_defines():
stats_path = os.path.join(
os.path.dirname(__file__), "../../Include/cpython/pystats.h"
)
with open(stats_path) as stats_src:
defines = parse_kinds(stats_src, prefix="EVAL_CALL")
return defines
def calculate_call_stats(stats, defines):
total = 0
for key, value in stats.items():
if "Calls to" in key:
total += value
rows = []
for key, value in stats.items():
if "Calls to" in key:
rows.append((key, value, format_ratio(value, total)))
elif key.startswith("Calls "):
name, index = key[:-1].split("[")
index = int(index)
label = name + " (" + pretty(defines[index][0]) + ")"
rows.append((label, value, format_ratio(value, total)))
for key, value in stats.items():
if key.startswith("Frame"):
rows.append((key, value, format_ratio(value, total)))
return rows
def emit_call_stats(stats, defines):
with Section("Call stats", summary="Inlined calls and frame stats"):
rows = calculate_call_stats(stats, defines)
emit_table(("", "Count:", "Ratio:"), rows)
def emit_comparative_call_stats(base_stats, head_stats, defines):
with Section("Call stats", summary="Inlined calls and frame stats"):
base_rows = calculate_call_stats(base_stats, defines)
head_rows = calculate_call_stats(head_stats, defines)
rows = join_rows(base_rows, head_rows)
rows.sort(key=lambda x: -percentage_to_float(x[-1]))
emit_table(
("", "Base Count:", "Base Ratio:", "Head Count:", "Head Ratio:"), rows
)
def calculate_object_stats(stats):
total_materializations = stats.get("Object new values")
total_allocations = stats.get("Object allocations") + stats.get(
"Object allocations from freelist"
)
total_increfs = stats.get("Object interpreter increfs") + stats.get(
"Object increfs"
)
total_decrefs = stats.get("Object interpreter decrefs") + stats.get(
"Object decrefs"
)
rows = []
for key, value in stats.items():
if key.startswith("Object"):
if "materialize" in key:
ratio = format_ratio(value, total_materializations)
elif "allocations" in key:
ratio = format_ratio(value, total_allocations)
elif "increfs" in key:
ratio = format_ratio(value, total_increfs)
elif "decrefs" in key:
ratio = format_ratio(value, total_decrefs)
else:
ratio = ""
label = key[6:].strip()
label = label[0].upper() + label[1:]
rows.append((label, value, ratio))
return rows
def calculate_gc_stats(stats):
gc_stats = []
for key, value in stats.items():
if not key.startswith("GC"):
continue
n, _, rest = key[3:].partition("]")
name = rest.strip()
gen_n = int(n)
while len(gc_stats) <= gen_n:
gc_stats.append({})
gc_stats[gen_n][name] = value
return [
(i, gen["collections"], gen["objects collected"], gen["object visits"])
for (i, gen) in enumerate(gc_stats)
]
def emit_object_stats(stats):
with Section("Object stats", summary="allocations, frees and dict materializatons"):
rows = calculate_object_stats(stats)
emit_table(("", "Count:", "Ratio:"), rows)
def emit_comparative_object_stats(base_stats, head_stats):
with Section("Object stats", summary="allocations, frees and dict materializatons"):
base_rows = calculate_object_stats(base_stats)
head_rows = calculate_object_stats(head_stats)
emit_table(
("", "Base Count:", "Base Ratio:", "Head Count:", "Head Ratio:"),
join_rows(base_rows, head_rows),
)
def emit_gc_stats(stats):
with Section("GC stats", summary="GC collections and effectiveness"):
rows = calculate_gc_stats(stats)
emit_table(
("Generation:", "Collections:", "Objects collected:", "Object visits:"),
rows,
)
def emit_comparative_gc_stats(base_stats, head_stats):
with Section("GC stats", summary="GC collections and effectiveness"):
base_rows = calculate_gc_stats(base_stats)
head_rows = calculate_gc_stats(head_stats)
emit_table(
(
"Generation:",
"Base collections:",
"Head collections:",
"Base objects collected:",
"Head objects collected:",
"Base object visits:",
"Head object visits:",
),
join_rows(base_rows, head_rows),
)
def get_total(opcode_stats):
total = 0
for opcode_stat in opcode_stats.values():
if "execution_count" in opcode_stat:
total += opcode_stat["execution_count"]
return total
def emit_pair_counts(opcode_stats, total):
pair_counts = []
for name_i, opcode_stat in opcode_stats.items():
for key, value in opcode_stat.items():
if key.startswith("pair_count"):
name_j, _, _ = key[11:].partition("]")
if value:
pair_counts.append((value, (name_i, name_j)))
with Section("Pair counts", summary="Pair counts for top 100 pairs"):
pair_counts.sort(reverse=True)
cumulative = 0
rows = []
for count, pair in itertools.islice(pair_counts, 100):
name_i, name_j = pair
cumulative += count
rows.append(
(
f"{name_i} {name_j}",
count,
format_ratio(count, total),
format_ratio(cumulative, total),
)
)
emit_table(("Pair", "Count:", "Self:", "Cumulative:"), rows)
with Section(
"Predecessor/Successor Pairs",
summary="Top 5 predecessors and successors of each opcode",
):
predecessors = collections.defaultdict(collections.Counter)
successors = collections.defaultdict(collections.Counter)
total_predecessors = collections.Counter()
total_successors = collections.Counter()
for count, (first, second) in pair_counts:
if count:
predecessors[second][first] = count
successors[first][second] = count
total_predecessors[second] += count
total_successors[first] += count
for name in opcode_stats.keys():
total1 = total_predecessors[name]
total2 = total_successors[name]
if total1 == 0 and total2 == 0:
continue
pred_rows = succ_rows = ()
if total1:
pred_rows = [
(pred, count, f"{count/total1:.1%}")
for (pred, count) in predecessors[name].most_common(5)
]
if total2:
succ_rows = [
(succ, count, f"{count/total2:.1%}")
for (succ, count) in successors[name].most_common(5)
]
with Section(name, 3, f"Successors and predecessors for {name}"):
emit_table(("Predecessors", "Count:", "Percentage:"), pred_rows)
emit_table(("Successors", "Count:", "Percentage:"), succ_rows)
def calculate_optimization_stats(stats):
attempts = stats["Optimization attempts"]
created = stats["Optimization traces created"]
executed = stats["Optimization traces executed"]
uops = stats["Optimization uops executed"]
trace_stack_overflow = stats["Optimization trace stack overflow"]
trace_stack_underflow = stats["Optimization trace stack underflow"]
trace_too_long = stats["Optimization trace too long"]
trace_too_short = stats["Optimiztion trace too short"]
inner_loop = stats["Optimization inner loop"]
recursive_call = stats["Optimization recursive call"]
return [
("Optimization attempts", attempts, ""),
("Traces created", created, format_ratio(created, attempts)),
("Traces executed", executed, ""),
("Uops executed", uops, int(uops / (executed or 1))),
("Trace stack overflow", trace_stack_overflow, ""),
("Trace stack underflow", trace_stack_underflow, ""),
("Trace too long", trace_too_long, ""),
("Trace too short", trace_too_short, ""),
("Inner loop found", inner_loop, ""),
("Recursive call", recursive_call, ""),
]
def calculate_uop_execution_counts(opcode_stats):
total = 0
counts = []
for name, opcode_stat in opcode_stats.items():
if "execution_count" in opcode_stat:
count = opcode_stat["execution_count"]
counts.append((count, name))
total += count
counts.sort(reverse=True)
cumulative = 0
rows = []
for count, name in counts:
cumulative += count
rows.append(
(name, count, format_ratio(count, total), format_ratio(cumulative, total))
)
return rows
def emit_optimization_stats(stats):
if "Optimization attempts" not in stats:
return
uop_stats = extract_opcode_stats(stats, "uops")
with Section(
"Optimization (Tier 2) stats", summary="statistics about the Tier 2 optimizer"
):
with Section("Overall stats", level=3):
rows = calculate_optimization_stats(stats)
emit_table(("", "Count:", "Ratio:"), rows)
emit_histogram(
"Trace length histogram",
stats,
"Trace length",
stats["Optimization traces created"],
)
emit_histogram(
"Optimized trace length histogram",
stats,
"Optimized trace length",
stats["Optimization traces created"],
)
emit_histogram(
"Trace run length histogram",
stats,
"Trace run length",
stats["Optimization traces executed"],
)
with Section("Uop stats", level=3):
rows = calculate_uop_execution_counts(uop_stats)
emit_table(("Uop", "Count:", "Self:", "Cumulative:"), rows)
with Section("Unsupported opcodes", level=3):
unsupported_opcodes = extract_opcode_stats(stats, "unsupported_opcode")
data = []
for opcode, entry in unsupported_opcodes.items():
data.append((entry["count"], opcode))
data.sort(reverse=True)
rows = [(x[1], x[0]) for x in data]
emit_table(("Opcode", "Count"), rows)
def emit_comparative_optimization_stats(base_stats, head_stats):
print("## Comparative optimization stats not implemented\n\n")
def output_single_stats(stats):
opcode_stats = extract_opcode_stats(stats, "opcode")
total = get_total(opcode_stats)
emit_execution_counts(opcode_stats, total)
emit_pair_counts(opcode_stats, total)
emit_specialization_stats(opcode_stats, stats["_defines"])
emit_specialization_overview(
opcode_stats, total, stats["_specialized_instructions"]
)
emit_call_stats(stats, stats["_stats_defines"])
emit_object_stats(stats)
emit_gc_stats(stats)
emit_optimization_stats(stats)
with Section("Meta stats", summary="Meta statistics"):
emit_table(("", "Count:"), [("Number of data files", stats["__nfiles__"])])
def output_comparative_stats(base_stats, head_stats):
base_opcode_stats = extract_opcode_stats(base_stats, "opcode")
base_total = get_total(base_opcode_stats)
head_opcode_stats = extract_opcode_stats(head_stats, "opcode")
head_total = get_total(head_opcode_stats)
emit_comparative_execution_counts(
base_opcode_stats, base_total, head_opcode_stats, head_total
)
emit_comparative_specialization_stats(
base_opcode_stats, head_opcode_stats, head_stats["_defines"]
)
emit_comparative_specialization_overview(
base_opcode_stats,
base_total,
head_opcode_stats,
head_total,
head_stats["_specialized_instructions"],
)
emit_comparative_call_stats(base_stats, head_stats, head_stats["_stats_defines"])
emit_comparative_object_stats(base_stats, head_stats)
emit_comparative_gc_stats(base_stats, head_stats)
emit_comparative_optimization_stats(base_stats, head_stats)
def output_stats(inputs, json_output=None):
if len(inputs) == 1:
stats = gather_stats(inputs[0])
if json_output is not None:
json.dump(stats, json_output)
output_single_stats(stats)
elif len(inputs) == 2:
if json_output is not None:
raise ValueError("Can not output to JSON when there are multiple inputs")
base_stats = gather_stats(inputs[0])
head_stats = gather_stats(inputs[1])
output_comparative_stats(base_stats, head_stats)
print("---")
print("Stats gathered on:", date.today())
def main():
parser = argparse.ArgumentParser(description="Summarize pystats results")
parser.add_argument(
"inputs",
nargs="*",
type=str,
default=[DEFAULT_DIR],
help=f"""
Input source(s).
For each entry, if a .json file, the output provided by --json-output from a previous run;
if a directory, a directory containing raw pystats .txt files.
If one source is provided, its stats are printed.
If two sources are provided, comparative stats are printed.
Default is {DEFAULT_DIR}.
""",
)
parser.add_argument(
"--json-output",
nargs="?",
type=argparse.FileType("w"),
help="Output complete raw results to the given JSON file.",
)
args = parser.parse_args()
if len(args.inputs) > 2:
raise ValueError("0-2 arguments may be provided.")
output_stats(args.inputs, json_output=args.json_output)
if __name__ == "__main__":
main()