diff --git a/common/build.gradle b/common/build.gradle index bc9b8299..c4fc6057 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -27,6 +27,7 @@ dependencies { transitive = false } modCompileOnly("curse.maven:diagonal-fences-458048:${diagonal_fences_version}") + modCompileOnly "curse.maven:spark-361579:${rootProject.spark_version}" // compile against the JEI API but do not include it at runtime modCompileOnly("mezz.jei:jei-${minecraft_version}-common:${jei_version}") modCompileOnly("mezz.jei:jei-${minecraft_version}-gui:${jei_version}") diff --git a/common/src/main/java/org/embeddedt/modernfix/ModernFix.java b/common/src/main/java/org/embeddedt/modernfix/ModernFix.java index dba7fba9..8521fc58 100644 --- a/common/src/main/java/org/embeddedt/modernfix/ModernFix.java +++ b/common/src/main/java/org/embeddedt/modernfix/ModernFix.java @@ -71,6 +71,7 @@ public class ModernFix { if(ModernFixPlatformHooks.isDedicatedServer()) { float gameStartTime = ManagementFactory.getRuntimeMXBean().getUptime() / 1000f; ModernFix.LOGGER.warn("Dedicated server took " + gameStartTime + " seconds to load"); + ModernFixPlatformHooks.onLaunchComplete(); } ClassInfoManager.clear(); } diff --git a/common/src/main/java/org/embeddedt/modernfix/ModernFixClient.java b/common/src/main/java/org/embeddedt/modernfix/ModernFixClient.java index 7fa3f7c3..38ecd0ee 100644 --- a/common/src/main/java/org/embeddedt/modernfix/ModernFixClient.java +++ b/common/src/main/java/org/embeddedt/modernfix/ModernFixClient.java @@ -71,6 +71,7 @@ public class ModernFixClient { } else if (openingScreen instanceof TitleScreen && gameStartTimeSeconds < 0) { gameStartTimeSeconds = ManagementFactory.getRuntimeMXBean().getUptime() / 1000f; ModernFix.LOGGER.warn("Game took " + gameStartTimeSeconds + " seconds to start"); + ModernFixPlatformHooks.onLaunchComplete(); } } diff --git a/common/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java b/common/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index 2c9ed884..50940d54 100644 --- a/common/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/common/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -149,6 +149,7 @@ public class ModernFixEarlyConfig { .put("mixin.perf.faster_item_rendering", false) .put("mixin.feature.spam_thread_dump", false) .put("mixin.feature.snapshot_easter_egg", true) + .put("mixin.feature.spark_profile_launch", false) .put("mixin.devenv", isDevEnv) .put("mixin.perf.remove_spawn_chunks", isDevEnv) .build(); diff --git a/common/src/main/java/org/embeddedt/modernfix/platform/ModernFixPlatformHooks.java b/common/src/main/java/org/embeddedt/modernfix/platform/ModernFixPlatformHooks.java index 0d106ca3..e5d73e0a 100644 --- a/common/src/main/java/org/embeddedt/modernfix/platform/ModernFixPlatformHooks.java +++ b/common/src/main/java/org/embeddedt/modernfix/platform/ModernFixPlatformHooks.java @@ -92,4 +92,14 @@ public class ModernFixPlatformHooks { public static Multimap getCustomModOptions() { throw new AssertionError(); } + + @ExpectPlatform + public static void onLaunchComplete() { + + } + + @ExpectPlatform + public static String getPlatformName() { + throw new AssertionError(); + } } diff --git a/common/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java b/common/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java new file mode 100644 index 00000000..33e9daf6 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java @@ -0,0 +1,177 @@ +package org.embeddedt.modernfix.spark; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; +import me.lucko.spark.common.SparkPlatform; +import me.lucko.spark.common.SparkPlugin; +import me.lucko.spark.common.command.sender.CommandSender; +import me.lucko.spark.common.platform.PlatformInfo; +import me.lucko.spark.common.sampler.Sampler; +import me.lucko.spark.common.sampler.SamplerSettings; +import me.lucko.spark.common.sampler.ThreadDumper; +import me.lucko.spark.common.sampler.ThreadGrouper; +import me.lucko.spark.common.sampler.async.AsyncSampler; +import me.lucko.spark.common.sampler.async.SampleCollector; +import me.lucko.spark.common.sampler.java.JavaSampler; +import me.lucko.spark.common.sampler.node.MergeMode; +import me.lucko.spark.common.util.MethodDisambiguator; +import me.lucko.spark.lib.adventure.text.Component; +import me.lucko.spark.proto.SparkSamplerProtos; +import net.minecraft.SharedConstants; +import org.embeddedt.modernfix.core.ModernFixMixinPlugin; +import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; + +import java.nio.file.Path; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.stream.Stream; + +/* Inspired by CensoredASM */ +public class SparkLaunchProfiler { + private static PlatformInfo platformInfo = new ModernFixPlatformInfo(); + private static CommandSender commandSender = new ModernFixCommandSender(); + private static Map ongoingSamplers = new Object2ReferenceOpenHashMap<>(); + private static ExecutorService executor = Executors.newSingleThreadScheduledExecutor((new ThreadFactoryBuilder()).setNameFormat("spark-modernfix-async-worker").build()); + private static final SparkPlatform platform = new SparkPlatform(new ModernFixSparkPlugin()); + + private static final boolean USE_JAVA_SAMPLER_FOR_LAUNCH = true; //Boolean.getBoolean("modernfix.profileLaunchWithJavaSampler"); + + public static void start(String key) { + if (!ongoingSamplers.containsKey(key)) { + Sampler sampler; + SamplerSettings settings = new SamplerSettings(4000, ThreadDumper.ALL, ThreadGrouper.BY_NAME, -1, false); + try { + if(USE_JAVA_SAMPLER_FOR_LAUNCH) { + throw new UnsupportedOperationException(); + } + sampler = new AsyncSampler(platform, settings, new SampleCollector.Execution(4000)); + } catch (UnsupportedOperationException e) { + sampler = new JavaSampler(platform, settings, true, true); + } + ongoingSamplers.put(key, sampler); + ModernFixMixinPlugin.instance.logger.warn("Profiler has started for stage [{}]...", key); + sampler.start(); + } + } + + public static void stop(String key) { + Sampler sampler = ongoingSamplers.remove(key); + if (sampler != null) { + sampler.stop(true); + output(key, sampler); + } + } + + private static void output(String key, Sampler sampler) { + executor.execute(() -> { + ModernFixMixinPlugin.instance.logger.warn("Stage [{}] profiler has stopped! Uploading results...", key); + SparkSamplerProtos.SamplerData output = sampler.toProto(platform, new Sampler.ExportProps() + .creator(new CommandSender.Data(commandSender.getName(), commandSender.getUniqueId())) + .comment("Stage: " + key) + .mergeMode(() -> MergeMode.separateParentCalls(new MethodDisambiguator())) + .classSourceLookup(platform::createClassSourceLookup)); + try { + String urlKey = platform.getBytebinClient().postContent(output, "application/x-spark-sampler").key(); + String url = "https://spark.lucko.me/" + urlKey; + ModernFixMixinPlugin.instance.logger.warn("Profiler results for Stage [{}]: {}", key, url); + } catch (Exception e) { + ModernFixMixinPlugin.instance.logger.fatal("An error occurred whilst uploading the results.", e); + } + }); + } + + static class ModernFixPlatformInfo implements PlatformInfo { + + @Override + public Type getType() { + return ModernFixPlatformHooks.isClient() ? Type.CLIENT : Type.SERVER; + } + + @Override + public String getName() { + return ModernFixPlatformHooks.getPlatformName(); + } + + @Override + public String getVersion() { + return ModernFixPlatformHooks.getVersionString(); + } + + @Override + public String getMinecraftVersion() { + return SharedConstants.getCurrentVersion().getName(); + } + + } + + public static class ModernFixCommandSender implements CommandSender { + + private final UUID uuid = UUID.randomUUID(); + private final String name; + + public ModernFixCommandSender() { + this.name = "ModernFix"; + } + + @Override + public String getName() { + return name; + } + + @Override + public UUID getUniqueId() { + return uuid; + } + + @Override + public boolean hasPermission(String s) { + return true; + } + + @Override + public void sendMessage(Component component) { + + } + } + + static class ModernFixSparkPlugin implements SparkPlugin { + + @Override + public String getVersion() { + return "1.0"; + } + + @Override + public Path getPluginDirectory() { + return ModernFixPlatformHooks.getGameDirectory().resolve("spark-modernfix"); + } + + @Override + public String getCommandName() { + return "spark-modernfix"; + } + + @Override + public Stream getCommandSenders() { + return Stream.of(); + } + + @Override + public void executeAsync(Runnable runnable) { + executor.execute(runnable); + } + + @Override + public void log(Level level, String s) { + ModernFixMixinPlugin.instance.logger.warn(s); + } + + @Override + public PlatformInfo getPlatformInfo() { + return platformInfo; + } + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/util/CommonModUtil.java b/common/src/main/java/org/embeddedt/modernfix/util/CommonModUtil.java new file mode 100644 index 00000000..003d638d --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/util/CommonModUtil.java @@ -0,0 +1,17 @@ +package org.embeddedt.modernfix.util; + +import org.embeddedt.modernfix.core.ModernFixMixinPlugin; + +public class CommonModUtil { + /** + * Avoid using this, it's bad practice but cleanest way of suppressing errors for nonessential mod-dependent + * functionality. + */ + public static void runWithoutCrash(Runnable r, String errorMsg) { + try { + r.run(); + } catch(Throwable e) { + ModernFixMixinPlugin.instance.logger.error(errorMsg, e); + } + } +} diff --git a/fabric/build.gradle b/fabric/build.gradle index debe5cfe..9b9840d2 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -34,6 +34,7 @@ dependencies { modImplementation(fabricApi.module("fabric-resource-loader-v0", rootProject.fabric_api_version)) { exclude group: 'net.fabricmc', module: 'fabric-loader' } modImplementation(fabricApi.module("fabric-data-generation-api-v1", rootProject.fabric_api_version)) { exclude group: 'net.fabricmc', module: 'fabric-loader' } modCompileOnly("com.terraformersmc:modmenu:${rootProject.modmenu_version}") { transitive false } + modImplementation "curse.maven:spark-361579:${rootProject.spark_version}" modRuntimeOnly("net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}") { exclude group: 'net.fabricmc', module: 'fabric-loader' } // Remove the next line if you don't want to depend on the API // modApi "me.shedaniel:architectury-fabric:${rootProject.architectury_version}" diff --git a/fabric/src/main/java/org/embeddedt/modernfix/ModernFixPreLaunchFabric.java b/fabric/src/main/java/org/embeddedt/modernfix/ModernFixPreLaunchFabric.java new file mode 100644 index 00000000..e2ec74ae --- /dev/null +++ b/fabric/src/main/java/org/embeddedt/modernfix/ModernFixPreLaunchFabric.java @@ -0,0 +1,15 @@ +package org.embeddedt.modernfix; + +import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; +import org.embeddedt.modernfix.core.ModernFixMixinPlugin; +import org.embeddedt.modernfix.spark.SparkLaunchProfiler; +import org.embeddedt.modernfix.util.CommonModUtil; + +public class ModernFixPreLaunchFabric implements PreLaunchEntrypoint { + @Override + public void onPreLaunch() { + if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.spark_profile_launch.OnFabric")) { + CommonModUtil.runWithoutCrash(() -> SparkLaunchProfiler.start("launch"), "Failed to start profiler"); + } + } +} diff --git a/fabric/src/main/java/org/embeddedt/modernfix/platform/fabric/ModernFixPlatformHooksImpl.java b/fabric/src/main/java/org/embeddedt/modernfix/platform/fabric/ModernFixPlatformHooksImpl.java index a0747808..a860705d 100644 --- a/fabric/src/main/java/org/embeddedt/modernfix/platform/fabric/ModernFixPlatformHooksImpl.java +++ b/fabric/src/main/java/org/embeddedt/modernfix/platform/fabric/ModernFixPlatformHooksImpl.java @@ -19,6 +19,9 @@ import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import org.embeddedt.modernfix.ModernFixFabric; import org.embeddedt.modernfix.api.constants.IntegrationConstants; +import org.embeddedt.modernfix.core.ModernFixMixinPlugin; +import org.embeddedt.modernfix.spark.SparkLaunchProfiler; +import org.embeddedt.modernfix.util.CommonModUtil; import org.objectweb.asm.tree.*; import java.nio.file.Path; @@ -109,4 +112,14 @@ public class ModernFixPlatformHooksImpl { } return modOptions; } + + public static void onLaunchComplete() { + if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.spark_profile_launch.OnFabric")) { + CommonModUtil.runWithoutCrash(() -> SparkLaunchProfiler.stop("launch"), "Failed to stop profiler"); + } + } + + public static String getPlatformName() { + return "Fabric"; + } } diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index f1518b24..98dfe637 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -23,6 +23,9 @@ "org.embeddedt.modernfix.ModernFixClientFabric" ], "modmenu": [ "org.embeddedt.modernfix.fabric.modmenu.ModernFixModMenuApiImpl" ], + "preLaunch": [ + "org.embeddedt.modernfix.ModernFixPreLaunchFabric" + ], "jei_mod_plugin": [ "org.embeddedt.modernfix.searchtree.JEIRuntimeCapturer"] }, "mixins": [ diff --git a/forge/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java b/forge/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java index d6448b7a..293dcfef 100644 --- a/forge/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java +++ b/forge/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java @@ -26,6 +26,8 @@ import org.embeddedt.modernfix.api.constants.IntegrationConstants; import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.forge.classloading.FastAccessTransformerList; import org.embeddedt.modernfix.forge.packet.PacketHandler; +import org.embeddedt.modernfix.spark.SparkLaunchProfiler; +import org.embeddedt.modernfix.util.CommonModUtil; import org.embeddedt.modernfix.util.DummyList; import org.objectweb.asm.tree.ClassNode; import org.spongepowered.asm.mixin.injection.struct.InjectorGroupInfo; @@ -138,6 +140,10 @@ public class ModernFixPlatformHooksImpl { } catch(RuntimeException | ReflectiveOperationException e) { ModernFixMixinPlugin.instance.logger.error("Failed to patch mixin memory leak", e); } + + if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.spark_profile_launch.OnForge")) { + CommonModUtil.runWithoutCrash(() -> SparkLaunchProfiler.start("launch"), "Failed to start profiler"); + } } public static void applyASMTransformers(String mixinClassName, ClassNode targetClass) { @@ -169,4 +175,14 @@ public class ModernFixPlatformHooksImpl { } return modOptions; } + + public static void onLaunchComplete() { + if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.spark_profile_launch.OnForge")) { + CommonModUtil.runWithoutCrash(() -> SparkLaunchProfiler.stop("launch"), "Failed to stop profiler"); + } + } + + public static String getPlatformName() { + return "Forge"; + } } diff --git a/gradle.properties b/gradle.properties index 25be66af..51646f16 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,3 +20,5 @@ fabric_api_version=0.76.0+1.19.2 modmenu_version=4.1.2 appeng_version=12.9.3 diagonal_fences_version=4545943 + +spark_version=4505310