Skip to content

Commit 90f37c9

Browse files
Add script to generate weekly report
1 parent 1bf40cf commit 90f37c9

File tree

1 file changed

+186
-0
lines changed

1 file changed

+186
-0
lines changed

triage/weekly_report.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#!/usr/bin/env python3
2+
3+
from datetime import date
4+
from math import log
5+
from pprint import pp
6+
from sys import exit
7+
import argparse
8+
import json
9+
import msgpack
10+
import sys
11+
import urllib.request
12+
13+
report = '''
14+
{date} Triage Log
15+
16+
TODO: Summary
17+
18+
Triage done by **@{username}**.
19+
Revision range: [{first_commit}..{last_commit}](https://perf.rust-lang.org/?start={first_commit}&end={last_commit}&absolute=false&stat=instructions%3Au)
20+
21+
{num_regressions} Regressions, {num_improvements} Improvements, {num_mixed} Mixed
22+
{num_rollups} of them in rollups
23+
24+
#### Regressions
25+
26+
{regressions}
27+
28+
#### Improvements
29+
30+
{improvements}
31+
32+
#### Mixed
33+
34+
{mixed}
35+
36+
#### Nags requiring follow up
37+
38+
TODO: Nags
39+
40+
'''
41+
42+
results = {
43+
'regressed': [],
44+
'improved': [],
45+
'mixed': [],
46+
}
47+
48+
49+
def relative_change(a, b):
50+
'''Returns `ln(a / b)`
51+
52+
This is prefereable to percentage change, since order doesn't matter and it
53+
scales equally for positive or negative changes.
54+
55+
For small changes, `ln(a / b) ≈ (b - a) / b`
56+
'''
57+
58+
return log(a / b)
59+
60+
61+
def gh_link(pr):
62+
return f'https://github.com/rust-lang/rust/issues/{pr}'
63+
64+
65+
def compare_link(start, end, stat='instructions:u'):
66+
return f'https://perf.rust-lang.org/compare.html?start={start}&end={end}&stat={stat}'
67+
68+
69+
def change_summary(change, status):
70+
pr = change['b']['pr']
71+
direction = status.capitalize()
72+
stat = 'instruction counts'
73+
start = change['a']['commit']
74+
end = change['b']['commit']
75+
76+
return '\n'.join([
77+
f'[#{pr}]({gh_link(pr)})'
78+
f'- {direction} results in [{stat}]({compare_link(start, end)}).'])
79+
80+
81+
def make_request_payload(start, end):
82+
payload = {
83+
'start': start,
84+
'end': end,
85+
'stat': 'instructions:u',
86+
}
87+
return bytes(json.dumps(payload), 'ascii')
88+
89+
90+
def make_request(start, end):
91+
req = urllib.request.Request('https://perf.rust-lang.org/perf/get')
92+
req.add_header('Content-Type', 'application/json')
93+
req.data = make_request_payload(start, end)
94+
with urllib.request.urlopen(req) as f:
95+
data = msgpack.unpack(f, raw=False)
96+
return data
97+
98+
99+
def do_triage(start):
100+
# Get the next commit after `start` by comparing it with itself
101+
initial_response = make_request(start, start)
102+
103+
if initial_response['next'] is None:
104+
print('Failed to get first commit', file=sys.stderr)
105+
sys.exit(1)
106+
107+
commits = [start, initial_response['next']]
108+
109+
while True:
110+
try:
111+
response = make_request(*commits)
112+
except urllib.error.HTTPError as e:
113+
print(e, file=sys.stderr)
114+
break
115+
116+
if not response['is_contiguous']:
117+
print('Reached a commit whose perf run is not yet complete',
118+
file=sys.stderr)
119+
break
120+
121+
handle_compare(response)
122+
123+
if 'next' not in response:
124+
break
125+
126+
commits[0], commits[1] = commits[1], response['next']
127+
128+
print(report.format(
129+
first_commit=start, last_commit=commits[0],
130+
date=date.today().strftime("%Y-%m-%d"),
131+
num_regressions=len(results['regressed']),
132+
num_improvements=len(results['improved']),
133+
num_mixed=len(results['mixed']),
134+
num_rollups='???',
135+
regressions='\n\n'.join(results['regressed']),
136+
improvements='\n\n'.join(results['improved']),
137+
mixed='\n\n'.join(results['mixed']),
138+
username='ecstaticmorse'
139+
))
140+
141+
142+
def handle_compare(res):
143+
print(f"Comparing {res['a']['commit']}..{res['b']['commit']}", file=sys.stderr)
144+
CHANGE_THRESHOLD = 0.01
145+
146+
data = [res[key]['data'] for key in ['a', 'b']]
147+
148+
max_regression = 0
149+
max_improvement = 0
150+
for bench_name in data[0].keys() & data[1].keys():
151+
# Ignore rustdoc benchmarks for now
152+
if bench_name.endswith('-doc'):
153+
continue
154+
155+
benches = [dict(datum[bench_name]) for datum in data]
156+
for cache_state in benches[0].keys() & benches[0].keys():
157+
measurements = [bench[cache_state] for bench in benches]
158+
rel_change = relative_change(*measurements)
159+
max_regression = min(max_regression, rel_change)
160+
max_improvement = max(max_improvement, rel_change)
161+
162+
improved = abs(max_improvement) > CHANGE_THRESHOLD
163+
regressed = abs(max_regression) > CHANGE_THRESHOLD
164+
165+
if improved and regressed:
166+
status = 'mixed'
167+
elif improved:
168+
status = 'improved'
169+
elif regressed:
170+
status = 'regressed'
171+
else:
172+
return
173+
174+
handle_significant_change(res, status)
175+
176+
177+
def handle_significant_change(res, status):
178+
results[status].append(change_summary(res, status))
179+
180+
181+
if __name__ == '__main__' and not sys.flags.inspect:
182+
parser = argparse.ArgumentParser(description='Generate a weekly triage report')
183+
parser.add_argument('first_commit', help="the last commit of last week's triage report")
184+
args = parser.parse_args()
185+
186+
do_triage(start=args.first_commit)

0 commit comments

Comments
 (0)