diff --git a/Spigot-API-Patches/Timings-v2.patch b/Spigot-API-Patches/Timings-v2.patch
index 6c81ecccf7..b6318bfc09 100644
--- a/Spigot-API-Patches/Timings-v2.patch
+++ b/Spigot-API-Patches/Timings-v2.patch
@@ -1184,7 +1184,7 @@ index 00000000..623dda49
 +}
 diff --git a/src/main/java/co/aikar/timings/Timings.java b/src/main/java/co/aikar/timings/Timings.java
 new file mode 100644
-index 00000000..483e3669
+index 00000000..0571c9e7
 --- /dev/null
 +++ b/src/main/java/co/aikar/timings/Timings.java
 @@ -0,0 +0,0 @@
@@ -1215,6 +1215,7 @@ index 00000000..483e3669
 +
 +import com.google.common.base.Preconditions;
 +import com.google.common.collect.EvictingQueue;
++import org.apache.commons.lang.Validate;
 +import org.bukkit.Bukkit;
 +import org.bukkit.command.CommandSender;
 +import org.bukkit.plugin.Plugin;
@@ -1431,7 +1432,16 @@ index 00000000..483e3669
 +        if (sender == null) {
 +            sender = Bukkit.getConsoleSender();
 +        }
-+        // Schedule report for end of tick
++        TimingsExport.requestingReport.add(sender);
++    }
++
++    /**
++     * Generates a report and sends it to the specified listener.
++     * Use with {@link org.bukkit.command.BufferedCommandSender} to get full response when done!
++     * @param sender The listener to send responses too.
++     */
++    public static void generateReport(TimingsReportListener sender) {
++        Validate.notNull(sender);
 +        TimingsExport.requestingReport.add(sender);
 +    }
 +
@@ -1589,7 +1599,7 @@ index 00000000..56b10e89
 +}
 diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java
 new file mode 100644
-index 00000000..9dd36419
+index 00000000..23a3daa8
 --- /dev/null
 +++ b/src/main/java/co/aikar/timings/TimingsExport.java
 @@ -0,0 +0,0 @@
@@ -1618,7 +1628,6 @@ index 00000000..9dd36419
 + */
 +package co.aikar.timings;
 +
-+import com.google.common.base.Function;
 +import com.google.common.collect.Lists;
 +import com.google.common.collect.Sets;
 +import org.apache.commons.lang.StringUtils;
@@ -1626,13 +1635,9 @@ index 00000000..9dd36419
 +import org.bukkit.ChatColor;
 +import org.bukkit.Material;
 +import org.bukkit.command.CommandSender;
-+import org.bukkit.command.ConsoleCommandSender;
-+import org.bukkit.command.MultiMessageCommandSender;
-+import org.bukkit.command.RemoteConsoleCommandSender;
 +import org.bukkit.configuration.ConfigurationSection;
 +import org.bukkit.configuration.MemorySection;
 +import org.bukkit.entity.EntityType;
-+import org.bukkit.plugin.Plugin;
 +import org.json.simple.JSONObject;
 +import org.json.simple.JSONValue;
 +
@@ -1640,7 +1645,6 @@ index 00000000..9dd36419
 +import java.io.IOException;
 +import java.io.InputStream;
 +import java.io.OutputStream;
-+import java.lang.management.GarbageCollectorMXBean;
 +import java.lang.management.ManagementFactory;
 +import java.lang.management.RuntimeMXBean;
 +import java.net.HttpURLConnection;
@@ -1653,20 +1657,25 @@ index 00000000..9dd36419
 +import java.util.zip.GZIPOutputStream;
 +
 +import static co.aikar.timings.TimingsManager.HISTORY;
