Paper/Spigot-Server-Patches/0398-Chunk-debug-command.patch
Shane Freeder 7e8ae207bd
Updated Upstream (Bukkit/CraftBukkit/Spigot)
Upstream has released updates that appears to apply and compile correctly.
This update has not been tested by PaperMC and as with ANY update, please do your own testing

Bukkit Changes:
e99c9444 Add Plugin Chunk Ticket API
6a235f06 Fix incorrect nullability annotations for PlayerJoinEvent's join message

CraftBukkit Changes:
5f889388 Tweak build expiration to 7 days
572c02b0 MC-155077, SPIGOT-5113: EntityTracker desync
7ad3a1f4 SPIGOT-5146: BlockDataMeta does not work
60860983 SPIGOT-5155: Setting EntityExplodeEvent yield to 0 still causes blocks to drop
087a2cf4 Print number of force loaded chunks per plugin in crash reports
07b5b06d Add Plugin Chunk Ticket API
7ffb2a27 SPIGOT-5149: resetRecipes does nothing
a2275f19 SPIGOT-5141: World.generateTree() causes ClassCastException with huge mushrooms
31d4a777 SPIGOT-5142: Ignore invalid firework effects

Spigot Changes:
5e4e7f32 BUILDTOOLS-471: Rebuild patches
6e944739 SPIGOT-5159: Raider activation range overridden by Monster range
2019-07-11 17:59:21 +01:00

462 lines
20 KiB
Diff

