diff --git a/.github/workflows/reusable_track_size.yml b/.github/workflows/reusable_track_size.yml index 3e82876eee36..4df28e81bd85 100644 --- a/.github/workflows/reusable_track_size.yml +++ b/.github/workflows/reusable_track_size.yml @@ -97,12 +97,18 @@ jobs: entries+=("$name:$file:MiB") done - data=$(python3 scripts/ci/sizes.py measure "${entries[@]}") - echo "$data" > "/tmp/data.json" + python3 scripts/ci/count_bytes.py "${entries[@]}" > /tmp/sizes.json + + python3 scripts/ci/count_dependencies.py -p re_sdk --no-default-features > /tmp/deps1.json + python3 scripts/ci/count_dependencies.py -p re_viewer --all-features > /tmp/deps2.json + python3 scripts/ci/count_dependencies.py -p rerun --all-features > /tmp/deps3.json + + # Merge the results, putting dependencies first (on top): + jq -s '.[0] + .[1] + .[2] + .[3]' /tmp/deps1.json /tmp/deps2.json /tmp/deps3.json /tmp/sizes.json > /tmp/data.json comparison=$( - python3 scripts/ci/sizes.py compare \ - --threshold=5% \ + python3 scripts/ci/compare.py \ + --threshold=2% \ --before-header=${{ (inputs.PR_NUMBER && github.event.pull_request.base.ref) || 'Before' }} \ --after-header=${{ github.ref_name }} \ "/tmp/prev.json" "/tmp/data.json" diff --git a/rerun_cpp/src/rerun/c/rerun.h b/rerun_cpp/src/rerun/c/rerun.h index 474114198508..f222dbf21776 100644 --- a/rerun_cpp/src/rerun/c/rerun.h +++ b/rerun_cpp/src/rerun/c/rerun.h @@ -233,7 +233,7 @@ typedef struct rr_error { /// /// This should match the string returned by `rr_version_string`. /// If not, the SDK's binary and the C header are out of sync. -#define RERUN_SDK_HEADER_VERSION "0.13.0" +#define RERUN_SDK_HEADER_VERSION "0.14.0-alpha.2" /// Returns a human-readable version string of the Rerun C SDK. /// diff --git a/scripts/ci/sizes.py b/scripts/ci/compare.py similarity index 56% rename from scripts/ci/sizes.py rename to scripts/ci/compare.py index c415a6606d5b..fb1508f0575a 100755 --- a/scripts/ci/sizes.py +++ b/scripts/ci/compare.py @@ -1,28 +1,20 @@ #!/usr/bin/env python3 """ -Measure or compare sizes of a list of files. +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/sizes.py --help + python3 scripts/ci/compare.py --help - python3 scripts/ci/sizes.py measure \ - "Wasm":web_viewer/re_viewer_bg.wasm - - python3 scripts/ci/sizes.py measure --format=github \ - "Wasm":web_viewer/re_viewer_bg.wasm - - python3 scripts/ci/sizes.py compare --threshold=20 previous.json current.json + python3 scripts/ci/compare.py --threshold=20 previous.json current.json """ from __future__ import annotations import argparse import json -import os.path import sys -from enum import Enum from pathlib import Path from typing import Any @@ -78,17 +70,6 @@ def render_table_rows(rows: list[Any], headers: list[str]) -> str: return table -class Format(Enum): - JSON = "json" - GITHUB = "github" - - def render(self, data: list[dict[str, str]]) -> str: - if self is Format.JSON: - return json.dumps(data) - if self is Format.GITHUB: - return render_table_dict(data) - - def compare( previous_path: str, current_path: str, @@ -113,23 +94,40 @@ def compare( rows: list[tuple[str, str, str, str]] = [] for name, entry in entries.items(): if "previous" in entry and "current" in entry: - previous_bytes = float(entry["previous"]["value"]) * DIVISORS[entry["previous"]["unit"]] - current_bytes = float(entry["current"]["value"]) * DIVISORS[entry["current"]["unit"]] - unit = get_unit(min(previous_bytes, current_bytes)) - div = get_divisor(unit) - - abs_diff_bytes = abs(current_bytes - previous_bytes) - min_diff_bytes = previous_bytes * (threshold_pct / 100) - if abs_diff_bytes >= min_diff_bytes: + 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 + previous = previous_bytes / div current = current_bytes / div - change_pct = ((current_bytes - previous_bytes) / previous_bytes) * 100 + + unit = get_unit(min(previous_bytes, current_bytes)) + div = get_divisor(unit) + + change_pct = ((current - previous) / previous) * 100 + 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"{previous:.2f} {unit}", - f"{current:.2f} {unit}", - f"{change_pct:+.2f}%", + f"{format_num(previous)} {unit}", + f"{format_num(current)} {unit}", + change, ) ) elif "current" in entry: @@ -148,56 +146,36 @@ def compare( sys.stdout.flush() -def measure(files: list[str], format: Format) -> None: - output: list[dict[str, str]] = [] - for arg in files: - parts = arg.split(":") - name = parts[0] - file = parts[1] - size = os.path.getsize(file) - unit = parts[2] if len(parts) > 2 else get_unit(size) - div = get_divisor(unit) - - output.append( - { - "name": name, - "value": str(round(size / div, 2)), - "unit": unit, - } - ) +def format_num(num: float) -> str: + if num.is_integer(): + return str(int(num)) + return f"{num:.2f}" - sys.stdout.write(format.render(output)) - sys.stdout.flush() - -def percentage(value: str) -> int: +def percentage(value: str) -> float: value = value.replace("%", "") - return int(value) + return float(value) def main() -> None: parser = argparse.ArgumentParser(description="Generate a PR summary page") - - cmds_parser = parser.add_subparsers(title="cmds", dest="cmd", help="Command") - - compare_parser = cmds_parser.add_parser("compare", help="Compare results") - compare_parser.add_argument("before", type=str, help="Previous result .json file") - compare_parser.add_argument("after", type=str, help="Current result .json file") - compare_parser.add_argument( + 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", ) - compare_parser.add_argument( + parser.add_argument( "--before-header", type=str, required=False, default="Before", help="Header for before column", ) - compare_parser.add_argument( + parser.add_argument( "--after-header", type=str, required=False, @@ -205,28 +183,15 @@ def main() -> None: help="Header for after column", ) - measure_parser = cmds_parser.add_parser("measure", help="Measure sizes") - measure_parser.add_argument( - "--format", - type=Format, - choices=list(Format), - default=Format.JSON, - help="Format to render", - ) - measure_parser.add_argument("files", nargs="*", help="Entries to measure. Format: name:path[:unit]") - args = parser.parse_args() - if args.cmd == "compare": - compare( - args.before, - args.after, - args.threshold, - args.before_header, - args.after_header, - ) - elif args.cmd == "measure": - measure(args.files, args.format) + compare( + args.before, + args.after, + args.threshold, + args.before_header, + args.after_header, + ) if __name__ == "__main__": diff --git a/scripts/ci/count_bytes.py b/scripts/ci/count_bytes.py new file mode 100755 index 000000000000..e840fc25b164 --- /dev/null +++ b/scripts/ci/count_bytes.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +""" +Measure 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/count_bytes.py --help + + python3 scripts/ci/count_bytes.py \ + "Wasm":web_viewer/re_viewer_bg.wasm + + python3 scripts/ci/count_bytes.py --format=github \ + "Wasm":web_viewer/re_viewer_bg.wasm +""" +from __future__ import annotations + +import argparse +import json +import os.path +import sys +from enum import Enum +from typing import Any + + +def get_unit(size: int | 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), max(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)) + + 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)) + 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))] + separator = "|" + "|".join("-" * (width + 2) for width in column_widths) + header_row = "|".join(f" {header.center(width)} " for header, width in zip(headers, column_widths)) + + 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)) + table += f"|{row_str}|\n" + + return table + + +class Format(Enum): + JSON = "json" + GITHUB = "github" + + def render(self, data: list[dict[str, str]]) -> str: + if self is Format.JSON: + return json.dumps(data) + if self is Format.GITHUB: + return render_table_dict(data) + + +def measure(files: list[str], format: Format) -> None: + output: list[dict[str, str]] = [] + for arg in files: + parts = arg.split(":") + name = parts[0] + file = parts[1] + size = os.path.getsize(file) + unit = parts[2] if len(parts) > 2 else get_unit(size) + div = get_divisor(unit) + + output.append( + { + "name": name, + "value": str(round(size / div, 2)), + "unit": unit, + } + ) + + sys.stdout.write(format.render(output)) + sys.stdout.flush() + + +def percentage(value: str) -> int: + value = value.replace("%", "") + return int(value) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate a PR summary page") + parser.add_argument( + "--format", + type=Format, + choices=list(Format), + default=Format.JSON, + help="Format to render", + ) + parser.add_argument("files", nargs="*", help="Entries to measure. Format: name:path[:unit]") + + args = parser.parse_args() + measure(args.files, args.format) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/count_dependencies.py b/scripts/ci/count_dependencies.py new file mode 100755 index 000000000000..f1c316bb6eab --- /dev/null +++ b/scripts/ci/count_dependencies.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +""" +Count the total number of dependencies of a file (recursively). + +This produces the format for use in https://github.com/benchmark-action/github-action-benchmark. + +Use the script: + python3 scripts/ci/count_dependencies.py -p rerun --all-features + python3 scripts/ci/count_dependencies.py -p rerun --no-default-features + +Unfortunately, this script under-counts compared to what `cargo build` outputs. +There is also `cargo deps-list`, which also under-counts. +For instance: + +* `scripts/ci/count_dependencies.py -p re_sdk --no-default-features` => 118 +* `cargo deps-list -p re_sdk --no-default-features` => 165 +* `cargo check -p re_sdk --no-default-features` => 213 + +So this script is more of a heurristic than an exact count. +""" +from __future__ import annotations + +import argparse +import json +import os +import sys + + +def main() -> None: + parser = argparse.ArgumentParser(description="Count crate dependencies") + + parser.add_argument("-p", required=True, type=str, help="Crate name") + parser.add_argument("--all-features", default=False, action="store_true", help="Use all features") + parser.add_argument("--no-default-features", default=False, action="store_true", help="Use no default features") + + args = parser.parse_args() + + crate = args.p + if args.all_features: + flags = "--all-features" + elif args.no_default_features: + flags = "--no-default-features" + else: + flags = "" + cmd = f'cargo tree --edges normal -p {crate} {flags} | tail -n +2 | grep -E "\\w+ v[0-9.]+" -o | sort -u | wc -l' + print(f"Running command: {cmd}", file=sys.stderr, flush=True) + count = int(os.popen(cmd).read().strip()) + assert count > 0, f"Command failed. Maybe unknown crate? cmd: {cmd}" + print(json.dumps([{"name": f"{crate} {flags}", "value": count, "unit": "crates"}])) + + +if __name__ == "__main__": + main()