From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf 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 f0a836db74ad3e20778d3863223bc9a35ff4ad41..9ead9b1ea1fafaa3d684c17efbae747386b7c587 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 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 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 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 - License: LGPLv2.1 */ diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java index 6921def2dc8dd7f2b8ba9d46ab697a9f9b62cf4e..9f4c79629c981d496b96cf8a7a4c8e058f102b8b 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java @@ -23,7 +23,7 @@ import org.apache.logging.log4j.Logger; public class ChunkProviderServer extends IChunkProvider { - private static final List b = ChunkStatus.a(); + private static final List b = ChunkStatus.a(); static final List getPossibleChunkStatuses() { return ChunkProviderServer.b; } // 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 2eacc5de340ca5558d9a0cba9baa26bbebef714d..baa0f67400d9f7d36900010c362fe4ef4186573f 100644 --- a/src/main/java/net/minecraft/server/MCUtil.java +++ b/src/main/java/net/minecraft/server/MCUtil.java @@ -5,7 +5,13 @@ import com.destroystokyo.paper.profile.CraftPlayerProfile; import com.destroystokyo.paper.profile.PlayerProfile; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.apache.commons.lang.exception.ExceptionUtils; +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.bukkit.Location; import org.bukkit.block.BlockFace; import org.bukkit.craftbukkit.CraftWorld; @@ -14,8 +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.LinkedBlockingQueue; @@ -521,4 +530,170 @@ public final class MCUtil { return null; } + + public static ChunkStatus getChunkStatus(PlayerChunk chunk) { + List statuses = ChunkProviderServer.getPossibleChunkStatuses(); + for (int i = statuses.size() - 1; i >= 0; --i) { + ChunkStatus curr = statuses.get(i); + CompletableFuture> 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: + * -data-version: + * -worlds: + * -name: + * -view-distance: + * -keep-spawn-loaded: + * -keep-spawn-loaded-range: + * -visible-chunk-count: + * -loaded-chunk-count: + * -verified-fully-loaded-chunks: + * -players: + * -chunk-data: + * + * Player format: + * -name: + * -x: + * -y: + * -z: + * + * Chunk Format: + * -x: + * -z: + * -ticket-level: + * -state: + * -queued-for-unload: + * -status: + * -tickets: + * + * + * Ticket format: + * -ticket-type: + * -ticket-level: + * -add-tick: + * -object-reason: // This depends on the type of ticket. ie POST_TELEPORT -> entity id + */ + List 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 visibleChunks = chunkMap.visibleChunks; + ChunkMapDistance chunkMapDistance = chunkMap.chunkDistanceManager; + List allChunks = new ArrayList<>(visibleChunks.values()); + List 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.getWorld().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> 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 5efd611859648ed3c1bcda70728ea6106bd4bf4c..d806b6acbcfbf141f4c1436bd5a163fbf11bf4e6 100644 --- a/src/main/java/net/minecraft/server/PlayerChunk.java +++ b/src/main/java/net/minecraft/server/PlayerChunk.java @@ -27,7 +27,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 936be3fd8954933ac218f62a172df6878d128ec9..ee062b05d0de2c434ca1199e9614824a3a23e73b 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -59,7 +59,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { public final Long2ObjectLinkedOpenHashMap updatingChunks = new Long2ObjectLinkedOpenHashMap(); public volatile Long2ObjectLinkedOpenHashMap visibleChunks; private final Long2ObjectLinkedOpenHashMap pendingUnload; - private final LongSet loadedChunks; + final LongSet loadedChunks; // Paper - private -> package public final WorldServer world; private final LightEngineThreaded lightEngine; private final IAsyncTaskHandler executor; diff --git a/src/main/java/net/minecraft/server/Ticket.java b/src/main/java/net/minecraft/server/Ticket.java index 77bb6b092a0763ff27f90f0401a8a81b15aebb8c..7a8397815a5b7f79f3e3a0348aeedf63fe879f8f 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 implements Comparable> { private final TicketType a; private final int b; - public final T identifier; - private long d; + public final T identifier; public final T getObjectReason() { return this.identifier; } // Paper - OBFHELPER + private long d; public final long getCreationTick() { return this.d; } // Paper - OBFHELPER protected Ticket(TicketType tickettype, int i, T t0) { this.a = tickettype; @@ -51,6 +51,7 @@ public final class Ticket implements Comparable> { return this.a; } + public final int getTicketLevel() { return this.b(); } // Paper - OBFHELPER public int b() { return this.b; }