mirror of
https://github.com/meshtastic/firmware.git
synced 2026-06-15 12:10:42 -04:00
* feat: add firmware size reporting and comparison scripts from #9860 * feat: add silence output feature to size_report and implement tests for size reporting scripts * rm shame * Fix baseline artifact paths in size report workflow * feat: add firmware size reporting and comparison scripts from #9860 * feat: add silence output feature to size_report and implement tests for size reporting scripts * rm shame * Fix baseline artifact paths in size report workflow * fix write permissions * tunk --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Austin <vidplace7@gmail.com>
172 lines
5.1 KiB
Python
172 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
|
|
"""Compare firmware size reports and generate a markdown summary.
|
|
|
|
Usage:
|
|
size_report.py <new_sizes.json> [--baseline <label>:<old_sizes.json>]...
|
|
|
|
Examples:
|
|
# Compare PR against develop and master baselines
|
|
size_report.py pr.json --baseline develop:develop.json --baseline master:master.json
|
|
|
|
# Single baseline comparison
|
|
size_report.py pr.json --baseline develop:develop.json
|
|
|
|
# No baselines — shows sizes with blank delta columns
|
|
size_report.py pr.json
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
|
|
|
|
def load_sizes(path):
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def format_delta(n):
|
|
"""Format byte delta with sign and human-friendly suffix."""
|
|
sign = "+" if n > 0 else ""
|
|
if abs(n) >= 1024:
|
|
return f"{sign}{n:,} ({sign}{n / 1024:.1f} KB)"
|
|
return f"{sign}{n:,}"
|
|
|
|
|
|
def generate_markdown(new_sizes, baselines, top_n=5):
|
|
"""Generate a single table with current size and delta columns per baseline.
|
|
|
|
baselines: list of (label, old_sizes_dict), may be empty
|
|
"""
|
|
labels = [label for label, _ in baselines]
|
|
|
|
# Build rows: (board, current_size, [(delta, abs_delta) per baseline])
|
|
rows = []
|
|
for board in sorted(new_sizes):
|
|
current = new_sizes[board]
|
|
deltas = []
|
|
for _, old_sizes in baselines:
|
|
old = old_sizes.get(board)
|
|
if old is not None:
|
|
d = current - old
|
|
deltas.append((d, abs(d)))
|
|
else:
|
|
deltas.append((None, 0))
|
|
# Sort key: max abs delta across baselines (biggest changes first)
|
|
max_abs = max((ad for _, ad in deltas), default=0)
|
|
rows.append((board, current, deltas, max_abs))
|
|
|
|
rows.sort(key=lambda r: r[3], reverse=True)
|
|
|
|
# Summary line
|
|
sections = []
|
|
summary_parts = [f"{len(new_sizes)} targets"]
|
|
for i, (label, old_sizes) in enumerate(baselines):
|
|
increases = sum(
|
|
1 for _, _, deltas, _ in rows if deltas[i][0] is not None and deltas[i][0] > 0
|
|
)
|
|
decreases = sum(
|
|
1 for _, _, deltas, _ in rows if deltas[i][0] is not None and deltas[i][0] < 0
|
|
)
|
|
net = sum(
|
|
deltas[i][0] for _, _, deltas, _ in rows if deltas[i][0] is not None
|
|
)
|
|
parts = []
|
|
if increases:
|
|
parts.append(f"{increases} increased")
|
|
if decreases:
|
|
parts.append(f"{decreases} decreased")
|
|
if parts:
|
|
parts.append(f"net {format_delta(net)}")
|
|
summary_parts.append(f"vs `{label}`: {', '.join(parts)}")
|
|
else:
|
|
summary_parts.append(f"vs `{label}`: no changes")
|
|
|
|
if not baselines:
|
|
summary_parts.append("no baseline available yet")
|
|
|
|
sections.append(f"**{' | '.join(summary_parts)}**\n")
|
|
|
|
# Table header
|
|
header = "| Target | Size |"
|
|
separator = "|--------|-----:|"
|
|
for label in labels:
|
|
header += f" vs `{label}` |"
|
|
separator += "----------:|"
|
|
sections.append(header)
|
|
sections.append(separator)
|
|
|
|
def format_row(board, current, deltas):
|
|
row = f"| `{board}` | {current:,} |"
|
|
for d, _ in deltas:
|
|
if d is None:
|
|
row += " |"
|
|
elif d == 0:
|
|
row += " 0 |"
|
|
else:
|
|
icon = "📈" if d > 0 else "📉"
|
|
row += f" {icon} {format_delta(d)} |"
|
|
return row
|
|
|
|
# Top N rows always visible
|
|
top = rows[:top_n]
|
|
for board, current, deltas, _ in top:
|
|
sections.append(format_row(board, current, deltas))
|
|
|
|
# Remaining rows in expandable section
|
|
rest = rows[top_n:]
|
|
if rest:
|
|
sections.append("")
|
|
sections.append(
|
|
f"<details><summary>Show {len(rest)} more target(s)</summary>\n"
|
|
)
|
|
sections.append(header)
|
|
sections.append(separator)
|
|
for board, current, deltas, _ in rest:
|
|
sections.append(format_row(board, current, deltas))
|
|
sections.append("\n</details>")
|
|
|
|
sections.append("")
|
|
return "\n".join(sections)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Compare firmware size reports")
|
|
parser.add_argument("new_sizes", help="Path to new sizes JSON")
|
|
parser.add_argument(
|
|
"--baseline",
|
|
action="append",
|
|
default=[],
|
|
metavar="LABEL:PATH",
|
|
help="Baseline to compare against (e.g. develop:develop.json)",
|
|
)
|
|
parser.add_argument(
|
|
"--top",
|
|
type=int,
|
|
default=5,
|
|
help="Number of top changes to show before collapsing (default: 5)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
new_sizes = load_sizes(args.new_sizes)
|
|
|
|
# Silence output when no targets were built — repo maintainer choice
|
|
if not new_sizes:
|
|
return
|
|
|
|
baselines = []
|
|
for b in args.baseline:
|
|
if ":" not in b:
|
|
print(f"Error: baseline must be LABEL:PATH, got '{b}'", file=sys.stderr)
|
|
sys.exit(1)
|
|
label, path = b.split(":", 1)
|
|
baselines.append((label, load_sizes(path)))
|
|
|
|
md = generate_markdown(new_sizes, baselines, top_n=args.top)
|
|
print(md)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|