Skip to content

Commit bd72d4d

Browse files
committed
github: add scripts for building and uploading stats
This adds some scripts for getting firmware size stats for each commit and builds interactive web pages for checking out the stats.
1 parent 15afee8 commit bd72d4d

File tree

9 files changed

+584
-12
lines changed

9 files changed

+584
-12
lines changed

.github/build-each-commit.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
STORAGE_ACCOUNT = os.environ["STORAGE_ACCOUNT"]
1313
STORAGE_KEY = os.environ["STORAGE_KEY"]
14-
TABLE_NAME = os.environ["TABLE_NAME"]
14+
FIRMWARE_SIZE_TABLE = os.environ["FIRMWARE_SIZE_TABLE"]
1515

1616
PYBRICKS_PATH = os.environ.get("PYBRICKS_PATH", ".")
1717

@@ -35,17 +35,17 @@
3535
pybricks.git.checkout(commit.hexsha)
3636

3737
# update only required submodules
38-
pybricks.git.submodule("update", "micropython")
39-
pybricks.git.submodule("update", "lib/libfixmath")
38+
pybricks.git.submodule("update", "--init", "micropython")
39+
pybricks.git.submodule("update", "--init", "lib/libfixmath")
4040
if args.hub in ["cityhub", "movehub", "technichub", "primehub"]:
4141
pybricks.submodule("micropython").module().git.submodule(
42-
"update", "lib/stm32lib"
42+
"update", "--init", "lib/stm32lib"
4343
)
4444
if args.hub == "primehub":
45-
pybricks.git.submodule("update", "--checkout", "lib/btstack")
45+
pybricks.git.submodule("update", "--init", "--checkout", "lib/btstack")
4646
if args.hub == "nxt":
4747
pybricks.git.submodule(
48-
"update", "--checkout", "bricks/nxt/nxt-firmware-drivers"
48+
"update", "--init", "--checkout", "bricks/nxt/nxt-firmware-drivers"
4949
)
5050

