Foundational work for Future Memory usage improvements

This commit doesn't do much on its own, but adds a new Java Cleaner API
that lets us hook into Garbage Collector events to reclaim pooled objects and
return them to the pool.

Adds framework for Network Packets to know when a packet has finished dispatching
to get an idea when a packet is done sending to players.

Rewrites PooledObjects impl to properly respect max pool size and remove
almost all risk of contention.

Bumps the Paper Async Task Queue to use 2 threads, and properly shuts it down on shutdown.
This commit is contained in:
Aikar 2020-05-16 21:38:19 -04:00
parent 4e7644ce04
commit 90072b811c
7 changed files with 284 additions and 163 deletions

View file

@ -1587,11 +1587,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ // apply fixes
+
+ try {
+ if (chunkData.poiData != null) {
+ chunkData.poiData = chunkData.poiData.clone(); // clone data for safety, file IO thread does not clone
+ }
+ chunkData.chunkData = chunkManager.getChunkData(this.world.getWorldProvider().getDimensionManager(),
+ chunkManager.getWorldPersistentDataSupplier(), chunkData.chunkData.clone(), chunkPos, this.world); // clone data for safety, file IO thread does not clone
+ chunkManager.getWorldPersistentDataSupplier(), chunkData.chunkData, chunkPos, this.world); // clone data for safety, file IO thread does not clone
+ } catch (final Throwable ex) {
+ PaperFileIOThread.LOGGER.error("Could not apply datafixers for chunk task: " + this.toString(), ex);
+ this.complete(ChunkLoadTask.createEmptyHolder());

View file

@ -226,7 +226,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
@@ -0,0 +0,0 @@ public final class MCUtil {
return null;

View file

@ -19,7 +19,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ <version>8.0.1</version> <!-- Paper -->
<scope>compile</scope>
</dependency>
<!-- deprecated API depend -->
<dependency>
diff --git a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java

View file

@ -2083,84 +2083,167 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@@ -0,0 +0,0 @@
+package com.destroystokyo.paper.util.pooled;
+
+import net.minecraft.server.MCUtil;
+import org.apache.commons.lang3.mutable.MutableInt;
+
+import java.util.ArrayDeque;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
+
+/**
+ * Object pooling with thread safe, low contention design. Pooled objects have no additional object overhead
+ * due to usage of ArrayDeque per insertion/removal unless a resizing is needed in the buckets.
+ * Supports up to bucket size (default 8) threads concurrently accessing if all buckets have a value.
+ * Releasing may conditionally have contention if multiple buckets have same current size, but randomization will be used.
+ *
+ * Original interface API by Spottedleaf
+ * Implementation by Aikar <aikar@aikar.co>
+ * @license MIT
+ */
+public final class PooledObjects<E> {
+
+ public static final PooledObjects<MutableInt> POOLED_MUTABLE_INTEGERS = new PooledObjects<>(MutableInt::new, 200, -1);
+ /**
+ * Wrapper for an object that will be have a cleaner registered for it, and may be automatically returned to pool.
+ */
+ public class AutoReleased {
+ private final E object;
+ private final Runnable cleaner;
+
+ public AutoReleased(E object, Runnable cleaner) {
+ this.object = object;
+ this.cleaner = cleaner;
+ }
+
+ public final E getObject() {
+ return object;
+ }
+
+ public final Runnable getCleaner() {
+ return cleaner;
+ }
+ }
+
+ public static final PooledObjects<MutableInt> POOLED_MUTABLE_INTEGERS = new PooledObjects<>(MutableInt::new, 1024, 16);
+
+ private final PooledObjectHandler<E> handler;
+ private final int maxPoolSize;
+ private final int expectingThreads;
+ private final int bucketCount;
+ private final int bucketSize;
+ private final ArrayDeque<E>[] buckets;
+ private final ReentrantLock[] locks;
+ private final AtomicLong bucketIdCounter = new AtomicLong(0);
+
+ private final IsolatedPool<E> mainPool;
+ // use these under contention
+ private final IsolatedPool<E>[] contendedPools;
+
+ public PooledObjects(final PooledObjectHandler<E> handler, final int maxPoolSize, int expectingThreads) {
+ public PooledObjects(final PooledObjectHandler<E> handler, int maxPoolSize) {
+ this(handler, maxPoolSize, 8);
+ }
+ public PooledObjects(final PooledObjectHandler<E> handler, int maxPoolSize, int bucketCount) {
+ if (handler == null) {
+ throw new NullPointerException("Handler must not be null");
+ }
+ if (maxPoolSize <= 0) {
+ throw new IllegalArgumentException("Max pool size must be greater-than 0");
+ }
+ if (expectingThreads <= 0) {
+ expectingThreads = Runtime.getRuntime().availableProcessors();
+ if (bucketCount < 1) {
+ throw new IllegalArgumentException("Bucket count must be greater-than 0");
+ }
+
+ int remainder = maxPoolSize % bucketCount;
+ if (remainder > 0) {
+ // Auto adjust up to the next bucket divisible size
+ maxPoolSize = maxPoolSize - remainder + bucketCount;
+ }
+ //noinspection unchecked
+ this.buckets = new ArrayDeque[bucketCount];
+ this.locks = new ReentrantLock[bucketCount];
+ this.bucketCount = bucketCount;
+ this.handler = handler;
+ this.maxPoolSize = maxPoolSize;
+ this.expectingThreads = expectingThreads;
+ this.mainPool = new IsolatedPool<>(handler, maxPoolSize);
+ final IsolatedPool<E>[] contendedPools = new IsolatedPool[2 * expectingThreads];
+ this.bucketSize = maxPoolSize / bucketCount;
+ for (int i = 0; i < bucketCount; i++) {
+ this.buckets[i] = new ArrayDeque<>(bucketSize / 4);
+ this.locks[i] = new ReentrantLock();
+ }
+ }
+
+ for (int i = 0; i < contendedPools.length; ++i) {
+ contendedPools[i] = new IsolatedPool<>(handler, Math.max(1, maxPoolSize / 2));
+ public AutoReleased acquireCleaner(Object holder) {
+ return acquireCleaner(holder, this::release);
+ }
+
+ public AutoReleased acquireCleaner(Object holder, Consumer<E> releaser) {
+ E resource = acquire();
+ Runnable cleaner = MCUtil.registerCleaner(holder, resource, releaser);
+ return new AutoReleased(resource, cleaner);
+ }
+
+
+ public long size() {
+ long size = 0;
+ for (int i = 0; i < bucketCount; i++) {
+ size += this.buckets[i].size();
+ }
+
+ this.contendedPools = contendedPools;
+ return size;
+ }
+
+ // Taken from
+ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
+ // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2016/06/25/fastrange.c
+ // Original license is public domain
+ public static int fastRandomBounded(final long randomInteger, final long limit) {
+ // randomInteger must be [0, pow(2, 32))
+ // limit must be [0, pow(2, 32))
+ return (int)((randomInteger * limit) >>> 32);
+ }
+
+ public E acquire() {
+ E ret;
+ PooledObjects.IsolatedPool<E> pooled = this.mainPool;
+ int lastIndex = -1;
+ while ((ret = pooled.tryAcquireUncontended()) == null) {
+ int index;
+ while (lastIndex == (index = fastRandomBounded(ThreadLocalRandom.current().nextInt() & 0xFFFFFFFFL, this.contendedPools.length)));
+ lastIndex = index;
+ pooled = this.contendedPools[index];
+ for (int base = (int) (this.bucketIdCounter.getAndIncrement() % bucketCount), i = 0; i < bucketCount; i++ ) {
+ int bucketId = (base + i) % bucketCount;
+ if (this.buckets[bucketId].isEmpty()) continue;
+ // lock will alloc an object if blocked, so spinwait instead since lock duration is super fast
+ lockBucket(bucketId);
+ E value = this.buckets[bucketId].poll();
+ this.locks[bucketId].unlock();
+ if (value != null) {
+ this.handler.onAcquire(value);
+ return value;
+ }
+ }
+ return this.handler.createNew();
+ }
+
+ return ret;
+ private void lockBucket(int bucketId) {
+ // lock will alloc an object if blocked, try to avoid unless 2 failures
+ ReentrantLock lock = this.locks[bucketId];
+ if (!lock.tryLock()) {
+ Thread.yield();
+ } else {
+ return;
+ }
+ if (!lock.tryLock()) {
+ Thread.yield();
+ lock.lock();
+ }
+ }
+
+ public void release(final E value) {
+ PooledObjects.IsolatedPool<E> pooled = this.mainPool;
+ int lastIndex = -1;
+ while (!pooled.tryReleaseUncontended(value)) {
+ int index;
+ while (lastIndex == (index = fastRandomBounded(ThreadLocalRandom.current().nextInt() & 0xFFFFFFFFL, this.contendedPools.length)));
+ lastIndex = index;
+ pooled = this.contendedPools[index];
+ }
+ int attempts = 3; // cap on contention
+ do {
+ // find least filled bucket before locking
+ int smallestIdx = -1;
+ int smallest = Integer.MAX_VALUE;
+ for (int i = 0; i < bucketCount; i++ ) {
+ ArrayDeque<E> bucket = this.buckets[i];
+ int size = bucket.size();
+ if (size < this.bucketSize && (smallestIdx == -1 || size < smallest || (size == smallest && ThreadLocalRandom.current().nextBoolean()))) {
+ smallestIdx = i;
+ smallest = size;
+ }
+ }
+ if (smallestIdx == -1) return; // Can not find a bucket to fill
+
+ lockBucket(smallestIdx);
+ ArrayDeque<E> bucket = this.buckets[smallestIdx];
+ if (bucket.size() < this.bucketSize) {
+ this.handler.onRelease(value);
+ bucket.push(value);
+ this.locks[smallestIdx].unlock();
+ return;
+ } else {
+ this.locks[smallestIdx].unlock();
+ }
+ } while (attempts-- > 0);
+ }
+
+ /** This object is restricted from interacting with any pool */
+ public static interface PooledObjectHandler<E> {
+ public interface PooledObjectHandler<E> {
+
+ /**
+ * Must return a non-null object
@ -2171,89 +2254,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+
+ default void onRelease(final E value) {}
+ }
+
+ protected static class IsolatedPool<E> {
+
+ protected final PooledObjectHandler<E> handler;
+
+ // We use arraydeque as it doesn't create garbage per element...
+ protected final ArrayDeque<E> pool;
+ protected final int maxPoolSize;
+
+ protected final ReentrantLock lock = new ReentrantLock();
+
+ public IsolatedPool(final PooledObjectHandler<E> handler, final int maxPoolSize) {
+ this.handler = handler;
+ this.pool = new ArrayDeque<>();
+ this.maxPoolSize = maxPoolSize;
+ }
+
+ protected E acquireOrCreateNoLock() {
+ E ret;
+
+ ret = this.pool.poll();
+
+ if (ret == null) {
+ ret = this.handler.createNew();
+ }
+ this.handler.onAcquire(ret);
+
+ return ret;
+ }
+
+ public E tryAcquireUncontended() {
+ if (!this.lock.tryLock()) {
+ return null;
+ }
+ try {
+ return this.acquireOrCreateNoLock();
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public E acquire() {
+ this.lock.lock();
+ try {
+ return this.acquireOrCreateNoLock();
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ protected void releaseNoLock(final E value) {
+ if (this.pool.size() >= this.maxPoolSize) {
+ this.handler.onRelease(value);
+ return; // can't accept, we're at capacity
+ }
+
+ this.pool.add(value);
+ this.handler.onRelease(value);
+ }
+
+ public boolean tryReleaseUncontended(final E value) {
+ if (!this.lock.tryLock()) {
+ return false;
+ }
+
+ try {
+ this.releaseNoLock(value);
+ } finally {
+ this.lock.unlock();
+ }
+
+ return true;
+ }
+
+ public void release(final E value) {
+ this.lock.lock();
+ try {
+ this.releaseNoLock(value);
+ } finally {
+ this.lock.unlock();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java
new file mode 100644
@ -3355,21 +3355,80 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public final class MCUtil {
+ private static final Executor asyncExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("Paper Async Task Handler Thread - %1$d").build());
+ public static final ThreadPoolExecutor asyncExecutor = new ThreadPoolExecutor(
+ 0, 2, 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<Runnable>(),
+ new ThreadFactoryBuilder().setNameFormat("Paper Async Task Handler Thread - %1$d").build()
+ );
+ public static final ThreadPoolExecutor cleanerExecutor = new ThreadPoolExecutor(
+ 1, 1, 0L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<Runnable>(),
+ new ThreadFactoryBuilder().setNameFormat("Paper Object Cleaner").build()
+ );
+
+ public static final long INVALID_CHUNK_KEY = getCoordinateKey(Integer.MAX_VALUE, Integer.MAX_VALUE);
+
+ public static void ensureTickThread(final String reason) {
+ if (MinecraftServer.getServer().serverThread != Thread.currentThread()) {
+ throw new IllegalStateException(reason);
+ }
+
+ public static Runnable once(Runnable run) {
+ AtomicBoolean ran = new AtomicBoolean(false);
+ return () -> {
+ if (ran.compareAndSet(false, true)) {
+ run.run();
+ }
+ };
+ }
+
+ private static Runnable makeCleanerCallback(Runnable run) {
+ return once(() -> cleanerExecutor.execute(run));
+ }
+
+ /**
+ * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky!
+ * @param obj
+ * @param run
+ * @return
+ */
+ public static Runnable registerCleaner(Object obj, Runnable run) {
+ // Wrap callback in its own method above or the lambda will leak object
+ Runnable cleaner = makeCleanerCallback(run);
+ co.aikar.cleaner.Cleaner.register(obj, cleaner);
+ return cleaner;
+ }
+
+ /**
+ * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky!
+ * @param obj
+ * @param list
+ * @param cleaner
+ * @param <T>
+ * @return
+ */
+ public static <T> Runnable registerListCleaner(Object obj, List<T> list, Consumer<T> cleaner) {
+ return registerCleaner(obj, () -> {
+ list.forEach(cleaner);
+ list.clear();
+ });
+ }
+
+ /**
+ * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky!
+ * @param obj
+ * @param resource
+ * @param cleaner
+ * @param <T>
+ * @return
+ */
+ public static <T> Runnable registerCleaner(Object obj, T resource, java.util.function.Consumer<T> cleaner) {
+ return registerCleaner(obj, () -> cleaner.accept(resource));
+ }
+
+ public static List<ChunkCoordIntPair> getSpiralOutChunks(BlockPosition blockposition, int radius) {
@ -3753,6 +3812,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ }
+ }
+}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
}
// Spigot start
+ MCUtil.asyncExecutor.shutdown(); // Paper
+ try { MCUtil.asyncExecutor.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS); // Paper
+ } catch (java.lang.InterruptedException ignored) {} // Paper
if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) {
LOGGER.info("Saving usercache.json");
this.getUserCache().c();
diff --git a/src/main/java/net/minecraft/server/NBTTagCompound.java b/src/main/java/net/minecraft/server/NBTTagCompound.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/NBTTagCompound.java

View file

@ -5,7 +5,6 @@ Subject: [PATCH] Optimize Network Manager and add advanced packet support
Adds ability for 1 packet to bundle other packets to follow it
Adds ability for a packet to delay sending more packets until a state is ready.
Adds ability to clean up a packet when it is finished (not sent, or finished encoding), such as freeing buffers
Removes synchronization from sending packets
Removes processing packet queue off of main thread
@ -51,6 +50,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
this.packetListener = packetlistener;
}
+ // Paper start
+ private EntityPlayer getPlayer() {
+ if (packetListener instanceof PlayerConnection) {
+ return ((PlayerConnection) packetListener).player;
+ } else {
+ return null;
+ }
+ }
+ private static class InnerUtil { // Attempt to hide these methods from ProtocolLib so it doesn't accidently pick them up.
+ private static java.util.List<Packet> buildExtraPackets(Packet packet) {
+ java.util.List<Packet> extra = packet.getExtraPackets();
@ -95,9 +101,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ // Paper start - handle oversized packets better
+ boolean connected = this.isConnected();
+ if (!connected && !preparing) {
+ packet.onPacketDone();
+ return; // Do nothing
+ }
+ packet.onPacketDispatch(getPlayer());
+ if (connected && (InnerUtil.canSendImmediate(this, packet) || (
+ MCUtil.isMainThread() && packet.isReady() && this.packetQueue.isEmpty() &&
+ (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())
@ -113,13 +119,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ } else {
+ java.util.List<NetworkManager.QueuedPacket> packets = new java.util.ArrayList<>(1 + extraPackets.size());
+ packets.add(new NetworkManager.QueuedPacket(packet, null)); // delay the future listener until the end of the extra packets
+
+ for (int i = 0, len = extraPackets.size(); i < len;) {
+ Packet extra = extraPackets.get(i);
+ boolean end = ++i == len;
+ packets.add(new NetworkManager.QueuedPacket(extra, end ? genericfuturelistener : null)); // append listener to the end
+ }
+
+ this.packetQueue.addAll(packets); // atomic
+ }
+ this.sendPacketQueue();
@ -128,6 +134,30 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
private void dispatchPacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> genericFutureListener) { this.b(packet, genericFutureListener); } // Paper - OBFHELPER
@@ -0,0 +0,0 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
if (genericfuturelistener != null) {
channelfuture.addListener(genericfuturelistener);
}
+ // Paper start
+ if (packet.hasFinishListener()) {
+ channelfuture.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(getPlayer(), channelFuture));
+ }
+ // Paper end
channelfuture.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
} else {
@@ -0,0 +0,0 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
if (genericfuturelistener != null) {
channelfuture1.addListener(genericfuturelistener);
}
+ // Paper start
+ if (packet.hasFinishListener()) {
+ channelfuture1.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(getPlayer(), channelFuture));
+ }
+ // Paper end
channelfuture1.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
});
@@ -0,0 +0,0 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
}
@ -188,7 +218,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
return this.socketAddress;
}
+ public void clearPacketQueue() { QueuedPacket packet; while ((packet = packetQueue.poll()) != null) packet.getPacket().onPacketDone(); } // Paper
+ // Paper start
+ public void clearPacketQueue() {
+ EntityPlayer player = getPlayer();
+ packetQueue.forEach(queuedPacket -> {
+ Packet<?> packet = queuedPacket.getPacket();
+ if (packet.hasFinishListener()) {
+ packet.onPacketDispatchFinish(player, null);
+ }
+ });
+ packetQueue.clear();
+ } // Paper end
public void close(IChatBaseComponent ichatbasecomponent) {
// Spigot Start
this.preparing = false;
@ -213,25 +253,23 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
void a(T t0);
// Paper start
+ default void onPacketDone() {}
+
+ /**
+ * @param player Null if not at PLAY stage yet
+ */
+ default void onPacketDispatch(@javax.annotation.Nullable EntityPlayer player) {}
+
+ /**
+ * @param player Null if not at PLAY stage yet
+ * @param future Can be null if packet was cancelled
+ */
+ default void onPacketDispatchFinish(@javax.annotation.Nullable EntityPlayer player, @javax.annotation.Nullable io.netty.channel.ChannelFuture future) {}
+ default boolean hasFinishListener() { return false; }
+ default boolean isReady() { return true; }
+ default java.util.List<Packet> getExtraPackets() { return null; }
default boolean packetTooLarge(NetworkManager manager) {
return false;
}
diff --git a/src/main/java/net/minecraft/server/PacketEncoder.java b/src/main/java/net/minecraft/server/PacketEncoder.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/PacketEncoder.java
+++ b/src/main/java/net/minecraft/server/PacketEncoder.java
@@ -0,0 +0,0 @@ public class PacketEncoder extends MessageToByteEncoder<Packet<?>> {
} else {
throw throwable;
}
- }
+ } finally { try { packet.onPacketDone(); } catch (Exception e) { e.printStackTrace(); } ; } // Paper
// Paper start
int packetLength = bytebuf.readableBytes();
diff --git a/src/main/java/net/minecraft/server/PlayerList.java b/src/main/java/net/minecraft/server/PlayerList.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/PlayerList.java

View file

@ -14,7 +14,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
// Spigot start
} catch (java.lang.InterruptedException ignored) {} // Paper
if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) {
LOGGER.info("Saving usercache.json");
- this.getUserCache().c();

View file

@ -56,6 +56,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
<scope>compile</scope>
</dependency>
@@ -0,0 +0,0 @@
<version>7.3.1</version>
<scope>compile</scope>
</dependency>
+ <dependency>
+ <!-- wrapper to use either java 8 sun cleaner or java9+ official cleaner -->
+ <groupId>co.aikar</groupId>
+ <artifactId>cleaner</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ </dependency>
<!-- deprecated API depend -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
@@ -0,0 +0,0 @@
<!-- This builds a completely 'ready to start' jar with all dependencies inside -->
<build>