Get the VoxelBench plugin for your Minecraft server
ad20d61571fe804b5eef713ad49deefc4242535b3b62683eca9ab9a2365dc223This release fixes one defect that ran through the whole bench, though its symptoms looked nothing alike: runs that stopped without a message, tests cut short on slow hosts, and entire reports thrown away while most of their measurements were perfectly good.
Scores remain comparable with 1.7.x. No formula changed, and a report produced by 1.7.0 still compares directly to one produced by 1.8.0.
Between two tests, VoxelBench waits eight seconds for the machine to settle. That delay was expressed in ticks — the server's internal clock. A tick lasts 50 ms when all is well, and a great deal longer when the server is struggling.
So the window stretched exactly in proportion to how useless it had become: the previous test had just saturated the machine, and that was precisely why we were waiting.
Measured on a Folia server during the mob-AI test, at 0.26 ticks per second — one tick every 3.8 seconds — the old window's 160 ticks would have taken 615 seconds: more than ten minutes for an eight-second pause. It now takes 8064 milliseconds.
The change only touches waits whose subject does not depend on the server's rhythm. TNT fuses, redstone timing, fluid propagation and per-tick chunk budgets stay in ticks: that is their correct unit, and converting them would have distorted the measurements instead of protecting them.
A reported log showed a benchmark stopping dead after the chunk-loading test. No error, no message — and the global lock held until the server was restarted.
The cause: nothing watched the gap between two tests. One test's watchdog is disarmed the moment it finishes; the next one's is only armed when it starts. In between sit the teardown of thousands of chunks and the entire setup of the test to come.
A watchdog now covers the run as a whole, and its thresholds are derived from the limits each test declares — a hand-picked threshold would have cut the memory test short, which legitimately asks for up to fifteen minutes.
The memory test had a fixed 300-second limit. On a slow host its final pass needed 377: the test was cut, and the whole report refused.
A test can now request a reprieve. Each completed unit of work pushes the deadline back, while an absolute ceiling stays in place — the point is not to remove the protection, but to tell a slow test from a stuck one.
The reprieve renews on evidence: a pass that genuinely finished. Never on a mere sign of life, or the watchdog would push back its own deadline forever and stop being worth anything.
A user read this in their chat:
⊘ Test ignoré: Mémoire — memoryPressure: projected peak heap 107.6% after pre-flight GC
A developer's sentence, in English, in the middle of a translated message. It names a cause and never a remedy.
Every test that does not run to completion now carries a structured reason: a
translated cause, and above all a computed remedy. No longer "raise -Xmx",
which leaves you guessing, but the amount of memory you would have needed, worked
out:
⊘ Test skipped: Memory — not enough memory — projected peak would reach
107.6% of the heap (1670 MB available)
→ Raise -Xmx (about 2304 MB for the standard profile) or run
/bench start low-memory.
Two additions complete this. A warning before the run when memory plainly will not be enough: the verdict was computable in the first second, and the user learned it in the eleventh minute. And an end-of-run summary that ties the skipped tests together — three notices six minutes apart do not connect themselves in a scrolling chat.
This is the most visible change for anyone measuring a small server.
When memory runs short, VoxelBench refuses to start a test rather than bring the server down. That is the right decision. But until now, a report missing a required test was rejected outright: a host with 1670 MB of heap lost twelve correctly measured tests because three had been skipped — that is, because the plugin had correctly protected itself.
The report now carries, for each test, a status and a cause code. The service can therefore tell a deliberate skip from a crash, and the report is kept and viewable.
It is not scored, and that will not change: putting a number on a run with a missing required test would mean scoring something nobody measured. It does not appear in the leaderboards — but you no longer lose eleven minutes of measurement.
The /bench test disk command reported "✓ Test passed", "TPS 20.00" and "MSPT
50.00 ms". Those three values were hard-coded: they appeared regardless of the
actual result.
Worse, all six throughput metrics displayed 0 MB/s on an NVMe drive that had just measured 2621 MB/s sequential read. The measurement was correct — it reached the report intact — only the display lied, and in the costliest direction for someone whose whole reason for running it was to diagnose their disk.
A missing metric now shows "—" rather than "0". Zero is a plausible throughput: confusing the two reads as a dead disk where there is only a missing value.
No key layers in MapLike[{}]) was written to the console
on every run, by VoxelBench's own bench-world creation. The fix existed but
had only been applied to Folia; three Paper logs said otherwise. The generated
terrain itself was always correct.Validated on three platforms, full run and report submitted: Paper 1.21.11, Spigot 26.1.2 and Canvas 26.1.2 (Folia family). Continuous integration additionally covers Spigot from 1.17.1 to 26.2.
The new report fields are optional: a service that does not know about them ignores them, and reports produced by earlier versions are read exactly as before.
This release fixes one defect that ran through the whole bench, though its symptoms looked nothing alike: runs that stopped without a message, tests cut short on slow hosts, and entire reports thrown away while most of their measurements were perfectly good.
Scores remain comparable with 1.7.x. No formula changed, and a report produced by 1.7.0 still compares directly to one produced by 1.8.0.
Between two tests, VoxelBench waits eight seconds for the machine to settle. That delay was expressed in ticks — the server's internal clock. A tick lasts 50 ms when all is well, and a great deal longer when the server is struggling.
So the window stretched exactly in proportion to how useless it had become: the previous test had just saturated the machine, and that was precisely why we were waiting.
Measured on a Folia server during the mob-AI test, at 0.26 ticks per second — one tick every 3.8 seconds — the old window's 160 ticks would have taken 615 seconds: more than ten minutes for an eight-second pause. It now takes 8064 milliseconds.
The change only touches waits whose subject does not depend on the server's rhythm. TNT fuses, redstone timing, fluid propagation and per-tick chunk budgets stay in ticks: that is their correct unit, and converting them would have distorted the measurements instead of protecting them.
A reported log showed a benchmark stopping dead after the chunk-loading test. No error, no message — and the global lock held until the server was restarted.
The cause: nothing watched the gap between two tests. One test's watchdog is disarmed the moment it finishes; the next one's is only armed when it starts. In between sit the teardown of thousands of chunks and the entire setup of the test to come.
A watchdog now covers the run as a whole, and its thresholds are derived from the limits each test declares — a hand-picked threshold would have cut the memory test short, which legitimately asks for up to fifteen minutes.
The memory test had a fixed 300-second limit. On a slow host its final pass needed 377: the test was cut, and the whole report refused.
A test can now request a reprieve. Each completed unit of work pushes the deadline back, while an absolute ceiling stays in place — the point is not to remove the protection, but to tell a slow test from a stuck one.
The reprieve renews on evidence: a pass that genuinely finished. Never on a mere sign of life, or the watchdog would push back its own deadline forever and stop being worth anything.
A user read this in their chat:
⊘ Test ignoré: Mémoire — memoryPressure: projected peak heap 107.6% after pre-flight GC
A developer's sentence, in English, in the middle of a translated message. It names a cause and never a remedy.
Every test that does not run to completion now carries a structured reason: a
translated cause, and above all a computed remedy. No longer "raise -Xmx",
which leaves you guessing, but the amount of memory you would have needed, worked
out:
⊘ Test skipped: Memory — not enough memory — projected peak would reach
107.6% of the heap (1670 MB available)
→ Raise -Xmx (about 2304 MB for the standard profile) or run
/bench start low-memory.
Two additions complete this. A warning before the run when memory plainly will not be enough: the verdict was computable in the first second, and the user learned it in the eleventh minute. And an end-of-run summary that ties the skipped tests together — three notices six minutes apart do not connect themselves in a scrolling chat.
This is the most visible change for anyone measuring a small server.
When memory runs short, VoxelBench refuses to start a test rather than bring the server down. That is the right decision. But until now, a report missing a required test was rejected outright: a host with 1670 MB of heap lost twelve correctly measured tests because three had been skipped — that is, because the plugin had correctly protected itself.
The report now carries, for each test, a status and a cause code. The service can therefore tell a deliberate skip from a crash, and the report is kept and viewable.
It is not scored, and that will not change: putting a number on a run with a missing required test would mean scoring something nobody measured. It does not appear in the leaderboards — but you no longer lose eleven minutes of measurement.
The /bench test disk command reported "✓ Test passed", "TPS 20.00" and "MSPT
50.00 ms". Those three values were hard-coded: they appeared regardless of the
actual result.
Worse, all six throughput metrics displayed 0 MB/s on an NVMe drive that had just measured 2621 MB/s sequential read. The measurement was correct — it reached the report intact — only the display lied, and in the costliest direction for someone whose whole reason for running it was to diagnose their disk.
A missing metric now shows "—" rather than "0". Zero is a plausible throughput: confusing the two reads as a dead disk where there is only a missing value.
No key layers in MapLike[{}]) was written to the console
on every run, by VoxelBench's own bench-world creation. The fix existed but
had only been applied to Folia; three Paper logs said otherwise. The generated
terrain itself was always correct.Validated on three platforms, full run and report submitted: Paper 1.21.11, Spigot 26.1.2 and Canvas 26.1.2 (Folia family). Continuous integration additionally covers Spigot from 1.17.1 to 26.2.
The new report fields are optional: a service that does not know about them ignores them, and reports produced by earlier versions are read exactly as before.
This release fixes what the bench was measuring, not how it scores. Three defects compounded, and together they meant the TNT stress scale never broke on any machine: it climbed to its configured ceiling on every host, measuring entity spawning and platform construction rather than your server. The headline is short — the bench now proves that a test ran before it publishes that test's number.
The explosion test detonated nothing: forcing a chunk loads it but never
brings it to ENTITY_TICKING, so fuses sat at 40/40 after ten seconds while the
charges were reported "alive". Underneath, the measurement layer failed open
— sixteen fallbacks across the region monitor and the tier verdict all meant
"healthy server", so a tier that measured nothing was published as
20.00 TPS / 0.00 ms, the best possible result. And under that,
max-tnt-per-tick: 100, the default on Spigot and Paper, silently capped
how much load ever reached the server.
protocolVersion stays 1.2.0 and API_VERSION stays 1.2.0: the wire
contract is additive. What changed is what the numbers mean.
Benchmark and stress-limit results from 1.6.0 and earlier are not shifted. For
stressTnt (every platform) and stressHoppers (Paper, Spigot) they are
void, and no Folia stress-limit result existed at all before this version.
There is nothing to convert and nothing to re-read: no 1.6.0 report carries a
single validity witness, so it is impossible to tell after the fact which of
these causes applied to it.
Re-run your campaigns, and expect lower limits. That is the measurement becoming real, not your machine regressing. Scale bounds moved as well, so a tier number and a load value no longer mean what they meant.
Measured breaking points on the corrected TNT scale, for reference: 1938 ±76 (Folia), 2229 ±92 (Paper), 2279 ±65 (Spigot). The corrected hopper scale breaks at 3112 ±71 on Paper, where it previously returned 40 000 — against 121 on Spigot and 2 661 on Folia, three mutually incompatible answers for one test.
Reports produced by 1.7.0 pre-releases are not comparable to this build
either. Reject any that show entityTickRatio of exactly 1.00 on a test with
transient entities, or workCompleted of exactly 0; both were wrong by
construction until late in the cycle.
Forcing a chunk attests to loading, not to ticking, and only a ticking entity burns a fuse. Charges were created, accepted by the server, then frozen — and since frozen work costs a region nothing, TPS held at 20.00 while the scale climbed. Zones now hold their own ticking lease, released zone by zone (one zone finishing used to release the force-load of the other three, which stopped ticking mid-flight), and the measurement window does not open until the zones really tick. At tier 0, 498 blocks now fall where zero fell before.
On Paper that promotion takes about 20 seconds — measured, 9 → 200/200
chunks in 17.8 s on a 150×8 run, against 1.8 ms on Spigot. The wait is announced
in console every 5 s, on the scoreboard and in the action bar, capped at 30 s,
and counted outside the measurement window: it does not enter
durationSeconds, blocksPerSecond or the TPS/MSPT window. It is not a freeze.
The gap between "loaded" and "ticking entities" is now measured rather than
assumed. Over 8 zones and 200 forced chunks, Paper returned 88 ENTITY_TICKING
against 112 TICKING — a level that runs blocks and random ticks, so it has
every appearance of an active zone — while Spigot returned 200/200.
The "no result" branch emitted 20.00 TPS and 0.00 ms across fourteen fields with
measurementDegraded: false: the most degraded case of all passed for the only
healthy one. Symmetrically, loadAbsorbedRatio: -1 — meaning "no witness" — was
remapped to 1.0, so the tier was not saturated and it passed, word for word
what that method's own documentation said it existed to prevent.
The verdict now distinguishes "no witness" from "a witness saying we cannot
judge" and returns NOT_MEASURED — neither held nor broken — read before the
health branch. A tier whose load was never censused therefore stops being
labelled "health score low" in a message stored in the database and shown to
you, as if your machine were at fault.
Two consequences follow:
regionTaskStarved and regionTickCeiling — on mobAI, under 0.68 tps,
corroborating the 0.30 tps the region monitor measured independently.max-tnt-per-tick: 100 is the default on Spigot and Paper. A test posting
1200 charges never saw most of them tick, so it measured a configuration file
rather than your machine — silently, on almost every host.
The cap is now raised in memory for the duration of the test and restored on
every exit path, including /bench stop, a timeout and an exception. Your
spigot.yml is never written, and a restart re-reads it intact. Measured on
150×8 over two consecutive runs per platform, the second run proving the first
restored: tntNeverTicked 300 → 0, tntPresented 900 → 1200/1200,
detonationRatio 0.89 → 1.00, loadAbsorbedRatio -1 → 1.00.
Reports carry hostMaxTntPerTick, hostTntQuotaSource and one of
hostTntQuotaRaised / hostTntQuotaLimited / hostTntQuotaUnknown, so two runs
of the same server stay distinguishable. Set
benchmark-tests.explosion.raise-host-tnt-quota: false to refuse any in-memory
change; the run is then reported as partial and a console warning names the value
to raise yourself.
The read itself was broken too. Bukkit.spigot().getClass().getMethod(...)
resolves an override declared by a non-public anonymous class, so invoke threw
IllegalAccessException on every call, swallowed by a catch — and every read
fell back to new File("spigot.yml"), relative to the JVM's working directory,
missing as soon as a panel or a systemd unit sets another one. There are three
ordered routes now, and the config-shape check refuses any file that is not a
spigot.yml, because reading a falsely high cap elsewhere would credit a host
with absorbing load it never received.
Each test publishes a witness saying whether its entities actually moved, how
many of its chunks really ticked entities, and how much of the announced load the
server was actually given. New fields include entityTickRatio, tntPresented,
tntNeverTicked, detonationRatio, detonationUnaccounted, explodeForeign,
zoneChunksEntityTicking, workCompleted with its unit, loadAbsorbedRatio,
sampleCount and per-tier measurementDegraded. systemInfo now also
distinguishes the host machine from what the server can actually use, through
availableProcessors, memoryLimitGb and primaryDisk.usableGb / .totalFsGb.
entityTickRatio is built on ticksLived, the one counter that only advances
when an entity is ticked. It is armed on every test, sampled over at most 256
entities and collected once a second from each owning region, so a partial
freeze stays visible; entityTickSamples and entityTickConclusive sit beside
it, because "the sampler never ran" and "nothing was readable" are different
failures. For explosions the fuse is the exact witness — it decrements once per
entity tick, so a charge still at its initial value was never ticked once. The
estimate it replaces returned 694 charges presented against 800 detonated, a
ratio of 1.15 that no value < threshold guard could ever catch.
Two rules follow for anyone reading a report:
loadNotPresented,
loadPresentationUnmeasured, zoneTickingNotReady, zoneTickingCapped,
entitiesNeverTicked, entitiesPartiallyTicked, entityCensusPartial,
regionTaskStarved and measurementDegraded describe a doubtful
measurement, not a slow machine, and that tier does not compare to the
others. They are raised only when something is wrong: a healthy report carries
none, because a permanent flag is noise and noise stops being read.-1 as 0. It means "cannot conclude", never "nothing moved".On a populated server, TNT lit by your players or your redstone is now counted
separately as explodeForeign instead of being credited to the machine under
test. The old probe incremented for any TNTPrimed explosion anywhere on the
server, and that counter decides the tier verdict: on the same host the same day,
1 zone gave 1.00, 8 zones gave 0.67, and 8 zones with simulation distance raised
gave 0.48 with no survivors — three limits for one machine, driven by a bench
parameter. Still, run your benchmarks on an idle server: foreign detonations no
longer distort the verdict, but they contaminate the run.
The last stable value of each stress test is remembered in
data/stress-warmstart.yml, and the next run starts near 70 % of it instead
of climbing the whole scale again. If the warm probe breaks, the runner refines
downwards; if nothing there holds, it falls back to a clean full climb. Stale and
other-algorithm entries are ignored. Tunable under stress-limit.warm-start:
enabled (true), factor (0.70), max-refine-down-steps (6), max-age-days
(30).
Warm start makes a run depend on the previous run of that machine, so two
successive runs of the same server are no longer directly comparable with it on.
Set stress-limit.warm-start.enabled: false when you are comparing two
servers against each other.
Separately, a broken tier is now replayed before the scale stops, on the way up and during refinement, with the confirmation variance tightened from 30 % to 12 %. An isolated hiccup no longer condemns a healthy machine. Runs take longer, and some limits go up relative to 1.6.0 for this reason.
/bench tier <test> [tiers=N] [from=N] [to=N] [duration=SEC] [zones=N] [cold] [noskip] runs a single stress test up its scale to diagnose it, without
launching a full campaign. It sits behind the new voxelbench.tier permission
(op by default), never writes the warm-start memory, and submits on the unit-test
channel governed by reports.backend.unit-tests — never as a stress-limit
report. Note that from and to are loads, not tier numbers.
Headline TPS and MSPT now describe the measured window instead of being an exponential-moving-average snapshot read at the worst moment of the run, so every Folia headline number changes magnitude.
An entity the current thread cannot read is also no longer counted as a dead one.
Reading an entity from a thread that does not own its region throws under Folia,
and the old code turned every missed read into a corpse — which produced "no
charges left", then "so they exploded", then "so a plugin is cancelling the
damage": three conclusions stacked on missed reads. EntityCensus now publishes
entitiesTracked, entitiesAlive and entitiesUnreadable, and a verdict resting
on "none are left" requires a complete census.
Scale bounds moved because the real breaking points did. All values are 1.6.0 → 1.7.0:
REFERENCE_VALUES.stressTnt is re-anchored on the results service in the same
move: scoring a 1.6.0 report with the 1.7.0 reference, or the reverse, gives an
incoherent score.
blocksDestroyed counted a ring of blocks nothing had destroyed: the
before-count used a hardcoded radius of 30 and the after-count the platform's
real half-extent, so at 150 TNT it compared 61×61 columns against 51×51 — and
the error grew with the load.explosionImpact was "20.0 minus minimum TPS" with a minimum that falls back
to 20.0 when nothing was measured, so it published 0.00, the best possible
result, on an absence of measurement.blockDamageSuppressed was raised on "no survivors and no damage", which a
never-ticked charge satisfies exactly as well as a neutralised explosion. It
now requires a detonation actually observed, and the report names the
subscribers to EntityExplodeEvent, BlockExplodeEvent and
ExplosionPrimeEvent as suspects, never as a verdict.could not un-force chunk X,Y — it may stay loaded after restart in console./bench stop and the server's lifetime, keeping
three handlers live on every real player's explosion and holding the dead test
instance with its thousands of charges. The standard /bench start path also
discarded every probe counter it collected, and tntCount / tntTotal were
read by the site's scoring (a difficulty factor up to ×1.05) but published by
nobody, so the bonus could never apply.workCompleted: 0 on a test that had done everything: work was derived from
the entities alive at the end, and a test with transient entities has none.
On Folia, blockPhysics published 16 000 tracked entities, 349 blocks/s and
20.0 TPS alongside workCompleted: 0. Work is now anchored on the entities the
test actually created.blockPhysics declared 20 % of its batch
frozen (0.80 Paper, 0.77 Folia); after the fix, Paper reads entityTickRatio
1.00 with no flag and workCompleted 16 000.sampleCount 23 against 37–40 everywhere else./bench test refused 8 of the 26 identifiers it advertises, and /bench stop
did not stop the run and left the server modified afterwards.durationSeconds.
The wait now sits outside the window on every platform.An extension's declared permission was dropped and getTestPermission answered
"no permission required" for every non-built-in identifier, so extension tests
ran without their permission and without their parameter bounds. After updating,
an extension test a non-op could previously launch may be refused — and one meant
to be gated is now genuinely gated.
Nine protected members left AbstractBenchmarkTest's surface —
checkMemoryBudget, getTotalDurationTicks, getWarmupSeconds,
hasReachedDuration, isPositionLocked, loadChunksSquare,
loadChunksSquareSpread, performAggressiveGc, unlockPlayerPosition — some
deleted as callerless, the rest restricted to private; eight new ones replace
them (armTickWitness, captureInProgressMetrics, freezeRegionMeasurement,
publishWork, putIfMeasured, registeredChunkCount, tickingLease,
witnessEntities). An extension built against 1.6.0 needs a recompile. That
class is not part of fr.wasabii.voxelBench.api.**, which is unchanged —
hence the minor bump.
Finally, flags.score is no longer sent in the stress-limit payload. The
plugin's own computed score left the wire; scoring belongs to the backend. Any
third-party consumer reading that field loses it. No other payload field was
removed.
Drop the new jar in plugins/ and restart. There is no database migration, and
your existing config.yml keeps working. Three points deserve attention:
config.yml.
config-version is unchanged at 8 and the plugin does not migrate configs, so
a server upgrading in place keeps its 1.6.0 file. Both keys default to their
new behaviour in code (raise-host-tnt-quota: true,
stress-limit.warm-start.enabled: true), so the new behaviour is what you get.
It is to turn either one off that you must add the block by hand, or
regenerate the file.data/stress-warmstart.yml before your first campaign, or wait out
max-age-days (30 by default). Remembered values at or above the current
ceiling are rejected outright, but a warm start onto a pre-1.7.0 value wastes
tiers.Java 16 or later, for Minecraft 1.17 through the latest release including 26.x. Tested on Spigot, Paper and Folia.
This release fixes what the bench was measuring, not how it scores. Three defects compounded, and together they meant the TNT stress scale never broke on any machine: it climbed to its configured ceiling on every host, measuring entity spawning and platform construction rather than your server. The headline is short — the bench now proves that a test ran before it publishes that test's number.
The explosion test detonated nothing: forcing a chunk loads it but never
brings it to ENTITY_TICKING, so fuses sat at 40/40 after ten seconds while the
charges were reported "alive". Underneath, the measurement layer failed open
— sixteen fallbacks across the region monitor and the tier verdict all meant
"healthy server", so a tier that measured nothing was published as
20.00 TPS / 0.00 ms, the best possible result. And under that,
max-tnt-per-tick: 100, the default on Spigot and Paper, silently capped
how much load ever reached the server.
protocolVersion stays 1.2.0 and API_VERSION stays 1.2.0: the wire
contract is additive. What changed is what the numbers mean.
Benchmark and stress-limit results from 1.6.0 and earlier are not shifted. For
stressTnt (every platform) and stressHoppers (Paper, Spigot) they are
void, and no Folia stress-limit result existed at all before this version.
There is nothing to convert and nothing to re-read: no 1.6.0 report carries a
single validity witness, so it is impossible to tell after the fact which of
these causes applied to it.
Re-run your campaigns, and expect lower limits. That is the measurement becoming real, not your machine regressing. Scale bounds moved as well, so a tier number and a load value no longer mean what they meant.
Measured breaking points on the corrected TNT scale, for reference: 1938 ±76 (Folia), 2229 ±92 (Paper), 2279 ±65 (Spigot). The corrected hopper scale breaks at 3112 ±71 on Paper, where it previously returned 40 000 — against 121 on Spigot and 2 661 on Folia, three mutually incompatible answers for one test.
Reports produced by 1.7.0 pre-releases are not comparable to this build
either. Reject any that show entityTickRatio of exactly 1.00 on a test with
transient entities, or workCompleted of exactly 0; both were wrong by
construction until late in the cycle.
Forcing a chunk attests to loading, not to ticking, and only a ticking entity burns a fuse. Charges were created, accepted by the server, then frozen — and since frozen work costs a region nothing, TPS held at 20.00 while the scale climbed. Zones now hold their own ticking lease, released zone by zone (one zone finishing used to release the force-load of the other three, which stopped ticking mid-flight), and the measurement window does not open until the zones really tick. At tier 0, 498 blocks now fall where zero fell before.
On Paper that promotion takes about 20 seconds — measured, 9 → 200/200
chunks in 17.8 s on a 150×8 run, against 1.8 ms on Spigot. The wait is announced
in console every 5 s, on the scoreboard and in the action bar, capped at 30 s,
and counted outside the measurement window: it does not enter
durationSeconds, blocksPerSecond or the TPS/MSPT window. It is not a freeze.
The gap between "loaded" and "ticking entities" is now measured rather than
assumed. Over 8 zones and 200 forced chunks, Paper returned 88 ENTITY_TICKING
against 112 TICKING — a level that runs blocks and random ticks, so it has
every appearance of an active zone — while Spigot returned 200/200.
The "no result" branch emitted 20.00 TPS and 0.00 ms across fourteen fields with
measurementDegraded: false: the most degraded case of all passed for the only
healthy one. Symmetrically, loadAbsorbedRatio: -1 — meaning "no witness" — was
remapped to 1.0, so the tier was not saturated and it passed, word for word
what that method's own documentation said it existed to prevent.
The verdict now distinguishes "no witness" from "a witness saying we cannot
judge" and returns NOT_MEASURED — neither held nor broken — read before the
health branch. A tier whose load was never censused therefore stops being
labelled "health score low" in a message stored in the database and shown to
you, as if your machine were at fault.
Two consequences follow:
regionTaskStarved and regionTickCeiling — on mobAI, under 0.68 tps,
corroborating the 0.30 tps the region monitor measured independently.max-tnt-per-tick: 100 is the default on Spigot and Paper. A test posting
1200 charges never saw most of them tick, so it measured a configuration file
rather than your machine — silently, on almost every host.
The cap is now raised in memory for the duration of the test and restored on
every exit path, including /bench stop, a timeout and an exception. Your
spigot.yml is never written, and a restart re-reads it intact. Measured on
150×8 over two consecutive runs per platform, the second run proving the first
restored: tntNeverTicked 300 → 0, tntPresented 900 → 1200/1200,
detonationRatio 0.89 → 1.00, loadAbsorbedRatio -1 → 1.00.
Reports carry hostMaxTntPerTick, hostTntQuotaSource and one of
hostTntQuotaRaised / hostTntQuotaLimited / hostTntQuotaUnknown, so two runs
of the same server stay distinguishable. Set
benchmark-tests.explosion.raise-host-tnt-quota: false to refuse any in-memory
change; the run is then reported as partial and a console warning names the value
to raise yourself.
The read itself was broken too. Bukkit.spigot().getClass().getMethod(...)
resolves an override declared by a non-public anonymous class, so invoke threw
IllegalAccessException on every call, swallowed by a catch — and every read
fell back to new File("spigot.yml"), relative to the JVM's working directory,
missing as soon as a panel or a systemd unit sets another one. There are three
ordered routes now, and the config-shape check refuses any file that is not a
spigot.yml, because reading a falsely high cap elsewhere would credit a host
with absorbing load it never received.
Each test publishes a witness saying whether its entities actually moved, how
many of its chunks really ticked entities, and how much of the announced load the
server was actually given. New fields include entityTickRatio, tntPresented,
tntNeverTicked, detonationRatio, detonationUnaccounted, explodeForeign,
zoneChunksEntityTicking, workCompleted with its unit, loadAbsorbedRatio,
sampleCount and per-tier measurementDegraded. systemInfo now also
distinguishes the host machine from what the server can actually use, through
availableProcessors, memoryLimitGb and primaryDisk.usableGb / .totalFsGb.
entityTickRatio is built on ticksLived, the one counter that only advances
when an entity is ticked. It is armed on every test, sampled over at most 256
entities and collected once a second from each owning region, so a partial
freeze stays visible; entityTickSamples and entityTickConclusive sit beside
it, because "the sampler never ran" and "nothing was readable" are different
failures. For explosions the fuse is the exact witness — it decrements once per
entity tick, so a charge still at its initial value was never ticked once. The
estimate it replaces returned 694 charges presented against 800 detonated, a
ratio of 1.15 that no value < threshold guard could ever catch.
Two rules follow for anyone reading a report:
loadNotPresented,
loadPresentationUnmeasured, zoneTickingNotReady, zoneTickingCapped,
entitiesNeverTicked, entitiesPartiallyTicked, entityCensusPartial,
regionTaskStarved and measurementDegraded describe a doubtful
measurement, not a slow machine, and that tier does not compare to the
others. They are raised only when something is wrong: a healthy report carries
none, because a permanent flag is noise and noise stops being read.-1 as 0. It means "cannot conclude", never "nothing moved".On a populated server, TNT lit by your players or your redstone is now counted
separately as explodeForeign instead of being credited to the machine under
test. The old probe incremented for any TNTPrimed explosion anywhere on the
server, and that counter decides the tier verdict: on the same host the same day,
1 zone gave 1.00, 8 zones gave 0.67, and 8 zones with simulation distance raised
gave 0.48 with no survivors — three limits for one machine, driven by a bench
parameter. Still, run your benchmarks on an idle server: foreign detonations no
longer distort the verdict, but they contaminate the run.
The last stable value of each stress test is remembered in
data/stress-warmstart.yml, and the next run starts near 70 % of it instead
of climbing the whole scale again. If the warm probe breaks, the runner refines
downwards; if nothing there holds, it falls back to a clean full climb. Stale and
other-algorithm entries are ignored. Tunable under stress-limit.warm-start:
enabled (true), factor (0.70), max-refine-down-steps (6), max-age-days
(30).
Warm start makes a run depend on the previous run of that machine, so two
successive runs of the same server are no longer directly comparable with it on.
Set stress-limit.warm-start.enabled: false when you are comparing two
servers against each other.
Separately, a broken tier is now replayed before the scale stops, on the way up and during refinement, with the confirmation variance tightened from 30 % to 12 %. An isolated hiccup no longer condemns a healthy machine. Runs take longer, and some limits go up relative to 1.6.0 for this reason.
/bench tier <test> [tiers=N] [from=N] [to=N] [duration=SEC] [zones=N] [cold] [noskip] runs a single stress test up its scale to diagnose it, without
launching a full campaign. It sits behind the new voxelbench.tier permission
(op by default), never writes the warm-start memory, and submits on the unit-test
channel governed by reports.backend.unit-tests — never as a stress-limit
report. Note that from and to are loads, not tier numbers.
Headline TPS and MSPT now describe the measured window instead of being an exponential-moving-average snapshot read at the worst moment of the run, so every Folia headline number changes magnitude.
An entity the current thread cannot read is also no longer counted as a dead one.
Reading an entity from a thread that does not own its region throws under Folia,
and the old code turned every missed read into a corpse — which produced "no
charges left", then "so they exploded", then "so a plugin is cancelling the
damage": three conclusions stacked on missed reads. EntityCensus now publishes
entitiesTracked, entitiesAlive and entitiesUnreadable, and a verdict resting
on "none are left" requires a complete census.
Scale bounds moved because the real breaking points did. All values are 1.6.0 → 1.7.0:
REFERENCE_VALUES.stressTnt is re-anchored on the results service in the same
move: scoring a 1.6.0 report with the 1.7.0 reference, or the reverse, gives an
incoherent score.
blocksDestroyed counted a ring of blocks nothing had destroyed: the
before-count used a hardcoded radius of 30 and the after-count the platform's
real half-extent, so at 150 TNT it compared 61×61 columns against 51×51 — and
the error grew with the load.explosionImpact was "20.0 minus minimum TPS" with a minimum that falls back
to 20.0 when nothing was measured, so it published 0.00, the best possible
result, on an absence of measurement.blockDamageSuppressed was raised on "no survivors and no damage", which a
never-ticked charge satisfies exactly as well as a neutralised explosion. It
now requires a detonation actually observed, and the report names the
subscribers to EntityExplodeEvent, BlockExplodeEvent and
ExplosionPrimeEvent as suspects, never as a verdict.could not un-force chunk X,Y — it may stay loaded after restart in console./bench stop and the server's lifetime, keeping
three handlers live on every real player's explosion and holding the dead test
instance with its thousands of charges. The standard /bench start path also
discarded every probe counter it collected, and tntCount / tntTotal were
read by the site's scoring (a difficulty factor up to ×1.05) but published by
nobody, so the bonus could never apply.workCompleted: 0 on a test that had done everything: work was derived from
the entities alive at the end, and a test with transient entities has none.
On Folia, blockPhysics published 16 000 tracked entities, 349 blocks/s and
20.0 TPS alongside workCompleted: 0. Work is now anchored on the entities the
test actually created.blockPhysics declared 20 % of its batch
frozen (0.80 Paper, 0.77 Folia); after the fix, Paper reads entityTickRatio
1.00 with no flag and workCompleted 16 000.sampleCount 23 against 37–40 everywhere else./bench test refused 8 of the 26 identifiers it advertises, and /bench stop
did not stop the run and left the server modified afterwards.durationSeconds.
The wait now sits outside the window on every platform.An extension's declared permission was dropped and getTestPermission answered
"no permission required" for every non-built-in identifier, so extension tests
ran without their permission and without their parameter bounds. After updating,
an extension test a non-op could previously launch may be refused — and one meant
to be gated is now genuinely gated.
Nine protected members left AbstractBenchmarkTest's surface —
checkMemoryBudget, getTotalDurationTicks, getWarmupSeconds,
hasReachedDuration, isPositionLocked, loadChunksSquare,
loadChunksSquareSpread, performAggressiveGc, unlockPlayerPosition — some
deleted as callerless, the rest restricted to private; eight new ones replace
them (armTickWitness, captureInProgressMetrics, freezeRegionMeasurement,
publishWork, putIfMeasured, registeredChunkCount, tickingLease,
witnessEntities). An extension built against 1.6.0 needs a recompile. That
class is not part of fr.wasabii.voxelBench.api.**, which is unchanged —
hence the minor bump.
Finally, flags.score is no longer sent in the stress-limit payload. The
plugin's own computed score left the wire; scoring belongs to the backend. Any
third-party consumer reading that field loses it. No other payload field was
removed.
Drop the new jar in plugins/ and restart. There is no database migration, and
your existing config.yml keeps working. Three points deserve attention:
config.yml.
config-version is unchanged at 8 and the plugin does not migrate configs, so
a server upgrading in place keeps its 1.6.0 file. Both keys default to their
new behaviour in code (raise-host-tnt-quota: true,
stress-limit.warm-start.enabled: true), so the new behaviour is what you get.
It is to turn either one off that you must add the block by hand, or
regenerate the file.data/stress-warmstart.yml before your first campaign, or wait out
max-age-days (30 by default). Remembered values at or above the current
ceiling are rejected outright, but a warm start onto a pre-1.7.0 value wastes
tiers.Java 16 or later, for Minecraft 1.17 through the latest release including 26.x. Tested on Spigot, Paper and Folia.
This is a hardening and tooling release built on top of the Folia work shipped in 1.5.0. The headline is that Folia measurement is now accurate and stable across the whole suite: a silent task-cancellation bug that produced ghost timeouts, a stuck action bar and garbage TPS deviation is fixed, and every gameplay test — built-in and extension alike — is now measured on the region actually under load. The release also brings a rebuilt monitor subsystem, a new connectivity diagnostic command, and a large internal cleanup.
Spigot and Paper runs remain byte-identical to 1.5.0. Every change described
below is gated behind ServerCompat, so if you do not run Folia, your numbers
are exactly the ones 1.5.0 produced.
On Folia, ScheduledTask.cancel() threw an IllegalAccessException because the
task class is not public, and the exception was silently swallowed. The
cancellation looked like it had happened; nothing had. Timers, test timeouts and
the per-region samplers all kept running.
The symptoms did not look related to one another:
The fix is setAccessible on the task class, so cancellation now takes effect.
Alongside it, all internal timers are tracked so they can be cancelled as a
group, and Folia chunk loading is now performed per region.
On Folia, the global region is idle while a benchmark runs — the work is happening in whichever region the test occupies. Until now, only the six dispersed tests fed the shared per-region monitor. Every other test read its TPS and MSPT from the idle global region, which carries none of the load.
The non-dispersed gameplay tests now feed that same monitor: mob AI, block physics, bone-meal, redstone, liquids, combat, villagers, collisions, cramming, projectiles, pathfinding and chunk ticking. Extension tests are covered too, and get correct Folia measurement without any change on their side.
/bench ping tells you where the connection breaksWhen a report will not submit, the useful question is which layer is failing.
/bench ping runs a staged probe — DNS, then TCP, then TLS, then HTTP — and
reports pass or fail for each step: an unresolved host, a blocked port, a TLS
handshake failure, a timeout, or the HTTP status when the service answers.
The probe runs off the main thread and is available on every build.
The monitor subsystem was restructured with no change in behaviour. The boss-bar logic moved into its own manager, the web dashboard's HTML was externalized with Chart.js bundled locally, and the monolithic monitor command was split into per-subsystem handlers. The two largest files shrank by roughly two to three times.
/bench verify no longer crashes the scheduler in SlpInjector, and no longer
floods chat on a successful link.loadChunksSpread completion latch is hardened against a missed signal on
Folia.Java 16 or newer, Minecraft 1.17 through the latest release including 26.x. Tested on Spigot, Paper and Folia. The continuous-integration compatibility matrix now extends to Minecraft 26.2 across Paper, Spigot and hybrid servers, alongside the versions already covered.
Drop the new jar into plugins/ and restart. There is no configuration change
and no database migration.
This release makes Folia a platform VoxelBench can actually measure. /bench start now runs the entire test suite on Folia — no skipped tests, no failing
ones — and produces a complete report instead of a partial one. The numbers in
that report are measured where Folia does its work, and the report says so, so
nothing gets compared apples-to-oranges against a single-threaded run.
Spigot and Paper runs are unchanged — byte-identical scores to 1.4.0. Every Folia behaviour is gated behind server-type detection. If you do not run Folia, upgrading changes nothing about your results.
Until now, a Folia benchmark came back with holes in it. The entire built-in catalogue now executes correctly on Folia's per-region scheduler: chunk loading, mob spawning, redstone, block physics, chunk ticking, hoppers, explosions, lighting, tile entities, mob AI, bone-meal growth, liquids and the rest.
The reason it works is that the benchmark is region-aware end to end, not merely
patched test by test. World acquisition, weather and time, the player's teleport,
gamemode and state, chunk loading, force-loading and cleanup all run on the
correct region thread, so a run no longer commits main-thread or async-region
violations and no longer trips the server watchdog. Force-loading in particular
now goes through plugin chunk tickets rather than setForceLoaded, which only
ever addressed the global region. Shared helpers — ZoneExecutor, a region-aware
BenchmarkContext, AbstractBenchmarkTest — carry that logic so it is not
re-implemented, differently, in every test.
On Folia the work is spread across region threads while the global region sits idle. A single global TPS/MSPT reading therefore measures the one thread that is doing nothing — a number that looks like a result and is meaningless.
The benchmark now samples TPS and MSPT per region and aggregates them into
the same result fields you already know — the average across regions, with
minimum and maximum standing for the best and worst region. Reports also carry a
regionized flag and a region count in their envelope, so the service reading
them knows it is looking at a multi-threaded run and interprets the figures
accordingly.
Alongside this, select tests — block physics and mob spawning — now also report throughput, expressed as work per second, which suits a parallel model better than a per-tick figure. These metrics are added, never substituted: the existing ones are untouched, and a service that does not know the new ones falls back to what it already understood.
Cannot set gamemode async on Folia. The player is now teleported first, and the
gamemode restored only once the asynchronous teleport has settled.UnsupportedOperationException. The temporary
benchmark world is now left in place and removed cleanly on the next server
start, instead of erroring at the end of every run.ScheduledTask.isCancelled() always answered
"cancelled" on Folia — it reflected a method Folia tasks do not expose — so
every if (!isCancelled()) cancel() quietly skipped the cancel. Samplers and
timeouts piled up across a run, raising per-tick overhead and GC pressure, and
degrading the very measurement they served. They are now cancelled correctly.The extension API moves to 1.2.0, with a region-aware BenchmarkContext. The
change is backward compatible, and the API remains PREVIEW.
It lets a third-party plugin register its own benchmark tests against VoxelBench, with typed parameters, declared output metrics, full lifecycle hooks and mock helpers for unit tests. It is functionally complete and exercised by the bundled sample plugins, but it is not officially announced for public use yet — expect changes until the public release.
Drop the new jar in plugins/ and restart. There is no configuration change and
no database migration.
One thing to expect on Folia: a faithful Folia benchmark deliberately disperses its work across region threads. It shines on machines with many cores, and is intentionally slower on a server with only a few.
Java 16 or later for MC 1.17 through 1.21.x; Java 21 recommended for MC 1.20.5 and above; Java 25 LTS for MC 26.1.x. Tested on Paper, Purpur, Pufferfish, Folia and Spigot. Continuous integration now includes Folia boot and runtime-compat smoke tests on Folia 26.1.2, with assertions that catch thread and region violations.
This is a tooling and interface release. The stress-limit mode was rebuilt so a
run keeps climbing until the server genuinely buckles, the CPU tests let you
choose which part of the processor you stress, the /bench menu now builds
itself from the live test catalogue, tests can be configured in-game through a
real form, and reports carry an inventory of the server's environment so the
leaderboards can flag setups that inflate scores.
No breaking changes to standard-benchmark scoring.
The stress-limit mode used to plateau early, or stop on a reading that was never a real rupture. It now ramps its tiers adaptively and measures lag faithfully, so a run continues until something actually gives.
Three failure modes are gone with it: false ruptures caused by a mean-based break trigger, endless hopper tiers, and tiers that never reached the breaking point at all.
The new villager-brain vector piles on villager AI and pathfinding until the brain ticks drown the server. That brings the count to seven stress vectors.
You can now compose your own stress recipe — which vector, which starting load,
which ramp — in a YAML file dropped into
plugins/VoxelBench/custom_benchmarks/, then run it from chat or from the menu.
The GUI carries a custom-profile browser so those files can be launched without
typing their names.
The single-core and multi-core tests can run one of four compute kernels, so you can target the part of the CPU you care about instead of a single fixed workload:
int — integer ALU throughputfloat — floating-point throughputmemory — memory-bound access patternbranch — branch-prediction stressThe kernel is selectable per test, through the command parameter, tab-completion, or the in-game configurator.
The /bench GUI was largely rewritten. Test menus now build themselves from the
live test catalogue, which means the menu can no longer drift out of sync with
what the plugin is actually able to run: new built-in tests, and third-party
tests registered through the extension API, appear on their own with no menu
edits.
The overhaul also brings a Mods screen, a server config-profile screen, a dynamic stress menu, a clearer tab bar, and the removal of a duplicate "Tests" entry in the main menu. All the new screens, and the test parameter labels, are localized in English and French.
One defect went with it: refresh() blanked the screen, which broke pagination,
value cycling and every refreshed menu. It now rebuilds and re-opens correctly.
On Paper 1.21.6+, opening a test brings up a proper configuration window instead of asking you to remember its parameters. Each test gets one form, with dropdowns for choice parameters, readable and translated labels, and free-text entry via shift-click.
This runs on a new reusable Paper dialog layer (gui.dialog). On any other
platform the configurator falls back automatically to an anvil, chat or inventory
flow, so every server can still set parameters cleanly.
Built-in tests now suggest sensible values for each parameter as you type, not
just the parameter names. Tests added through the extension API get the same
value completion via ParamSpec suggested values.
Benchmark reports now inventory the server's mods, plugins and configuration profile. This is what lets the leaderboards spot and flag setups that inflate scores — tweaked view-distance, spawn limits, performance mods — so honest runs are not drowned out by doctored ones. The plugin only reports the data; the flagging happens server-side.
The extension API moves to 1.1.0 — suggested values, backward compatible, and still marked PREVIEW.
Third-party plugins can register their own benchmark tests against VoxelBench, with typed parameters, declared output metrics, full lifecycle hooks and mock helpers for unit tests. The surface is functionally complete and exercised by the bundled sample plugins, but it is not officially announced for public use yet: expect changes until the public release.
Drop the new jar in plugins/ and restart. There is no config or database
migration — the new options ship with sensible defaults. On Paper 1.21.6+ the
in-game configuration windows light up on their own; every other platform keeps
the anvil / chat / inventory flow. Custom stress profiles live next to the
benchmark profiles in plugins/VoxelBench/custom_benchmarks/; delete the samples
you do not need.
Java 16+ for MC 1.17 to 1.21.x. Java 21 recommended for MC 1.20.5+. Java 25 LTS for MC 26.1.x.
Tested on Paper, Purpur, Pufferfish, Folia and Spigot. The in-game configuration windows require Paper 1.21.6+; everything else degrades gracefully on older or non-Paper platforms.
This release is about not bringing your server down while measuring it, knowing
what kind of machine a measurement came from, and being told what could spoil a
run before it starts. Three layered guards now stand between the bench and an
OutOfMemoryError, the plugin identifies the host it runs on, and the
confirmation before a run has become a screen listing everything that could
spoil the result.
Chunk-loading results are not comparable with 1.2.1 and earlier. The standard test now loads a different number of chunks; the details are in the next section.
The standard chunkLoading test loads 2000 chunks per zone instead of 5000.
With the default eight dispersed zones, a run covers 16 000 chunks instead of
roughly 40 000 — a figure that was itself clamped to about 37 800 by the adaptive
memory cap on a 6 GB heap.
The reason is the memory work below. Forty thousand chunks routinely pushed heap
to the 92 % mark that now trips the watchdog on 6 GB hosts; sixteen thousand
stays comfortably under the threshold at which the pre-flight budget refuses to
start a test at all. The bundled standard.yml profile mirror was updated to
match, and low-memory.yml was taken down to 1000 chunks per zone across four
zones — 4000 total — so that it stays meaningfully lighter than the new standard.
The consequence for you: a 1.3.0 run is not directly comparable to a 1.2.1 run for this test, nor for the aggregate score that weights it. Leaderboards for chunk loading have to be read per plugin version.
A bench that would previously have taken the server down now refuses to start, or stops itself, and says which of the two happened.
benchmark.post-cleanup-gc, on by default.SKIPPED with
the reason memoryPressure. Tunable with
benchmark.memory-budget.{warn-pct,fail-pct}.FAILURE with an oom_abort warning and cleanup is forced,
instead of letting the JVM throw an actual OutOfMemoryError. Tunable with
benchmark.memory-watchdog.{critical-pct,hold-seconds}.Five probes look for signs of a free-tier host: marker files (Aternos, Minehut,
FalixNodes, Server.pro, the MSH wrapper, Pterodactyl), plugin names, environment
variables, JVM resources (a heap of 1 or 2 GB, a single-CPU JVM), and
/proc/1/cgroup markers. Their verdicts aggregate into a five-level tier —
DEDICATED_OR_VPS, LIKELY_PAID_HOSTED, SUSPECTED_FREE, LIKELY_FREE,
CONFIRMED_FREE. A single vendor marker forces the tier to at least
LIKELY_FREE whatever the score says.
This surfaces twice. At /bench start, a non-blocking chat warning names the
detected provider, the signals that pointed at it, the variance you should
expect, and points to /bench custom run free-host. And every report now carries
a hostingEnvironment block — tier, score, vendor, signals, allocated heap,
processor count — which is what the service needs to keep free-tier runs out of
the public leaderboards. That block is deliberately left out of anonymization
even in FULL mode: without the provider name the filtering cannot work.
Detection can be turned off with hosting-detection.enabled.
free-host.yml is roughly a tenth of the standard workload, sized to fit under
the free-tier killers on Aternos and MSH — TPS killer, RAM cap, CPU throttle.
low-memory.yml is roughly a third, for the 2 to 4 GB heaps of small VPS and
dedicated hosts. Neither is comparable with /bench start; low-memory.yml
runs remain comparable with each other.
The old flow asked you to type /bench start twice within ten seconds, which
told you nothing about why a second thought might be warranted. It is replaced
by an inventory screen with one tile per detected risk, coloured by severity:
voxelbench_* world; or automatic
temporary worlds are disabled and no world is pinned.LIKELY_FREE or worse.Clicking a tile prints the full details in chat. A critical finding disables the
green Start button; only a Force button, visible to holders of
voxelbench.start.force, can launch through it. Closing the screen without
clicking counts as a cancellation. When nothing at all is detected, the screen is
skipped and the run starts.
systemInfo.primaryDisk classifies the device holding the world folder as NVMe,
SSD, HDD, RAMDISK or unknown. The classification tries filesystem type first
(tmpfs or ramfs means a RAM disk), then the device name (nvme*), then the
Linux rotational flag, and only then a heuristic on the model string. Model,
type, size and mount point reach the report and appear as a dedicated tile in the
disks screen.
systemInfo.memory exposes each memory module: capacity, speed, DDR type,
manufacturer, part number and bank label, plus an aggregate type and speed across
modules — reported as mixed when they differ. This comes from SMBIOS, which on
Linux requires root, so a non-root server returns an empty list; it does so with
stable UNKNOWN and -1 sentinels rather than a different JSON shape. CAS
latency and timings are not included and will not be: they need kernel-level SPD
access that no JVM API offers.
Anonymization was extended to cover both blocks. The mount point is always masked
in partial and full modes, because /home/<user>/... leaks a Linux username. The
disk model is generalised to "Generic NVMe" and friends in full mode. Part
numbers are hashed in partial mode and redacted in full — hashing rather than
dropping keeps the "do these two servers have identical RAM kits?" comparison
working. Manufacturer and bank label are redacted in full mode.
/bench info, rewritten from top to bottomFifteen sections, where several used to be near-empty titles. The existing cpu,
ram, disk, network, spigot, java and system sections were enriched,
and eight are new: performance (live TPS, MSPT and CPU), plugins (with
enabled and disabled state), worlds (chunks, entities, players and seed per
world), sensors (CPU temperature, voltage, fans), auth (mode, anonymization,
server ID, rate limit), bench (last run summary), build (plugin version,
profile, full JVM arguments) and hosting (tier and signals).
Columns align to the pixel. A new width utility mixes bold and regular spaces to
pad with 1-pixel precision, where plain spaces only offer a 4-pixel resolution.
Tab completion now proposes the fifteen real sections instead of four obsolete
ones. And /bench info auth masks the server ID by default, printing
abcd…wxyz (64 chars); /bench info auth hash reveals it.
Until this release, every test ended on "✓ Test complete" regardless of whether the watchdog had killed it or the memory budget had refused to start it. The outcome line now branches on the real status: ✓ complete, ⊘ skipped with its reason, ✗ failed with its reason.
Wiring that up uncovered an older defect. The two-argument and eight-argument
constructors of the unified test result set success but never synchronised
status, which stayed at its declared default of failure. Nobody read the status,
so nothing showed — until the outcome line started reading it, at which point
every successful test built through those constructors would have been displayed
as failed. Both constructors now set the two fields in lockstep.
/bench stop now stopsThe stop command was lying. Three coupled bugs, each hiding the next:
isStopped flag
was never flipped and the scoreboard stayed on screen.All three are fixed. Cleanup is synchronous and the scoreboard disappears immediately, the late callback is suppressed, and no outcome line is emitted for a stopped run.
A 4xx response from the backend now surfaces its body in chat. Instead of
HTTP 400, you read HTTP 400 — missing required field 'X' at /tests[2]/status. Staging builds additionally write every response body next to
the request payload in plugins/VoxelBench/raw-reports/, so the two can be
diffed side by side; on production builds this does nothing.
The first run after upgrading on a host with 4 GB of heap or less may show one or
two skipped tests from the new memory budget. That is the intended behaviour: the
bench is refusing to start tests that would have run the server out of memory.
Check heap usage with /bench info ram, or raise -Xmx.
Reports now carry three new blocks — hostingEnvironment,
systemInfo.primaryDisk and systemInfo.memory. A service that predates them
ignores them silently; there is no schema break.
VoxelBench 1.2.0 is, for the most part, a release for people who write
benchmark tests rather than for people who run them. From a server operator's
seat the plugin behaves as 1.1.0 did, with two exceptions that matter: hybrid
runtimes finally generate and keep their benchmark worlds, and the /bench
command stops discarding what you type. The bulk of the diff is a new typed
extension API for plugin developers who want to add their own tests — a surface
that is in preview and not yet announced for public third-party use.
On Arclight and Mohist, the flat benchmark world was not flat. Forge silently
intercepts WorldType.FLAT on those runtimes, so the world VoxelBench asked for
was not the world it received. A new FlatChunkGenerator bypasses the NMS layer
through Bukkit's own ChunkGenerator API. It is activated only on hybrid
runtimes: Paper, Spigot and Folia keep exactly the code path they had.
On hybrids, the world also used to disappear on every server restart. Worlds an
operator created (voxelbench_<name>) are now loaded again when the plugin
enables, so a benchmark world set up once stays set up.
Related: /bench world create <name> now refuses when a folder of that name is
already on disk from a previous session. It used to load the legacy chunks and
layer the new flat generation on top, which produced half-flat terrain.
One test was affected badly enough to be worth naming. On hybrids,
world.spawnEntity() returns null without complaint when the target chunk is
not ticking, and each of those nulls was counted as a successful spawn — so
samples.entitySpawn "finished" in zero seconds having spawned zero mobs. The
test now pre-loads the nine chunks around the centre of its zone before the
spawn loop starts.
Continuous integration grew a hybrid smoketest matrix to keep these fixed: Mohist 1.20.1 (via a community mirror), Arclight 1.20.1 and Arclight 1.20.4, with Mohist 1.20.2 and NeoTenet 1.21.1 soft-skipped where upstream is broken.
/bench command says what it did, and keeps what you typedExtension tests now accept key=value parameters on the command line —
/bench test myext.foo count=20 url=https://example.com. They were previously
thrown away.
The rest of the command surface was tightened in the same pass:
/bench test <id> <TAB> completes from the live registry, so extension tests
appear where you would expect them./bench tests list is built from that registry too — grouped by category,
each test tagged with its owner — instead of a hard-coded list./bench tests is now an alias of /bench test, in both the dispatcher and
tab completion./bench help is an explicit case rather than the silent default, and an
unknown subcommand prints CMD_UNKNOWN before the help dump instead of
looking like help was what you asked for./bench custom run respects the world pinned with /bench world set, which
it previously ignored.Results are rendered as three distinct outcomes rather than two: success
(green ✓), skipped (yellow ⊘) and failure (red ✗). The message attached to a
failure is now read from the result's dedicated error field; it was being
looked up in metrics["error"], which extension tests never wrote to, so
failures explained themselves to nobody.
Two smaller irritations went with it. Boolean metrics display "Yes" and "No"
instead of [Missing: result.no], and the createScoreboard() returned null
warning was demoted to verbose — it is the normal, expected outcome for hardware
tests and for every extension test, not an error worth a console line.
Finally, mobCount in samples.entitySpawn was silently clamped at 5000. The
cap is now 50,000: asking for 10,000 mobs gets you 10,000 mobs.
An extension test that had completed normally could still trip the parent's
safety timeout 60 or 120 seconds later, killing a run that had nothing wrong
with it. The adapter now routes its result through finishTest() instead of
invoking the callback directly, which disarms the timeout on the way through.
The API 1.1 surface described below is marked @ApiStatus(EXPERIMENTAL). It is
usable and documented, but it is not yet announced for public third-party use,
and the stability guarantees are the ones set out in docs/API_STABILITY.md §5.
ParamSpec gives a test a typed declaration of the parameters it accepts —
intParam, longParam, doubleParam, stringParam, booleanParam, each with
a default value, an optional range, a description and a required flag.
MetricSpec is its symmetric counterpart for what the test emits: scalar,
scalarInt, series, flag and text, annotated with a unit, a
higherIsBetter direction, a precision and a primary marker.
Declaring is only half of it. The host now runs ParamValidator before calling
run(): a missing required parameter fails the test, a type mismatch is coerced
or fails, and an out-of-range value is clamped with a warning to the sender.
After a successful run, a warn-only MetricValidator compares what was emitted
against what was declared, surfacing typos and drift without failing a run that
otherwise worked.
Because the host derives the legacy hint list from the declared specs,
TestDescriptor.Builder.paramHints(String...) is deprecated in favour of
.params(ParamSpec...).
TestResult.Status is a tri-state enum — SUCCESS, FAILURE, SKIPPED — with
a TestResult.skipped(reason) factory alongside success(...) and
failure(...). The distinction is between "the server lost" and "this test does
not apply here", and it is meant to be honoured downstream: a backend should
exclude skipped tests from leaderboards rather than count them as losses.
TestBuilder.build(...) now receives a BuildContext, a narrow build-time view
with no world, no sender and no stop signal; BenchmarkContext extends it for
the runtime view, so runtime callers see no change. "ctx.getWorld() returns
null at build time" is now a compile error rather than a null at the worst
moment. Samples that never touched the context at build time are
source-compatible; the ones that did were already buggy.
TestCompletion wraps the raw Consumer<TestResult> callback with single-call
enforcement (a second call logs SEVERE rather than doing something undefined),
an automatic bounce to the main thread, and typed success / failure /
skipped shortcuts. The host carries its own anti-double-callback guard, whose
SEVERE log names the offending extension.
ApiVersion replaces string-sniffing compatibility checks such as
getApiVersion().startsWith("1.") with a structured (major, minor, patch)
value exposing isAtLeast(major, minor) and isCompatibleWith(other).
TestDescriptor gained .author(...), .version(...), .tags(...) and
.documentationUrl(...), plus an automatically computed schemaHash: 12 hex
characters of the SHA-256 of the canonical schema — id, version, the shape of
the parameters and the shape of the metrics. A backend can compare that hash
across submissions and notice when a test's schema changed underneath it.
Reports carry these fields (provider, author, extensionVersion,
schemaHash, documentationUrl) at the top level of each test object, and they
are filtered out of the metrics{} and flags{} blocks so they stay structured
metadata rather than pseudo-measurements.
TestDescriptor.Builder.owner(String) is deprecated: the registry injects the
owner from the Plugin passed to register(desc, plugin). Existing call sites
keep working with a deprecation warning.
Two Bukkit events, TestStartingEvent and TestCompletedEvent, are fired by
the host before and after each test run, with listener exceptions isolated so a
misbehaving listener cannot take a run down. Metric is now a marker interface
implemented by RichMetric and RichMetricSeries, and RichMetric.ofInt(long)
and RichMetric.ofDouble(double) are explicit factories — ofInt defaults to a
precision of 0, so an integer counter renders as "500" and not "500.00".
For unit tests, testing.MockBenchmarkContext and testing.CapturingCompletion
let an extension be tested without bootstrapping a Bukkit server.
Two contexts that used to mislead were fixed along the way: ctx.getLogger()
actually prefixes each record with [testId] now instead of handing back the
unprefixed host logger, and ctx.getSender() is exposed on BenchmarkContext
so an extension can route out-of-range warnings to the operator in-game rather
than only to the server console.
./gradlew newExtension -PextName=MyBench -PtestId=mybench.foo scaffolds a
fresh extension plugin under extensions/<name>/, wired with the canonical 1.1
patterns: parameter and metric specs, TestCompletion, owner auto-injection and
depend: [VoxelBench].
./gradlew checkApiImports fails the build if any file under api/ imports
outside the allowed surface — Bukkit, the JDK, and its own package. It is wired
into the standard check lifecycle, so the API stays free of internals by
construction rather than by review.
Five documents accompany the surface: API_STABILITY.md (SemVer and deprecation
policy, the three-tier @ApiStatus model, a breaking-change matrix),
API_THREADING.md (which thread each method runs on, three execution patterns,
the cancellation contract, cleanup() semantics, anti-patterns),
API_CONVENTIONS.md (maintainer conventions), BACKEND_INGESTION_SPEC.md (an
exhaustive presence matrix for every JSON key the plugin emits, sentinel-value
handling and a recommended ingestion pipeline), and
EXTENSION_API_REFERENCE.md, refreshed to 1.1 with a status table.
The five canonical samples were migrated to the 1.1 patterns — TestCompletion,
ParamSpec, MetricSpec, ofInt and provenance metadata. One of them is new:
CreateKineticBenchmark, a hybrid-only benchmark targeting the Create mod
(around 100 million downloads on CurseForge), which demonstrates the
probe-then-skip pattern — detect the mod's blocks through
Bukkit.createBlockData(), and return TestResult.skipped(...) when they are
absent instead of failing.
Until now, VoxelBench always benchmarked in the first world your server loaded — usually the one your players live in. This release lets you pick the world instead, or have the plugin build a dedicated one for you in a single command. It also integrates with Multiverse-Core, blocks gameplay benchmarks from the console where they never worked properly, and makes sure a disconnect mid-run no longer leaves you stranded inside a bench world.
Everything here is opt-in. If you never touch /bench world, the plugin
behaves exactly like v1.0.2. There is no config change to make and no database
migration to run.
The new /bench world subcommand pins a world for every future benchmark:
/bench world list — list the loaded worlds, tagged [pinned], [bench] and [MV]/bench world show — show which world is currently pinned/bench world set <name> — pin a loaded world for every future run/bench world unset — clear the pin and go back to the default behaviour/bench world create <name> — create a flat, normal-environment world/bench world delete <name> — delete a voxelbench_* worldA world created this way is auto-prefixed with voxelbench_, which is what makes
deletion safe: /bench world delete only accepts names carrying that prefix, so
the command cannot be pointed at your survival map. Its seed is fixed to
benchmark, so the terrain is the same on every server and across every run —
two benchmarks measure the machine, not the landscape they happened to land in.
The pin is stored in config as benchmark.target-world, and it is honoured by
every code path that touches a world: /bench start, /bench stresslimit,
individual /bench test runs, the multi-run warmup, post-run region cleanup, the
test lock manager — so /bench stop and the shutdown cleanup target the right
world — and the confirmation message shown when a run starts. Tab completion
knows the subcommand, and only suggests voxelbench_* names for delete.
Deletion refuses outright if a benchmark is currently running in that world, and
if you delete the world that was pinned, the pin is cleared for you rather than
left dangling. The user-supplied part of a world name must match
^[A-Za-z0-9_-]{1,32}$, which blocks path traversal, characters that are
illegal on Windows filesystems, reserved names and lengths that overflow ext4 —
and the same check runs again on delete, not only on create.
The plugin detects Multiverse-Core at startup and routes world creation and
deletion through it when it is present, falling back to Bukkit's native
WorldCreator and folder removal when it is not. The feature therefore works on
every server, with or without Multiverse.
Detection is based on the plugin name rather than on a specific API surface, so
it holds across MV major versions. Worlds you create are imported into Multiverse
(/mv import), which means they show up in /mv list and inherit your MV
defaults instead of existing as something only VoxelBench knows about. Deletion
observes the actual server state after each step rather than assuming a fixed
sequence, so it behaves correctly whether MV 5.x's /mv remove already deleted
everything or MV 4.x merely un-registered the world. Multiverse-Core is declared
as a softdepend in plugin.yml, so Bukkit's loader guarantees it boots first.
At startup the plugin now prints a summary of the third-party plugins it has hooked into, so you can confirm an integration was picked up instead of inferring it from behaviour.
Disconnecting mid-benchmark — or restarting the server — used to leave you inside the benchmark world with no way back to where you were.
Your position and gamemode from before the test are now written to disk, in a
per-UUID YAML file under plugins/VoxelBench/data/player-states/. When you
reconnect inside a voxelbench_* world, a listener puts you back: at the exact
pre-test position when the snapshot is there, and otherwise at your bed spawn or
at the spawn of the first non-benchmark world. Because the snapshot lives on
disk rather than in memory, it survives a JVM crash, a server restart and a
plugin reload.
Players teleported into a fresh Multiverse-managed benchmark world landed in SURVIVAL and immediately fell to their death. The gamemode is now re-applied three times around the teleport — before it, immediately after it, and again on the next tick — which defeats per-world gamemode plugins that were silently overriding it on entry into a new world.
Gameplay benchmarks and gameplay tests can no longer be started from the console. They always misbehaved there, silently, because they need a real player in the world; the refusal is now explicit and translated.
Hardware tests stay scriptable from the console — memory, disk, multicore,
singlecore and network. The distinction is not a hard-coded list: it is keyed off
each test's own requiresPlayerPresence() metadata, so any test added later
inherits the right behaviour without anyone remembering to update a list.
Minecraft's year-based version numbers are now formatted correctly: 26.1.2 displays as 26.1.2 rather than 1.1.2, and version comparisons treat a year-based major as being past every 1.x check.
The published jar carries its version in the filename (VoxelBench-1.1.0.jar),
and the version inside plugin.yml is derived from the release tag, so the file
you downloaded and the version the server reports can no longer disagree.
Drop the new jar into plugins/ and restart. No config or database migration is
needed, and every behaviour change is opt-in behind /bench world set.
The Java requirement is unchanged: Java 16 or newer for Minecraft 1.17 through 1.21.x. Minecraft 26.1.x requires Java 25 LTS — that is Mojang's requirement, not ours.
VoxelBench 1.0.2 runs on Minecraft 26.1.x, and compatibility across the full
1.17 to 26.1.2 range is now checked every day by booting real Paper servers.
Nothing else moves: no new features, no behaviour changes, the plugin's runtime
surface is identical to 1.0.1. Drop the new jar in plugins/, restart, and you
are done.
Minecraft 26.1.x arrived in April 2026 with Mojang's new year-based versioning. VoxelBench is drop-in compatible with that line — 26.1, 26.1.1 and 26.1.2 — and the version is part of the compile-time CI matrix, built against the Java SE 25 LTS toolchain.
Nothing broke on the way there. There are no API breakages in 26.1.x, and the Bukkit surface VoxelBench relies on remains stable all the way back to 1.17, so supporting the new line cost nothing to the old one.
The entity compatibility layer gained six accessors for mobs and projectiles introduced after 1.20: armadillo (1.20.5+), breeze, bogged and wind charge (1.21+), creaking (1.21.3+) and happy ghast (1.21.5+). On a server that predates any of them, the accessor simply returns nothing — the existing supported range is unaffected.
VoxelBench is tested across the full 1.17 to 26.1.2 range, and that testing no
longer stops at compilation. A new workflow boots actual Paper servers with the
plugin installed — 1.17.1, 1.21 and 26.1.2 — every day at 4am UTC, on every push,
and on demand. Each one is given /bench info and /bench tests list over
stdin, and the logs are searched for the patterns that mean a plugin failed to
load: Caused by:, NoSuchMethodError, ClassNotFoundException.
Each version boots under the JVM its Paper bootstrap actually supports — Java 17 for 1.17.1, 21 for 1.21, 25 for 26.1.2 — because a toolchain mismatch is one of the ways this kind of check quietly stops proving anything. Server logs are kept as artifacts for seven days whether the run passed or failed, so a failure can be read after the fact rather than reproduced.
The practical consequence: a compatibility regression is caught here, before it is caught on your server.
The Adventure text and formatting library moves from 4.17.0 to 4.26.1, along with the MiniMessage and legacy serializer components that go with it.
Drop the new jar in plugins/ and restart. It is a drop-in replacement for
1.0.1: no configuration migration, no database change, nothing to adjust.
Java 16 or newer for Minecraft 1.17 through 1.21.x. Java 25 LTS is required if you run Minecraft 26.1.x.
The obfuscated jar's hash changes with this release. That is expected — the
Adventure bump shifts the bytecode — but it means the new hash has to be added to
VALID_PLUGIN_HASHES before reports from 1.0.2 will be accepted.
The build now requires JDK 21; CI uses Temurin 21.
The plugin's web dashboard was written in French: every label and message on its pages was a hardcoded French string. This release makes English the default, adds a language selector to every page, and moves the translations into files that can be extended without touching the pages.
The three dashboard pages — login, dashboard, and the API-only page — no longer
carry hardcoded French strings. Their text is resolved through DashboardI18n, a
translation loader backed by .properties files. English (en_US) is now the
default; French (fr_FR) is available as an option.
Each of the three pages carries a language selector. The choice is stored in the
voxelbench_locale cookie for one year (SameSite=Lax), so the dashboard opens
in the language you picked the next time you sign in. When no cookie is present,
the dashboard reads the browser's Accept-Language header, and falls back to
en_US when that yields nothing.
Translations live as .properties files under resources/dashboard/lang/.
Adding a language means dropping one more file into that directory; the page
code does not change.
The monitor's server logs are now written in English too, for consistency with an international audience.
This release rebuilds the stress-limit mode, adds four tests — among them a raw single-core measurement — and lets the plugin tell you when a newer version is available.
The stress limit raises the load tier after tier until the server gives way. Each tier used to multiply the previous one's load by 1.8, so the answer came back as a wide bracket: the last tier that held, and one nearly twice as heavy that did not. The multiplier is now 1.2, which makes the ladder far finer.
Once a tier breaks, VoxelBench no longer stops there. It bisects between the broken tier and the last one that held, up to two times, to close in on the point where the server actually gives way.
Tiers the machine absorbs without effort no longer cost a full measurement: a tier still at 19.5 TPS or above after five seconds is skipped. Those five seconds are now counted on the wall clock rather than in ticks, so a falling tick rate no longer stretches them.
Base values and maximum caps were raised for every stress test type.
Monitoring, the scoreboard and the metrics were aligned on the standard benchmark flow: monitoring is delegated to the shared test base class, and a single scoreboard covers the whole run. The scoreboard can also display average values, so what you read on screen matches the metric the run is actually measuring.
The run also gained the preparation a benchmark needs. The test zone is generated once and shared across tiers, the player's state is saved before the run and restored afterwards, and garbage collection is stabilised before measurement begins.
One test was measuring the wrong thing: the hopper test ran with its tick acceleration disabled in stress-limit mode. It no longer does.
Finally, every message the stress limit prints goes through the translation files, like the rest of the plugin.
Each tier now carries a hover tooltip with its full metrics — percentiles, standard deviation, garbage collection.
A stress-limit run can also be submitted to the backend, over protocol v1.1.0.
VoxelBench now checks the backend for a newer version, asynchronously, and notifies operators when they join the server. You no longer have to go and look.