diff options
| author | hathach <[email protected]> | 2025-12-03 11:09:41 +0700 |
|---|---|---|
| committer | hathach <[email protected]> | 2025-12-03 23:04:49 +0700 |
| commit | f51ca33f25841147e93c72458c927261806cdc0e (patch) | |
| tree | 7882c1ebe7d468512a3d7d770000474d7d01af0e /tools | |
| parent | ee3d3e3551f95757b85de1c2c9777a1daed8f78d (diff) | |
upload metrics.json and aggregate code metrics, post metrics comment
fine tune ci matrix run
Diffstat (limited to 'tools')
| -rwxr-xr-x | tools/build.py | 2 | ||||
| -rwxr-xr-x | tools/get_deps.py | 2 | ||||
| -rw-r--r-- | tools/metrics.py | 254 |
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__': |