-+import static co.aikar.util.JSONUtil.*;
++import static co.aikar.util.JSONUtil.appendObjectData;
++import static co.aikar.util.JSONUtil.createObject;
++import static co.aikar.util.JSONUtil.pair;
++import static co.aikar.util.JSONUtil.toArray;
++import static co.aikar.util.JSONUtil.toArrayMapper;
++import static co.aikar.util.JSONUtil.toObjectMapper;
 +
 +@SuppressWarnings({"rawtypes", "SuppressionAnnotation"})
 +class TimingsExport extends Thread {
 +
-+    private final CommandSender sender;
++    private final TimingsReportListener listeners;
 +    private final Map out;
 +    private final TimingHistory[] history;
 +    private static long lastReport = 0;
 +    final static List<CommandSender> requestingReport = Lists.newArrayList();
 +
-+    private TimingsExport(CommandSender sender, Map out, TimingHistory[] history) {
++    private TimingsExport(TimingsReportListener listeners, Map out, TimingHistory[] history) {
 +        super("Timings paste thread");
-+        this.sender = sender;
++        this.listeners = listeners;
 +        this.out = out;
 +        this.history = history;
 +    }
@@ -1678,19 +1687,24 @@ index 00000000..9dd36419
 +        if (requestingReport.isEmpty()) {
 +            return;
 +        }
-+        CommandSender sender = new MultiMessageCommandSender(requestingReport);
++        TimingsReportListener listeners = new TimingsReportListener(requestingReport);
++        listeners.addConsoleIfNeeded();
++
 +        requestingReport.clear();
 +        long now = System.currentTimeMillis();
 +        final long lastReportDiff = now - lastReport;
 +        if (lastReportDiff < 60000) {
-+            sender.sendMessage(ChatColor.RED + "Please wait at least 1 minute in between Timings reports. (" + (int)((60000 - lastReportDiff) / 1000) + " seconds)");
++            listeners.sendMessage(ChatColor.RED + "Please wait at least 1 minute in between Timings reports. (" + (int)((60000 - lastReportDiff) / 1000) + " seconds)");
++            listeners.done();
 +            return;
 +        }
 +        final long lastStartDiff = now - TimingsManager.timingStart;
 +        if (lastStartDiff < 180000) {
-+            sender.sendMessage(ChatColor.RED + "Please wait at least 3 minutes before generating a Timings report. Unlike Timings v1, v2 benefits from longer timings and is not as useful with short timings. (" + (int)((180000 - lastStartDiff) / 1000) + " seconds)");
++            listeners.sendMessage(ChatColor.RED + "Please wait at least 3 minutes before generating a Timings report. Unlike Timings v1, v2 benefits from longer timings and is not as useful with short timings. (" + (int)((180000 - lastStartDiff) / 1000) + " seconds)");
++            listeners.done();
 +            return;
 +        }
++        listeners.sendMessage(ChatColor.GREEN + "Preparing Timings Report...");
 +        lastReport = now;
 +        Map parent = createObject(
 +            // Get some basic system details about the server
@@ -1722,12 +1736,7 @@ index 00000000..9dd36419
 +                pair("cpu", runtime.availableProcessors()),
 +                pair("runtime", ManagementFactory.getRuntimeMXBean().getUptime()),
 +                pair("flags", StringUtils.join(runtimeBean.getInputArguments(), " ")),
-+                pair("gc", toObjectMapper(ManagementFactory.getGarbageCollectorMXBeans(), new Function<GarbageCollectorMXBean, JSONPair>() {
-+                    @Override
-+                    public JSONPair apply(GarbageCollectorMXBean input) {
-+                        return pair(input.getName(), toArray(input.getCollectionCount(), input.getCollectionTime()));
-+                    }
-+                }))
++                pair("gc", toObjectMapper(ManagementFactory.getGarbageCollectorMXBeans(), input -> pair(input.getName(), toArray(input.getCollectionCount(), input.getCollectionTime()))))
 +            )
 +        );
 +
@@ -1763,49 +1772,24 @@ index 00000000..9dd36419
 +
 +        parent.put("idmap", createObject(
 +            pair("groups", toObjectMapper(
-+                TimingIdentifier.GROUP_MAP.values(), new Function<TimingIdentifier.TimingGroup, JSONPair>() {
-+                @Override
-+                public JSONPair apply(TimingIdentifier.TimingGroup group) {
-+                    return pair(group.id, group.name);
-+                }
-+            })),
++                TimingIdentifier.GROUP_MAP.values(), group -> pair(group.id, group.name))),
 +            pair("handlers", handlers),
-+            pair("worlds", toObjectMapper(TimingHistory.worldMap.entrySet(), new Function<Map.Entry<String, Integer>, JSONPair>() {
-+                    @Override
-+                    public JSONPair apply(Map.Entry<String, Integer> input) {
-+                        return pair(input.getValue(), input.getKey());
-+                    }
-+                })),
++            pair("worlds", toObjectMapper(TimingHistory.worldMap.entrySet(), input -> pair(input.getValue(), input.getKey()))),
 +            pair("tileentity",
-+                toObjectMapper(tileEntityTypeSet, new Function<Material, JSONPair>() {
-+                    @Override
-+                    public JSONPair apply(Material input) {
-+                        return pair(input.getId(), input.name());
-+                    }
-+                })),
++                toObjectMapper(tileEntityTypeSet, input -> pair(input.getId(), input.name()))),
 +            pair("entity",
-+                toObjectMapper(entityTypeSet, new Function<EntityType, JSONPair>() {
-+                    @Override
-+                    public JSONPair apply(EntityType input) {
-+                        return pair(input.getTypeId(), input.name());
-+                    }
-+                }))
++                toObjectMapper(entityTypeSet, input -> pair(input.getTypeId(), input.name())))
 +        ));
 +
 +        // Information about loaded plugins
 +
 +        parent.put("plugins", toObjectMapper(Bukkit.getPluginManager().getPlugins(),
-+            new Function<Plugin, JSONPair>() {
-+                @Override
-+                public JSONPair apply(Plugin plugin) {
-+                    return pair(plugin.getName(), createObject(
-+                        pair("version", plugin.getDescription().getVersion()),
-+                        pair("description", String.valueOf(plugin.getDescription().getDescription()).trim()),
-+                        pair("website", plugin.getDescription().getWebsite()),
-+                        pair("authors", StringUtils.join(plugin.getDescription().getAuthors(), ", "))
-+                    ));
-+                }
-+            }));
++                plugin -> pair(plugin.getName(), createObject(
++                    pair("version", plugin.getDescription().getVersion()),
++                    pair("description", String.valueOf(plugin.getDescription().getDescription()).trim()),
++                    pair("website", plugin.getDescription().getWebsite()),
++                    pair("authors", StringUtils.join(plugin.getDescription().getAuthors(), ", "))
++                ))));
 +
 +
 +
