Build each engine once per sweep instead of once per matrix cell - #7
Build each engine once per sweep instead of once per matrix cell#7jamesx-improving wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Are we adding a test for this? Where is this run?
There was a problem hiding this comment.
To follow up on this:
- Do we have existing GitHub workflows and commands for these tests? If not, I don't think there is much value in adding them?
- If we are going to test logic, I think that logic should be extracted to class(es). We can write unit tests for those class(es), and then the script can just be a thin wrapper over that logic.
| # Same as java-run but assumes the engine is already built. Used by the matrix | ||
| # orchestrator, which builds each engine it needs once per sweep instead of | ||
| # once per cell. | ||
| java-run-nobuild: | ||
| $(JAVA_RUN_CMD) |
There was a problem hiding this comment.
Nit. Similar comments about unnecessary comments. Perhaps just do a sweep of this and other PRs? 🤷
| java-run: java-build | ||
| java -jar $(JAVA_JAR) \ | ||
| --server $(SERVER) \ | ||
| --driver $(DRIVER) \ | ||
| --workload $(WORKLOAD) \ | ||
| --metrics $(METRICS_OUTPUT) | ||
| $(JAVA_RUN_CMD) |
There was a problem hiding this comment.
Could this be written as:
java-run: java-build
$(MAKE) java-run-nobuildThat would also let us get rid of the *_RUN_CMD constants, right?
| csharp-run: csharp-build | ||
| dotnet run --project $(CSHARP_PROJECT) -c Release -- \ | ||
| --server $(SERVER) \ | ||
| --driver $(DRIVER) \ | ||
| --workload $(WORKLOAD) \ | ||
| --metrics $(METRICS_OUTPUT) | ||
| dotnet run --project $(CSHARP_PROJECT) -c Release -- $(CSHARP_RUN_ARGS) |
There was a problem hiding this comment.
If dotnet run builds by default, why do we need the call to csharp-build as well?
| | Target | Description | | ||
| |--------|-------------| | ||
| | `make java-run` | Run Java engine (DRIVER, WORKLOAD, SERVER) | | ||
| | `make ruby-run` | Run Ruby engine (DRIVER, WORKLOAD, SERVER) | | ||
| | `make csharp-run` | Run C# engine (DRIVER, WORKLOAD, SERVER) | | ||
| | `make java-run` | Build, then run Java engine (DRIVER, WORKLOAD, SERVER) | | ||
| | `make ruby-run` | Build, then run Ruby engine (DRIVER, WORKLOAD, SERVER) | | ||
| | `make csharp-run` | Build, then run C# engine (DRIVER, WORKLOAD, SERVER) | | ||
| | `make java-run-nobuild` | Run Java engine without rebuilding (used by the matrix orchestrator) | | ||
| | `make ruby-run-nobuild` | Run Ruby engine without re-running `bundle install` | | ||
| | `make csharp-run-nobuild` | Run C# engine without rebuilding | | ||
| | `make java-build` | Build Java JAR | | ||
| | `make ruby-build` | Install Ruby dependencies | | ||
| | `make csharp-build` | Build C# executable | |
There was a problem hiding this comment.
Isn't this information already specified in the Makefile? Maybe worth just pointing readers there so that this information only needs to be specified/updated in one place?
|
|
||
| ## Engine Builds — Once Per Sweep | ||
|
|
||
| Before the sweep starts, the orchestrator resolves the engine behind every `driver_config` (via the driver's `driver_id`) and runs `make <engine>-build` **once for each engine the matrix actually needs**. A Java-only matrix builds Java only; a mixed matrix builds Java, Ruby and C#. If a build fails the run aborts immediately, rather than failing every cell. |
There was a problem hiding this comment.
A Java-only matrix builds Java only; a mixed matrix builds Java, Ruby and C#
Is this accurate and/or necessary? Would C# get built even if the matrix only had Ruby and Java?
| | Target | Behavior | | ||
| |--------|----------| | ||
| | `make java-run` / `ruby-run` / `csharp-run` | Build, then run. Unchanged — the right target for one-off manual runs. | | ||
| | `make java-run-nobuild` / `ruby-run-nobuild` / `csharp-run-nobuild` | Run only. Assumes the engine is already built; used by the matrix orchestrator. | |
There was a problem hiding this comment.
Similar to other comments: is there a single source of truth that we can point to for commands syntax – e.g. Makefile?
| Each config file is read once, however many cells reference it. | ||
| """ | ||
| driver_configs = {combo["driver_config"] for combo in series_combos} | ||
| return {path: detect_engine_for_driver(path) for path in driver_configs} |
There was a problem hiding this comment.
It seems like this methods returns a dictionary, but the caller then immediately calls values() – so could we just return a list of engines? Could we even just inline this entire method, since without that I think it might just be simplified to a one-liner?
| def build_engines(engines): | ||
| """Run `make <engine>-build` for each engine, aborting on the first failure. | ||
|
|
||
| Cells run via the `*-run-nobuild` targets, so a sweep builds each engine it | ||
| needs exactly once instead of once per cell. | ||
| """ |
There was a problem hiding this comment.
Do we need to mention "cells" and "sweep" here? Seems more likely to confuse, and I think the first line is sufficient to explain what this does? 🤷
| def build_engines(engines): | |
| """Run `make <engine>-build` for each engine, aborting on the first failure. | |
| Cells run via the `*-run-nobuild` targets, so a sweep builds each engine it | |
| needs exactly once instead of once per cell. | |
| """ | |
| def build_engines(engines): | |
| """Run `make <engine>-build` for each engine, aborting on the first failure.""" |
There was a problem hiding this comment.
To follow up on this:
- Do we have existing GitHub workflows and commands for these tests? If not, I don't think there is much value in adding them?
- If we are going to test logic, I think that logic should be extracted to class(es). We can write unit tests for those class(es), and then the script can just be a thin wrapper over that logic.
Signed-off-by: James Xin <james.xin@improving.com>
f8ca40d to
382f2ca
Compare
Closes #3
Summary
Separate building from running. The orchestrator now resolves which engines the
chosen matrix actually needs, builds each of them exactly once before the sweep
starts, aborts immediately if any build fails, and dispatches each cell to a new
*-run-nobuildtarget that does not rebuild. The deadrun_benchmark()helperis removed. Docs are updated to describe the two-target contract.
Build-once is per engine, not global: a mixed matrix builds Java, Ruby and C#;
a Java-only matrix does not pay for the .NET or Ruby build.
Motivation — cost
Cell counts from
--dry-run:driver-comparison-high-tpsvalkey-glide-thread-sweepdriver-comparison-defaultslettuce-pool-sweepvalkey-glide-basic-multiclientsMeasured on a 6-cell run: 165.9s → 98.5s (-41%), 6 builds down to 1. On the
720-cell matrix the same change removes 719 build invocations.
Output is unchanged: NDJSON record counts are identical, and
_manifest.jsonisbyte-identical before and after.
Why new
*-run-nobuildtargets and notSKIP_BUILD=1Both interfaces were considered.
SKIP_BUILD=1is the smaller diff, but theorchestrator passes each cell
os.environ.copy()— so aSKIP_BUILDleft in ashell or a CI job environment would silently disable the build for
make java-runeverywhere, for every caller. "Silently benchmarked a stale jar" isthe worst possible failure mode for a benchmarking tool.
The additive targets avoid that entirely:
*-runbehaves exactly as it doestoday for every existing caller, and skipping the build is something you have to
ask for by name. Two details worth noting in review:
*-run/*-run-nobuildpair shares its recipe through a variable, sothe two cannot drift apart. (For C# only the arguments are shared, because
dotnet runbuilds by default and the nobuild variant must pass--no-build.)java-run: java-build java-run-nobuildwas deliberately not used:prerequisite ordering is not guaranteed under
make -j, and a duplicatedprerequisite is cheaper than a race that runs a stale artifact.
What changed
Makefile— addjava-run-nobuild,ruby-run-nobuild,csharp-run-nobuild;factor each run command into a shared variable; drop the
java-buildprerequisite from
benchmark-matrix(the orchestrator now builds exactly theengines the matrix needs); extend
.PHONYandmake help.scripts/run_benchmark_matrix.py— addengines_for_combos()(resolve engineper distinct driver config, reading each config once) and
build_engines()(build each engine once,
sys.exiton the first failure); resolve engines upfront and dispatch cells to
<engine>-run-nobuild; remove the deadrun_benchmark().scripts/tests/test_engine_build.py— new, 9 tests.docs/BENCHMARK_MATRIX.md— new "Engine Builds — Once Per Sweep" section anda target-behavior table.
README.md— document the*-run-nobuildtargets and add the missingruby-buildrow.docs/ADDING_LANGUAGE.md— the worked Python example now shows the shared-variable pattern and both run targets, so the in-flight Python engine adopts
this interface instead of reintroducing
python-run: python-build.Verification
Dispatch, via a PATH-shimmed fake
makethat logs argv:*-run-nobuildcalls each.java-build, 4java-run-nobuild, and zerocsharp / ruby / dotnet invocations.
run, and the output directory is never created.
Negative test: with the jar moved aside,
java-run-nobuildfails loudly(
Unable to access jarfile) instead of rebuilding — confirming there is nohidden build left in the path.
Guard test with teeth: a parametrized test asserts that every engine in
DRIVER_ENGINE_MAPhas both a-buildand a-run-nobuildrule in theMakefile. It passes today and will fail the moment a new engine is registered
without its target —
python-run-nobuilddoes not exist yet, so this genuinelybites rather than being decorative.
Tests: the 9 new tests pass. Full unit suite: 101 passed, 8 failed; e2e:
8 passed, 1 failed. Every one of those failures is the pre-existing
macOS
/proclimitation intest_system_monitor.py— unrelated to this changeand present on the base commit.
Backward compatibility
An external consumer,
valkey-io/spring-data-valkey, clones this repo atunpinned
HEADand drives it through this Makefile, so five entry points had tostay byte-identical in behavior:
make java-buildwork/valkey/bin/valkey-serverfile targetserver-standalone-start/server-cluster-initserver-standalone-stop/server-cluster-stopcleanVerified by diffing make's own database (
make -qp, which parses theMakefile without running any recipe) between base and this branch. The only
rule-level differences are: the three new phony targets, the three
*-runrecipes now referencing shared variables, and
benchmark-matrixlosing aprerequisite.
make -n java-runproduces byte-identical argv to base.That consumer does not call
make java-runat all — it runsjava -jardirectly after
make java-build— so it is unaffected either way.Not verified / limitations
Stated explicitly rather than implied:
csharp-run-nobuildwas never executed. The local .NET SDKs are 9.0.301and 8.0.411 while the project targets
net10.0, sodotnet buildfails withNETSDK1045. It was verified only viamake -nexpansion and shim dispatch.ruby-run-nobuildwas likewise never executed.make -qp, notby execution.
~/.m2; cold-cache was not measured.Notes for the reviewer
first and also touches
scripts/run_benchmark_matrix.pyand the Makefile'smatrix block. Flagging for ordering only — no attempt made to accommodate it
here.
ruby-runprefixes../$(DRIVER), so the orchestrator's absolute temp paths become..//var/folders/...and Ruby cells cannot work in a sweep. The splitpreserves that bug faithfully; it is tracked separately.