2014-02-01 08:07:22 +01:00
From 0db589c05b3eb0d213df3e7640818b77935c2e02 Mon Sep 17 00:00:00 2001
2014-01-28 10:32:37 +01:00
From: md_5 <git@md-5.net>
Date: Tue, 28 Jan 2014 20:32:07 +1100
2014-01-28 11:45:06 +01:00
Subject: [PATCH] Implement Threaded Bulk Chunk Compression and Caching
2014-01-28 10:32:37 +01:00
2014-01-28 23:04:51 +01:00
diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
index 9b853a9..a4c8843 100644
--- a/src/main/java/net/minecraft/server/EntityPlayer.java
+++ b/src/main/java/net/minecraft/server/EntityPlayer.java
@@ -228,6 +228,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
}
if (!arraylist.isEmpty()) {
+ java.util.Collections.sort( arraylist, new PlayerChunkMap.ChunkComparator( this ) ); // Spigot - sort a final time before sending
this.playerConnection.sendPacket(new PacketPlayOutMapChunkBulk(arraylist));
Iterator iterator2 = arraylist1.iterator();
2014-01-28 10:32:37 +01:00
diff --git a/src/main/java/net/minecraft/server/PacketPlayOutMapChunkBulk.java b/src/main/java/net/minecraft/server/PacketPlayOutMapChunkBulk.java
In short, there isn't actually anything wrong with the async chunk compressor, it just accidentally caused console logging of errors which were previously ignored.
This commit restores that behaviour
You may be asking yourself why we are completely ignoring any errors which come this far down the pipeline.
The answer is quite simple:
Mojang did it
The default Mojang pipeline doesn't have any ChannelOutboundHandlerAdapter or similar instances, and thus nothing to handle exceptionCaught
So when a channel.write() or channel.flush() fails, the error message is actually just passed straight to the future provided.
It is then subsequently discarded, the channel closed, and no one except the user was any the wiser it actually happened!
Unfortunately for us, the default exceptionCaught in this class sends a blaring warning to the server admins indicating that it couldn't send a packet to a disconnected user!
We don't care about these warnings, if we did something wrong to disconnect the user, it is already logged in the proper location, as are broken sockets
tl;dr no need to blare warnings on each write to a broken socket
2014-02-01 07:53:32 +01:00
index fc92026..484d727 100644
2014-01-28 10:32:37 +01:00
--- a/src/main/java/net/minecraft/server/PacketPlayOutMapChunkBulk.java
+++ b/src/main/java/net/minecraft/server/PacketPlayOutMapChunkBulk.java
2014-01-29 02:51:20 +01:00
@@ -12,9 +12,9 @@ public class PacketPlayOutMapChunkBulk extends Packet {
private int[] b;
2014-01-28 11:45:06 +01:00
private int[] c;
private int[] d;
- private byte[] buffer;
2014-01-29 02:51:20 +01:00
- private byte[][] inflatedBuffers;
2014-01-28 11:45:06 +01:00
- private int size;
2014-01-29 02:51:20 +01:00
+ public byte[] buffer; // Spigot
+ public byte[][] inflatedBuffers; // Spigot
2014-01-28 11:45:06 +01:00
+ public int size; // Spigot
private boolean h;
private byte[] buildBuffer = new byte[0]; // CraftBukkit - remove static
// CraftBukkit start
2014-01-28 23:04:51 +01:00
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
index ae4ca63..962e902 100644
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
@@ -279,8 +279,27 @@ public class PlayerChunkMap {
return playermanager.d;
}
+ // Spigot Start
+ public static class ChunkComparator implements java.util.Comparator<Chunk>
+ {
+
+ private final ChunkCoordComparator base;
+
+ public ChunkComparator(EntityPlayer player)
+ {
+ this.base = new ChunkCoordComparator( player );
+ }
+
+ @Override
+ public int compare(Chunk o1, Chunk o2)
+ {
+ return base.compare( o1.l(), o2.l() );
+ }
+
+ }
// CraftBukkit start - Sorter to load nearby chunks first
- private static class ChunkCoordComparator implements java.util.Comparator<ChunkCoordIntPair> {
+ public static class ChunkCoordComparator implements java.util.Comparator<ChunkCoordIntPair> {
+ // Spigot End
private int x;
private int z;
2014-01-28 10:32:37 +01:00
diff --git a/src/main/java/net/minecraft/server/ServerConnectionChannel.java b/src/main/java/net/minecraft/server/ServerConnectionChannel.java
2014-01-30 06:02:25 +01:00
index fb95be4..a382235 100644
2014-01-28 10:32:37 +01:00
--- a/src/main/java/net/minecraft/server/ServerConnectionChannel.java
+++ b/src/main/java/net/minecraft/server/ServerConnectionChannel.java
2014-01-30 06:02:25 +01:00
@@ -1,14 +1,25 @@
2014-01-28 10:43:57 +01:00
package net.minecraft.server;
+import com.google.common.util.concurrent.ThreadFactoryBuilder; // Spigot
import net.minecraft.util.io.netty.channel.Channel;
import net.minecraft.util.io.netty.channel.ChannelException;
import net.minecraft.util.io.netty.channel.ChannelInitializer;
import net.minecraft.util.io.netty.channel.ChannelOption;
import net.minecraft.util.io.netty.handler.timeout.ReadTimeoutHandler;
+// Spigot Start
+import net.minecraft.util.io.netty.util.concurrent.DefaultEventExecutorGroup;
+import net.minecraft.util.io.netty.util.concurrent.EventExecutorGroup;
+import org.spigotmc.ChunkCompressor;
+import org.spigotmc.SpigotConfig;
+// Spigot End
class ServerConnectionChannel extends ChannelInitializer {
final ServerConnection a;
+ // Spigot Start
+ private static final EventExecutorGroup threadPool = new DefaultEventExecutorGroup( SpigotConfig.compressionThreads, new ThreadFactoryBuilder().setNameFormat( "Chunk Compressor #%d" ).setDaemon( true ).build() );
+ private static final ChunkCompressor chunkCompressor = new ChunkCompressor();
+ // Spigot End
2014-01-30 06:02:25 +01:00
2014-01-28 10:43:57 +01:00
ServerConnectionChannel(ServerConnection serverconnection) {
this.a = serverconnection;
2014-01-30 06:02:25 +01:00
@@ -28,6 +39,7 @@ class ServerConnectionChannel extends ChannelInitializer {
2014-01-28 10:32:37 +01:00
}
2014-01-30 06:02:25 +01:00
channel.pipeline().addLast("timeout", new ReadTimeoutHandler(30)).addLast("legacy_query", new LegacyPingHandler(this.a)).addLast("splitter", new PacketSplitter()).addLast("decoder", new PacketDecoder()).addLast("prepender", new PacketPrepender()).addLast("encoder", new PacketEncoder());
+ channel.pipeline().addLast( threadPool, "compressor", chunkCompressor ); // Spigot
2014-01-28 10:32:37 +01:00
NetworkManager networkmanager = new NetworkManager(false);
ServerConnection.a(this.a).add(networkmanager);
diff --git a/src/main/java/org/spigotmc/ChunkCompressor.java b/src/main/java/org/spigotmc/ChunkCompressor.java
new file mode 100644
2014-02-01 08:07:22 +01:00
index 0000000..481211b
2014-01-28 10:32:37 +01:00
--- /dev/null
+++ b/src/main/java/org/spigotmc/ChunkCompressor.java
2014-02-01 08:07:22 +01:00
@@ -0,0 +1,94 @@
2014-01-28 10:32:37 +01:00
+package org.spigotmc;
+
2014-01-28 11:45:06 +01:00
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
2014-01-29 20:58:37 +01:00
+import java.util.logging.Level;
2014-01-29 02:51:20 +01:00
+import java.util.zip.CRC32;
2014-01-28 10:50:34 +01:00
+import net.minecraft.server.PacketPlayOutMapChunkBulk;
2014-01-28 10:43:57 +01:00
+import net.minecraft.util.io.netty.channel.ChannelHandler;
2014-01-28 10:32:37 +01:00
+import net.minecraft.util.io.netty.channel.ChannelHandlerContext;
+import net.minecraft.util.io.netty.channel.ChannelOutboundHandlerAdapter;
+import net.minecraft.util.io.netty.channel.ChannelPromise;
2014-01-29 20:58:37 +01:00
+import org.bukkit.Bukkit;
2014-01-28 10:32:37 +01:00
+
2014-01-28 10:43:57 +01:00
+@ChannelHandler.Sharable
2014-01-28 10:32:37 +01:00
+public class ChunkCompressor extends ChannelOutboundHandlerAdapter
+{
+
2014-01-29 02:51:20 +01:00
+ private final LinkedHashMap<Long, byte[]> cache = new LinkedHashMap<Long, byte[]>( 16, 0.75f, true ); // Defaults, order by access
2014-01-28 11:45:06 +01:00
+ private volatile int cacheSize;
+
2014-01-28 10:32:37 +01:00
+ @Override
2014-01-28 11:45:06 +01:00
+ public synchronized void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception
2014-01-28 10:32:37 +01:00
+ {
2014-01-30 06:02:25 +01:00
+ try
2014-01-28 10:32:37 +01:00
+ {
2014-01-30 06:02:25 +01:00
+ long start = System.currentTimeMillis();
+ boolean cached = false;
+ if ( msg instanceof PacketPlayOutMapChunkBulk )
2014-01-28 11:45:06 +01:00
+ {
2014-01-30 06:02:25 +01:00
+ PacketPlayOutMapChunkBulk chunk = (PacketPlayOutMapChunkBulk) msg;
2014-02-01 08:07:22 +01:00
+ // Well shit, orebfuscator is at it again
+ if ( chunk.inflatedBuffers == null )
+ {
+ Bukkit.getServer().getLogger().warning( "chunk.inflatedBuffers is null. Are you running Oreobfuscator? If so, please note that it is unsupported and will disable async compression" );
+ super.write( ctx, msg, promise );
+ }
+
2014-01-30 06:02:25 +01:00
+ // Here we assign a hash to the chunk based on its contents. CRC32 is fast and sufficient for use here.
+ CRC32 crc = new CRC32();
+ for ( byte[] c : chunk.inflatedBuffers )
+ {
+ crc.update( c );
+ }
+ long hash = crc.getValue();
2014-01-28 11:45:06 +01:00
+
2014-01-30 06:02:25 +01:00
+ byte[] deflated = cache.get( hash );
+ if ( deflated != null )
+ {
+ chunk.buffer = deflated;
+ chunk.size = deflated.length;
+ cached = true;
+ } else
2014-01-28 11:45:06 +01:00
+ {
2014-01-30 06:02:25 +01:00
+ chunk.compress(); // Compress the chunk
+ byte[] buffer = Arrays.copyOf( chunk.buffer, chunk.size ); // Resize the array to correct sizing
+
+ Iterator<byte[]> iter = cache.values().iterator(); // Grab a single iterator reference
+ // Whilst this next entry is too big for us, and we have stuff to remove
+ while ( cacheSize + buffer.length > org.spigotmc.SpigotConfig.chunkCacheBytes && iter.hasNext() )
+ {
+ cacheSize -= iter.next().length; // Update size table
+ iter.remove(); // Remove it alltogether
+ }
+ cacheSize += buffer.length; // Update size table
+ cache.put( hash, buffer ); // Pop the new one in the cache
2014-01-28 11:45:06 +01:00
+ }
2014-01-30 06:02:25 +01:00
+ // System.out.println( "Time: " + ( System.currentTimeMillis() - start ) + " " + cached + " " + cacheSize );
2014-01-28 11:45:06 +01:00
+ }
2014-01-30 06:02:25 +01:00
+ } catch ( Throwable t )
+ {
+ Bukkit.getServer().getLogger().log( Level.WARNING, "Error compressing or caching chunk", t );
2014-01-28 10:32:37 +01:00
+ }
2014-01-28 11:45:06 +01:00
+
2014-01-28 10:32:37 +01:00
+ super.write( ctx, msg, promise );
+ }
In short, there isn't actually anything wrong with the async chunk compressor, it just accidentally caused console logging of errors which were previously ignored.
This commit restores that behaviour
You may be asking yourself why we are completely ignoring any errors which come this far down the pipeline.
The answer is quite simple:
Mojang did it
The default Mojang pipeline doesn't have any ChannelOutboundHandlerAdapter or similar instances, and thus nothing to handle exceptionCaught
So when a channel.write() or channel.flush() fails, the error message is actually just passed straight to the future provided.
It is then subsequently discarded, the channel closed, and no one except the user was any the wiser it actually happened!
Unfortunately for us, the default exceptionCaught in this class sends a blaring warning to the server admins indicating that it couldn't send a packet to a disconnected user!
We don't care about these warnings, if we did something wrong to disconnect the user, it is already logged in the proper location, as are broken sockets
tl;dr no need to blare warnings on each write to a broken socket
2014-02-01 07:53:32 +01:00
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
+ {
+ // In short, there isn't actually anything wrong with the async chunk compressor, it just accidentally caused console logging of errors which were previously ignored.\
+ // This commit restores that behaviour
+
+ // You may be asking yourself why we are completely ignoring any errors which come this far down the pipeline.
+ // The answer is quite simple:
+ // Mojang did it
+ // The default Mojang pipeline doesn't have any ChannelOutboundHandlerAdapter or similar instances, and thus nothing to handle exceptionCaught
+ // So when a channel.write() or channel.flush() fails, the error message is actually just passed straight to the future provided.
+ // It is then subsequently discarded, the channel closed, and no one except the user was any the wiser it actually happened!
+ // Unfortunately for us, the default exceptionCaught in this class sends a blaring warning to the server admins indicating that it couldn't send a packet to a disconnected user!
+ // We don't care about these warnings, if we did something wrong to disconnect the user, it is already logged in the proper location, as are broken sockets
+ // tl;dr no need to blare warnings on each write to a broken socket
+ }
2014-01-28 10:32:37 +01:00
+}
diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java
In short, there isn't actually anything wrong with the async chunk compressor, it just accidentally caused console logging of errors which were previously ignored.
This commit restores that behaviour
You may be asking yourself why we are completely ignoring any errors which come this far down the pipeline.
The answer is quite simple:
Mojang did it
The default Mojang pipeline doesn't have any ChannelOutboundHandlerAdapter or similar instances, and thus nothing to handle exceptionCaught
So when a channel.write() or channel.flush() fails, the error message is actually just passed straight to the future provided.
It is then subsequently discarded, the channel closed, and no one except the user was any the wiser it actually happened!
Unfortunately for us, the default exceptionCaught in this class sends a blaring warning to the server admins indicating that it couldn't send a packet to a disconnected user!
We don't care about these warnings, if we did something wrong to disconnect the user, it is already logged in the proper location, as are broken sockets
tl;dr no need to blare warnings on each write to a broken socket
2014-02-01 07:53:32 +01:00
index 769ef2a..fb944e7 100755
2014-01-28 10:32:37 +01:00
--- a/src/main/java/org/spigotmc/SpigotConfig.java
+++ b/src/main/java/org/spigotmc/SpigotConfig.java
2014-01-28 11:45:06 +01:00
@@ -266,4 +266,16 @@ public class SpigotConfig
2014-01-28 10:32:37 +01:00
{
playerShuffle = getInt( "settings.player-shuffle", 0 );
}
+
2014-01-28 23:08:29 +01:00
+ public static int compressionThreads = 1; // Stupid unit tests
2014-01-28 11:45:06 +01:00
+ public static int chunkCacheBytes;
+ private static void chunkStuff()
2014-01-28 10:32:37 +01:00
+ {
+ compressionThreads = getInt( "settings.compression-threads", 4 );
+ Bukkit.getLogger().log( Level.INFO, "Using {0} threads for async chunk compression", compressionThreads );
2014-01-28 11:45:06 +01:00
+
+ chunkCacheBytes = getInt( "settings.compressed-chunk-cache", 64 ) << 20;
+ Bukkit.getLogger().log( Level.INFO, "Reserving {0} bytes for compressed chunk cache", chunkCacheBytes );
+
2014-01-28 10:32:37 +01:00
+ }
}
--
1.8.3.2