@@ -1817,7 +1801,7 @@ index 00000000..9dd36419
 +            pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null))
 +        ));
 +
-+        new TimingsExport(sender, parent, history).start();
++        new TimingsExport(listeners, parent, history).start();
 +    }
 +
 +    static long getCost() {
@@ -1874,12 +1858,7 @@ index 00000000..9dd36419
 +        if (!(val instanceof MemorySection)) {
 +            if (val instanceof List) {
 +                Iterable<Object> v = (Iterable<Object>) val;
-+                return toArrayMapper(v, new Function<Object, Object>() {
-+                    @Override
-+                    public Object apply(Object input) {
-+                        return valAsJSON(input, parentKey);
-+                    }
-+                });
++                return toArrayMapper(v, input -> valAsJSON(input, parentKey));
 +            } else {
 +                return val.toString();
 +            }
@@ -1888,30 +1867,9 @@ index 00000000..9dd36419
 +        }
 +    }
 +
-+    @SuppressWarnings("CallToThreadRun")
-+    @Override
-+    public synchronized void start() {
-+        if (sender instanceof RemoteConsoleCommandSender) {
-+            sender.sendMessage(ChatColor.RED + "Warning: Timings report done over RCON will cause lag spikes.");
-+            sender.sendMessage(ChatColor.RED + "You should use " + ChatColor.YELLOW +
-+                "/timings report" + ChatColor.RED + " in game or console.");
-+            run();
-+        } else {
-+            super.start();
-+        }
-+    }
-+
 +    @Override
 +    public void run() {
-+        sender.sendMessage(ChatColor.GREEN + "Preparing Timings Report...");
-+
-+
-+        out.put("data", toArrayMapper(history, new Function<TimingHistory, Object>() {
-+            @Override
-+            public Object apply(TimingHistory input) {
-+                return input.export();
-+            }
-+        }));
++        out.put("data", toArrayMapper(history, TimingHistory::export));
 +
 +
 +        String response = null;
@@ -1936,9 +1894,9 @@ index 00000000..9dd36419
 +            response = getResponse(con);
 +
 +            if (con.getResponseCode() != 302) {
-+                sender.sendMessage(
++                listeners.sendMessage(
 +                    ChatColor.RED + "Upload Error: " + con.getResponseCode() + ": " + con.getResponseMessage());
-+                sender.sendMessage(ChatColor.RED + "Check your logs for more information");
++                listeners.sendMessage(ChatColor.RED + "Check your logs for more information");
 +                if (response != null) {
 +                    Bukkit.getLogger().log(Level.SEVERE, response);
 +                }
@@ -1946,20 +1904,19 @@ index 00000000..9dd36419
 +            }
 +
 +            String location = con.getHeaderField("Location");
-+            sender.sendMessage(ChatColor.GREEN + "View Timings Report: " + location);
-+            if (!(sender instanceof ConsoleCommandSender)) {
-+                Bukkit.getLogger().log(Level.INFO, "View Timings Report: " + location);
-+            }
++            listeners.sendMessage(ChatColor.GREEN + "View Timings Report: " + location);
 +
 +            if (response != null && !response.isEmpty()) {
 +                Bukkit.getLogger().log(Level.INFO, "Timing Response: " + response);
 +            }
 +        } catch (IOException ex) {
-+            sender.sendMessage(ChatColor.RED + "Error uploading timings, check your logs for more information");
++            listeners.sendMessage(ChatColor.RED + "Error uploading timings, check your logs for more information");
 +            if (response != null) {
 +                Bukkit.getLogger().log(Level.SEVERE, response);
 +            }
 +            Bukkit.getLogger().log(Level.SEVERE, "Could not paste timings", ex);
++        } finally {
++            this.listeners.done();
 +        }
 +    }
 +
