433 lines
18 KiB
Diff
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);
|
|
}
|