summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorhathach <[email protected]>2025-12-03 11:09:41 +0700
committerhathach <[email protected]>2025-12-03 23:04:49 +0700
commitf51ca33f25841147e93c72458c927261806cdc0e (patch)
tree7882c1ebe7d468512a3d7d770000474d7d01af0e /tools
parentee3d3e3551f95757b85de1c2c9777a1daed8f78d (diff)
upload metrics.json and aggregate code metrics, post metrics comment
fine tune ci matrix run
Diffstat (limited to 'tools')
-rwxr-xr-xtools/build.py2
-rwxr-xr-xtools/get_deps.py2
-rw-r--r--tools/metrics.py254
3 files changed, 236 insertions, 22 deletions
diff --git a/tools/build.py b/tools/build.py
index b87af6c6a..e4909f45f 100755
--- a/tools/build.py
+++ b/tools/build.py
@@ -122,7 +122,7 @@ def cmake_board(board, build_args, build_flags_on):
rcmd = run_cmd(cmd)
if rcmd.returncode == 0:
ret[0] += 1
- run_cmd(["cmake", "--build", build_dir, '--target', 'tinyusb_examples_metrics'])
+ run_cmd(["cmake", "--build", build_dir, '--target', 'tinyusb_metrics'])
# print(rcmd.stdout.decode("utf-8"))
else:
ret[1] += 1
diff --git a/tools/get_deps.py b/tools/get_deps.py
index 9634451e2..99e406ce7 100755
--- a/tools/get_deps.py
+++ b/tools/get_deps.py
@@ -15,7 +15,7 @@ deps_mandatory = {
'159e31b689577dbf69cf0683bbaffbd71fa5ee10',
'all'],
'tools/linkermap': ['https://github.com/hathach/linkermap.git',
- '46c3c2947db366fb66af6723709febf80d860bc1',
+ '8a8206c39d0dfd7abfa615a676b3291165fcd65c',
'all'],
'tools/uf2': ['https://github.com/microsoft/uf2.git',
'c594542b2faa01cc33a2b97c9fbebc38549df80a',
diff --git a/tools/metrics.py b/tools/metrics.py
index c6cd49d57..7e54531f5 100644
--- a/tools/metrics.py
+++ b/tools/metrics.py
@@ -3,6 +3,7 @@
import argparse
import glob
+import json
import sys
import os
@@ -39,8 +40,6 @@ def combine_maps(map_files, filters=None):
Returns:
all_json_data: Dictionary with mapfiles list and data from each map file
"""
- import json
-
filters = filters or []
all_json_data = {"mapfiles": [], "data": []}
@@ -128,24 +127,185 @@ def compute_avg(all_json_data):
return json_average
-def main(argv=None):
- parser = argparse.ArgumentParser(description='Calculate average size from linker map files')
- parser.add_argument('files', nargs='+', help='Path to map file(s) or glob pattern(s)')
- parser.add_argument('-f', '--filter', dest='filters', action='append', default=[],
- help='Only include object files whose path contains this substring (can be repeated)')
- parser.add_argument('-o', '--out', dest='out', default='metrics',
- help='Output path basename for JSON and Markdown files (default: metrics)')
- parser.add_argument('-j', '--json', dest='json_out', action='store_true',
- help='Write JSON output file')
- parser.add_argument('-m', '--markdown', dest='markdown_out', action='store_true',
- help='Write Markdown output file')
- parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
- help='Suppress summary output')
- args = parser.parse_args(argv)
+def compare_maps(base_file, new_file, filters=None):
+ """Compare two map/json files and generate difference report.
- # Expand glob patterns
- map_files = expand_files(args.files)
+ Args:
+ base_file: Path to base map/json file
+ new_file: Path to new map/json file
+ filters: List of path substrings to filter object files
+
+ Returns:
+ Dictionary with comparison data
+ """
+ filters = filters or []
+
+ # Load both files
+ base_data = combine_maps([base_file], filters)
+ new_data = combine_maps([new_file], filters)
+
+ if not base_data["data"] or not new_data["data"]:
+ return None
+
+ base_avg = compute_avg(base_data)
+ new_avg = compute_avg(new_data)
+
+ if not base_avg or not new_avg:
+ return None
+
+ # Collect all sections from both
+ all_sections = list(base_avg["sections"])
+ for s in new_avg["sections"]:
+ if s not in all_sections:
+ all_sections.append(s)
+
+ # Build file lookup
+ base_files = {f["file"]: f for f in base_avg["files"]}
+ new_files = {f["file"]: f for f in new_avg["files"]}
+
+ # Get all file names
+ all_file_names = set(base_files.keys()) | set(new_files.keys())
+
+ # Build comparison data
+ comparison = []
+ for fname in sorted(all_file_names):
+ base_f = base_files.get(fname)
+ new_f = new_files.get(fname)
+
+ row = {"file": fname, "sections": {}, "total": {}}
+
+ for section in all_sections:
+ base_val = base_f["sections"].get(section, 0) if base_f else 0
+ new_val = new_f["sections"].get(section, 0) if new_f else 0
+ row["sections"][section] = {"base": base_val, "new": new_val, "diff": new_val - base_val}
+
+ base_total = base_f["total"] if base_f else 0
+ new_total = new_f["total"] if new_f else 0
+ row["total"] = {"base": base_total, "new": new_total, "diff": new_total - base_total}
+
+ comparison.append(row)
+
+ return {
+ "base_file": base_file,
+ "new_file": new_file,
+ "sections": all_sections,
+ "files": comparison
+ }
+
+
+def format_diff(base, new, diff):
+ """Format a diff value with percentage."""
+ if base == 0 and new == 0:
+ return "0"
+ if base == 0:
+ return f"{new} (new)"
+ if new == 0:
+ return f"{base} ➡ 0"
+ if diff == 0:
+ return f"{base} ➡ {new}"
+ pct = (diff / base) * 100
+ sign = "+" if diff > 0 else ""
+ return f"{base} ➡ {new} ({sign}{diff}, {sign}{pct:.1f}%)"
+
+
+def get_sort_key(sort_order):
+ """Get sort key function based on sort order.
+
+ Args:
+ sort_order: One of 'size-', 'size+', 'name-', 'name+'
+ Returns:
+ Tuple of (key_func, reverse)
+ """
+ if sort_order == 'size-':
+ return lambda x: x.get('total', 0) if isinstance(x.get('total'), int) else x['total']['new'], True
+ elif sort_order == 'size+':
+ return lambda x: x.get('total', 0) if isinstance(x.get('total'), int) else x['total']['new'], False
+ elif sort_order == 'name-':
+ return lambda x: x.get('file', ''), True
+ else: # name+
+ return lambda x: x.get('file', ''), False
+
+
+def write_compare_markdown(comparison, path, sort_order='size'):
+ """Write comparison data to markdown file."""
+ sections = comparison["sections"]
+
+ md_lines = [
+ "# TinyUSB Code Size Different Report",
+ "",
+ f"**Base:** `{comparison['base_file']}`",
+ f"**New:** `{comparison['new_file']}`",
+ "",
+ ]
+
+ # Build header
+ header = "| File |"
+ separator = "|:-----|"
+ for s in sections:
+ header += f" {s} |"
+ separator += "-----:|"
+ header += " Total |"
+ separator += "------:|"
+
+ md_lines.append(header)
+ md_lines.append(separator)
+
+ # Sort files based on sort_order
+ if sort_order == 'size-':
+ key_func = lambda x: abs(x["total"]["diff"])
+ reverse = True
+ elif sort_order in ('size', 'size+'):
+ key_func = lambda x: abs(x["total"]["diff"])
+ reverse = False
+ elif sort_order == 'name-':
+ key_func = lambda x: x['file']
+ reverse = True
+ else: # name or name+
+ key_func = lambda x: x['file']
+ reverse = False
+ sorted_files = sorted(comparison["files"], key=key_func, reverse=reverse)
+
+ sum_base = {s: 0 for s in sections}
+ sum_base["total"] = 0
+ sum_new = {s: 0 for s in sections}
+ sum_new["total"] = 0
+
+ for f in sorted_files:
+ # Skip files with no changes
+ if f["total"]["diff"] == 0 and all(f["sections"][s]["diff"] == 0 for s in sections):
+ continue
+
+ row = f"| {f['file']} |"
+ for s in sections:
+ sd = f["sections"][s]
+ sum_base[s] += sd["base"]
+ sum_new[s] += sd["new"]
+ row += f" {format_diff(sd['base'], sd['new'], sd['diff'])} |"
+
+ td = f["total"]
+ sum_base["total"] += td["base"]
+ sum_new["total"] += td["new"]
+ row += f" {format_diff(td['base'], td['new'], td['diff'])} |"
+
+ md_lines.append(row)
+
+ # Add sum row
+ sum_row = "| **SUM** |"
+ for s in sections:
+ diff = sum_new[s] - sum_base[s]
+ sum_row += f" {format_diff(sum_base[s], sum_new[s], diff)} |"
+ total_diff = sum_new["total"] - sum_base["total"]
+ sum_row += f" {format_diff(sum_base['total'], sum_new['total'], total_diff)} |"
+ md_lines.append(sum_row)
+
+ with open(path, "w", encoding="utf-8") as f:
+ f.write("\n".join(md_lines))
+
+
+def cmd_combine(args):
+ """Handle combine subcommand."""
+ map_files = expand_files(args.files)
all_json_data = combine_maps(map_files, args.filters)
json_average = compute_avg(all_json_data)
@@ -154,11 +314,65 @@ def main(argv=None):
sys.exit(1)
if not args.quiet:
- linkermap.print_summary(json_average, False)
+ linkermap.print_summary(json_average, False, args.sort)
if args.json_out:
linkermap.write_json(json_average, args.out + '.json')
if args.markdown_out:
- linkermap.write_markdown(json_average, args.out + '.md')
+ linkermap.write_markdown(json_average, args.out + '.md', sort_opt=args.sort,
+ title="TinyUSB Average Code Size Metrics")
+
+
+def cmd_compare(args):
+ """Handle compare subcommand."""
+ comparison = compare_maps(args.base, args.new, args.filters)
+
+ if comparison is None:
+ print("Failed to compare files", file=sys.stderr)
+ sys.exit(1)
+
+ write_compare_markdown(comparison, args.out + '.md', args.sort)
+ print(f"Comparison written to {args.out}.md")
+
+
+def main(argv=None):
+ parser = argparse.ArgumentParser(description='Code size metrics tool')
+ subparsers = parser.add_subparsers(dest='command', required=True, help='Available commands')
+
+ # Combine subcommand
+ combine_parser = subparsers.add_parser('combine', help='Combine and average multiple map files')
+ combine_parser.add_argument('files', nargs='+', help='Path to map file(s) or glob pattern(s)')
+ combine_parser.add_argument('-f', '--filter', dest='filters', action='append', default=[],
+ help='Only include object files whose path contains this substring (can be repeated)')
+ combine_parser.add_argument('-o', '--out', dest='out', default='metrics',
+ help='Output path basename for JSON and Markdown files (default: metrics)')
+ combine_parser.add_argument('-j', '--json', dest='json_out', action='store_true',
+ help='Write JSON output file')
+ combine_parser.add_argument('-m', '--markdown', dest='markdown_out', action='store_true',
+ help='Write Markdown output file')
+ combine_parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
+ help='Suppress summary output')
+ combine_parser.add_argument('-S', '--sort', dest='sort', default='name+',
+ choices=['size', 'size-', 'size+', 'name', 'name-', 'name+'],
+ help='Sort order: size/size- (descending), size+ (ascending), name/name+ (ascending), name- (descending). Default: name+')
+
+ # Compare subcommand
+ compare_parser = subparsers.add_parser('compare', help='Compare two map files')
+ compare_parser.add_argument('base', help='Base map/json file')
+ compare_parser.add_argument('new', help='New map/json file')
+ compare_parser.add_argument('-f', '--filter', dest='filters', action='append', default=[],
+ help='Only include object files whose path contains this substring (can be repeated)')
+ compare_parser.add_argument('-o', '--out', dest='out', default='metrics_compare',
+ help='Output path basename for Markdown file (default: metrics_compare)')
+ compare_parser.add_argument('-S', '--sort', dest='sort', default='name+',
+ choices=['size', 'size-', 'size+', 'name', 'name-', 'name+'],
+ help='Sort order: size/size- (descending), size+ (ascending), name/name+ (ascending), name- (descending). Default: name+')
+
+ args = parser.parse_args(argv)
+
+ if args.command == 'combine':
+ cmd_combine(args)
+ elif args.command == 'compare':
+ cmd_compare(args)
if __name__ == '__main__':