99import subprocess as sp
1010import sys
1111from dataclasses import dataclass
12+ from glob import glob , iglob
1213from inspect import cleandoc
1314from os import getenv
1415from pathlib import Path
1819 """
1920 usage:
2021
21- ./ci/ci-util.py <SUBCOMMAND>
22+ ./ci/ci-util.py <COMMAND> [flags]
2223
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>.
2641 """
2742)
2843
2944REPO_ROOT = Path (__file__ ).parent .parent
3045GIT = ["git" , "-C" , REPO_ROOT ]
46+ DEFAULT_BRANCH = "master"
47+ WORKFLOW_NAME = "CI" # Workflow that generates the benchmark artifacts
48+ ARTIFACT_GLOB = "baseline-icount*"
3149
3250# Don't run exhaustive tests if these files change, even if they contaiin a function
3351# definition.
4058TYPES = ["f16" , "f32" , "f64" , "f128" ]
4159
4260
61+ def eprint (* args , ** kwargs ):
62+ """Print to stderr."""
63+ print (* args , file = sys .stderr , ** kwargs )
64+
65+
4366class FunctionDef (TypedDict ):
4467 """Type for an entry in `function-definitions.json`"""
4568
@@ -145,9 +168,125 @@ def make_workflow_output(self) -> str:
145168 return output
146169
147170
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 )
151290
152291
153292def main ():
@@ -156,6 +295,12 @@ def main():
156295 ctx = Context ()
157296 output = ctx .make_workflow_output ()
158297 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 )
159304 case ["--help" | "-h" ]:
160305 print (USAGE )
161306 exit ()
0 commit comments