mirror of
https://github.com/Winds-Studio/Leaf.git
synced 2025-12-25 09:59:15 +00:00
SparklyPaper: Parallel world ticking (#246)
* SparklyPaper: Parallel world ticking * per world mspt (/leaf mspt) * fix chunk loading off-main violations * rebase and make tpsbar per world * temp fix for async chunk sending crash * add /leaf mspt compact and more cleanup * TCRF SparklyPaper (Pathothingi): Fix Nether and End portals for non-player entities * fix Potothingi's name * change thread name * fix plugin related async ticks (hopefully) * Revert "fix plugin related async ticks (hopefully)" This reverts commit 7a9b79adc538989ecbec162dd377245706522a87. * Add more config guards * rebase on upstream * actually add the paper patches * fix villagers failing to release poi * rebase * make async chunk send work with parallel world ticking again --------- Co-authored-by: Taiyou06 <kaandindar21@gmail.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package org.dreeam.leaf.async.world;
|
||||
|
||||
import ca.spottedleaf.moonrise.common.util.TickThread;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
public class SparklyPaperServerLevelTickExecutorThreadFactory implements ThreadFactory {
|
||||
private final String worldName;
|
||||
|
||||
public SparklyPaperServerLevelTickExecutorThreadFactory(final String worldName) {
|
||||
this.worldName = worldName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(@NotNull Runnable runnable) {
|
||||
TickThread.ServerLevelTickThread tickThread = new TickThread.ServerLevelTickThread(runnable, "LeafParallelWorld-ticker-worker " + this.worldName);
|
||||
|
||||
if (tickThread.isDaemon()) {
|
||||
tickThread.setDaemon(false);
|
||||
}
|
||||
|
||||
if (tickThread.getPriority() != 5) {
|
||||
tickThread.setPriority(5);
|
||||
}
|
||||
|
||||
return tickThread;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import it.unimi.dsi.fastutil.Pair;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.minecraft.Util;
|
||||
import org.dreeam.leaf.command.subcommands.MSPTCommand;
|
||||
import org.dreeam.leaf.command.subcommands.ReloadCommand;
|
||||
import org.dreeam.leaf.command.subcommands.VersionCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -37,11 +38,13 @@ public final class LeafCommand extends Command {
|
||||
// subcommand label -> subcommand
|
||||
private static final LeafSubcommand RELOAD_SUBCOMMAND = new ReloadCommand();
|
||||
private static final LeafSubcommand VERSION_SUBCOMMAND = new VersionCommand();
|
||||
private static final LeafSubcommand MSPT_SUBCOMMAND = new MSPTCommand();
|
||||
private static final Map<String, LeafSubcommand> SUBCOMMANDS = Util.make(() -> {
|
||||
final Map<Set<String>, LeafSubcommand> commands = new HashMap<>();
|
||||
|
||||
commands.put(Set.of(ReloadCommand.LITERAL_ARGUMENT), RELOAD_SUBCOMMAND);
|
||||
commands.put(Set.of(VersionCommand.LITERAL_ARGUMENT), VERSION_SUBCOMMAND);
|
||||
commands.put(Set.of(MSPTCommand.LITERAL_ARGUMENT), MSPT_SUBCOMMAND);
|
||||
|
||||
return commands.entrySet().stream()
|
||||
.flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
package org.dreeam.leaf.command.subcommands;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.framework.qual.DefaultQualifier;
|
||||
import org.dreeam.leaf.command.LeafCommand;
|
||||
import org.dreeam.leaf.command.PermissionedLeafSubcommand;
|
||||
import org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static net.kyori.adventure.text.Component.text;
|
||||
import static net.kyori.adventure.text.format.NamedTextColor.*;
|
||||
|
||||
@DefaultQualifier(NonNull.class)
|
||||
public final class MSPTCommand extends PermissionedLeafSubcommand {
|
||||
|
||||
public static final String LITERAL_ARGUMENT = "mspt";
|
||||
public static final String PERM = LeafCommand.BASE_PERM + "." + LITERAL_ARGUMENT;
|
||||
private static final DecimalFormat DF = new DecimalFormat("########0.0");
|
||||
private static final Component SLASH = text("/");
|
||||
|
||||
public MSPTCommand() {
|
||||
super(PERM, PermissionDefault.TRUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
|
||||
// Check if parallel world ticking is enabled
|
||||
if (!SparklyPaperParallelWorldTicking.enabled) {
|
||||
sender.sendMessage(Component.text()
|
||||
.content("Per-world MSPT tracking is only available when parallel world ticking is enabled.")
|
||||
.color(RED)
|
||||
.build());
|
||||
sender.sendMessage(Component.text()
|
||||
.content("Please enable it in your Leaf configuration to use this command.")
|
||||
.color(GRAY)
|
||||
.build());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if compact mode is requested
|
||||
boolean compactMode = args.length > 0 && args[0].equalsIgnoreCase("compact");
|
||||
|
||||
MinecraftServer server = MinecraftServer.getServer();
|
||||
|
||||
if (compactMode) {
|
||||
displayCompactStats(sender, server);
|
||||
} else {
|
||||
// Display header
|
||||
sender.sendMessage(Component.text()
|
||||
.content("━━━━━━━━━━━━━ ")
|
||||
.color(GOLD)
|
||||
.append(Component.text("MSPT Statistics").color(YELLOW))
|
||||
.append(Component.text(" ━━━━━━━━━━━━━").color(GOLD))
|
||||
.build());
|
||||
|
||||
// Overall server MSPT
|
||||
displayServerMSPT(sender, server);
|
||||
|
||||
// Add separator
|
||||
sender.sendMessage(Component.text(""));
|
||||
|
||||
// World-specific MSPT
|
||||
displayWorldMSPT(sender, server);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void displayCompactStats(CommandSender sender, MinecraftServer server) {
|
||||
// Get server stats (only 5s data with avg/min/max)
|
||||
List<Component> serverTimes = eval(server.tickTimes5s.getTimes());
|
||||
|
||||
// Display server stats in compact form
|
||||
sender.sendMessage(Component.text()
|
||||
.content("Server: ")
|
||||
.color(GOLD)
|
||||
.append(serverTimes.get(0)).append(SLASH).append(serverTimes.get(1)).append(SLASH).append(serverTimes.get(2))
|
||||
.build());
|
||||
|
||||
// Display world stats in compact form
|
||||
for (net.minecraft.server.level.ServerLevel serverLevel : server.getAllLevels()) {
|
||||
List<Component> worldTimes = eval(serverLevel.tickTimes5s.getTimes());
|
||||
|
||||
sender.sendMessage(Component.text()
|
||||
.content(serverLevel.getWorld().getName() + ": ")
|
||||
.color(GOLD)
|
||||
.append(worldTimes.get(0)).append(SLASH).append(worldTimes.get(1)).append(SLASH).append(worldTimes.get(2))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private void displayServerMSPT(CommandSender sender, MinecraftServer server) {
|
||||
List<Component> times = new ArrayList<>();
|
||||
times.addAll(eval(server.tickTimes5s.getTimes()));
|
||||
times.addAll(eval(server.tickTimes10s.getTimes()));
|
||||
times.addAll(eval(server.tickTimes60s.getTimes()));
|
||||
|
||||
sender.sendMessage(Component.text()
|
||||
.content("Server tick times ")
|
||||
.color(GOLD)
|
||||
.append(Component.text()
|
||||
.content("(avg/min/max)")
|
||||
.color(YELLOW)
|
||||
)
|
||||
.build());
|
||||
|
||||
sender.sendMessage(Component.text()
|
||||
.content(" 5s: ")
|
||||
.color(GOLD)
|
||||
.append(times.get(0)).append(SLASH).append(times.get(1)).append(SLASH).append(times.get(2))
|
||||
.build());
|
||||
|
||||
sender.sendMessage(Component.text()
|
||||
.content(" 10s: ")
|
||||
.color(GOLD)
|
||||
.append(times.get(3)).append(SLASH).append(times.get(4)).append(SLASH).append(times.get(5))
|
||||
.build());
|
||||
|
||||
sender.sendMessage(Component.text()
|
||||
.content(" 60s: ")
|
||||
.color(GOLD)
|
||||
.append(times.get(6)).append(SLASH).append(times.get(7)).append(SLASH).append(times.get(8))
|
||||
.build());
|
||||
}
|
||||
|
||||
private void displayWorldMSPT(CommandSender sender, MinecraftServer server) {
|
||||
sender.sendMessage(Component.text()
|
||||
.content("World-specific tick times ")
|
||||
.color(GOLD)
|
||||
.append(Component.text()
|
||||
.content("(avg/min/max)")
|
||||
.color(YELLOW)
|
||||
)
|
||||
.build());
|
||||
|
||||
for (net.minecraft.server.level.ServerLevel serverLevel : server.getAllLevels()) {
|
||||
List<Component> worldTimes = new ArrayList<>();
|
||||
worldTimes.addAll(eval(serverLevel.tickTimes5s.getTimes()));
|
||||
worldTimes.addAll(eval(serverLevel.tickTimes10s.getTimes()));
|
||||
worldTimes.addAll(eval(serverLevel.tickTimes60s.getTimes()));
|
||||
|
||||
// World name header
|
||||
sender.sendMessage(Component.text()
|
||||
.content("➤ ")
|
||||
.color(YELLOW)
|
||||
.append(Component.text(serverLevel.getWorld().getName()).color(GOLD))
|
||||
.build());
|
||||
|
||||
// Display time periods
|
||||
sender.sendMessage(Component.text()
|
||||
.content(" 5s: ")
|
||||
.color(GRAY)
|
||||
.append(worldTimes.get(0)).append(SLASH).append(worldTimes.get(1)).append(SLASH).append(worldTimes.get(2))
|
||||
.build());
|
||||
|
||||
sender.sendMessage(Component.text()
|
||||
.content(" 10s: ")
|
||||
.color(GRAY)
|
||||
.append(worldTimes.get(3)).append(SLASH).append(worldTimes.get(4)).append(SLASH).append(worldTimes.get(5))
|
||||
.build());
|
||||
|
||||
sender.sendMessage(Component.text()
|
||||
.content(" 60s: ")
|
||||
.color(GRAY)
|
||||
.append(worldTimes.get(6)).append(SLASH).append(worldTimes.get(7)).append(SLASH).append(worldTimes.get(8))
|
||||
.build());
|
||||
|
||||
boolean hasMoreWorlds = false;
|
||||
Iterable<net.minecraft.server.level.ServerLevel> levels = server.getAllLevels();
|
||||
for (net.minecraft.server.level.ServerLevel level : levels) {
|
||||
if (level != serverLevel) {
|
||||
hasMoreWorlds = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMoreWorlds) {
|
||||
sender.sendMessage(Component.text(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Component> eval(long[] times) {
|
||||
long min = Integer.MAX_VALUE;
|
||||
long max = 0L;
|
||||
long total = 0L;
|
||||
int count = 0;
|
||||
|
||||
for (long value : times) {
|
||||
if (value > 0L) {
|
||||
count++;
|
||||
if (value < min) min = value;
|
||||
if (value > max) max = value;
|
||||
total += value;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
// No data available yet
|
||||
return Arrays.asList(
|
||||
text("N/A", GRAY),
|
||||
text("N/A", GRAY),
|
||||
text("N/A", GRAY)
|
||||
);
|
||||
}
|
||||
|
||||
double avgD = ((double) total / (double) count) * 1.0E-6D;
|
||||
double minD = ((double) min) * 1.0E-6D;
|
||||
double maxD = ((double) max) * 1.0E-6D;
|
||||
|
||||
return Arrays.asList(getColoredValue(avgD), getColoredValue(minD), getColoredValue(maxD));
|
||||
}
|
||||
|
||||
private static Component getColoredValue(double value) {
|
||||
return text(DF.format(value) + "ms",
|
||||
value >= 50 ? RED :
|
||||
value >= 40 ? YELLOW :
|
||||
value >= 30 ? GOLD :
|
||||
value >= 20 ? GREEN :
|
||||
AQUA);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
|
||||
if (!SparklyPaperParallelWorldTicking.enabled) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (args.length == 1) {
|
||||
return Collections.singletonList("compact");
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,9 @@ public class AsyncChunkSend extends ConfigModules {
|
||||
public void onLoaded() {
|
||||
config.addCommentRegionBased(getBasePath(),
|
||||
"""
|
||||
**Experimental feature**
|
||||
Makes chunk packet preparation and sending asynchronous to improve server performance.
|
||||
This can significantly reduce main thread load when many players are loading chunks.""",
|
||||
"""
|
||||
**实验性功能**
|
||||
使区块数据包准备和发送异步化以提高服务器性能.
|
||||
当许多玩家同时加载区块时, 这可以显著减少主线程负载.""");
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.dreeam.leaf.config.modules.async;
|
||||
|
||||
import org.dreeam.leaf.config.ConfigModules;
|
||||
import org.dreeam.leaf.config.EnumConfigCategory;
|
||||
|
||||
public class SparklyPaperParallelWorldTicking extends ConfigModules {
|
||||
public String getBasePath() {
|
||||
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".parallel-world-tracking";
|
||||
}
|
||||
|
||||
public static boolean enabled = false;
|
||||
public static int threads = 8;
|
||||
public static boolean logContainerCreationStacktraces = false;
|
||||
public static boolean disableHardThrow = false;
|
||||
|
||||
@Override
|
||||
public void onLoaded() {
|
||||
config.addCommentRegionBased(getBasePath(),
|
||||
"""
|
||||
**Experimental feature**
|
||||
Enables parallel world ticking to improve performance on multi-core systems..""",
|
||||
"""
|
||||
**实验性功能**
|
||||
启用并行世界处理以提高多核系统的性能.""");
|
||||
|
||||
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
|
||||
threads = config.getInt(getBasePath() + ".threads", threads);
|
||||
threads = enabled ? threads : 0;
|
||||
logContainerCreationStacktraces = config.getBoolean(getBasePath() + ".log-container-creation-stacktraces", logContainerCreationStacktraces);
|
||||
logContainerCreationStacktraces = enabled && logContainerCreationStacktraces;
|
||||
disableHardThrow = config.getBoolean(getBasePath() + ".disable-hard-throw", disableHardThrow);
|
||||
disableHardThrow = enabled && disableHardThrow;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user