Skip to content

Commit 2625247

Browse files
emilkjprochazk
andauthored
Track crate dependency count over time (#5228)
### What * Part of #4788 * Closes #5101 ### Checklist * [x] I have read and agree to [Contributor Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and the [Code of Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md) * [x] I've included a screenshot or gif (if applicable) * [x] I have tested the web demo (if applicable): * Using newly built examples: [app.rerun.io](https://app.rerun.io/pr/5228/index.html) * Using examples from latest `main` build: [app.rerun.io](https://app.rerun.io/pr/5228/index.html?manifest_url=https://app.rerun.io/version/main/examples_manifest.json) * Using full set of examples from `nightly` build: [app.rerun.io](https://app.rerun.io/pr/5228/index.html?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json) * [x] The PR title and labels are set such as to maximize their usefulness for the next release's CHANGELOG * [x] If applicable, add a new check to the [release checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)! - [PR Build Summary](https://build.rerun.io/pr/5228) - [Docs preview](https://rerun.io/preview/0223b21104bad205cfc5004aa33386d9b52fd603/docs) <!--DOCS-PREVIEW--> - [Examples preview](https://rerun.io/preview/0223b21104bad205cfc5004aa33386d9b52fd603/examples) <!--EXAMPLES-PREVIEW--> - [Recent benchmark results](https://build.rerun.io/graphs/crates.html) - [Wasm size tracking](https://build.rerun.io/graphs/sizes.html) --------- Co-authored-by: jprochazk <[email protected]>
1 parent 9b37940 commit 2625247

File tree

5 files changed

+247
-90
lines changed

5 files changed

+247
-90
lines changed

.github/workflows/reusable_track_size.yml

+10-4
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,18 @@ jobs:
9797
entries+=("$name:$file:MiB")
9898
done
9999
100-
data=$(python3 scripts/ci/sizes.py measure "${entries[@]}")
101-
echo "$data" > "/tmp/data.json"
100+
python3 scripts/ci/count_bytes.py "${entries[@]}" > /tmp/sizes.json
101+
102+
python3 scripts/ci/count_dependencies.py -p re_sdk --no-default-features > /tmp/deps1.json
103+
python3 scripts/ci/count_dependencies.py -p re_viewer --all-features > /tmp/deps2.json
104+
python3 scripts/ci/count_dependencies.py -p rerun --all-features > /tmp/deps3.json
105+
106+
# Merge the results, putting dependencies first (on top):
107+
jq -s '.[0] + .[1] + .[2] + .[3]' /tmp/deps1.json /tmp/deps2.json /tmp/deps3.json /tmp/sizes.json > /tmp/data.json
102108
103109
comparison=$(
104-
python3 scripts/ci/sizes.py compare \
105-
--threshold=5% \
110+
python3 scripts/ci/compare.py \
111+
--threshold=2% \
106112
--before-header=${{ (inputs.PR_NUMBER && github.event.pull_request.base.ref) || 'Before' }} \
107113
--after-header=${{ github.ref_name }} \
108114
"/tmp/prev.json" "/tmp/data.json"

rerun_cpp/src/rerun/c/rerun.h

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/ci/sizes.py renamed to scripts/ci/compare.py

+50-85
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
11
#!/usr/bin/env python3
22

33
"""
4-
Measure or compare sizes of a list of files.
4+
Compare sizes of a list of files.
55
66
This produces the format for use in https://github.com/benchmark-action/github-action-benchmark.
77
88
Use the script:
9-
python3 scripts/ci/sizes.py --help
9+
python3 scripts/ci/compare.py --help
1010
11-
python3 scripts/ci/sizes.py measure \
12-
"Wasm":web_viewer/re_viewer_bg.wasm
13-
14-
python3 scripts/ci/sizes.py measure --format=github \
15-
"Wasm":web_viewer/re_viewer_bg.wasm
16-
17-
python3 scripts/ci/sizes.py compare --threshold=20 previous.json current.json
11+
python3 scripts/ci/compare.py --threshold=20 previous.json current.json
1812
"""
1913
from __future__ import annotations
2014

2115
import argparse
2216
import json
23-
import os.path
2417
import sys
25-
from enum import Enum
2618
from pathlib import Path
2719
from typing import Any
2820

@@ -78,17 +70,6 @@ def render_table_rows(rows: list[Any], headers: list[str]) -> str:
7870
return table
7971

8072

81-
class Format(Enum):
82-
JSON = "json"
83-
GITHUB = "github"
84-
85-
def render(self, data: list[dict[str, str]]) -> str:
86-
if self is Format.JSON:
87-
return json.dumps(data)
88-
if self is Format.GITHUB:
89-
return render_table_dict(data)
90-
91-
9273
def compare(
9374
previous_path: str,
9475
current_path: str,
@@ -113,23 +94,40 @@ def compare(
11394
rows: list[tuple[str, str, str, str]] = []
11495
for name, entry in entries.items():
11596
if "previous" in entry and "current" in entry:
116-
previous_bytes = float(entry["previous"]["value"]) * DIVISORS[entry["previous"]["unit"]]
117-
current_bytes = float(entry["current"]["value"]) * DIVISORS[entry["current"]["unit"]]
118-
unit = get_unit(min(previous_bytes, current_bytes))
119-
div = get_divisor(unit)
120-
121-
abs_diff_bytes = abs(current_bytes - previous_bytes)
122-
min_diff_bytes = previous_bytes * (threshold_pct / 100)
123-
if abs_diff_bytes >= min_diff_bytes:
97+
previous_unit = entry["previous"]["unit"]
98+
current_unit = entry["current"]["unit"]
99+
100+
previous = float(entry["previous"]["value"])
101+
current = float(entry["current"]["value"])
102+
103+
if previous_unit == current_unit:
104+
div = 1
105+
unit = previous_unit
106+
else:
107+
previous_divisor = DIVISORS.get(previous_unit, 1)
108+
current_divisor = DIVISORS.get(current_unit, 1)
109+
110+
previous_bytes = previous * previous_divisor
111+
current_bytes = current * current_divisor
112+
124113
previous = previous_bytes / div
125114
current = current_bytes / div
126-
change_pct = ((current_bytes - previous_bytes) / previous_bytes) * 100
115+
116+
unit = get_unit(min(previous_bytes, current_bytes))
117+
div = get_divisor(unit)
118+
119+
change_pct = ((current - previous) / previous) * 100
120+
if abs(change_pct) >= threshold_pct:
121+
if unit in DIVISORS:
122+
change = f"{change_pct:+.2f}%"
123+
else:
124+
change = f"{format_num(current - previous)} {unit}"
127125
rows.append(
128126
(
129127
name,
130-
f"{previous:.2f} {unit}",
131-
f"{current:.2f} {unit}",
132-
f"{change_pct:+.2f}%",
128+
f"{format_num(previous)} {unit}",
129+
f"{format_num(current)} {unit}",
130+
change,
133131
)
134132
)
135133
elif "current" in entry:
@@ -148,85 +146,52 @@ def compare(
148146
sys.stdout.flush()
149147

150148

151-
def measure(files: list[str], format: Format) -> None:
152-
output: list[dict[str, str]] = []
153-
for arg in files:
154-
parts = arg.split(":")
155-
name = parts[0]
156-
file = parts[1]
157-
size = os.path.getsize(file)
158-
unit = parts[2] if len(parts) > 2 else get_unit(size)
159-
div = get_divisor(unit)
160-
161-
output.append(
162-
{
163-
"name": name,
164-
"value": str(round(size / div, 2)),
165-
"unit": unit,
166-
}
167-
)
149+
def format_num(num: float) -> str:
150+
if num.is_integer():
151+
return str(int(num))
152+
return f"{num:.2f}"
168153

169-
sys.stdout.write(format.render(output))
170-
sys.stdout.flush()
171154

172-
173-
def percentage(value: str) -> int:
155+
def percentage(value: str) -> float:
174156
value = value.replace("%", "")
175-
return int(value)
157+
return float(value)
176158

177159

178160
def main() -> None:
179161
parser = argparse.ArgumentParser(description="Generate a PR summary page")
180-
181-
cmds_parser = parser.add_subparsers(title="cmds", dest="cmd", help="Command")
182-
183-
compare_parser = cmds_parser.add_parser("compare", help="Compare results")
184-
compare_parser.add_argument("before", type=str, help="Previous result .json file")
185-
compare_parser.add_argument("after", type=str, help="Current result .json file")
186-
compare_parser.add_argument(
162+
parser.add_argument("before", type=str, help="Previous result .json file")
163+
parser.add_argument("after", type=str, help="Current result .json file")
164+
parser.add_argument(
187165
"--threshold",
188166
type=percentage,
189167
required=False,
190168
default=20,
191169
help="Only print row if value is N%% larger or smaller",
192170
)
193-
compare_parser.add_argument(
171+
parser.add_argument(
194172
"--before-header",
195173
type=str,
196174
required=False,
197175
default="Before",
198176
help="Header for before column",
199177
)
200-
compare_parser.add_argument(
178+
parser.add_argument(
201179
"--after-header",
202180
type=str,
203181
required=False,
204182
default="After",
205183
help="Header for after column",
206184
)
207185

208-
measure_parser = cmds_parser.add_parser("measure", help="Measure sizes")
209-
measure_parser.add_argument(
210-
"--format",
211-
type=Format,
212-
choices=list(Format),
213-
default=Format.JSON,
214-
help="Format to render",
215-
)
216-
measure_parser.add_argument("files", nargs="*", help="Entries to measure. Format: name:path[:unit]")
217-
218186
args = parser.parse_args()
219187

220-
if args.cmd == "compare":
221-
compare(
222-
args.before,
223-
args.after,
224-
args.threshold,
225-
args.before_header,
226-
args.after_header,
227-
)
228-
elif args.cmd == "measure":
229-
measure(args.files, args.format)
188+
compare(
189+
args.before,
190+
args.after,
191+
args.threshold,
192+
args.before_header,
193+
args.after_header,
194+
)
230195

231196

232197
if __name__ == "__main__":

scripts/ci/count_bytes.py

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Measure sizes of a list of files.
5+
6+
This produces the format for use in https://github.com/benchmark-action/github-action-benchmark.
7+
8+
Use the script:
9+
python3 scripts/ci/count_bytes.py --help
10+
11+
python3 scripts/ci/count_bytes.py \
12+
"Wasm":web_viewer/re_viewer_bg.wasm
13+
14+
python3 scripts/ci/count_bytes.py --format=github \
15+
"Wasm":web_viewer/re_viewer_bg.wasm
16+
"""
17+
from __future__ import annotations
18+
19+
import argparse
20+
import json
21+
import os.path
22+
import sys
23+
from enum import Enum
24+
from typing import Any
25+
26+
27+
def get_unit(size: int | float) -> str:
28+
UNITS = ["B", "kiB", "MiB", "GiB", "TiB"]
29+
30+
unit_index = 0
31+
while size > 1024:
32+
size /= 1024
33+
unit_index += 1
34+
35+
return UNITS[unit_index]
36+
37+
38+
DIVISORS = {
39+
"B": 1,
40+
"kiB": 1024,
41+
"MiB": 1024 * 1024,
42+
"GiB": 1024 * 1024 * 1024,
43+
"TiB": 1024 * 1024 * 1024 * 1024,
44+
}
45+
46+
47+
def get_divisor(unit: str) -> int:
48+
return DIVISORS[unit]
49+
50+
51+
def render_table_dict(data: list[dict[str, str]]) -> str:
52+
keys = data[0].keys()
53+
column_widths = [max(len(key), max(len(str(row[key])) for row in data)) for key in keys]
54+
separator = "|" + "|".join("-" * (width + 2) for width in column_widths)
55+
header_row = "|".join(f" {key.center(width)} " for key, width in zip(keys, column_widths))
56+
57+
table = f"|{header_row}|\n{separator}|\n"
58+
for row in data:
59+
row_str = "|".join(f" {str(row.get(key, '')).ljust(width)} " for key, width in zip(keys, column_widths))
60+
table += f"|{row_str}|\n"
61+
62+
return table
63+
64+
65+
def render_table_rows(rows: list[Any], headers: list[str]) -> str:
66+
column_widths = [max(len(str(item)) for item in col) for col in zip(*([tuple(headers)] + rows))]
67+
separator = "|" + "|".join("-" * (width + 2) for width in column_widths)
68+
header_row = "|".join(f" {header.center(width)} " for header, width in zip(headers, column_widths))
69+
70+
table = f"|{header_row}|\n{separator}|\n"
71+
for row in rows:
72+
row_str = "|".join(f" {str(item).ljust(width)} " for item, width in zip(row, column_widths))
73+
table += f"|{row_str}|\n"
74+
75+
return table
76+
77+
78+
class Format(Enum):
79+
JSON = "json"
80+
GITHUB = "github"
81+
82+
def render(self, data: list[dict[str, str]]) -> str:
83+
if self is Format.JSON:
84+
return json.dumps(data)
85+
if self is Format.GITHUB:
86+
return render_table_dict(data)
87+
88+
89+
def measure(files: list[str], format: Format) -> None:
90+
output: list[dict[str, str]] = []
91+
for arg in files:
92+
parts = arg.split(":")
93+
name = parts[0]
94+
file = parts[1]
95+
size = os.path.getsize(file)
96+
unit = parts[2] if len(parts) > 2 else get_unit(size)
97+
div = get_divisor(unit)
98+
99+
output.append(
100+
{
101+
"name": name,
102+
"value": str(round(size / div, 2)),
103+
"unit": unit,
104+
}
105+
)
106+
107+
sys.stdout.write(format.render(output))
108+
sys.stdout.flush()
109+
110+
111+
def percentage(value: str) -> int:
112+
value = value.replace("%", "")
113+
return int(value)
114+
115+
116+
def main() -> None:
117+
parser = argparse.ArgumentParser(description="Generate a PR summary page")
118+
parser.add_argument(
119+
"--format",
120+
type=Format,
121+
choices=list(Format),
122+
default=Format.JSON,
123+
help="Format to render",
124+
)
125+
parser.add_argument("files", nargs="*", help="Entries to measure. Format: name:path[:unit]")
126+
127+
args = parser.parse_args()
128+
measure(args.files, args.format)
129+
130+
131+
if __name__ == "__main__":
132+
main()

0 commit comments

Comments
 (0)