Repository navigation
refactor(ci): moveprovider benchmark, scoredirector reporting - #157
Conversation
# Conflicts: # .github/workflows/performance_move_provider.yml
The two scenarios were singleDraw (1 draw + commit) and manyDraws (10 draws + commit), and the commit dominated both. A fit of A + B*n to them shows the move provider does 2 % of the work in singleDraw and 15 % in manyDraws, so neither measured move generation - which is what a move-provider benchmark exists for. Replace them with commitMove and drawOnly. commitMove is the old singleDraw, renamed to say what it measures: apply one move and settle the neighborhood network. It calculates no score, so it is not a whole step. drawOnly draws 500 candidates and commits none, so the solution never changes and its speed is generation alone. manyDraws goes away; it was exactly commitMove + 9 * drawOnly. drawOnly carries @OperationsPerInvocation, so its speed is drawn moves for each second, and its movedValues counter divided by that speed is the mean move size. Its draw count is frozen: a bi move iterator retires dead left tuples for the life of one iterator, so a longer invocation draws from a more pruned pool and reports a faster number. The report keeps its five columns but now ranks both costs, with one ranking line for each scenario above each table, and it explains all four columns at the bottom. It also maps a baseline's singleDraw onto commitMove, so a baseline jar built from an older ref still joins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The table shows a delta but no margin of error, so a reader cannot tell whether a delta is bigger than the noise it sits in. Every cell already marked with a high score error now prints one line below its family table, naming both margins and the delta they must be weighed against. A clean table prints no such line, and a margin on a zero score prints as absent rather than crashing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The legend named every verdict but explained none, and "undetermined" is the one label that cannot explain itself. It now says what makes a row undetermined, that it fails the build, and what to do about it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Do not merge; revert after the experiment run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Run 33599612547 raised the inline budget on both sides and the per-fork modes survived, so the 4.0 band derived from within-mode spread does not hold yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Do not merge; revert after the experiment run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Background compilation let each JVM race C2 against its own profile and keep the outcome for life, splitting the forks of one benchmark into two speeds 10-30 % apart. -Xbatch removes the race: SUB_LIST_SWAP/drawOnly went from a 1.37x per-fork spread (CV 12 %) to 1.01x (CV 0.2 %) in run 33606514634. Forks drop from 40 to 20, since they no longer average over two speeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new summarizers can crash on edge-case JMH output (division by zero) and the move-provider flush cadence does not match its “bounded periodic” contract, risking unstable/biased measurements.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR refactors the CI performance workflows by switching both the score-director and move-provider benchmarks to a subjob-per-slice model that uploads raw JMH JSON artifacts, and adds Python summarizers that aggregate those artifacts into a single markdown regression report (failing CI on regressions/undetermined/missing data). It also introduces a new move-provider microbenchmark suite (Java) with supporting configuration and “no-op” constraint providers to minimize non-measured overhead.
Changes:
- Add
summarize-scoredirector.pyandsummarize-moveprovider.pyto aggregate per-subjob artifacts into a single CI report with verdicts and links to benchmark assets. - Introduce a new MoveProvider microbenchmark suite (cases, JMH benchmarks, runner main, dataset loading, and a runnable non-JMH test).
- Update GitHub Actions workflows to build baseline/SUT once, run benchmark subjobs on GitHub-hosted ARM runners, archive raw results + asset URLs, and run a final summarizing job.
File summaries
| File | Description |
|---|---|
| summarize-scoredirector.py | New: aggregates per-example score-director benchmark artifacts into one markdown table + CI exit code. |
| summarize-moveprovider.py | New: aggregates per-provider/scenario move-provider benchmark artifacts into tables + CI exit code. |
| src/test/java/ai/timefold/solver/benchmarks/micro/moveprovider/MoveProviderBenchmarkTest.java | New: non-JMH runnable lifecycle check across all built-in move providers. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/problems/MoveProviderProblem.java | New: implements commitMove/drawOnly benchmark scenarios with undo/flush behavior. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/NoOpVehicleRoutingConstraintProvider.java | New: no-op constraints to avoid join overhead for move-provider benchmarks. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/NoOpTaskAssigningConstraintProvider.java | New: no-op constraints to avoid join overhead for move-provider benchmarks. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/NoOpPatientAdmissionScheduleConstraintProvider.java | New: no-op constraints to avoid join overhead for move-provider benchmarks. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/NoOpMeetingSchedulingConstraintProvider.java | New: no-op constraints to avoid join overhead for move-provider benchmarks. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/MoveProviderCase.java | New: common interface for move-provider enum cases and moved-values counting. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/Main.java | New: benchmark entrypoint wiring configuration → JMH runner, plus score error warnings. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/ListMoveProviderCase.java | New: list-variable move-provider case definitions and moved-values counters. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/jmh/MovedValueCounter.java | New: JMH aux counter for moved values to normalize throughput in CI reporting. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/jmh/ListVariableMoveProviderBenchmark.java | New: JMH benchmark subclass for list-variable providers. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/jmh/BasicVariableMoveProviderBenchmark.java | New: JMH benchmark subclass for basic-variable providers. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/jmh/AbstractMoveProviderBenchmark.java | New: shared JMH config/scenarios/constants for move-provider benchmarks. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/Example.java | New: dataset + solver-config selection for move-provider benchmark suite. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/Configuration.java | New: benchmark config parsing for selecting provider subsets and JMH parameters. |
| src/main/java/ai/timefold/solver/benchmarks/micro/moveprovider/BasicMoveProviderCase.java | New: basic-variable move-provider case definitions and moved-values counters. |
| src/main/java/ai/timefold/solver/benchmarks/micro/common/AbstractMain.java | Update: add -Xbatch JVM arg to reduce compilation nondeterminism in perf runs. |
| src/main/java/ai/timefold/solver/benchmarks/micro/common/AbstractConfiguration.java | Update: treat blank selection properties as “run nothing” instead of “run everything”. |
| run-moveprovider.sh | New: local runner script for move-provider benchmark (mirrors score-director script). |
| moveprovider-benchmark.properties | New: example properties file for selecting move providers and benchmark parameters. |
| .github/workflows/performance_score_director.yml | Update: refactor into per-example benchmark subjobs + final summarizing report job. |
| .github/workflows/performance_quickstarts.yml | Update: bump default baseline version. |
| .github/workflows/performance_move_provider.yml | Update: refactor into per-provider benchmark subjobs + final summarizing report job; tune fork/warmup/measurement iterations. |
Review details
Suppressed comments (2)
summarize-scoredirector.py:110
evaluate_row()can raiseZeroDivisionErrorwhen the baseline score is 0, because it divides byold["score"]unconditionally. Even if JMH normally reports non-zero throughput, a failed/degenerate run could produce 0 and crash the summarizer instead of reporting an undetermined/missing row.
def evaluate_row(old: "dict | None", new: "dict | None") -> tuple["float | None", Verdict, bool]:
"""Returns (delta_pct, verdict, high_error). delta_pct is None only for MISSING."""
if old is None or new is None:
return None, MISSING, False
delta_pct = (new["score"] / old["score"] - 1) * 100
error_limit = TOLERANCE_PCT / 100 / HIGH_ERROR_BAND_FRACTION
summarize-moveprovider.py:178
evaluate_row()can raiseZeroDivisionErrorwhen the baseline score is 0, because it divides byold["score"]unconditionally. If a JMH run ever produces 0 throughput (for example due to a degenerate/failed run), the summarizer would crash instead of producing a verdict.
def evaluate_row(old: "dict | None", new: "dict | None", tolerance_pct: float) -> tuple["float | None", Verdict, bool]:
"""Returns (delta_pct, verdict, high_error). delta_pct is None only for MISSING.
tolerance_pct belongs to the scenario, not to the report; see TOLERANCE_PCT.
"""
if old is None or new is None:
return None, MISSING, False
delta_pct = (new["score"] / old["score"] - 1) * 100
error_limit = tolerance_pct / 100 / HIGH_ERROR_BAND_FRACTION
- Files reviewed: 25/27 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| private void maybeFlushConstraintStreamSession() { | ||
| if (invocationCounter >= AbstractMoveProviderBenchmark.FLUSH_EVERY_N_STEPS && scoreDirector.requiresFlushing()) { | ||
| scoreDirector.calculateScore(); | ||
| } | ||
| } |
| url_path = os.path.join(provider_dir, "assets-url.txt") | ||
| if os.path.exists(url_path): | ||
| url = open(url_path).read().strip() |
| for baseline_path in glob.glob(os.path.join(data_dir, "*", "baseline-results.json")): | ||
| example_dir = os.path.dirname(baseline_path) | ||
| sut_path = os.path.join(example_dir, "sut-results.json") | ||
| baseline.update(load_side(baseline_path)) | ||
| if os.path.exists(sut_path): | ||
| sut.update(load_side(sut_path)) | ||
| url_path = os.path.join(example_dir, "assets-url.txt") | ||
| if os.path.exists(url_path): | ||
| url = open(url_path).read().strip() | ||
| if url: | ||
| for example in load_side(baseline_path): | ||
| asset_urls[example] = url |
No description provided.