adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
307 lines
10 KiB
Python
Executable File
307 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Analyze profiling output from sampler.sh + playwright run.
|
|
|
|
Usage: analyze.py <results_dir>
|
|
|
|
Reads totals.csv, processes.csv, playwright.json (if present) and prints a
|
|
summary of resource patterns over the run.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import json
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
|
|
def kb_to_mb(kb: float) -> float:
|
|
return kb / 1024.0
|
|
|
|
|
|
def load_totals(path: Path):
|
|
rows = []
|
|
with path.open() as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
# Sentinel row from sampler trap
|
|
if row.get("py_count") == "SAMPLER_STOPPED":
|
|
continue
|
|
try:
|
|
rows.append({
|
|
"ts": int(row["ts"]),
|
|
"py_count": int(row["py_count"]),
|
|
"py_rss_kb": int(row["py_rss_kb"]),
|
|
"node_count": int(row["node_count"]),
|
|
"node_rss_kb": int(row["node_rss_kb"]),
|
|
"chrome_count": int(row["chrome_count"]),
|
|
"chrome_rss_kb": int(row["chrome_rss_kb"]),
|
|
"playwright_count": int(row["playwright_count"]),
|
|
"playwright_rss_kb": int(row["playwright_rss_kb"]),
|
|
})
|
|
except (ValueError, KeyError):
|
|
continue
|
|
return rows
|
|
|
|
|
|
def load_processes(path: Path):
|
|
rows = []
|
|
with path.open() as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
try:
|
|
rows.append({
|
|
"ts": int(row["ts"]),
|
|
"pid": int(row["pid"]),
|
|
"ppid": int(row["ppid"]),
|
|
"pcpu": float(row["pcpu"]),
|
|
"rss_kb": int(row["rss_kb"]),
|
|
"comm": row["comm"],
|
|
"cmd": row["cmd"],
|
|
})
|
|
except (ValueError, KeyError):
|
|
continue
|
|
return rows
|
|
|
|
|
|
def load_playwright(path: Path):
|
|
if not path.exists():
|
|
return None
|
|
try:
|
|
return json.loads(path.read_text())
|
|
except json.JSONDecodeError:
|
|
return None
|
|
|
|
|
|
def summarize_totals(rows):
|
|
if not rows:
|
|
print(" (no totals data)")
|
|
return
|
|
|
|
first = rows[0]
|
|
last = rows[-1]
|
|
duration_s = last["ts"] - first["ts"]
|
|
|
|
print(f" duration: {duration_s}s ({duration_s/60:.1f} min), {len(rows)} samples")
|
|
print()
|
|
print(" category start_count peak_count end_count start_rss peak_rss end_rss delta_rss")
|
|
print(" -------- ----------- ---------- --------- --------- -------- ------- ---------")
|
|
|
|
for cat, count_key, rss_key in [
|
|
("python ", "py_count", "py_rss_kb"),
|
|
("node ", "node_count", "node_rss_kb"),
|
|
("playwrght", "playwright_count", "playwright_rss_kb"),
|
|
("chrome ", "chrome_count", "chrome_rss_kb"),
|
|
]:
|
|
peak_count = max(r[count_key] for r in rows)
|
|
peak_rss = max(r[rss_key] for r in rows)
|
|
delta = last[rss_key] - first[rss_key]
|
|
print(
|
|
f" {cat} {first[count_key]:>11} {peak_count:>10} {last[count_key]:>9}"
|
|
f" {kb_to_mb(first[rss_key]):>7.0f}MB {kb_to_mb(peak_rss):>6.0f}MB {kb_to_mb(last[rss_key]):>5.0f}MB {kb_to_mb(delta):>+7.0f}MB"
|
|
)
|
|
|
|
|
|
def find_steepest_growth(rows, key, window_samples=15):
|
|
"""Find window of N samples where `key` grows fastest. Returns (start_ts, end_ts, delta_kb)."""
|
|
if len(rows) < window_samples:
|
|
return None
|
|
best = (0, 0, 0)
|
|
for i in range(len(rows) - window_samples):
|
|
delta = rows[i + window_samples][key] - rows[i][key]
|
|
if delta > best[2]:
|
|
best = (rows[i]["ts"], rows[i + window_samples]["ts"], delta)
|
|
return best if best[2] > 0 else None
|
|
|
|
|
|
def summarize_growth(rows):
|
|
print()
|
|
print("Steepest 30s growth windows:")
|
|
for label, key in [
|
|
("python RSS ", "py_rss_kb"),
|
|
("python procs ", "py_count"),
|
|
("node RSS ", "node_rss_kb"),
|
|
("chrome RSS ", "chrome_rss_kb"),
|
|
]:
|
|
result = find_steepest_growth(rows, key)
|
|
if result is None:
|
|
print(f" {label}: (no growth)")
|
|
continue
|
|
start, end, delta = result
|
|
if "rss" in key:
|
|
print(f" {label}: +{kb_to_mb(delta):.0f}MB between ts={start}..{end}")
|
|
else:
|
|
print(f" {label}: +{delta} procs between ts={start}..{end}")
|
|
|
|
|
|
def classify_node(cmd: str) -> str:
|
|
"""Bucket a node process by purpose so editor/LSP noise doesn't drown
|
|
out the test runner + gradio SSR signal."""
|
|
c = cmd.lower()
|
|
if "templates/register.mjs" in c or "templates/node/build" in c:
|
|
return "gradio-ssr"
|
|
if "playwright" in c:
|
|
return "playwright"
|
|
if "vitest" in c:
|
|
return "vitest"
|
|
if "vite/bin/vite" in c or "/vite.js" in c:
|
|
return "vite"
|
|
if "pnpm" in c:
|
|
return "pnpm"
|
|
if any(s in c for s in [
|
|
"zed", "basedpyright", "tsserver", "typescript-language",
|
|
"language-server", "langserver", "vscode-", "cursor",
|
|
"discord", "adobe", "creative cloud", "ccxprocess",
|
|
"twinkleplop",
|
|
]):
|
|
return "editor-or-app"
|
|
return "other-node"
|
|
|
|
|
|
def summarize_top_pids(processes, end_ts):
|
|
"""At end of run, list top python processes by RSS, plus a per-bucket
|
|
breakdown of node processes (so the test-runner/gradio-ssr signal is
|
|
legible against editor/LSP background noise)."""
|
|
if not processes:
|
|
print(" (no per-process data)")
|
|
return
|
|
|
|
last_window_start = end_ts - 10
|
|
recent = [p for p in processes if p["ts"] >= last_window_start]
|
|
|
|
by_pid_py = defaultdict(list)
|
|
by_pid_node = defaultdict(list)
|
|
for p in recent:
|
|
if p["comm"] == "python":
|
|
by_pid_py[p["pid"]].append(p["rss_kb"])
|
|
elif p["comm"] == "node":
|
|
by_pid_node[p["pid"]].append(p["rss_kb"])
|
|
|
|
def show(label, data):
|
|
if not data:
|
|
print(f" {label}: none")
|
|
return
|
|
avg = sorted(
|
|
((pid, sum(rss) / len(rss)) for pid, rss in data.items()),
|
|
key=lambda x: -x[1],
|
|
)
|
|
print(f" {label}: {len(avg)} procs alive, top-10 by RSS:")
|
|
for pid, rss in avg[:10]:
|
|
cmd = next(
|
|
(p["cmd"] for p in recent if p["pid"] == pid),
|
|
"?",
|
|
)
|
|
print(f" pid={pid:>6} {kb_to_mb(rss):>6.0f}MB {cmd[:90]}")
|
|
|
|
show("python", by_pid_py)
|
|
show("node (all)", by_pid_node)
|
|
|
|
print()
|
|
print("Node breakdown by purpose (final 10s window):")
|
|
bucket_counts: dict[str, int] = defaultdict(int)
|
|
bucket_rss: dict[str, float] = defaultdict(float)
|
|
for pid, rss_samples in by_pid_node.items():
|
|
avg_rss = sum(rss_samples) / len(rss_samples)
|
|
cmd = next((p["cmd"] for p in recent if p["pid"] == pid), "")
|
|
bucket = classify_node(cmd)
|
|
bucket_counts[bucket] += 1
|
|
bucket_rss[bucket] += avg_rss
|
|
|
|
print(" bucket count rss")
|
|
print(" ------ ----- ---")
|
|
for bucket in sorted(bucket_counts.keys(), key=lambda b: -bucket_rss[b]):
|
|
print(f" {bucket:<16} {bucket_counts[bucket]:>5} {kb_to_mb(bucket_rss[bucket]):>6.0f}MB")
|
|
|
|
|
|
def summarize_node_growth(processes, totals):
|
|
"""Track gradio-ssr + playwright node procs across the run, in 30s windows,
|
|
so we can see whether either category leaks during SSR mode."""
|
|
if not processes or not totals:
|
|
return
|
|
|
|
# Bucket processes by 30s windows from run start
|
|
start_ts = totals[0]["ts"]
|
|
by_window: dict[int, dict[str, dict[int, int]]] = defaultdict(
|
|
lambda: defaultdict(dict)
|
|
)
|
|
|
|
for p in processes:
|
|
if p["comm"] != "node":
|
|
continue
|
|
bucket = classify_node(p["cmd"])
|
|
if bucket not in ("gradio-ssr", "playwright", "vite", "vitest"):
|
|
continue
|
|
window = (p["ts"] - start_ts) // 30
|
|
# Keep last RSS we saw for this pid in this window (averages out below)
|
|
by_window[window][bucket][p["pid"]] = p["rss_kb"]
|
|
|
|
if not by_window:
|
|
return
|
|
|
|
print()
|
|
print("Test-runner / gradio-ssr node procs over time (per 30s window):")
|
|
print(" win gradio-ssr (n,rss) playwright (n,rss) vite (n,rss) vitest (n,rss)")
|
|
for window in sorted(by_window.keys()):
|
|
parts = []
|
|
for bucket in ("gradio-ssr", "playwright", "vite", "vitest"):
|
|
data = by_window[window].get(bucket, {})
|
|
n = len(data)
|
|
rss = sum(data.values())
|
|
parts.append(f"{n:>2},{kb_to_mb(rss):>5.0f}MB")
|
|
secs = window * 30
|
|
print(f" {secs:>3}s {parts[0]:>16} {parts[1]:>16} {parts[2]:>14} {parts[3]:>14}")
|
|
|
|
|
|
def summarize_playwright(pw_data):
|
|
if not pw_data:
|
|
print(" (no playwright JSON — file missing or unparseable)")
|
|
return
|
|
|
|
stats = pw_data.get("stats", {})
|
|
print(f" expected: {stats.get('expected', '?')}")
|
|
print(f" unexpected: {stats.get('unexpected', '?')}")
|
|
print(f" flaky: {stats.get('flaky', '?')}")
|
|
print(f" skipped: {stats.get('skipped', '?')}")
|
|
print(f" duration: {stats.get('duration', '?'):.0f}ms")
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 2:
|
|
print("Usage: analyze.py <results_dir>", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
results_dir = Path(sys.argv[1])
|
|
if not results_dir.is_dir():
|
|
print(f"not a directory: {results_dir}", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
print(f"=== Analysis: {results_dir.name} ===")
|
|
print()
|
|
|
|
print("Totals over the run:")
|
|
totals = load_totals(results_dir / "totals.csv")
|
|
summarize_totals(totals)
|
|
|
|
if totals:
|
|
summarize_growth(totals)
|
|
|
|
print()
|
|
print("Top processes by RSS (final 10s window):")
|
|
processes = load_processes(results_dir / "processes.csv")
|
|
end_ts = totals[-1]["ts"] if totals else 0
|
|
summarize_top_pids(processes, end_ts)
|
|
|
|
summarize_node_growth(processes, totals)
|
|
|
|
print()
|
|
print("Playwright run summary:")
|
|
pw = load_playwright(results_dir / "playwright.json")
|
|
summarize_playwright(pw)
|
|
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|