testserver/patches/server/0016-Chunk-debug-command.patch

433 lines
18 KiB
Diff

From 0000000000000000000000000000000000000000 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/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
index b3a58bf4b654e336826dc04da9e2f80ff8b9a9a7..8e773f522521d2dd6349c87b582a3337b76f161f 100644
--- a/src/main/java/io/papermc/paper/command/PaperCommand.java
+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
@@ -1,5 +1,6 @@
package io.papermc.paper.command;
+import io.papermc.paper.command.subcommands.ChunkDebugCommand;
import io.papermc.paper.command.subcommands.EntityCommand;
import io.papermc.paper.command.subcommands.HeapDumpCommand;
import io.papermc.paper.command.subcommands.ReloadCommand;
@@ -40,6 +41,7 @@ public final class PaperCommand extends Command {
commands.put(Set.of("entity"), new EntityCommand());
commands.put(Set.of("reload"), new ReloadCommand());
commands.put(Set.of("version"), new VersionCommand());
+ commands.put(Set.of("debug", "chunkinfo"), new ChunkDebugCommand());
return commands.entrySet().stream()
.flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
diff --git a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..28a9550449be9a212f054b02e43fbd8a3781efcf
--- /dev/null
+++ b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
@@ -0,0 +1,166 @@
+package io.papermc.paper.command.subcommands;
+
+import io.papermc.paper.command.CommandUtil;
+import io.papermc.paper.command.PaperSubcommand;
+import java.io.File;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import net.minecraft.server.MCUtil;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ServerLevel;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+import static net.kyori.adventure.text.Component.text;
+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
+import static net.kyori.adventure.text.format.NamedTextColor.RED;
+
+@DefaultQualifier(NonNull.class)
+public final class ChunkDebugCommand implements PaperSubcommand {
+ @Override
+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
+ switch (subCommand) {
+ case "debug" -> this.doDebug(sender, args);
+ case "chunkinfo" -> this.doChunkInfo(sender, args);
+ }
+ return true;
+ }
+
+ @Override
+ public List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
+ switch (subCommand) {
+ case "debug" -> {
+ if (args.length == 1) {
+ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks");
+ }
+ }
+ case "chunkinfo" -> {
+ List<String> worldNames = new ArrayList<>();
+ worldNames.add("*");
+ for (org.bukkit.World world : Bukkit.getWorlds()) {
+ worldNames.add(world.getName());
+ }
+ if (args.length == 1) {
+ return CommandUtil.getListMatchingLast(sender, args, worldNames);
+ }
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ private void doChunkInfo(final CommandSender sender, final String[] args) {
+ List<org.bukkit.World> worlds;
+ if (args.length < 1 || args[0].equals("*")) {
+ worlds = Bukkit.getWorlds();
+ } else {
+ worlds = new ArrayList<>(args.length);
+ for (final String arg : args) {
+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg);
+ if (world == null) {
+ sender.sendMessage(text("World '" + arg + "' is invalid", RED));
+ return;
+ }
+ worlds.add(world);
+ }
+ }
+
+ int accumulatedTotal = 0;
+ int accumulatedInactive = 0;
+ int accumulatedBorder = 0;
+ int accumulatedTicking = 0;
+ int accumulatedEntityTicking = 0;
+
+ for (final org.bukkit.World bukkitWorld : worlds) {
+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
+
+ int total = 0;
+ int inactive = 0;
+ int border = 0;
+ int ticking = 0;
+ int entityTicking = 0;
+
+ for (final ChunkHolder chunk : net.minecraft.server.ChunkSystem.getVisibleChunkHolders(world)) {
+ if (chunk.getFullChunkNowUnchecked() == null) {
+ continue;
+ }
+
+ ++total;
+
+ ChunkHolder.FullChunkStatus state = chunk.getFullStatus();
+
+ switch (state) {
+ case INACCESSIBLE -> ++inactive;
+ case BORDER -> ++border;
+ case TICKING -> ++ticking;
+ case ENTITY_TICKING -> ++entityTicking;
+ }
+ }
+
+ accumulatedTotal += total;
+ accumulatedInactive += inactive;
+ accumulatedBorder += border;
+ accumulatedTicking += ticking;
+ accumulatedEntityTicking += entityTicking;
+
+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":")));
+ sender.sendMessage(text().color(DARK_AQUA).append(
+ text("Total: ", BLUE), text(total),
+ text(" Inactive: ", BLUE), text(inactive),
+ text(" Border: ", BLUE), text(border),
+ text(" Ticking: ", BLUE), text(ticking),
+ text(" Entity: ", BLUE), text(entityTicking)
+ ));
+ }
+ if (worlds.size() > 1) {
+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA)));
+ sender.sendMessage(text().color(DARK_AQUA).append(
+ text("Total: ", BLUE), text(accumulatedTotal),
+ text(" Inactive: ", BLUE), text(accumulatedInactive),
+ text(" Border: ", BLUE), text(accumulatedBorder),
+ text(" Ticking: ", BLUE), text(accumulatedTicking),
+ text(" Entity: ", BLUE), text(accumulatedEntityTicking)
+ ));
+ }
+ }
+
+ private void doDebug(final CommandSender sender, final String[] args) {
+ if (args.length < 1) {
+ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
+ return;
+ }
+
+ final String debugType = args[0].toLowerCase(Locale.ENGLISH);
+ switch (debugType) {
+ case "chunks" -> {
+ if (args.length >= 2 && args[1].toLowerCase(Locale.ENGLISH).equals("help")) {
+ sender.sendMessage(text("Use /paper debug chunks [world] to dump loaded chunk information to a file", RED));
+ 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(text("Writing chunk information dump to " + file, GREEN));
+ try {
+ MCUtil.dumpChunks(file);
+ sender.sendMessage(text("Successfully written chunk information!", GREEN));
+ } catch (Throwable thr) {
+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
+ sender.sendMessage(text("Failed to dump chunk information, see console", RED));
+ }
+ }
+ // "help" & default
+ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
+ }
+ }
+
+}
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
index b310d51b7fe3e8cef0a450674725969fe1ce78a4..2e56c52e3ee45b0304a9e6a5eab863ef96b2aab0 100644
--- a/src/main/java/net/minecraft/server/MCUtil.java
+++ b/src/main/java/net/minecraft/server/MCUtil.java
@@ -1,15 +1,27 @@
package net.minecraft.server;
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.datafixers.util.Either;
import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
import java.lang.ref.Cleaner;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.DistanceManager;
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 org.bukkit.Location;
import org.bukkit.block.BlockFace;
@@ -19,8 +31,11 @@ import org.spigotmc.AsyncCatcher;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
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;
@@ -505,6 +520,163 @@ public final class MCUtil {
}
}
+ public static ChunkStatus getChunkStatus(ChunkHolder chunk) {
+ return chunk.getChunkHolderStatus();
+ }
+
+ 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();
+
+ ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle();
+ ChunkMap chunkMap = world.getChunkSource().chunkMap;
+ DistanceManager chunkMapDistance = chunkMap.distanceManager;
+ List<ChunkHolder> allChunks = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(world);
+ List<ServerPlayer> players = world.players;
+
+ int fullLoadedChunks = 0;
+
+ for (ChunkHolder chunk : allChunks) {
+ if (chunk.getFullChunkNowUnchecked() != null) {
+ ++fullLoadedChunks;
+ }
+ }
+
+ // sorting by coordinate makes the log easier to read
+ allChunks.sort((ChunkHolder v1, ChunkHolder v2) -> {
+ if (v1.pos.x != v2.pos.x) {
+ return Integer.compare(v1.pos.x, v2.pos.x);
+ }
+ return Integer.compare(v1.pos.z, v2.pos.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().spawn.keepSpawnLoadedRange * 16);
+ worldData.addProperty("visible-chunk-count", allChunks.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<Ticket<?>> tickets = chunkMapDistance.tickets.get(playerChunk.pos.longKey);
+ ChunkStatus status = getChunkStatus(playerChunk);
+
+ chunkData.addProperty("x", playerChunk.pos.x);
+ chunkData.addProperty("z", playerChunk.pos.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.longKey));
+ 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.key));
+ ticketData.addProperty("add-tick", ticket.createdTick);
+
+ 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, StandardCharsets.UTF_8)) {
+ out.print(fileData);
+ }
+ }
+
public static int getTicketLevelFor(net.minecraft.world.level.chunk.ChunkStatus status) {
return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.ChunkStatus.getDistance(status);
}