#!/usr/bin/env python3 """Micro-benchmarks for fold speed/FP optimization work. This harness generates tiny synthetic C++ projects that stress the specific surfaces identified in docs/internal/perf-audit.md, runs fold with --profile, and writes a stable JSON/Markdown summary. It is intended for A/B validation before and after an optimization patch, not for broad corpus scoring. Examples: python3 bench/run_micro_opt.py --case all --repeats 3 python3 bench/run_micro_opt.py --case branch-merge --size 200 --repeats 5 python3 bench/run_micro_opt.py --fold build/release/src/cli/fold --out-dir /tmp/fold-micro """ from __future__ import annotations import argparse import json import os import shutil import statistics import subprocess import sys import tempfile import time from dataclasses import dataclass from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_FOLD = ROOT / "build" / "release" / "src" / "cli" / "fold" @dataclass(frozen=True) class CaseSpec: name: str detectors: list[str] description: str size_default: int expect_actionable: bool = True extra_args: tuple[str, ...] = () uses_config_profile: bool = False CASES: dict[str, CaseSpec] = { "branch-merge": CaseSpec( name="branch-merge", detectors=["null-deref"], size_default=160, description=( "Stresses SecurityWalker::walkIf and SymbolicEnv branch merge cost. " "A dirty-binding optimization should reduce null-deref wall_ms per state merge " "without changing findings." ), ), "defuse-null": CaseSpec( name="defuse-null", detectors=["null-deref"], size_default=180, description=( "Stresses NullDerefObserver::shouldQuery and DefUseChains::reachingDefs. " "A direct (var, use_loc) lookup should reduce wall_ms without changing " "query/finding counts." ), ), "cross-tu-prime": CaseSpec( name="cross-tu-prime", detectors=["null-deref", "div-by-zero"], size_default=48, description=( "Stresses global and local cross-TU property priming. A skip/dedup of redundant " "per-TU priming should reduce serial_dispatch_ms / total wall while preserving " "cross-TU callsite findings." ), ), "precision-fp": CaseSpec( name="precision-fp", detectors=["null-deref", "div-by-zero", "int-overflow"], size_default=80, description=( "Tracks confidence distribution for mixed exact, heuristic, guarded, and " "over-approximate-looking security findings. A precision-evidence change should " "demote imprecise SATs without changing exact findings." ), extra_args=("--nullable-allocations",), ), "expr-cache": CaseSpec( name="expr-cache", detectors=["div-by-zero", "int-overflow"], size_default=220, description=( "Stresses repeated equivalent solver queries and hash/cache overhead. ExprCache " "changes should reduce detector wall/z3 query overhead without changing findings." ), ), "overflow-cache": CaseSpec( name="overflow-cache", detectors=["int-overflow"], size_default=120, description=( "Isolates repeated integer-overflow queries in one symbolic function. Abstract " "range or dominance reuse should reduce solver queries, conflicts, wall time, and " "peak RSS without changing findings." ), ), "curl-str-bound": CaseSpec( name="curl-str-bound", detectors=["oob"], size_default=128, expect_actionable=False, description=( "Regression gate for Curl_str parser bounds. curlx_str_* records an output length " "upper bound, and curlx_strlen(&out) used as the index into a max+1 stack buffer " "must not produce an OOB false positive." ), ), "control-nesting": CaseSpec( name="control-nesting", detectors=["null-deref"], size_default=80, description=( "Stresses nested control-flow recursion and the sink precheck counters. " "Precision-preserving walker optimizations should reduce control_walk_ms or " "functions_prechecked_safe without changing findings." ), ), "clone-t3": CaseSpec( name="clone-t3", detectors=["clone"], size_default=160, description=( "Stresses Type-3 clone scoring: many same-shape functions with literal " "edits avoid exact/rename buckets, then form a large literal-insensitive " "bucket. The baseline scalar DP still pays full edit-distance cost on the " "normalized bodies; bit-parallel distance should reduce clone wall_ms " "without changing finding counts or scores." ), extra_args=("--clone-type", "3", "--clone-similarity", "0.70"), ), "call-graph": CaseSpec( name="call-graph", detectors=["production-reachability"], size_default=1200, expect_actionable=False, description=( "Stresses cold incremental call-graph contribution collection, merge/SCC work, " "index persistence, and graph storage overhead. Graph representation changes " "should reduce graph subphase time or peak RSS without changing node/edge counts." ), extra_args=("--incremental",), ), "guarded-call-graph": CaseSpec( name="guarded-call-graph", detectors=["production-reachability"], size_default=1200, description=( "Stresses guarded production reachability with one profile-controlled frontier " "followed by a long unconditional chain. Equivalent downstream reachability proofs " "should be reused without changing classifications or witnesses." ), extra_args=( "--incremental", "--budget-seconds", "60", "--per-query-ms", "2000", ), uses_config_profile=True, ), "guarded-call-graph-active": CaseSpec( name="guarded-call-graph-active", detectors=["production-reachability"], size_default=1200, expect_actionable=False, description=( "Uses the guarded-call-graph shape with its frontier enabled. Comparing this " "zero-finding lane against the inactive lane attributes retained report and " "witness memory separately from graph and solver memory." ), extra_args=( "--incremental", "--budget-seconds", "60", "--per-query-ms", "2000", ), uses_config_profile=True, ), "guarded-call-graph-infeasible": CaseSpec( name="guarded-call-graph-infeasible", detectors=["production-reachability"], size_default=1200, description=( "Uses a contradictory guarded frontier so all downstream findings are infeasible " "and carry no SAT witness. Comparing it with the inactive lane isolates witness " "retention from general finding/report memory." ), extra_args=( "--incremental", "--budget-seconds", "60", "--per-query-ms", "2000", ), uses_config_profile=True, ), } def write_compile_db(project: Path, files: list[Path], std: str = "c++20") -> None: commands = [] for src in files: commands.append( { "directory": str(project), "command": f"clang++ -std={std} -c {src.name}", "file": str(src), } ) (project / "compile_commands.json").write_text(json.dumps(commands, indent=2) + "\n") def gen_branch_merge(project: Path, size: int) -> None: lines = ["int sink(int);", "int f(int x, int* p) {", " int acc = 0;"] for i in range(size): lines.append(f" int v{i} = x + {i};") for i in range(size): lines.append(f" if ((x + {i}) & 1) {{") lines.append(f" v{i} = v{i} + x;") lines.append(" } else {") lines.append(f" v{i} = v{i} - x;") lines.append(" }") lines.append(f" acc += v{i};") lines.append(" int* q = nullptr;") lines.append(" return acc + *q;") lines.append("}") src = project / "branch_merge.cpp" src.write_text("\n".join(lines) + "\n") write_compile_db(project, [src]) def gen_defuse_null(project: Path, size: int) -> None: lines = ["int g = 1;", "int f(int selector) {", " int acc = 0;"] for i in range(size): lines.append(f" int* p{i} = nullptr;") # Make half definitely safe after a nullable def. The final deref still # makes shouldQuery inspect reaching defs for many uses. if i % 2: lines.append(f" p{i} = &g;") lines.append(f" if (selector == {i}) acc += *p{i};") lines.append(" return acc;") lines.append("}") src = project / "defuse_null.cpp" src.write_text("\n".join(lines) + "\n") write_compile_db(project, [src]) def gen_cross_tu_prime(project: Path, size: int) -> None: files: list[Path] = [] for i in range(size): src = project / f"callee_{i}.cpp" if i % 2 == 0: src.write_text(f"int null_sink_{i}(int* p) {{ return *p; }}\n") else: src.write_text(f"int div_sink_{i}(int x) {{ return 100 / x; }}\n") files.append(src) caller = project / "caller.cpp" lines: list[str] = [] for i in range(size): if i % 2 == 0: lines.append(f"int null_sink_{i}(int*);") else: lines.append(f"int div_sink_{i}(int);") lines.append("int caller(int x) {") lines.append(" int* p = nullptr;") lines.append(" int acc = 0;") for i in range(size): if i % 2 == 0: lines.append(f" acc += null_sink_{i}(p);") else: lines.append(f" acc += div_sink_{i}(x - x);") lines.append(" return acc;") lines.append("}") caller.write_text("\n".join(lines) + "\n") files.append(caller) write_compile_db(project, files) def gen_precision_fp(project: Path, size: int) -> None: lines = [ "extern \"C\" void* malloc(unsigned long);", "[[noreturn]] extern \"C\" void abort(void);", "int global;", "int f(int* p, int x) {", " int acc = 0;", " if (!p) return 0;", " acc += *p;", # guard should suppress " int* q = nullptr;", " acc += *q;", # exact positive " int* h = (int*)malloc(sizeof(int));", " acc += *h;", # heuristic when nullable allocations enabled " if (x == 0) abort();", " acc += 10 / x;", # guard should suppress div-by-zero ] for i in range(size): lines.append(f" int y{i} = x + {i};") lines.append(f" if (y{i} > 0) acc += y{i}; else acc -= y{i};") lines.append(" return acc;") lines.append("}") src = project / "precision_fp.cpp" src.write_text("\n".join(lines) + "\n") write_compile_db(project, [src]) def gen_expr_cache(project: Path, size: int) -> None: lines = ["int f(int x, int y) {", " int acc = 0;"] # Repeated same-shape hazards in one function. This tracks local query cache # and canonical ExprCache behavior under large repeated predicates. for i in range(size): lines.append(f" if ((y + {i}) >= 0) acc += 100 / x;") lines.append(f" acc += x + {i};") lines.append(" return acc;") lines.append("}") src = project / "expr_cache.cpp" src.write_text("\n".join(lines) + "\n") write_compile_db(project, [src]) def gen_control_nesting(project: Path, size: int) -> None: lines = ["int f(int x, int* p) {", " int acc = 0;"] for i in range(size): lines.append(f" if ((x + {i}) > 0) {{") lines.append(f" for (int j{i} = 0; j{i} < 2; ++j{i}) {{") lines.append(f" if ((j{i} + x) & 1) acc += j{i}; else acc -= j{i};") lines.append(" }") lines.append(" } else {") lines.append(f" if (x == {i}) acc += {i};") lines.append(" }") lines.append(" int* q = nullptr;") lines.append(" return acc + *q;") lines.append("}") src = project / "control_nesting.cpp" src.write_text("\n".join(lines) + "\n") write_compile_db(project, [src]) def gen_curl_str_bound(project: Path, size: int) -> None: lines = [ "struct Curl_str { const char* str; unsigned long len; };", "extern int curlx_str_quotedword(const char**, Curl_str*, unsigned long);", "#define curlx_strlen(x) ((x)->len)", "int f(const char* line_in) {", " const char* line = line_in;", " int acc = 0;", ] for i in range(size): lines.append(f" Curl_str date{i}{{}};") lines.append(f" if(curlx_str_quotedword(&line, &date{i}, 17)) return acc;") lines.append(f" char dbuf{i}[18];") lines.append(f" dbuf{i}[curlx_strlen(&date{i})] = 0;") lines.append(f" acc += dbuf{i}[0];") lines.append(" return acc;") lines.append("}") src = project / "curl_str_bound.cpp" src.write_text("\n".join(lines) + "\n") write_compile_db(project, [src]) def gen_clone_t3(project: Path, size: int) -> None: lines = ["int sink(int);", "namespace clone_t3 {"] for i in range(size): # Same AST shape across functions, but enough literal changes to avoid # Exact/RenameInsensitive buckets and force Type-3 scoring inside the # LiteralInsensitive bucket. op = "+" if i % 3 else "-" scale = 2 + (i % 5) lines.append(f"int f_{i}(int x, int y) {{") lines.append(" int acc = 0;") for j in range(8): literal = (i + j) % 11 lines.append(f" int v{j} = (x {op} {literal}) * {scale};") lines.append(f" if ((v{j} + y) & 1) {{") lines.append(f" acc += sink(v{j});") lines.append(" } else {") lines.append(f" acc -= v{j};") lines.append(" }") lines.append(" return acc;") lines.append("}") lines.append("}") src = project / "clone_t3.cpp" src.write_text("\n".join(lines) + "\n") write_compile_db(project, [src]) def gen_call_graph(project: Path, size: int) -> None: lines = [f"static int node_{i}(int);" for i in range(size)] for i in range(size): callees = [] if i + 1 < size: callees.append(f"node_{i + 1}(value)") if i + 2 < size: callees.append(f"node_{i + 2}(value)") expression = " + ".join(callees) if callees else "value" lines.append(f"static int node_{i}(int value) {{ return {expression}; }}") lines.append("int main() { return node_0(1); }") src = project / "call_graph.cpp" src.write_text("\n".join(lines) + "\n") write_compile_db(project, [src]) def gen_guarded_call_graph_case( project: Path, size: int, condition: str, profile_value: bool ) -> None: lines = ["bool feature;", *[f"static int node_{i}(int);" for i in range(size)]] for i in range(size): expression = f"node_{i + 1}(value)" if i + 1 < size else "value" lines.append(f"static int node_{i}(int value) {{ return {expression}; }}") lines.append(f"int main() {{ return {condition} ? node_0(1) : 0; }}") src = project / "guarded_call_graph.cpp" src.write_text("\n".join(lines) + "\n") (project / "config_profile.json").write_text( json.dumps( { "version": 1, "entrypoints": ["main"], "assumptions": [ { "kind": "global", "selector": "feature", "value": profile_value, } ], }, indent=2, ) + "\n" ) write_compile_db(project, [src]) def gen_guarded_call_graph(project: Path, size: int) -> None: gen_guarded_call_graph_case(project, size, "feature", False) def gen_guarded_call_graph_active(project: Path, size: int) -> None: gen_guarded_call_graph_case(project, size, "feature", True) def gen_guarded_call_graph_infeasible(project: Path, size: int) -> None: gen_guarded_call_graph_case(project, size, "feature && !feature", False) GENERATORS = { "branch-merge": gen_branch_merge, "defuse-null": gen_defuse_null, "cross-tu-prime": gen_cross_tu_prime, "precision-fp": gen_precision_fp, "expr-cache": gen_expr_cache, "overflow-cache": gen_expr_cache, "curl-str-bound": gen_curl_str_bound, "control-nesting": gen_control_nesting, "clone-t3": gen_clone_t3, "call-graph": gen_call_graph, "guarded-call-graph": gen_guarded_call_graph, "guarded-call-graph-active": gen_guarded_call_graph_active, "guarded-call-graph-infeasible": gen_guarded_call_graph_infeasible, } def find_fold(path: str | None) -> Path: if path: return Path(path).resolve() env = os.environ.get("FOLD_BINARY") if env: return Path(env).resolve() if DEFAULT_FOLD.exists(): return DEFAULT_FOLD found = shutil.which("fold") if found: return Path(found).resolve() raise SystemExit("fold binary not found; pass --fold or set FOLD_BINARY") def detector_profile(profile: dict[str, Any], name: str) -> dict[str, Any]: for det in profile.get("detectors", []): if det.get("name") == name: return det return {} def sarif_counts(path: Path) -> dict[str, Any]: try: doc = json.loads(path.read_text()) except Exception: return {"finding_count": 0, "by_rule": {}, "by_confidence": {}, "levels": {}} results = doc.get("runs", [{}])[0].get("results", []) by_rule: dict[str, int] = {} by_conf: dict[str, int] = {} levels: dict[str, int] = {} for r in results: by_rule[r.get("ruleId", "")] = by_rule.get(r.get("ruleId", ""), 0) + 1 props = r.get("properties", {}) conf = props.get("confidence", "") by_conf[conf] = by_conf.get(conf, 0) + 1 lvl = r.get("level", "") levels[lvl] = levels.get(lvl, 0) + 1 return { "finding_count": len(results), "by_rule": dict(sorted(by_rule.items())), "by_confidence": dict(sorted(by_conf.items())), "levels": dict(sorted(levels.items())), } def run_one(fold: Path, case: str, size: int, repeat_index: int, out_dir: Path, keep_workdir: bool) -> dict[str, Any]: spec = CASES[case] if keep_workdir: project = out_dir / "work" / f"{case}-{size}-r{repeat_index}" project.mkdir(parents=True, exist_ok=True) else: project = Path(tempfile.mkdtemp(prefix=f"fold-micro-{case}-")) try: GENERATORS[case](project, size) report = out_dir / f"{case}-{size}-r{repeat_index}.sarif" profile_path = out_dir / f"{case}-{size}-r{repeat_index}.profile.json" stdout_path = out_dir / f"{case}-{size}-r{repeat_index}.stdout.log" stderr_path = out_dir / f"{case}-{size}-r{repeat_index}.stderr.log" cmd = [ str(fold), "--quiet", "scan", "--compile-db", str(project / "compile_commands.json"), "--processes", "1", "--profile", "--profile-output", str(profile_path), "--format", "json", "--output", str(report), ] for d in spec.detectors: cmd += ["--detectors", d] if spec.uses_config_profile: cmd += ["--config-profile", str(project / "config_profile.json")] cmd += list(spec.extra_args) t0 = time.perf_counter() proc = subprocess.run(cmd, cwd=ROOT, text=True, capture_output=True) elapsed_ms = int((time.perf_counter() - t0) * 1000) stdout_path.write_text(proc.stdout) stderr_path.write_text(proc.stderr) if proc.returncode not in (0, 1): raise RuntimeError( f"{case} failed with rc={proc.returncode}; see {stderr_path}\ncmd: {' '.join(cmd)}" ) profile = json.loads(profile_path.read_text()) scan = profile.get("scan", {}) findings = sarif_counts(report) detectors = {d: detector_profile(profile, d) for d in spec.detectors} return { "case": case, "size": size, "repeat": repeat_index, "returncode": proc.returncode, "elapsed_ms_external": elapsed_ms, "scan": scan, "detectors": detectors, "findings": findings, "artifacts": { "project": str(project), "report": str(report), "profile": str(profile_path), "stdout": str(stdout_path), "stderr": str(stderr_path), }, } finally: if not keep_workdir: shutil.rmtree(project, ignore_errors=True) def summarize_case(rows: list[dict[str, Any]]) -> dict[str, Any]: def nums(path: tuple[str, ...]) -> list[float]: out = [] for r in rows: cur: Any = r ok = True for p in path: if not isinstance(cur, dict) or p not in cur: ok = False break cur = cur[p] if ok and isinstance(cur, (int, float)): out.append(float(cur)) return out def stat(values: list[float]) -> dict[str, float | int | None]: if not values: return {"n": 0, "min": None, "median": None, "mean": None, "max": None} return { "n": len(values), "min": min(values), "median": statistics.median(values), "mean": statistics.mean(values), "max": max(values), } case = rows[0]["case"] if rows else "" detector_stats: dict[str, Any] = {} for det in CASES[case].detectors: detector_stats[det] = { "wall_ms": stat(nums(("detectors", det, "wall_ms"))), "z3_queries": stat(nums(("detectors", det, "z3_queries"))), "z3_timeouts": stat(nums(("detectors", det, "z3_timeouts"))), "z3_elapsed_ms": stat(nums(("detectors", det, "z3_elapsed_ms"))), "findings_emitted": stat(nums(("detectors", det, "findings_emitted"))), "states_merged": stat(nums(("detectors", det, "states_merged"))), "states_kept_separate": stat(nums(("detectors", det, "states_kept_separate"))), "walker_ms": stat(nums(("detectors", det, "walker_ms"))), "control_walk_ms": stat(nums(("detectors", det, "control_walk_ms"))), "condition_fact_ms": stat(nums(("detectors", det, "condition_fact_ms"))), "branch_feasibility_checks": stat( nums(("detectors", det, "branch_feasibility_checks")) ), "branch_feasibility_fast_paths": stat( nums(("detectors", det, "branch_feasibility_fast_paths")) ), "env_clone_ms": stat(nums(("detectors", det, "env_clone_ms"))), "env_clone_count": stat(nums(("detectors", det, "env_clone_count"))), "encode_ms": stat(nums(("detectors", det, "encode_ms"))), "hook_ms": stat(nums(("detectors", det, "hook_ms"))), "state_merge_ms": stat(nums(("detectors", det, "state_merge_ms"))), "stmt_count": stat(nums(("detectors", det, "stmt_count"))), "control_stmt_count": stat(nums(("detectors", det, "control_stmt_count"))), "candidate_sink_count": stat(nums(("detectors", det, "candidate_sink_count"))), "functions_prechecked_safe": stat(nums(("detectors", det, "functions_prechecked_safe"))), "expr_cache_hits": stat(nums(("detectors", det, "expr_cache_hits"))), "expr_cache_misses": stat(nums(("detectors", det, "expr_cache_misses"))), } return { "case": case, "size": rows[0]["size"] if rows else None, "description": CASES[case].description if case in CASES else "", "elapsed_ms_external": stat(nums(("elapsed_ms_external",))), "total_wall_ms": stat(nums(("scan", "total_wall_ms"))), "serial_dispatch_ms": stat(nums(("scan", "serial_dispatch_ms"))), "call_graph_ms": stat(nums(("scan", "call_graph_ms"))), "project_graph_collect_ms": stat(nums(("scan", "project_graph_collect_ms"))), "project_graph_merge_ms": stat(nums(("scan", "project_graph_merge_ms"))), "project_graph_fallback_ms": stat(nums(("scan", "project_graph_fallback_ms"))), "project_index_load_ms": stat(nums(("scan", "project_index_load_ms"))), "project_index_update_ms": stat(nums(("scan", "project_index_update_ms"))), "security_candidate_ms": stat(nums(("scan", "security_candidate_ms"))), "call_graph_nodes": stat(nums(("scan", "call_graph_nodes"))), "call_graph_edges": stat(nums(("scan", "call_graph_edges"))), "peak_rss_kb": stat(nums(("scan", "peak_rss_kb"))), "num_tus": stat(nums(("scan", "num_tus"))), "num_functions": stat(nums(("scan", "num_functions"))), "detectors": detector_stats, "finding_counts": rows[-1]["findings"] if rows else {}, } def write_markdown(summary: dict[str, Any], path: Path) -> None: lines = ["# fold optimization micro-benchmarks", ""] lines.append("These benchmarks are synthetic A/B gates for speed and FP optimization patches.") lines.append("Run before and after a patch and compare `summary.json` medians.") lines.append("") for case in summary["cases"]: lines.append(f"## {case['case']} size={case['size']}") lines.append("") lines.append(case["description"]) lines.append("") lines.append("| metric | median | mean | min | max |") lines.append("|---|---:|---:|---:|---:|") for key in [ "elapsed_ms_external", "total_wall_ms", "serial_dispatch_ms", "call_graph_ms", "project_graph_collect_ms", "project_graph_merge_ms", "project_graph_fallback_ms", "project_index_load_ms", "project_index_update_ms", "security_candidate_ms", "call_graph_nodes", "call_graph_edges", "peak_rss_kb", "num_tus", "num_functions", ]: s = case[key] lines.append(f"| {key} | {s['median']} | {s['mean']} | {s['min']} | {s['max']} |") for det, stats in case["detectors"].items(): for metric, s in stats.items(): lines.append( f"| {det}.{metric} | {s['median']} | {s['mean']} | {s['min']} | {s['max']} |" ) lines.append("") lines.append("Finding distribution from last repeat:") lines.append("```json") lines.append(json.dumps(case["finding_counts"], indent=2, sort_keys=True)) lines.append("```") lines.append("") path.write_text("\n".join(lines) + "\n") def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--fold", help="Path to fold binary; defaults to build/release/src/cli/fold or $FOLD_BINARY") parser.add_argument("--case", choices=["all", *CASES.keys()], default="all") parser.add_argument("--size", type=int, help="Override synthetic workload size for selected case(s)") parser.add_argument("--repeats", type=int, default=1) parser.add_argument("--out-dir", default=None, help="Output directory; default bench/results/_micro-opt") parser.add_argument("--keep-workdir", action="store_true", help="Keep generated synthetic projects under out-dir/work") args = parser.parse_args() fold = find_fold(args.fold) if not fold.exists(): raise SystemExit(f"fold binary does not exist: {fold}") stamp = time.strftime("%Y-%m-%dT%H-%M-%SZ", time.gmtime()) out_dir = Path(args.out_dir) if args.out_dir else ROOT / "bench" / "results" / f"{stamp}_micro-opt" out_dir.mkdir(parents=True, exist_ok=True) selected = list(CASES.keys()) if args.case == "all" else [args.case] raw_rows: list[dict[str, Any]] = [] cases_summary: list[dict[str, Any]] = [] for case in selected: size = args.size if args.size is not None else CASES[case].size_default rows = [] for repeat in range(args.repeats): row = run_one(fold, case, size, repeat, out_dir, args.keep_workdir) rows.append(row) raw_rows.append(row) print( f"{case} r{repeat}: rc={row['returncode']} " f"wall={row['scan'].get('total_wall_ms')}ms findings={row['findings'].get('finding_count')}", flush=True, ) cases_summary.append(summarize_case(rows)) summary = { "tool": "fold-micro-opt", "fold_binary": str(fold), "repeats": args.repeats, "cases": cases_summary, "raw": raw_rows, } (out_dir / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n") write_markdown(summary, out_dir / "summary.md") print(f"wrote {out_dir / 'summary.json'}") print(f"wrote {out_dir / 'summary.md'}") return 0 if __name__ == "__main__": raise SystemExit(main())