From 8befd3b3ec4606c96161450e60dc63ca5d7d1c8e Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sat, 1 Jun 2019 13:00:55 -0700
Subject: [PATCH] Chunk debug command
Prints all chunk information to a text file into the debug
folder in the root server folder. The format is in JSON, and
the data format is described in MCUtil#dumpChunks(File)
The command will output server version and all online players to the
file as well. We do not log anything but the location, world and
username of the player.
Also logs the value of these config values (note not all are paper's):
- keep spawn loaded value
- spawn radius
- view distance
Each chunk has the following logged:
- Coordinate
- Ticket level & its corresponding state
- Whether it is queued for unload
- Chunk status (may be unloaded)
- All tickets on the chunk
Example log:
https://gist.githubusercontent.com/Spottedleaf/0131e7710ffd5d531e5fd246c3367380/raw/169ae1b2e240485f99bc7a6bd8e78d90e3af7397/chunks-2019-06-01_19.57.05.txt
For references on certain keywords (ticket, status, etc), please see:
https://bugs.mojang.com/browse/MC-141484?focusedCommentId=528273&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-528273
https://bugs.mojang.com/browse/MC-141484?focusedCommentId=528577&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-528577
diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
index 391726d99..8db92edc3 100644
--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
@@ -28,14 +28,14 @@ public class PaperCommand extends Command {
public PaperCommand(String name) {
super(name);
this.description = "Paper related commands";
- this.usageMessage = "/paper [heap | entity | reload | version]";
+ this.usageMessage = "/paper [heap | entity | reload | version | debug | chunkinfo]";
this.setPermission("bukkit.command.paper");
}
@Override
public List<String> tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException {
if (args.length <= 1)
- return getListMatchingLast(args, "heap", "entity", "reload", "version");
+ return getListMatchingLast(args, "heap", "entity", "reload", "version", "debug", "chunkinfo");
switch (args[0].toLowerCase(Locale.ENGLISH))
{
@@ -45,6 +45,21 @@ public class PaperCommand extends Command {
if (args.length == 3)
return getListMatchingLast(args, EntityTypes.getEntityNameList().stream().map(MinecraftKey::toString).sorted().toArray(String[]::new));
break;
+ case "debug":
+ if (args.length == 2) {
+ return getListMatchingLast(args, "help", "chunks");
+ }
+ break;
+ case "chunkinfo":
+ List<String> worldNames = new ArrayList<>();
+ worldNames.add("*");
+ for (org.bukkit.World world : Bukkit.getWorlds()) {
+ worldNames.add(world.getName());
+ }
+ if (args.length == 2) {
+ return getListMatchingLast(args, worldNames);
+ }
+ break;
}
return Collections.emptyList();
}
@@ -109,6 +124,12 @@ public class PaperCommand extends Command {
case "reload":
doReload(sender);
break;
+ case "debug":
+ doDebug(sender, args);
+ break;
+ case "chunkinfo":
+ doChunkInfo(sender, args);
+ break;
case "ver":
case "version":
Command ver = org.bukkit.Bukkit.getServer().getCommandMap().getCommand("version");
@@ -125,6 +146,96 @@ public class PaperCommand extends Command {
return true;
}
+ private void doChunkInfo(CommandSender sender, String[] args) {
+ List<org.bukkit.World> worlds;
+ if (args.length < 2 || args[1].equals("*")) {
+ worlds = Bukkit.getWorlds();
+ } else {
+ worlds = new ArrayList<>(args.length - 1);
+ for (int i = 1; i < args.length; ++i) {
+ org.bukkit.World world = Bukkit.getWorld(args[i]);
+ if (world == null) {
+ sender.sendMessage(ChatColor.RED + "World '" + args[i] + "' is invalid");
+ return;
+ }
+ worlds.add(world);
+ }
+ }
+
+ for (org.bukkit.World bukkitWorld : worlds) {
+ WorldServer world = ((CraftWorld)bukkitWorld).getHandle();
+
+ int total = 0;
+ int inactive = 0;
+ int border = 0;
+ int ticking = 0;
+ int entityTicking = 0;
+
+ for (PlayerChunk chunk : world.getChunkProvider().playerChunkMap.updatingChunks.values()) {
+ if (chunk.getFullChunkIfCached() == null) {
+ continue;
+ }
+
+ ++total;
+
+ PlayerChunk.State state = PlayerChunk.getChunkState(chunk.getTicketLevel());
+
+ switch (state) {
+ case INACCESSIBLE:
+ ++inactive;
+ continue;
+ case BORDER:
+ ++border;
+ continue;
+ case TICKING:
+ ++ticking;
+ continue;
+ case ENTITY_TICKING:
+ ++entityTicking;
+ continue;
+ }
+ }
+
+ sender.sendMessage(ChatColor.BLUE + "Chunks in " + ChatColor.GREEN + bukkitWorld.getName() + ChatColor.DARK_AQUA + ":");
+ sender.sendMessage(ChatColor.BLUE + "Total: " + ChatColor.DARK_AQUA + total + ChatColor.BLUE + " Inactive: " + ChatColor.DARK_AQUA
+ + inactive + ChatColor.BLUE + " Border: " + ChatColor.DARK_AQUA + border + ChatColor.BLUE + " Ticking: "
+ + ChatColor.DARK_AQUA + ticking + ChatColor.BLUE + " Entity: " + ChatColor.DARK_AQUA + entityTicking);
+ }
+ }
+
+ private void doDebug(CommandSender sender, String[] args) {
+ if (args.length < 2) {
+ sender.sendMessage(ChatColor.RED + "Use /paper debug [chunks] help for more information on a specific command");
+ return;
+ }
+
+ String debugType = args[1].toLowerCase(Locale.ENGLISH);
+ switch (debugType) {
+ case "chunks":
+ if (args.length >= 3 && args[2].toLowerCase(Locale.ENGLISH).equals("help")) {
+ sender.sendMessage(ChatColor.RED + "Use /paper debug chunks to dump loaded chunk information to a file");
+ break;
+ }
+ File file = new File(new File(new File("."), "debug"),
+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt");
+ sender.sendMessage(ChatColor.GREEN + "Writing chunk information dump to " + file.toString());
+ try {
+ MCUtil.dumpChunks(file);
+ sender.sendMessage(ChatColor.GREEN + "Successfully written chunk information!");
+ } catch (Throwable thr) {
+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
+ sender.sendMessage(ChatColor.RED + "Failed to dump chunk information, see console");
+ }
+
+ break;
+ case "help":
+ // fall through to default
+ default:
+ sender.sendMessage(ChatColor.RED + "Use /paper debug [chunks] help for more information on a specific command");
+ return;
+ }
+ }
+
/*
* Ported from MinecraftForge - author: LexManos <LexManos@gmail.com> - License: LGPLv2.1
*/
diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
index 4c3bcb63d..630feec16 100644
--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
@@ -21,7 +21,7 @@ import org.apache.logging.log4j.Logger;
public class ChunkProviderServer extends IChunkProvider {
private static final int b = (int) Math.pow(17.0D, 2.0D);
- private static final List<ChunkStatus> c = ChunkStatus.a();
+ private static final List<ChunkStatus> c = ChunkStatus.a(); static final List<ChunkStatus> getPossibleChunkStatuses() { return ChunkProviderServer.c; } // Paper - OBFHELPER
private final ChunkMapDistance chunkMapDistance;
public final ChunkGenerator<?> chunkGenerator;
private final WorldServer world;
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
index ec3732193..23d1935dd 100644
--- a/src/main/java/net/minecraft/server/MCUtil.java
+++ b/src/main/java/net/minecraft/server/MCUtil.java
@@ -4,7 +4,13 @@ import com.destroystokyo.paper.block.TargetBlockInfo;
import com.destroystokyo.paper.profile.CraftPlayerProfile;
import com.destroystokyo.paper.profile.PlayerProfile;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.internal.Streams;
+import com.google.gson.stream.JsonWriter;
import com.mojang.authlib.GameProfile;
+import com.mojang.datafixers.util.Either;
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.bukkit.Location;
import org.bukkit.block.BlockFace;
@@ -14,7 +20,11 @@ import org.spigotmc.AsyncCatcher;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Queue;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
@@ -354,4 +364,170 @@ public final class MCUtil {
return null;
}
+
+ public static ChunkStatus getChunkStatus(PlayerChunk chunk) {
+ List<ChunkStatus> statuses = ChunkProviderServer.getPossibleChunkStatuses();
+ for (int i = statuses.size() - 1; i >= 0; --i) {
+ ChunkStatus curr = statuses.get(i);
+ CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> future = chunk.getStatusFutureUnchecked(curr);
+ if (future != PlayerChunk.UNLOADED_CHUNK_ACCESS_FUTURE) {
+ return curr;
+ }
+ }
+ return null; // unloaded
+ }
+
+ public static void dumpChunks(File file) throws IOException {
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ /*
+ * Json format:
+ *
+ * Main data format:
+ * -server-version:<string>
+ * -data-version:<int>
+ * -worlds:
+ * -name:<world name>
+ * -view-distance:<int>
+ * -keep-spawn-loaded:<boolean>
+ * -keep-spawn-loaded-range:<int>
+ * -visible-chunk-count:<int>
+ * -loaded-chunk-count:<int>
+ * -verified-fully-loaded-chunks:<int>
+ * -players:<array of player>
+ * -chunk-data:<array of chunks>
+ *
+ * Player format:
+ * -name:<string>
+ * -x:<double>
+ * -y:<double>
+ * -z:<double>
+ *
+ * Chunk Format:
+ * -x:<integer>
+ * -z:<integer>
+ * -ticket-level:<integer>
+ * -state:<string>
+ * -queued-for-unload:<boolean>
+ * -status:<string>
+ * -tickets:<array of tickets>
+ *
+ *
+ * Ticket format:
+ * -ticket-type:<string>
+ * -ticket-level:<int>
+ * -add-tick:<long>
+ * -object-reason:<string> // This depends on the type of ticket. ie POST_TELEPORT -> entity id
+ */
+ List<org.bukkit.World> worlds = org.bukkit.Bukkit.getWorlds();
+ JsonObject data = new JsonObject();
+
+ data.addProperty("server-version", org.bukkit.Bukkit.getVersion());
+ data.addProperty("data-version", 0);
+
+ JsonArray worldsData = new JsonArray();
+
+ for (org.bukkit.World bukkitWorld : worlds) {
+ JsonObject worldData = new JsonObject();
+
+ WorldServer world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle();
+ PlayerChunkMap chunkMap = world.getChunkProvider().playerChunkMap;
+ Long2ObjectLinkedOpenHashMap<PlayerChunk> visibleChunks = chunkMap.visibleChunks;
+ ChunkMapDistance chunkMapDistance = chunkMap.getChunkMapDistanceManager();
+ List<PlayerChunk> allChunks = new ArrayList<>(visibleChunks.values());
+ List<EntityPlayer> players = world.players;
+
+ int fullLoadedChunks = 0;
+
+ for (PlayerChunk chunk : allChunks) {
+ if (chunk.getFullChunkIfCached() != null) {
+ ++fullLoadedChunks;
+ }
+ }
+
+ // sorting by coordinate makes the log easier to read
+ allChunks.sort((PlayerChunk v1, PlayerChunk v2) -> {
+ if (v1.location.x != v2.location.x) {
+ return Integer.compare(v1.location.x, v2.location.x);
+ }
+ return Integer.compare(v1.location.z, v2.location.z);
+ });
+
+ worldData.addProperty("name", world.getWorldData().getName());
+ worldData.addProperty("view-distance", world.spigotConfig.viewDistance);
+ worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory);
+ worldData.addProperty("keep-spawn-loaded-range", world.paperConfig.keepLoadedRange);
+ worldData.addProperty("visible-chunk-count", visibleChunks.size());
+ worldData.addProperty("loaded-chunk-count", chunkMap.loadedChunks.size());
+ worldData.addProperty("verified-fully-loaded-chunks", fullLoadedChunks);
+
+ JsonArray playersData = new JsonArray();
+
+ for (EntityPlayer player : players) {
+ JsonObject playerData = new JsonObject();
+
+ playerData.addProperty("name", player.getName());
+ playerData.addProperty("x", player.locX);
+ playerData.addProperty("y", player.locY);
+ playerData.addProperty("z", player.locZ);
+
+ playersData.add(playerData);
+
+ }
+
+ worldData.add("players", playersData);
+
+ JsonArray chunksData = new JsonArray();
+
+ for (PlayerChunk playerChunk : allChunks) {
+ JsonObject chunkData = new JsonObject();
+
+ Set<Ticket<?>> tickets = chunkMapDistance.tickets.get(playerChunk.location.pair());
+ ChunkStatus status = getChunkStatus(playerChunk);
+
+ chunkData.addProperty("x", playerChunk.location.x);
+ chunkData.addProperty("z", playerChunk.location.z);
+ chunkData.addProperty("ticket-level", playerChunk.getTicketLevel());
+ chunkData.addProperty("state", PlayerChunk.getChunkState(playerChunk.getTicketLevel()).toString());
+ chunkData.addProperty("queued-for-unload", chunkMap.unloadQueue.contains(playerChunk.location.pair()));
+ chunkData.addProperty("status", status == null ? "unloaded" : status.toString());
+
+ JsonArray ticketsData = new JsonArray();
+
+ if (tickets != null) {
+ for (Ticket<?> ticket : tickets) {
+ JsonObject ticketData = new JsonObject();
+
+ ticketData.addProperty("ticket-type", ticket.getTicketType().toString());
+ ticketData.addProperty("ticket-level", ticket.getTicketLevel());
+ ticketData.addProperty("object-reason", String.valueOf(ticket.getObjectReason()));
+ ticketData.addProperty("add-tick", ticket.getCreationTick());
+
+ ticketsData.add(ticketData);
+ }
+ }
+
+ chunkData.add("tickets", ticketsData);
+ chunksData.add(chunkData);
+ }
+
+
+ worldData.add("chunk-data", chunksData);
+ worldsData.add(worldData);
+ }
+
+ data.add("worlds", worldsData);
+
+ StringWriter stringWriter = new StringWriter();
+ JsonWriter jsonWriter = new JsonWriter(stringWriter);
+ jsonWriter.setIndent(" ");
+ jsonWriter.setLenient(false);
+ Streams.write(data, jsonWriter);
+
+ String fileData = stringWriter.toString();
+
+ try (PrintStream out = new PrintStream(new FileOutputStream(file), false, "UTF-8")) {
+ out.print(fileData);
+ }
+ }
}
diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java
index 5599d0e71..0daf5f99e 100644
--- a/src/main/java/net/minecraft/server/PlayerChunk.java
+++ b/src/main/java/net/minecraft/server/PlayerChunk.java
@@ -26,7 +26,7 @@ public class PlayerChunk {
public int oldTicketLevel;
private int ticketLevel;
private int n;
- private final ChunkCoordIntPair location;
+ final ChunkCoordIntPair location; // Paper - private -> package
private final short[] dirtyBlocks;
private int dirtyCount;
private int r;
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
index 171c3b4b1..a259f3582 100644
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
@@ -52,7 +52,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
public final Long2ObjectLinkedOpenHashMap<PlayerChunk> updatingChunks = new Long2ObjectLinkedOpenHashMap();
public volatile Long2ObjectLinkedOpenHashMap<PlayerChunk> visibleChunks;
private final Long2ObjectLinkedOpenHashMap<PlayerChunk> pendingUnload;
- private final LongSet loadedChunks;
+ final LongSet loadedChunks; // Paper - private -> package
public final WorldServer world;
private final LightEngineThreaded lightEngine;
private final IAsyncTaskHandler<Runnable> executor;
@@ -65,7 +65,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
private final Mailbox<ChunkTaskQueueSorter.a<Runnable>> mailboxWorldGen;
private final Mailbox<ChunkTaskQueueSorter.a<Runnable>> mailboxMain;
public final WorldLoadListener worldLoadListener;
- public final PlayerChunkMap.a u; // CraftBukkit - private -> public // PAIL chunkDistanceManager
+ public final PlayerChunkMap.a u; public final PlayerChunkMap.a getChunkMapDistanceManager() { return this.u; } // Paper - OBFHELPER // CraftBukkit - private -> public // PAIL chunkDistanceManager
private final AtomicInteger v;
private final DefinedStructureManager definedStructureManager;
private final File x;
diff --git a/src/main/java/net/minecraft/server/Ticket.java b/src/main/java/net/minecraft/server/Ticket.java
index 16e3114bc..efc89c537 100644
--- a/src/main/java/net/minecraft/server/Ticket.java
+++ b/src/main/java/net/minecraft/server/Ticket.java
@@ -6,8 +6,8 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
private final TicketType<T> a;
private final int b;
- public final T c; // CraftBukkit - private -> public // PAIL identifier
- private final long d;
+ public final T c; public final T getObjectReason() { return this.c; } // Paper - OBFHELPER // CraftBukkit - private -> public // PAIL identifier
+ private final long d; public final long getCreationTick() { return this.d; } // Paper - OBFHELPER
protected Ticket(TicketType<T> tickettype, int i, T t0, long j) {
this.a = tickettype;
@@ -52,6 +52,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
return this.a;
}
+ public final int getTicketLevel() { return this.b(); } // Paper - OBFHELPER
public int b() {
return this.b;
}
--
2.22.0