summaryrefslogtreecommitdiff
path: root/build-support/spill-matrix.py
blob: 77c617a13c52bca0fb0248f3c36e0c97d67747cd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#!/usr/bin/env python3
"""Measure allocator chapters, check cohort membership, and render scaled spills."""
import argparse
from collections import Counter, defaultdict
from pathlib import Path
import re
import subprocess
import sys

ROOT = Path(__file__).resolve().parent.parent
CHAPTERS = range(20, 26)
START = "<!-- spill-matrix:start -->"
END = "<!-- spill-matrix:end -->"


def read_log(path):
    data = path.read_bytes()
    return data.decode("utf-16" if data.startswith((b"\xff\xfe", b"\xfe\xff")) else "utf-8")


def read_results(log_dir):
    results = {}
    for chapter in CHAPTERS:
        path = log_dir / f"spill-matrix-{chapter}.log"
        text = read_log(path)
        if re.search(r"FAILED|(?:[Ff]ailures|Spill golden failures): [1-9]", text):
            raise ValueError(f"Chapter {chapter} failed; see {path}")
        if not re.search(r"(?:Tests:|Historical allocations:).*failures: 0", text):
            raise ValueError(f"Chapter {chapter} has no successful completion record")
        cohorts = defaultdict(list)
        sums = {}
        for line in text.splitlines():
            if line.startswith("allocation,"):
                _, cohort, test, cpu, abi, moves, scaled = line.split(",")
                cohort = int(cohort.removeprefix("Chapter"))
                # These two Chapter 20 programs moved into Chapter20Test in 21.
                if cohort == 20 and test in ("BrainFuckTest.testBrainfuck", "MergeSortTest.testMergeSort"):
                    test = "Chapter20Test." + test.split(".")[1]
                abi = "win64" if abi.lower() == "win64" else abi
                cohorts[cohort].append((test, cpu, abi, int(moves), int(scaled)))
            elif re.match(r"Chapter\d+,ALL,ALL,", line):
                cohort, _, _, count, moves, scaled = line.split(",")
                sums[int(cohort.removeprefix("Chapter"))] = (int(count), int(moves), int(scaled))
        expected = set(range(20, chapter + 1))
        if set(cohorts) != expected or set(sums) != expected:
            raise ValueError(f"Chapter {chapter}: missing or unexpected cohorts")
        for cohort, rows in cohorts.items():
            measured = (len(rows), sum(r[3] for r in rows), sum(r[4] for r in rows))
            if measured != sums[cohort]:
                raise ValueError(f"Chapter {chapter}, cohort {cohort}: inconsistent report totals")
        results[chapter] = cohorts
    # A count alone can hide a missing case replaced by a different one.
    for cohort in CHAPTERS:
        baseline = Counter(r[:3] for r in results[cohort][cohort])
        for chapter in range(cohort + 1, 26):
            actual = Counter(r[:3] for r in results[chapter][cohort])
            if actual != baseline:
                raise ValueError(f"Chapter {chapter}, cohort {cohort}: membership differs\n"
                                 f"  missing: {baseline - actual}\n  extra: {actual - baseline}")
    return results


def table(results, last=25):
    chapters = range(20, last + 1)
    lines = ["| Test cohort | # tests | " + " | ".join(f"Ch {c}" for c in chapters) + " |",
             "|---|---:|" + "---:|" * len(chapters)]
    for cohort in chapters:
        cells = [f"Ch {cohort}", str(len(results[cohort][cohort]))]
        cells += ["" if c < cohort else f"{sum(r[4] for r in results[c][cohort]):,}" for c in chapters]
        lines.append("| " + " | ".join(cells) + " |")
    return "\n".join(lines)


def update_doc(path, matrix):
    text = path.read_text(encoding="utf-8")
    if text.count(START) != 1 or text.count(END) != 1:
        raise ValueError(f"Missing or duplicate matrix markers in {path}")
    before, rest = text.split(START)
    _, after = rest.split(END)
    path.write_text(before + START + "\n" + matrix + "\n" + END + after,
                    encoding="utf-8", newline="\n")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--log-dir", type=Path, default=ROOT / "build/spill-stats")
    parser.add_argument("--reuse", action="store_true", help="read completed logs without rerunning tests")
    parser.add_argument("--update-docs", action="store_true", help="refresh the marked tables in docs and READMEs")
    args = parser.parse_args()
    args.log_dir.mkdir(parents=True, exist_ok=True)
    if not args.reuse:
        for chapter in CHAPTERS:
            path = args.log_dir / f"spill-matrix-{chapter}.log"
            print(f"Measuring chapter {chapter}: {path}", flush=True)
            with path.open("wb") as log:
                run = subprocess.run(["make", "-C", str(ROOT / f"chapter{chapter}"), "spill-stats"],
                                     stdout=log, stderr=subprocess.STDOUT)
            if run.returncode:
                raise ValueError(f"Chapter {chapter} failed (exit {run.returncode}); see {path}")
    results = read_results(args.log_dir)
    matrix = table(results)
    (args.log_dir / "matrix.md").write_text(matrix + "\n", encoding="utf-8", newline="\n")
    if args.update_docs:
        paths = [(ROOT / "docs/regalloc-spills.md", matrix)]
        paths += [(ROOT / f"chapter{c}/README.md", table(results, c)) for c in CHAPTERS]
        # Validate all destinations before changing any document.
        for path, _ in paths:
            text = path.read_text(encoding="utf-8")
            if text.count(START) != 1 or text.count(END) != 1:
                raise ValueError(f"Missing or duplicate matrix markers in {path}")
        for path, value in paths:
            update_doc(path, value)
    print(matrix)


if __name__ == "__main__":
    try:
        main()
    except (ValueError, OSError) as error:
        sys.exit(str(error))