Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c92cb53b26 | ||
|
|
b82cbf07cf |
@@ -48,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val log4jPlugins = sourceSets.create("log4jPlugins") {
|
val log4jPlugins = sourceSets.create("log4jPlugins") {
|
||||||
@@ -134,7 +_,14 @@
|
@@ -134,7 +_,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -60,6 +60,8 @@
|
|||||||
+ implementation("com.github.luben:zstd-jni:1.5.4-1")
|
+ implementation("com.github.luben:zstd-jni:1.5.4-1")
|
||||||
+ implementation("net.openhft:zero-allocation-hashing:0.16")
|
+ implementation("net.openhft:zero-allocation-hashing:0.16")
|
||||||
+ implementation("net.objecthunter:exp4j:0.4.8")
|
+ implementation("net.objecthunter:exp4j:0.4.8")
|
||||||
|
+ implementation("tools.profiler:async-profiler:4.5")
|
||||||
|
+ implementation("tools.profiler:jfr-converter:4.5")
|
||||||
+ // Shiroha end
|
+ // Shiroha end
|
||||||
implementation("ca.spottedleaf:leafpile:1.0.0")
|
implementation("ca.spottedleaf:leafpile:1.0.0")
|
||||||
implementation("org.jline:jline-terminal-ffm:3.27.1") // use ffm on java 22+
|
implementation("org.jline:jline-terminal-ffm:3.27.1") // use ffm on java 22+
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: NanaChiyo0721 <nanachiyo0721@163.com>
|
||||||
|
Date: Sun, 16 Aug 2026 01:31:31 +0800
|
||||||
|
Subject: [PATCH] Per region async profiler
|
||||||
|
|
||||||
|
|
||||||
|
diff --git a/io/papermc/paper/threadedregions/TickRegionScheduler.java b/io/papermc/paper/threadedregions/TickRegionScheduler.java
|
||||||
|
index 724332d074bd7d3dfdcb60e83cf1fd6ce706000e..eb65077d18d86fce3ae1131445ebdcb736f5dc93 100644
|
||||||
|
--- a/io/papermc/paper/threadedregions/TickRegionScheduler.java
|
||||||
|
+++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java
|
||||||
|
@@ -220,6 +220,7 @@ public final class TickRegionScheduler {
|
||||||
|
*/
|
||||||
|
public void scheduleRegion(final RegionScheduleHandle region) {
|
||||||
|
region.scheduler = this;
|
||||||
|
+ region.scheduleProfiler = TickRegions.getScheduleProfilerManager().register(region.id); // Shiroha - per region async profiler
|
||||||
|
this.scheduler.schedule(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -230,6 +231,7 @@ public final class TickRegionScheduler {
|
||||||
|
public void descheduleRegion(final RegionScheduleHandle region) {
|
||||||
|
// To avoid acquiring any of the locks the scheduler may be using, we
|
||||||
|
// simply cancel the next action.
|
||||||
|
+ TickRegions.getScheduleProfilerManager().deRegister(region.id); region.scheduleProfiler = null; // Shiroha - per region async profiler
|
||||||
|
region.markNonSchedulable();
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -296,6 +298,7 @@ public final class TickRegionScheduler {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class RegionScheduleHandle extends SchedulableTick {
|
||||||
|
+ public io.nanachiyo0721.shiroha.utils.profiling.RegionScheduleProfiler scheduleProfiler = null; // Shiroha - per region async profiler
|
||||||
|
|
||||||
|
protected long currentTick;
|
||||||
|
protected long lastTickStart;
|
||||||
|
@@ -391,6 +394,7 @@ public final class TickRegionScheduler {
|
||||||
|
|
||||||
|
final FoliaWatchdogThread.RunningTick runningTick = new FoliaWatchdogThread.RunningTick(tickStart, this, Thread.currentThread()); // Folia - watchdog
|
||||||
|
WATCHDOG_THREAD.addTick(runningTick); // Folia - watchdog
|
||||||
|
+ if (this.scheduleProfiler != null) this.scheduleProfiler.taskBegin(); // Shiroha - per region async profiler
|
||||||
|
try {
|
||||||
|
this.runRegionTasks(() -> {
|
||||||
|
return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean();
|
||||||
|
@@ -400,6 +404,7 @@ public final class TickRegionScheduler {
|
||||||
|
// don't release region for another tick
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
+ if (this.scheduleProfiler != null) this.scheduleProfiler.taskEnd(); // Shiroha - per region async profiler
|
||||||
|
WATCHDOG_THREAD.removeTick(runningTick); // Folia - watchdog
|
||||||
|
final long tickEnd = System.nanoTime();
|
||||||
|
final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
|
||||||
|
@@ -476,6 +481,7 @@ public final class TickRegionScheduler {
|
||||||
|
|
||||||
|
final FoliaWatchdogThread.RunningTick runningTick = new FoliaWatchdogThread.RunningTick(tickStart, this, Thread.currentThread()); // Folia - region threading
|
||||||
|
WATCHDOG_THREAD.addTick(runningTick); // Folia - region threading
|
||||||
|
+ if (this.scheduleProfiler != null) this.scheduleProfiler.tickBegin(); // Shiroha - per region async profiler
|
||||||
|
try {
|
||||||
|
// next start isn't updated until the end of this tick
|
||||||
|
this.tickRegion(tickCount, tickStart, scheduledEnd);
|
||||||
|
@@ -489,6 +495,7 @@ public final class TickRegionScheduler {
|
||||||
|
// regionFailed will schedule a shutdown, so we should avoid letting this region tick further
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
+ if (this.scheduleProfiler != null) this.scheduleProfiler.tickEnd(); // Shiroha - per region async profiler
|
||||||
|
WATCHDOG_THREAD.removeTick(runningTick); // Folia - region threading
|
||||||
|
final long tickEnd = System.nanoTime();
|
||||||
|
final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
|
||||||
|
diff --git a/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java
|
||||||
|
index ab94ea7f18799d9dd3cf164a1332db69f40a9278..863f78680150d87c5b82132e471baac527452b6b 100644
|
||||||
|
--- a/io/papermc/paper/threadedregions/TickRegions.java
|
||||||
|
+++ b/io/papermc/paper/threadedregions/TickRegions.java
|
||||||
|
@@ -36,10 +36,12 @@ public final class TickRegions implements ThreadedRegionizer.RegionCallbacks<Tic
|
||||||
|
private static boolean initialised;
|
||||||
|
private static boolean started;
|
||||||
|
private static TickRegionScheduler scheduler;
|
||||||
|
+ private static final io.nanachiyo0721.shiroha.utils.profiling.RegionScheduleProfilerManager scheduleProfilerManager = new io.nanachiyo0721.shiroha.utils.profiling.RegionScheduleProfilerManager(); // Shiroha - per region async profiler
|
||||||
|
|
||||||
|
public static TickRegionScheduler getScheduler() {
|
||||||
|
return scheduler;
|
||||||
|
}
|
||||||
|
+ public static io.nanachiyo0721.shiroha.utils.profiling.RegionScheduleProfilerManager getScheduleProfilerManager() { return scheduleProfilerManager; } // Shiroha - per region async profiler
|
||||||
|
|
||||||
|
private static int getTickThreads(final GlobalConfiguration.ThreadedRegions config) {
|
||||||
|
int tickThreads;
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package io.nanachiyo0721.shiroha.commands;
|
package io.nanachiyo0721.shiroha.commands;
|
||||||
|
|
||||||
import io.nanachiyo0721.shiroha.commands.bar.BarCommand;
|
import io.nanachiyo0721.shiroha.commands.bar.BarCommand;
|
||||||
|
import io.nanachiyo0721.shiroha.commands.profiler.ProfilerCommand;
|
||||||
|
import io.nanachiyo0721.shiroha.config.modules.function.ProfilerConfig;
|
||||||
|
import io.papermc.paper.threadedregions.TickRegions;
|
||||||
|
|
||||||
public class CommandRegister {
|
public class CommandRegister {
|
||||||
/**
|
/**
|
||||||
@@ -10,5 +13,10 @@ public class CommandRegister {
|
|||||||
*/
|
*/
|
||||||
public static void register() {
|
public static void register() {
|
||||||
new BarCommand().register();
|
new BarCommand().register();
|
||||||
|
if (ProfilerConfig.enabled) {
|
||||||
|
TickRegions.getScheduleProfilerManager().init();
|
||||||
|
|
||||||
|
new ProfilerCommand(TickRegions.getScheduleProfilerManager()).register();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+375
@@ -0,0 +1,375 @@
|
|||||||
|
package io.nanachiyo0721.shiroha.commands.profiler;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||||
|
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import com.mojang.brigadier.suggestion.Suggestions;
|
||||||
|
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||||
|
import io.nanachiyo0721.shiroha.enums.EnumProfilingCategory;
|
||||||
|
import io.nanachiyo0721.shiroha.enums.EnumProfilingType;
|
||||||
|
import io.nanachiyo0721.shiroha.utils.profiling.RegionScheduleProfilerManager;
|
||||||
|
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||||
|
import io.papermc.paper.threadedregions.RegionizedServer;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.leavesmc.leaves.command.ArgumentNode;
|
||||||
|
import org.leavesmc.leaves.command.CommandContext;
|
||||||
|
import org.leavesmc.leaves.command.LiteralNode;
|
||||||
|
import org.leavesmc.leaves.command.RootNode;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class ProfilerCommand extends RootNode {
|
||||||
|
private static final String PERM_BASE = "shiroha.commands.sprofiler";
|
||||||
|
private static final TextColor SUCCESS = TextColor.color(0, 255, 0);
|
||||||
|
private static final TextColor ERROR = TextColor.color(255, 0, 0);
|
||||||
|
|
||||||
|
private final RegionScheduleProfilerManager profilerManager;
|
||||||
|
|
||||||
|
public ProfilerCommand(RegionScheduleProfilerManager profilerManager) {
|
||||||
|
super("sprofiler", PERM_BASE);
|
||||||
|
this.profilerManager = profilerManager;
|
||||||
|
children(
|
||||||
|
new StartCommand(),
|
||||||
|
new StartAtCommand(),
|
||||||
|
new StopCommand()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasPermission(@NotNull CommandSender sender, String... subcommand) {
|
||||||
|
return hasPermission(PERM_BASE, sender, subcommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean execute(@NotNull CommandContext context) {
|
||||||
|
context.getSender().sendMessage(Component.text(
|
||||||
|
"Usage: /sprofiler start <type> <category> <regionId> <seconds> | "
|
||||||
|
+ "/sprofiler start-at <type> <category> <world> <blockX> <blockZ> <seconds> | "
|
||||||
|
+ "/sprofiler stop <sessionId>"
|
||||||
|
));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Throwable unwrap(Throwable throwable) {
|
||||||
|
while (throwable.getCause() != null
|
||||||
|
&& (throwable instanceof java.util.concurrent.CompletionException
|
||||||
|
|| throwable instanceof java.util.concurrent.ExecutionException)) {
|
||||||
|
throwable = throwable.getCause();
|
||||||
|
}
|
||||||
|
return throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendMessage(CommandSender sender, Component message) {
|
||||||
|
RegionizedServer.getInstance().addTask(() -> sender.sendMessage(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendFailure(CommandSender sender, String operation, Throwable throwable) {
|
||||||
|
final Throwable cause = unwrap(throwable);
|
||||||
|
final String detail = cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage();
|
||||||
|
sendMessage(sender, Component.text("Failed to " + operation + ": " + detail).color(ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startProfiling(
|
||||||
|
CommandSender sender,
|
||||||
|
String typeInput,
|
||||||
|
String categoryInput,
|
||||||
|
long regionId,
|
||||||
|
int seconds,
|
||||||
|
String targetDescription
|
||||||
|
) {
|
||||||
|
final EnumProfilingType profilingType;
|
||||||
|
final EnumProfilingCategory category;
|
||||||
|
try {
|
||||||
|
profilingType = EnumProfilingType.valueOf(typeInput.toUpperCase(Locale.ROOT));
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
sender.sendMessage(Component.text("Unknown profiling type: " + typeInput).color(ERROR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
category = EnumProfilingCategory.valueOf(categoryInput.toUpperCase(Locale.ROOT));
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
sender.sendMessage(Component.text("Unknown profiling category: " + categoryInput).color(ERROR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final long profilingEnd;
|
||||||
|
try {
|
||||||
|
final long durationNanos = Math.multiplyExact((long) seconds, 1_000_000_000L);
|
||||||
|
profilingEnd = Math.addExact(System.nanoTime(), durationNanos);
|
||||||
|
} catch (ArithmeticException exception) {
|
||||||
|
sender.sendMessage(Component.text("Profiling duration is too long").color(ERROR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.profilerManager.startProfilingSession(profilingType, category, regionId, profilingEnd)
|
||||||
|
.whenComplete((session, startFailure) -> {
|
||||||
|
if (startFailure != null) {
|
||||||
|
sendFailure(sender, "start profiler", startFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(sender, Component.text(
|
||||||
|
"Started profiler session " + session.sessionId()
|
||||||
|
+ " for " + targetDescription
|
||||||
|
+ " for " + seconds + " seconds"
|
||||||
|
).color(SUCCESS));
|
||||||
|
|
||||||
|
session.output().whenComplete((output, exportFailure) -> {
|
||||||
|
if (exportFailure != null) {
|
||||||
|
sendFailure(sender, "export profiler session " + session.sessionId(), exportFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (output == null) {
|
||||||
|
sendMessage(sender, Component.text(
|
||||||
|
"Profiler session " + session.sessionId() + " ended without an output file"
|
||||||
|
).color(ERROR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Path absoluteOutput = output.toAbsolutePath().normalize();
|
||||||
|
sendMessage(sender, Component.text(
|
||||||
|
"Profiler session " + session.sessionId() + " exported to " + absoluteOutput
|
||||||
|
).color(SUCCESS));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class StartCommand extends LiteralNode {
|
||||||
|
private StartCommand() {
|
||||||
|
super("start");
|
||||||
|
children(new TypeArg());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requires(@NotNull CommandSourceStack source) {
|
||||||
|
return ProfilerCommand.hasPermission(source.getSender(), this.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class TypeArg extends ArgumentNode<String> {
|
||||||
|
private TypeArg() {
|
||||||
|
super("type", StringArgumentType.word());
|
||||||
|
children(new CategoryArg());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected CompletableFuture<Suggestions> getSuggestions(
|
||||||
|
@NotNull CommandContext context,
|
||||||
|
@NotNull SuggestionsBuilder builder
|
||||||
|
) {
|
||||||
|
Arrays.stream(EnumProfilingType.values())
|
||||||
|
.map(value -> value.name().toLowerCase(Locale.ROOT))
|
||||||
|
.forEach(builder::suggest);
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class CategoryArg extends ArgumentNode<String> {
|
||||||
|
private CategoryArg() {
|
||||||
|
super("category", StringArgumentType.word());
|
||||||
|
children(new RegionArg());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected CompletableFuture<Suggestions> getSuggestions(
|
||||||
|
@NotNull CommandContext context,
|
||||||
|
@NotNull SuggestionsBuilder builder
|
||||||
|
) {
|
||||||
|
Arrays.stream(EnumProfilingCategory.values())
|
||||||
|
.map(value -> value.name().toLowerCase(Locale.ROOT))
|
||||||
|
.forEach(builder::suggest);
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class RegionArg extends ArgumentNode<Long> {
|
||||||
|
private RegionArg() {
|
||||||
|
super("regionId", LongArgumentType.longArg(0L));
|
||||||
|
children(new SecondsArg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class SecondsArg extends ArgumentNode<Integer> {
|
||||||
|
private SecondsArg() {
|
||||||
|
super("seconds", IntegerArgumentType.integer(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean execute(@NotNull CommandContext context) {
|
||||||
|
final CommandSender sender = context.getSender();
|
||||||
|
final String typeInput = context.getArgument(TypeArg.class);
|
||||||
|
final String categoryInput = context.getArgument(CategoryArg.class);
|
||||||
|
final long regionId = context.getArgument("regionId", Long.class);
|
||||||
|
final int seconds = context.getArgument(SecondsArg.class);
|
||||||
|
|
||||||
|
startProfiling(sender, typeInput, categoryInput, regionId, seconds, "region " + regionId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class StartAtCommand extends LiteralNode {
|
||||||
|
private StartAtCommand() {
|
||||||
|
super("start-at");
|
||||||
|
children(new CoordinateTypeArg());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requires(@NotNull CommandSourceStack source) {
|
||||||
|
return ProfilerCommand.hasPermission(source.getSender(), this.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class CoordinateTypeArg extends ArgumentNode<String> {
|
||||||
|
private CoordinateTypeArg() {
|
||||||
|
super("coordinateType", StringArgumentType.word());
|
||||||
|
children(new CoordinateCategoryArg());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected CompletableFuture<Suggestions> getSuggestions(
|
||||||
|
@NotNull CommandContext context,
|
||||||
|
@NotNull SuggestionsBuilder builder
|
||||||
|
) {
|
||||||
|
Arrays.stream(EnumProfilingType.values())
|
||||||
|
.map(value -> value.name().toLowerCase(Locale.ROOT))
|
||||||
|
.forEach(builder::suggest);
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class CoordinateCategoryArg extends ArgumentNode<String> {
|
||||||
|
private CoordinateCategoryArg() {
|
||||||
|
super("coordinateCategory", StringArgumentType.word());
|
||||||
|
children(new WorldArg());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected CompletableFuture<Suggestions> getSuggestions(
|
||||||
|
@NotNull CommandContext context,
|
||||||
|
@NotNull SuggestionsBuilder builder
|
||||||
|
) {
|
||||||
|
Arrays.stream(EnumProfilingCategory.values())
|
||||||
|
.map(value -> value.name().toLowerCase(Locale.ROOT))
|
||||||
|
.forEach(builder::suggest);
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class WorldArg extends ArgumentNode<String> {
|
||||||
|
private WorldArg() {
|
||||||
|
super("world", StringArgumentType.word());
|
||||||
|
children(new BlockXArg());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected CompletableFuture<Suggestions> getSuggestions(
|
||||||
|
@NotNull CommandContext context,
|
||||||
|
@NotNull SuggestionsBuilder builder
|
||||||
|
) {
|
||||||
|
Bukkit.getWorlds().stream().map(World::getName).forEach(builder::suggest);
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class BlockXArg extends ArgumentNode<Integer> {
|
||||||
|
private BlockXArg() {
|
||||||
|
super("blockX", IntegerArgumentType.integer());
|
||||||
|
children(new BlockZArg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class BlockZArg extends ArgumentNode<Integer> {
|
||||||
|
private BlockZArg() {
|
||||||
|
super("blockZ", IntegerArgumentType.integer());
|
||||||
|
children(new CoordinateSecondsArg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class CoordinateSecondsArg extends ArgumentNode<Integer> {
|
||||||
|
private CoordinateSecondsArg() {
|
||||||
|
super("coordinateSeconds", IntegerArgumentType.integer(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean execute(@NotNull CommandContext context) {
|
||||||
|
final CommandSender sender = context.getSender();
|
||||||
|
final String worldName = context.getArgument(WorldArg.class);
|
||||||
|
final int blockX = context.getArgument(BlockXArg.class);
|
||||||
|
final int blockZ = context.getArgument(BlockZArg.class);
|
||||||
|
final World world = Bukkit.getWorld(worldName);
|
||||||
|
|
||||||
|
if (world == null) {
|
||||||
|
sender.sendMessage(Component.text("Unknown world: " + worldName).color(ERROR));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var region = ((CraftWorld) world).getHandle().regioniser
|
||||||
|
.getRegionAtSynchronised(blockX >> 4, blockZ >> 4);
|
||||||
|
if (region == null) {
|
||||||
|
sender.sendMessage(Component.text(
|
||||||
|
"No active region at " + worldName + " " + blockX + " " + blockZ
|
||||||
|
).color(ERROR));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final long regionId = region.getData().getRegionSchedulingHandle().id;
|
||||||
|
final int seconds = context.getArgument(CoordinateSecondsArg.class);
|
||||||
|
startProfiling(
|
||||||
|
sender,
|
||||||
|
context.getArgument(CoordinateTypeArg.class),
|
||||||
|
context.getArgument(CoordinateCategoryArg.class),
|
||||||
|
regionId,
|
||||||
|
seconds,
|
||||||
|
"region " + regionId + " at " + worldName + " " + blockX + " " + blockZ
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class StopCommand extends LiteralNode {
|
||||||
|
private StopCommand() {
|
||||||
|
super("stop");
|
||||||
|
children(new SessionArg());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requires(@NotNull CommandSourceStack source) {
|
||||||
|
return ProfilerCommand.hasPermission(source.getSender(), this.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class SessionArg extends ArgumentNode<Integer> {
|
||||||
|
private SessionArg() {
|
||||||
|
super("sessionId", IntegerArgumentType.integer(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean execute(@NotNull CommandContext context) {
|
||||||
|
final CommandSender sender = context.getSender();
|
||||||
|
final int sessionId = context.getInteger("sessionId");
|
||||||
|
|
||||||
|
profilerManager.endProfilingAsync(sessionId).whenComplete((stopped, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
sendFailure(sender, "stop profiler session " + sessionId, failure);
|
||||||
|
} else if (stopped) {
|
||||||
|
sendMessage(sender, Component.text(
|
||||||
|
"Stopped profiler session " + sessionId + "; exporting the result"
|
||||||
|
).color(SUCCESS));
|
||||||
|
} else {
|
||||||
|
sendMessage(sender, Component.text(
|
||||||
|
"Profiler session " + sessionId + " is not active"
|
||||||
|
).color(ERROR));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package io.nanachiyo0721.shiroha.config.modules.function;
|
||||||
|
|
||||||
|
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||||
|
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||||
|
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||||
|
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||||
|
import io.nanachiyo0721.shiroha.config.flags.DoNotLoad;
|
||||||
|
import io.nanachiyo0721.shiroha.config.flags.HotReloadUnsupported;
|
||||||
|
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||||
|
import io.papermc.paper.threadedregions.TickRegions;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ConfigClassInfo(name = "shiroha_profiler", category = EnumConfigCategory.FUNCTION)
|
||||||
|
public class ProfilerConfig implements IConfigModule {
|
||||||
|
@ConfigInfo(name = "enabled")
|
||||||
|
@HotReloadUnsupported
|
||||||
|
public static boolean enabled = false;
|
||||||
|
|
||||||
|
@DoNotLoad
|
||||||
|
private static boolean shutdownHookAttached = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||||
|
if (enabled) {
|
||||||
|
if (!shutdownHookAttached) {
|
||||||
|
shutdownHookAttached = true;
|
||||||
|
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> TickRegions.getScheduleProfilerManager().shutdown()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package io.nanachiyo0721.shiroha.enums;
|
||||||
|
|
||||||
|
public enum EnumProfilingCategory {
|
||||||
|
TICK("tick"),
|
||||||
|
TASK("task");
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
EnumProfilingCategory(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTagCategory() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package io.nanachiyo0721.shiroha.enums;
|
||||||
|
|
||||||
|
public enum EnumProfilingType {
|
||||||
|
ALLOC("alloc"),
|
||||||
|
CPU("cpu"),
|
||||||
|
WALL("wall");
|
||||||
|
|
||||||
|
private final String eventName;
|
||||||
|
|
||||||
|
EnumProfilingType(String eventName) {
|
||||||
|
this.eventName = eventName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEventName() {
|
||||||
|
return this.eventName;
|
||||||
|
}
|
||||||
|
}
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
package io.nanachiyo0721.shiroha.utils.profiling;
|
||||||
|
|
||||||
|
import io.nanachiyo0721.shiroha.enums.EnumProfilingCategory;
|
||||||
|
import one.profiler.Span;
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
import org.jspecify.annotations.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note: 1.a single instance should be called single threadedly(except method "toggle")
|
||||||
|
* 2.tickEnd, tickBegin, taskEnd, taskBegin are called serially not concurrently, if not, exception or unexpected behavior would be
|
||||||
|
* surely happened
|
||||||
|
*/
|
||||||
|
public class RegionScheduleProfiler {
|
||||||
|
private final long id;
|
||||||
|
private final String tagNameTick;
|
||||||
|
private final String tagNameTask;
|
||||||
|
|
||||||
|
private ExecutionRecord executionState;
|
||||||
|
|
||||||
|
private volatile boolean enabled = false;
|
||||||
|
private volatile long lastExecutionTime = Long.MIN_VALUE;
|
||||||
|
|
||||||
|
public RegionScheduleProfiler(long id) {
|
||||||
|
this.id = id;
|
||||||
|
|
||||||
|
this.tagNameTick = "region-" + EnumProfilingCategory.TICK.getTagCategory() + "-" + this.id;
|
||||||
|
this.tagNameTask = "region-" + EnumProfilingCategory.TASK.getTagCategory() + "-" + this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggle(boolean enabled) {
|
||||||
|
synchronized (this) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryToggle(boolean enabled) {
|
||||||
|
boolean oldVal = this.enabled;
|
||||||
|
|
||||||
|
if (oldVal == enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
oldVal = this.enabled;
|
||||||
|
|
||||||
|
if (oldVal == enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enabled = enabled;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tickBegin() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.executionState = ExecutionRecord.of(Span.start(), System.nanoTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tickEnd() {
|
||||||
|
final long end = System.nanoTime();
|
||||||
|
|
||||||
|
if (this.executionState != null) {
|
||||||
|
Span.end(this.executionState.span, this.tagNameTick);
|
||||||
|
|
||||||
|
this.lastExecutionTime = end - this.executionState.start;
|
||||||
|
this.executionState = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void taskBegin() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.executionState = ExecutionRecord.of(Span.start(), System.nanoTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void taskEnd() {
|
||||||
|
final long end = System.nanoTime();
|
||||||
|
|
||||||
|
if (this.executionState != null) {
|
||||||
|
Span.end(this.executionState.span, this.tagNameTask);
|
||||||
|
|
||||||
|
this.lastExecutionTime = end - this.executionState.start;
|
||||||
|
this.executionState = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the execution time of last operation
|
||||||
|
* @return -1 -> no execution history yet, otherwise the execution time of last operation
|
||||||
|
*/
|
||||||
|
public long lastExecutionTime() {
|
||||||
|
long executionTime = this.lastExecutionTime;
|
||||||
|
|
||||||
|
if (executionTime == Long.MIN_VALUE) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return executionTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long profilerId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExecutionRecord(long span, long start) {
|
||||||
|
@Contract("_,_ -> new")
|
||||||
|
public static @NonNull ExecutionRecord of(long span, long start) {
|
||||||
|
return new ExecutionRecord(span, start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+497
@@ -0,0 +1,497 @@
|
|||||||
|
package io.nanachiyo0721.shiroha.utils.profiling;
|
||||||
|
|
||||||
|
import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
|
||||||
|
import com.mojang.logging.LogUtils;
|
||||||
|
import io.nanachiyo0721.shiroha.enums.EnumProfilingCategory;
|
||||||
|
import io.nanachiyo0721.shiroha.enums.EnumProfilingType;
|
||||||
|
import it.unimi.dsi.fastutil.Pair;
|
||||||
|
import one.convert.Arguments;
|
||||||
|
import one.convert.Main;
|
||||||
|
import one.profiler.AsyncProfiler;
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
import org.jspecify.annotations.NonNull;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class RegionScheduleProfilerManager {
|
||||||
|
private static final Logger LOGGER = LogUtils.getLogger();
|
||||||
|
private static final Path PROFILER_FOLDER = Path.of("./shiroha_profiler");
|
||||||
|
private static final Pattern SESSION_FOLDER_PATTERN = Pattern.compile("session-([1-9]\\d*)");
|
||||||
|
|
||||||
|
private final Map<Long, RegionScheduleProfiler> registereddProfilers = new ConcurrentHashMap<>();
|
||||||
|
private final OpThread opThread = new OpThread();
|
||||||
|
|
||||||
|
// note: following fields are managed by the OpThread
|
||||||
|
private boolean profiling = false;
|
||||||
|
private int activeSessionId = -1;
|
||||||
|
private long profilingStartNs = Long.MIN_VALUE;
|
||||||
|
private String profilingOutput = "";
|
||||||
|
private List<Long> lastProfilingIds = Collections.emptyList();
|
||||||
|
private Consumer<Throwable> lastOnEnd;
|
||||||
|
// end
|
||||||
|
|
||||||
|
public record ProfilingSession(int sessionId, CompletableFuture<Path> output) {
|
||||||
|
public ProfilingSession {
|
||||||
|
Objects.requireNonNull(output, "output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do not rely on the serialized operation to be executed on the global region
|
||||||
|
// as we are also profiling that
|
||||||
|
private static final class OpThread extends Thread {
|
||||||
|
private volatile boolean shutdown = false;
|
||||||
|
|
||||||
|
private final MultiThreadedQueue<Runnable> ops = new MultiThreadedQueue<>();
|
||||||
|
private final MultiThreadedQueue<Runnable> afterShutdown = new MultiThreadedQueue<>();
|
||||||
|
|
||||||
|
public void signalShutdown() {
|
||||||
|
this.shutdown = true;
|
||||||
|
LockSupport.unpark(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdownAndAwait() {
|
||||||
|
this.signalShutdown();
|
||||||
|
|
||||||
|
final Thread toUnpark = Thread.currentThread();
|
||||||
|
if (!afterShutdown.offer(() -> LockSupport.unpark(toUnpark))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LockSupport.park();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean postOpIfNotOnOpThread(Runnable op) {
|
||||||
|
if (Thread.currentThread() == this) {
|
||||||
|
op.run();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.postOp(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean postOp(Runnable op) {
|
||||||
|
final boolean posted = this.ops.offer(op);
|
||||||
|
|
||||||
|
if (posted) {
|
||||||
|
LockSupport.unpark(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return posted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postShutdown() {
|
||||||
|
Runnable op;
|
||||||
|
while ((op = this.afterShutdown.pollOrBlockAdds()) != null) {
|
||||||
|
try {
|
||||||
|
op.run();
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
LOGGER.warn("Failed to execute operation!", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushOps() {
|
||||||
|
Runnable op;
|
||||||
|
while ((op = (this.shutdown ? this.ops.pollOrBlockAdds() : this.ops.poll())) != null) {
|
||||||
|
try {
|
||||||
|
op.run();
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
LOGGER.warn("Failed to execute operation!", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (true) {
|
||||||
|
this.flushOps();
|
||||||
|
|
||||||
|
if (this.shutdown) {
|
||||||
|
// final flush
|
||||||
|
this.flushOps();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// idle
|
||||||
|
LockSupport.park();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.postShutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
// call for a simple bootstrap
|
||||||
|
AsyncProfiler.getInstance();
|
||||||
|
|
||||||
|
// kick off op thread
|
||||||
|
this.opThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
this.opThread.postOpIfNotOnOpThread(this::haltAllProfiler);
|
||||||
|
this.opThread.shutdownAndAwait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void haltAllProfiler() {
|
||||||
|
if (this.profiling) {
|
||||||
|
this.stopActiveProfiling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (RegionScheduleProfiler profiler : this.registereddProfilers.values()) {
|
||||||
|
profiler.toggle(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endProfiling(int sessionId) {
|
||||||
|
this.endProfilingAsync(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Boolean> endProfilingAsync(int sessionId) {
|
||||||
|
final CompletableFuture<Boolean> callback = new CompletableFuture<>();
|
||||||
|
|
||||||
|
if (!this.opThread.postOpIfNotOnOpThread(() -> {
|
||||||
|
if (!this.profiling || sessionId != this.activeSessionId) {
|
||||||
|
callback.complete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Throwable stopFailure = this.stopActiveProfiling();
|
||||||
|
if (stopFailure == null) {
|
||||||
|
callback.complete(true);
|
||||||
|
} else {
|
||||||
|
callback.completeExceptionally(stopFailure);
|
||||||
|
}
|
||||||
|
})) {
|
||||||
|
callback.completeExceptionally(new IllegalStateException("Profiler manager has been shut down"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Throwable stopActiveProfiling() {
|
||||||
|
for (long id : this.lastProfilingIds) {
|
||||||
|
final RegionScheduleProfiler profiler = this.registereddProfilers.get(id);
|
||||||
|
|
||||||
|
// may be it was death, so no need to warn
|
||||||
|
if (profiler == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean success = profiler.tryToggle(false);
|
||||||
|
if (!success) {
|
||||||
|
LOGGER.warn("Failed to deactivate async profiler for region of id {}!", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final long profiledTime = System.nanoTime() - this.profilingStartNs;
|
||||||
|
final long profiledTimeSeconds = profiledTime / 1_000_000_000L;
|
||||||
|
|
||||||
|
Throwable stopFailure = null;
|
||||||
|
String commandResult = "";
|
||||||
|
try {
|
||||||
|
commandResult = AsyncProfiler.getInstance().execute("stop");
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
stopFailure = throwable;
|
||||||
|
LOGGER.warn("Failed to stop async profiler!", throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info(
|
||||||
|
"Profiler stopped with ret: {}, executed for {} seconds, saved to {}",
|
||||||
|
commandResult,
|
||||||
|
profiledTimeSeconds,
|
||||||
|
this.profilingOutput
|
||||||
|
);
|
||||||
|
|
||||||
|
final Consumer<Throwable> onEnd = this.lastOnEnd;
|
||||||
|
|
||||||
|
this.profiling = false;
|
||||||
|
this.activeSessionId = -1;
|
||||||
|
this.profilingStartNs = Long.MIN_VALUE;
|
||||||
|
this.profilingOutput = "";
|
||||||
|
this.lastProfilingIds = Collections.emptyList();
|
||||||
|
this.lastOnEnd = null;
|
||||||
|
|
||||||
|
if (onEnd != null) {
|
||||||
|
try {
|
||||||
|
onEnd.accept(stopFailure);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
LOGGER.warn("Failed to retire profiling callback!", throwable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stopFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Pair<Integer, Path>> startProfiling(
|
||||||
|
EnumProfilingType profilingType,
|
||||||
|
EnumProfilingCategory category,
|
||||||
|
long regionId,
|
||||||
|
long profilingEnd
|
||||||
|
) {
|
||||||
|
final CompletableFuture<Pair<Integer, Path>> callback = new CompletableFuture<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.startProfilingSession(profilingType, category, regionId, profilingEnd).whenComplete((session, startFailure) -> {
|
||||||
|
if (startFailure != null) {
|
||||||
|
callback.completeExceptionally(startFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.output().whenComplete((output, exportFailure) -> {
|
||||||
|
if (exportFailure != null) {
|
||||||
|
LOGGER.warn("Error occurred while profiling!", exportFailure);
|
||||||
|
callback.complete(Pair.of(session.sessionId(), null));
|
||||||
|
} else {
|
||||||
|
callback.complete(Pair.of(session.sessionId(), output));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
callback.completeExceptionally(throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<ProfilingSession> startProfilingSession(
|
||||||
|
EnumProfilingType profilingType,
|
||||||
|
EnumProfilingCategory category,
|
||||||
|
long regionId,
|
||||||
|
long profilingEnd
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(profilingType, "profilingType");
|
||||||
|
Objects.requireNonNull(category, "category");
|
||||||
|
|
||||||
|
final CompletableFuture<ProfilingSession> callback = new CompletableFuture<>();
|
||||||
|
|
||||||
|
// prevent scheduling ahead
|
||||||
|
if (profilingEnd <= System.nanoTime()) {
|
||||||
|
callback.completeExceptionally(new IllegalArgumentException("profilingEnd must be in the future"));
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.opThread.postOpIfNotOnOpThread(() -> {
|
||||||
|
// prevent duplicated profiling
|
||||||
|
if (this.profiling) {
|
||||||
|
callback.completeExceptionally(new IllegalStateException("Another profiler session is already active"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for existence
|
||||||
|
final RegionScheduleProfiler profiler = this.registereddProfilers.get(regionId);
|
||||||
|
if (profiler == null) {
|
||||||
|
callback.completeExceptionally(new IllegalArgumentException("No registered region profiler with id " + regionId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final AllocatedSession allocatedSession;
|
||||||
|
try {
|
||||||
|
allocatedSession = this.reserveSessionDirectory();
|
||||||
|
} catch (IOException exception) {
|
||||||
|
LOGGER.warn("Failed to reserve profiler session directory", exception);
|
||||||
|
callback.completeExceptionally(exception);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int sessionId = allocatedSession.sessionId();
|
||||||
|
final Path outputFolder = allocatedSession.folder();
|
||||||
|
final Path jfrOutput = outputFolder.resolve(regionId + ".jfr");
|
||||||
|
|
||||||
|
final CompletableFuture<Path> convertCallback = new CompletableFuture<>();
|
||||||
|
|
||||||
|
if (!profiler.tryToggle(true)) {
|
||||||
|
LOGGER.warn("Failed to activate async profiler for region of id {}!", regionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String builtCommand = "start,event=" + profilingType.getEventName() + ",jfr,file=" + jfrOutput;
|
||||||
|
String commandResult;
|
||||||
|
try {
|
||||||
|
commandResult = AsyncProfiler.getInstance().execute(builtCommand);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
profiler.toggle(false);
|
||||||
|
LOGGER.error("Failed to start async profiler!", throwable);
|
||||||
|
callback.completeExceptionally(throwable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// my shit()
|
||||||
|
this.profiling = true;
|
||||||
|
this.activeSessionId = sessionId;
|
||||||
|
this.profilingStartNs = System.nanoTime();
|
||||||
|
this.profilingOutput = jfrOutput.toString();
|
||||||
|
this.lastProfilingIds = List.of(regionId);
|
||||||
|
|
||||||
|
this.lastOnEnd = stopFailure -> {
|
||||||
|
if (stopFailure != null) {
|
||||||
|
convertCallback.completeExceptionally(stopFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.export(jfrOutput, outputFolder, regionId, profilingType, category).whenComplete((path, exportFailure) -> {
|
||||||
|
if (exportFailure == null) {
|
||||||
|
convertCallback.complete(path);
|
||||||
|
} else {
|
||||||
|
convertCallback.completeExceptionally(exportFailure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
LOGGER.info(
|
||||||
|
"Started profiling for region with id {} at {} system ns, output: {}, profiling event: {}, return message: {}",
|
||||||
|
regionId,
|
||||||
|
this.profilingStartNs,
|
||||||
|
jfrOutput,
|
||||||
|
profilingType,
|
||||||
|
commandResult
|
||||||
|
);
|
||||||
|
|
||||||
|
callback.complete(new ProfilingSession(sessionId, convertCallback));
|
||||||
|
|
||||||
|
if (this.profiling && this.activeSessionId == sessionId) {
|
||||||
|
final long delay = profilingEnd - System.nanoTime();
|
||||||
|
|
||||||
|
if (delay <= 0) {
|
||||||
|
this.endProfiling(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture.delayedExecutor(delay, TimeUnit.NANOSECONDS)
|
||||||
|
.execute(() -> this.endProfiling(sessionId));
|
||||||
|
}
|
||||||
|
})) {
|
||||||
|
callback.completeExceptionally(new IllegalStateException("Profiler manager has been shut down"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AllocatedSession(int sessionId, Path folder) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract(" -> new")
|
||||||
|
private RegionScheduleProfilerManager.@NonNull AllocatedSession reserveSessionDirectory() throws IOException {
|
||||||
|
Files.createDirectories(PROFILER_FOLDER);
|
||||||
|
|
||||||
|
int maxSessionId = 0;
|
||||||
|
try (Stream<Path> children = Files.list(PROFILER_FOLDER)) {
|
||||||
|
for (Path child : children.toList()) {
|
||||||
|
if (!Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Matcher matcher = SESSION_FOLDER_PATTERN.matcher(child.getFileName().toString());
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final int sessionId = Integer.parseInt(matcher.group(1));
|
||||||
|
if (sessionId > maxSessionId) {
|
||||||
|
maxSessionId = sessionId;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (maxSessionId < Integer.MAX_VALUE) {
|
||||||
|
final int sessionId = maxSessionId + 1;
|
||||||
|
final Path sessionPath = PROFILER_FOLDER.resolve("session-" + sessionId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.createDirectory(sessionPath);
|
||||||
|
return new AllocatedSession(sessionId, sessionPath);
|
||||||
|
} catch (FileAlreadyExistsException ignored) {
|
||||||
|
maxSessionId = sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException("Profiler session IDs are exhausted");
|
||||||
|
}
|
||||||
|
|
||||||
|
public RegionScheduleProfiler register(long id) {
|
||||||
|
RegionScheduleProfiler ret = new RegionScheduleProfiler(id);
|
||||||
|
|
||||||
|
if (this.registereddProfilers.putIfAbsent(id, ret) != null) {
|
||||||
|
throw new IllegalStateException("Already registed profiler for region of id " + id + "!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deRegister(long id) {
|
||||||
|
this.registereddProfilers.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Path> export(
|
||||||
|
Path jfrDataFile,
|
||||||
|
Path outputFolder,
|
||||||
|
long regionId,
|
||||||
|
EnumProfilingType exportType,
|
||||||
|
EnumProfilingCategory profilingCategory
|
||||||
|
) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(outputFolder);
|
||||||
|
|
||||||
|
final String tag = "region-"
|
||||||
|
+ profilingCategory.getTagCategory()
|
||||||
|
+ "-"
|
||||||
|
+ regionId;
|
||||||
|
|
||||||
|
final String outputName =
|
||||||
|
"region-" + regionId
|
||||||
|
+ "-" + profilingCategory.getTagCategory()
|
||||||
|
+ "-" + exportType.name().toLowerCase(Locale.ROOT)
|
||||||
|
+ ".html";
|
||||||
|
|
||||||
|
final Path output = outputFolder.resolve(outputName);
|
||||||
|
|
||||||
|
final Arguments args = new Arguments();
|
||||||
|
|
||||||
|
args.output = "html";
|
||||||
|
args.tag = tag;
|
||||||
|
|
||||||
|
switch (exportType) {
|
||||||
|
case CPU -> args.cpu = true;
|
||||||
|
case ALLOC -> args.alloc = true;
|
||||||
|
case WALL -> args.wall = true;
|
||||||
|
default -> throw new IllegalArgumentException(
|
||||||
|
"Unsupported export type: " + exportType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Main.convert(
|
||||||
|
jfrDataFile.toString(),
|
||||||
|
output.toString(),
|
||||||
|
args
|
||||||
|
);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CompletionException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user