diff --git a/docs/parity-workflow.md b/docs/parity-workflow.md index 5c83c6a33..f527c9476 100644 --- a/docs/parity-workflow.md +++ b/docs/parity-workflow.md @@ -42,7 +42,7 @@ The game logs into `HORIZON_HOME` and falls back to the player's own game folder Harnesses can also run on the Mac mini, the desktop and playhorizon (`tools/parity_hosts.json` describes them; `desktop` and `playhorizon` use their CI runner's environment and vanilla data, and the desktop refuses work, or stops it, while someone uses it: docs/release-flow.md, "The desktop's runner"; its `default` list is empty, so pass `--hosts macmini,playhorizon` or set `PARITY_HOSTS`, after copying `shared-local/` to `~/code/horizon-parity/shared-local` on each host). The runner mirrors the worktree there with rsync (not `.git`, `.local`, `target` or ignored files) into `~/code/horizon-parity/remote/`, whose `.local` links to the vanilla data in `~/code/horizon-parity/shared-local`, and starts `tools/parity.py --worker` over ssh. The worker builds the probes (about 80 s the first time) and runs what it is sent under the same tracer, admitting harnesses against its own free memory. One queue, longest first, feeds every machine, remote ones first: the Mac takes up to 14 harnesses, the desktop 8, playhorizon 3 (on CPUs 0-3: its CI runner has 4-7). Each pass comes back with the content hash of every file it depended on, so it is keyed on exactly what ran, and logs come back to `.local/parity-runner/logs/`; state, sharing and `gate.py` work as before. If a host can't be reached, or drops out, its harnesses run here. -Harnesses a host can't run are listed in the file: `local_only` (`save_roundtrip` runs the game on a virtual screen) and each host's `exclude`. The Mac excludes nothing: vanilla's `Math.sin`, `pow`, `log` and the like don't round the same on an Apple silicon Mac as on Linux (HotSpot uses fdlibm there, Intel's stubs on x86-64), and Rust's float functions round like neither (they call the platform libm), so code that mirrors a vanilla `Math` call uses `horizon_core::jmath`, which computes what HotSpot computes on the host architecture. The ThinkPad no longer runs harnesses (2026-10-26): playhorizon is the Linux host that is always on. +Harnesses a host can't run are listed in the file: `local_only` (`save_roundtrip` runs the game on a virtual screen) and each host's `exclude`. A host's `exclude` also holds when `parity.py` runs on that host itself, as its CI runner does: the harness is skipped there with its reason (playhorizon skips `remote_fireball` and `remote_fireworks`, which its CPU-starved CI slice fails even on bare main), unless named on the command line. The Mac mini's runner and the nightly audit still run them. The Mac excludes nothing: vanilla's `Math.sin`, `pow`, `log` and the like don't round the same on an Apple silicon Mac as on Linux (HotSpot uses fdlibm there, Intel's stubs on x86-64), and Rust's float functions round like neither (they call the platform libm), so code that mirrors a vanilla `Math` call uses `horizon_core::jmath`, which computes what HotSpot computes on the host architecture. The ThinkPad no longer runs harnesses (2026-10-26): playhorizon is the Linux host that is always on. The file's `solo` list names harnesses that drive game windows (`remote_fireworks`, `server_player_sculk`, `world_reopen`, ...). On a machine where `parity.py` has 4 CPUs or fewer, such as playhorizon's CI runner, each of them runs alone, after the others: there frames are drawn on the CPU, and beside other harnesses a screenshot or a control request took more ticks than the check allows. `PARITY_SOLO=1` or `0` forces this on or off. diff --git a/tests/test_parity_runner.py b/tests/test_parity_runner.py index fdc636a24..f26cbd5da 100644 --- a/tests/test_parity_runner.py +++ b/tests/test_parity_runner.py @@ -145,6 +145,13 @@ class ParityRunnerTests(unittest.TestCase): with mock.patch.object(parity.sys, "platform", "darwin"): self.assertEqual(parity.platform_skips(config), {"options": "libm"}) + def test_a_hosts_excludes_are_skipped_when_it_runs_parity_itself(self): + config = {"hosts": {"playhorizon": {"exclude": {"remote_fireworks": "starved"}}, "macmini": {"exclude": {}}}} + with mock.patch.object(parity.sys, "platform", "linux"): + self.assertEqual(parity.platform_skips(config, "playhorizon"), {"remote_fireworks": "starved"}) + self.assertEqual(parity.platform_skips(config, "macmini"), {}) + self.assertEqual(parity.platform_skips(config, "elsewhere"), {}) + def test_named_only_harnesses_are_listed_from_the_host_config(self): self.assertEqual(parity.named_only({"named_only": {"lan_integration": "slow"}}), {"lan_integration": "slow"}) self.assertEqual(parity.named_only({}), {}) diff --git a/tools/parity.py b/tools/parity.py index 38d113cab..f8f48fbcf 100644 --- a/tools/parity.py +++ b/tools/parity.py @@ -1454,11 +1454,13 @@ def affected_harnesses(found, records, changed, reach=True, base=None): return affected -def platform_skips(config): - """Harnesses tools/parity_hosts.json marks as giving different results on this platform: {name: reason}.""" - if sys.platform == "linux": - return {} - return dict(config.get("linux_only", {})) +def platform_skips(config, host=None): + """Harnesses tools/parity_hosts.json marks as giving different results on this platform, and those this host's + entry excludes (playhorizon's CI runner runs its own pull request jobs here, not only workers sent to it): + {name: reason}.""" + skips = {} if sys.platform == "linux" else dict(config.get("linux_only", {})) + skips.update(config.get("hosts", {}).get(HOST if host is None else host, {}).get("exclude", {})) + return skips def known_flaky(config): @@ -1593,7 +1595,7 @@ def main(): chosen = [name for name in found if stale(name, state, hasher, environment)] skips = platform_skips(host_config()) for name in [name for name in chosen if base_name(name) in skips and not args.names]: - print(f"skipped on {sys.platform}: {name}: {skips[base_name(name)]}", flush=True) + print(f"skipped on {HOST} ({sys.platform}): {name}: {skips[base_name(name)]}", flush=True) chosen.remove(name) manual = {} if args.names or args.all else named_only(host_config()) for name in [name for name in chosen if base_name(name) in manual]: diff --git a/tools/parity_hosts.json b/tools/parity_hosts.json index cec022194..2df32ba41 100644 --- a/tools/parity_hosts.json +++ b/tools/parity_hosts.json @@ -31,7 +31,9 @@ "exclude": { "mob_ai": "the vanilla oracle runs out of its 3 GB heap here (OutOfMemoryError in IO workers; passes on the desktop and the Mac) - 2026-10-26 trial", "lan_integration": "two clients rendered on the CPU (lavapipe): different timing subtests fail on each run (elytra, consume-release, barter, critical-hits) - 2026-10-26 trial", - "world_reopen": "failed twice here (no Save and Quit button after reopening; the minecart-count flake), passes on the desktop - 2026-10-26 trial" + "world_reopen": "failed twice here (no Save and Quit button after reopening; the minecart-count flake), passes on the desktop - 2026-10-26 trial", + "remote_fireball": "fails here even alone, at bare main 3b19c386c (2 of 2 runs and the retry), and passes on the Mac mini: the guest's egg is seen in 2 of 6 samples. CPU-starved: the whole horizon-ci slice (runner on 4-7, workers on 0-3) shares a 700% quota and was throttled in 27% of periods, so lavapipe frames and screenshots outlast the projectile's flight - 2026-10-27", + "remote_fireworks": "fails here even alone, at bare main 3b19c386c (2 of 2 runs and the retry), and passes on the Mac mini: the guest's sparks are gone by its trail screenshot (same CPU starvation as remote_fireball; a private Xvfb on CPUs 0-3 did not help) - 2026-10-27" }, "note": "a partner's server (Ubuntu, 8 EPYC threads, 30 GB, Detroit; tailnet only), its CI runner has CPUs 4-7, so workers get 0-3 (as tools/remote.sh runs do) and 3 jobs, in the CI runner's environment, slice and Xvfb screen (tools/ci/linux-runner.sh); the always-on Linux host since the ThinkPad stopped running harnesses (2026-10-26)" }