import argparse import io import json import subprocess import tarfile import tempfile from datetime import date from html import escape from pathlib import Path import progress SERIES = (("libraries", "System libraries", "#2f81f7"), ("shaders", "GPU shader instructions", "#2ea043")) SHORT = {"libraries": "Libraries", "shaders": "Shaders"} WIDTH, HEIGHT, LEFT, RIGHT, TOP, BOTTOM = 900, 380, 50, 130, 20, 60 MAX_DOTS = 45 BACKGROUND, GRID, TEXT = "#0d1117", "#30363d", "#c9d1d9" def git(*args): return subprocess.run(["git", "-C", str(progress.ROOT), *args], check=True, capture_output=True).stdout def daily_commits(ref): seen = {} for line in git("log", "--first-parent", "--format=%H %cs", ref).decode().splitlines(): sha, day = line.split() seen.setdefault(day, sha) return sorted(seen.items()) def measure(sha): paths = [progress.PRX.relative_to(progress.ROOT).as_posix(), progress.OPCODES.relative_to(progress.ROOT).as_posix()] with tempfile.TemporaryDirectory() as directory: root = Path(directory) try: tarfile.open(fileobj=io.BytesIO(git("archive", sha, "--", *paths))).extractall(root, filter="data") except subprocess.CalledProcessError: return None saved = progress.PRX, progress.OPCODES progress.PRX, progress.OPCODES = root / paths[0], root / paths[1] try: libraries, shaders = progress.collect_libraries(), progress.collect_shaders() except (OSError, AttributeError): return None finally: progress.PRX, progress.OPCODES = saved return {name: {key: data[key] for key in ("done", "total", "percent")} for name, data in (("libraries", libraries), ("shaders", shaders))} def collect(ref): points = [] for day, sha in daily_commits(ref): counts = measure(sha) if counts: points.append({"date": day, "commit": sha, **counts}) return points def labelled(day, span): if span <= 31: return day.strftime("%m-%d") if span <= 150: return day.strftime("%m-%d") if day.weekday() == 0 else "" return day.strftime("%Y-%m") if day.day <= 7 and day.weekday() == 0 else "" def chart(points): days = [date.fromisoformat(p["date"]).toordinal() for p in points] first, span = days[0], max(days[-1] - days[0], 1) top = max(10, int(-(-max(p[name]["percent"] for p in points for name, _, _ in SERIES) // 10) * 10)) px = lambda d: LEFT + (d - first) / span * (WIDTH - LEFT - RIGHT) py = lambda v: TOP + (1 - v / top) * (HEIGHT - TOP - BOTTOM) base = py(0) out = [f'', f''] for tick in range(0, top + 1, 10): out.append(f'' f'{tick}%') for d, p in zip(days, points): x = px(d) label = labelled(date.fromordinal(d), span) if label: out.append(f'' f'{label}') for name, label, color in SERIES: coords = [(px(d), py(p[name]["percent"])) for d, p in zip(days, points)] line = " ".join(f"{x:.1f},{y:.1f}" for x, y in coords) out.append(f'' f'') for (x, y), p in zip(coords, points) if len(points) <= MAX_DOTS else (): out.append(f'{p["date"]}: {label} ' f'{p[name]["percent"]}% ({p[name]["done"]}/{p[name]["total"]})') last = points[-1][name] out.append(f'' f'{SHORT[name]} {last["percent"]}%') out.append("") return "\n".join(out) def page(points): rows = "\n".join(f'{p["date"]}' + "".join( f'{p[name]["done"]}/{p[name]["total"]}{p[name]["percent"]}%' for name, _, _ in SERIES) + "" for p in reversed(points)) return "\n".join([ '', '', 'AnyPS5 progress history', '', '', '

Progress history

', chart(points), f'

One point per day with commits on main, taken at the last commit of the day. Every point is measured ' f'with the current tools/progress.py rules and instruction list, ' 'so the series is comparable across days. Hover a point for the exact numbers. Raw numbers: history.json. ' 'Back to the current progress.

', '' + "".join(f'' for _, label, _ in SERIES) + '', rows, '
Date{escape(label)}%
', '', ]) + "\n" if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("output", type=Path) parser.add_argument("--ref", default="HEAD", help="branch or commit whose first-parent history is measured") args = parser.parse_args() points = collect(args.ref) if not points: parser.error("no commit with a measurable tree") args.output.mkdir(parents=True, exist_ok=True) (args.output / "history.json").write_text(json.dumps(points, indent=2)) (args.output / "history.svg").write_text(chart(points) + "\n") (args.output / "history.html").write_text(page(points)) print(f"{len(points)} days, {points[0]['date']} .. {points[-1]['date']}")