5151
# build the firmware
@@ -64,7 +64,7 @@
6464
bin_path = os.path.join(PYBRICKS_PATH, "bricks", args.hub, "build", "firmware.bin")
6565
size = os.path.getsize(bin_path)
6666
service.insert_or_merge_entity(
67-
TABLE_NAME,
67+
FIRMWARE_SIZE_TABLE,
6868
{
6969
"PartitionKey": "size",
7070
"RowKey": commit.hexsha,

.github/build-missing-commits.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
"""Builds any missing commits for master branch of Pybricks MicroPython"""
3+
4+
import os
5+
import subprocess
6+
import sys
7+
8+
import git
9+
10+
from azure.common import AzureMissingResourceHttpError
11+
from azure.cosmosdb.table.tableservice import TableService
12+
13+
STORAGE_ACCOUNT = os.environ["STORAGE_ACCOUNT"]
14+
STORAGE_KEY = os.environ["STORAGE_KEY"]
15+
CI_STATUS_TABLE = os.environ["CI_STATUS_TABLE"]
16+
FIRMWARE_SIZE_TABLE = os.environ["FIRMWARE_SIZE_TABLE"]
17+
18+
PYBRICKS_PATH = os.environ.get("PYBRICKS_PATH", ".")
19+
20+
PYBRICKS_BRANCH = "origin/master"
21+
22+
HUBS = ["movehub", "cityhub", "technichub", "primehub", "nxt"]
23+
24+
print("Building commits...")
25+
26+
try:
27+
pybricks = git.Repo(PYBRICKS_PATH)
28+
except Exception as e:
29+
print(f"Repository not found at '{PYBRICKS_PATH}':", e)
30+
print("try setting the PYBRICKS_PATH environment variable")
31+
sys.exit(1)
32+
33+
assert not pybricks.bare, "Repository not found"
34+
35+
service = TableService(STORAGE_ACCOUNT, STORAGE_KEY)
36+
37+
start_hash = service.get_entity(CI_STATUS_TABLE, "build", "lastHash")["hash"]
38+
39+
if start_hash == pybricks.commit(PYBRICKS_BRANCH).hexsha:
40+
print("Already up to date.")
41+
sys.exit(0)
42+
43+
44+
# Process the commits in the tree and log the data
45+
for commit in pybricks.iter_commits(f"{start_hash}..{PYBRICKS_BRANCH}"):
46+
print(f"trying {commit.hexsha}...")
47+
try:
48+
entity = service.get_entity(FIRMWARE_SIZE_TABLE, "size", commit.hexsha)
49+
# if entity is found but some hubs had null size, redo only those hubs
50+
hubs = [h for h in HUBS if entity.get(h) is None]
51+
except AzureMissingResourceHttpError:
52+
# if there is no entry at all, build all hubs
53+
hubs = HUBS
54+
55+
if not hubs:
56+
print("nothing to do for this commit")
57+
continue
58+
59+
# Checkout the Pybricks MicroPython commit for processing
60+
print("Checking out:", commit.hexsha)
61+
pybricks.git.checkout(commit.hexsha)
62+
63+
# update required submodules
64+
print("Checking out submodules")
65+
pybricks.git.submodule("update", "--init", "micropython")
66+
pybricks.git.submodule("update", "--init", "lib/libfixmath")
67+
pybricks.submodule("micropython").module().git.submodule(
68+
"update", "--init", "lib/stm32lib"
69+
)
70+
pybricks.git.submodule("update", "--init", "--checkout", "lib/btstack")
71+
pybricks.git.submodule(
72+
"update", "--init", "--checkout", "bricks/nxt/nxt-firmware-drivers"
73+
)
74+
75+
# Make mpy-cross once
76+
print("Building mpy-cross")
77+
mpy_cross_path = os.path.join(PYBRICKS_PATH, "micropython", "mpy-cross")
78+
subprocess.check_call(["make", "-C", mpy_cross_path, "CROSS_COMPILE="])
79+
80+
# Make the targets simultaneously
81+
print("Building firmware")
82+
procs = {
83+
target: subprocess.Popen(
84+
[
85+
"make",
86+
"-C",
87+
os.path.join(PYBRICKS_PATH, "bricks", target),
88+
"clean",
89+
"build/firmware.bin",
90+
]
91+
)
92+
for target in hubs
93+
}
94+
95+
# Get target sizes
96+
sizes = {}
97+
for target, proc in procs.items():
98+
# Wait for the target to complete building or fail
99+
proc.wait()
100+
101+
# Get build size on success
102+
bin_path = os.path.join(
103+
PYBRICKS_PATH, "bricks", target, "build", "firmware.bin"
104+
)
105+
try:
106+
sizes[target] = os.path.getsize(bin_path)
107+
except FileNotFoundError:
108+
pass
109+
110+
service.insert_or_replace_entity(
111+
FIRMWARE_SIZE_TABLE, {"PartitionKey": "size", "RowKey": commit.hexsha, **sizes}
112+
)
113+
114+
service.update_entity(
115+
CI_STATUS_TABLE,
116+
{
117+
"PartitionKey": "build",
118+
"RowKey": "lastHash",
119+
"hash": pybricks.commit(PYBRICKS_BRANCH).hexsha,
120+
},
121+
)

.github/build-stats-web.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#!/usr/bin/env python3
2+
"""Creates interactive graph of pybricks-micropython firmware size changes."""
3+
4+
import json
5+
import os
6+
import re
7+
from pathlib import Path
8+
9+
from plotly import graph_objects as go
10+
from plotly.offline import plot
11+
from plotly.subplots import make_subplots
12+
13+
BUILD_DIR = os.environ.get("BUILD_DIR", "build")
14+
15+
# Number of digits of hash to display
16+
HASH_SIZE = 7
17+
18+
HUBS = ["cityhub", "technichub", "movehub", "primehub", "nxt"]
19+
20+
GITHUB_REPO_URL = "https://github.com/pybricks/pybricks-micropython"
21+
22+
23+
def select(commits, hub):
24+
"""Selects the useful fields from sorted items. Skips the first diff as well
25+
as commits that did not change the firmware size.
26+
27+
Args:
28+
commits (list of dict): contents commits.json from download.py
29+
hub (str): The hub type.
30+
31+
Yields:
32+
(tuple of int, string, string, int, int) The index, commit hash, commit
33+
message, firmware size and change in size from previous commit
34+
"""
35+
prev_size = 0
36+
for i, commit in enumerate(reversed(commits)):
37+
sha = commit["oid"][:HASH_SIZE]
38+
message = commit["messageHeadline"]
39+
size = commit["firmwareSize"][hub]
40+
diff = 0
41+
if size is None:
42+
size = 0
43+
else:
44+
if prev_size != 0:
45+
diff = size - prev_size
46+
message = f"{diff:+}<br />{message}"
47+
prev_size = size
48+
49+
yield i, sha, message, size, diff
50+
51+
52+
def create_plot(commits, hub):
53+
indexes, shas, messages, sizes, diffs = zip(*select(commits, hub))
54+
55+
# Find sensible ranges to display by default
56+
x_end = len(indexes)
57+
x_start = x_end - 100
58+
y_end = max([s for s in sizes[x_start - 1 : x_end]])
59+
y_start = min([s for s in sizes[x_start - 1 : x_end]])
60+
diff_peak = max([abs(d) for d in diffs[x_start - 1 : x_end]])
61+
62+
# Create the figure with two subplots
63+
fig = make_subplots(rows=2, cols=1)
64+
fig.update_layout(
65+
showlegend=False,
66+
title_text=f"Pybricks {hub} firmware size",
67+
titlefont=dict(size=36),
68+
dragmode="zoom",
69+
)
70+
fig.update_xaxes(showticklabels=False, range=[x_start, x_end])
71+
72+
# Add and configure size plot
73+
fig.append_trace(
74+
go.Scatter(
75+
x=indexes,
76+
y=sizes,
77+
name="Size",
78+
line={"shape": "hv"},
79+
mode="lines+markers",
80+
hovertext=messages,
81+
hoverinfo="y+text",
82+
customdata=shas,
83+
),
84+
row=1,
85+
col=1,
86+
)
87+
fig.update_yaxes(
88+
row=1,
89+
exponentformat="none",
90+
tickmode="array",
91+
tickvals=[i * 1024 for i in range(256)],
92+
ticktext=[f"{i}KB" for i in range(256)],
93+
range=[y_start, y_end],
94+
)
95+
96+
# Add and configure diff plot
97+
fig.append_trace(
98+
go.Bar(
99+
x=indexes,
100+
y=diffs,
101+
hovertext=messages,
102+
hoverinfo="text+y",
103+
name="Delta",
104+
customdata=shas,
105+
),
106+
row=2,
107+
col=1,
108+
)
109+
fig.update_yaxes(row=2, range=[-diff_peak, diff_peak])
110+
111+
# Export plot
112+
# https://community.plot.ly/t/hyperlink-to-markers-on-map/17858/6
113+
114+
# Get HTML representation of plotly.js and this figure
115+
plot_div = plot(fig, output_type="div", include_plotlyjs="cdn")
116+
117+
# Get id of html div element that looks like
118+
# <div id="301d22ab-bfba-4621-8f5d-dc4fd855bb33" ... >
119+
res = re.search('<div id="([^"]*)"', plot_div)
120+
div_id = res.groups()[0]
121+
122+
# Build JavaScript callback for handling clicks
123+
# and opening the URL in the trace's customdata
124+
js_callback = f"""
125+
<script>
126+
const base_url = '{GITHUB_REPO_URL}/commit/';
127+
const plot_element = document.getElementById('{div_id}');
128+
plot_element.on('plotly_click', function(data) {{
129+
console.debug(data);
130+
const point = data.points[0];
131+
if (point) {{
132+
console.debug(point.customdata);
133+
window.open(base_url + point.customdata);
134+
}}
135+
}})
136+
</script>
137+
"""
138+
139+
# Build HTML string
140+
html_str = f"""
141+
<html>
142+
<head>
143+
</head>
144+
<body>
145+
{plot_div}
146+
{js_callback}
147+
</body>
148+
</html>
149+
"""
150+
151+
# Write out HTML file
152+
with open(Path(BUILD_DIR, f"{hub}.html"), "w") as f:
153+
f.write(html_str)
154+
155+
156+
def main():
157+
with open(Path(BUILD_DIR, "commits.json"), "r") as f:
158+
commits = json.load(f)
159+
160+
for h in HUBS:
161+
create_plot(commits, h)
162+
163+
164+
if __name__ == "__main__":
165+
main()

0 commit comments

Comments
 (0)