perf(light): keep the opacity tables of a chunk with its light - #34
Conversation
Measured through the workflow
µs/op, lower is better. The control is what makes this readable at all. The two runs landed on different hardware — an The portable form is the ratio measured inside one job, which is what the workflow's own
What this does and does not establishIt establishes that a steady incremental pass got substantially cheaper, and by more than the machine The two |
ChunkLightArea#read built the opacity tables of every chunk of the area and of its ring on every pass, whether or not anything in that chunk had moved. A table describes the blocks of a section and nothing else, so it stays right for exactly as long as those blocks do — the same condition the kept light already rests on, reported through the same two methods. The tables therefore move into the same bounded entry, tied to a version that recordChange and forget move on. Two things fall out that the class already said in prose without acting on. The neighbours of an edit are marked because their light changed while their own blocks did not, so their tables survive the pass. And a table is not a kind of light, so the sky pass reuses what the block pass built instead of reading every chunk a second time. The version is read before the block states and stored with them, so a change landing between the two costs a rebuild rather than leaving a stale table behind. The tables are shared as an immutable list: safely published, and a write through it fails loudly. tableBuilds() reports whether the reuse happens. It is a count, so it is exact and the same on every machine, which separates "does it reuse" from "is the reuse worth anything". One existing test asserted that the sky pass costs a second read of the block states. It no longer does; it costs a second propagation, which is what that test now counts.
Test results 198 files 198 suites 3m 56s ⏱️ Results for commit 127819f. ♻️ This comment has been updated with latest results. |
6c2d5af to
127819f
Compare
Closes the first entry of
Openon Project Status — the opacity tables are rebuilt on every pass.ChunkLightArea#readcalledopacityOffor every chunk of the area and of its ring, on everypass, whether or not anything in that chunk had moved. A table describes the blocks of a section and
nothing else, so it stays right for exactly as long as those blocks do. That is the same condition
the kept light already rests on, reported through the same two methods, so the tables move into the
same bounded entry, tied to a version that
recordChangeandforgetmove on.Two things fall out that the class already stated in prose without acting on:
ChunkLightScheduler#markChangedmarks the eight neighbours of an edit and says why nothing oftheirs is dropped — "Their own blocks did not move". Their tables now survive the pass too.
group; the second one used to read every chunk again.
What it does, exactly
Counted, not timed — the same 3×3 group the scheduler forms around one edit, so 9 chunks plus a ring
of 16:
A steady tick over that group built 50 table sets and now builds 1. These are counts: exact,
identical on every machine, and unaffected by load or JIT.
tableBuilds()is public so a server cancheck the same thing about its own world, next to the
fullPropagations()that already exists.What this does not change is
compute(…), the recalculating path. It is documented as keepingnothing, and a caller with no revisions to offer has reported no changes either — so it has no basis
to reuse anything.
AreaPassStageBenchmark, which measures that path, is therefore expected to beunmoved by this commit.
Correctness
The risk here is the one the wiki entry names: a table that outlives the blocks it describes produces
light written with the update flag of the section cleared, and nothing ever corrects it.
IncrementalLightUpdateTestalready compares the incremental path against a full recalculation,position by position, for both kinds of light, across 48 random changes that remove as often as they
place. Deliberately dropping the invalidation turns four of its six tests red — so that harness
covers this change, and it was verified to do so rather than assumed to.
OpacityTableReuseTest(new, 7 tests) pins the counts above. Deliberately disabling the reuse turnsfive of the seven red; the two that stay green are the two that assert reuse must not happen.
The version is read before the block states and stored with them, so a change landing between the two
costs a rebuild rather than leaving a stale table behind. The failure direction of that race is
wasted work, never a wrong table. Tables are shared as an immutable
List: safely published, and awrite through one fails loudly instead of corrupting the next pass.
The cost
Memory, on top of the light, under the same bound of chunks but not the same size per chunk: a
uniform section costs nothing at all because
SectionOpacityholds no arrays for one, and a fullymixed one costs 8 KB. The figure is a property of the world rather than a constant, and the class
javadoc says so rather than quoting a number it cannot back.
Measurement
IncrementalVsFullBenchmarkis already an A/B inside one jar —incremental()againstfull()—and this commit moves only the first of the two.
full()is therefore an internal control in thesame job, which is what makes a run on a shared runner worth anything. Both arms are being measured
through the
Benchmarkworkflow (#29/#31),customprofile, three forks, once onmainand once onthis branch. Numbers follow in a comment.
The counts above stand on their own regardless of what the timings say.