Skip to content

Commit cc6887f

Browse files
authored
Merge pull request #10684 from julek-wolfssl/parallel-make-check-warn-stale-minutes
parallel-make-check: warn when a job runtime drifts >50% from "minutes"
2 parents 8ef48f3 + ea3bd56 commit cc6887f

1 file changed

Lines changed: 45 additions & 4 deletions

File tree

.github/scripts/parallel-make-check.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
# minutes expected duration, from the Minutes column of a previous
2020
# run's summary (default 1.0). Schedule weight only - configs
2121
# run longest-first and --shard balances shards by it; a stale
22-
# value just packs the schedule a little worse.
22+
# value just packs the schedule a little worse, but a run
23+
# whose measured time lands more than +/-50% away from it
24+
# draws a warning (never a failure) so it is easy to spot
25+
# and update.
2326
# user_settings header staged as <builddir>/user_settings.h before
2427
# configure (path relative to the source root); pair it with
2528
# --enable-usersettings in "configure"
@@ -88,6 +91,9 @@ class Config:
8891
check: bool = True
8992
prepare: list[list[str]] = field(default_factory=list)
9093
run: list[list[str]] = field(default_factory=list)
94+
# Whether "minutes" was given in the JSON (vs the 1.0 default); only an
95+
# explicit estimate is checked for >50% drift against the real time.
96+
minutes_provided: bool = False
9197

9298
SRCDIR = Path(__file__).resolve().parents[2]
9399
ON_GITHUB = os.environ.get("GITHUB_ACTIONS") == "true"
@@ -208,7 +214,8 @@ def load_configs(opts: argparse.Namespace,
208214
entry.get("ldflags", opts.ldflags),
209215
float(minutes), user_settings, check,
210216
list(entry.get("prepare", [])),
211-
list(entry.get("run", []))))
217+
list(entry.get("run", [])),
218+
minutes_provided="minutes" in entry))
212219
if not configs:
213220
error(f"{opts.json}: no configs")
214221
return configs
@@ -227,8 +234,16 @@ def privatize_dirs(bdir: Path, dirs: list[str]) -> None:
227234
shutil.copytree(SRCDIR / name, d, symlinks=True)
228235

229236

237+
def gh_escape(data: str) -> str:
238+
# Percent-encode workflow-command data (GitHub's documented encoding)
239+
# so a stray %, CR or LF - e.g. from a config name or step out of the
240+
# JSON - can't truncate the command or be parsed as a second one.
241+
return data.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
242+
243+
230244
def dump(title: str, path: Path) -> None:
231-
print(f"::group::{title}" if ON_GITHUB else f"==== {title} ====")
245+
# ::group:: is a workflow command; escape its title like warn() does.
246+
print(f"::group::{gh_escape(title)}" if ON_GITHUB else f"==== {title} ====")
232247
try:
233248
sys.stdout.write(path.read_text(errors="replace"))
234249
except OSError as e:
@@ -238,6 +253,23 @@ def dump(title: str, path: Path) -> None:
238253
sys.stdout.flush()
239254

240255

256+
def warn(msg: str) -> None:
257+
# GitHub surfaces ::warning:: as an annotation at the top of the run;
258+
# locally it is just a line. Informational only - never fails the run.
259+
print(f"::warning::{gh_escape(msg)}" if ON_GITHUB else f"WARNING: {msg}")
260+
261+
262+
def stale_estimate(cfg: Config, minutes: float) -> bool:
263+
# "minutes" is only a scheduling estimate (configs run longest-first;
264+
# --shard balances by it), never a pass/fail bound. Flag a finished
265+
# config whose real time drifted past +/-50% of an explicitly given
266+
# estimate so stale values - which pack the schedule worse - are easy
267+
# to find and update. Configs that omit "minutes" ride the 1.0 default
268+
# placeholder and are left alone.
269+
return (cfg.minutes_provided
270+
and not 0.5 * cfg.minutes <= minutes <= 1.5 * cfg.minutes)
271+
272+
241273
def run_config(cfg: Config, opts: argparse.Namespace) -> tuple[str | None,
242274
float]:
243275
if opts.fail_fast and stop_event.is_set():
@@ -344,6 +376,10 @@ def record_failure(step: str) -> str:
344376
# One line per passing config; the full logs would bloat the CI
345377
# log (they stay in build-<name>/make-check.log).
346378
print(f"{cfg.name}: pass [{minutes:.1f} min]")
379+
if stale_estimate(cfg, minutes):
380+
warn(f"{cfg.name}: ran {minutes:.1f} min but \"minutes\" "
381+
f"says {cfg.minutes:g} (>50% off) - update it in the "
382+
f"config JSON")
347383
sys.stdout.flush()
348384
else:
349385
dump(f"{cfg.name}: FAIL ({failed}) [{minutes:.1f} min]", log)
@@ -364,6 +400,11 @@ def summarize(results: list[tuple[Config, str | None, float]],
364400
ok = f":x: FAIL ({failed})"
365401
else:
366402
ok = ":white_check_mark: pass"
403+
if stale_estimate(cfg, minutes):
404+
# Non-fatal nudge mirroring the per-config warning, kept in
405+
# the summary next to the Minutes value to copy over.
406+
ok += (f' :warning: "minutes" {cfg.minutes:g} is >50% off, '
407+
f"update to ~{minutes:.1f}")
367408
lines.append(f"| {cfg.name} | {ok} | {minutes:.1f} |")
368409
# Two views of how efficiently the pool used the machine: thread
369410
# occupancy is the time the workers spent running configs out of the
@@ -495,7 +536,7 @@ def run_one(cfg: Config) -> tuple[Config, str | None, float]:
495536
else "aborted without a recorded failure"
496537
if aborted:
497538
msg += f" ({aborted} config(s) aborted by fail-fast)"
498-
print(f"::error::{msg}" if ON_GITHUB else msg)
539+
print(f"::error::{gh_escape(msg)}" if ON_GITHUB else msg)
499540
return 1
500541
return 0
501542

0 commit comments

Comments
 (0)