Files
manifold/scripts/publish_weekly_benchmark_results.py
T
AnshulandClaude Sonnet 4.6 ff42ddc885 Weekly benchmarks (#1758)
* support weekly benchmarks

* updated for perftest

* rem clipper

* other tests

* parse cmake output + use ember-benchmark-cases.json

* add only required STl files for ember tests

* remove ember tests+fix corrupted stl file

* shell script to py + minor changes

* mac-os

* more info of machine with linux fallback

* storage for weekly benchmarks

* rebase

* binary stl

* clean

* .

* fix ember phase capture and drop removed phases

- merge stderr into stdout in run_ember_case so man_bench diagnostic
  output is not silently dropped
- remove Assembly/Triangulation/Simplification/Sorting from
  INDEPENDENT_PHASES; their Timer::Print calls were removed in PR #1745
  and they always produced 0.0; only Intersect12 and Winding03 phases
  are still reported
- update build_ember_summary table to match the 4 remaining phases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert runner

* add frontend + trigger on publish + san extended test

* fix formatting

* TEMP: allow workflow_dispatch to exercise data-publish steps for testing

Not for merge - revert before this PR lands. Only relaxing the if: gate
so a manual run can validate the folded run: > block and sanitizer arg
passing reviewers asked about, without waiting for a scheduled/release run.

* Revert "TEMP: allow workflow_dispatch to exercise data-publish steps for testing"

This reverts commit e00c5f5784eefa27255681dce4105bd45be3daa0.

* Move weekly sanitizer lane to Linux, fix artifact path, derive compiler

- Move build_sanitizer/test_sanitizer in weekly_benchmark.yml from
  macos-15 to ubuntu-24.04, matching the existing PR-triggered sanitizer
  lane in manifold.yml, since nothing about ASan/UBSan requires macOS
  here. Drops the compiler overrides and macOS-only setup steps.
- Download sanitizer build artifacts into build/ instead of the
  workspace root, so manifold_test's baked-in RPATH to libsamples can
  actually resolve. It was crashing on load with
  "Library not loaded: @rpath/libsamples.3.dylib" because the artifact
  download flattened the build/ directory away.
- Capture the sanitizer build's own cmake configure log and derive
  SANITIZER_SUMMARY_COMPILER from it at runtime instead of hardcoding
  the string, reusing system_metadata.py's existing helpers.
- Trim set -euo pipefail to set -u in the data-publish steps, since
  GitHub Actions' default bash invocation already applies -e/pipefail.
- Remove the duplicate weeklybenchmark-test job from manifold.yml in
  favor of testing weekly_benchmark.yml directly via workflow_dispatch.

* Merge dashboard pages + remove dead JS fallbacks + gtest JSON output + simplify perf summary formatting + clean up .gitignore

* Stop forcing clang-18 for sanitizer builds, use runner default (gcc)

* small changes

* -Wno-maybe-uninitialized

* review changes

* dead code

* fix cmke read

* change flags

* remove redundant flags and schema version

* /dashboard route + remove upload route + change dead and retention changes

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-02 17:14:38 +08:00

152 lines
4.9 KiB
Python

#!/usr/bin/env python3
import argparse
import datetime
import json
import os
import shutil
import sys
from pathlib import Path
SCHEMA_VERSION = 1
# Metadata fields copied into each index entry. The index is a lightweight
# listing, so it deliberately carries a subset - the full metadata (cmake
# details, per-core CPU counts, ...) stays in the run's result.json.
INDEX_METADATA_FIELDS = (
"commit_sha",
"workflow",
"runner",
"os",
"compiler",
"cpu_model",
"cpu_count",
)
def utc_now() -> datetime.datetime:
return datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
def parse_timestamp(raw: str) -> datetime.datetime:
return datetime.datetime.fromisoformat(raw.replace("Z", "+00:00")).astimezone(
datetime.timezone.utc
)
def json_dump(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
def github_run_url(run_id: str) -> str | None:
server = os.getenv("GITHUB_SERVER_URL")
repo = os.getenv("GITHUB_REPOSITORY")
if not server or not repo or not os.getenv("GITHUB_RUN_ID"):
return None
return f"{server}/{repo}/actions/runs/{run_id}"
def load_index(index_path: Path) -> dict:
if not index_path.exists():
return {"schema_version": SCHEMA_VERSION, "runs": []}
return json.loads(index_path.read_text(encoding="utf-8-sig"))
def optional_field(payload: dict, key: str, value: str | int | None) -> None:
if value not in (None, ""):
payload[key] = value
def sanitizer_summary(args: argparse.Namespace) -> dict | None:
summary = {}
optional_field(summary, "subset", args.sanitizer_subset)
optional_field(summary, "build_result", args.sanitizer_build_result)
optional_field(summary, "test_result", args.sanitizer_test_result)
optional_field(summary, "runner", args.sanitizer_runner)
optional_field(summary, "os", args.sanitizer_os)
return summary or None
def main() -> int:
parser = argparse.ArgumentParser(description="Publish weekly benchmark result files.")
parser.add_argument("suite_dir", type=Path)
parser.add_argument("data_dir", type=Path)
parser.add_argument("--release-tag")
parser.add_argument("--sanitizer-subset")
parser.add_argument("--sanitizer-build-result")
parser.add_argument("--sanitizer-test-result")
parser.add_argument("--sanitizer-runner")
parser.add_argument("--sanitizer-os")
args = parser.parse_args()
result_path = args.suite_dir / "result.json"
summary_path = args.suite_dir / "summary.md"
if not result_path.exists():
raise FileNotFoundError(f"Missing weekly benchmark result: {result_path}")
if not summary_path.exists():
raise FileNotFoundError(f"Missing weekly benchmark summary: {summary_path}")
result = json.loads(result_path.read_text(encoding="utf-8-sig"))
metadata = result["metadata"]
timestamp = parse_timestamp(metadata["timestamp"])
timestamp_iso = timestamp.isoformat().replace("+00:00", "Z")
run_id = os.getenv("GITHUB_RUN_ID") or timestamp.strftime("%Y%m%dT%H%M%SZ")
dated_rel = (
Path("weekly")
/ timestamp.strftime("%Y")
/ timestamp.strftime("%m")
/ timestamp.strftime("%d")
/ run_id
)
dated_dir = args.data_dir / dated_rel
dated_dir.mkdir(parents=True, exist_ok=True)
entry = {
"run_id": run_id,
"timestamp": timestamp_iso,
"date": timestamp.strftime("%Y-%m-%d"),
**{name: metadata[name] for name in INDEX_METADATA_FIELDS},
"result_path": str(dated_rel / "result.json").replace("\\", "/"),
"summary_path": str(dated_rel / "summary.md").replace("\\", "/"),
"github_run_url": github_run_url(run_id),
}
if args.release_tag:
entry["release_tag"] = args.release_tag
sanitizer = sanitizer_summary(args)
if sanitizer:
entry["sanitizer"] = sanitizer
entry["trigger"] = "release" if args.release_tag else "weekly"
result_dest = dated_dir / "result.json"
summary_dest = dated_dir / "summary.md"
stored_result = dict(result)
stored_result["storage"] = entry
json_dump(result_dest, stored_result)
shutil.copy2(summary_path, summary_dest)
json_dump(args.data_dir / "weekly" / "latest.json", stored_result)
index_path = args.data_dir / "weekly" / "index.json"
index = load_index(index_path)
runs = [run for run in index.get("runs", []) if str(run.get("run_id")) != run_id]
runs.append(entry)
runs.sort(key=lambda run: run.get("timestamp", ""))
index = {
"schema_version": SCHEMA_VERSION,
"generated_at": utc_now().isoformat().replace("+00:00", "Z"),
"latest_run_id": run_id,
"runs": runs,
}
json_dump(index_path, index)
print(f"Published weekly benchmark result to {result_dest}")
print(f"Updated {index_path}")
return 0
if __name__ == "__main__":
sys.exit(main())