@@ -1977,7 +1934,7 @@ index 00000000..9dd36419
 +            return bos.toString();
 +
 +        } catch (IOException ex) {
-+            sender.sendMessage(ChatColor.RED + "Error uploading timings, check your logs for more information");
++            listeners.sendMessage(ChatColor.RED + "Error uploading timings, check your logs for more information");
 +            Bukkit.getLogger().log(Level.WARNING, con.getResponseMessage(), ex);
 +            return null;
 +        } finally {
@@ -2189,6 +2146,74 @@ index 00000000..58ed35e0
 +        return null;
 +    }
 +}
+diff --git a/src/main/java/co/aikar/timings/TimingsReportListener.java b/src/main/java/co/aikar/timings/TimingsReportListener.java
+new file mode 100644
+index 00000000..4d492d4b
+--- /dev/null
++++ b/src/main/java/co/aikar/timings/TimingsReportListener.java
+@@ -0,0 +0,0 @@
++package co.aikar.timings;
++
++import com.google.common.collect.Lists;
++import org.apache.commons.lang.Validate;
++import org.bukkit.Bukkit;
++import org.bukkit.command.CommandSender;
++import org.bukkit.command.ConsoleCommandSender;
++import org.bukkit.command.MessageCommandSender;
++import org.bukkit.command.RemoteConsoleCommandSender;
++
++import java.util.List;
++
++@SuppressWarnings("WeakerAccess")
++public class TimingsReportListener implements MessageCommandSender {
++    private final List<CommandSender> senders;
++    private final Runnable onDone;
++
++    public TimingsReportListener(CommandSender senders) {
++        this(senders, null);
++    }
++    public TimingsReportListener(CommandSender sender, Runnable onDone) {
++        this(Lists.newArrayList(sender), onDone);
++    }
++    public TimingsReportListener(List<CommandSender> senders) {
++        this(senders, null);
++    }
++    public TimingsReportListener(List<CommandSender> senders, Runnable onDone) {
++        Validate.notNull(senders);
++        Validate.notEmpty(senders);
++
++        this.senders = Lists.newArrayList(senders);
++        this.onDone = onDone;
++    }
++
++    public void done() {
++        if (onDone != null) {
++            onDone.run();
++        }
++        for (CommandSender sender : senders) {
++            if (sender instanceof TimingsReportListener) {
++                ((TimingsReportListener) sender).done();
++            }
++        }
++    }
++
++    @Override
++    public void sendMessage(String message) {
++        senders.forEach((sender) -> sender.sendMessage(message));
++    }
++
++    public void addConsoleIfNeeded() {
++        boolean hasConsole = false;
++        for (CommandSender sender : this.senders) {
++            if (sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender) {
++                hasConsole = true;
++            }
++        }
++        if (!hasConsole) {
++            this.senders.add(Bukkit.getConsoleSender());
++        }
++    }
++}
 diff --git a/src/main/java/co/aikar/timings/UnsafeTimingHandler.java b/src/main/java/co/aikar/timings/UnsafeTimingHandler.java
 new file mode 100644
 index 00000000..5edaba12
