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 8fd716bf2e1402694798b8be03fd85821153be44..53dd6c18de8e80378852bbb141016d9574d42162 100644 --- a/src/main/java/com/destroystokyo/paper/PaperCommand.java +++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java @@ -6,13 +6,15 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ServerChunkCache; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.level.ChunkPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MCUtil; import org.apache.commons.lang3.tuple.MutablePair; import org.apache.commons.lang3.tuple.Pair; import org.bukkit.Bukkit; @@ -41,7 +43,7 @@ import java.util.stream.Collectors; public class PaperCommand extends Command { private static final String BASE_PERM = "bukkit.command.paper."; - private static final ImmutableSet SUBCOMMANDS = ImmutableSet.builder().add("heap", "entity", "reload", "version").build(); + private static final ImmutableSet SUBCOMMANDS = ImmutableSet.builder().add("heap", "entity", "reload", "version", "debug", "chunkinfo").build(); public PaperCommand(String name) { super(name); @@ -69,6 +71,21 @@ public class PaperCommand extends Command { if (args.length == 3) return getListMatchingLast(sender, args, EntityType.getEntityNameList().stream().map(ResourceLocation::toString).sorted().toArray(String[]::new)); break; + case "debug": + if (args.length == 2) { + return getListMatchingLast(sender, 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(sender, args, worldNames); + } + break; } return Collections.emptyList(); } @@ -135,6 +152,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": if (!testPermission(sender, "version")) break; // "ver" needs a special check because it's an alias. All other commands are checked up before the switch statement (because they are present in the SUBCOMMANDS set) case "version": @@ -152,6 +175,114 @@ 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); + } + } + + int accumulatedTotal = 0; + int accumulatedInactive = 0; + int accumulatedBorder = 0; + int accumulatedTicking = 0; + int accumulatedEntityTicking = 0; + + for (org.bukkit.World bukkitWorld : worlds) { + ServerLevel world = ((CraftWorld)bukkitWorld).getHandle(); + + int total = 0; + int inactive = 0; + int border = 0; + int ticking = 0; + int entityTicking = 0; + + for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunkMap.values()) { + if (chunk.getFullChunkIfCached() == null) { + continue; + } + + ++total; + + ChunkHolder.FullChunkStatus state = ChunkHolder.getFullChunkStatus(chunk.getTicketLevel()); + + switch (state) { + case INACCESSIBLE: + ++inactive; + continue; + case BORDER: + ++border; + continue; + case TICKING: + ++ticking; + continue; + case ENTITY_TICKING: + ++entityTicking; + continue; + } + } + + accumulatedTotal += total; + accumulatedInactive += inactive; + accumulatedBorder += border; + accumulatedTicking += ticking; + accumulatedEntityTicking += entityTicking; + + 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); + } + if (worlds.size() > 1) { + sender.sendMessage(ChatColor.BLUE + "Chunks in " + ChatColor.GREEN + "all listed worlds" + ChatColor.DARK_AQUA + ":"); + sender.sendMessage(ChatColor.BLUE + "Total: " + ChatColor.DARK_AQUA + accumulatedTotal + ChatColor.BLUE + " Inactive: " + ChatColor.DARK_AQUA + + accumulatedInactive + ChatColor.BLUE + " Border: " + ChatColor.DARK_AQUA + accumulatedBorder + ChatColor.BLUE + " Ticking: " + + ChatColor.DARK_AQUA + accumulatedTicking + ChatColor.BLUE + " Entity: " + ChatColor.DARK_AQUA + accumulatedEntityTicking); + } + } + + 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/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java index 1fecc81b25109592907623741225a6222a8c5ccc..a16551c81a444685f6337a65b6d7862b8c0dc684 100644 --- a/src/main/java/net/minecraft/server/MCUtil.java +++ b/src/main/java/net/minecraft/server/MCUtil.java @@ -9,13 +9,27 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.ServerChunkCache; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.Ticket; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; 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; @@ -24,8 +38,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; @@ -531,4 +548,170 @@ public final class MCUtil { return null; } + + public static ChunkStatus getChunkStatus(ChunkHolder chunk) { + List statuses = ServerChunkCache.getPossibleChunkStatuses(); + for (int i = statuses.size() - 1; i >= 0; --i) { + ChunkStatus curr = statuses.get(i); + CompletableFuture> future = chunk.getFutureIfPresentUnchecked(curr); + if (future != ChunkHolder.UNLOADED_CHUNK_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(); + + ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle(); + ChunkMap chunkMap = world.getChunkSource().chunkMap; + Long2ObjectLinkedOpenHashMap visibleChunks = chunkMap.visibleChunkMap; + DistanceManager chunkMapDistance = chunkMap.distanceManager; + List allChunks = new ArrayList<>(visibleChunks.values()); + List players = world.players; + + int fullLoadedChunks = 0; + + for (ChunkHolder chunk : allChunks) { + if (chunk.getFullChunkIfCached() != null) { + ++fullLoadedChunks; + } + } + + // sorting by coordinate makes the log easier to read + allChunks.sort((ChunkHolder v1, ChunkHolder 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.entitiesInLevel.size()); + worldData.addProperty("verified-fully-loaded-chunks", fullLoadedChunks); + + JsonArray playersData = new JsonArray(); + + for (ServerPlayer player : players) { + JsonObject playerData = new JsonObject(); + + playerData.addProperty("name", player.getScoreboardName()); + playerData.addProperty("x", player.getX()); + playerData.addProperty("y", player.getY()); + playerData.addProperty("z", player.getZ()); + + playersData.add(playerData); + + } + + worldData.add("players", playersData); + + JsonArray chunksData = new JsonArray(); + + for (ChunkHolder playerChunk : allChunks) { + JsonObject chunkData = new JsonObject(); + + Set> tickets = chunkMapDistance.tickets.get(playerChunk.pos.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", ChunkHolder.getFullChunkStatus(playerChunk.getTicketLevel()).toString()); + chunkData.addProperty("queued-for-unload", chunkMap.toDrop.contains(playerChunk.pos.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.getType().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/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java index 89e90806b78d94d5c1d781113da420dafa47930a..a89b9dab043ad4536014141d5a942670b4152a95 100644 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java +++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java @@ -53,7 +53,7 @@ public class ChunkHolder { public int oldTicketLevel; private int ticketLevel; private int queueLevel; - private final ChunkPos pos; + final ChunkPos pos; // Paper - private -> package private boolean hasChangedSections; private final ShortSet[] changedBlocksPerSection; private int blockChangedLightSectionFilter; diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 5d87a282042d7112415b7d7175031f734219f2c9..7585b6f87b72f53deccbcb8627a13503921fc682 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -104,7 +104,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public final Long2ObjectLinkedOpenHashMap updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); public volatile Long2ObjectLinkedOpenHashMap visibleChunkMap; private final Long2ObjectLinkedOpenHashMap pendingUnloads; - private final LongSet entitiesInLevel; + public final LongSet entitiesInLevel; // Paper - private -> public public final ServerLevel level; private final ThreadedLevelLightEngine lightEngine; private final BlockableEventLoop mainThreadExecutor; diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index a7122a0411f4a8656efd4facde3403c8093bc8a6..6d33c1ee44bc732b58d18a8f6b0fd4bbdcb2dcd6 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -46,7 +46,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Paper public class ServerChunkCache extends ChunkSource { - private static final List CHUNK_STATUSES = ChunkStatus.getStatusList(); + private static final List CHUNK_STATUSES = ChunkStatus.getStatusList(); public static final List getPossibleChunkStatuses() { return ServerChunkCache.CHUNK_STATUSES; } // Paper - OBFHELPER private final DistanceManager distanceManager; public final ChunkGenerator generator; private final ServerLevel level; diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java index 0c118d482e304c567fe7fe778c6ff386f960bdde..c6b5f32153b63ac92df9c4b31b8de168481f79f2 100644 --- a/src/main/java/net/minecraft/server/level/Ticket.java +++ b/src/main/java/net/minecraft/server/level/Ticket.java @@ -6,8 +6,8 @@ public final class Ticket implements Comparable> { private final TicketType type; private final int ticketLevel; - public final T key; - private long createdTick; + public final T key; public final T getObjectReason() { return this.key; } // Paper - OBFHELPER + private long createdTick; public final long getCreationTick() { return this.createdTick; } // Paper - OBFHELPER protected Ticket(TicketType type, int level, T argument) { this.type = type; @@ -51,6 +51,7 @@ public final class Ticket implements Comparable> { return this.type; } + public final int getTicketLevel() { return this.getTicketLevel(); } // Paper - OBFHELPER public int getTicketLevel() { return this.ticketLevel; }