9
0
mirror of https://github.com/BX-Team/DivineMC.git synced 2025-12-19 14:59:25 +00:00

improve buffered

This commit is contained in:
NONPLAYT
2025-07-19 23:49:06 +03:00
parent d2465af868
commit faecae75a1
3 changed files with 264 additions and 84 deletions

View File

@@ -5,6 +5,9 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bxteam.divinemc.async.pathfinding.AsyncPathProcessor; import org.bxteam.divinemc.async.pathfinding.AsyncPathProcessor;
import org.bxteam.divinemc.async.tracking.MultithreadedTracker; import org.bxteam.divinemc.async.tracking.MultithreadedTracker;
import org.bxteam.divinemc.config.DivineConfig;
import org.bxteam.divinemc.region.EnumRegionFileExtension;
import org.bxteam.divinemc.region.type.BufferedRegionFile;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -13,6 +16,14 @@ public class ExecutorShutdown {
public static final Logger LOGGER = LogManager.getLogger(ExecutorShutdown.class.getSimpleName()); public static final Logger LOGGER = LogManager.getLogger(ExecutorShutdown.class.getSimpleName());
public static void shutdown(MinecraftServer server) { public static void shutdown(MinecraftServer server) {
if (BufferedRegionFile.flusherInitialized && DivineConfig.MiscCategory.regionFileType == EnumRegionFileExtension.B_LINEAR) {
LOGGER.info("Shutting down buffered region executors...");
try {
BufferedRegionFile.shutdown();
} catch (InterruptedException ignored) { }
}
if (server.mobSpawnExecutor != null && server.mobSpawnExecutor.thread.isAlive()) { if (server.mobSpawnExecutor != null && server.mobSpawnExecutor.thread.isAlive()) {
LOGGER.info("Shutting down mob spawn executor..."); LOGGER.info("Shutting down mob spawn executor...");

View File

@@ -1,34 +0,0 @@
package org.bxteam.divinemc.region;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
public class BufferReleaser {
private static final Method CLEANER_METHOD;
private static final Object UNSAFE;
static {
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = theUnsafe.get(null);
CLEANER_METHOD = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
} catch (Exception ex) {
throw new RuntimeException("Unsafe init failed", ex);
}
}
public static boolean clean(@NotNull ByteBuffer buffer) {
if (!buffer.isDirect()) return false;
try {
CLEANER_METHOD.invoke(UNSAFE, buffer);
return true;
} catch (Exception e) {
return false;
}
}
}

View File

@@ -1,16 +1,22 @@
package org.bxteam.divinemc.region.type; package org.bxteam.divinemc.region.type;
import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import net.jpountz.xxhash.XXHash32; import net.jpountz.xxhash.XXHash32;
import net.jpountz.xxhash.XXHashFactory; import net.jpountz.xxhash.XXHashFactory;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.ChunkPos;
import org.bxteam.divinemc.region.BufferReleaser; import org.bxteam.divinemc.region.EnumRegionFileExtension;
import org.bxteam.divinemc.region.IRegionFile; import org.bxteam.divinemc.region.IRegionFile;
import org.bxteam.divinemc.config.DivineConfig;
import org.bxteam.divinemc.util.NamedAgnosticThreadFactory;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
import java.io.*; import java.io.*;
import java.lang.invoke.VarHandle;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.nio.file.Files; import java.nio.file.Files;
@@ -19,20 +25,15 @@ import java.nio.file.StandardOpenOption;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.*;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/** /**
* A buffered region file implementation that provides efficient chunk storage and retrieval * A buffered region file implementation that provides efficient chunk storage and retrieval
* with compression, checksums, and automatic compaction capabilities. * with compression, checksums, and automatic compaction capabilities.
* *
* <p>This implementation includes:
* <ul>
* <li>Zstandard compression for chunk data</li>
* <li>XXHash32 checksums for data integrity verification</li>
* <li>Automatic file compaction when fragmentation exceeds thresholds</li>
* <li>Thread-safe operations with read-write locks</li>
* <li>Direct ByteBuffer usage for memory efficiency</li>
* </ul>
*
* <p>For conversion tools between MCA and buffered region file formats, see: * <p>For conversion tools between MCA and buffered region file formats, see:
* <a href="https://github.com/NONPLAYT/LinearRegionFileFormatTools">LinearRegionFileFormatTools</a> * <a href="https://github.com/NONPLAYT/LinearRegionFileFormatTools">LinearRegionFileFormatTools</a>
*/ */
@@ -52,10 +53,24 @@ public class BufferedRegionFile implements IRegionFile {
private byte compressionLevel = 6; private byte compressionLevel = 6;
private int xxHash32Seed = HASH_SEED; private int xxHash32Seed = HASH_SEED;
private FileChannel channel; private FileChannel channel;
private boolean closed = false;
private volatile boolean synced = true;
private volatile boolean beingSynced = false;
private volatile long lastWritten = 0L;
private static final Set<BufferedRegionFile> MANAGED_FILES = new ObjectLinkedOpenHashSet<>();
private static volatile ScheduledFuture<?> flusherChecker;
private static volatile Executor ioWorkerPool;
private static final Object FLUSHER_LOCK = new Object();
public static volatile boolean flusherInitialized = false;
private static final VarHandle SYNCED_HANDLE = ConcurrentUtil.getVarHandle(BufferedRegionFile.class, "synced", boolean.class);
private static final VarHandle BEING_SYNCED_HANDLE = ConcurrentUtil.getVarHandle(BufferedRegionFile.class, "beingSynced", boolean.class);
private static final VarHandle LAST_WRITTEN_HANDLE = ConcurrentUtil.getVarHandle(BufferedRegionFile.class, "lastWritten", long.class);
public BufferedRegionFile(Path filePath, int compressionLevel) throws IOException { public BufferedRegionFile(Path filePath, int compressionLevel) throws IOException {
this(filePath); this(filePath);
this.compressionLevel = (byte) compressionLevel; this.compressionLevel = (byte) compressionLevel;
} }
@@ -73,6 +88,179 @@ public class BufferedRegionFile implements IRegionFile {
} }
this.readHeaders(); this.readHeaders();
if (DivineConfig.MiscCategory.regionFileType == EnumRegionFileExtension.B_LINEAR) initializeFlusherIfNeeded();
addToFlusherManagement();
}
private static void initializeFlusherIfNeeded() {
if (flusherInitialized) return;
synchronized (FLUSHER_LOCK) {
if (flusherInitialized) {
return;
}
final int nIoThreads = DivineConfig.MiscCategory.linearIoThreadCount;
final long checkIntervalMs = 20;
ioWorkerPool = Executors.newFixedThreadPool(nIoThreads,
new NamedAgnosticThreadFactory<>(
"BufferedRegionFile I/O Worker",
(group, runnable, name) -> {
Thread thread = new Thread(group, runnable, name);
thread.setDaemon(true);
return thread;
},
Thread.NORM_PRIORITY
)
);
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(
new NamedAgnosticThreadFactory<>(
"BufferedRegionFile Flusher Checker",
(group, runnable, name) -> {
Thread thread = new Thread(group, runnable, name);
thread.setDaemon(true);
return thread;
},
Thread.NORM_PRIORITY
)
);
flusherChecker = scheduler.scheduleWithFixedDelay(
BufferedRegionFile::runFlusherCheck,
checkIntervalMs,
checkIntervalMs,
TimeUnit.MILLISECONDS
);
flusherInitialized = true;
}
}
private static void runFlusherCheck() {
final long currentNanos = System.nanoTime();
final BufferedRegionFile[] copied;
synchronized (MANAGED_FILES) {
copied = Arrays.copyOf(
MANAGED_FILES.toArray(new BufferedRegionFile[0]),
MANAGED_FILES.size(),
BufferedRegionFile[].class
);
}
final List<BufferedRegionFile> toRemove = new ObjectArrayList<>();
for (BufferedRegionFile file : copied) {
if (!file.softReadLock()) {
continue;
}
boolean closed;
try {
closed = file.isClosedRaw();
} finally {
file.releaseReadLock();
}
if (closed) {
toRemove.add(file);
continue;
}
if (!file.shouldSync()) {
continue;
}
final long lastWriteNanos = file.getLastWritten();
final long timeElapsed = (currentNanos - lastWriteNanos) / 1_000_000;
final long flushTimeoutMs = DivineConfig.MiscCategory.linearIoFlushDelayMs;
if (timeElapsed >= flushTimeoutMs) {
if (!file.markAsBeingSynced()) {
continue;
}
ioWorkerPool.execute(() -> {
try {
file.flush();
file.syncIfNeeded();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
synchronized (MANAGED_FILES) {
for (BufferedRegionFile file : toRemove) {
MANAGED_FILES.remove(file);
}
}
}
public static void shutdown() throws InterruptedException {
synchronized (FLUSHER_LOCK) {
if (!flusherInitialized) {
return;
}
if (flusherChecker != null) {
flusherChecker.cancel(false);
}
if (ioWorkerPool instanceof ExecutorService) {
((ExecutorService) ioWorkerPool).shutdown();
//noinspection StatementWithEmptyBody
while (!((ExecutorService) ioWorkerPool).awaitTermination(100, TimeUnit.MILLISECONDS));
}
flusherInitialized = false;
}
}
private void addToFlusherManagement() {
synchronized (MANAGED_FILES) {
MANAGED_FILES.add(this);
}
}
private void removeFromFlusherManagement() {
synchronized (MANAGED_FILES) {
MANAGED_FILES.remove(this);
}
}
public boolean softReadLock() {
return this.fileAccessLock.readLock().tryLock();
}
public void releaseReadLock() {
this.fileAccessLock.readLock().unlock();
}
public boolean isClosedRaw() {
return this.closed;
}
public boolean shouldSync() {
return !(boolean) SYNCED_HANDLE.get(this);
}
public long getLastWritten() {
return (long) LAST_WRITTEN_HANDLE.get(this);
}
public boolean markAsBeingSynced() {
return BEING_SYNCED_HANDLE.compareAndSet(this, false, true);
}
public void syncIfNeeded() throws IOException {
if (this.channel != null && this.channel.isOpen()) {
this.channel.force(true);
}
} }
private void readHeaders() throws IOException { private void readHeaders() throws IOException {
@@ -98,8 +286,6 @@ public class BufferedRegionFile implements IRegionFile {
this.currentAcquiredIndex = Math.max(this.currentAcquiredIndex, sector.offset + sector.length); this.currentAcquiredIndex = Math.max(this.currentAcquiredIndex, sector.offset + sector.length);
} }
} }
BufferReleaser.clean(buffer);
} }
private void writeHeaders() throws IOException { private void writeHeaders() throws IOException {
@@ -121,8 +307,6 @@ public class BufferedRegionFile implements IRegionFile {
while (buffer.hasRemaining()) { while (buffer.hasRemaining()) {
offset += this.channel.write(buffer, offset); offset += this.channel.write(buffer, offset);
} }
BufferReleaser.clean(buffer);
} }
private int sectorSize() { private int sectorSize() {
@@ -143,6 +327,10 @@ public class BufferedRegionFile implements IRegionFile {
} }
private void flushInternal() throws IOException { private void flushInternal() throws IOException {
if (this.closed) {
return;
}
this.writeHeaders(); this.writeHeaders();
long spareSize = this.channel.size(); long spareSize = this.channel.size();
@@ -163,6 +351,7 @@ public class BufferedRegionFile implements IRegionFile {
} }
private void closeInternal() throws IOException { private void closeInternal() throws IOException {
this.closed = true;
this.writeHeaders(); this.writeHeaders();
this.channel.force(true); this.channel.force(true);
this.compact(); this.compact();
@@ -186,35 +375,36 @@ public class BufferedRegionFile implements IRegionFile {
while (headerBuffer.hasRemaining()) { while (headerBuffer.hasRemaining()) {
offsetHeader += tempChannel.write(headerBuffer, offsetHeader); offsetHeader += tempChannel.write(headerBuffer, offsetHeader);
} }
BufferReleaser.clean(headerBuffer);
int offsetPointer = this.headerSize(); long offsetPointer = this.headerSize();
tempChannel.position(offsetPointer);
for (Sector sector : this.sectors) { for (Sector sector : this.sectors) {
if (!sector.hasData()) { if (!sector.hasData()) {
continue; continue;
} }
final ByteBuffer sectorData = sector.read(this.channel); long transferred = 0;
final int length = sectorData.remaining(); while (transferred < sector.length) {
transferred += this.channel.transferTo(
final Sector newRecalculated = new Sector(sector.index, offsetPointer, length); sector.offset + transferred,
offsetPointer += length; sector.length - transferred,
this.sectors[sector.index] = newRecalculated; tempChannel);
newRecalculated.hasData = true;
long offset = newRecalculated.offset;
while (sectorData.hasRemaining()) {
offset += tempChannel.write(sectorData, offset);
} }
BufferReleaser.clean(sectorData); final Sector newRecalculated = new Sector(sector.index, offsetPointer, sector.length);
newRecalculated.hasData = true;
offsetPointer += sector.length;
this.sectors[sector.index] = newRecalculated;
} }
tempChannel.force(true); tempChannel.force(true);
this.currentAcquiredIndex = tempChannel.size(); this.currentAcquiredIndex = offsetPointer;
} }
this.channel.close();
Files.move( Files.move(
new File(this.filePath.toString() + ".tmp").toPath(), new File(this.filePath.toString() + ".tmp").toPath(),
this.filePath, this.filePath,
@@ -242,6 +432,9 @@ public class BufferedRegionFile implements IRegionFile {
final Sector sector = this.sectors[chunkOrdinal]; final Sector sector = this.sectors[chunkOrdinal];
sector.store(chunkData, this.channel); sector.store(chunkData, this.channel);
SYNCED_HANDLE.set(this, false);
LAST_WRITTEN_HANDLE.set(this, System.nanoTime());
} }
private @Nullable ByteBuffer readChunkDataRaw(int chunkOrdinal) throws IOException { private @Nullable ByteBuffer readChunkDataRaw(int chunkOrdinal) throws IOException {
@@ -260,6 +453,9 @@ public class BufferedRegionFile implements IRegionFile {
sector.clear(); sector.clear();
this.writeHeaders(); this.writeHeaders();
SYNCED_HANDLE.set(this, false);
LAST_WRITTEN_HANDLE.set(this, System.nanoTime());
} }
private static int getChunkIndex(int x, int z) { private static int getChunkIndex(int x, int z) {
@@ -281,13 +477,12 @@ public class BufferedRegionFile implements IRegionFile {
final ByteBuffer chunkSectionBuilder = ByteBuffer.allocateDirect(compressedData.remaining() + 4 + 8 + 4); final ByteBuffer chunkSectionBuilder = ByteBuffer.allocateDirect(compressedData.remaining() + 4 + 8 + 4);
chunkSectionBuilder.putInt(data.remaining()); // Uncompressed length chunkSectionBuilder.putInt(data.remaining()); // Uncompressed length
chunkSectionBuilder.putLong(System.nanoTime()); // Timestamp chunkSectionBuilder.putLong(System.currentTimeMillis()); // Timestamp
chunkSectionBuilder.putInt(xxHash32OfData); // xxHash32 of the original data chunkSectionBuilder.putInt(xxHash32OfData); // xxHash32 of the original data
chunkSectionBuilder.put(compressedData); // Compressed data chunkSectionBuilder.put(compressedData); // Compressed data
chunkSectionBuilder.flip(); chunkSectionBuilder.flip();
this.writeChunkDataRaw(chunkIndex, chunkSectionBuilder); this.writeChunkDataRaw(chunkIndex, chunkSectionBuilder);
BufferReleaser.clean(chunkSectionBuilder);
} }
private @Nullable ByteBuffer readChunk(int x, int z) throws IOException { private @Nullable ByteBuffer readChunk(int x, int z) throws IOException {
@@ -297,17 +492,15 @@ public class BufferedRegionFile implements IRegionFile {
return null; return null;
} }
final int uncompressedLength = compressed.getInt(); // compressed length final int uncompressedLength = compressed.getInt();
final long timestamp = compressed.getLong(); // TODO use this timestamp for something? final long timestamp = compressed.getLong(); // TODO use this timestamp for something?
final int dataXXHash32 = compressed.getInt(); // XXHash32 for validation final int dataXXHash32 = compressed.getInt();
final ByteBuffer decompressed = this.decompress(this.ensureDirectBuffer(compressed), uncompressedLength); final ByteBuffer decompressed = this.decompress(this.ensureDirectBuffer(compressed), uncompressedLength);
BufferReleaser.clean(compressed);
final IOException xxHash32CheckFailedEx = this.checkXXHash32(dataXXHash32, decompressed); final IOException xxHash32CheckFailedEx = this.checkXXHash32(dataXXHash32, decompressed);
if (xxHash32CheckFailedEx != null) { if (xxHash32CheckFailedEx != null) {
throw xxHash32CheckFailedEx; // prevent from loading throw xxHash32CheckFailedEx;
} }
return decompressed; return decompressed;
@@ -437,10 +630,8 @@ public class BufferedRegionFile implements IRegionFile {
final byte[] dataBytes = new byte[data.remaining()]; final byte[] dataBytes = new byte[data.remaining()];
data.get(dataBytes); data.get(dataBytes);
BufferReleaser.clean(data);
return new DataInputStream(new ByteArrayInputStream(dataBytes)); return new DataInputStream(new ByteArrayInputStream(dataBytes));
}finally { } finally {
this.fileAccessLock.readLock().unlock(); this.fileAccessLock.readLock().unlock();
} }
} }
@@ -450,7 +641,7 @@ public class BufferedRegionFile implements IRegionFile {
this.fileAccessLock.readLock().lock(); this.fileAccessLock.readLock().lock();
try { try {
return this.hasData(getChunkIndex(pos.x, pos.z)); return this.hasData(getChunkIndex(pos.x, pos.z));
}finally { } finally {
this.fileAccessLock.readLock().unlock(); this.fileAccessLock.readLock().unlock();
} }
} }
@@ -465,7 +656,7 @@ public class BufferedRegionFile implements IRegionFile {
this.fileAccessLock.writeLock().lock(); this.fileAccessLock.writeLock().lock();
try { try {
this.clearChunkData(getChunkIndex(pos.x, pos.z)); this.clearChunkData(getChunkIndex(pos.x, pos.z));
}finally { } finally {
this.fileAccessLock.writeLock().unlock(); this.fileAccessLock.writeLock().unlock();
} }
} }
@@ -515,7 +706,6 @@ public class BufferedRegionFile implements IRegionFile {
public int getRecalculateCount() { public int getRecalculateCount() {
return this.recalculateCount.get(); return this.recalculateCount.get();
} }
// MCC end
@Override @Override
public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) { public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) {
@@ -531,8 +721,21 @@ public class BufferedRegionFile implements IRegionFile {
public void flush() throws IOException { public void flush() throws IOException {
this.fileAccessLock.writeLock().lock(); this.fileAccessLock.writeLock().lock();
try { try {
this.flushInternal(); if ((boolean) SYNCED_HANDLE.get(this)) {
}finally { return;
}
if (!BEING_SYNCED_HANDLE.compareAndSet(this, false, true)) {
return;
}
try {
this.flushInternal();
SYNCED_HANDLE.set(this, true);
} finally {
BEING_SYNCED_HANDLE.set(this, false);
}
} finally {
this.fileAccessLock.writeLock().unlock(); this.fileAccessLock.writeLock().unlock();
} }
} }
@@ -541,8 +744,9 @@ public class BufferedRegionFile implements IRegionFile {
public void close() throws IOException { public void close() throws IOException {
this.fileAccessLock.writeLock().lock(); this.fileAccessLock.writeLock().lock();
try { try {
removeFromFlusherManagement();
this.closeInternal(); this.closeInternal();
}finally { } finally {
this.fileAccessLock.writeLock().unlock(); this.fileAccessLock.writeLock().unlock();
} }
} }
@@ -577,7 +781,7 @@ public class BufferedRegionFile implements IRegionFile {
long offset = this.offset; long offset = this.offset;
while (newData.hasRemaining()) { while (newData.hasRemaining()) {
offset = channel.write(newData, offset); offset += channel.write(newData, offset);
} }
} }
@@ -611,7 +815,6 @@ public class BufferedRegionFile implements IRegionFile {
} }
static int sizeOfSingle() { static int sizeOfSingle() {
// offset length hasData
return Long.BYTES * 2 + 1; return Long.BYTES * 2 + 1;
} }
} }
@@ -630,7 +833,7 @@ public class BufferedRegionFile implements IRegionFile {
ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count);
BufferedRegionFile.this.writeChunk(this.pos.x, this.pos.z, bytebuffer); BufferedRegionFile.this.writeChunk(this.pos.x, this.pos.z, bytebuffer);
}finally { } finally {
BufferedRegionFile.this.fileAccessLock.writeLock().unlock(); BufferedRegionFile.this.fileAccessLock.writeLock().unlock();
} }
} }