diff options
author | Michael Droettboom <mdboom@gmail.com> | 2023-10-24 08:57:39 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-24 08:57:39 (GMT) |
commit | 81eba7645082a192c027e739b8eb99a94b4c0eec (patch) | |
tree | 5ebcc78493a85b3e26cec45a651f188a9f8ff512 /Tools/scripts/summarize_stats.py | |
parent | 6b9babf140445ec2a560d1df056b795e898c6bd0 (diff) | |
download | cpython-81eba7645082a192c027e739b8eb99a94b4c0eec.zip cpython-81eba7645082a192c027e739b8eb99a94b4c0eec.tar.gz cpython-81eba7645082a192c027e739b8eb99a94b4c0eec.tar.bz2 |
gh-110019: Refactor summarize_stats (GH-110398)
Diffstat (limited to 'Tools/scripts/summarize_stats.py')
-rw-r--r-- | Tools/scripts/summarize_stats.py | 1832 |
1 files changed, 1041 insertions, 791 deletions
diff --git a/Tools/scripts/summarize_stats.py b/Tools/scripts/summarize_stats.py index bdca51d..071b24a 100644 --- a/Tools/scripts/summarize_stats.py +++ b/Tools/scripts/summarize_stats.py @@ -2,910 +2,1160 @@ default stats folders. """ +from __future__ import annotations + # NOTE: Bytecode introspection modules (opcode, dis, etc.) should only -# happen when loading a single dataset. When comparing datasets, it +# be imported 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 collections.abc import KeysView from datetime import date +import enum +import functools import itertools -import sys +import json +from operator import itemgetter +import os +from pathlib import Path import re +import sys +from typing import Any, Callable, TextIO, TypeAlias + + +RawData: TypeAlias = dict[str, Any] +Rows: TypeAlias = list[tuple] +Columns: TypeAlias = tuple[str, ...] +RowCalculator: TypeAlias = Callable[["Stats"], Rows] + + +# TODO: Check for parity + if os.name == "nt": DEFAULT_DIR = "c:\\temp\\py_stats\\" else: DEFAULT_DIR = "/tmp/py_stats/" + +SOURCE_DIR = Path(__file__).parents[2] + + 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 pretty(name: str) -> str: + return name.replace("_", " ").lower() -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 _load_metadata_from_source(): + def get_defines(filepath: Path, prefix: str = "SPEC_FAIL"): + with open(SOURCE_DIR / filepath) as spec_src: + 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 + + import opcode + + return { + "_specialized_instructions": [ + op for op in opcode._specialized_opmap.keys() if "__" not in op # type: ignore + ], + "_stats_defines": get_defines( + Path("Include") / "cpython" / "pystats.h", "EVAL_CALL" + ), + "_defines": get_defines(Path("Python") / "specialize.c"), + } + + +def load_raw_data(input: Path) -> RawData: + if input.is_file(): + with open(input, "r") as fd: + data = json.load(fd) + data["_stats_defines"] = {int(k): v for k, v in data["_stats_defines"].items()} + data["_defines"] = {int(k): v for k, v in data["_defines"].items()} -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 [] + return data - 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") + elif input.is_dir(): + stats = collections.Counter[str]() - 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") + for filename in input.iterdir(): + with open(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 + stats[key.strip()] += int(value) + stats["__nfiles__"] += 1 - if len(a_rows) and len(b_rows) and a_ncols[0] != b_ncols[0]: - raise ValueError("Tables have different widths") + data = dict(stats) + data.update(_load_metadata_from_source()) + return data - if len(a_rows): - ncols = a_ncols[0] else: - ncols = b_ncols[0] + raise ValueError(f"{input:r} is not a file or directory path") - 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") +def save_raw_data(data: RawData, json_output: TextIO): + json.dump(data, json_output) - # 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] +class OpcodeStats: + """ + Manages the data related to specific set of opcodes, e.g. tier1 (with prefix + "opcode") or tier2 (with prefix "uops"). + """ -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), - ) + def __init__(self, data: dict[str, Any], defines, specialized_instructions): + self._data = data + self._defines = defines + self._specialized_instructions = specialized_instructions + + def get_opcode_names(self) -> KeysView[str]: + return self._data.keys() + + def get_pair_counts(self) -> dict[tuple[str, str], int]: + pair_counts = {} + for name_i, opcode_stat in self._data.items(): + for key, value in opcode_stat.items(): + if value and key.startswith("pair_count"): + name_j, _, _ = key[len("pair_count") + 1 :].partition("]") + pair_counts[(name_i, name_j)] = value + return pair_counts + + def get_total_execution_count(self) -> int: + return sum(x.get("execution_count", 0) for x in self._data.values()) + + def get_execution_counts(self) -> dict[str, tuple[int, int]]: + counts = {} + for name, opcode_stat in self._data.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", 0) + counts[name] = (count, miss) + return counts + + @functools.cache + def _get_pred_succ( + self, + ) -> tuple[dict[str, collections.Counter], dict[str, collections.Counter]]: + pair_counts = self.get_pair_counts() + + predecessors: dict[str, collections.Counter] = collections.defaultdict( + collections.Counter ) - return rows + successors: dict[str, collections.Counter] = collections.defaultdict( + collections.Counter + ) + for (first, second), count in pair_counts.items(): + if count: + predecessors[second][first] = count + successors[first][second] = count + + return predecessors, successors + + def get_predecessors(self, opcode: str) -> collections.Counter[str]: + return self._get_pred_succ()[0][opcode] + + def get_successors(self, opcode: str) -> collections.Counter[str]: + return self._get_pred_succ()[1][opcode] + + def _get_stats_for_opcode(self, opcode: str) -> dict[str, int]: + return self._data[opcode] + + def get_specialization_total(self, opcode: str) -> int: + family_stats = self._get_stats_for_opcode(opcode) + return sum(family_stats.get(kind, 0) for kind in TOTAL) + + def get_specialization_counts(self, opcode: str) -> dict[str, int]: + family_stats = self._get_stats_for_opcode(opcode) + result = {} + for key, value in sorted(family_stats.items()): + if key.startswith("specialization."): + label = key[len("specialization.") :] + if label in ("success", "failure") or label.startswith("failure_kinds"): + continue + elif key in ( + "execution_count", + "specializable", + ) or key.startswith("pair"): + continue + else: + label = key + result[label] = value + + return result -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: + def get_specialization_success_failure(self, opcode: str) -> dict[str, int]: + family_stats = self._get_stats_for_opcode(opcode) + result = {} 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), - ) + result[label] = val + return result + + def get_specialization_failure_total(self, opcode: str) -> int: + return self._get_stats_for_opcode(opcode).get("specialization.failure", 0) + + def get_specialization_failure_kinds(self, opcode: str) -> dict[str, int]: + def kind_to_text(kind: int, opcode: str): + if kind <= 8: + return pretty(self._defines[kind][0]) + if opcode == "LOAD_SUPER_ATTR": + opcode = "SUPER" + elif opcode.endswith("ATTR"): + opcode = "ATTR" + elif opcode in ("FOR_ITER", "SEND"): + opcode = "ITER" + elif opcode.endswith("SUBSCR"): + opcode = "SUBSCR" + for name in self._defines[kind]: + if name.startswith(opcode): + return pretty(name[len(opcode) + 1 :]) + return "kind " + str(kind) + + family_stats = self._get_stats_for_opcode(opcode) + failure_kinds = [0] * 40 + for key in family_stats: + if not key.startswith("specialization.failure_kind"): + continue + index = int(key[:-1].split("[")[1]) + failure_kinds[index] = family_stats[key] + return { + kind_to_text(index, opcode): value + for (index, value) in enumerate(failure_kinds) + if value + } + def is_specializable(self, opcode: str) -> bool: + return "specializable" in self._get_stats_for_opcode(opcode) -def gather_stats(input): - # Note the output of this function must be JSON-serializable + def get_specialized_total_counts(self) -> tuple[int, int, int]: + basic = 0 + specialized = 0 + not_specialized = 0 + for opcode, opcode_stat in self._data.items(): + if "execution_count" not in opcode_stat: + continue + count = opcode_stat["execution_count"] + if "specializable" in opcode_stat: + not_specialized += count + elif opcode in self._specialized_instructions: + miss = opcode_stat.get("specialization.miss", 0) + not_specialized += miss + specialized += count - miss + else: + basic += count + return basic, specialized, not_specialized - if os.path.isfile(input): - with open(input, "r") as fd: - stats = json.load(fd) + def get_deferred_counts(self) -> dict[str, int]: + return { + opcode: opcode_stat.get("specialization.deferred", 0) + for opcode, opcode_stat in self._data.items() + } - stats["_stats_defines"] = { - int(k): v for k, v in stats["_stats_defines"].items() + def get_misses_counts(self) -> dict[str, int]: + return { + opcode: opcode_stat.get("specialization.miss", 0) + for opcode, opcode_stat in self._data.items() + if not self.is_specializable(opcode) } - 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 + def get_opcode_counts(self) -> dict[str, int]: + counts = {} + for opcode, entry in self._data.items(): + count = entry.get("count", 0) + if count: + counts[opcode] = count + return counts - 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() +class Stats: + def __init__(self, data: RawData): + self._data = data - return stats - else: - raise ValueError(f"{input:r} is not a file or directory path") + def get(self, key: str) -> int: + return self._data.get(key, 0) + @functools.cache + def get_opcode_stats(self, prefix: str) -> OpcodeStats: + opcode_stats = collections.defaultdict[str, dict](dict) + for key, value in self._data.items(): + if not key.startswith(prefix): + continue + name, _, rest = key[len(prefix) + 1 :].partition("]") + opcode_stats[name][rest.strip(".")] = value + return OpcodeStats( + opcode_stats, + self._data["_defines"], + self._data["_specialized_instructions"], + ) -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 + def get_call_stats(self) -> dict[str, int]: + defines = self._data["_stats_defines"] + result = {} + for key, value in sorted(self._data.items()): + if "Calls to" in key: + result[key] = value + elif key.startswith("Calls "): + name, index = key[:-1].split("[") + label = f"{name} ({pretty(defines[int(index)][0])})" + result[label] = value + + for key, value in sorted(self._data.items()): + if key.startswith("Frame"): + result[key] = value + + return result + + def get_object_stats(self) -> dict[str, tuple[int, int]]: + total_materializations = self._data.get("Object new values", 0) + total_allocations = self._data.get("Object allocations", 0) + self._data.get( + "Object allocations from freelist", 0 + ) + total_increfs = self._data.get( + "Object interpreter increfs", 0 + ) + self._data.get("Object increfs", 0) + total_decrefs = self._data.get( + "Object interpreter decrefs", 0 + ) + self._data.get("Object decrefs", 0) + + result = {} + for key, value in self._data.items(): + if key.startswith("Object"): + if "materialize" in key: + den = total_materializations + elif "allocations" in key: + den = total_allocations + elif "increfs" in key: + den = total_increfs + elif "decrefs" in key: + den = total_decrefs + else: + den = None + label = key[6:].strip() + label = label[0].upper() + label[1:] + result[label] = (value, den) + return result + + def get_gc_stats(self) -> list[dict[str, int]]: + gc_stats: list[dict[str, int]] = [] + for key, value in self._data.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 gc_stats + + def get_optimization_stats(self) -> dict[str, tuple[int, int | None]]: + if "Optimization attempts" not in self._data: + return {} + + attempts = self._data["Optimization attempts"] + created = self._data["Optimization traces created"] + executed = self._data["Optimization traces executed"] + uops = self._data["Optimization uops executed"] + trace_stack_overflow = self._data["Optimization trace stack overflow"] + trace_stack_underflow = self._data["Optimization trace stack underflow"] + trace_too_long = self._data["Optimization trace too long"] + trace_too_short = self._data["Optimization trace too short"] + inner_loop = self._data["Optimization inner loop"] + recursive_call = self._data["Optimization recursive call"] + + return { + "Optimization attempts": (attempts, None), + "Traces created": (created, attempts), + "Trace stack overflow": (trace_stack_overflow, attempts), + "Trace stack underflow": (trace_stack_underflow, attempts), + "Trace too long": (trace_too_long, attempts), + "Trace too short": (trace_too_short, attempts), + "Inner loop found": (inner_loop, attempts), + "Recursive call": (recursive_call, attempts), + "Traces executed": (executed, None), + "Uops executed": (uops, executed), + } + + def get_histogram(self, prefix: str) -> list[tuple[int, int]]: + rows = [] + for k, v in self._data.items(): + match = re.match(f"{prefix}\\[([0-9]+)\\]", k) + if match is not None: + entry = int(match.groups()[0]) + rows.append((entry, v)) + rows.sort() + return rows + + +class Count(int): + def markdown(self) -> str: + return format(self, ",d") + + +class Ratio: + def __init__(self, num: int, den: int | None, percentage: bool = True): + self.num = num + self.den = den + self.percentage = percentage + if den == 0 and num != 0: + raise ValueError("Invalid denominator") + + def __float__(self): + if self.den == 0: + return 0.0 + elif self.den is None: + return self.num + else: + return self.num / self.den + + def markdown(self) -> str: + if self.den == 0 or self.den is None: + return "" + elif self.percentage: + return f"{self.num / self.den:,.01%}" + else: + return f"{self.num / self.den:,.02f}" + + +class DiffRatio(Ratio): + def __init__(self, base: int | str, head: int | str): + if isinstance(base, str) or isinstance(head, str): + super().__init__(0, 0) else: - basic += count - return basic, not_specialized, specialized + super().__init__(head - base, base) + + +class JoinMode(enum.Enum): + # Join using the first column as a key + SIMPLE = 0 + # Join using the first column as a key, and indicate the change in the + # second column of each input table as a new column + CHANGE = 1 + # Join using the first column as a key, indicating the change in the second + # column of each input table as a ne column, and omit all other columns + CHANGE_ONE_COLUMN = 2 + + +class Table: + """ + A Table defines how to convert a set of Stats into a specific set of rows + displaying some aspect of the data. + """ + + def __init__( + self, + column_names: Columns, + calc_rows: RowCalculator, + join_mode: JoinMode = JoinMode.SIMPLE, + ): + self.columns = column_names + self.calc_rows = calc_rows + self.join_mode = join_mode + + def join_row(self, key: str, row_a: tuple, row_b: tuple) -> tuple: + match self.join_mode: + case JoinMode.SIMPLE: + return (key, *row_a, *row_b) + case JoinMode.CHANGE: + return (key, *row_a, *row_b, DiffRatio(row_a[0], row_b[0])) + case JoinMode.CHANGE_ONE_COLUMN: + return (key, row_a[0], row_b[0], DiffRatio(row_a[0], row_b[0])) + + def join_columns(self, columns: Columns) -> Columns: + match self.join_mode: + case JoinMode.SIMPLE: + return ( + columns[0], + *("Base " + x for x in columns[1:]), + *("Head " + x for x in columns[1:]), + ) + case JoinMode.CHANGE: + return ( + columns[0], + *("Base " + x for x in columns[1:]), + *("Head " + x for x in columns[1:]), + ) + ("Change:",) + case JoinMode.CHANGE_ONE_COLUMN: + return ( + columns[0], + "Base " + columns[1], + "Head " + columns[1], + "Change:", + ) + + def join_tables(self, rows_a: Rows, rows_b: Rows) -> tuple[Columns, Rows]: + ncols = len(self.columns) + + default = ("",) * (ncols - 1) + data_a = {x[0]: x[1:] for x in rows_a} + data_b = {x[0]: x[1:] for x in rows_b} + if len(data_a) != len(rows_a) or len(data_b) != len(rows_b): + raise ValueError("Duplicate keys") -def print_title(name, level=2): - print("#" * level, name) - print() + # To preserve ordering, use A's keys as is and then add any in B that + # aren't in A + keys = list(data_a.keys()) + [k for k in data_b.keys() if k not in data_a] + rows = [ + self.join_row(k, data_a.get(k, default), data_b.get(k, default)) + for k in keys + ] + if self.join_mode in (JoinMode.CHANGE, JoinMode.CHANGE_ONE_COLUMN): + rows.sort(key=lambda row: abs(float(row[-1])), reverse=True) + + columns = self.join_columns(self.columns) + return columns, rows + + def get_table( + self, base_stats: Stats, head_stats: Stats | None = None + ) -> tuple[Columns, Rows]: + if head_stats is None: + rows = self.calc_rows(base_stats) + return self.columns, rows + else: + rows_a = self.calc_rows(base_stats) + rows_b = self.calc_rows(head_stats) + cols, rows = self.join_tables(rows_a, rows_b) + return cols, rows class Section: - def __init__(self, title, level=2, summary=None): + """ + A Section defines a section of the output document. + """ + + def __init__( + self, + title: str = "", + summary: str = "", + part_iter=None, + comparative: bool = True, + ): self.title = title - self.level = level - if summary is None: + if not summary: self.summary = title.lower() else: self.summary = summary + if part_iter is None: + part_iter = [] + if isinstance(part_iter, list): - def __enter__(self): - print_title(self.title, self.level) - print("<details>") - print("<summary>", self.summary, "</summary>") - print() - return self + def iter_parts(base_stats: Stats, head_stats: Stats | None): + yield from part_iter - def __exit__(*args): - print() - print("</details>") - print() + self.part_iter = iter_parts + else: + self.part_iter = part_iter + self.comparative = comparative -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, +def calc_execution_count_table(prefix: str) -> RowCalculator: + def calc(stats: Stats) -> Rows: + opcode_stats = stats.get_opcode_stats(prefix) + counts = opcode_stats.get_execution_counts() + total = opcode_stats.get_total_execution_count() + cumulative = 0 + rows: Rows = [] + for opcode, (count, miss) in sorted( + counts.items(), key=itemgetter(1), reverse=True + ): + cumulative += count + if miss: + miss_val = Ratio(miss, count) + else: + miss_val = None + rows.append( + ( + opcode, + Count(count), + Ratio(count, total), + Ratio(cumulative, total), + miss_val, + ) ) - ) - return rows + return rows + return calc -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 execution_count_section() -> Section: + return Section( + "Execution counts", + "execution counts for all instructions", + [ + Table( + ("Name", "Count:", "Self:", "Cumulative:", "Miss ratio:"), + calc_execution_count_table("opcode"), + join_mode=JoinMode.CHANGE_ONE_COLUMN, + ) + ], + ) -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%}")) +def pair_count_section() -> Section: + def calc_pair_count_table(stats: Stats) -> Rows: + opcode_stats = stats.get_opcode_stats("opcode") + pair_counts = opcode_stats.get_pair_counts() + total = opcode_stats.get_total_execution_count() - rows.sort(key=lambda x: abs(percentage_to_float(x[-1])), reverse=True) + cumulative = 0 + rows: Rows = [] + for (opcode_i, opcode_j), count in itertools.islice( + sorted(pair_counts.items(), key=itemgetter(1), reverse=True), 100 + ): + cumulative += count + rows.append( + ( + f"{opcode_i} {opcode_j}", + Count(count), + Ratio(count, total), + Ratio(cumulative, total), + ) + ) + return rows + + return Section( + "Pair counts", + "Pair counts for top 100 pairs", + [ + Table( + ("Pair", "Count:", "Self:", "Cumulative:"), + calc_pair_count_table, + ) + ], + comparative=False, + ) - emit_table(("Name", "Base Count:", "Head Count:", "Change:"), rows) +def pre_succ_pairs_section() -> Section: + def iter_pre_succ_pairs_tables(base_stats: Stats, head_stats: Stats | None = None): + assert head_stats is None -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) + opcode_stats = base_stats.get_opcode_stats("opcode") + for opcode in opcode_stats.get_opcode_names(): + predecessors = opcode_stats.get_predecessors(opcode) + successors = opcode_stats.get_successors(opcode) + predecessors_total = predecessors.total() + successors_total = successors.total() + if predecessors_total == 0 and successors_total == 0: + continue + pred_rows = [ + (pred, Count(count), Ratio(count, predecessors_total)) + for (pred, count) in predecessors.most_common(5) + ] + succ_rows = [ + (succ, Count(count), Ratio(count, successors_total)) + for (succ, count) in successors.most_common(5) + ] + + yield Section( + opcode, + f"Successors and predecessors for {opcode}", + [ + Table( + ("Predecessors", "Count:", "Percentage:"), + lambda *_: pred_rows, # type: ignore + ), + Table( + ("Successors", "Count:", "Percentage:"), + lambda *_: succ_rows, # type: ignore + ), + ], + ) -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 + return Section( + "Predecessor/Successor Pairs", + "Top 5 predecessors and successors of each opcode", + iter_pre_succ_pairs_tables, + comparative=False, + ) -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 specialization_section() -> Section: + def calc_specialization_table(opcode: str) -> RowCalculator: + def calc(stats: Stats) -> Rows: + opcode_stats = stats.get_opcode_stats("opcode") + total = opcode_stats.get_specialization_total(opcode) + specialization_counts = opcode_stats.get_specialization_counts(opcode) + return [ + ( + f"{label:>12}", + Count(count), + Ratio(count, total), + ) + for label, count in specialization_counts.items() + ] -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 + return calc + + def calc_specialization_success_failure_table(name: str) -> RowCalculator: + def calc(stats: Stats) -> Rows: + values = stats.get_opcode_stats( + "opcode" + ).get_specialization_success_failure(name) + total = sum(values.values()) + if total: + return [ + (label.capitalize(), Count(val), Ratio(val, total)) + for label, val in values.items() + ] + else: + return [] + + return calc + + def calc_specialization_failure_kind_table(name: str) -> RowCalculator: + def calc(stats: Stats) -> Rows: + opcode_stats = stats.get_opcode_stats("opcode") + failures = opcode_stats.get_specialization_failure_kinds(name) + total = opcode_stats.get_specialization_failure_total(name) + + return sorted( + [ + (label, Count(value), Ratio(value, total)) + for label, value in failures.items() + if value + ], + key=itemgetter(1), + reverse=True, ) + return calc + + def iter_specialization_tables(base_stats: Stats, head_stats: Stats | None = None): + opcode_base_stats = base_stats.get_opcode_stats("opcode") + names = opcode_base_stats.get_opcode_names() + if head_stats is not None: + opcode_head_stats = head_stats.get_opcode_stats("opcode") + names &= opcode_head_stats.get_opcode_names() # type: ignore + else: + opcode_head_stats = None -def calculate_specialization_effectiveness( - opcode_stats, total, specialized_instructions -): - basic, not_specialized, specialized = categorized_counts( - opcode_stats, specialized_instructions + for opcode in sorted(names): + if not opcode_base_stats.is_specializable(opcode): + continue + if opcode_base_stats.get_specialization_total(opcode) == 0 and ( + opcode_head_stats is None + or opcode_head_stats.get_specialization_total(opcode) == 0 + ): + continue + yield Section( + opcode, + f"specialization stats for {opcode} family", + [ + Table( + ("Kind", "Count:", "Ratio:"), + calc_specialization_table(opcode), + JoinMode.CHANGE, + ), + Table( + ("", "Count:", "Ratio:"), + calc_specialization_success_failure_table(opcode), + JoinMode.CHANGE, + ), + Table( + ("Failure kind", "Count:", "Ratio:"), + calc_specialization_failure_kind_table(opcode), + JoinMode.CHANGE, + ), + ], + ) + + return Section( + "Specialization stats", + "specialization stats by family", + iter_specialization_tables, ) - 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( +def specialization_effectiveness_section() -> Section: + def calc_specialization_effectiveness_table(stats: Stats) -> Rows: + opcode_stats = stats.get_opcode_stats("opcode") + total = opcode_stats.get_total_execution_count() + + ( + basic, + specialized, + not_specialized, + ) = opcode_stats.get_specialized_total_counts() + + return [ + ("Basic", Count(basic), Ratio(basic, total)), ( - "Instructions", - "Base Count:", - "Base Ratio:", - "Head Count:", - "Head Ratio:", + "Not specialized", + Count(not_specialized), + Ratio(not_specialized, total), ), - join_rows(base_rows, head_rows), - ) + ("Specialized", Count(specialized), Ratio(specialized, total)), + ] + + def calc_deferred_by_table(stats: Stats) -> Rows: + opcode_stats = stats.get_opcode_stats("opcode") + deferred_counts = opcode_stats.get_deferred_counts() + total = sum(deferred_counts.values()) + if total == 0: + return [] + + return [ + (name, Count(value), Ratio(value, total)) + for name, value in sorted( + deferred_counts.items(), key=itemgetter(1), reverse=True + )[:10] + ] + def calc_misses_by_table(stats: Stats) -> Rows: + opcode_stats = stats.get_opcode_stats("opcode") + misses_counts = opcode_stats.get_misses_counts() + total = sum(misses_counts.values()) + if total == 0: + return [] + + return [ + (name, Count(value), Ratio(value, total)) + for name, value in sorted( + misses_counts.items(), key=itemgetter(1), reverse=True + )[:10] + ] -def get_stats_defines(): - stats_path = os.path.join( - os.path.dirname(__file__), "../../Include/cpython/pystats.h" + return Section( + "Specialization effectiveness", + "", + [ + Table( + ("Instructions", "Count:", "Ratio:"), + calc_specialization_effectiveness_table, + JoinMode.CHANGE, + ), + Section( + "Deferred by instruction", + "", + [ + Table( + ("Name", "Count:", "Ratio:"), + calc_deferred_by_table, + JoinMode.CHANGE, + ) + ], + ), + Section( + "Misses by instruction", + "", + [ + Table( + ("Name", "Count:", "Ratio:"), + calc_misses_by_table, + JoinMode.CHANGE, + ) + ], + ), + ], ) - 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" +def call_stats_section() -> Section: + def calc_call_stats_table(stats: Stats) -> Rows: + call_stats = stats.get_call_stats() + total = sum(v for k, v in call_stats.items() if "Calls to" in k) + return [ + (key, Count(value), Ratio(value, total)) + for key, value in call_stats.items() + ] + + return Section( + "Call stats", + "Inlined calls and frame stats", + [ + Table( + ("", "Count:", "Ratio:"), + calc_call_stats_table, + JoinMode.CHANGE, + ) + ], ) - 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 object_stats_section() -> Section: + def calc_object_stats_table(stats: Stats) -> Rows: + object_stats = stats.get_object_stats() + return [ + (label, Count(value), Ratio(value, den)) + for label, (value, den) in object_stats.items() + ] + return Section( + "Object stats", + "allocations, frees and dict materializatons", + [ + Table( + ("", "Count:", "Ratio:"), + calc_object_stats_table, + JoinMode.CHANGE, + ) + ], + ) -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 gc_stats_section() -> Section: + def calc_gc_stats(stats: Stats) -> Rows: + gc_stats = stats.get_gc_stats() -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), - ) + return [ + ( + Count(i), + Count(gen["collections"]), + Count(gen["objects collected"]), + Count(gen["object visits"]), ) - 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 + for (i, gen) in enumerate(gc_stats) + ] + return Section( + "GC stats", + "GC collections and effectiveness", + [ + Table( + ("Generation:", "Collections:", "Objects collected:", "Object visits:"), + calc_gc_stats, + ) + ], + ) -def emit_optimization_stats(stats): - if "Optimization attempts" not in stats: - return - uop_stats = extract_opcode_stats(stats, "uops") +def optimization_section() -> Section: + def calc_optimization_table(stats: Stats) -> Rows: + optimization_stats = stats.get_optimization_stats() - 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"], + return [ + ( + label, + Count(value), + Ratio(value, den, percentage=label != "Uops executed"), + ) + for label, (value, den) in optimization_stats.items() + ] + + def calc_histogram_table(key: str, den: str) -> RowCalculator: + def calc(stats: Stats) -> Rows: + histogram = stats.get_histogram(key) + denominator = stats.get(den) + + rows: Rows = [] + last_non_zero = 0 + for k, v in histogram: + if v != 0: + last_non_zero = len(rows) + rows.append( + ( + f"<= {k:,d}", + Count(v), + Ratio(v, denominator), + ) + ) + # Don't include any zero entries at the end + rows = rows[: last_non_zero + 1] + return rows + + return calc + + def calc_unsupported_opcodes_table(stats: Stats) -> Rows: + unsupported_opcodes = stats.get_opcode_stats("unsupported_opcode") + return sorted( + [ + (opcode, Count(count)) + for opcode, count in unsupported_opcodes.get_opcode_counts().items() + ], + key=itemgetter(1), + reverse=True, ) - emit_histogram( - "Optimized trace length histogram", - stats, - "Optimized trace length", - stats["Optimization traces created"], + + def iter_optimization_tables(base_stats: Stats, head_stats: Stats | None = None): + if not base_stats.get_optimization_stats() or ( + head_stats is not None and not head_stats.get_optimization_stats() + ): + return + + yield Table(("", "Count:", "Ratio:"), calc_optimization_table, JoinMode.CHANGE) + for name, den in [ + ("Trace length", "Optimization traces created"), + ("Optimized trace length", "Optimization traces created"), + ("Trace run length", "Optimization traces executed"), + ]: + yield Section( + f"{name} histogram", + "", + [ + Table( + ("Range", "Count:", "Ratio:"), + calc_histogram_table(name, den), + JoinMode.CHANGE, + ) + ], + ) + yield Section( + "Uop stats", + "", + [ + Table( + ("Name", "Count:", "Self:", "Cumulative:", "Miss ratio:"), + calc_execution_count_table("uops"), + JoinMode.CHANGE_ONE_COLUMN, + ) + ], ) - emit_histogram( - "Trace run length histogram", - stats, - "Trace run length", - stats["Optimization traces executed"], + yield Section( + "Unsupported opcodes", + "", + [ + Table( + ("Opcode", "Count:"), + calc_unsupported_opcodes_table, + JoinMode.CHANGE, + ) + ], ) - 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) - + return Section( + "Optimization (Tier 2) stats", + "statistics about the Tier 2 optimizer", + iter_optimization_tables, + ) -def emit_comparative_optimization_stats(base_stats, head_stats): - print("## Comparative optimization stats not implemented\n\n") +def meta_stats_section() -> Section: + def calc_rows(stats: Stats) -> Rows: + return [("Number of data files", Count(stats.get("__nfiles__")))] -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"] + return Section( + "Meta stats", + "Meta statistics", + [Table(("", "Count:"), calc_rows, JoinMode.CHANGE)], ) - 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()) +LAYOUT = [ + execution_count_section(), + pair_count_section(), + pre_succ_pairs_section(), + specialization_section(), + specialization_effectiveness_section(), + call_stats_section(), + object_stats_section(), + gc_stats_section(), + optimization_section(), + meta_stats_section(), +] + + +def output_markdown( + out: TextIO, + obj: Section | Table | list, + base_stats: Stats, + head_stats: Stats | None = None, + level: int = 2, +) -> None: + def to_markdown(x): + if hasattr(x, "markdown"): + return x.markdown() + elif isinstance(x, str): + return x + elif x is None: + return "" + else: + raise TypeError(f"Can't convert {x} to markdown") + + match obj: + case Section(): + if obj.title: + print("#" * level, obj.title, file=out) + print(file=out) + print("<details>", file=out) + print("<summary>", obj.summary, "</summary>", file=out) + print(file=out) + if head_stats is not None and obj.comparative is False: + print("Not included in comparative output.\n") + else: + for part in obj.part_iter(base_stats, head_stats): + output_markdown(out, part, base_stats, head_stats, level=level + 1) + print(file=out) + if obj.title: + print("</details>", file=out) + print(file=out) + + case Table(): + header, rows = obj.get_table(base_stats, head_stats) + if len(rows) == 0: + return + + 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, file=out) + print(under_line, file=out) + for row in rows: + if len(row) != width: + raise ValueError( + "Wrong number of elements in row '" + str(row) + "'" + ) + print("|", " | ".join(to_markdown(i) for i in row), "|", file=out) + print(file=out) + + case list(): + for part in obj: + output_markdown(out, part, base_stats, head_stats, level=level) + + print("---", file=out) + print("Stats gathered on:", date.today(), file=out) + + +def output_stats(inputs: list[Path], json_output=TextIO | None): + match len(inputs): + case 1: + data = load_raw_data(Path(inputs[0])) + if json_output is not None: + save_raw_data(data, json_output) # type: ignore + stats = Stats(data) + output_markdown(sys.stdout, LAYOUT, stats) + case 2: + if json_output is not None: + raise ValueError( + "Can not output to JSON when there are multiple inputs" + ) + base_data = load_raw_data(Path(inputs[0])) + head_data = load_raw_data(Path(inputs[1])) + base_stats = Stats(base_data) + head_stats = Stats(head_data) + output_markdown(sys.stdout, LAYOUT, base_stats, head_stats) def main(): |