一些优化
Some checks failed
check-does-build / build (push) Failing after 11s

This commit is contained in:
2026-01-12 00:10:10 +08:00
parent 4c0399ca40
commit 6e065e4df4
3 changed files with 70 additions and 56 deletions

View File

@@ -18,7 +18,9 @@ import java.util.concurrent.ConcurrentLinkedQueue;
public class DirtyUpdateService {
private final VoxyServerInstance instance;
private final ConcurrentLinkedQueue<DirtySection> dirtyQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentHashMap<WorldIdentifier, List<DirtySection>> batchedUpdates = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, PendingDirty> pendingSections = new ConcurrentHashMap<>();
private static final int MAX_PROCESS_PER_TICK = 1000;
private static final long MERGE_WINDOW_MILLIS = 200;
public DirtyUpdateService(VoxyServerInstance instance) {
this.instance = instance;
@@ -26,13 +28,46 @@ public class DirtyUpdateService {
private record DirtySection(WorldEngine world, WorldSection section, int updateFlags, int neighborMsk) {}
private static final class PendingDirty {
final WorldEngine world;
final WorldSection section;
volatile int updateFlags;
volatile int neighborMsk;
volatile long lastDirtyTime;
PendingDirty(WorldEngine world, WorldSection section, int updateFlags, int neighborMsk, long time) {
this.world = world;
this.section = section;
this.updateFlags = updateFlags;
this.neighborMsk = neighborMsk;
this.lastDirtyTime = time;
}
void merge(int flags, int neighbor, long time) {
this.updateFlags |= flags;
this.neighborMsk |= neighbor;
this.lastDirtyTime = time;
}
}
public void onSectionDirty(WorldEngine world, WorldSection section, int updateFlags, int neighborMsk) {
// Filter out sections that are not important for clients or debounce
if ((updateFlags & WorldEngine.UPDATE_TYPE_BLOCK_BIT) != 0) {
// We need to keep the section alive until we process it
long key = section.key;
long now = System.currentTimeMillis();
PendingDirty existing = this.pendingSections.get(key);
if (existing == null) {
section.acquire();
PendingDirty created = new PendingDirty(world, section, updateFlags, neighborMsk, now);
existing = this.pendingSections.putIfAbsent(key, created);
if (existing == null) {
this.dirtyQueue.add(new DirtySection(world, section, updateFlags, neighborMsk));
// Logger.info("Section dirty: " + section.lvl + " " + section.x + " " + section.y + " " + section.z);
} else {
existing.merge(updateFlags, neighborMsk, now);
section.release();
}
} else {
existing.merge(updateFlags, neighborMsk, now);
}
int lvl = section.lvl;
int sx = section.x;
int sz = section.z;
@@ -48,13 +83,27 @@ public class DirtyUpdateService {
}
public void tick() {
// Process queue
DirtySection dirty;
int processed = 0;
// Logger.info("Dirty Queue Size: " + dirtyQueue.size());
while ((dirty = this.dirtyQueue.poll()) != null && processed++ < 1000) {
while ((dirty = this.dirtyQueue.poll()) != null && processed < MAX_PROCESS_PER_TICK) {
long key = dirty.section.key;
PendingDirty pending = this.pendingSections.get(key);
if (pending != null) {
long now = System.currentTimeMillis();
if (now - pending.lastDirtyTime < MERGE_WINDOW_MILLIS) {
this.dirtyQueue.add(dirty);
try {
Thread.sleep(1);
} catch (InterruptedException ignored) {
}
continue;
}
dirty = new DirtySection(pending.world, pending.section, pending.updateFlags, pending.neighborMsk);
this.pendingSections.remove(key);
}
processDirty(dirty);
dirty.section.release();
processed++;
}
}
@@ -121,6 +170,9 @@ public class DirtyUpdateService {
if (distSq > standardViewDist * standardViewDist && distSq < voxyDistance * voxyDistance) {
int absX = (sx << (lvl + 1)) | dx;
int absZ = (sz << (lvl + 1)) | dz;
if (!matchLevel.getChunkSource().hasChunk(absX, absZ)) {
continue;
}
cols.add(new long[]{absX, absZ, Double.doubleToRawLongBits(distSq)});
}
}
@@ -140,7 +192,9 @@ public class DirtyUpdateService {
var voxelized = WorldSectionToVoxelizedConverter.convert(dirty.section, lvl, absX, absY, absZ);
if (voxelized.lvl0NonAirCount > 0) {
var payload = VoxyNetwork.LodUpdatePayload.create(voxelized, dirty.world.getMapper());
server.execute(() -> {
ServerPlayNetworking.send(player, payload);
});
perPlayerBudget--;
}
}

View File

@@ -77,7 +77,7 @@ public class PlayerLodTracker {
WorldEngine engine = this.instance.getNullable(wi);
if (engine == null) return;
int standardViewDist = level.getServer().getPlayerList().getViewDistance(); // Chunks
int standardViewDist = level.getServer().getPlayerList().getViewDistance();
// Use client-negotiated view distance if available (via ConfigSync or just assume Voxy Config)
// Ideally the client sends its requested Voxy distance.
@@ -90,7 +90,6 @@ public class PlayerLodTracker {
java.util.concurrent.atomic.AtomicInteger budget = new java.util.concurrent.atomic.AtomicInteger(50);
state.tracker.process(20, (x, z) -> {
// On Add (Load)
double dx = x - state.lastX;
double dz = z - state.lastZ;
double distSq = dx*dx + dz*dz;
@@ -118,7 +117,6 @@ public class PlayerLodTracker {
// defer processing to global prioritized loop
}
}, (x, z) -> {
// On Remove (Unload)
long key = (((long)x) << 32) ^ (z & 0xffffffffL);
state.sentColumns.remove(key);
});
@@ -156,41 +154,7 @@ public class PlayerLodTracker {
}
private void sendLodSection(ServerPlayer player, WorldEngine engine, int x, int z, int y) {
// Decide LOD level based on distance?
// For now, let's just send LOD 0 if it exists.
// Optimization: Distant things can be higher LOD.
// Simple heuristic:
// 0-64 chunks (1024 blocks): L0
// >64 chunks: L1?
// Actually, Voxy client requests specific LODs usually.
// But since we are pushing, we should push what makes sense.
// Sending L0 for everything is safe but bandwidth heavy.
// Let's stick to L0 for now as a proof of concept to fix the "blank world" issue.
int lvl = 0;
// We need to acquire the section from storage.
// Warning: acquiring on main thread might lag if not in cache.
// We should probably offload this IO?
// WorldEngine.acquireIfExists loads from disk if not in memory?
// acquireIfExists(..., true) means "only return if loaded in memory"?
// No, check WorldEngine code.
// acquireIfExists(..., true) -> sectionTracker.acquire(..., true) -> "if (allowLoad) ..."
// It seems `acquireIfExists` might still load?
// Let's look at WorldEngine.java again.
// acquireIfExists calls acquire(..., true).
// ActiveSectionTracker.acquire(..., boolean load)
// If load is true, it loads.
// We want to load if it exists in DB, but not generate if missing?
// Voxy doesn't really "generate" on demand in the same way, it ingests.
// If it's not in DB, it's empty/air.
// We should use `engine.acquire` but we need to be careful about blocking main thread.
// Ideally we schedule a task on the Voxy thread pool to fetch and send.
this.instance.getThreadPool().execute(() -> {
try {
var sec = engine.acquire(lvl, x >> (lvl + 1), y >> (lvl + 1), z >> (lvl + 1));
@@ -199,7 +163,9 @@ public class PlayerLodTracker {
var voxelized = WorldSectionToVoxelizedConverter.convert(sec, lvl, x, y, z);
if (voxelized.lvl0NonAirCount > 0) {
var payload = VoxyNetwork.LodUpdatePayload.create(voxelized, engine.getMapper());
this.instance.getServer().execute(() -> {
ServerPlayNetworking.send(player, payload);
});
}
} finally {
sec.release();

View File

@@ -40,12 +40,6 @@ public class VoxyServerInstance extends VoxyInstance {
me.cortex.voxy.common.world.WorldSection.setArrayReuseCacheTarget(VoxyServerConfig.CONFIG.reuseCacheMax);
this.threadPool.serviceManager.createServiceNoCleanup(() -> me.cortex.voxy.common.world.WorldSection::trimReuseCacheToTarget, 60000, "ReuseCacheTrim");
// Start a service to tick the dirty service
this.threadPool.serviceManager.createServiceNoCleanup(() -> this.dirtyUpdateService::tick, VoxyServerConfig.CONFIG.dirtyUpdateDelay * 50, "DirtyUpdateService");
// Start service for player tracker (run every tick or so)
this.threadPool.serviceManager.createServiceNoCleanup(() -> this.playerLodTracker::tick, 50, "PlayerLodTracker");
// Register join handler to sync config and init tracker
net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.JOIN.register((handler, sender, s) -> {
var payload = new me.cortex.voxy.common.network.VoxyNetwork.ConfigSyncPayload(VoxyServerConfig.CONFIG.viewDistance);