gh-115362: Add documentation to pystats output (#115365)

This commit is contained in:
Michael Droettboom 2024-02-16 12:06:07 -05:00 committed by GitHub
parent 2ac9d9f2fb
commit fbb0169731
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -11,6 +11,7 @@
import argparse
import collections
from collections.abc import KeysView
from dataclasses import dataclass
from datetime import date
import enum
import functools
@ -21,6 +22,7 @@
from pathlib import Path
import re
import sys
import textwrap
from typing import Any, Callable, TextIO, TypeAlias
@ -115,6 +117,64 @@ def save_raw_data(data: RawData, json_output: TextIO):
json.dump(data, json_output)
@dataclass(frozen=True)
class Doc:
text: str
doc: str
def markdown(self) -> str:
return textwrap.dedent(
f"""
{self.text}
<details>
<summary></summary>
{self.doc}
</details>
"""
)
class Count(int):
def markdown(self) -> str:
return format(self, ",d")
@dataclass(frozen=True)
class Ratio:
num: int
den: int | None = None
percentage: bool = True
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 is None:
return ""
elif self.den == 0:
if self.num != 0:
return f"{self.num:,} / 0 !!"
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:
super().__init__(head - base, base)
class OpcodeStats:
"""
Manages the data related to specific set of opcodes, e.g. tier1 (with prefix
@ -389,17 +449,54 @@ def get_optimization_stats(self) -> dict[str, tuple[int, int | None]]:
low_confidence = self._data["Optimization low confidence"]
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),
"Low confidence": (low_confidence, attempts),
"Traces executed": (executed, None),
"Uops executed": (uops, executed),
Doc(
"Optimization attempts",
"The number of times a potential trace is identified. Specifically, this "
"occurs in the JUMP BACKWARD instruction when the counter reaches a "
"threshold.",
): (
attempts,
None,
),
Doc(
"Traces created", "The number of traces that were successfully created."
): (created, attempts),
Doc(
"Trace stack overflow",
"A trace is truncated because it would require more than 5 stack frames.",
): (trace_stack_overflow, attempts),
Doc(
"Trace stack underflow",
"A potential trace is abandoned because it pops more frames than it pushes.",
): (trace_stack_underflow, attempts),
Doc(
"Trace too long",
"A trace is truncated because it is longer than the instruction buffer.",
): (trace_too_long, attempts),
Doc(
"Trace too short",
"A potential trace is abandoced because it it too short.",
): (trace_too_short, attempts),
Doc(
"Inner loop found", "A trace is truncated because it has an inner loop"
): (inner_loop, attempts),
Doc(
"Recursive call",
"A trace is truncated because it has a recursive call.",
): (recursive_call, attempts),
Doc(
"Low confidence",
"A trace is abandoned because the likelihood of the jump to top being taken "
"is too low.",
): (low_confidence, attempts),
Doc("Traces executed", "The number of traces that were executed"): (
executed,
None,
),
Doc("Uops executed", "The total number of uops (micro-operations) that were executed"): (
uops,
executed,
),
}
def get_histogram(self, prefix: str) -> list[tuple[int, int]]:
@ -415,52 +512,12 @@ def get_histogram(self, prefix: str) -> list[tuple[int, int]]:
def get_rare_events(self) -> list[tuple[str, int]]:
prefix = "Rare event "
return [
(key[len(prefix) + 1:-1].replace("_", " "), val)
(key[len(prefix) + 1 : -1].replace("_", " "), val)
for key, val in self._data.items()
if key.startswith(prefix)
]
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
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 is None:
return ""
elif self.den == 0:
if self.num != 0:
return f"{self.num:,} / 0 !!"
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:
super().__init__(head - base, base)
class JoinMode(enum.Enum):
# Join using the first column as a key
SIMPLE = 0
@ -568,13 +625,16 @@ def __init__(
title: str = "",
summary: str = "",
part_iter=None,
*,
comparative: bool = True,
doc: str = "",
):
self.title = title
if not summary:
self.summary = title.lower()
else:
self.summary = summary
self.doc = textwrap.dedent(doc)
if part_iter is None:
part_iter = []
if isinstance(part_iter, list):
@ -620,7 +680,7 @@ def calc(stats: Stats) -> Rows:
def execution_count_section() -> Section:
return Section(
"Execution counts",
"execution counts for all instructions",
"Execution counts for Tier 1 instructions.",
[
Table(
("Name", "Count:", "Self:", "Cumulative:", "Miss ratio:"),
@ -628,6 +688,11 @@ def execution_count_section() -> Section:
join_mode=JoinMode.CHANGE_ONE_COLUMN,
)
],
doc="""
The "miss ratio" column shows the percentage of times the instruction
executed that it deoptimized. When this happens, the base unspecialized
instruction is not counted.
""",
)
@ -655,7 +720,7 @@ def calc_pair_count_table(stats: Stats) -> Rows:
return Section(
"Pair counts",
"Pair counts for top 100 pairs",
"Pair counts for top 100 Tier 1 instructions",
[
Table(
("Pair", "Count:", "Self:", "Cumulative:"),
@ -663,6 +728,10 @@ def calc_pair_count_table(stats: Stats) -> Rows:
)
],
comparative=False,
doc="""
Pairs of specialized operations that deoptimize and are then followed by
the corresponding unspecialized instruction are not counted as pairs.
""",
)
@ -705,22 +774,33 @@ def iter_pre_succ_pairs_tables(base_stats: Stats, head_stats: Stats | None = Non
return Section(
"Predecessor/Successor Pairs",
"Top 5 predecessors and successors of each opcode",
"Top 5 predecessors and successors of each Tier 1 opcode.",
iter_pre_succ_pairs_tables,
comparative=False,
doc="""
This does not include the unspecialized instructions that occur after a
specialized instruction deoptimizes.
""",
)
def specialization_section() -> Section:
def calc_specialization_table(opcode: str) -> RowCalculator:
def calc(stats: Stats) -> Rows:
DOCS = {
"deferred": 'Lists the number of "deferred" (i.e. not specialized) instructions executed.',
"hit": "Specialized instructions that complete.",
"miss": "Specialized instructions that deopt.",
"deopt": "Specialized instructions that deopt.",
}
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}",
Doc(label, DOCS[label]),
Count(count),
Ratio(count, total),
)
@ -790,7 +870,7 @@ def iter_specialization_tables(base_stats: Stats, head_stats: Stats | None = Non
JoinMode.CHANGE,
),
Table(
("", "Count:", "Ratio:"),
("Success", "Count:", "Ratio:"),
calc_specialization_success_failure_table(opcode),
JoinMode.CHANGE,
),
@ -804,7 +884,7 @@ def iter_specialization_tables(base_stats: Stats, head_stats: Stats | None = Non
return Section(
"Specialization stats",
"specialization stats by family",
"Specialization stats by family",
iter_specialization_tables,
)
@ -822,19 +902,35 @@ def calc_specialization_effectiveness_table(stats: Stats) -> Rows:
) = opcode_stats.get_specialized_total_counts()
return [
("Basic", Count(basic), Ratio(basic, total)),
(
"Not specialized",
Doc(
"Basic",
"Instructions that are not and cannot be specialized, e.g. `LOAD_FAST`.",
),
Count(basic),
Ratio(basic, total),
),
(
Doc(
"Not specialized",
"Instructions that could be specialized but aren't, e.g. `LOAD_ATTR`, `BINARY_SLICE`.",
),
Count(not_specialized),
Ratio(not_specialized, total),
),
(
"Specialized hits",
Doc(
"Specialized hits",
"Specialized instructions, e.g. `LOAD_ATTR_MODULE` that complete.",
),
Count(specialized_hits),
Ratio(specialized_hits, total),
),
(
"Specialized misses",
Doc(
"Specialized misses",
"Specialized instructions, e.g. `LOAD_ATTR_MODULE` that deopt.",
),
Count(specialized_misses),
Ratio(specialized_misses, total),
),
@ -879,7 +975,7 @@ def calc_misses_by_table(stats: Stats) -> Rows:
),
Section(
"Deferred by instruction",
"",
"Breakdown of deferred (not specialized) instruction counts by family",
[
Table(
("Name", "Count:", "Ratio:"),
@ -890,7 +986,7 @@ def calc_misses_by_table(stats: Stats) -> Rows:
),
Section(
"Misses by instruction",
"",
"Breakdown of misses (specialized deopts) instruction counts by family",
[
Table(
("Name", "Count:", "Ratio:"),
@ -900,6 +996,10 @@ def calc_misses_by_table(stats: Stats) -> Rows:
],
),
],
doc="""
All entries are execution counts. Should add up to the total number of
Tier 1 instructions executed.
""",
)
@ -922,6 +1022,13 @@ def calc_call_stats_table(stats: Stats) -> Rows:
JoinMode.CHANGE,
)
],
doc="""
This shows what fraction of calls to Python functions are inlined (i.e.
not having a call at the C level) and for those that are not, where the
call comes from. The various categories overlap.
Also includes the count of frame objects created.
""",
)
@ -935,7 +1042,7 @@ def calc_object_stats_table(stats: Stats) -> Rows:
return Section(
"Object stats",
"allocations, frees and dict materializatons",
"Allocations, frees and dict materializatons",
[
Table(
("", "Count:", "Ratio:"),
@ -943,6 +1050,16 @@ def calc_object_stats_table(stats: Stats) -> Rows:
JoinMode.CHANGE,
)
],
doc="""
Below, "allocations" means "allocations that are not from a freelist".
Total allocations = "Allocations from freelist" + "Allocations".
"New values" is the number of values arrays created for objects with
managed dicts.
The cache hit/miss numbers are for the MRO cache, split into dunder and
other names.
""",
)
@ -969,6 +1086,9 @@ def calc_gc_stats(stats: Stats) -> Rows:
calc_gc_stats,
)
],
doc="""
Collected/visits gives some measure of efficiency.
""",
)
@ -1074,7 +1194,19 @@ def iter_optimization_tables(base_stats: Stats, head_stats: Stats | None = None)
def rare_event_section() -> Section:
def calc_rare_event_table(stats: Stats) -> Table:
return [(x, Count(y)) for x, y in stats.get_rare_events()]
DOCS = {
"set class": "Setting an object's class, `obj.__class__ = ...`",
"set bases": "Setting the bases of a class, `cls.__bases__ = ...`",
"set eval frame func": (
"Setting the PEP 523 frame eval function "
"`_PyInterpreterState_SetFrameEvalFunc()`"
),
"builtin dict": "Modifying the builtins, `__builtins__.__dict__[var] = ...`",
"func modification": "Modifying a function, e.g. `func.__defaults__ = ...`, etc.",
"watched dict modification": "A watched dict has been modified",
"watched globals modification": "A watched `globals()` dict has been modified",
}
return [(Doc(x, DOCS[x]), Count(y)) for x, y in stats.get_rare_events()]
return Section(
"Rare events",
@ -1134,6 +1266,9 @@ def to_markdown(x):
print("<details>", file=out)
print("<summary>", obj.summary, "</summary>", file=out)
print(file=out)
if obj.doc:
print(obj.doc, file=out)
if head_stats is not None and obj.comparative is False:
print("Not included in comparative output.\n")
else:
@ -1149,24 +1284,36 @@ def to_markdown(x):
if len(rows) == 0:
return
width = len(header)
header_line = "|"
under_line = "|"
alignments = []
for item in header:
under = "---"
if item.endswith(":"):
alignments.append("right")
else:
alignments.append("left")
print("<table>", file=out)
print("<thead>", file=out)
print("<tr>", file=out)
for item, align in zip(header, alignments):
if item.endswith(":"):
item = item[:-1]
under += ":"
header_line += item + " | "
under_line += under + "|"
print(header_line, file=out)
print(under_line, file=out)
print(f'<th align="{align}">{item}</th>', file=out)
print("</tr>", file=out)
print("</thead>", file=out)
print("<tbody>", file=out)
for row in rows:
if len(row) != width:
if len(row) != len(header):
raise ValueError(
"Wrong number of elements in row '" + str(row) + "'"
)
print("|", " | ".join(to_markdown(i) for i in row), "|", file=out)
print("<tr>", file=out)
for col, align in zip(row, alignments):
print(f'<td align="{align}">{to_markdown(col)}</td>', file=out)
print("</tr>", file=out)
print("</tbody>", file=out)
print("</table>", file=out)
print(file=out)
case list():