diff --git a/leaf-server/minecraft-patches/features/0127-ensureCapacity-with-collectTickingChunks.patch b/leaf-server/minecraft-patches/features/0127-ensureCapacity-with-collectTickingChunks.patch new file mode 100644 index 00000000..2a56b1c6 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0127-ensureCapacity-with-collectTickingChunks.patch @@ -0,0 +1,33 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Sun, 16 Feb 2025 01:13:04 +0100 +Subject: [PATCH] ensureCapacity with collectTickingChunks + + +diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java +index dd956431bb882daa70267685f2283d0c358336be..ca12b33b1297afcb211bf9a4e423ee11c1ec57e9 100644 +--- a/net/minecraft/server/level/ServerChunkCache.java ++++ b/net/minecraft/server/level/ServerChunkCache.java +@@ -573,17 +573,13 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked(); + final int size = tickingChunks.size(); + +- final ChunkMap chunkMap = this.chunkMap; ++ // Directly add all pre-filtered ticking chunks to output ++ if (output instanceof ArrayList arrayList) { ++ arrayList.ensureCapacity(size); ++ } + + for (int i = 0; i < size; ++i) { +- final ServerChunkCache.ChunkAndHolder chunkAndHolder = raw[i]; +- final LevelChunk levelChunk = chunkAndHolder.chunk(); +- +- if (!this.isChunkNearPlayer(chunkMap, levelChunk.getPos(), levelChunk)) { +- continue; +- } +- +- output.add(levelChunk); ++ output.add(raw[i].chunk()); + } + // Paper end - chunk tick iteration optimisation + } diff --git a/leaf-server/minecraft-patches/features/0128-Bulk-writes-to-writeLongArray-during-chunk-loading.patch b/leaf-server/minecraft-patches/features/0128-Bulk-writes-to-writeLongArray-during-chunk-loading.patch new file mode 100644 index 00000000..d2fdc347 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0128-Bulk-writes-to-writeLongArray-during-chunk-loading.patch @@ -0,0 +1,43 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Fri, 21 Feb 2025 15:06:55 +0100 +Subject: [PATCH] Bulk writes to writeLongArray during chunk loading + + +diff --git a/net/minecraft/network/FriendlyByteBuf.java b/net/minecraft/network/FriendlyByteBuf.java +index abb0141426fd716e79a947b9498a8351daa342fc..838a7d64e717759f506e64c74eb76eea05f72811 100644 +--- a/net/minecraft/network/FriendlyByteBuf.java ++++ b/net/minecraft/network/FriendlyByteBuf.java +@@ -341,9 +341,30 @@ public class FriendlyByteBuf extends ByteBuf { + + public FriendlyByteBuf writeLongArray(long[] array) { + this.writeVarInt(array.length); ++ if (array.length == 0) { ++ return this; ++ } ++ ++ this.source.ensureWritable(array.length * Long.BYTES); ++ int writerIndex = this.source.writerIndex(); ++ ++ if (this.source.hasArray()) { ++ byte[] dest = this.source.array(); ++ int offset = this.source.arrayOffset() + writerIndex; + +- for (long l : array) { +- this.writeLong(l); ++ ByteBuffer buf = ByteBuffer.wrap(dest, offset, array.length * Long.BYTES).order(this.source.order()); ++ buf.asLongBuffer().put(array); ++ ++ this.source.writerIndex(writerIndex + array.length * Long.BYTES); ++ } else if (this.source.nioBufferCount() > 0) { ++ ByteBuffer nioBuf = this.source.nioBuffer(writerIndex, array.length * Long.BYTES); ++ nioBuf.asLongBuffer().put(array); ++ this.source.writerIndex(writerIndex + array.length * Long.BYTES); ++ } else { ++ ByteBuffer temp = ByteBuffer.allocate(array.length * Long.BYTES).order(this.source.order()); ++ temp.asLongBuffer().put(array); ++ temp.rewind(); ++ this.source.writeBytes(temp); + } + + return this; diff --git a/leaf-server/minecraft-patches/features/0129-Use-BFS-on-getSlopeDistance-in-fluids.patch b/leaf-server/minecraft-patches/features/0129-Use-BFS-on-getSlopeDistance-in-fluids.patch new file mode 100644 index 00000000..65d9000f --- /dev/null +++ b/leaf-server/minecraft-patches/features/0129-Use-BFS-on-getSlopeDistance-in-fluids.patch @@ -0,0 +1,145 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Sun, 16 Feb 2025 15:15:16 +0100 +Subject: [PATCH] Use BFS on getSlopeDistance in fluids + + +diff --git a/net/minecraft/world/level/material/FlowingFluid.java b/net/minecraft/world/level/material/FlowingFluid.java +index 4c2c2efd5380ff1fa5ad7553b51babae20f516ae..bd556e02f4e0822e2a62f53ceca7ad61bbfb736b 100644 +--- a/net/minecraft/world/level/material/FlowingFluid.java ++++ b/net/minecraft/world/level/material/FlowingFluid.java +@@ -2,12 +2,16 @@ package net.minecraft.world.level.material; + + import com.google.common.collect.Maps; + import it.unimi.dsi.fastutil.objects.Object2ByteLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + import it.unimi.dsi.fastutil.shorts.Short2BooleanMap; + import it.unimi.dsi.fastutil.shorts.Short2BooleanOpenHashMap; + import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; + import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; + import java.util.Map; + import java.util.Map.Entry; ++import java.util.Queue; ++import java.util.Set; ++ + import net.minecraft.core.BlockPos; + import net.minecraft.core.Direction; + import net.minecraft.server.level.ServerLevel; +@@ -342,30 +346,65 @@ public abstract class FlowingFluid extends Fluid { + protected abstract void beforeDestroyingBlock(LevelAccessor level, BlockPos pos, BlockState state); + + protected int getSlopeDistance(LevelReader level, BlockPos pos, int depth, Direction direction, BlockState state, FlowingFluid.SpreadContext spreadContext) { +- int i = 1000; ++ int maxDepth = this.getSlopeFindDistance(level); ++ int initialCapacity = (int) Math.pow(4, maxDepth); // Pre-size based on max possible nodes ++ Queue queue = new java.util.ArrayDeque<>(initialCapacity); ++ it.unimi.dsi.fastutil.longs.LongSet visited = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(); ++ ++ queue.add(new QueueEntry(pos, depth, direction)); ++ visited.add(encodeVisited(pos, direction)); ++ ++ while (!queue.isEmpty()) { ++ QueueEntry current = queue.poll(); ++ BlockPos currentPos = current.pos; ++ int currentDepth = current.depth; ++ Direction excludeDir = current.excludeDir; ++ ++ for (Direction dir : Direction.Plane.HORIZONTAL) { ++ if (dir == excludeDir) continue; ++ ++ BlockPos nextPos = currentPos.relative(dir); ++ Direction nextExcludeDir = dir.getOpposite(); ++ long nextKey = encodeVisited(nextPos, nextExcludeDir); ++ ++ if (!visited.add(nextKey)) continue; ++ ++ BlockState blockState = spreadContext.getBlockStateIfLoaded(nextPos); ++ if (blockState == null) continue; + +- for (Direction direction1 : Direction.Plane.HORIZONTAL) { +- if (direction1 != direction) { +- BlockPos blockPos = pos.relative(direction1); +- BlockState blockState = spreadContext.getBlockStateIfLoaded(blockPos); // Paper - Prevent chunk loading from fluid flowing +- if (blockState == null) continue; // Paper - Prevent chunk loading from fluid flowing + FluidState fluidState = blockState.getFluidState(); +- if (this.canPassThrough(level, this.getFlowing(), pos, state, direction1, blockPos, blockState, fluidState)) { +- if (spreadContext.isHole(blockPos)) { +- return depth; ++ if (this.canPassThrough(level, this.getFlowing(), currentPos, state, dir, nextPos, blockState, fluidState)) { ++ if (spreadContext.isHole(nextPos)) { ++ return currentDepth; + } + +- if (depth < this.getSlopeFindDistance(level)) { +- int slopeDistance = this.getSlopeDistance(level, blockPos, depth + 1, direction1.getOpposite(), blockState, spreadContext); +- if (slopeDistance < i) { +- i = slopeDistance; +- } ++ if (currentDepth + 1 <= maxDepth) { ++ queue.add(new QueueEntry(nextPos, currentDepth + 1, nextExcludeDir)); + } + } + } + } + +- return i; ++ return 1000; ++ } ++ ++ // Encode BlockPos and Direction into a long (x: 26 bits, z: 26 bits, dir: 4 bits) ++ private static long encodeVisited(BlockPos pos, Direction dir) { ++ return ((long) (pos.getX() & 0x3FFFFFF) << 38) ++ | ((long) (pos.getZ() & 0x3FFFFFF) << 12) ++ | (dir.ordinal() & 0xF); ++ } ++ ++ private static class QueueEntry { ++ final BlockPos pos; ++ final int depth; ++ final Direction excludeDir; ++ ++ QueueEntry(BlockPos pos, int depth, Direction excludeDir) { ++ this.pos = pos.immutable(); ++ this.depth = depth; ++ this.excludeDir = excludeDir; ++ } + } + + boolean isWaterHole(BlockGetter level, BlockPos pos, BlockState state, BlockPos belowPos, BlockState belowState) { +@@ -612,12 +651,30 @@ public abstract class FlowingFluid extends Fluid { + } + + public boolean isHole(BlockPos pos) { +- return this.holeCache.computeIfAbsent(this.getCacheKey(pos), s -> { +- BlockState blockState = this.getBlockState(pos, s); +- BlockPos blockPos = pos.below(); +- BlockState blockState1 = this.level.getBlockState(blockPos); +- return FlowingFluid.this.isWaterHole(this.level, pos, blockState, blockPos, blockState1); +- }); ++ short key = this.getCacheKey(pos); ++ // Fast path - check if we already have the result ++ if (this.holeCache.containsKey(key)) { ++ return this.holeCache.get(key); ++ } ++ // Get cached block state for current position ++ BlockState blockState = this.stateCache.get(key); ++ if (blockState == null) { ++ blockState = this.level.getBlockState(pos); ++ this.stateCache.put(key, blockState); ++ } ++ // Get position below and its key ++ BlockPos belowPos = pos.below(); ++ short belowKey = this.getCacheKey(belowPos); ++ // Get cached block state for position below ++ BlockState belowState = this.stateCache.get(belowKey); ++ if (belowState == null) { ++ belowState = this.level.getBlockState(belowPos); ++ this.stateCache.put(belowKey, belowState); ++ } ++ // Compute result and cache it ++ boolean result = FlowingFluid.this.isWaterHole(this.level, pos, blockState, belowPos, belowState); ++ this.holeCache.put(key, result); ++ return result; + } + + private short getCacheKey(BlockPos pos) { diff --git a/leaf-server/minecraft-patches/features/0130-Optimize-AABB.patch b/leaf-server/minecraft-patches/features/0130-Optimize-AABB.patch new file mode 100644 index 00000000..91f81f61 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0130-Optimize-AABB.patch @@ -0,0 +1,69 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Sun, 16 Feb 2025 19:03:23 +0100 +Subject: [PATCH] Optimize AABB + + +diff --git a/net/minecraft/world/phys/AABB.java b/net/minecraft/world/phys/AABB.java +index f64c04b32dd2d0fe143fc8bf9f498e52beb66a58..3a41291aa9c0d728b54cf962360303750725bc82 100644 +--- a/net/minecraft/world/phys/AABB.java ++++ b/net/minecraft/world/phys/AABB.java +@@ -220,13 +220,14 @@ public class AABB { + } + + public AABB intersect(AABB other) { +- double max = Math.max(this.minX, other.minX); +- double max1 = Math.max(this.minY, other.minY); +- double max2 = Math.max(this.minZ, other.minZ); +- double min = Math.min(this.maxX, other.maxX); +- double min1 = Math.min(this.maxY, other.maxY); +- double min2 = Math.min(this.maxZ, other.maxZ); +- return new AABB(max, max1, max2, min, min1, min2); ++ return new AABB( ++ this.minX > other.minX ? this.minX : other.minX, ++ this.minY > other.minY ? this.minY : other.minY, ++ this.minZ > other.minZ ? this.minZ : other.minZ, ++ this.maxX < other.maxX ? this.maxX : other.maxX, ++ this.maxY < other.maxY ? this.maxY : other.maxY, ++ this.maxZ < other.maxZ ? this.maxZ : other.maxZ ++ ); + } + + public AABB minmax(AABB other) { +@@ -258,16 +259,33 @@ public class AABB { + } + + public boolean intersects(AABB other) { +- return this.intersects(other.minX, other.minY, other.minZ, other.maxX, other.maxY, other.maxZ); ++ // Removed redundant method call overhead ++ return this.minX < other.maxX && ++ this.maxX > other.minX && ++ this.minY < other.maxY && ++ this.maxY > other.minY && ++ this.minZ < other.maxZ && ++ this.maxZ > other.minZ; + } + + public boolean intersects(double x1, double y1, double z1, double x2, double y2, double z2) { +- return this.minX < x2 && this.maxX > x1 && this.minY < y2 && this.maxY > y1 && this.minZ < z2 && this.maxZ > z1; ++ // No temporary variables needed, direct comparison ++ return this.minX < x2 && ++ this.maxX > x1 && ++ this.minY < y2 && ++ this.maxY > y1 && ++ this.minZ < z2 && ++ this.maxZ > z1; + } + + public boolean intersects(Vec3 min, Vec3 max) { + return this.intersects( +- Math.min(min.x, max.x), Math.min(min.y, max.y), Math.min(min.z, max.z), Math.max(min.x, max.x), Math.max(min.y, max.y), Math.max(min.z, max.z) ++ min.x < max.x ? min.x : max.x, ++ min.y < max.y ? min.y : max.y, ++ min.z < max.z ? min.z : max.z, ++ min.x > max.x ? min.x : max.x, ++ min.y > max.y ? min.y : max.y, ++ min.z > max.z ? min.z : max.z + ); + } + diff --git a/leaf-server/minecraft-patches/features/0131-Improve-sorting-in-SortedArraySet.patch b/leaf-server/minecraft-patches/features/0131-Improve-sorting-in-SortedArraySet.patch new file mode 100644 index 00000000..7099f08a --- /dev/null +++ b/leaf-server/minecraft-patches/features/0131-Improve-sorting-in-SortedArraySet.patch @@ -0,0 +1,84 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Thu, 20 Feb 2025 15:37:39 +0100 +Subject: [PATCH] Improve sorting in SortedArraySet + + +diff --git a/net/minecraft/util/SortedArraySet.java b/net/minecraft/util/SortedArraySet.java +index 339b19e88567be382e550ed54477fabd58d51faa..bde5b4cb4cda003acd7343b16f09f915b71fe3f2 100644 +--- a/net/minecraft/util/SortedArraySet.java ++++ b/net/minecraft/util/SortedArraySet.java +@@ -11,6 +11,7 @@ import javax.annotation.Nullable; + public class SortedArraySet extends AbstractSet implements ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet { // Paper - rewrite chunk system + private static final int DEFAULT_INITIAL_CAPACITY = 10; + private final Comparator comparator; ++ private final boolean isNaturalOrder; + T[] contents; + int size; + +@@ -93,10 +94,11 @@ public class SortedArraySet extends AbstractSet implements ca.spottedleaf. + + private SortedArraySet(int initialCapacity, Comparator comparator) { + this.comparator = comparator; ++ this.isNaturalOrder = comparator == Comparator.naturalOrder(); + if (initialCapacity < 0) { + throw new IllegalArgumentException("Initial capacity (" + initialCapacity + ") is negative"); + } else { +- this.contents = (T[])castRawArray(new Object[initialCapacity]); ++ this.contents = (T[]) castRawArray(new Object[initialCapacity]); + } + } + +@@ -121,7 +123,51 @@ public class SortedArraySet extends AbstractSet implements ca.spottedleaf. + } + + private int findIndex(T object) { +- return Arrays.binarySearch(this.contents, 0, this.size, object, this.comparator); ++ return isNaturalOrder ? naturalBinarySearch(object) : customBinarySearch(object); ++ } ++ ++ private int naturalBinarySearch(T object) { ++ int low = 0; ++ int high = this.size - 1; ++ Comparable key = (Comparable) object; ++ T[] a = this.contents; ++ ++ while (low <= high) { ++ int mid = (low + high) >>> 1; ++ T midVal = a[mid]; ++ int cmp = key.compareTo(midVal); ++ ++ if (cmp < 0) { ++ high = mid - 1; ++ } else if (cmp > 0) { ++ low = mid + 1; ++ } else { ++ return mid; ++ } ++ } ++ return -(low + 1); ++ } ++ ++ private int customBinarySearch(T object) { ++ int low = 0; ++ int high = this.size - 1; ++ T[] a = this.contents; ++ Comparator c = this.comparator; ++ ++ while (low <= high) { ++ int mid = (low + high) >>> 1; ++ T midVal = a[mid]; ++ int cmp = c.compare(midVal, object); ++ ++ if (cmp < 0) { ++ low = mid + 1; ++ } else if (cmp > 0) { ++ high = mid - 1; ++ } else { ++ return mid; ++ } ++ } ++ return -(low + 1); + } + + private static int getInsertionPosition(int index) { diff --git a/leaf-server/minecraft-patches/features/0132-Make-removeIf-slightly-faster.patch b/leaf-server/minecraft-patches/features/0132-Make-removeIf-slightly-faster.patch new file mode 100644 index 00000000..3094fbf0 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0132-Make-removeIf-slightly-faster.patch @@ -0,0 +1,55 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Sun, 23 Feb 2025 00:37:39 +0100 +Subject: [PATCH] Make removeIf slightly faster + + +diff --git a/net/minecraft/util/SortedArraySet.java b/net/minecraft/util/SortedArraySet.java +index bde5b4cb4cda003acd7343b16f09f915b71fe3f2..7af029d8c2677ef00186acb5a3794b0ab3267ebd 100644 +--- a/net/minecraft/util/SortedArraySet.java ++++ b/net/minecraft/util/SortedArraySet.java +@@ -18,35 +18,22 @@ public class SortedArraySet extends AbstractSet implements ca.spottedleaf. + // Paper start - rewrite chunk system + @Override + public final boolean removeIf(final java.util.function.Predicate filter) { +- // prev. impl used an iterator, which could be n^2 and creates garbage + int i = 0; + final int len = this.size; + final T[] backingArray = this.contents; + +- for (;;) { +- if (i >= len) { +- return false; +- } +- if (!filter.test(backingArray[i])) { +- ++i; +- continue; +- } +- break; +- } ++ // Find first element to remove ++ while (i < len && !filter.test(backingArray[i])) i++; ++ if (i == len) return false; + +- // we only want to write back to backingArray if we really need to +- +- int lastIndex = i; // this is where new elements are shifted to +- +- for (; i < len; ++i) { +- final T curr = backingArray[i]; +- if (!filter.test(curr)) { // if test throws we're screwed +- backingArray[lastIndex++] = curr; +- } ++ // Shift elements in-place ++ int lastIndex = i; ++ for (i++; i < len; i++) { ++ T curr = backingArray[i]; ++ if (!filter.test(curr)) backingArray[lastIndex++] = curr; + } + +- // cleanup end +- Arrays.fill(backingArray, lastIndex, len, null); ++ // Only update size - skip Arrays.fill (safe in ChunkHolderManager's context) + this.size = lastIndex; + return true; + } diff --git a/leaf-server/minecraft-patches/features/0133-Optimize-LinearPalette.patch b/leaf-server/minecraft-patches/features/0133-Optimize-LinearPalette.patch new file mode 100644 index 00000000..6424abc9 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0133-Optimize-LinearPalette.patch @@ -0,0 +1,169 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Fri, 21 Feb 2025 18:05:09 +0100 +Subject: [PATCH] Optimize LinearPalette + + +diff --git a/net/minecraft/world/level/chunk/LinearPalette.java b/net/minecraft/world/level/chunk/LinearPalette.java +index 2073f6ff41aa570102621d183ee890b076267d54..459b6adca18868354374d00f3da906395fb474ab 100644 +--- a/net/minecraft/world/level/chunk/LinearPalette.java ++++ b/net/minecraft/world/level/chunk/LinearPalette.java +@@ -1,5 +1,6 @@ + package net.minecraft.world.level.chunk; + ++import java.util.Arrays; + import java.util.List; + import java.util.function.Predicate; + import net.minecraft.core.IdMap; +@@ -10,6 +11,8 @@ import org.apache.commons.lang3.Validate; + public class LinearPalette implements Palette, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette { // Paper - optimise palette reads + private final IdMap registry; + private final T[] values; ++ private final int[] byteSizes; ++ private final int[] idCache; // Cached registry IDs for values + private final PaletteResize resizeHandler; + private final int bits; + private int size; +@@ -23,24 +26,34 @@ public class LinearPalette implements Palette, ca.spottedleaf.moonrise.pat + + private LinearPalette(IdMap registry, int bits, PaletteResize resizeHandler, List values) { + this.registry = registry; +- this.values = (T[])(new Object[1 << bits]); ++ this.values = (T[]) (new Object[1 << bits]); ++ this.idCache = new int[1 << bits]; ++ this.byteSizes = new int[1 << bits]; // Initialize byteSizes + this.bits = bits; + this.resizeHandler = resizeHandler; + Validate.isTrue(values.size() <= this.values.length, "Can't initialize LinearPalette of size %d with %d entries", this.values.length, values.size()); + + for (int i = 0; i < values.size(); i++) { +- this.values[i] = values.get(i); ++ T value = values.get(i); ++ this.values[i] = value; ++ int id = registry.getId(value); ++ this.idCache[i] = id; ++ this.byteSizes[i] = VarInt.getByteSize(id); // Precompute byte size + } +- + this.size = values.size(); + } + +- private LinearPalette(IdMap registry, T[] values, PaletteResize resizeHandler, int bits, int size) { ++ private LinearPalette(IdMap registry, T[] values, int[] idCache, PaletteResize resizeHandler, int bits, int size) { + this.registry = registry; + this.values = values; ++ this.idCache = idCache; + this.resizeHandler = resizeHandler; + this.bits = bits; + this.size = size; ++ this.byteSizes = new int[idCache.length]; ++ for (int i = 0; i < idCache.length; i++) { ++ this.byteSizes[i] = VarInt.getByteSize(idCache[i]); ++ } + } + + public static Palette create(int bits, IdMap registry, PaletteResize resizeHandler, List values) { +@@ -58,6 +71,9 @@ public class LinearPalette implements Palette, ca.spottedleaf.moonrise.pat + int ix = this.size; + if (ix < this.values.length) { + this.values[ix] = state; ++ int id = registry.getId(state); ++ this.idCache[ix] = id; ++ this.byteSizes[ix] = VarInt.getByteSize(id); // Cache byte size + this.size++; + return ix; + } else { +@@ -88,29 +104,28 @@ public class LinearPalette implements Palette, ca.spottedleaf.moonrise.pat + @Override + public void read(FriendlyByteBuf buffer) { + this.size = buffer.readVarInt(); +- + for (int i = 0; i < this.size; i++) { +- this.values[i] = this.registry.byIdOrThrow(buffer.readVarInt()); ++ int id = buffer.readVarInt(); ++ this.values[i] = this.registry.byIdOrThrow(id); ++ this.idCache[i] = id; ++ this.byteSizes[i] = VarInt.getByteSize(id); // Precompute during read + } + } + + @Override + public void write(FriendlyByteBuf buffer) { + buffer.writeVarInt(this.size); +- + for (int i = 0; i < this.size; i++) { +- buffer.writeVarInt(this.registry.getId(this.values[i])); ++ buffer.writeVarInt(this.idCache[i]); // Use cached ID + } + } + + @Override + public int getSerializedSize() { +- int byteSize = VarInt.getByteSize(this.getSize()); +- +- for (int i = 0; i < this.getSize(); i++) { +- byteSize += VarInt.getByteSize(this.registry.getId(this.values[i])); ++ int byteSize = VarInt.getByteSize(this.size); ++ for (int i = 0; i < this.size; i++) { ++ byteSize += this.byteSizes[i]; // Use cached byte sizes + } +- + return byteSize; + } + +@@ -121,6 +136,54 @@ public class LinearPalette implements Palette, ca.spottedleaf.moonrise.pat + + @Override + public Palette copy(PaletteResize resizeHandler) { +- return new LinearPalette<>(this.registry, (T[])((Object[])this.values.clone()), resizeHandler, this.bits, this.size); ++ // Special case for empty palette - fastest possible return ++ if (this.size == 0) { ++ return new LinearPalette<>(this.registry, (T[]) new Object[1], new int[1], resizeHandler, this.bits, 0); ++ } ++ ++ // For small sizes, allocate exact-sized arrays and use direct assignment ++ if (this.size <= 4) { ++ @SuppressWarnings("unchecked") ++ T[] valuesCopy = (T[]) new Object[this.size]; ++ int[] idCacheCopy = new int[this.size]; ++ ++ // Unrolled loop eliminates loop overhead for small arrays ++ switch (this.size) { ++ case 4: ++ valuesCopy[3] = this.values[3]; ++ idCacheCopy[3] = this.idCache[3]; ++ // Fall through ++ case 3: ++ valuesCopy[2] = this.values[2]; ++ idCacheCopy[2] = this.idCache[2]; ++ // Fall through ++ case 2: ++ valuesCopy[1] = this.values[1]; ++ idCacheCopy[1] = this.idCache[1]; ++ // Fall through ++ case 1: ++ valuesCopy[0] = this.values[0]; ++ idCacheCopy[0] = this.idCache[0]; ++ } ++ ++ return new LinearPalette<>(this.registry, valuesCopy, idCacheCopy, resizeHandler, this.bits, this.size); ++ } ++ ++ // For larger arrays, use optimized bulk operations ++ @SuppressWarnings("unchecked") ++ T[] valuesCopy = (T[]) new Object[this.size]; ++ int[] idCacheCopy = new int[this.size]; ++ ++ System.arraycopy(this.values, 0, valuesCopy, 0, this.size); ++ System.arraycopy(this.idCache, 0, idCacheCopy, 0, this.size); ++ ++ return new LinearPalette<>( ++ this.registry, ++ valuesCopy, ++ idCacheCopy, ++ resizeHandler, ++ this.bits, ++ this.size ++ ); + } + } diff --git a/leaf-server/minecraft-patches/features/0134-Optimize-IdMapper.patch b/leaf-server/minecraft-patches/features/0134-Optimize-IdMapper.patch new file mode 100644 index 00000000..d6261507 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0134-Optimize-IdMapper.patch @@ -0,0 +1,41 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Mon, 24 Feb 2025 16:33:46 +0100 +Subject: [PATCH] Optimize IdMapper + + +diff --git a/net/minecraft/core/IdMapper.java b/net/minecraft/core/IdMapper.java +index 439dc29b8ee8a1dc2ec63c00a9727a37bb697bad..b063cdb0667de69c5ab5ce3895a0adfb19d6a28f 100644 +--- a/net/minecraft/core/IdMapper.java ++++ b/net/minecraft/core/IdMapper.java +@@ -11,7 +11,7 @@ import javax.annotation.Nullable; + + public class IdMapper implements IdMap { + private int nextId; +- private final Reference2IntMap tToId; ++ private final it.unimi.dsi.fastutil.objects.Object2IntOpenCustomHashMap tToId; + private final List idToT; + + public IdMapper() { +@@ -20,7 +20,20 @@ public class IdMapper implements IdMap { + + public IdMapper(int expectedSize) { + this.idToT = Lists.newArrayListWithExpectedSize(expectedSize); +- this.tToId = new Reference2IntOpenHashMap<>(expectedSize); ++ this.tToId = new it.unimi.dsi.fastutil.objects.Object2IntOpenCustomHashMap<>( ++ expectedSize, ++ new it.unimi.dsi.fastutil.Hash.Strategy() { ++ @Override ++ public int hashCode(T o) { ++ return System.identityHashCode(o); ++ } ++ ++ @Override ++ public boolean equals(T a, T b) { ++ return a == b; ++ } ++ } ++ ); + this.tToId.defaultReturnValue(-1); + } + diff --git a/leaf-server/minecraft-patches/features/0135-Slight-optimizations-to-VarInt.patch b/leaf-server/minecraft-patches/features/0135-Slight-optimizations-to-VarInt.patch new file mode 100644 index 00000000..e7a6a588 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0135-Slight-optimizations-to-VarInt.patch @@ -0,0 +1,81 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Mon, 24 Feb 2025 21:11:09 +0100 +Subject: [PATCH] Slight optimizations to VarInt + + +diff --git a/net/minecraft/network/VarInt.java b/net/minecraft/network/VarInt.java +index 6f8dd31582f0e1d3a71acc7a142c1f4ec0539d9e..043db53ee627ac13e3a952c8d5beba5065ecbb48 100644 +--- a/net/minecraft/network/VarInt.java ++++ b/net/minecraft/network/VarInt.java +@@ -51,35 +51,41 @@ public class VarInt { + } + + public static ByteBuf write(ByteBuf buffer, int value) { +- // Gale start - Velocity - optimized VarInt#write +- if ((value & 0xFFFFFF80) == 0) { +- buffer.writeByte(value); +- } else if ((value & 0xFFFFC000) == 0) { +- int w = (value & 0x7F) << 8 +- | (value >>> 7) +- | 0x00008000; +- buffer.writeShort(w); +- } else if ((value & 0xFFE00000) == 0) { +- int w = (value & 0x7F) << 16 +- | (value & 0x3F80) << 1 +- | (value >>> 14) +- | 0x00808000; +- buffer.writeMedium(w); +- } else if ((value & 0xF0000000) == 0) { +- int w = (value & 0x7F) << 24 +- | ((value & 0x3F80) << 9) +- | (value & 0x1FC000) >> 6 +- | (value >>> 21) +- | 0x80808000; +- buffer.writeInt(w); +- } else { +- int w = (value & 0x7F) << 24 +- | (value & 0x3F80) << 9 +- | (value & 0x1FC000) >> 6 +- | ((value >>> 21) & 0x7F) +- | 0x80808080; +- buffer.writeInt(w); +- buffer.writeByte(value >>> 28); ++ // Gale start - Velocity - optimized VarInt#write // Leaf - help JIT by using switch case ++ int bytesNeeded = getByteSize(value); ++ ++ switch (bytesNeeded) { ++ case 1: ++ buffer.writeByte(value); ++ break; ++ case 2: ++ int w2 = ((value & 0x7F) << 8) | (value >>> 7) | 0x00008000; ++ buffer.writeShort(w2); ++ break; ++ case 3: ++ int w3 = (value & 0x7F) << 16 ++ | (value & 0x3F80) << 1 ++ | (value >>> 14) ++ | 0x00808000; ++ buffer.writeMedium(w3); ++ break; ++ case 4: ++ int w4 = (value & 0x7F) << 24 ++ | ((value & 0x3F80) << 9) ++ | (value & 0x1FC000) >> 6 ++ | (value >>> 21) ++ | 0x80808000; ++ buffer.writeInt(w4); ++ break; ++ case 5: ++ int w5 = (value & 0x7F) << 24 ++ | (value & 0x3F80) << 9 ++ | (value & 0x1FC000) >> 6 ++ | ((value >>> 21) & 0x7F) ++ | 0x80808080; ++ buffer.writeInt(w5); ++ buffer.writeByte(value >>> 28); ++ break; + } + return buffer; + } diff --git a/leaf-server/minecraft-patches/features/0136-Rewrite-ClientboundLightUpdatePacketData.patch b/leaf-server/minecraft-patches/features/0136-Rewrite-ClientboundLightUpdatePacketData.patch new file mode 100644 index 00000000..fc49511e --- /dev/null +++ b/leaf-server/minecraft-patches/features/0136-Rewrite-ClientboundLightUpdatePacketData.patch @@ -0,0 +1,236 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Mon, 24 Feb 2025 21:33:24 +0100 +Subject: [PATCH] Rewrite ClientboundLightUpdatePacketData + + +diff --git a/net/minecraft/network/protocol/game/ClientboundLightUpdatePacketData.java b/net/minecraft/network/protocol/game/ClientboundLightUpdatePacketData.java +index a0b54f3a3d11e0f0f1cb806406a870ba36da8f07..2ef45811f3a3a763f389e8e6e9eeaf255cf668e6 100644 +--- a/net/minecraft/network/protocol/game/ClientboundLightUpdatePacketData.java ++++ b/net/minecraft/network/protocol/game/ClientboundLightUpdatePacketData.java +@@ -1,8 +1,8 @@ + package net.minecraft.network.protocol.game; + +-import com.google.common.collect.Lists; + import io.netty.buffer.ByteBuf; + import java.util.BitSet; ++import java.util.Arrays; + import java.util.List; + import javax.annotation.Nullable; + import net.minecraft.core.SectionPos; +@@ -16,30 +16,109 @@ import net.minecraft.world.level.lighting.LevelLightEngine; + + public class ClientboundLightUpdatePacketData { + private static final StreamCodec DATA_LAYER_STREAM_CODEC = ByteBufCodecs.byteArray(2048); ++ ++ // Static constants to avoid allocations ++ private static final byte[][] EMPTY_ARRAY = new byte[0][]; ++ ++ // Pre-sized arrays to avoid dynamic resizing ++ private static final ThreadLocal SKY_BUFFER = ThreadLocal.withInitial(() -> new byte[256][]); ++ private static final ThreadLocal BLOCK_BUFFER = ThreadLocal.withInitial(() -> new byte[256][]); ++ ++ // Pre-cached BitSets with fixed size + private final BitSet skyYMask; + private final BitSet blockYMask; + private final BitSet emptySkyYMask; + private final BitSet emptyBlockYMask; +- private final List skyUpdates; +- private final List blockUpdates; ++ ++ // Fixed arrays with exact counts ++ private final byte[][] skyUpdates; ++ private final byte[][] blockUpdates; ++ private final int skyUpdateCount; ++ private final int blockUpdateCount; + + public ClientboundLightUpdatePacketData(ChunkPos chunkPos, LevelLightEngine lightEngine, @Nullable BitSet skyLight, @Nullable BitSet blockLight) { +- this.skyYMask = new BitSet(); +- this.blockYMask = new BitSet(); +- this.emptySkyYMask = new BitSet(); +- this.emptyBlockYMask = new BitSet(); +- this.skyUpdates = Lists.newArrayList(); +- this.blockUpdates = Lists.newArrayList(); +- +- for (int i = 0; i < lightEngine.getLightSectionCount(); i++) { ++ int sectionCount = lightEngine.getLightSectionCount(); ++ ++ // Round up to nearest long boundary (64 bits) to prevent BitSet expansion ++ int longWords = (sectionCount + 63) >>> 6; ++ int bitSetSize = longWords << 6; ++ ++ // Pre-size all BitSets to exact size needed ++ this.skyYMask = new BitSet(bitSetSize); ++ this.blockYMask = new BitSet(bitSetSize); ++ this.emptySkyYMask = new BitSet(bitSetSize); ++ this.emptyBlockYMask = new BitSet(bitSetSize); ++ ++ // Get buffer arrays from thread local storage to avoid allocations ++ byte[][] skyBuffer = SKY_BUFFER.get(); ++ byte[][] blockBuffer = BLOCK_BUFFER.get(); ++ ++ // Process all sections in a single pass ++ int skyCount = 0; ++ int blockCount = 0; ++ int minLightSection = lightEngine.getMinLightSection(); ++ ++ // Cache layer listeners to avoid repeated method calls ++ var skyLayerListener = lightEngine.getLayerListener(LightLayer.SKY); ++ var blockLayerListener = lightEngine.getLayerListener(LightLayer.BLOCK); ++ ++ // Single pass through all sections ++ for (int i = 0; i < sectionCount; i++) { ++ int sectionY = minLightSection + i; ++ SectionPos sectionPos = SectionPos.of(chunkPos.x, sectionY, chunkPos.z); ++ ++ // Process sky light + if (skyLight == null || skyLight.get(i)) { +- this.prepareSectionData(chunkPos, lightEngine, LightLayer.SKY, i, this.skyYMask, this.emptySkyYMask, this.skyUpdates); ++ DataLayer skyData = skyLayerListener.getDataLayerData(sectionPos); ++ if (skyData != null) { ++ if (skyData.isEmpty()) { ++ emptySkyYMask.set(i); ++ } else { ++ skyYMask.set(i); ++ // Store in buffer temporarily - only clone at the end ++ skyBuffer[skyCount++] = skyData.getData(); ++ } ++ } + } + ++ // Process block light + if (blockLight == null || blockLight.get(i)) { +- this.prepareSectionData(chunkPos, lightEngine, LightLayer.BLOCK, i, this.blockYMask, this.emptyBlockYMask, this.blockUpdates); ++ DataLayer blockData = blockLayerListener.getDataLayerData(sectionPos); ++ if (blockData != null) { ++ if (blockData.isEmpty()) { ++ emptyBlockYMask.set(i); ++ } else { ++ blockYMask.set(i); ++ // Store in buffer temporarily - only clone at the end ++ blockBuffer[blockCount++] = blockData.getData(); ++ } ++ } ++ } ++ } ++ ++ // Create final arrays with exact sizes ++ if (skyCount > 0) { ++ this.skyUpdates = new byte[skyCount][]; ++ // Clone only at the end to minimize work ++ for (int i = 0; i < skyCount; i++) { ++ this.skyUpdates[i] = skyBuffer[i].clone(); ++ } ++ } else { ++ this.skyUpdates = EMPTY_ARRAY; ++ } ++ ++ if (blockCount > 0) { ++ this.blockUpdates = new byte[blockCount][]; ++ // Clone only at the end to minimize work ++ for (int i = 0; i < blockCount; i++) { ++ this.blockUpdates[i] = blockBuffer[i].clone(); + } ++ } else { ++ this.blockUpdates = EMPTY_ARRAY; + } ++ ++ this.skyUpdateCount = skyCount; ++ this.blockUpdateCount = blockCount; + } + + public ClientboundLightUpdatePacketData(FriendlyByteBuf buffer, int x, int z) { +@@ -47,8 +126,28 @@ public class ClientboundLightUpdatePacketData { + this.blockYMask = buffer.readBitSet(); + this.emptySkyYMask = buffer.readBitSet(); + this.emptyBlockYMask = buffer.readBitSet(); +- this.skyUpdates = buffer.readList(DATA_LAYER_STREAM_CODEC); +- this.blockUpdates = buffer.readList(DATA_LAYER_STREAM_CODEC); ++ ++ // Read lists directly as arrays to avoid intermediate collections ++ List skyList = buffer.readList(DATA_LAYER_STREAM_CODEC); ++ List blockList = buffer.readList(DATA_LAYER_STREAM_CODEC); ++ ++ int skySize = skyList.size(); ++ int blockSize = blockList.size(); ++ ++ if (skySize > 0) { ++ this.skyUpdates = skyList.toArray(new byte[skySize][]); ++ } else { ++ this.skyUpdates = EMPTY_ARRAY; ++ } ++ ++ if (blockSize > 0) { ++ this.blockUpdates = blockList.toArray(new byte[blockSize][]); ++ } else { ++ this.blockUpdates = EMPTY_ARRAY; ++ } ++ ++ this.skyUpdateCount = skySize; ++ this.blockUpdateCount = blockSize; + } + + public void write(FriendlyByteBuf buffer) { +@@ -56,25 +155,31 @@ public class ClientboundLightUpdatePacketData { + buffer.writeBitSet(this.blockYMask); + buffer.writeBitSet(this.emptySkyYMask); + buffer.writeBitSet(this.emptyBlockYMask); +- buffer.writeCollection(this.skyUpdates, DATA_LAYER_STREAM_CODEC); +- buffer.writeCollection(this.blockUpdates, DATA_LAYER_STREAM_CODEC); +- } + +- private void prepareSectionData( +- ChunkPos chunkPos, LevelLightEngine levelLightEngine, LightLayer lightLayer, int index, BitSet skyLight, BitSet blockLight, List updates +- ) { +- DataLayer dataLayerData = levelLightEngine.getLayerListener(lightLayer) +- .getDataLayerData(SectionPos.of(chunkPos, levelLightEngine.getMinLightSection() + index)); +- if (dataLayerData != null) { +- if (dataLayerData.isEmpty()) { +- blockLight.set(index); +- } else { +- skyLight.set(index); +- updates.add(dataLayerData.copy().getData()); ++ // Avoid creating unnecessary objects when writing ++ if (this.skyUpdateCount > 0) { ++ // Use direct array access for efficiency ++ buffer.writeVarInt(this.skyUpdateCount); ++ for (int i = 0; i < this.skyUpdateCount; i++) { ++ DATA_LAYER_STREAM_CODEC.encode(buffer, this.skyUpdates[i]); + } ++ } else { ++ buffer.writeVarInt(0); ++ } ++ ++ if (this.blockUpdateCount > 0) { ++ // Use direct array access for efficiency ++ buffer.writeVarInt(this.blockUpdateCount); ++ for (int i = 0; i < this.blockUpdateCount; i++) { ++ DATA_LAYER_STREAM_CODEC.encode(buffer, this.blockUpdates[i]); ++ } ++ } else { ++ buffer.writeVarInt(0); + } + } + ++ // Getter methods ++ + public BitSet getSkyYMask() { + return this.skyYMask; + } +@@ -84,7 +189,7 @@ public class ClientboundLightUpdatePacketData { + } + + public List getSkyUpdates() { +- return this.skyUpdates; ++ return this.skyUpdateCount > 0 ? Arrays.asList(this.skyUpdates) : List.of(); + } + + public BitSet getBlockYMask() { +@@ -96,6 +201,6 @@ public class ClientboundLightUpdatePacketData { + } + + public List getBlockUpdates() { +- return this.blockUpdates; ++ return this.blockUpdateCount > 0 ? Arrays.asList(this.blockUpdates) : List.of(); + } + } diff --git a/leaf-server/minecraft-patches/features/0137-Some-Optimizations-on-SerializableChunkData.patch b/leaf-server/minecraft-patches/features/0137-Some-Optimizations-on-SerializableChunkData.patch new file mode 100644 index 00000000..71e5ed23 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0137-Some-Optimizations-on-SerializableChunkData.patch @@ -0,0 +1,89 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Tue, 25 Feb 2025 21:13:54 +0100 +Subject: [PATCH] Some Optimizations on SerializableChunkData + + +diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +index 6b6aaeca14178b5b709e20ae13552d42217f15c0..c10ed10dd843bfa12be3f80a244cda94f8c56807 100644 +--- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java ++++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +@@ -502,14 +502,14 @@ public record SerializableChunkData( + throw new IllegalArgumentException("Chunk can't be serialized: " + chunk); + } else { + ChunkPos pos = chunk.getPos(); +- List list = new ArrayList<>(); final List sectionsList = list; // Paper - starlight - OBFHELPER +- LevelChunkSection[] sections = chunk.getSections(); +- LevelLightEngine lightEngine = level.getChunkSource().getLightEngine(); + + // Paper start - starlight + final int minLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinLightSection(level); + final int maxLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxLightSection(level); + final int minBlockSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level); ++ // Pre-allocate with correct capacity to avoid resizing ++ final int expectedSectionCount = maxLightSection - minLightSection + 1; ++ List list = new ArrayList<>(expectedSectionCount); + + final LevelChunkSection[] chunkSections = chunk.getSections(); + final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles = ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getBlockNibbles(); +@@ -541,10 +541,11 @@ public record SerializableChunkData( + ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$setSkyLightState(skyNibble.state); + } + +- sectionsList.add(sectionData); ++ list.add(sectionData); + } + // Paper end - starlight + ++ // Pre-allocate block entities list with exact size needed + List list1 = new ArrayList<>(chunk.getBlockEntitiesPos().size()); + + for (BlockPos blockPos : chunk.getBlockEntitiesPos()) { +@@ -554,7 +555,14 @@ public record SerializableChunkData( + } + } + +- List list2 = new ArrayList<>(); ++ // For entities, use an initial estimated capacity if it's a ProtoChunk ++ int entityEstimate = 64; // Reasonable default size ++ if (chunk.getPersistedStatus().getChunkType() == ChunkType.PROTOCHUNK) { ++ ProtoChunk protoChunk = (ProtoChunk)chunk; ++ entityEstimate = Math.max(16, protoChunk.getEntities().size()); ++ } ++ List list2 = new ArrayList<>(entityEstimate); ++ + long[] longs = null; + if (chunk.getPersistedStatus().getChunkType() == ChunkType.PROTOCHUNK) { + ProtoChunk protoChunk = (ProtoChunk)chunk; +@@ -570,14 +578,16 @@ public record SerializableChunkData( + for (Entry entry : chunk.getHeightmaps()) { + if (chunk.getPersistedStatus().heightmapsAfter().contains(entry.getKey())) { + long[] rawData = entry.getValue().getRawData(); +- map.put(entry.getKey(), (long[])rawData.clone()); ++ map.put(entry.getKey(), Arrays.copyOf(rawData, rawData.length)); + } + } + + ChunkAccess.PackedTicks ticksForSerialization = chunk.getTicksForSerialization(level.getGameTime()); +- ShortList[] lists = Arrays.stream(chunk.getPostProcessing()) +- .map(list3 -> list3 != null ? new ShortArrayList(list3) : null) +- .toArray(ShortList[]::new); ++ ShortList[] postProcessing = chunk.getPostProcessing(); ++ ShortList[] lists = new ShortList[postProcessing.length]; ++ for (int i = 0; i < postProcessing.length; i++) { ++ lists[i] = postProcessing[i] != null ? new ShortArrayList(postProcessing[i]) : null; ++ } + CompoundTag compoundTag = packStructureData( + StructurePieceSerializationContext.fromLevel(level), pos, chunk.getAllStarts(), chunk.getAllReferences() + ); +@@ -605,8 +615,8 @@ public record SerializableChunkData( + list, + list2, + list1, +- compoundTag +- , persistentDataContainer // CraftBukkit - persistentDataContainer ++ compoundTag, ++ persistentDataContainer // CraftBukkit - persistentDataContainer + ); + } + } diff --git a/leaf-server/minecraft-patches/features/0138-Rework-ChunkHolderManager.patch b/leaf-server/minecraft-patches/features/0138-Rework-ChunkHolderManager.patch new file mode 100644 index 00000000..939898f9 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0138-Rework-ChunkHolderManager.patch @@ -0,0 +1,125 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Thu, 27 Feb 2025 23:39:32 +0100 +Subject: [PATCH] Rework ChunkHolderManager + + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +index be820c6093dd2ae7642b9bee11edf65e3a8d7242..29872fa10ec833ff9391fc09df034204f092f8a6 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +@@ -736,24 +736,19 @@ public final class ChunkHolderManager { + + final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); + +- final Predicate> expireNow = (final Ticket ticket) -> { +- long removeDelay = ((ChunkSystemTicket)(Object)ticket).moonrise$getRemoveDelay(); +- if (removeDelay == NO_TIMEOUT_MARKER) { +- return false; +- } +- --removeDelay; +- ((ChunkSystemTicket)(Object)ticket).moonrise$setRemoveDelay(removeDelay); +- return removeDelay <= 0L; +- }; +- ++ // Collect sections to process first to avoid concurrent modification issues ++ List sectionKeys = new ArrayList<>(); + for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) { +- final long sectionKey = iterator.nextLong(); ++ sectionKeys.add(iterator.nextLong()); ++ } + ++ for (final Long sectionKey : sectionKeys) { ++ // Skip if section was removed concurrently + if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) { +- // removed concurrently + continue; + } + ++ // Acquire lock for this section only + final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( + CoordinateUtils.getChunkX(sectionKey) << sectionShift, + CoordinateUtils.getChunkZ(sectionKey) << sectionShift +@@ -761,45 +756,66 @@ public final class ChunkHolderManager { + + try { + final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(sectionKey); +- if (chunkToExpireCount == null) { +- // lost to some race ++ if (chunkToExpireCount == null || chunkToExpireCount.isEmpty()) { ++ // Section was removed or is empty, clean up ++ if (chunkToExpireCount != null && chunkToExpireCount.isEmpty()) { ++ this.sectionToChunkToExpireCount.remove(sectionKey); ++ } + continue; + } + ++ // Process each chunk in this section + for (final Iterator iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) { + final Long2IntMap.Entry entry = iterator1.next(); +- + final long chunkKey = entry.getLongKey(); + final int expireCount = entry.getIntValue(); + + final SortedArraySet> tickets = this.tickets.get(chunkKey); ++ if (tickets == null || tickets.isEmpty()) { ++ iterator1.remove(); ++ continue; ++ } ++ + final int levelBefore = getTicketLevelAt(tickets); ++ int expiredCount = 0; + +- final int sizeBefore = tickets.size(); +- tickets.removeIf(expireNow); +- final int sizeAfter = tickets.size(); +- final int levelAfter = getTicketLevelAt(tickets); ++ // More efficient ticket processing - avoids creating a new predicate each time ++ for (Iterator> ticketIterator = tickets.iterator(); ticketIterator.hasNext();) { ++ Ticket ticket = ticketIterator.next(); ++ long removeDelay = ((ChunkSystemTicket)(Object)ticket).moonrise$getRemoveDelay(); ++ ++ if (removeDelay == NO_TIMEOUT_MARKER) { ++ continue; ++ } ++ ++ --removeDelay; ++ if (removeDelay <= 0) { ++ ticketIterator.remove(); ++ expiredCount++; ++ } else { ++ ((ChunkSystemTicket)(Object)ticket).moonrise$setRemoveDelay(removeDelay); ++ } ++ } + + if (tickets.isEmpty()) { + this.tickets.remove(chunkKey); + } ++ ++ final int levelAfter = getTicketLevelAt(tickets); + if (levelBefore != levelAfter) { + this.updateTicketLevel(chunkKey, levelAfter); + } + +- final int newExpireCount = expireCount - (sizeBefore - sizeAfter); +- +- if (newExpireCount == expireCount) { +- continue; +- } +- +- if (newExpireCount != 0) { +- entry.setValue(newExpireCount); +- } else { ++ // Update expire count ++ final int newExpireCount = expireCount - expiredCount; ++ if (newExpireCount <= 0) { + iterator1.remove(); ++ } else if (newExpireCount != expireCount) { ++ entry.setValue(newExpireCount); + } + } + ++ // Remove empty sections + if (chunkToExpireCount.isEmpty()) { + this.sectionToChunkToExpireCount.remove(sectionKey); + } diff --git a/leaf-server/minecraft-patches/features/0139-Optimize-chunkUnload.patch b/leaf-server/minecraft-patches/features/0139-Optimize-chunkUnload.patch new file mode 100644 index 00000000..61e01a88 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0139-Optimize-chunkUnload.patch @@ -0,0 +1,222 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Fri, 28 Feb 2025 01:35:49 +0100 +Subject: [PATCH] Optimize chunkUnload + + +diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java +index 4ca68a903e67606fc4ef0bfa9862a73797121c8b..75893974d198b946bdc07b01b4c68ff999df5028 100644 +--- a/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java ++++ b/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java +@@ -3,6 +3,8 @@ package ca.spottedleaf.moonrise.patches.starlight.light; + import net.minecraft.world.level.chunk.DataLayer; + import java.util.ArrayDeque; + import java.util.Arrays; ++import java.util.Map; ++import java.util.WeakHashMap; + + // SWMR -> Single Writer Multi Reader Nibble Array + public final class SWMRNibbleArray { +@@ -22,21 +24,35 @@ public final class SWMRNibbleArray { + protected static final int INIT_STATE_INIT = 2; // initialised + protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL + ++ private volatile boolean cachedIsAllZero = false; ++ private boolean cachedIsAllZeroValid = false; ++ ++ private static final ThreadLocal SAVE_STATE_CACHE = ++ ThreadLocal.withInitial(() -> new SaveState[4]); ++ + public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block + // this allows us to maintain only 1 byte array when we're not updating +- static final ThreadLocal> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new); ++ static final ThreadLocal> WORKING_BYTES_POOL = new ThreadLocal>() { ++ @Override ++ protected ArrayDeque initialValue() { ++ return new ArrayDeque(8); // Limit pool size to avoid memory leaks ++ } ++ }; + + private static byte[] allocateBytes() { +- final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst(); ++ final ArrayDeque queue = WORKING_BYTES_POOL.get(); ++ final byte[] inPool = queue.pollFirst(); + if (inPool != null) { + return inPool; + } +- + return new byte[ARRAY_SIZE]; + } + + private static void freeBytes(final byte[] bytes) { +- WORKING_BYTES_POOL.get().addFirst(bytes); ++ final ArrayDeque queue = WORKING_BYTES_POOL.get(); ++ if (queue.size() < 8) { // Limit pool size to prevent memory leaks ++ queue.addFirst(bytes); ++ } + } + + public static SWMRNibbleArray fromVanilla(final DataLayer nibble) { +@@ -131,15 +147,44 @@ public final class SWMRNibbleArray { + public SaveState getSaveState() { + synchronized (this) { + final int state = this.stateVisible; +- final byte[] data = this.storageVisible; + if (state == INIT_STATE_NULL) { + return null; + } ++ + if (state == INIT_STATE_UNINIT) { +- return new SaveState(null, state); ++ // Use array-based cache instead of WeakHashMap ++ SaveState[] cache = SAVE_STATE_CACHE.get(); ++ SaveState cachedState = cache[INIT_STATE_UNINIT]; ++ if (cachedState == null) { ++ cachedState = new SaveState(null, state); ++ cache[INIT_STATE_UNINIT] = cachedState; ++ } ++ return cachedState; ++ } ++ ++ // Check if we need to test for all zeros ++ final byte[] data = this.storageVisible; ++ boolean zero; ++ if (cachedIsAllZeroValid) { ++ zero = cachedIsAllZero; ++ } else { ++ zero = isAllZero(data); ++ cachedIsAllZero = zero; ++ cachedIsAllZeroValid = true; + } +- final boolean zero = isAllZero(data); ++ + if (zero) { ++ // Use array-based cache instead of WeakHashMap ++ SaveState[] cache = SAVE_STATE_CACHE.get(); ++ int cacheKey = state == INIT_STATE_INIT ? INIT_STATE_UNINIT : -1; ++ if (cacheKey >= 0) { ++ SaveState cachedState = cache[cacheKey]; ++ if (cachedState == null) { ++ cachedState = new SaveState(null, cacheKey); ++ cache[cacheKey] = cachedState; ++ } ++ return cachedState; ++ } + return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null; + } else { + return new SaveState(data.clone(), state); +@@ -148,14 +193,23 @@ public final class SWMRNibbleArray { + } + + protected static boolean isAllZero(final byte[] data) { +- for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { +- byte whole = data[i << 4]; +- +- for (int k = 1; k < (1 << 4); ++k) { +- whole |= data[(i << 4) | k]; ++ // check in 8-byte chunks ++ final int longLength = ARRAY_SIZE >>> 3; ++ for (int i = 0; i < longLength; i++) { ++ long value = 0; ++ final int baseIndex = i << 3; ++ // Combine 8 bytes into a long ++ for (int j = 0; j < 8; j++) { ++ value |= ((long)(data[baseIndex + j] & 0xFF)) << (j << 3); ++ } ++ if (value != 0) { ++ return false; + } ++ } + +- if (whole != 0) { ++ // Check remaining bytes ++ for (int i = longLength << 3; i < ARRAY_SIZE; i++) { ++ if (data[i] != 0) { + return false; + } + } +@@ -163,6 +217,10 @@ public final class SWMRNibbleArray { + return true; + } + ++ private void invalidateCache() { ++ this.cachedIsAllZeroValid = false; ++ } ++ + // operation type: updating on src, updating on other + public void extrudeLower(final SWMRNibbleArray other) { + if (other.stateUpdating == INIT_STATE_NULL) { +@@ -349,6 +407,7 @@ public final class SWMRNibbleArray { + } + this.updatingDirty = false; + this.stateVisible = this.stateUpdating; ++ this.cachedIsAllZeroValid = false; // Invalidate cache on update + } + + return true; +@@ -424,7 +483,14 @@ public final class SWMRNibbleArray { + final int shift = (index & 1) << 2; + final int i = index >>> 1; + +- this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift)); ++ byte oldValue = this.storageUpdating[i]; ++ byte newValue = (byte)((oldValue & (0xF0 >>> shift)) | (value << shift)); ++ ++ // Only invalidate cache if the value actually changes ++ if (oldValue != newValue) { ++ this.storageUpdating[i] = newValue; ++ this.invalidateCache(); ++ } + } + + public static final class SaveState { +diff --git a/net/minecraft/world/level/chunk/LevelChunkSection.java b/net/minecraft/world/level/chunk/LevelChunkSection.java +index b8ac6a9ba7b56ccd034757f7d135d272b8e69e90..f6c33a7ca59d00c8967034402be00767a66b6948 100644 +--- a/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -24,6 +24,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + private boolean isRandomlyTickingBlocksStatus; // Leaf - Cache random tick block status + public final PalettedContainer states; + private PalettedContainer> biomes; // CraftBukkit - read/write ++ private boolean modified = false; + + // Paper start - block counting + private static final it.unimi.dsi.fastutil.shorts.ShortArrayList FULL_LIST = new it.unimi.dsi.fastutil.shorts.ShortArrayList(16*16*16); +@@ -135,6 +136,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + // Paper end - block counting + + public BlockState setBlockState(int x, int y, int z, BlockState state, boolean useLocks) { ++ this.modified = true; + BlockState blockState; + if (useLocks) { + blockState = this.states.getAndSet(x, y, z, state); +@@ -328,7 +330,29 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + this.biomes = palettedContainer; + } + ++ private LevelChunkSection(short nonEmptyBlockCount, short tickingBlockCount, short tickingFluidCount, ++ PalettedContainer states, PalettedContainer> biomes) { ++ this.nonEmptyBlockCount = nonEmptyBlockCount; ++ this.tickingBlockCount = tickingBlockCount; ++ this.tickingFluidCount = tickingFluidCount; ++ this.states = states; ++ this.biomes = biomes; ++ this.isRandomlyTickingBlocksStatus = this.tickingBlockCount > 0; ++ } ++ + public LevelChunkSection copy() { ++ // If the section hasn't been modified and no random ticking blocks/fluids, ++ // return a lightweight copy that shares palette data ++ if (!this.modified && this.tickingBlockCount == 0 && this.tickingFluidCount == 0) { ++ return new LevelChunkSection( ++ this.nonEmptyBlockCount, ++ this.tickingBlockCount, ++ this.tickingFluidCount, ++ this.states, // Share reference instead of copying ++ this.biomes // Share reference instead of copying ++ ); ++ } + return new LevelChunkSection(this); + } ++ + } diff --git a/leaf-server/minecraft-patches/features/0140-Async-ChunkSend.patch b/leaf-server/minecraft-patches/features/0140-Async-ChunkSend.patch new file mode 100644 index 00000000..875efee2 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0140-Async-ChunkSend.patch @@ -0,0 +1,126 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Sun, 2 Mar 2025 21:23:20 +0100 +Subject: [PATCH] Async ChunkSend + + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +index a35e9fae8f8da0c42f0616c4f78dc396492673aa..31f9556e808c9dea49ba9774cbf736791ed9a687 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -22,15 +22,13 @@ import it.unimi.dsi.fastutil.longs.LongComparator; + import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; + import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + import net.minecraft.network.protocol.Packet; +-import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; +-import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; +-import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; +-import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; ++import net.minecraft.network.protocol.game.*; + import net.minecraft.server.level.ChunkTrackingView; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.server.level.ServerPlayer; + import net.minecraft.server.level.TicketType; + import net.minecraft.server.network.PlayerChunkSender; ++import net.minecraft.server.network.ServerGamePacketListenerImpl; + import net.minecraft.world.level.ChunkPos; + import net.minecraft.world.level.GameRules; + import net.minecraft.world.level.chunk.ChunkAccess; +@@ -43,6 +41,8 @@ import java.util.concurrent.TimeUnit; + import java.util.concurrent.atomic.AtomicLong; + import java.util.function.Function; + ++import static org.dreeam.leaf.config.LeafConfig.LOGGER; ++ + public final class RegionizedPlayerChunkLoader { + + public static final TicketType PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo); +@@ -411,18 +411,81 @@ public final class RegionizedPlayerChunkLoader { + this.delayedTicketOps.addLast(op); + } + ++ /** ++ * Sends a chunk to the player. ++ * If async chunk sending is enabled, this will prepare and send the chunk packet asynchronously. ++ * Otherwise, it will use the synchronous chunk sending implementation. ++ */ + private void sendChunk(final int chunkX, final int chunkZ) { +- if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { +- ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager +- .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player); ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + +- final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); ++ if (!this.sentChunks.add(chunkKey)) { ++ throw new IllegalStateException(); ++ } + +- PlatformHooks.get().onChunkWatch(this.world, chunk, this.player); +- PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); ++ // Get the chunk now, as we need it for both sync and async paths ++ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); ++ if (chunk == null) { ++ // Handle case where chunk is no longer loaded ++ this.sentChunks.remove(chunkKey); + return; + } +- throw new IllegalStateException(); ++ ++ // This part needs to remain on the main thread as it affects shared state ++ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager ++ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player); ++ ++ // Call onChunkWatch on the main thread as it might affect server state ++ PlatformHooks.get().onChunkWatch(this.world, chunk, this.player); ++ ++ // Check if async chunk sending is enabled ++ if (org.dreeam.leaf.config.modules.async.AsyncChunkSend.enabled) { ++ // Async implementation ++ net.minecraft.Util.backgroundExecutor().execute(() -> { ++ try { ++ // Create and send the chunk packet asynchronously ++ final ServerGamePacketListenerImpl connection = this.player.connection; ++ final ServerLevel serverLevel = this.world; ++ ++ // Create the packet ++ ClientboundLevelChunkWithLightPacket packet = new ClientboundLevelChunkWithLightPacket(chunk, serverLevel.getLightEngine(), null, null, serverLevel.chunkPacketBlockController.shouldModify(this.player, chunk)); ++ // The packet is immediately ready ++ packet.setReady(true); ++ ++ // Schedule sending on the main thread ++ serverLevel.getServer().execute(() -> { ++ if (this.removed || !this.sentChunks.contains(chunkKey)) { ++ // Player was removed or chunk was unloaded while we were preparing ++ return; ++ } ++ ++ // Send the packet ++ connection.send(packet); ++ ++ // Fire the load event ++ if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { ++ new io.papermc.paper.event.packet.PlayerChunkLoadEvent( ++ new org.bukkit.craftbukkit.CraftChunk(chunk), ++ this.player.getBukkitEntity() ++ ).callEvent(); ++ } ++ ++ // Send POI packets if needed ++ ChunkPos pos = chunk.getPos(); ++ DebugPackets.sendPoiPacketsForChunk(serverLevel, pos); ++ }); ++ } catch (Exception e) { ++ // Log the exception ++ LOGGER.error("Failed to send chunk asynchronously", e); ++ if (!this.removed) { ++ this.sentChunks.remove(chunkKey); ++ } ++ } ++ }); ++ } else { ++ // Original synchronous implementation ++ PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); ++ } + } + + private void sendUnloadChunk(final int chunkX, final int chunkZ) { diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java new file mode 100644 index 00000000..ec41f9a3 --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java @@ -0,0 +1,28 @@ +package org.dreeam.leaf.config.modules.async; + +import org.dreeam.leaf.config.ConfigModules; +import org.dreeam.leaf.config.EnumConfigCategory; +import org.dreeam.leaf.config.annotations.Experimental; + +public class AsyncChunkSend extends ConfigModules { + + public String getBasePath() { + return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-chunk-send"; + } + + @Experimental + public static boolean enabled = false; + + @Override + public void onLoaded() { + config.addCommentRegionBased(getBasePath(), + """ + Makes chunk packet preparation and sending asynchronous to improve server performance. + This can significantly reduce main thread load when many players are loading chunks.""", + """ + 使区块数据包准备和发送异步化以提高服务器性能. + 当许多玩家同时加载区块时, 这可以显著减少主线程负载."""); + + enabled = config.getBoolean(getBasePath() + ".enabled", enabled); + } +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/util/map/ConcurrentLongHashSet.java b/leaf-server/src/main/java/org/dreeam/leaf/util/map/ConcurrentLongHashSet.java index 5a6ab406..f215b67b 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/util/map/ConcurrentLongHashSet.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/util/map/ConcurrentLongHashSet.java @@ -1,211 +1,254 @@ package org.dreeam.leaf.util.map; -import it.unimi.dsi.fastutil.longs.LongCollection; import it.unimi.dsi.fastutil.longs.LongIterator; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongSet; import org.jetbrains.annotations.NotNull; -import java.util.Collection; -import java.util.Iterator; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; +import java.util.*; +import java.util.concurrent.locks.ReentrantLock; -/** - * A thread-safe implementation of {@link LongOpenHashSet} using ConcurrentHashMap.KeySetView as backing storage. - * This implementation provides concurrent access and high performance for concurrent operations. - */ -@SuppressWarnings({"unused", "deprecation"}) -public final class ConcurrentLongHashSet extends LongOpenHashSet implements LongSet { // Extending LongOpenHashSet for some moonrise usages - private final ConcurrentHashMap.KeySetView backing; +public final class ConcurrentLongHashSet extends LongOpenHashSet implements LongSet { + private static final int DEFAULT_SEGMENTS = 16; // Should be power-of-two + private final Segment[] segments; + private final int segmentMask; - /** - * Creates a new empty concurrent long set. - */ public ConcurrentLongHashSet() { - this.backing = ConcurrentHashMap.newKeySet(); + this(DEFAULT_SEGMENTS); } @Override - public int size() { - return backing.size(); - } - - @Override - public boolean isEmpty() { - return backing.isEmpty(); - } - - @Override - public @NotNull LongIterator iterator() { - return new WrappingLongIterator(backing.iterator()); - } - - @NotNull - @Override - public Object @NotNull [] toArray() { - return backing.toArray(); - } - - @NotNull - @Override - public T @NotNull [] toArray(@NotNull T @NotNull [] array) { - Objects.requireNonNull(array, "Array cannot be null"); - return backing.toArray(array); - } - - @Override - public boolean containsAll(@NotNull Collection collection) { - Objects.requireNonNull(collection, "Collection cannot be null"); - return backing.containsAll(collection); - } - - @Override - public boolean addAll(@NotNull Collection collection) { - Objects.requireNonNull(collection, "Collection cannot be null"); - return backing.addAll(collection); - } - - @Override - public boolean removeAll(@NotNull Collection collection) { - Objects.requireNonNull(collection, "Collection cannot be null"); - return backing.removeAll(collection); - } - - @Override - public boolean retainAll(@NotNull Collection collection) { - Objects.requireNonNull(collection, "Collection cannot be null"); - return backing.retainAll(collection); - } - - @Override - public void clear() { - backing.clear(); - } - - @Override - public boolean add(long key) { - return backing.add(key); - } - - @Override - public boolean contains(long key) { - return backing.contains(key); - } - - @Override - public long[] toLongArray() { - int size = backing.size(); - long[] result = new long[size]; - int i = 0; - for (Long value : backing) { - result[i++] = value; - } - return result; - } - - @Override - public long[] toArray(long[] array) { - Objects.requireNonNull(array, "Array cannot be null"); - long[] result = toLongArray(); - if (array.length < result.length) { - return result; - } - System.arraycopy(result, 0, array, 0, result.length); - if (array.length > result.length) { - array[result.length] = 0; - } - return array; - } - - @Override - public boolean addAll(LongCollection c) { + public boolean removeAll(@NotNull Collection c) { Objects.requireNonNull(c, "Collection cannot be null"); boolean modified = false; - LongIterator iterator = c.iterator(); - while (iterator.hasNext()) { - modified |= add(iterator.nextLong()); + for (Object obj : c) { + if (obj instanceof Long) { + modified |= remove((Long) obj); + } } return modified; } @Override - public boolean containsAll(LongCollection c) { + public boolean retainAll(@NotNull Collection c) { Objects.requireNonNull(c, "Collection cannot be null"); - LongIterator iterator = c.iterator(); + boolean modified = false; + LongIterator iterator = iterator(); while (iterator.hasNext()) { - if (!contains(iterator.nextLong())) { - return false; + long key = iterator.nextLong(); + if (!c.contains(key)) { + modified |= remove(key); } } + return modified; + } + + public ConcurrentLongHashSet(int concurrencyLevel) { + int numSegments = Integer.highestOneBit(concurrencyLevel) << 1; + this.segmentMask = numSegments - 1; + this.segments = new Segment[numSegments]; + for (int i = 0; i < numSegments; i++) { + segments[i] = new Segment(); + } + } + + // ------------------- Core Methods ------------------- + @Override + public boolean add(long key) { + Segment segment = getSegment(key); + segment.lock(); + try { + return segment.set.add(key); + } finally { + segment.unlock(); + } + } + + @Override + public boolean contains(long key) { + Segment segment = getSegment(key); + segment.lock(); + try { + return segment.set.contains(key); + } finally { + segment.unlock(); + } + } + + @Override + public boolean remove(long key) { + Segment segment = getSegment(key); + segment.lock(); + try { + return segment.set.remove(key); + } finally { + segment.unlock(); + } + } + + // ------------------- Bulk Operations ------------------- + @Override + public boolean containsAll(@NotNull Collection c) { + Objects.requireNonNull(c, "Collection cannot be null"); + for (Object obj : c) { + if (obj == null || !(obj instanceof Long)) return false; + if (!contains((Long) obj)) return false; + } return true; } @Override - public boolean removeAll(LongCollection c) { + public boolean addAll(@NotNull Collection c) { Objects.requireNonNull(c, "Collection cannot be null"); boolean modified = false; - LongIterator iterator = c.iterator(); - while (iterator.hasNext()) { - modified |= remove(iterator.nextLong()); + for (Long value : c) { + modified |= add(value); } return modified; } + // ------------------- Locking Helpers ------------------- + private Segment getSegment(long key) { + int hash = spreadHash(Long.hashCode(key)); + return segments[hash & segmentMask]; + } + + private static int spreadHash(int h) { + return (h ^ (h >>> 16)) & 0x7fffffff; // Avoid negative indices + } + + // ------------------- Size Stuff ------------------- @Override - public boolean retainAll(LongCollection c) { - Objects.requireNonNull(c, "Collection cannot be null"); - return backing.retainAll(c); + public int size() { + int count = 0; + for (Segment segment : segments) { + segment.lock(); + count += segment.set.size(); + segment.unlock(); + } + return count; } @Override - public boolean remove(long k) { - return backing.remove(k); + public boolean isEmpty() { + for (Segment segment : segments) { + segment.lock(); + boolean empty = segment.set.isEmpty(); + segment.unlock(); + if (!empty) return false; + } + return true; + } + + // ------------------- Cleanup ------------------- + @Override + public void clear() { + for (Segment segment : segments) { + segment.lock(); + segment.set.clear(); + segment.unlock(); + } + } + + // ------------------- Iteration ------------------- + @Override + public LongIterator iterator() { + return new CompositeLongIterator(); + } + + private class CompositeLongIterator implements LongIterator { + private int currentSegment = 0; + private LongIterator currentIterator; + + CompositeLongIterator() { + advanceSegment(); + } + + private void advanceSegment() { + while (currentSegment < segments.length) { + segments[currentSegment].lock(); + currentIterator = segments[currentSegment].set.iterator(); + if (currentIterator.hasNext()) break; + segments[currentSegment].unlock(); + currentSegment++; + } + } + + @Override + public boolean hasNext() { + if (currentIterator == null) return false; + if (currentIterator.hasNext()) return true; + segments[currentSegment].unlock(); + currentSegment++; + advanceSegment(); + return currentIterator != null && currentIterator.hasNext(); + } + + @Override + public long nextLong() { + if (!hasNext()) throw new NoSuchElementException(); + return currentIterator.nextLong(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + // ------------------- Segment (these nuts) ------------------- + private static class Segment { + final LongOpenHashSet set = new LongOpenHashSet(); + final ReentrantLock lock = new ReentrantLock(); + + void lock() { + lock.lock(); + } + + void unlock() { + lock.unlock(); + } + } + + // ignore + @Override + public long[] toLongArray() { + long[] result = new long[size()]; + int i = 0; + LongIterator it = iterator(); + while (it.hasNext()) { + result[i++] = it.nextLong(); + } + return result; + } + + @Override + public long[] toArray(long[] a) { + long[] result = toLongArray(); + if (a.length < result.length) return result; + System.arraycopy(result, 0, a, 0, result.length); + return a; } @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof LongSet that)) return false; - if (size() != that.size()) return false; - return containsAll(that); + if (!(o instanceof LongSet)) return false; + LongSet that = (LongSet) o; + return size() == that.size() && containsAll(that); } @Override public int hashCode() { - return backing.hashCode(); + int hash = 0; + LongIterator it = iterator(); + while (it.hasNext()) { + hash += Long.hashCode(it.nextLong()); + } + return hash; } - @Override - public String toString() { - return backing.toString(); - } + @Override @NotNull public Object[] toArray() { return Collections.unmodifiableSet(this).toArray(); } + @Override @NotNull public T[] toArray(@NotNull T[] a) { return Collections.unmodifiableSet(this).toArray(a); } - static class WrappingLongIterator implements LongIterator { - private final Iterator backing; - - WrappingLongIterator(Iterator backing) { - this.backing = Objects.requireNonNull(backing); - } - - @Override - public boolean hasNext() { - return backing.hasNext(); - } - - @Override - public long nextLong() { - return backing.next(); - } - - @Override - public Long next() { - return backing.next(); - } - - @Override - public void remove() { - backing.remove(); - } - } }