diff --git a/src/main/java/li/cil/oc2/common/network/ProjectorLoadBalancer.java b/src/main/java/li/cil/oc2/common/network/ProjectorLoadBalancer.java index 0831ec03..fa3fed94 100644 --- a/src/main/java/li/cil/oc2/common/network/ProjectorLoadBalancer.java +++ b/src/main/java/li/cil/oc2/common/network/ProjectorLoadBalancer.java @@ -2,9 +2,6 @@ package li.cil.oc2.common.network; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.RemovalNotification; import li.cil.oc2.api.API; import li.cil.oc2.common.Config; import li.cil.oc2.common.blockentity.ProjectorBlockEntity; @@ -19,17 +16,16 @@ import net.minecraftforge.fml.common.Mod; import javax.annotation.Nullable; import java.nio.ByteBuffer; -import java.time.Duration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; import java.util.WeakHashMap; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; -import static java.util.Objects.requireNonNull; - /** * Mostly round-robin load balancer for allowing projectors to send data to clients. *

@@ -63,11 +59,7 @@ public final class ProjectorLoadBalancer { * but then only one keeps watching it. In that case, we want to still remove the other player * from the projector info, but keep the info for the still watching player. */ - private static final Cache PROJECTOR_INFO = CacheBuilder.newBuilder() - .weakKeys() - .expireAfterWrite(Duration.ofMillis(CACHE_EXPIRES_AFTER)) - .removalListener(ProjectorLoadBalancer::handleProjectorInfoRemoved) - .build(); + private static final Map PROJECTOR_INFO = new HashMap<>(); /** * Global byte budget for sending stuff to clients. This is filled up every tick and consumed @@ -84,16 +76,15 @@ public final class ProjectorLoadBalancer { */ @Nullable private static ProjectorInfo lastSender; + /////////////////////////////////////////////////////////////////// + /** * Updates timestamp of a player currently watching a projector. */ public static void updateWatcher(final ProjectorBlockEntity projector, final ServerPlayer player) { - try { - PROJECTOR_INFO.get(projector, () -> addProjectorInfo(projector)) - .players.put(player, System.currentTimeMillis()); - } catch (final ExecutionException e) { - throw new RuntimeException(e); - } + PROJECTOR_INFO + .computeIfAbsent(projector, ProjectorLoadBalancer::addProjectorInfo) + .handleWatchedBy(player); } /** @@ -102,7 +93,7 @@ public final class ProjectorLoadBalancer { * Ignored if there are no players watching the projector. */ public static void offerFrame(final ProjectorBlockEntity projector, final Supplier messageSupplier) { - final ProjectorInfo info = PROJECTOR_INFO.getIfPresent(projector); + final ProjectorInfo info = PROJECTOR_INFO.get(projector); if (info != null) { info.nextFrameSupplier = messageSupplier; } @@ -114,8 +105,7 @@ public final class ProjectorLoadBalancer { */ @SubscribeEvent public static void handleServerTick(final TickEvent.ServerTickEvent event) { - PROJECTOR_INFO.cleanUp(); - removeExpiredPlayers(); + updateCache(); if (BUDGET.updateAndGet(ProjectorLoadBalancer::replenishBudget) > 0) { sendNextReadyPacket(); @@ -127,9 +117,11 @@ public final class ProjectorLoadBalancer { */ @SubscribeEvent public static void handleServerStopped(final ServerStoppedEvent event) { - PROJECTOR_INFO.invalidateAll(); + PROJECTOR_INFO.clear(); } + /////////////////////////////////////////////////////////////////// + private static int getMaxBudget() { // We allow over-budgeting projectors to some degree, to allow short bursts of larger frame changes. // Otherwise, this would be divided by twenty, since we attempt to send every tick. @@ -140,6 +132,19 @@ public final class ProjectorLoadBalancer { return Math.min(getMaxBudget(), budget + Math.max(1, Config.projectorAverageMaxBytesPerSecond / 20)); } + private static void updateCache() { + final Iterator iterator = PROJECTOR_INFO.values().iterator(); + while (iterator.hasNext()) { + final ProjectorInfo info = iterator.next(); + info.removeExpiredPlayers(); + if (info.isNoLongerWatched()) { + iterator.remove(); + + removeProjectorInfo(info); + } + } + } + private static ProjectorInfo addProjectorInfo(final ProjectorBlockEntity projector) { projector.setRequiresKeyframe(); // When first watcher starts, immediately request keyframe. final ProjectorInfo info = new ProjectorInfo(projector.getBlockPos()); @@ -153,32 +158,19 @@ public final class ProjectorLoadBalancer { return info; } - private static void handleProjectorInfoRemoved(final RemovalNotification notification) { - final ProjectorInfo info = requireNonNull(notification.getValue()); - + private static void removeProjectorInfo(final ProjectorInfo info) { if (lastSender == info) { if (lastSender.next == lastSender) { - lastSender = null; // Last element in list, clear list. + // Last element in list, clear list. + lastSender = null; } else { - lastSender = info.next; // Shift current entry to next. + // Shift current entry to next. + lastSender = info.next; } } - info.remove(); } - private static void removeExpiredPlayers() { - if (lastSender == null) { - return; - } - - final ProjectorInfo start = lastSender; - do { - lastSender.removeExpiredPlayers(); - lastSender = lastSender.next; - } while (lastSender != start); - } - private static void sendNextReadyPacket() { if (lastSender == null) { return; @@ -193,6 +185,8 @@ public final class ProjectorLoadBalancer { } while (lastSender != start); } + /////////////////////////////////////////////////////////////////// + /** * Tracks info for a single projector. This class is an entry in a circular double linked list, * i.e. the last entry will always point back to the first entry, which makes looping over it @@ -225,6 +219,7 @@ public final class ProjectorLoadBalancer { * The current penalty, in the form of rounds in the round-robin to skip. */ private int skipCount; + @Nullable private Supplier nextFrameSupplier; @Nullable private Future runningEncode; @@ -252,10 +247,18 @@ public final class ProjectorLoadBalancer { next = null; } + public void handleWatchedBy(final ServerPlayer player) { + players.put(player, System.currentTimeMillis()); + } + public void removeExpiredPlayers() { players.entrySet().removeIf(entry -> System.currentTimeMillis() - entry.getValue() > CACHE_EXPIRES_AFTER); } + public boolean isNoLongerWatched() { + return players.isEmpty(); + } + public boolean sendIfReady() { if (skipCount > 0) { skipCount--;