mirror of
https://github.com/python/cpython.git
synced 2025-08-04 00:48:58 +00:00
bpo-46072: Output stats as markdown with collapsible sections. (GH-31228)
This commit is contained in:
parent
77bab59c8a
commit
f71a69aa92
2 changed files with 381 additions and 257 deletions
|
@ -5,6 +5,7 @@ default stats folders.
|
|||
import collections
|
||||
import os.path
|
||||
import opcode
|
||||
from datetime import date
|
||||
|
||||
if os.name == "nt":
|
||||
DEFAULT_DIR = "c:\\temp\\py_stats\\"
|
||||
|
@ -24,37 +25,53 @@ for name in opcode.opname[1:]:
|
|||
|
||||
TOTAL = "specialization.deferred", "specialization.hit", "specialization.miss", "execution_count"
|
||||
|
||||
def print_specialization_stats(name, family_stats):
|
||||
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
|
||||
print(name+":")
|
||||
for key in sorted(family_stats):
|
||||
if key.startswith("specialization.failure_kinds"):
|
||||
continue
|
||||
if key.startswith("specialization."):
|
||||
with Section(name, 3, f"specialization stats for {name} family"):
|
||||
rows = []
|
||||
for key in sorted(family_stats):
|
||||
if key.startswith("specialization.failure_kinds"):
|
||||
continue
|
||||
if key.startswith("specialization."):
|
||||
label = key[len("specialization."):]
|
||||
elif key == "execution_count":
|
||||
label = "unquickened"
|
||||
else:
|
||||
label = key
|
||||
if key not in ("specialization.success", "specialization.failure", "specializable"):
|
||||
rows.append((f"{label:>12}", f"{family_stats[key]:>12}", f"{100*family_stats[key]/total:0.1f}%"))
|
||||
emit_table(("Kind", "Count", "Ratio"), rows)
|
||||
print_title("Specialization attempts", 4)
|
||||
total_attempts = 0
|
||||
for key in ("specialization.success", "specialization.failure"):
|
||||
total_attempts += family_stats.get(key, 0)
|
||||
rows = []
|
||||
for key in ("specialization.success", "specialization.failure"):
|
||||
label = key[len("specialization."):]
|
||||
elif key == "execution_count":
|
||||
label = "unquickened"
|
||||
if key not in ("specialization.success", "specialization.failure"):
|
||||
print(f"{label:>12}:{family_stats[key]:>12} {100*family_stats[key]/total:0.1f}%")
|
||||
for key in ("specialization.success", "specialization.failure"):
|
||||
label = key[len("specialization."):]
|
||||
print(f" {label}:{family_stats.get(key, 0):>12}")
|
||||
total_failures = family_stats.get("specialization.failure", 0)
|
||||
failure_kinds = [ 0 ] * 30
|
||||
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]
|
||||
for index, value in enumerate(failure_kinds):
|
||||
if not value:
|
||||
continue
|
||||
print(f" kind {index:>2}: {value:>8} {100*value/total_failures:0.1f}%")
|
||||
label = label[0].upper() + label[1:]
|
||||
val = family_stats.get(key, 0)
|
||||
rows.append((label, val, f"{100*val/total_attempts:0.1f}%"))
|
||||
emit_table(("", "Count", "Ratio"), rows)
|
||||
total_failures = family_stats.get("specialization.failure", 0)
|
||||
failure_kinds = [ 0 ] * 30
|
||||
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, f"{100*value/total_failures:0.1f}%"))
|
||||
emit_table(("Failure kind", "Count", "Ratio"), rows)
|
||||
|
||||
def gather_stats():
|
||||
stats = collections.Counter()
|
||||
|
@ -76,6 +93,31 @@ def extract_opcode_stats(stats):
|
|||
opcode_stats[int(n)][rest.strip(".")] = value
|
||||
return opcode_stats
|
||||
|
||||
def parse_kinds(spec_src):
|
||||
defines = collections.defaultdict(list)
|
||||
for line in spec_src:
|
||||
line = line.strip()
|
||||
if not line.startswith("#define SPEC_FAIL_"):
|
||||
continue
|
||||
line = line[len("#define SPEC_FAIL_"):]
|
||||
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 < 7:
|
||||
return pretty(defines[kind][0])
|
||||
if opname.endswith("ATTR"):
|
||||
opname = "ATTR"
|
||||
if 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):
|
||||
basic = 0
|
||||
|
@ -104,57 +146,131 @@ def categorized_counts(opcode_stats):
|
|||
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 emit_table(header, rows):
|
||||
width = len(header)
|
||||
print("|", " | ".join(header), "|")
|
||||
print("|", " | ".join(["---"]*width), "|")
|
||||
for row in rows:
|
||||
if width is not None and len(row) != width:
|
||||
raise ValueError("Wrong number of elements in row '" + str(rows) + "'")
|
||||
print("|", " | ".join(str(i) for i in row), "|")
|
||||
print()
|
||||
|
||||
def emit_execution_counts(opcode_stats, total):
|
||||
with Section("Execution counts", summary="execution counts for all instructions"):
|
||||
counts = []
|
||||
for i, opcode_stat in enumerate(opcode_stats):
|
||||
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, opname[i], miss))
|
||||
counts.sort(reverse=True)
|
||||
cumulative = 0
|
||||
rows = []
|
||||
for (count, name, miss) in counts:
|
||||
cumulative += count
|
||||
if miss:
|
||||
miss = f"{100*miss/count:0.1f}%"
|
||||
else:
|
||||
miss = ""
|
||||
rows.append((name, count, f"{100*count/total:0.1f}%",
|
||||
f"{100*cumulative/total:0.1f}%", miss))
|
||||
emit_table(
|
||||
("Name", "Count", "Self", "Cumulative", "Miss ratio"),
|
||||
rows
|
||||
)
|
||||
|
||||
|
||||
def emit_specialization_stats(opcode_stats):
|
||||
spec_path = os.path.join(os.path.dirname(__file__), "../../Python/specialize.c")
|
||||
with open(spec_path) as spec_src:
|
||||
defines = parse_kinds(spec_src)
|
||||
with Section("Specialization stats", summary="specialization stats by family"):
|
||||
for i, opcode_stat in enumerate(opcode_stats):
|
||||
name = opname[i]
|
||||
print_specialization_stats(name, opcode_stat, defines)
|
||||
|
||||
def emit_specialization_overview(opcode_stats, total):
|
||||
basic, not_specialized, specialized = categorized_counts(opcode_stats)
|
||||
with Section("Specialization effectiveness"):
|
||||
emit_table(("Instructions", "Count", "Ratio"), (
|
||||
("Basic", basic, f"{basic*100/total:0.1f}%"),
|
||||
("Not specialized", not_specialized, f"{not_specialized*100/total:0.1f}%"),
|
||||
("Specialized", specialized, f"{specialized*100/total:0.1f}%"),
|
||||
))
|
||||
|
||||
def emit_call_stats(stats):
|
||||
with Section("Call stats", summary="Inlined calls and frame stats"):
|
||||
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, f"{100*value/total:0.1f}%"))
|
||||
for key, value in stats.items():
|
||||
if key.startswith("Frame"):
|
||||
rows.append((key, value, f"{100*value/total:0.1f}%"))
|
||||
emit_table(("", "Count", "Ratio"), rows)
|
||||
|
||||
def emit_object_stats(stats):
|
||||
with Section("Object stats", summary="allocations, frees and dict materializatons"):
|
||||
total = stats.get("Object new values")
|
||||
rows = []
|
||||
for key, value in stats.items():
|
||||
if key.startswith("Object"):
|
||||
if "materialize" in key:
|
||||
materialize = f"{100*value/total:0.1f}%"
|
||||
else:
|
||||
materialize = ""
|
||||
label = key[6:].strip()
|
||||
label = label[0].upper() + label[1:]
|
||||
rows.append((label, value, materialize))
|
||||
emit_table(("", "Count", "Ratio"), rows)
|
||||
|
||||
def main():
|
||||
stats = gather_stats()
|
||||
opcode_stats = extract_opcode_stats(stats)
|
||||
print("Execution counts:")
|
||||
counts = []
|
||||
total = 0
|
||||
for i, opcode_stat in enumerate(opcode_stats):
|
||||
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, opname[i], miss))
|
||||
total += count
|
||||
counts.sort(reverse=True)
|
||||
cummulative = 0
|
||||
for (count, name, miss) in counts:
|
||||
cummulative += count
|
||||
print(f"{name}: {count} {100*count/total:0.1f}% {100*cummulative/total:0.1f}%")
|
||||
if miss:
|
||||
print(f" Misses: {miss} {100*miss/count:0.1f}%")
|
||||
print("Specialization stats:")
|
||||
for i, opcode_stat in enumerate(opcode_stats):
|
||||
name = opname[i]
|
||||
print_specialization_stats(name, opcode_stat)
|
||||
basic, not_specialized, specialized = categorized_counts(opcode_stats)
|
||||
print("Specialization effectiveness:")
|
||||
print(f" Base instructions {basic} {basic*100/total:0.1f}%")
|
||||
print(f" Not specialized {not_specialized} {not_specialized*100/total:0.1f}%")
|
||||
print(f" Specialized {specialized} {specialized*100/total:0.1f}%")
|
||||
print("Call stats:")
|
||||
total = 0
|
||||
for key, value in stats.items():
|
||||
if "Calls to" in key:
|
||||
total += value
|
||||
for key, value in stats.items():
|
||||
if "Calls to" in key:
|
||||
print(f" {key}: {value} {100*value/total:0.1f}%")
|
||||
for key, value in stats.items():
|
||||
if key.startswith("Frame"):
|
||||
print(f" {key}: {value} {100*value/total:0.1f}%")
|
||||
print("Object stats:")
|
||||
total = stats.get("Object new values")
|
||||
for key, value in stats.items():
|
||||
if key.startswith("Object"):
|
||||
if "materialize" in key:
|
||||
print(f" {key}: {value} {100*value/total:0.1f}%")
|
||||
else:
|
||||
print(f" {key}: {value}")
|
||||
total = 0
|
||||
|
||||
total += opcode_stat['execution_count']
|
||||
emit_execution_counts(opcode_stats, total)
|
||||
emit_specialization_stats(opcode_stats)
|
||||
emit_specialization_overview(opcode_stats, total)
|
||||
emit_call_stats(stats)
|
||||
emit_object_stats(stats)
|
||||
print("---")
|
||||
print("Stats gathered on:", date.today())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue