Fix projector watcher caching.

On write didn't do what I hoped it'd do in combination with computed get.
This commit is contained in:
Florian Nücke
2022-02-06 20:33:57 +01:00
parent 6d726de6ae
commit fb8b9d4c9f

View File

@@ -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.
* <p>
@@ -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<ProjectorBlockEntity, ProjectorInfo> PROJECTOR_INFO = CacheBuilder.newBuilder()
.weakKeys()
.expireAfterWrite(Duration.ofMillis(CACHE_EXPIRES_AFTER))
.removalListener(ProjectorLoadBalancer::handleProjectorInfoRemoved)
.build();
private static final Map<ProjectorBlockEntity, ProjectorInfo> 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<ByteBuffer> 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<ProjectorInfo> 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<ProjectorBlockEntity, ProjectorInfo> 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<ByteBuffer> 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--;