9
9
import subprocess as sp
10
10
import sys
11
11
from dataclasses import dataclass
12
+ from glob import glob , iglob
12
13
from inspect import cleandoc
13
14
from os import getenv
14
15
from pathlib import Path
18
19
"""
19
20
usage:
20
21
21
- ./ci/ci-util.py <SUBCOMMAND>
22
+ ./ci/ci-util.py <COMMAND> [flags]
22
23
23
- SUBCOMMAND:
24
- generate-matrix Calculate a matrix of which functions had source change,
25
- print that as JSON object.
24
+ COMMAND:
25
+ generate-matrix
26
+ Calculate a matrix of which functions had source change, print that as
27
+ a JSON object.
28
+
29
+ locate-baseline [--download] [--extract]
30
+ Locate the most recent benchmark baseline available in CI and, if flags
31
+ specify, download and extract it. Never exits with nonzero status if
32
+ downloading fails.
33
+
34
+ Note that `--extract` will overwrite files in `iai-home`.
35
+
36
+ check-regressions [iai-home]
37
+ Check `iai-home` (or `iai-home` if unspecified) for `summary.json`
38
+ files and see if there are any regressions. This is used as a workaround
39
+ for `iai-callgrind` not exiting with error status; see
40
+ <https://github.com/iai-callgrind/iai-callgrind/issues/337>.
26
41
"""
27
42
)
28
43
29
44
REPO_ROOT = Path (__file__ ).parent .parent
30
45
GIT = ["git" , "-C" , REPO_ROOT ]
46
+ DEFAULT_BRANCH = "master"
47
+ WORKFLOW_NAME = "CI" # Workflow that generates the benchmark artifacts
48
+ ARTIFACT_GLOB = "baseline-icount*"
31
49
32
50
# Don't run exhaustive tests if these files change, even if they contaiin a function
33
51
# definition.
40
58
TYPES = ["f16" , "f32" , "f64" , "f128" ]
41
59
42
60
61
+ def eprint (* args , ** kwargs ):
62
+ """Print to stderr."""
63
+ print (* args , file = sys .stderr , ** kwargs )
64
+
65
+
43
66
class FunctionDef (TypedDict ):
44
67
"""Type for an entry in `function-definitions.json`"""
45
68
@@ -145,9 +168,125 @@ def make_workflow_output(self) -> str:
145
168
return output
146
169
147
170
148
- def eprint (* args , ** kwargs ):
149
- """Print to stderr."""
150
- print (* args , file = sys .stderr , ** kwargs )
171
+ def locate_baseline (flags : list [str ]) -> None :
172
+ """Find the most recent baseline from CI, download it if specified.
173
+
174
+ This returns rather than erroring, even if the `gh` commands fail. This is to avoid
175
+ erroring in CI if the baseline is unavailable (artifact time limit exceeded, first
176
+ run on the branch, etc).
177
+ """
178
+
179
+ download = False
180
+ extract = False
181
+
182
+ while len (flags ) > 0 :
183
+ match flags [0 ]:
184
+ case "--download" :
185
+ download = True
186
+ case "--extract" :
187
+ extract = True
188
+ case _:
189
+ eprint (USAGE )
190
+ exit (1 )
191
+ flags = flags [1 :]
192
+
193
+ if extract and not download :
194
+ eprint ("cannot extract without downloading" )
195
+ exit (1 )
196
+
197
+ try :
198
+ # Locate the most recent job to complete with success on our branch
199
+ latest_job = sp .check_output (
200
+ [
201
+ "gh" ,
202
+ "run" ,
203
+ "list" ,
204
+ "--limit=1" ,
205
+ "--status=success" ,
206
+ f"--branch={ DEFAULT_BRANCH } " ,
207
+ "--json=databaseId,url,headSha,conclusion,createdAt,"
208
+ "status,workflowDatabaseId,workflowName" ,
209
+ f'--jq=select(.[].workflowName == "{ WORKFLOW_NAME } ")' ,
210
+ ],
211
+ text = True ,
212
+ )
213
+ eprint (f"latest: '{ latest_job } '" )
214
+ except sp .CalledProcessError as e :
215
+ eprint (f"failed to run github command: { e } " )
216
+ return
217
+
218
+ try :
219
+ latest = json .loads (latest_job )[0 ]
220
+ eprint ("latest job: " , json .dumps (latest , indent = 4 ))
221
+ except json .JSONDecodeError as e :
222
+ eprint (f"failed to decode json '{ latest_job } ', { e } " )
223
+ return
224
+
225
+ if not download :
226
+ eprint ("--download not specified, returning" )
227
+ return
228
+
229
+ job_id = latest .get ("databaseId" )
230
+ if job_id is None :
231
+ eprint ("skipping download step" )
232
+ return
233
+
234
+ sp .run (
235
+ ["gh" , "run" , "download" , str (job_id ), f"--pattern={ ARTIFACT_GLOB } " ],
236
+ check = False ,
237
+ )
238
+
239
+ if not extract :
240
+ eprint ("skipping extraction step" )
241
+ return
242
+
243
+ # Find the baseline with the most recent timestamp. GH downloads the files to e.g.
244
+ # `some-dirname/some-dirname.tar.xz`, so just glob the whole thing together.
245
+ candidate_baselines = glob (f"{ ARTIFACT_GLOB } /{ ARTIFACT_GLOB } " )
246
+ if len (candidate_baselines ) == 0 :
247
+ eprint ("no possible baseline directories found" )
248
+ return
249
+
250
+ candidate_baselines .sort (reverse = True )
251
+ baseline_archive = candidate_baselines [0 ]
252
+ eprint (f"extracting { baseline_archive } " )
253
+ sp .run (["tar" , "xJvf" , baseline_archive ], check = True )
254
+ eprint ("baseline extracted successfully" )
255
+
256
+
257
+ def check_iai_regressions (iai_home : str | None | Path ):
258
+ """Find regressions in iai summary.json files, exit with failure if any are
259
+ found.
260
+ """
261
+ if iai_home is None :
262
+ iai_home = "iai-home"
263
+ iai_home = Path (iai_home )
264
+
265
+ found_summaries = False
266
+ regressions = []
267
+ for summary_path in iglob ("**/summary.json" , root_dir = iai_home , recursive = True ):
268
+ found_summaries = True
269
+ with open (iai_home / summary_path , "r" ) as f :
270
+ summary = json .load (f )
271
+
272
+ summary_regs = []
273
+ run = summary ["callgrind_summary" ]["callgrind_run" ]
274
+ name_entry = {"name" : f"{ summary ["function_name" ]} .{ summary ["id" ]} " }
275
+
276
+ for segment in run ["segments" ]:
277
+ summary_regs .extend (segment ["regressions" ])
278
+
279
+ summary_regs .extend (run ["total" ]["regressions" ])
280
+
281
+ regressions .extend (name_entry | reg for reg in summary_regs )
282
+
283
+ if not found_summaries :
284
+ eprint (f"did not find any summary.json files within { iai_home } " )
285
+ exit (1 )
286
+
287
+ if len (regressions ) > 0 :
288
+ eprint ("Found regressions:" , json .dumps (regressions , indent = 4 ))
289
+ exit (1 )
151
290
152
291
153
292
def main ():
@@ -156,6 +295,12 @@ def main():
156
295
ctx = Context ()
157
296
output = ctx .make_workflow_output ()
158
297
print (f"matrix={ output } " )
298
+ case ["locate-baseline" , * flags ]:
299
+ locate_baseline (flags )
300
+ case ["check-regressions" ]:
301
+ check_iai_regressions (None )
302
+ case ["check-regressions" , iai_home ]:
303
+ check_iai_regressions (iai_home )
159
304
case ["--help" | "-h" ]:
160
305
print (USAGE )
161
306
exit ()
0 commit comments