Merge pull request #6808 from horizon-rust/perf/entities-stress

🤖 Agent: stress scenes for perf_bench (Horizon and vanilla); Horizon beats vanilla x2.2 (armor stands) to x10.8 (signs) on the M4 Pro; Python tests 607 ok, the bench mod compiles against 26.3.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-02 07:15:49 +01:00
commit a7df9ec019
8 files changed
+443 -14

No files matched your search

+3
View File
@@ -88,6 +88,9 @@ confidence interval of the mean, Student's t, null when `n` is 1). The unit is t
| `tick.mspt`, `tick.tps` | integrated server tick time and rate (planned) |
| `memory.rss_peak_mb`, `memory.rss_steady_mb` | resident memory: peak over the run, median while flying |
| `memory.gpu_mb`, `memory.<phase>.gpu_mb` | video memory the game allocated (or NVIDIA attributes to it), where readable |
| `memory.<phase>.rss_mb` | resident memory, median over that phase's flight (perf_bench) |
| `cpu.<phase>.p50_ms` | median CPU time of a frame (the frame minus its waits for the GPU, the display and the limiter; Horizon only) |
| `tick.<phase>.client_ms` | mean time of one client tick during the phase (vanilla: `Minecraft.tick`; Horizon: the frames' tick phase over the ticks run) |
| `power.package_w`, `power.battery_drain_pct` | power draw, battery used per hour (laptops; planned) |
| `thermal.cpu_c`, `thermal.throttled_pct` | temperature, share of the run spent throttled (planned) |
| `launcher.start_ms` | launcher process start to its first frame (a session) |
+12
View File
@@ -57,6 +57,18 @@ python3 tools/perf_bench.py --table horizon-mine vanilla-mine mods-mine
Options: `--distances 8,16`, `--sizes 1920x1080`, `--scenes village,ocean`, `--seconds 20`, `--repeats 5`, `--heap 4G` (vanilla), `--offscreen` (Horizon, for locked or headless sessions). Results go to `benchmarks/perf/<label>-rd<N>.json` at 1280x720 and `<label>-1080p-rd<N>.json` at 1920x1080 (other sizes `<label>-<W>x<H>-rd<N>.json`; `--table` takes those labels), one [benchmark report](benchmark-report.md) per window size and render distance (the same format the game's `--benchmark` and the launcher write), with the load average before each game; each game's log, and the vanilla driver's event log, stay in `.local/perf-bench/`. Keep the machine otherwise idle, plugged in, and the window on a screen.
### Many-entity stress scenes
`--scenes stress` (or some of `stress_empty`, `stress_mobs`, `stress_items`, `stress_frames`, `stress_armor_stands`, `stress_chests`, `stress_signs`, `stress_banners`, `stress_particles`) runs the scenes of `tools/stress_scenes.py` instead of flying: 1000 still mobs of 8 kinds, 2000 dropped items, 512 item frames, 500 armoured armor stands, 800 chests and shulker boxes, a wall of 687 written signs, 300 banners, and steady particles from repeating command blocks (flames, smoke, end rods, firework bursts, TNT in the air). Each is built high above the terrain by the same vanilla commands in every game (Horizon through `horizon-ctl command`, vanilla and vanilla-mods through the driver mod), settles 6 seconds and is held still for `--seconds`; `stress_empty` is the same camera and floor without them. Entities are drawn only within a distance that grows with the render distance up to 20 (`Entity.shouldRenderAtSqrDistance`), so run them at 16 or more:
```sh
python3 tools/perf_bench.py --game horizon --scenes stress --distances 16 --label stress-horizon
python3 tools/perf_bench.py --game vanilla --scenes stress --distances 16 --label stress-vanilla
python3 tools/perf_bench.py --table stress-horizon stress-vanilla
```
Besides the FPS table, `--table` then gives each scene's median frame CPU time (Horizon), client tick time and resident memory. Chests stay closed: opening them needs a player, which neither driver has. The stress scenes use 26.3's command syntax, so they are not for `--game neoforge`.
On Windows, run `python tools\perf_bench.py ...` the same way. Horizon's control endpoint there is a localhost TCP port, which the game writes to a `.json` file in the temporary folder. `--build` can point at an installed release (`%APPDATA%\horizon-launcher\versions\vX.Y.Z`, with `--resources` its `.local\horizon-client\26.3`), and the report then gives that release's version.
## Frame pacing
@@ -37,9 +37,10 @@ import net.minecraft.world.phys.Vec3;
*
* Script (system property horizon.bench.script): {"world", "seed", "settle",
* "seconds", "timeout", "scenes": [{"name", "from": [x,y,z], "to": [x,y,z],
* "pitch"}]}, and "server" ("host:port") to join that server instead of
* creating a world (the player needs op there for /tp and /time). Output:
* horizon.bench.out.
* "pitch", "settle"?, "setup"?: [command...], "cleanup"?: [command...]}]}: a scene's own settle replaces
* the script's, its setup commands (tools/stress_scenes.py) run once its chunks are loaded and its cleanup
* commands after its flight. "server" ("host:port") joins that server instead of creating a world (the
* player needs op there for /tp and /time). Output: horizon.bench.out.
*/
public final class VanillaBench implements ClientModInitializer {
private static VanillaBench bench;
@@ -57,6 +58,9 @@ public final class VanillaBench implements ClientModInitializer {
private int lastLoaded = -1;
private long lastChange;
private final List<Float> frames = new ArrayList<>();
/** Client tick durations (Minecraft.tick) while flying, in milliseconds. */
private final List<Float> ticks = new ArrayList<>();
private long tickStart;
/** Fabric's entrypoint instance; the benchmark itself is the static one. */
public VanillaBench() {
@@ -83,6 +87,17 @@ public final class VanillaBench implements ClientModInitializer {
bench.event("init");
}
/** Called around every client tick (Minecraft.tick). */
public static void tick(boolean start) {
if (bench == null) return;
long now = System.nanoTime();
if (start) {
bench.tickStart = now;
} else if (bench.phase == Phase.FLY && bench.tickStart != 0) {
bench.ticks.add((now - bench.tickStart) / 1e6f);
}
}
/** Called at the start of every frame (Minecraft.runTick). */
public static void frame(Minecraft mc) {
if (bench != null) bench.step(mc);
@@ -151,13 +166,19 @@ public final class VanillaBench implements ClientModInitializer {
e.addProperty("load_ms", (lastChange - phaseStart) / 1e6);
e.addProperty("chunks", lastLoaded);
event("loaded", e);
if (s.has("setup")) {
for (var line : s.getAsJsonArray("setup")) mc.player.connection.sendCommand(line.getAsString());
}
enter(Phase.SETTLE, now);
}
}
case SETTLE -> {
place(mc, scenes.get(scene), 0);
if (elapsed >= script.get("settle").getAsDouble()) {
JsonObject s = scenes.get(scene);
place(mc, s, 0);
double settle = (s.has("settle") ? s : script).get("settle").getAsDouble();
if (elapsed >= settle) {
frames.clear();
ticks.clear();
event("fly", named(scenes.get(scene)));
enter(Phase.FLY, now);
}
@@ -171,8 +192,19 @@ public final class VanillaBench implements ClientModInitializer {
JsonArray times = new JsonArray();
frames.forEach(times::add);
e.add("frame_ms", times);
JsonArray tickTimes = new JsonArray();
ticks.forEach(tickTimes::add);
e.add("tick_ms", tickTimes);
JsonObject entities = new JsonObject();
entities.addProperty("all", mc.level.getEntityCount());
e.add("entities", entities);
e.addProperty("chunks", mc.level.getChunkSource().getLoadedChunksCount());
event("flown", e);
if (scenes.get(scene).has("cleanup")) {
for (var line : scenes.get(scene).getAsJsonArray("cleanup")) {
mc.player.connection.sendCommand(line.getAsString());
}
}
next(mc, now);
}
}
@@ -0,0 +1,23 @@
package dev.horizonlab.bench.mixin;
import dev.horizonlab.bench.VanillaBench;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Times the client tick (Minecraft.tick), for the stress scenes' client tick figure. */
@Mixin(Minecraft.class)
public abstract class TickHookMixin {
@Inject(method = "tick", at = @At("HEAD"))
private void horizonBenchTickStart(CallbackInfo callback) {
VanillaBench.tick(true);
}
@Inject(method = "tick", at = @At("RETURN"))
private void horizonBenchTickEnd(CallbackInfo callback) {
VanillaBench.tick(false);
}
}
@@ -3,7 +3,8 @@
"package": "dev.horizonlab.bench.mixin",
"compatibilityLevel": "JAVA_25",
"client": [
"FrameHookMixin"
"FrameHookMixin",
"TickHookMixin"
],
"injectors": {
"defaultRequire": 1
+63
View File
@@ -332,3 +332,66 @@ class PortabilityTests(unittest.TestCase):
"-Djava.library.path=${natives_directory}/java", "-cp", "${classpath}"]}}
self.assertEqual(manifest_jvm_flags(manifest), ["-XX:StackShadowPages=32"])
self.assertEqual(manifest_jvm_flags({}), [])
class StressSceneTests(unittest.TestCase):
def test_stress_selects_every_stress_scene_after_the_flights(self):
import argparse
from tools import stress_scenes
from tools.perf_bench import scene_list
names = [s[0] for s in scene_list(argparse.Namespace(scenes="ocean,stress"))]
self.assertEqual(names, ["ocean"] + stress_scenes.NAMES)
self.assertEqual([s[0] for s in scene_list(argparse.Namespace(scenes="stress_mobs"))], ["stress_mobs"])
def test_scenes_have_their_entity_counts(self):
from tools import stress_scenes
summons = lambda name, kind: sum(c.startswith(f"summon {kind} ") for c in stress_scenes.setup(name))
self.assertEqual(sum(summons("stress_mobs", k) for k in stress_scenes.MOBS), 1000)
self.assertEqual(summons("stress_items", "item"), 2000)
self.assertEqual(summons("stress_frames", "item_frame"), 512)
self.assertEqual(summons("stress_armor_stands", "armor_stand"), 500)
self.assertEqual(stress_scenes.setup("stress_empty"), stress_scenes.SETTINGS + stress_scenes.floor(
*stress_scenes.origin("stress_empty")))
# Only the run's first stress scene sets the game rules.
self.assertEqual(stress_scenes.setup("stress_empty", first=False),
stress_scenes.floor(*stress_scenes.origin("stress_empty")))
def test_every_entity_is_near_enough_to_be_drawn_at_rd16(self):
# Entity.shouldRenderAtSqrDistance with viewScale 2: a dropped item (0.25) within 32 blocks.
from tools import stress_scenes
for name in ("stress_mobs", "stress_items", "stress_frames", "stress_armor_stands"):
_, at, _, _ = stress_scenes.scene(name)
eye = (at[0], at[1] + 1.62, at[2])
for line in stress_scenes.setup(name):
if line.startswith("summon "):
x, y, z = map(float, line.split()[2:5])
distance = sum((a - b) ** 2 for a, b in zip((x, y, z), eye)) ** 0.5
self.assertLess(distance, 32.0, (name, line))
self.assertGreater(z, eye[2], (name, line))
def test_commands_are_single_lines_inside_the_world(self):
from tools import stress_scenes
for name in stress_scenes.NAMES:
for line in stress_scenes.setup(name):
self.assertNotIn("\n", line)
self.assertLess(len(line), 1024)
self.assertTrue(all(len(c) < 1024 for c in stress_scenes.cleanup(name)))
def test_cleanup_covers_every_block_a_scene_places(self):
from tools import stress_scenes
slabs = [list(map(int, c.split()[1:7])) for c in stress_scenes.cleanup("stress_mobs") if c.startswith("fill")]
for slab in slabs:
dx, dy, dz = (abs(slab[i + 3] - slab[i]) + 1 for i in range(3))
self.assertLessEqual(dx * dy * dz, 32768) # vanilla's fill limit
covered = lambda x, y, z: any(s[0] <= x <= s[3] and s[1] <= y <= s[4] and s[2] <= z <= s[5] for s in slabs)
for name in stress_scenes.NAMES:
for line in stress_scenes.setup(name):
words = line.split()
if words[0] == "fill":
a, b = list(map(int, words[1:4])), list(map(int, words[4:7]))
for x in (a[0], b[0]):
for y in (a[1], b[1]):
for z in (a[2], b[2]):
self.assertTrue(covered(x, y, z), (name, line))
elif words[0] == "setblock":
self.assertTrue(covered(*map(int, words[1:4])), (name, line))
+95 -8
View File
@@ -30,6 +30,11 @@ Games:
(mods/horizon/src/neoforge-bench) + the jars of --mod-jars and
--deps, the user's own; 26.3 options it does not know are ignored
Many-entity stress scenes (tools/stress_scenes.py: mobs, dropped items, item frames, armor stands,
containers, signs, banners, particles), built by the same vanilla commands in every game and held still:
python3 tools/perf_bench.py --game horizon --scenes stress --distances 16 --label stress-horizon
--port-work loads the mods a horizon-port-pack work folder carried over into
Horizon as dev mods.
@@ -60,10 +65,11 @@ import zipfile
from pathlib import Path
try:
from . import lab, runtime
from . import lab, runtime, stress_scenes
except ImportError:
import lab
import runtime
import stress_scenes
ROOT = lab.ROOT
WORK = ROOT / ".local/perf-bench"
@@ -320,9 +326,36 @@ def size_label(label, width, height):
def scene_list(args):
"""(name, from, to, pitch) of the scenes asked for: the flights, then the stress scenes ("stress" is all of them)."""
picked = args.scenes.split(",") if args.scenes else [s[0] for s in SCENES]
if "stress" in picked:
picked += stress_scenes.NAMES
# --server-bots adds the crowd scene over the bots.
return [s for s in SCENES if s[0] in picked] + list(getattr(args, "extra_scenes", None) or [])
return ([s for s in SCENES if s[0] in picked] + list(getattr(args, "extra_scenes", None) or [])
+ [stress_scenes.scene(n) for n in stress_scenes.NAMES if n in picked])
# Seconds a stress scene settles after its setup: the server spawns, the client receives and draws it.
STRESS_SETTLE = 6.0
def first_stress(args):
"""The first stress scene of the run, which sets the game rules for all of them."""
return next((s[0] for s in scene_list(args) if s[0] in stress_scenes.NAMES), None)
def settle_seconds(args, name):
return max(args.settle, STRESS_SETTLE) if name in stress_scenes.NAMES else args.settle
def scene_rss(sampler, windows):
"""Median resident memory (MB) inside each named window: {name: MB}."""
out = {}
for name, (a, b) in windows.items():
during = [rss for t, rss in sampler.samples if a <= t <= b]
if during:
out[name] = statistics.median(during) / 2**20
return out
def yaw(start, end):
@@ -591,7 +624,7 @@ def run_horizon(args, distance):
sampler.start()
control = Control(sock)
result = {"options_txt": options, "scenes": {}}
windows = []
windows, scene_windows = [], {}
try:
title, at = poll(control, lambda s: s.get("screen") == "title" and s.get("renderer"), 300)
result["renderer"] = title["renderer"]
@@ -617,13 +650,27 @@ def run_horizon(args, distance):
# Section meshes built while loading and while flying (Horizon only).
loaded_meshes = control("status")["meshed_sections"]
scene["meshes_load"] = loaded_meshes - meshed
time.sleep(args.settle)
loaded_meshes = control("status")["meshed_sections"]
if name in stress_scenes.NAMES:
for line in stress_scenes.setup(name, first=name == first_stress(args)):
control("command " + line)
time.sleep(settle_seconds(args, name))
before = control("status")
loaded_meshes = before["meshed_sections"]
begin = time.time()
summary = control("fly {} {} {} {} {} {} {} {} raw".format(*start, *end, args.seconds, pitch))
windows.append((begin, time.time()))
scene_windows[name] = windows[-1]
scene.update({k: summary.get(k) or [] for k in ("frame_ms", "cpu_ms", "gpu_ms")})
status = control("status")
# The client's own ticks: the frames' tick phase over the ticks the flight ran.
ticks = status.get("ticks", 0) - before.get("ticks", 0)
tick_phase = (summary.get("phases_avg_ms") or {}).get("tick")
if ticks > 0 and tick_phase is not None:
scene["client_tick_ms"] = tick_phase * summary.get("frames", 0) / ticks
scene["entities"] = {"mobs": status.get("mobs"), "block_entities": status.get("block_entities")}
if name in stress_scenes.NAMES:
for line in stress_scenes.cleanup(name):
control("command " + line)
scene["meshes_flight"] = status["meshed_sections"] - loaded_meshes
renderer = status.get("renderer") or {}
scene["gpu_mb"] = nvidia_memory_mb(proc.pid) or (renderer.get("gpu_allocated_bytes") or 0) / 2**20 or None
@@ -638,6 +685,8 @@ def run_horizon(args, distance):
sampler.stop.set()
sock.unlink(missing_ok=True)
result["memory"] = sampler.summary(windows)
for name, mb in scene_rss(sampler, scene_windows).items():
result["scenes"][name]["rss_mb"] = mb
args.release = release_version(WORK / f"horizon-rd{distance}.log")
return result
@@ -756,7 +805,7 @@ def run_vanilla(args, distance):
script = WORK / "script.json"
script.write_text(json.dumps({
"world": WORLD, "seed": SEED, "settle": args.settle, "seconds": args.seconds, "timeout": args.timeout,
"scenes": [{"name": n, "from": a, "to": b, "pitch": p} for n, a, b, p in scene_list(args)],
"scenes": driver_scenes(args),
**({"server": args.server_address} if args.server else {}),
}))
events = WORK / f"{args.game}-rd{distance}.events.jsonl"
@@ -784,6 +833,18 @@ def run_vanilla(args, distance):
return driven(args, distance, command, game_dir, env, log, events, options, loaded_mods)
def driver_scenes(args):
"""The scenes as the driver mods read them: stress scenes carry their setup commands and settle time."""
out = []
for n, a, b, p in scene_list(args):
scene = {"name": n, "from": a, "to": b, "pitch": p, "settle": settle_seconds(args, n)}
if n in stress_scenes.NAMES:
scene["setup"] = stress_scenes.setup(n, first=n == first_stress(args))
scene["cleanup"] = stress_scenes.cleanup(n)
out.append(scene)
return out
def driven(args, distance, command, game_dir, env, log, events, options, loaded_mods):
"""Runs a Java client whose driver mod writes `events`, and reads its results."""
started = time.time()
@@ -815,17 +876,24 @@ def driven(args, distance, command, game_dir, env, log, events, options, loaded_
**({"join_s": (world["ready_epoch_ms"] - title["epoch_ms"]) / 1000} if args.server else {})},
"scenes": {},
}
windows = []
windows, scene_windows = [], {}
loaded = {e["scene"]: e for e in by("loaded")}
flights = {e["scene"]: e for e in by("fly")}
for flown in by("flown"):
name = flown["scene"]
windows.append((flights[name]["epoch_ms"] / 1000, flown["epoch_ms"] / 1000))
scene_windows[name] = windows[-1]
scene = {"load_s": loaded[name]["load_ms"] / 1000, "frame_ms": flown["frame_ms"],
"chunks": flown["chunks"], "gpu_mb": nvidia}
if flown.get("tick_ms"):
scene["client_tick_ms"] = statistics.mean(flown["tick_ms"])
if "entities" in flown:
scene["entities"] = flown["entities"]
result["scenes"][name] = scene
print_scene(args.game, distance, name, scene)
result["memory"] = sampler.summary(windows)
for name, mb in scene_rss(sampler, scene_windows).items():
result["scenes"][name]["rss_mb"] = mb
return result
@@ -852,7 +920,7 @@ def run_neoforge(args, distance):
script = WORK / "script.json"
script.write_text(json.dumps({
"world": WORLD, "seed": SEED, "settle": args.settle, "seconds": args.seconds, "timeout": args.timeout,
"scenes": [{"name": n, "from": a, "to": b, "pitch": p} for n, a, b, p in scene_list(args)],
"scenes": driver_scenes(args),
**({"server": args.server_address} if args.server else {}),
}))
events = WORK / f"{args.game}-rd{distance}.events.jsonl"
@@ -991,6 +1059,9 @@ def report(args, distance, runs, app):
for key in ("avg_fps", "low_1pct_fps", "low_01pct_fps"):
metric(f"fps.{name}.{key}", [f[key] for f in flights if f])
metric(f"memory.{name}.gpu_mb", [s.get("gpu_mb") for s in scenes])
metric(f"memory.{name}.rss_mb", [s.get("rss_mb") for s in scenes])
metric(f"tick.{name}.client_ms", [s.get("client_tick_ms") for s in scenes])
metric(f"cpu.{name}.p50_ms", [f.get("cpu_p50_ms") for f in flights if f])
metric("memory.rss_peak_mb", [r["memory"].get("rss_peak_mb") for r in runs])
metric("memory.rss_steady_mb", [r["memory"].get("rss_steady_mb") for r in runs])
metric("memory.footprint_peak_mb", [r["memory"].get("footprint_peak_mb") for r in runs])
@@ -1037,6 +1108,22 @@ def table(labels):
cells.append(" / ".join(cell(m.get(f"fps.{scene}.{k}")) for k in ("avg_fps", "low_1pct_fps", "low_01pct_fps")))
print(f"| {d} | {scene} | " + " | ".join(cells) + " |")
print()
# Per-scene costs where any run recorded them (stress scenes): frame CPU, client tick, resident memory.
per_scene = ("cpu.{}.p50_ms", "tick.{}.client_ms", "memory.{}.rss_mb")
rows = [(d, scene) for d in distances
for scene in sorted({k.split(".")[1] for by in runs.values() if d in by for k in by[d]["metrics"]
if k.split(".")[0] in ("cpu", "tick") or (k.startswith("memory.") and k.endswith(".rss_mb"))})]
if rows:
print("| RD | scene | " + " | ".join(f"{l}: frame CPU p50 ms / client tick ms / RSS MB" for l in labels) + " |")
print("| --- | --- |" + " --- |" * len(labels))
for d, scene in rows:
cells = []
for l in labels:
m = runs.get(l, {}).get(d, {}).get("metrics", {})
cells.append(" / ".join(cell(m.get(k.format(scene)), 0 if k.startswith("memory") else 2)
for k in per_scene))
print(f"| {d} | {scene} | " + " | ".join(cells) + " |")
print()
print("| RD | " + " | ".join(f"{l}: title / world s, mean scene load s, RSS steady / peak MB, footprint steady / peak MB, load avg" for l in labels) + " |")
print("| --- |" + " --- |" * len(labels))
for d in distances:
+208
View File
@@ -0,0 +1,208 @@
"""Many-entity stress scenes for tools/perf_bench.py (`--scenes stress` or by name).
Each scene is built by vanilla commands, the same lines for every game (Horizon through horizon-ctl `command`,
vanilla and vanilla-mods through the bench driver mod), on a stone floor at y 289 high above the world spawn of
seed 1234 (loaded already: no terrain to generate), then held still for the flight's seconds, looking along +Z at
the scene. Its cleanup commands then remove every entity and block it made, before the next scene.
Vanilla renders an entity only within 64 * viewScale * its box size (Entity.shouldRenderAtSqrDistance), with
viewScale = clamp(render distance / 8, 1, 2.5): a dropped item (0.25) only within 32 blocks at RD 16. The scenes
keep everything within ~30 blocks of the camera and inside its view, so run them at RD 16 or more.
Entities are still and never despawn (NoAI, NoGravity, PersistenceRequired; items lie on barriers): the same picture every
frame and every run. Particles come from repeating command blocks under the floor, so they keep coming at the
same rate. Chests stay closed: opening one needs a player, which neither driver has.
"""
FLOOR_Y = 289
# The player's feet (the eyes are 1.62 higher).
PLAYER_Y = 298.0
PITCH = 15.0
# The camera stands at the origin, above the spawn of seed 1234 (-519.5, 58.5); the scene lies in front of it, at +Z.
ORIGIN = (-520, 40)
# What a scene may touch: x and z around the origin, y from the command blocks to above the TNT.
BOX = (-24, 24, 2, 40, FLOOR_Y - 9, FLOOR_Y + 26)
SETTINGS = [
# The setup's own commands and the command blocks would fill the chat.
"gamerule send_command_feedback false",
"gamerule command_block_output false",
"gamerule advance_time false",
"gamerule spawn_mobs false",
"time set 6000",
]
MOBS = ["husk", "creeper", "cow", "pig", "sheep", "chicken", "villager", "spider"]
ITEMS = ["stone", "oak_log", "diamond", "apple", "iron_sword", "bread", "redstone", "torch"]
FRAMED = ["diamond_sword", "compass", "clock", "stone", "oak_planks", "apple", "shield", "golden_apple"]
CONTAINERS = ["chest", "trapped_chest", "ender_chest", "white_shulker_box"]
BANNER = ('{patterns:[{pattern:"minecraft:stripe_top",color:"red"},{pattern:"minecraft:creeper",color:"black"},'
'{pattern:"minecraft:border",color:"blue"}]}')
SIGN_TEXT = '{front_text:{messages:["Horizon","stress","sign","wall"]}}'
def num(v):
"""A coordinate as commands write it: no trailing zeros, never an exponent."""
text = f"{v:.3f}".rstrip("0").rstrip(".")
return "0" if text == "-0" else text
def floor(ox, oz):
return [f"fill {ox - 24} {FLOOR_Y} {oz + 2} {ox + 24} {FLOOR_Y} {oz + 40} smooth_stone"]
def wall(ox, oz):
"""A stone wall facing the camera, 17 blocks ahead: the backing of frames and wall signs."""
return [f"fill {ox - 16} {FLOOR_Y - 1} {oz + 17} {ox + 17} {FLOOR_Y + 16} {oz + 17} stone"]
def empty(ox, oz):
return []
def mobs(ox, oz):
"""1000 mobs of 8 common kinds: 20 x 10 x 5 layers, 1.5 / 2 / 2.5 blocks apart."""
out, i = [], 0
for layer in range(5):
for row in range(10):
for col in range(20):
kind = MOBS[i % len(MOBS)]
x, y, z = ox + 0.5 - 14.25 + 1.5 * col, FLOOR_Y + 1 + 2.5 * layer, oz + 8 + 2 * row
out.append(f"summon {kind} {num(x)} {num(y)} {num(z)} "
"{NoAI:1b,NoGravity:1b,PersistenceRequired:1b,Silent:1b}")
i += 1
return out
def items(ox, oz):
"""2000 dropped items of 8 kinds: 25 x 16 on the floor and on four invisible barrier layers above it, 2 blocks
apart, a block apart in a layer (vanilla merges equal stacks within 0.625). They fall the last 0.2 block onto
their layer: no NoGravity, which not every game reads for items."""
out = [f"fill {ox - 12} {FLOOR_Y + 2 * layer} {oz + 8} {ox + 12} {FLOOR_Y + 2 * layer} {oz + 23} barrier"
for layer in range(1, 5)]
i = 0
for layer in range(5):
for row in range(16):
for col in range(25):
item = ITEMS[i % len(ITEMS)]
x, y, z = ox + 0.5 - 12 + col, FLOOR_Y + 1.2 + 2 * layer, oz + 8.5 + row
out.append(f"summon item {num(x)} {num(y)} {num(z)} "
f'{{Item:{{id:"minecraft:{item}",count:1}},Age:-32768s,PickupDelay:32767s}}')
i += 1
return out
def frames(ox, oz):
"""512 item frames holding 8 kinds of item on a wall (32 x 16)."""
out = wall(ox, oz)
i = 0
for y in range(FLOOR_Y, FLOOR_Y + 16):
for x in range(ox - 15, ox + 17):
item = FRAMED[i % len(FRAMED)]
# block_pos too: summoning places the entity, not the block a frame hangs in.
out.append(f"summon item_frame {x} {y} {oz + 16} {{block_pos:[I;{x},{y},{oz + 16}],"
f'Facing:2b,Fixed:1b,Item:{{id:"minecraft:{item}",count:1}}}}')
i += 1
return out
def armor_stands(ox, oz):
"""500 armor stands in full armour holding a sword (25 x 20)."""
gear = ('{ShowArms:1b,NoGravity:1b,equipment:{head:{id:"minecraft:diamond_helmet"},'
'chest:{id:"minecraft:iron_chestplate"},legs:{id:"minecraft:golden_leggings"},'
'feet:{id:"minecraft:leather_boots"},mainhand:{id:"minecraft:diamond_sword"}}}')
return [f"summon armor_stand {num(ox + 0.5 - 12 + col)} {FLOOR_Y + 1} {oz + 8 + row} {gear}"
for row in range(20) for col in range(25)]
def chests(ox, oz):
"""800 containers with block-entity models: four walls 25 wide and 8 high, one kind each."""
return [f"fill {ox - 12} {FLOOR_Y + 1} {oz + 10 + 4 * i} {ox + 12} {FLOOR_Y + 8} {oz + 10 + 4 * i} {kind}"
for i, kind in enumerate(CONTAINERS)]
def signs(ox, oz):
"""A wall of 512 written wall signs (32 x 16) and a field of 175 standing ones in front of it."""
return wall(ox, oz) + [
f"fill {ox - 15} {FLOOR_Y} {oz + 16} {ox + 16} {FLOOR_Y + 15} {oz + 16} oak_wall_sign[facing=north]{SIGN_TEXT}",
f"fill {ox - 12} {FLOOR_Y + 1} {oz + 6} {ox + 12} {FLOOR_Y + 1} {oz + 12} oak_sign[rotation=8]{SIGN_TEXT}",
]
def banners(ox, oz):
"""300 waving banners with three patterns: 12 rows of 25."""
return [f"fill {ox - 12} {FLOOR_Y + 1} {oz + 8 + 2 * row} {ox + 12} {FLOOR_Y + 1} {oz + 8 + 2 * row} "
f"white_banner[rotation=8]{BANNER}" for row in range(12)]
def particles(ox, oz):
"""Steady heavy particles: repeating command blocks under the floor, powered by a redstone block under each,
every tick: flames, smoke, end rods, firework rockets that burst over the floor, and TNT that explodes in the
air (nothing within its reach)."""
def block(dx, dz, command):
command = command.replace('"', '\\"')
at = f"{ox + dx} {FLOOR_Y - 8} {oz + dz}"
return [f'setblock {at} repeating_command_block{{Command:"{command}"}}',
f"setblock {ox + dx} {FLOOR_Y - 9} {oz + dz} redstone_block"]
x, z = ox + 0.5, oz + 20.5
emitters = [
("flame", 80, 0.01), ("flame", 80, 0.01), ("flame", 80, 0.01), ("flame", 80, 0.01),
("campfire_cosy_smoke", 4, 0.0), ("campfire_cosy_smoke", 4, 0.0),
("end_rod", 40, 0.05), ("end_rod", 40, 0.05),
]
out = [line for i, (kind, count, speed) in enumerate(emitters) for line in
block(-8 + 2 * i, 4, f"particle minecraft:{kind} {num(x)} {FLOOR_Y + 5} {num(z)} 8 4 8 {speed} {count} force")]
rocket = ('{LifeTime:18,FireworksItem:{id:"minecraft:firework_rocket",count:1,components:{"minecraft:fireworks":'
'{explosions:[{shape:"large_ball",colors:[I;16711680,65280,255],has_trail:1b}]}}}}')
out += [line for i in range(3) for line in
block(-8 + 2 * i, 6, f"summon firework_rocket {num(x - 6 + 6 * i)} {FLOOR_Y + 2} {num(z)} {rocket}")]
out += block(0, 8, f"summon tnt {num(x)} {FLOOR_Y + 24} {num(z + 6)} {{fuse:0}}")
return out
# name, builder: in the order --scenes stress runs them.
STRESS = [
("stress_empty", empty),
("stress_mobs", mobs),
("stress_items", items),
("stress_frames", frames),
("stress_armor_stands", armor_stands),
("stress_chests", chests),
("stress_signs", signs),
("stress_banners", banners),
("stress_particles", particles),
]
NAMES = [name for name, _ in STRESS]
def origin(name):
return ORIGIN
def scene(name):
"""(name, from, to, pitch) as perf_bench's scenes: a hold at the camera, facing +Z."""
ox, oz = origin(name)
at = (ox + 0.5, PLAYER_Y, oz + 0.5)
return name, at, (at[0], at[1], at[2] + 0.01), PITCH
def setup(name, first=True):
"""The commands that build the scene, after its chunks are loaded; the game rules only for the first scene of
a run (setting a rule again says so in red in the chat, which would stay on screen)."""
ox, oz = origin(name)
return (SETTINGS if first else []) + floor(ox, oz) + dict(STRESS)[name](ox, oz)
def cleanup(name):
"""Removes the scene: its entities first (a frame would drop its item when its wall goes), then every block
in its box, in slabs under vanilla's fill limit, then anything that still dropped. Nothing it runs fails, so
no red line stays in the chat during the next scene: `execute as` kills nothing quietly, and a marker block in
each slab gives every fill something to clear."""
ox, oz = origin(name)
x0, x1, z0, z1, y0, y1 = BOX
kill = "execute as @e[type=!minecraft:player] run kill @s"
out = [kill]
for y in range(y0, y1 + 1, 12):
out.append(f"setblock {ox + x0} {y} {oz + z0} barrier")
out.append(f"fill {ox + x0} {y} {oz + z0} {ox + x1} {min(y + 11, y1)} {oz + z1} air")
return out + [kill]