@@ -2940,6 +2965,30 @@ index 120dba25..77cfe561 100644
          /**
           * Sends the component to the player
           *
+diff --git a/src/main/java/org/bukkit/command/BufferedCommandSender.java b/src/main/java/org/bukkit/command/BufferedCommandSender.java
+new file mode 100644
+index 00000000..fd452bce
+--- /dev/null
++++ b/src/main/java/org/bukkit/command/BufferedCommandSender.java
+@@ -0,0 +0,0 @@
++package org.bukkit.command;
++
++public class BufferedCommandSender implements MessageCommandSender {
++    private final StringBuffer buffer = new StringBuffer();
++    @Override
++    public void sendMessage(String message) {
++        buffer.append(message);
++        buffer.append("\n");
++    }
++
++    public String getBuffer() {
++        return buffer.toString();
++    }
++
++    public void reset() {
++        this.buffer.setLength(0);
++    }
++}
 diff --git a/src/main/java/org/bukkit/command/Command.java b/src/main/java/org/bukkit/command/Command.java
 index 08a9739f..347d2189 100644
 --- a/src/main/java/org/bukkit/command/Command.java
@@ -2992,17 +3041,15 @@ index 3f07d7f4..f89ad075 100644
      private static boolean inRange(int i, int j, int k) {
          return i >= j && i <= k;
      }
-diff --git a/src/main/java/org/bukkit/command/MultiMessageCommandSender.java b/src/main/java/org/bukkit/command/MultiMessageCommandSender.java
+diff --git a/src/main/java/org/bukkit/command/MessageCommandSender.java b/src/main/java/org/bukkit/command/MessageCommandSender.java
 new file mode 100644
-index 00000000..7b512fd4
+index 00000000..66232339
 --- /dev/null
-+++ b/src/main/java/org/bukkit/command/MultiMessageCommandSender.java
++++ b/src/main/java/org/bukkit/command/MessageCommandSender.java
 @@ -0,0 +0,0 @@
 +package org.bukkit.command;
 +
-+import com.google.common.collect.Lists;
 +import org.apache.commons.lang.NotImplementedException;
-+import org.apache.commons.lang.Validate;
 +import org.bukkit.Bukkit;
 +import org.bukkit.Server;
 +import org.bukkit.permissions.Permission;
@@ -3010,105 +3057,92 @@ index 00000000..7b512fd4
 +import org.bukkit.permissions.PermissionAttachmentInfo;
 +import org.bukkit.plugin.Plugin;
 +
-+import java.util.List;
 +import java.util.Set;
 +
 +/**
-+ * Used for proxying messages to multiple senders.
-+ * Do not use this for anything else but messages.
++ * For when all you care about is just messaging
 + */
