1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
|
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/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
index 1eb45df9dca5d0c31ac46709e706136a246cb8ea..005361c38b02713fb823d0be40954400d59f0c4d 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<String> SUBCOMMANDS = ImmutableSet.<String>builder().add("heap", "entity", "reload", "version").build();
+ private static final ImmutableSet<String> SUBCOMMANDS = ImmutableSet.<String>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<String> 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<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);
+ }
+ }
+
+ 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.getFullChunkUnchecked() == 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 <LexManos@gmail.com> - License: LGPLv2.1
*/
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
index 3b10ef3801ffd47707836b3ed3482e99ddd0050b..2fe519d4059fac06781c30e140895b604e13104f 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;
@@ -535,6 +552,172 @@ public final class MCUtil {
return null;
}
+ public static ChunkStatus getChunkStatus(ChunkHolder chunk) {
+ List<ChunkStatus> statuses = net.minecraft.server.level.ServerChunkCache.CHUNK_STATUSES;
+ for (int i = statuses.size() - 1; i >= 0; --i) {
+ ChunkStatus curr = statuses.get(i);
+ CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> 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:<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;
+ Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunks = chunkMap.visibleChunkMap;
+ DistanceManager chunkMapDistance = chunkMap.distanceManager;
+ List<ChunkHolder> allChunks = new ArrayList<>(visibleChunks.values());
+ List<ServerPlayer> players = world.players;
+
+ int fullLoadedChunks = 0;
+
+ for (ChunkHolder chunk : allChunks) {
+ if (chunk.getFullChunkUnchecked() != 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.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<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, "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);
}
|