Files
2026-07-13 13:05:14 +08:00

206 lines
5.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Compare sizes of a list of files.
This produces the format for use in <https://github.com/benchmark-action/github-action-benchmark>.
Use the script:
python3 scripts/ci/compare.py --help
python3 scripts/ci/compare.py --threshold=20 previous.json current.json
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
def get_unit(size: float) -> str:
UNITS = ["B", "kiB", "MiB", "GiB", "TiB"]
unit_index = 0
while size > 1024:
size /= 1024
unit_index += 1
return UNITS[unit_index]
DIVISORS = {
"B": 1,
"kiB": 1024,
"MiB": 1024 * 1024,
"GiB": 1024 * 1024 * 1024,
"TiB": 1024 * 1024 * 1024 * 1024,
}
def get_divisor(unit: str) -> int:
return DIVISORS[unit]
def render_table_dict(data: list[dict[str, str]]) -> str:
keys = data[0].keys()
column_widths = [max(len(key), *(len(str(row[key])) for row in data)) for key in keys]
separator = "|" + "|".join("-" * (width + 2) for width in column_widths)
header_row = "|".join(f" {key.center(width)} " for key, width in zip(keys, column_widths, strict=False))
table = f"|{header_row}|\n{separator}|\n"
for row in data:
row_str = "|".join(
f" {str(row.get(key, '')).ljust(width)} " for key, width in zip(keys, column_widths, strict=False)
)
table += f"|{row_str}|\n"
return table
def render_table_rows(rows: list[Any], headers: list[str]) -> str:
column_widths = [max(len(str(item)) for item in col) for col in zip(*([tuple(headers), *rows]), strict=False)]
separator = "|" + "|".join("-" * (width + 2) for width in column_widths)
header_row = "|".join(f" {header.center(width)} " for header, width in zip(headers, column_widths, strict=False))
table = f"|{header_row}|\n{separator}|\n"
for row in rows:
row_str = "|".join(f" {str(item).ljust(width)} " for item, width in zip(row, column_widths, strict=False))
table += f"|{row_str}|\n"
return table
def compare(
previous_path: str,
current_path: str,
threshold_pct: float,
before_header: str,
after_header: str,
) -> None:
previous = json.loads(Path(previous_path).read_text(encoding="utf-8"))
current = json.loads(Path(current_path).read_text(encoding="utf-8"))
entries = {}
for entry in current:
name = entry["name"]
entries[name] = {"current": entry}
for entry in previous:
name = entry["name"]
if name not in entries:
entries[name] = {}
entries[name]["previous"] = entry
headers = ["Name", before_header, after_header, "Change"]
rows: list[tuple[str, str, str, str]] = []
for name, entry in entries.items():
if "previous" in entry and "current" in entry:
previous_unit = entry["previous"]["unit"]
current_unit = entry["current"]["unit"]
previous = float(entry["previous"]["value"])
current = float(entry["current"]["value"])
if previous_unit == current_unit:
div = 1
unit = previous_unit
else:
previous_divisor = DIVISORS.get(previous_unit, 1)
current_divisor = DIVISORS.get(current_unit, 1)
previous_bytes = previous * previous_divisor
current_bytes = current * current_divisor
unit = get_unit(min(previous_bytes, current_bytes))
div = get_divisor(unit)
previous = previous_bytes / div
current = current_bytes / div
if previous == current:
change_pct = 0.0 # e.g. both are zero
elif previous == 0:
change_pct = 100.0
else:
change_pct = 100 * (current - previous) / previous
if abs(change_pct) >= threshold_pct:
if unit in DIVISORS:
change = f"{change_pct:+.2f}%"
else:
change = f"{format_num(current - previous)} {unit}"
rows.append((
name,
f"{format_num(previous)} {unit}",
f"{format_num(current)} {unit}",
change,
))
elif "current" in entry:
value = entry["current"]["value"]
unit = entry["current"]["unit"]
rows.append((name, "(none)", f"{value} {unit}", "+100%"))
elif "previous" in entry:
value = entry["previous"]["value"]
unit = entry["previous"]["unit"]
rows.append((name, f"{value} {unit}", "(deleted)", "-100%"))
if len(rows) > 0:
sys.stdout.write(render_table_rows(rows, headers))
sys.stdout.flush()
def format_num(num: float) -> str:
if num.is_integer():
return str(int(num))
return f"{num:.2f}"
def percentage(value: str) -> float:
value = value.replace("%", "")
return float(value)
def main() -> None:
parser = argparse.ArgumentParser(description="Generate a PR summary page")
parser.add_argument("before", type=str, help="Previous result .json file")
parser.add_argument("after", type=str, help="Current result .json file")
parser.add_argument(
"--threshold",
type=percentage,
required=False,
default=20,
help="Only print row if value is N%% larger or smaller",
)
parser.add_argument(
"--before-header",
type=str,
required=False,
default="Before",
help="Header for before column",
)
parser.add_argument(
"--after-header",
type=str,
required=False,
default="After",
help="Header for after column",
)
args = parser.parse_args()
compare(
args.before,
args.after,
args.threshold,
args.before_header,
args.after_header,
)
if __name__ == "__main__":
main()