Log in
scripts/fleet_report.py 326 lines · 12.4 KB · blame Source
88e86b3faccd Rebuild benchmarks as a clean single-root reposi 2 months ago
1
#!/usr/bin/env python3
2
"""Fleet-scale report over contention-lane row JSONL files.
3
4
Reads parallel_contention rows (canonical or .partial.jsonl β€” same encoding)
5
and renders a markdown report with three sections, each computed by a pure
6
function the CLI merely renders:
7
8
1. Throughput saturation: commits/sec per worker tier and the saturation
9
   knee. KNEE RULE: per-worker throughput at tier w is throughput(w)/w; the
10
   baseline is per-worker throughput at the w2 tier (falling back to the
11
   smallest observed tier when w2 is absent); the knee is the LARGEST tier
12
   whose per-worker throughput is >= 70% of that baseline. Above the knee,
13
   adding workers buys less than 70 cents on the baseline dollar.
14
2. Fairness per scenario: spread between the p99 and p50 of per-worker
15
   elapsed_ms (worker_p99_spread = p99/p50), plus starvation β€” workers whose
16
   measured completed-op count is exactly 0 (null/absent counts are
17
   unmeasured, never starved; ADR-0002).
18
3. Server-error detection per tier: rows whose stderr-ish fields match
19
   5xx/throttle patterns ("HTTP 5xx", "throttl", "rate limit", "503", "502",
20
   "500"), plus nonzero-returncode counts (skips excluded) as the fallback
21
   signal when no stderr is present.
22
23
Usage:
24
    python3 scripts/fleet_report.py results/parallel-contention/latest.jsonl
25
    python3 scripts/fleet_report.py a.jsonl b.partial.jsonl --out report.md
26
"""
27
28
from __future__ import annotations
29
30
import argparse
31
import json
32
import math
33
import re
34
from pathlib import Path
35
from typing import Any, Optional
36
37
SKIP_RETURNCODE = 77
38
KNEE_THRESHOLD = 0.7
39
KNEE_BASELINE_TIER = 2
40
WORKER_OPERATIONS = ("parallel.worker", "parallel.reader")
41
# Completed-op count per worker row: the first present, non-null counter wins.
42
COMPLETED_OP_KEYS = ("commits_succeeded", "lands_succeeded", "polls_total", "pushes_succeeded")
43
SERVER_ERROR_PATTERNS = (
44
    re.compile(r"HTTP[ /]?5\d\d", re.IGNORECASE),
45
    re.compile(r"\b50[023]\b"),
46
    re.compile(r"throttl", re.IGNORECASE),
47
    re.compile(r"rate limit", re.IGNORECASE),
48
)
49
50
51
def load_rows(paths: list[Path]) -> list[dict[str, Any]]:
52
    """Parse JSONL rows from every path, skipping blank/malformed lines."""
53
    rows: list[dict[str, Any]] = []
54
    for path in paths:
55
        for line in Path(path).read_text(encoding="utf-8").splitlines():
56
            stripped = line.strip()
57
            if not stripped:
58
                continue
59
            try:
60
                row = json.loads(stripped)
61
            except json.JSONDecodeError:
62
                continue
63
            if isinstance(row, dict):
64
                rows.append(row)
65
    return rows
66
67
68
def percentile_nearest(values: list[float], pct: float) -> Optional[float]:
69
    """Nearest-rank percentile: sorted[ceil(p/100 * n) - 1]. None when empty."""
70
    if not values:
71
        return None
72
    ordered = sorted(values)
73
    rank = max(1, math.ceil(pct / 100.0 * len(ordered)))
74
    return float(ordered[rank - 1])
75
76
77
def _number(value: Any) -> Optional[float]:
78
    if isinstance(value, bool) or not isinstance(value, (int, float)):
79
        return None
80
    return float(value)
81
82
83
def total_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
84
    return [
85
        row
86
        for row in rows
87
        if row.get("operation") == "parallel.total" and not row.get("skipped")
88
    ]
89
90
91
def worker_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
92
    return [row for row in rows if row.get("operation") in WORKER_OPERATIONS]
93
94
95
def throughput_by_tier(rows: list[dict[str, Any]]) -> dict[tuple[str, str], dict[int, float]]:
96
    """Mean commit_throughput_per_s per worker tier, grouped by
97
    (subject, contention_mode). Rows without a measured throughput are
98
    excluded (null is unmeasured, never zero)."""
99
    samples: dict[tuple[str, str], dict[int, list[float]]] = {}
100
    for row in total_rows(rows):
101
        metrics = row.get("parallel_metrics") or {}
102
        throughput = _number(metrics.get("commit_throughput_per_s"))
103
        workers = row.get("workers")
104
        if throughput is None or not isinstance(workers, int):
