import csv
import sqlite3
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
DB = REPO / "data" / "benchmarks.db"
CSV_OUT = REPO / "data" / "benchmark_timing.csv"
TEX_OUT = REPO / "paper" / "fig_timing.tex"
START_DATE = "2026-03-20" N_TICKS = 11
QUERY = """
WITH real AS (
SELECT r.run_id, r.sqc_version, r.started_at,
(SELECT COUNT(*) FROM cwe_scans cs WHERE cs.run_id=r.run_id) AS cwes,
(SELECT SUM(cs.duration_s) FROM cwe_scans cs WHERE cs.run_id=r.run_id) AS analysis_s,
ROW_NUMBER() OVER (PARTITION BY r.sqc_version ORDER BY r.started_at DESC) AS rn
FROM runs r
WHERE r.status='completed' AND r.started_at >= ?
)
SELECT sqc_version, cwes, ROUND(analysis_s,1), substr(started_at,1,10)
FROM real WHERE rn=1 AND analysis_s IS NOT NULL
ORDER BY started_at;
"""
def main() -> None:
con = sqlite3.connect(DB)
rows = con.execute(QUERY, (START_DATE,)).fetchall()
con.close()
if not rows:
raise SystemExit("no timing rows found")
series = [
{
"i": i + 1,
"version": v,
"cwes": cwes,
"analysis_s": analysis_s,
"s_per_cwe": round(analysis_s / cwes, 1),
"date": date,
}
for i, (v, cwes, analysis_s, date) in enumerate(rows)
]
with CSV_OUT.open("w", newline="") as f:
w = csv.DictWriter(f, fieldnames=["i", "version", "cwes", "analysis_s", "s_per_cwe", "date"])
w.writeheader()
w.writerows(series)
n = len(series)
step = max(1, (n - 1) // (N_TICKS - 1))
tick_idx = list(range(0, n, step))
if tick_idx[-1] != n - 1:
tick_idx.append(n - 1)
xtick = ", ".join(str(series[k]["i"]) for k in tick_idx)
xlabels = ", ".join("v" + series[k]["version"] for k in tick_idx)
coords = " ".join(f"({s['i']}, {s['s_per_cwe']})" for s in series)
ys = [s["s_per_cwe"] for s in series]
ymax = (int(max(ys)) // 25 + 1) * 25
tex = f"""% Auto-generated by scripts/benchmark_timing_figure.py -- do not edit by hand.
% Juliet scan time per CWE (parallelism- and corpus-size-independent), v{series[0]['version']}-v{series[-1]['version']}.
\\begin{{figure}}[t]
\\centering
\\begin{{tikzpicture}}
\\begin{{axis}}[
width=\\columnwidth,
height=5cm,
xlabel={{Version}},
ylabel={{Scan time per CWE (s)}},
ymin=0, ymax={ymax},
xtick={{{xtick}}},
xticklabels={{{xlabels}}},
xticklabel style={{rotate=45, anchor=east, font=\\footnotesize}},
ylabel style={{font=\\footnotesize}},
xlabel style={{font=\\footnotesize}},
tick label style={{font=\\footnotesize}},
grid=major,
grid style={{dashed, gray!30}},
]
\\addplot[color=blue, thick, mark=none] coordinates {{
{coords}
}};
\\end{{axis}}
\\end{{tikzpicture}}
\\caption{{Juliet scan time per CWE across {n} released versions
(v{series[0]['version']}--v{series[-1]['version']}). The metric is the summed
per-CWE \\texttt{{sqc}} analysis time divided by the number of CWEs scanned,
making it independent of both parallelism (\\texttt{{--jobs}}) and corpus growth
(68--74 CWEs). The sharp drop at v0.3.67 is a single perf optimization
($\\approx$8$\\times$); the subsequent climb is the cost of added
inter-procedural analysis, which still settles below the v0.3.5x peak.}}
\\label{{fig:scan-time}}
\\end{{figure}}
"""
TEX_OUT.write_text(tex)
print(f"wrote {CSV_OUT.relative_to(REPO)} ({n} versions)")
print(f"wrote {TEX_OUT.relative_to(REPO)}")
print(f"range: {min(ys)}-{max(ys)} s/CWE (latest v{series[-1]['version']}: {series[-1]['s_per_cwe']})")
if __name__ == "__main__":
main()