Source code for ppbcc.benchmark.reports
"""Normalize Google Benchmark JSON reports into tidy tables."""
from __future__ import annotations
import json
from pathlib import Path
import pandas as pd
from loguru import logger
from ppbcc.constants import (
BENCHMARK_PROBLEM,
COLUMN_LIST,
DESCRIPTION,
HARDWARE,
ITERATIONS,
PARADIGM,
PRECISION,
PROBLEM_SIZE,
RAW_KEY_TO_COLUMN,
RUNTIME_COLUMNS,
)
def _normalize_report(json_data: dict, hardware: str) -> pd.DataFrame:
"""Turn a parsed Google Benchmark report into a tidy table.
Args:
json_data: Parsed report object.
hardware: Hardware label to attach to every row.
Returns:
Normalized benchmark rows.
"""
context = json_data["context"]
paradigm = context.get("paradigm", "Unknown")
float_type = str(context.get("float_type", "Unknown"))
df = pd.DataFrame(json_data["benchmarks"])
if df.empty:
return df
# Drop aggregate rows (BigO / RMS / mean / stddev ...); we only want raw runs.
if "run_type" in df.columns:
df = df[df["run_type"] == "iteration"].copy()
if df.empty:
return df
# --- Parse the encoded name -------------------------------------------- #
# Convention: "<BenchmarkProblem>[-<Description>][/<ProblemSize>]"
name_size = df["name"].str.split("/", expand=True)
base = name_size[0]
has_size_in_name = name_size.shape[1] > 1
details = base.str.split("-", n=1, expand=True)
df[BENCHMARK_PROBLEM] = details[0].astype(str)
df[DESCRIPTION] = details[1].fillna("").astype(str) if details.shape[1] > 1 else ""
# --- Problem size ------------------------------------------------------- #
if has_size_in_name:
df[PROBLEM_SIZE] = pd.to_numeric(name_size[1], errors="coerce")
elif "NumFaces" in df.columns:
# The polyhedral benchmark has no size in the name; it reports the number
# of faces of the processed mesh as a user counter instead.
df[PROBLEM_SIZE] = pd.to_numeric(df["NumFaces"], errors="coerce")
else:
df[PROBLEM_SIZE] = pd.NA
# --- Context columns ---------------------------------------------------- #
df[PARADIGM] = paradigm
df[PRECISION] = float_type
df[HARDWARE] = hardware
# --- Rename the runtime/counter columns we know about ------------------- #
df = df.rename(columns={k: v for k, v in RAW_KEY_TO_COLUMN.items() if k in df})
# Ensure every output column exists, then select & order them.
for col in COLUMN_LIST:
if col not in df.columns:
df[col] = pd.NA
df = df[COLUMN_LIST]
# Coerce numeric columns.
for col in [PROBLEM_SIZE, ITERATIONS, *RUNTIME_COLUMNS]:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df
[docs]
def load_reports(report_files: list[Path], hardware: str = "") -> pd.DataFrame:
"""Load and normalize a list of Google-Benchmark JSON reports.
Args:
report_files: Paths to JSON report files.
hardware: Free-form hardware identifier stored in the ``Hardware`` column
(e.g. ``"RTX5080"``).
Returns:
A single tidy DataFrame (one row per benchmark run).
"""
frames: list[pd.DataFrame] = []
logger.info(f"Loading {len(report_files)} report file(s)")
for file_path in report_files:
logger.debug(f"Loading {file_path}")
try:
with open(file_path, "r") as fh:
json_data = json.load(fh)
except (ValueError, OSError) as e:
logger.error(f"Could not read {file_path}: {e}")
continue
df = _normalize_report(json_data, hardware)
if df.empty:
logger.warning(f"No usable benchmark rows in {file_path}")
continue
frames.append(df)
if not frames:
logger.error("No benchmark data could be loaded.")
return pd.DataFrame(columns=COLUMN_LIST)
combined = pd.concat(frames, ignore_index=True)
logger.success(
f"Loaded {len(combined)} rows across "
f"{combined[BENCHMARK_PROBLEM].nunique()} problem(s) and "
f"{combined[PARADIGM].nunique()} paradigm(s)"
)
return combined
# --------------------------------------------------------------------------- #
# Plotting helpers
# --------------------------------------------------------------------------- #