105
            continue
106
        key = (str(row.get("subject")), str(row.get("contention_mode")))
107
        samples.setdefault(key, {}).setdefault(workers, []).append(throughput)
108
    return {
109
        key: {tier: sum(values) / len(values) for tier, values in tiers.items()}
110
        for key, tiers in samples.items()
111
    }
112
113
114
def saturation_knee(
115
    tier_throughput: dict[int, float],
116
    baseline_tier: int = KNEE_BASELINE_TIER,
117
    threshold: float = KNEE_THRESHOLD,
118
) -> Optional[int]:
119
    """The saturation knee for one (subject, mode) throughput curve.
120
121
    Rule (documented in the module docstring): baseline per-worker throughput
122
    is throughput(w2)/2 (smallest observed tier if w2 is absent). The knee is
123
    the largest tier whose throughput/worker >= threshold * baseline. Returns
124
    None when no baseline is measurable.
125
    """
126
    if not tier_throughput:
127
        return None
128
    base = baseline_tier if baseline_tier in tier_throughput else min(tier_throughput)
129
    baseline = tier_throughput[base] / base
130
    if baseline <= 0:
131
        return None
132
    qualifying = [
133
        tier
134
        for tier, throughput in tier_throughput.items()
135
        if (throughput / tier) >= threshold * baseline
136
    ]
137
    return max(qualifying) if qualifying else None
138
139
140
def completed_ops(row: dict[str, Any]) -> Optional[int]:
141
    """Measured completed-op count for a worker row; None means unmeasured."""
142
    for key in COMPLETED_OP_KEYS:
143
        value = row.get(key)
144
        if isinstance(value, bool):
145
            continue
146
        if isinstance(value, int):
147
            return value
148
    return None
