Skip to content

Skip matchMaterial's regex for enum names and cache blockdata encodings - #1022

Merged
Intelli merged 1 commit into
PlayPro:masterfrom
tricrotism:for-upstream/material-blockdata-fastpath
Sep 25, 2026
Merged

Intelli merged 1 commit into
PlayPro:masterfrom
tricrotism:for-upstream/material-blockdata-fastpath

Conversation

@tricrotism

Copy link
Copy Markdown
Contributor

Summary

Two conversions run for nearly every block row on the consumer thread and redo the same work each time: a regex-based material lookup and a blockdata string to id-bytes encoding. This change skips the regex when it cannot change the result, and caches the encoding. It cuts about 1 µs of consumer CPU per row on both SQLite and DuckDB.

The problem

  • MaterialUtils.getType(String) (utility/MaterialUtils.java:165-179) calls Material.matchMaterial. Before looking the name up, that runs two regex replaceAll passes: whitespace to underscores, then non-word characters removed. The block loggers call it per row with keys like STONE or OAK_PLANKS, for which both passes change nothing.
  • BlockUtils.stringToByteData (utility/BlockUtils.java:33-75) turns a blockdata string such as minecraft:oak_stairs[facing=east,half=bottom] into the comma-separated blockdata ids CoreProtect stores. On every row it builds a default BlockData for the type and serializes it with getAsString() for comparison, splits the property list, looks up each property's id, joins them and encodes the result. A server logs the same few hundred states over and over, and the answer only changes when the id maps change.

The fix

  • getType(String) calls Material.getMaterial(name) directly when the name contains only A-Z, 0-9 and _. For such names matchMaterial returns exactly getMaterial(name). Anything else still goes through matchMaterial.
  • stringToByteData keeps a ConcurrentHashMap from blockdata string to its type and result, capped at 4,096 entries and cleared when full.
    • The cache belongs to the current material and blockdata id maps. Loading those maps assigns new map instances, and a changed instance starts a new cache.
    • The cache is off on ClickHouse, which can uncache ids when a batch is discarded.
    • A result that had an unresolved property id is not cached, so a later call can still resolve it.
    • An entry only answers for the same type it was built for.

Behaviour change

None. The bytes produced for any input are the same as before.

Risk

  • The cache reuses results across rows, so a stale cache would write wrong ids. It is keyed to the id map instances, which every reload replaces, and it skips ClickHouse, the one backend that uncaches ids during normal operation.
  • 4,096 entries of short strings and byte arrays is well under a megabyte.

Testing

Build: mvn package passes.

Performance, a consumer benchmark plugin (ConsumerLoad) on Paper 26.2, 2 GB heap. Each round queues 200,000 rows through the API (50% logPlacement, 30% logRemoval, 20% logInteraction, 10 users) on the main thread, then waits for the consumer to drain. Consumer CPU is the consumer thread's CPU time over the round. One warmup round, then 3 measured rounds, 2 runs of each jar.

Backend Metric Upstream 3af1079 This branch
SQLite consumer CPU per row 8.5 to 10.6 µs 7.0 to 9.8 µs
SQLite drain time per 200k rows 3.8 to 4.4 s 3.6 to 4.2 s
DuckDB consumer CPU per row 3.4 to 4.6 µs 2.4 to 3.2 µs
DuckDB drain time per 200k rows 2.9 to 3.4 s 3.1 to 3.3 s

The spread within each cell is mostly the database growing across the three rounds of one run. DuckDB's drain time does not follow its CPU, because the consumer is not CPU-bound there.

Row parity: a 47-step scenario plugin (blocks, containers, pistons, fluids, dispensers, bone meal, explosions, then rollback and restore) compared order-insensitively against upstream, with each row's stored blockdata bytes compared by hash. Paper 26.2 and Folia 1.21.11 with SQLite matched upstream row for row. Paper 26.2 with DuckDB differed by one far-basin water row whose settling window lands either side of the boundary depending on run pacing. No new errors.

The consumer ran Material.matchMaterial's two regex passes on names they cannot change, and re-encoded the same blockdata strings into ids on every row. Plain enum names now go straight to getMaterial, and stringToByteData caches its results per string, bounded, reset when the id maps reload and skipped on ClickHouse.
@netlify

netlify Bot commented Sep 23, 2026

Copy link
Copy Markdown

❌ Deploy Preview for coreprotect failed. Why did it fail? →

Name Link
🔨 Latest commit 1f349a2
🔍 Latest deploy log https://app.netlify.com/projects/coreprotect/deploys/6ab3f02f937744000752ee6a

@Intelli
Intelli merged commit 72d5655 into PlayPro:master Sep 25, 2026
2 of 6 checks passed
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.

2 participants