Per region async profiler
Shiroha CI / build (push) Canceled after 0s
Shiroha CI / Event File (push) Canceled after 0s

这一坨给我干的头大了()

指令交互部分是gpt写的(review了(讲真这个指令系统真乱套()))

由于async profiler的问题目前可能不支持瘟斗士
This commit is contained in:
2026-08-16 01:53:49 +08:00
parent b82cbf07cf
commit c92cb53b26
9 changed files with 1150 additions and 1 deletions
+3 -1
View File
@@ -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();
}
} }
} }
@@ -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;
}
}
}
@@ -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()));
}
}
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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);
}
});
}
}