149
150
151
def fairness_by_scenario(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
152
    """Per (scenario, subject): worker p99/p50 elapsed spread and starvation.
153
154
    worker_p50_ms/worker_p99_ms are nearest-rank percentiles of per-worker
155
    elapsed_ms; worker_p99_spread = p99/p50 (None when p50 is 0 or missing).
156
    starvation_count counts workers whose measured completed-op count is 0;
157
    unmeasured workers are never counted as starved.
158
    """
159
    groups: dict[tuple[str, str], list[dict[str, Any]]] = {}
160
    for row in worker_rows(rows):
161
        key = (str(row.get("scenario")), str(row.get("subject")))
162
        groups.setdefault(key, []).append(row)
163
164
    report: list[dict[str, Any]] = []
165
    for (scenario, subject), members in sorted(groups.items()):
166
        elapsed = [value for value in (_number(row.get("elapsed_ms")) for row in members) if value is not None]
167
        p50 = percentile_nearest(elapsed, 50.0)
168
        p99 = percentile_nearest(elapsed, 99.0)
169
        spread = round(p99 / p50, 3) if p50 not in (None, 0) and p99 is not None else None
170
        starved = [
171
            row.get("worker")
172
            for row in members
173
            if completed_ops(row) == 0
174
        ]
175
        report.append(
176
            {
177
                "scenario": scenario,
178
                "subject": subject,
179
                "workers_observed": len(members),
180
                "worker_p50_ms": p50,
181
                "worker_p99_ms": p99,
182
                "worker_p99_spread": spread,
183
                "starvation_count": len(starved),
184
                "starved_workers": starved,
185
            }
186
        )
187
    return report
188
189
190
def _row_error_text(row: dict[str, Any]) -> str:
191
    parts: list[str] = []
192
    stderr = row.get("stderr")
193
    if isinstance(stderr, str):
194
        parts.append(stderr)
195
    examples = row.get("error_examples")
196
    if isinstance(examples, list):
197
        parts.extend(str(item) for item in examples)
198
    return "\n".join(parts)
199
200
201
def server_error_summary(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
202
    """Per worker tier: 5xx/throttle pattern hits in stderr-ish fields, and
203
    nonzero returncodes (skip rows excluded) as the fallback signal."""
204
    tiers: dict[int, dict[str, int]] = {}
205
    for row in rows:
206
        workers = row.get("workers")
207
        if not isinstance(workers, int):
208
            continue
209
        bucket = tiers.setdefault(
210
            workers, {"rows": 0, "server_error_pattern_hits": 0, "nonzero_returncodes": 0}
211
        )
212
        bucket["rows"] += 1
213
        text = _row_error_text(row)
214
        if text and any(pattern.search(text) for pattern in SERVER_ERROR_PATTERNS):
215
            bucket["server_error_pattern_hits"] += 1
216
        returncode = row.get("returncode")
217
        if isinstance(returncode, int) and returncode not in (0, SKIP_RETURNCODE):
218
            bucket["nonzero_returncodes"] += 1
219
    return [
220
        {"workers": tier, **counts} for tier, counts in sorted(tiers.items())
221
    ]
222
223
224
def _fmt(value: Any) -> str:
225
    if value is None:
226
        return "unmeasured"
227
    if isinstance(value, float):
228
        return f"{value:.3f}"
229
    return str(value)
230
231
232
def render_markdown(rows: list[dict[str, Any]]) -> str:
233
    lines: list[str] = ["# Fleet Report", ""]
234
235
    lines += [
236
        "## Throughput vs worker tier",
237
        "",
238
        "commits/sec is the mean `parallel_metrics.commit_throughput_per_s` of total",
239
        "rows per tier. Saturation knee rule: per-worker throughput at tier w is",
240
        "throughput(w)/w; baseline is the w2 tier (smallest observed tier when w2 is",
241
        f"absent); the knee is the largest tier with throughput/worker >= {KNEE_THRESHOLD:.0%}",
242
        "of the baseline.",
243
        "",
244
        "| Subject | Mode | Tier (workers) | Commits/s | Commits/s/worker | Knee |",
245
        "| --- | --- | ---: | ---: | ---: | --- |",
246
    ]
247
    curves = throughput_by_tier(rows)
248
    if not curves:
249
        lines.append("| (no measured throughput rows) | | | | | |")
250
    for (subject, mode), tiers in sorted(curves.items()):
251
        knee = saturation_knee(tiers)
252
        for tier in sorted(tiers):
253
            throughput = tiers[tier]
254
            marker = f"knee (w{knee})" if knee == tier else ""
255
            lines.append(
256
                f"| `{subject}` | `{mode}` | {tier} | {throughput:.3f} | {throughput / tier:.3f} | {marker} |"
257
            )
258
        if knee is None:
259
            lines.append(f"| `{subject}` | `{mode}` | β€” | β€” | β€” | knee: unmeasured |")
260
    lines.append("")
261
262
    lines += [
263
        "## Fairness per scenario",
264
        "",
265
        "Spread is p99/p50 of per-worker elapsed_ms (nearest-rank percentiles over the",
266
        "observed workers). Starvation counts workers whose measured completed-op count",
267
        "is exactly 0; unmeasured workers are never counted as starved (ADR-0002).",
268
        "",
269
        "| Scenario | Subject | Workers observed | p50 ms | p99 ms | p99/p50 spread | Starved |",
270
        "| --- | --- | ---: | ---: | ---: | ---: | ---: |",
271
    ]
272
    fairness = fairness_by_scenario(rows)
273
    if not fairness:
274
        lines.append("| (no worker rows) | | | | | | |")
275
    for entry in fairness:
276
        lines.append(
277
            f"| `{entry['scenario']}` | `{entry['subject']}` | {entry['workers_observed']} | "
278
            f"{_fmt(entry['worker_p50_ms'])} | {_fmt(entry['worker_p99_ms'])} | "
279
            f"{_fmt(entry['worker_p99_spread'])} | {entry['starvation_count']} |"
280
        )
281
    lines.append("")
282
283
    lines += [
284
        "## Server errors / throttling per tier",
285
        "",
286
        "Pattern hits scan `stderr` and `error_examples` for HTTP 5xx / throttle /",
287
        "rate-limit signatures. Nonzero returncodes (skips excluded) are the fallback",
288
        "signal when no stderr text is present.",
289
        "",
290
        "| Tier (workers) | Rows | 5xx/throttle pattern hits | Nonzero returncodes |",
291
        "| ---: | ---: | ---: | ---: |",
292
    ]
293
    errors = server_error_summary(rows)
294
    if not errors:
295
        lines.append("| (no tiered rows) | | | |")
296
    for entry in errors:
297
        lines.append(
298
            f"| {entry['workers']} | {entry['rows']} | {entry['server_error_pattern_hits']} | "
299
            f"{entry['nonzero_returncodes']} |"
300
        )
301
    lines.append("")
302
    return "\n".join(lines)
303
304
305
def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
306
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
307
    parser.add_argument("paths", nargs="+", type=Path, help="Row JSONL paths (canonical or .partial.jsonl)")
308
    parser.add_argument("--out", type=Path, help="Write the markdown report here (default: stdout)")
309
    return parser.parse_args(argv)
310
311
312
def main(argv: Optional[list[str]] = None) -> int:
313
    args = parse_args(argv)
314
    rows = load_rows(args.paths)
315
    report = render_markdown(rows)
316
    if args.out:
317
        args.out.parent.mkdir(parents=True, exist_ok=True)
318
        args.out.write_text(report)
319
        print(f"[report] {args.out}")
320
    else:
321
        print(report)
322
    return 0
323
324
325
if __name__ == "__main__":
326
    raise SystemExit(main())