Skip to content

Cache block state conversions in the WorldEdit and FAWE loggers - #1049

Open
twme-ai wants to merge 2 commits into
PlayPro:masterfrom
twme-ai:cache-block-state-conversions
Open

twme-ai wants to merge 2 commits into
PlayPro:masterfrom
twme-ai:cache-block-state-conversions

Conversation

@twme-ai

@twme-ai twme-ai commented Nov 3, 2026

Copy link
Copy Markdown

Summary

For every block a WorldEdit or FAWE edit changes, CoreProtect converts the old and new block state to Bukkit Material and BlockData and serializes both with getAsString(). These results depend only on the block state, and an edit usually repeats a small number of states many times. This change converts each state once and reuses the result. In a benchmark of 262,144-block edits, it cuts the time CoreProtect adds to an edit by about 45% on both paths:

Path CoreProtect overhead per edit, master This branch
WorldEdit 7.4.5 ~1,120 ms ~630 ms
FAWE 2.15.4 ~460 ms ~255 ms

The problem

For each changed block, the loggers currently:

  • convert the old block type to Material;
  • convert the old and new state to BlockData with BukkitAdapter.adapt, which builds a new instance each time. In WorldEdit 7.4.5, this serializes the state to a string and parses it with Bukkit.createBlockData, without caching;
  • serialize both BlockData with getAsString(), which builds a new string each time.

WorldEditLogger.getOtherHalf does the same conversion for the neighbouring block of every bisected block. A //set stone over 262,144 blocks only involves two states (air and stone), but does all of this 262,144 times.

The fix

  • New package-private class BlockStateConversion holds the Material, a template BlockData and the data string of one WorldEdit block state, all in final fields.
    • BlockData is mutable, and postProcess can modify it (BlockUtils.checkWaterlogged), so callers get createBlockData(), a clone of the template. The template itself is only read, by isOtherHalf.
  • WorldEdit path (CoreProtectLogger, also used by FAWE with fast-placement: false) and getOtherHalf: BlockStateConversion.of(state) looks the state up in a ConcurrentHashMap. WorldEdit keeps a single instance per block state and caches its hash code, so lookups match by identity.
  • FAWE queue path (FastAsyncWorldEditLogger): the conversions are indexed by FAWE block state ordinal in an array sized from BlockTypesCache.states.length, which avoids hashing for every changed block. FAWE runs this processor on several threads. A plain array is safe because every field of BlockStateConversion is final, and two threads filling the same slot store equivalent conversions.
  • WorldEditLogger.postProcess now takes the converted Material, BlockData and data strings. The two overloads that converted them are no longer used and are removed.
  • The unused extent field and BlockVector3 import are removed from FastAsyncWorldEditLogger.

Behaviour change

None. On every test scenario below, the co_block rows written by this branch are identical to master's.

Scope

  • Database writes are unchanged: the same rows are queued, only the conversion before queueing is faster.
  • The benchmark uses two block states, the best case for the cache. An edit with hundreds of states (for example a schematic paste) converts each state once and looks it up after that, so I expect a smaller gain there, but I have not measured it.

Risk

  • The caches assume the block state registry does not change while the server runs. The FAWE array is sized when the class loads, after FAWE has loaded its registry. The map is bounded by the number of block states.
  • Memory: one array slot per block state for the FAWE path and one map entry per state seen on the WorldEdit path. Only states that appear in an edit are converted; each conversion is a BlockData and a short string.
  • A BlockData copy is still made for each use, because postProcess may modify it.

Testing

Build: mvn package -Dproject.branch=development passes.

Environment: Paper 26.2 build 135, DuckDB (the default backend), a 3-core VM. The edits are run by a small test plugin that builds an EditSession with the console as actor. The plugin is not part of this PR.

Row parity, in four modes: WorldEdit 7.4.5, FAWE 2.15.4 on the main thread, FAWE from an async task (parallel queue), and FAWE with fast-placement: false. Each run, on master and on this branch:

  • sets a 16×6×16 region to air, then stone, writes a glass checkerboard, replaces stone with oak planks (BlockTypeMask);
  • runs /co rollback and /co restore for those edits;
  • places 8 oak doors (both halves), removes only their top halves, places them again and clears the region.

Then all co_block rows (user, world, position, type, data, meta, blockdata, action, rolled_back) were dumped, sorted and compared:

Mode Rows, master Rows, this branch Identical
WorldEdit 6,194 6,194 yes
FAWE, main thread 6,184 6,184 yes
FAWE, async 6,184 6,184 yes
FAWE, fast-placement: false 6,184 6,184 yes

Block counts after each step, rollback and restore were also identical, with no errors in the console.

Performance: setBlocks on a 64×64×64 region, alternating stone and air (262,144 changed blocks per edit). Per setup: 2 server runs × (1 warm-up edit + 10 measured edits), 20 measured edits in total. All setups were run alternately in the same session. The time is measured around the EditSession and includes the edit itself.

Setup Median Mean Range Rows logged
WorldEdit without CoreProtect 997 ms 1,205 ms 746 to 3,131 ms none
WorldEdit + master (dffa04f) 2,119 ms 2,236 ms 1,667 to 3,262 ms 2,883,584
WorldEdit + this branch 1,628 ms 1,755 ms 1,142 to 2,641 ms 2,883,584
FAWE without CoreProtect 34 ms 43 ms 23 to 126 ms none
FAWE + master (dffa04f) 493 ms 529 ms 356 to 796 ms 2,883,584
FAWE + this branch 289 ms 303 ms 154 to 587 ms 2,883,584

The overhead figures in the summary are the medians minus the median without CoreProtect. In every run the consumer queue was empty by the end. I have not profiled where the remaining time goes. Timings on this VM vary between sessions, so the table above, where all setups ran side by side, is the one to compare.

twme-ai and others added 2 commits November 2, 2026 09:59
For every changed block, FastAsyncWorldEditLogger converted the old
and new block state to Bukkit Material and BlockData and serialized
both with getAsString(). These results depend only on the block state,
and an edit usually repeats a few states many times.

BlockStateConversion holds the Material, a BlockData template and the
data string of one block state, all in final fields. The processor
indexes conversions by FAWE ordinal in a plain array, which is safe
across FAWE's threads because the fields are final; two threads filling
the same slot store equivalent conversions. BlockData is mutable and
postProcess may modify it (checkWaterlogged), so callers get a clone.

WorldEditLogger gains a postProcess overload that takes the converted
values; the existing overloads delegate to it unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CoreProtectLogger, used with WorldEdit and with FAWE when
fast-placement is off, did the same per-block conversions, and
getOtherHalf converted the neighbour of every bisected block.
WorldEdit's BukkitAdapter builds BlockData by serializing the state and
parsing the string, without caching.

Both now use BlockStateConversion, looked up in a ConcurrentHashMap
keyed by the WorldEdit block state. WorldEdit keeps a single instance
per state with a cached hash code, so lookups match by identity.

The postProcess overloads that converted the data are now unused and
removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-11-03T08:39:47.190396Z
We couldn’t complete this review. Comment @codex review to try again.
fa9d905 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant