From 1bc70761dc6f619e54f1c65d5fe855fbe2598061 Mon Sep 17 00:00:00 2001 From: Arbousier1 Date: Sun, 19 Jul 2026 10:07:01 +0800 Subject: [PATCH] Optimize watcher shifts and CraftEngine light batches --- .../craftengine/CraftEngineLightManager.java | 44 ++-- .../managers/TileEntityManager.java | 199 ++++++++++++++++-- .../TileEntityManagerLifecycleOrderTest.java | 55 +++++ 3 files changed, 259 insertions(+), 39 deletions(-) diff --git a/common/src/main/java/com/loohp/interactionvisualizer/integration/craftengine/CraftEngineLightManager.java b/common/src/main/java/com/loohp/interactionvisualizer/integration/craftengine/CraftEngineLightManager.java index d00643ce..1f4446c9 100644 --- a/common/src/main/java/com/loohp/interactionvisualizer/integration/craftengine/CraftEngineLightManager.java +++ b/common/src/main/java/com/loohp/interactionvisualizer/integration/craftengine/CraftEngineLightManager.java @@ -63,7 +63,8 @@ public final class CraftEngineLightManager implements ILightManager, Listener { private final Map desired = new HashMap<>(); private final Map applied = new HashMap<>(); private final Map externalBaseline = new HashMap<>(); - private final Set dirty = new HashSet<>(); + private Set dirty = new HashSet<>(); + private Set flushBatch = new HashSet<>(); private final Set waitingForChunk = new HashSet<>(); private final Set restoreOnLoad = new HashSet<>(); private final Map> trackedByChunk = new HashMap<>(); @@ -158,25 +159,30 @@ private void flush() { if (closed) { return; } - batch = Set.copyOf(dirty); - dirty.clear(); + Set spare = flushBatch; + flushBatch = dirty; + dirty = spare; + batch = flushBatch; } - for (LightKey key : batch) { - try { - apply(key); - } catch (LinkageError error) { - disableAfterLinkageFailure(error); - return; - } catch (RuntimeException exception) { - plugin.getLogger().log(Level.WARNING, - "Could not update a CraftEngine light at " + key, exception); + try { + for (LightKey key : batch) { + try { + apply(key); + } catch (LinkageError error) { + disableAfterLinkageFailure(error); + return; + } catch (RuntimeException exception) { + plugin.getLogger().log(Level.WARNING, + "Could not update a CraftEngine light at " + key, exception); + } } - } - - synchronized (lock) { - if (!dirty.isEmpty()) { - scheduleLocked(); + } finally { + synchronized (lock) { + flushBatch.clear(); + if (!closed && !dirty.isEmpty()) { + scheduleLocked(); + } } } } @@ -427,6 +433,7 @@ public void shutdown() { applied.clear(); externalBaseline.clear(); dirty.clear(); + flushBatch.clear(); waitingForChunk.clear(); restoreOnLoad.clear(); trackedByChunk.clear(); @@ -437,7 +444,7 @@ public void shutdown() { public int retainedStateCount() { synchronized (lock) { return desired.size() + applied.size() + externalBaseline.size() - + dirty.size() + waitingForChunk.size() + restoreOnLoad.size() + + dirty.size() + flushBatch.size() + waitingForChunk.size() + restoreOnLoad.size() + trackedByChunk.size() + (flushTask == null ? 0 : 1); } } @@ -449,6 +456,7 @@ private void disableAfterLinkageFailure(LinkageError error) { applied.clear(); externalBaseline.clear(); dirty.clear(); + flushBatch.clear(); waitingForChunk.clear(); restoreOnLoad.clear(); trackedByChunk.clear(); diff --git a/common/src/main/java/com/loohp/interactionvisualizer/managers/TileEntityManager.java b/common/src/main/java/com/loohp/interactionvisualizer/managers/TileEntityManager.java index 4bfe0f75..ab5415a6 100644 --- a/common/src/main/java/com/loohp/interactionvisualizer/managers/TileEntityManager.java +++ b/common/src/main/java/com/loohp/interactionvisualizer/managers/TileEntityManager.java @@ -89,6 +89,7 @@ interface LifecycleDispatcher { private static final Map> byChunk = new HashMap<>(); private static final Map lastActiveTypes = new HashMap<>(); private static final Map> watchedChunksByPlayer = new HashMap<>(); + private static final Map watchedChunkCentersByPlayer = new HashMap<>(); private static final Map watcherCounts = new HashMap<>(); private static final Set dirtyWatcherChunks = new LinkedHashSet<>(); private static final Set unloadingChunks = new LinkedHashSet<>(); @@ -106,16 +107,20 @@ public synchronized static void _init_() { } Scheduler.runTaskTimer(plugin, () -> { if (InteractionVisualizer.eventDrivenBlockUpdates) { - Set online = new LinkedHashSet<>(); for (Player player : Bukkit.getOnlinePlayers()) { - online.add(player.getUniqueId()); - updateWatchedChunks(player.getUniqueId(), getAllChunks(player.getLocation())); + updateWatchedChunks(player.getUniqueId(), player.getLocation()); } - for (UUID playerId : new LinkedHashSet<>(watchedChunksByPlayer.keySet())) { - if (!online.contains(playerId)) { - clearWatchedChunks(playerId); + Iterator>> iterator = watchedChunksByPlayer.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + Player player = Bukkit.getPlayer(entry.getKey()); + if (player == null || !player.isOnline()) { + iterator.remove(); + watchedChunkCentersByPlayer.remove(entry.getKey()); + removeWatcherCounts(entry.getValue()); } } + drainWatcherChanges(); return; } for (TileEntityType type : tileEntityTypes) { @@ -125,7 +130,7 @@ public synchronized static void _init_() { }, 0, InteractionVisualizerAPI.getGCPeriod()); for (Player player : Bukkit.getOnlinePlayers()) { if (InteractionVisualizer.eventDrivenBlockUpdates) { - updateWatchedChunks(player.getUniqueId(), getAllChunks(player.getLocation())); + updateWatchedChunks(player.getUniqueId(), player.getLocation()); } else { addTileEntities(getAllChunks(player.getLocation())); } @@ -274,6 +279,7 @@ private static void clearRuntimeState() { byChunk.clear(); lastActiveTypes.clear(); watchedChunksByPlayer.clear(); + watchedChunkCentersByPlayer.clear(); watcherCounts.clear(); dirtyWatcherChunks.clear(); unloadingChunks.clear(); @@ -283,7 +289,7 @@ private static void clearRuntimeState() { /** Number of block, chunk or viewer-index roots retained by this lifecycle. */ public synchronized static int retainedStateCount() { return active.size() + byChunk.size() + lastActiveTypes.size() - + watchedChunksByPlayer.size() + watcherCounts.size() + + watchedChunksByPlayer.size() + watchedChunkCentersByPlayer.size() + watcherCounts.size() + dirtyWatcherChunks.size() + unloadingChunks.size(); } @@ -430,18 +436,162 @@ private synchronized static void finishChunkUnload(ChunkPosition chunk) { } } - private synchronized static void updateWatchedChunks(UUID playerId, Set nextChunks) { + private synchronized static void updateWatchedChunks(UUID playerId, Location location) { + World world = location.getWorld(); + if (world == null) { + clearWatchedChunks(playerId); + return; + } + int chunkX = location.getBlockX() >> 4; + int chunkZ = location.getBlockZ() >> 4; + WatchedChunkCenter previous = watchedChunkCentersByPlayer.get(playerId); + int range = InteractionVisualizer.tileEntityCheckingRange; + if (previous != null && previous.matches(world, chunkX, chunkZ, range)) { + return; + } + + Set watched = watchedChunksByPlayer.get(playerId); + if (previous != null && watched != null && range >= 0 && previous.range() == range + && previous.worldId().equals(world.getUID()) + && Math.abs(chunkX - previous.chunkX()) <= 1 + && Math.abs(chunkZ - previous.chunkZ()) <= 1) { + shiftWatchedWindow(watchedChunksByPlayer, watcherCounts, dirtyWatcherChunks, + playerId, world, previous.chunkX(), previous.chunkZ(), chunkX, chunkZ, range); + watchedChunkCentersByPlayer.put(playerId, + new WatchedChunkCenter(world.getUID(), chunkX, chunkZ, range)); + drainWatcherChanges(); + return; + } + + watchedChunkCentersByPlayer.put(playerId, + new WatchedChunkCenter(world.getUID(), chunkX, chunkZ, range)); + replaceWatchedChunks(playerId, getAllChunks(location)); + } + + private synchronized static void replaceWatchedChunks(UUID playerId, Set nextChunks) { commitWatcherUpdate( watchedChunksByPlayer, watcherCounts, dirtyWatcherChunks, playerId, nextChunks); drainWatcherChanges(); } + private synchronized static void extendWatchedChunks(UUID playerId, ChunkPosition chunk) { + Set watched = watchedChunksByPlayer.get(playerId); + if (watched == null || !watched.add(chunk)) { + return; + } + watchedChunkCentersByPlayer.remove(playerId); + incrementWatcherCount(watcherCounts, dirtyWatcherChunks, chunk); + drainWatcherChanges(); + } + private synchronized static void clearWatchedChunks(UUID playerId) { + watchedChunkCentersByPlayer.remove(playerId); if (!watchedChunksByPlayer.containsKey(playerId)) { return; } - updateWatchedChunks(playerId, Set.of()); + replaceWatchedChunks(playerId, Set.of()); + } + + static

int shiftWatchedWindow( + Map> watchedByPlayer, + Map counts, Set dirty, + P playerId, World world, + int previousChunkX, int previousChunkZ, + int nextChunkX, int nextChunkZ, int range) { + Set watched = watchedByPlayer.get(playerId); + if (watched == null) { + return 0; + } + + UUID worldId = world.getUID(); + int minX = nextChunkX - range; + int maxX = nextChunkX + range; + int minZ = nextChunkZ - range; + int maxZ = nextChunkZ + range; + Iterator iterator = watched.iterator(); + while (iterator.hasNext()) { + ChunkPosition chunk = iterator.next(); + if (!worldId.equals(chunk.getWorldUID()) || chunk.getChunkX() < minX || chunk.getChunkX() > maxX + || chunk.getChunkZ() < minZ || chunk.getChunkZ() > maxZ) { + iterator.remove(); + decrementWatcherCount(counts, dirty, chunk); + } + } + + int candidates = 0; + if (nextChunkX > previousChunkX) { + candidates += addWatcherStripe(watched, counts, dirty, world, + previousChunkX + range + 1, nextChunkX + range, minZ, maxZ, true); + } else if (nextChunkX < previousChunkX) { + candidates += addWatcherStripe(watched, counts, dirty, world, + nextChunkX - range, previousChunkX - range - 1, minZ, maxZ, true); + } + int zCrossStart = minX; + int zCrossEnd = maxX; + if (nextChunkX > previousChunkX) { + zCrossEnd--; + } else if (nextChunkX < previousChunkX) { + zCrossStart++; + } + if (nextChunkZ > previousChunkZ) { + candidates += addWatcherStripe(watched, counts, dirty, world, + previousChunkZ + range + 1, nextChunkZ + range, + zCrossStart, zCrossEnd, false); + } else if (nextChunkZ < previousChunkZ) { + candidates += addWatcherStripe(watched, counts, dirty, world, + nextChunkZ - range, previousChunkZ - range - 1, + zCrossStart, zCrossEnd, false); + } + return candidates; + } + + private static int addWatcherStripe( + Set watched, Map counts, + Set dirty, World world, + int stripeStart, int stripeEnd, int crossStart, int crossEnd, + boolean xStripe) { + int candidates = 0; + for (int stripe = stripeStart; stripe <= stripeEnd; stripe++) { + for (int cross = crossStart; cross <= crossEnd; cross++) { + candidates++; + ChunkPosition chunk = xStripe + ? new ChunkPosition(world, stripe, cross) + : new ChunkPosition(world, cross, stripe); + if (watched.add(chunk)) { + incrementWatcherCount(counts, dirty, chunk); + } + } + } + return candidates; + } + + private static void removeWatcherCounts(Collection chunks) { + for (ChunkPosition chunk : chunks) { + decrementWatcherCount(watcherCounts, dirtyWatcherChunks, chunk); + } + } + + private static void incrementWatcherCount( + Map counts, Set dirty, + ChunkPosition chunk) { + int count = counts.getOrDefault(chunk, 0); + counts.put(chunk, count + 1); + if (count == 0) { + dirty.add(chunk); + } + } + + private static void decrementWatcherCount( + Map counts, Set dirty, + ChunkPosition chunk) { + int count = counts.getOrDefault(chunk, 0); + if (count <= 1) { + counts.remove(chunk); + dirty.add(chunk); + } else { + counts.put(chunk, count - 1); + } } static void commitWatcherUpdate( @@ -524,7 +674,7 @@ private static void callDeactivatedEvent(Block block, TileEntityType type) { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onJoin(PlayerJoinEvent event) { if (InteractionVisualizer.eventDrivenBlockUpdates) { - updateWatchedChunks(event.getPlayer().getUniqueId(), getAllChunks(event.getPlayer().getLocation())); + updateWatchedChunks(event.getPlayer().getUniqueId(), event.getPlayer().getLocation()); } else { addTileEntities(getAllChunks(event.getPlayer().getLocation())); } @@ -538,13 +688,13 @@ public void onQuit(PlayerQuitEvent event) { public void onRespawn(PlayerRespawnEvent event) { if (InteractionVisualizer.eventDrivenBlockUpdates) { - updateWatchedChunks(event.getPlayer().getUniqueId(), getAllChunks(event.getRespawnLocation())); + updateWatchedChunks(event.getPlayer().getUniqueId(), event.getRespawnLocation()); } } public void onChangedWorld(PlayerChangedWorldEvent event) { if (InteractionVisualizer.eventDrivenBlockUpdates) { - updateWatchedChunks(event.getPlayer().getUniqueId(), getAllChunks(event.getPlayer().getLocation())); + updateWatchedChunks(event.getPlayer().getUniqueId(), event.getPlayer().getLocation()); } } @@ -555,9 +705,9 @@ public void onInteract(PlayerInteractEvent event) { TileEntityType type = TileEntity.getTileEntityType(block.getType()); if (type != null) { if (InteractionVisualizer.eventDrivenBlockUpdates) { - Set chunks = getAllChunks(event.getPlayer().getLocation()); - chunks.add(getChunk(block.getLocation())); - updateWatchedChunks(event.getPlayer().getUniqueId(), chunks); + UUID playerId = event.getPlayer().getUniqueId(); + updateWatchedChunks(playerId, event.getPlayer().getLocation()); + extendWatchedChunks(playerId, getChunk(block.getLocation())); } if (!active.get(type).contains(block)) { addTileEntities(getChunk(block.getLocation())); @@ -572,7 +722,7 @@ public void onTeleport(PlayerTeleportEvent event) { Location to = event.getTo(); if (!from.getWorld().equals(to.getWorld()) || from.getBlockX() >> 4 != to.getBlockX() >> 4 || from.getBlockZ() >> 4 != to.getBlockZ() >> 4) { if (InteractionVisualizer.eventDrivenBlockUpdates) { - updateWatchedChunks(event.getPlayer().getUniqueId(), getAllChunks(to)); + updateWatchedChunks(event.getPlayer().getUniqueId(), to); } else { addTileEntities(getAllChunks(to)); } @@ -585,14 +735,14 @@ public void onPlayerMove(PlayerMoveEvent event) { Location to = event.getTo(); if (!from.getWorld().equals(to.getWorld())) { if (InteractionVisualizer.eventDrivenBlockUpdates) { - updateWatchedChunks(event.getPlayer().getUniqueId(), getAllChunks(to)); + updateWatchedChunks(event.getPlayer().getUniqueId(), to); } else { addTileEntities(getAllChunks(to)); } } else if (from.getBlockX() >> 4 != to.getBlockX() >> 4 || from.getBlockZ() >> 4 != to.getBlockZ() >> 4) { if (!isMovingTooFast(event.getPlayer(), from, to)) { if (InteractionVisualizer.eventDrivenBlockUpdates) { - updateWatchedChunks(event.getPlayer().getUniqueId(), getAllChunks(to)); + updateWatchedChunks(event.getPlayer().getUniqueId(), to); } else { addTileEntities(getAllChunks(to)); } @@ -611,10 +761,9 @@ public void onVehicleMove(VehicleMoveEvent event) { if (InteractionVisualizer.eventDrivenBlockUpdates) { boolean movingTooFast = !changedWorld && isMovingTooFast(null, from, to); if (!movingTooFast) { - Set chunks = getAllChunks(to); for (org.bukkit.entity.Entity passenger : event.getVehicle().getPassengers()) { if (passenger instanceof Player player) { - updateWatchedChunks(player.getUniqueId(), chunks); + updateWatchedChunks(player.getUniqueId(), to); } } } @@ -644,6 +793,14 @@ public void onChunkUnload(ChunkUnloadEvent event) { Scheduler.runTask(plugin, () -> finishChunkUnload(chunk)); } + private record WatchedChunkCenter(UUID worldId, int chunkX, int chunkZ, int range) { + + private boolean matches(World world, int x, int z, int expectedRange) { + return worldId.equals(world.getUID()) && chunkX == x && chunkZ == z + && range == expectedRange; + } + } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBreakBlock(BlockBreakEvent event) { if (TileEntity.isTileEntityType(event.getBlock().getType())) { diff --git a/common/src/test/java/com/loohp/interactionvisualizer/managers/TileEntityManagerLifecycleOrderTest.java b/common/src/test/java/com/loohp/interactionvisualizer/managers/TileEntityManagerLifecycleOrderTest.java index c40dd022..2a447bda 100644 --- a/common/src/test/java/com/loohp/interactionvisualizer/managers/TileEntityManagerLifecycleOrderTest.java +++ b/common/src/test/java/com/loohp/interactionvisualizer/managers/TileEntityManagerLifecycleOrderTest.java @@ -11,9 +11,12 @@ package com.loohp.interactionvisualizer.managers; +import com.loohp.interactionvisualizer.objectholders.ChunkPosition; import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType; +import org.bukkit.World; import org.junit.jupiter.api.Test; +import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; @@ -22,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -283,6 +287,33 @@ void reentrantWatcherUpdateCommitsCountsBeforeAnyLifecycleSideEffects() { assertEquals(Set.of("a", "d", "b", "c"), dirty); } + @Test + void adjacentWatcherShiftMatchesAFullWindowReplacement() { + World world = world(UUID.randomUUID()); + Set previous = chunkWindow(world, 10, -4, 1); + Map> watched = new HashMap<>(); + watched.put("player", new LinkedHashSet<>(previous)); + Map counts = new HashMap<>(); + previous.forEach(chunk -> counts.put(chunk, 1)); + Set dirty = new LinkedHashSet<>(); + + int candidates = TileEntityManager.shiftWatchedWindow( + watched, counts, dirty, "player", world, + 10, -4, 11, -3, 1); + + Set expected = chunkWindow(world, 11, -3, 1); + Set changed = new HashSet<>(previous); + changed.removeAll(expected); + Set entered = new HashSet<>(expected); + entered.removeAll(previous); + changed.addAll(entered); + assertEquals(expected, watched.get("player")); + assertEquals(expected, counts.keySet()); + assertTrue(counts.values().stream().allMatch(count -> count == 1)); + assertEquals(changed, dirty); + assertEquals(5, candidates); + } + @Test void nestedSameChunkScanCannotBeOverwrittenByOuterStaleType() { Map> index = new HashMap<>(); @@ -371,4 +402,28 @@ private static Map> emptyActiveSets() { } return active; } + + private static Set chunkWindow(World world, int centerX, int centerZ, int range) { + Set chunks = new LinkedHashSet<>(); + for (int z = centerZ - range; z <= centerZ + range; z++) { + for (int x = centerX - range; x <= centerX + range; x++) { + chunks.add(new ChunkPosition(world, x, z)); + } + } + return chunks; + } + + private static World world(UUID id) { + return (World) Proxy.newProxyInstance( + World.class.getClassLoader(), new Class[]{World.class}, + (proxy, method, args) -> { + if (method.getName().equals("getUID")) { + return id; + } + if (method.getName().equals("toString")) { + return "World[" + id + "]"; + } + throw new UnsupportedOperationException(method.toString()); + }); + } }