-+public class MultiMessageCommandSender implements CommandSender {
-+    private final List<CommandSender> senders;
++public interface MessageCommandSender extends CommandSender {
 +
-+    public MultiMessageCommandSender(List<CommandSender> senders) {
-+        Validate.notNull(senders);
-+        Validate.notEmpty(senders);
-+
-+        this.senders = Lists.newArrayList(senders);
++    @Override
++    default void sendMessage(String[] messages) {
++        for (String message : messages) {
++            sendMessage(message);
++        }
 +    }
 +
 +    @Override
-+    public void sendMessage(String message) {
-+        senders.forEach((sender) -> sender.sendMessage(message));
-+    }
-+
-+    @Override
-+    public void sendMessage(String[] messages) {
-+        senders.forEach((sender) -> sender.sendMessage(messages));
-+    }
-+
-+    @Override
-+    public Server getServer() {
++    default Server getServer() {
 +        return Bukkit.getServer();
 +    }
 +
 +    @Override
-+    public String getName() {
++    default String getName() {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public boolean isOp() {
++    default boolean isOp() {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public void setOp(boolean value) {
++    default void setOp(boolean value) {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public boolean isPermissionSet(String name) {
++    default boolean isPermissionSet(String name) {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public boolean isPermissionSet(Permission perm) {
++    default boolean isPermissionSet(Permission perm) {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public boolean hasPermission(String name) {
++    default boolean hasPermission(String name) {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public boolean hasPermission(Permission perm) {
++    default boolean hasPermission(Permission perm) {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) {
++    default PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public PermissionAttachment addAttachment(Plugin plugin) {
++    default PermissionAttachment addAttachment(Plugin plugin) {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) {
++    default PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public PermissionAttachment addAttachment(Plugin plugin, int ticks) {
++    default PermissionAttachment addAttachment(Plugin plugin, int ticks) {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public void removeAttachment(PermissionAttachment attachment) {
++    default void removeAttachment(PermissionAttachment attachment) {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public void recalculatePermissions() {
++    default void recalculatePermissions() {
 +        throw new NotImplementedException();
 +    }
 +
 +    @Override
-+    public Set<PermissionAttachmentInfo> getEffectivePermissions() {
++    default Set<PermissionAttachmentInfo> getEffectivePermissions() {
 +        throw new NotImplementedException();
 +    }
 +}
diff --git a/Spigot-Server-Patches/Timings-v2.patch b/Spigot-Server-Patches/Timings-v2.patch
index 1a67672bb7..1f8f957ac0 100644
--- a/Spigot-Server-Patches/Timings-v2.patch
+++ b/Spigot-Server-Patches/Timings-v2.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Timings v2
 
 
 diff --git a/pom.xml b/pom.xml
-index 0e88ae2a7..31b8401aa 100644
+index 8b96966d8..8d1e8680b 100644
 --- a/pom.xml
 +++ b/pom.xml
 @@ -0,0 +0,0 @@
@@ -493,7 +493,7 @@ index 81fc04ed3..bd3b16025 100644
  
      private void z() {
 diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
-index 87c07f2be..e18fb875f 100644
+index daf2c0a67..3ba489d4f 100644
 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
 +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
 @@ -0,0 +0,0 @@ public class ChunkProviderServer implements IChunkProvider {
@@ -555,7 +555,7 @@ index a97e7d3c2..4890023d7 100644
          // return chunk; // CraftBukkit
      }
 diff --git a/src/main/java/net/minecraft/server/DedicatedServer.java b/src/main/java/net/minecraft/server/DedicatedServer.java
-index cb83e4f56..4dab9e962 100644
+index cb83e4f56..e6819139f 100644
 --- a/src/main/java/net/minecraft/server/DedicatedServer.java
 +++ b/src/main/java/net/minecraft/server/DedicatedServer.java
 @@ -0,0 +0,0 @@ import java.io.PrintStream;
@@ -585,6 +585,28 @@ index cb83e4f56..4dab9e962 100644
      }
  
      public boolean aa() {
+@@ -0,0 +0,0 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer
+                 return remoteControlCommandListener.getMessages();
+             }
+         };
+-        processQueue.add(waitable);
++        // Paper start
++        if (s.toLowerCase().startsWith("timings") && s.toLowerCase().matches("timings (report|paste|get|merged|seperate)")) {
++            org.bukkit.command.BufferedCommandSender sender = new org.bukkit.command.BufferedCommandSender();
++            waitable = new Waitable<String>() {
++                @Override
++                protected String evaluate() {
++                    return sender.getBuffer();
++                }
++            };
++            co.aikar.timings.Timings.generateReport(new co.aikar.timings.TimingsReportListener(sender, waitable));
++        } else {
++            processQueue.add(waitable);
++        }
++        // Paper end
+         try {
+             return waitable.get();
+         } catch (java.util.concurrent.ExecutionException e) {
 diff --git a/src/main/java/net/minecraft/server/Entity.java b/src/main/java/net/minecraft/server/Entity.java
 index e7b1ebbe3..05312c6ac 100644
 --- a/src/main/java/net/minecraft/server/Entity.java