From 0d3b71f3163d34a35506668066f94fcad8d5c6f8 Mon Sep 17 00:00:00 2001 From: Paint_Ninja Date: Sun, 7 Aug 2022 19:04:33 +0100 Subject: Improve operating system info (#238) --- .../src/main/java/me/lucko/spark/common/util/LinuxProc.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'spark-common/src/main/java/me/lucko/spark/common/util') diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java b/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java index 7d688d7..44fd3f9 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java @@ -49,7 +49,12 @@ public enum LinuxProc { /** * Information about the system network usage. */ - NET_DEV("/proc/net/dev"); + NET_DEV("/proc/net/dev"), + + /** + * Information about the operating system distro. + */ + OSINFO("/etc/os-release"); private final Path path; -- cgit From c96f88f7c125109edcae382b8d10011e87125102 Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 7 Aug 2022 21:53:11 +0100 Subject: Refactor Windows wmic utility --- .../me/lucko/spark/common/monitor/LinuxProc.java | 89 +++++++++++++++++++++ .../me/lucko/spark/common/monitor/WindowsWmic.java | 74 ++++++++++++++++++ .../me/lucko/spark/common/monitor/cpu/CpuInfo.java | 32 +++----- .../spark/common/monitor/memory/MemoryInfo.java | 2 +- .../common/monitor/net/NetworkInterfaceInfo.java | 2 +- .../common/monitor/os/OperatingSystemInfo.java | 90 ++++++++++++---------- .../platform/PlatformStatisticsProvider.java | 7 +- .../java/me/lucko/spark/common/util/LinuxProc.java | 89 --------------------- 8 files changed, 229 insertions(+), 156 deletions(-) create mode 100644 spark-common/src/main/java/me/lucko/spark/common/monitor/LinuxProc.java create mode 100644 spark-common/src/main/java/me/lucko/spark/common/monitor/WindowsWmic.java delete mode 100644 spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java (limited to 'spark-common/src/main/java/me/lucko/spark/common/util') diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/LinuxProc.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/LinuxProc.java new file mode 100644 index 0000000..563e247 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/LinuxProc.java @@ -0,0 +1,89 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.common.monitor; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; + +/** + * Utility for reading from /proc/ on Linux systems. + */ +public enum LinuxProc { + + /** + * Information about the system CPU. + */ + CPUINFO("/proc/cpuinfo"), + + /** + * Information about the system memory. + */ + MEMINFO("/proc/meminfo"), + + /** + * Information about the system network usage. + */ + NET_DEV("/proc/net/dev"), + + /** + * Information about the operating system distro. + */ + OSINFO("/etc/os-release"); + + private final Path path; + + LinuxProc(String path) { + this.path = resolvePath(path); + } + + private static @Nullable Path resolvePath(String path) { + try { + Path p = Paths.get(path); + if (Files.isReadable(p)) { + return p; + } + } catch (Exception e) { + // ignore + } + return null; + } + + public @NonNull List read() { + if (this.path != null) { + try { + return Files.readAllLines(this.path, StandardCharsets.UTF_8); + } catch (IOException e) { + // ignore + } + } + + return Collections.emptyList(); + } + +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/WindowsWmic.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/WindowsWmic.java new file mode 100644 index 0000000..6b602d9 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/WindowsWmic.java @@ -0,0 +1,74 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.common.monitor; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Utility for reading from wmic (Windows Management Instrumentation Commandline) on Windows systems. + */ +public enum WindowsWmic { + + /** + * Gets the CPU name + */ + CPU_GET_NAME("wmic", "cpu", "get", "name", "/FORMAT:list"), + + /** + * Gets the operating system name (caption) and version. + */ + OS_GET_CAPTION_AND_VERSION("wmic", "os", "get", "caption,version", "/FORMAT:list"); + + private static final boolean SUPPORTED = System.getProperty("os.name").startsWith("Windows"); + + private final String[] cmdArgs; + + WindowsWmic(String... cmdArgs) { + this.cmdArgs = cmdArgs; + } + + public @NonNull List read() { + if (SUPPORTED) { + ProcessBuilder process = new ProcessBuilder(this.cmdArgs).redirectErrorStream(true); + try (BufferedReader buf = new BufferedReader(new InputStreamReader(process.start().getInputStream()))) { + List lines = new ArrayList<>(); + + String line; + while ((line = buf.readLine()) != null) { + lines.add(line); + } + + return lines; + } catch (Exception e) { + // ignore + } + } + + return Collections.emptyList(); + } +} + diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/cpu/CpuInfo.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/cpu/CpuInfo.java index fcb70c0..9954bd5 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/monitor/cpu/CpuInfo.java +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/cpu/CpuInfo.java @@ -20,11 +20,9 @@ package me.lucko.spark.common.monitor.cpu; -import me.lucko.spark.common.util.LinuxProc; +import me.lucko.spark.common.monitor.LinuxProc; +import me.lucko.spark.common.monitor.WindowsWmic; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; import java.util.regex.Pattern; /** @@ -41,27 +39,19 @@ public enum CpuInfo { * @return the cpu model */ public static String queryCpuModel() { - if (System.getProperty("os.name").startsWith("Windows")) { - final String[] args = { "wmic", "cpu", "get", "name", "/FORMAT:list" }; - try (final BufferedReader buf = new BufferedReader(new InputStreamReader(new ProcessBuilder(args).redirectErrorStream(true).start().getInputStream()))) { - String line; - while ((line = buf.readLine()) != null) { - if (line.startsWith("Name")) { - return line.substring(5).trim(); - } - } - } catch (final IOException e) { - return ""; + for (String line : LinuxProc.CPUINFO.read()) { + String[] splitLine = SPACE_COLON_SPACE_PATTERN.split(line); + if (splitLine[0].equals("model name") || splitLine[0].equals("Processor")) { + return splitLine[1]; } - } else { - for (String line : LinuxProc.CPUINFO.read()) { - String[] splitLine = SPACE_COLON_SPACE_PATTERN.split(line); + } - if (splitLine[0].equals("model name") || splitLine[0].equals("Processor")) { - return splitLine[1]; - } + for (String line : WindowsWmic.CPU_GET_NAME.read()) { + if (line.startsWith("Name")) { + return line.substring(5).trim(); } } + return ""; } diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/MemoryInfo.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/MemoryInfo.java index 226f75b..8f63f71 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/MemoryInfo.java +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/MemoryInfo.java @@ -20,7 +20,7 @@ package me.lucko.spark.common.monitor.memory; -import me.lucko.spark.common.util.LinuxProc; +import me.lucko.spark.common.monitor.LinuxProc; import java.lang.management.ManagementFactory; import java.util.regex.Matcher; diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceInfo.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceInfo.java index bd9e187..332077a 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceInfo.java +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceInfo.java @@ -22,7 +22,7 @@ package me.lucko.spark.common.monitor.net; import com.google.common.collect.ImmutableMap; -import me.lucko.spark.common.util.LinuxProc; +import me.lucko.spark.common.monitor.LinuxProc; import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/os/OperatingSystemInfo.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/os/OperatingSystemInfo.java index 4eeebd1..1c2732c 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/monitor/os/OperatingSystemInfo.java +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/os/OperatingSystemInfo.java @@ -20,59 +20,67 @@ package me.lucko.spark.common.monitor.os; -import me.lucko.spark.common.util.LinuxProc; +import me.lucko.spark.common.monitor.LinuxProc; +import me.lucko.spark.common.monitor.WindowsWmic; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; +/** + * Small utility to query the operating system name & version. + */ +public final class OperatingSystemInfo { + private final String name; + private final String version; + private final String arch; + + public OperatingSystemInfo(String name, String version, String arch) { + this.name = name; + this.version = version; + this.arch = arch; + } -public enum OperatingSystemInfo { - ; + public String name() { + return this.name; + } - private static String name = null; - private static String version = null; + public String version() { + return this.version; + } + + public String arch() { + return this.arch; + } - static { - final String osNameJavaProp = System.getProperty("os.name"); + public static OperatingSystemInfo poll() { + String name = null; + String version = null; - if (osNameJavaProp.startsWith("Windows")) { - final String[] args = { "wmic", "os", "get", "caption,version", "/FORMAT:list" }; - try (final BufferedReader buf = new BufferedReader(new InputStreamReader(new ProcessBuilder(args).redirectErrorStream(true).start().getInputStream()))) { - String line; - while ((line = buf.readLine()) != null) { - if (line.startsWith("Caption")) { - name = line.substring(18).trim(); - } else if (line.startsWith("Version")) { - version = line.substring(8).trim(); - } - } - } catch (final IOException | IndexOutOfBoundsException e) { - // ignore + for (String line : LinuxProc.OSINFO.read()) { + if (line.startsWith("PRETTY_NAME") && line.length() > 13) { + name = line.substring(13).replace('"', ' ').trim(); } - } else { - for (final String line : LinuxProc.OSINFO.read()) { - if (line.startsWith("PRETTY_NAME")) { - try { - name = line.substring(13).replace('"', ' ').trim(); - } catch (final IndexOutOfBoundsException e) { - // ignore - } - } + } + + for (String line : WindowsWmic.OS_GET_CAPTION_AND_VERSION.read()) { + if (line.startsWith("Caption") && line.length() > 18) { + // Caption=Microsoft Windows something + // \----------------/ = 18 chars + name = line.substring(18).trim(); + } else if (line.startsWith("Version")) { + // Version=10.0.something + // \------/ = 8 chars + version = line.substring(8).trim(); } } - if (name == null) - name = osNameJavaProp; + if (name == null) { + name = System.getProperty("os.name"); + } - if (version == null) + if (version == null) { version = System.getProperty("os.version"); - } + } - public static String getName() { - return name; - } + String arch = System.getProperty("os.arch"); - public static String getVersion() { - return version; + return new OperatingSystemInfo(name, version, arch); } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/platform/PlatformStatisticsProvider.java b/spark-common/src/main/java/me/lucko/spark/common/platform/PlatformStatisticsProvider.java index e5be647..1eb9753 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/platform/PlatformStatisticsProvider.java +++ b/spark-common/src/main/java/me/lucko/spark/common/platform/PlatformStatisticsProvider.java @@ -51,6 +51,7 @@ public class PlatformStatisticsProvider { public SystemStatistics getSystemStatistics() { RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean(); + OperatingSystemInfo osInfo = OperatingSystemInfo.poll(); SystemStatistics.Builder builder = SystemStatistics.newBuilder() .setCpu(SystemStatistics.Cpu.newBuilder() @@ -87,9 +88,9 @@ public class PlatformStatisticsProvider { .build() ) .setOs(SystemStatistics.Os.newBuilder() - .setArch(System.getProperty("os.arch")) - .setName(OperatingSystemInfo.getName()) - .setVersion(OperatingSystemInfo.getVersion()) + .setArch(osInfo.arch()) + .setName(osInfo.name()) + .setVersion(osInfo.version()) .build() ) .setJava(SystemStatistics.Java.newBuilder() diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java b/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java deleted file mode 100644 index 44fd3f9..0000000 --- a/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * This file is part of spark. - * - * Copyright (c) lucko (Luck) - * Copyright (c) contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package me.lucko.spark.common.util; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; - -/** - * Utility for reading from /proc/ on Linux systems. - */ -public enum LinuxProc { - - /** - * Information about the system CPU. - */ - CPUINFO("/proc/cpuinfo"), - - /** - * Information about the system memory. - */ - MEMINFO("/proc/meminfo"), - - /** - * Information about the system network usage. - */ - NET_DEV("/proc/net/dev"), - - /** - * Information about the operating system distro. - */ - OSINFO("/etc/os-release"); - - private final Path path; - - LinuxProc(String path) { - this.path = resolvePath(path); - } - - private static @Nullable Path resolvePath(String path) { - try { - Path p = Paths.get(path); - if (Files.isReadable(p)) { - return p; - } - } catch (Exception e) { - // ignore - } - return null; - } - - public @NonNull List read() { - if (this.path != null) { - try { - return Files.readAllLines(this.path, StandardCharsets.UTF_8); - } catch (IOException e) { - // ignore - } - } - - return Collections.emptyList(); - } - -} -- cgit From 7ef9b6281135ce0a24f3c14c2255d9a2c2eca969 Mon Sep 17 00:00:00 2001 From: ishland Date: Mon, 19 Sep 2022 21:48:28 +0800 Subject: Display source info for mixin injected methods (#249) Co-authored-by: Luck --- .../spark/common/sampler/AbstractSampler.java | 12 +- .../lucko/spark/common/util/ClassSourceLookup.java | 257 +++++++++++++++++++-- .../src/main/proto/spark/spark_sampler.proto | 2 + spark-fabric/build.gradle | 10 +- .../spark/fabric/FabricClassSourceLookup.java | 159 ++++++++++++- .../fabric/plugin/FabricSparkMixinPlugin.java | 71 ++++++ .../me/lucko/spark/fabric/smap/MixinUtils.java | 52 +++++ .../lucko/spark/fabric/smap/SourceDebugCache.java | 87 +++++++ .../java/me/lucko/spark/fabric/smap/SourceMap.java | 133 +++++++++++ .../lucko/spark/fabric/smap/SourceMapProvider.java | 53 +++++ spark-fabric/src/main/resources/spark.mixins.json | 3 +- 11 files changed, 806 insertions(+), 33 deletions(-) create mode 100644 spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkMixinPlugin.java create mode 100644 spark-fabric/src/main/java/me/lucko/spark/fabric/smap/MixinUtils.java create mode 100644 spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceDebugCache.java create mode 100644 spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMap.java create mode 100644 spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMapProvider.java (limited to 'spark-common/src/main/java/me/lucko/spark/common/util') diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java index 1c217db..3cfef0b 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java @@ -164,8 +164,16 @@ public abstract class AbstractSampler implements Sampler { classSourceVisitor.visit(entry); } - if (classSourceVisitor.hasMappings()) { - proto.putAllClassSources(classSourceVisitor.getMapping()); + if (classSourceVisitor.hasClassSourceMappings()) { + proto.putAllClassSources(classSourceVisitor.getClassSourceMapping()); + } + + if (classSourceVisitor.hasMethodSourceMappings()) { + proto.putAllMethodSources(classSourceVisitor.getMethodSourceMapping()); + } + + if (classSourceVisitor.hasLineSourceMappings()) { + proto.putAllLineSources(classSourceVisitor.getLineSourceMapping()); } } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java b/spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java index bd9ec37..668f31a 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java @@ -38,9 +38,11 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; /** - * A function which defines the source of given {@link Class}es. + * A function which defines the source of given {@link Class}es or (Mixin) method calls. */ public interface ClassSourceLookup { @@ -52,6 +54,26 @@ public interface ClassSourceLookup { */ @Nullable String identify(Class clazz) throws Exception; + /** + * Identify the given method call. + * + * @param methodCall the method call info + * @return the source of the method call + */ + default @Nullable String identify(MethodCall methodCall) throws Exception { + return null; + } + + /** + * Identify the given method call. + * + * @param methodCall the method call info + * @return the source of the method call + */ + default @Nullable String identify(MethodCallByLine methodCall) throws Exception { + return null; + } + /** * A no-operation {@link ClassSourceLookup}. */ @@ -156,9 +178,17 @@ public interface ClassSourceLookup { interface Visitor { void visit(ThreadNode node); - boolean hasMappings(); + boolean hasClassSourceMappings(); + + Map getClassSourceMapping(); + + boolean hasMethodSourceMappings(); + + Map getMethodSourceMapping(); + + boolean hasLineSourceMappings(); - Map getMapping(); + Map getLineSourceMapping(); } static Visitor createVisitor(ClassSourceLookup lookup) { @@ -177,25 +207,46 @@ public interface ClassSourceLookup { } @Override - public boolean hasMappings() { + public boolean hasClassSourceMappings() { + return false; + } + + @Override + public Map getClassSourceMapping() { + return Collections.emptyMap(); + } + + @Override + public boolean hasMethodSourceMappings() { + return false; + } + + @Override + public Map getMethodSourceMapping() { + return Collections.emptyMap(); + } + + @Override + public boolean hasLineSourceMappings() { return false; } @Override - public Map getMapping() { + public Map getLineSourceMapping() { return Collections.emptyMap(); } } /** - * Visitor which scans {@link StackTraceNode}s and accumulates class identities. + * Visitor which scans {@link StackTraceNode}s and accumulates class/method call identities. */ class VisitorImpl implements Visitor { private final ClassSourceLookup lookup; private final ClassFinder classFinder = new ClassFinder(); - // class name --> identifier (plugin name) - private final Map map = new HashMap<>(); + private final SourcesMap classSources = new SourcesMap<>(Function.identity()); + private final SourcesMap methodSources = new SourcesMap<>(MethodCall::toString); + private final SourcesMap lineSources = new SourcesMap<>(MethodCallByLine::toString); VisitorImpl(ClassSourceLookup lookup) { this.lookup = lookup; @@ -208,34 +259,194 @@ public interface ClassSourceLookup { } } + private void visitStackNode(StackTraceNode node) { + this.classSources.computeIfAbsent( + node.getClassName(), + className -> { + Class clazz = this.classFinder.findClass(className); + if (clazz == null) { + return null; + } + return this.lookup.identify(clazz); + }); + + if (node.getMethodDescription() != null) { + MethodCall methodCall = new MethodCall(node.getClassName(), node.getMethodName(), node.getMethodDescription()); + this.methodSources.computeIfAbsent(methodCall, this.lookup::identify); + } else { + MethodCallByLine methodCall = new MethodCallByLine(node.getClassName(), node.getMethodName(), node.getLineNumber()); + this.lineSources.computeIfAbsent(methodCall, this.lookup::identify); + } + + // recursively + for (StackTraceNode child : node.getChildren()) { + visitStackNode(child); + } + } + @Override - public boolean hasMappings() { - return !this.map.isEmpty(); + public boolean hasClassSourceMappings() { + return this.classSources.hasMappings(); } @Override - public Map getMapping() { - this.map.values().removeIf(Objects::isNull); - return this.map; + public Map getClassSourceMapping() { + return this.classSources.export(); } - private void visitStackNode(StackTraceNode node) { - String className = node.getClassName(); - if (!this.map.containsKey(className)) { + @Override + public boolean hasMethodSourceMappings() { + return this.methodSources.hasMappings(); + } + + @Override + public Map getMethodSourceMapping() { + return this.methodSources.export(); + } + + @Override + public boolean hasLineSourceMappings() { + return this.lineSources.hasMappings(); + } + + @Override + public Map getLineSourceMapping() { + return this.lineSources.export(); + } + } + + final class SourcesMap { + // --> identifier (plugin name) + private final Map map = new HashMap<>(); + private final Function keyToStringFunction; + + private SourcesMap(Function keyToStringFunction) { + this.keyToStringFunction = keyToStringFunction; + } + + public void computeIfAbsent(T key, ComputeSourceFunction function) { + if (!this.map.containsKey(key)) { try { - Class clazz = this.classFinder.findClass(className); - Objects.requireNonNull(clazz); - this.map.put(className, this.lookup.identify(clazz)); + this.map.put(key, function.compute(key)); } catch (Throwable e) { - this.map.put(className, null); + this.map.put(key, null); } } + } - // recursively - for (StackTraceNode child : node.getChildren()) { - visitStackNode(child); + public boolean hasMappings() { + this.map.values().removeIf(Objects::isNull); + return !this.map.isEmpty(); + } + + public Map export() { + this.map.values().removeIf(Objects::isNull); + if (this.keyToStringFunction.equals(Function.identity())) { + //noinspection unchecked + return (Map) this.map; + } else { + return this.map.entrySet().stream().collect(Collectors.toMap( + e -> this.keyToStringFunction.apply(e.getKey()), + Map.Entry::getValue + )); } } + + private interface ComputeSourceFunction { + String compute(T key) throws Exception; + } + } + + /** + * Encapsulates information about a given method call using the name + method description. + */ + final class MethodCall { + private final String className; + private final String methodName; + private final String methodDescriptor; + + public MethodCall(String className, String methodName, String methodDescriptor) { + this.className = className; + this.methodName = methodName; + this.methodDescriptor = methodDescriptor; + } + + public String getClassName() { + return this.className; + } + + public String getMethodName() { + return this.methodName; + } + + public String getMethodDescriptor() { + return this.methodDescriptor; + } + + @Override + public String toString() { + return this.className + ";" + this.methodName + ";" + this.methodDescriptor; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MethodCall)) return false; + MethodCall that = (MethodCall) o; + return this.className.equals(that.className) && + this.methodName.equals(that.methodName) && + this.methodDescriptor.equals(that.methodDescriptor); + } + + @Override + public int hashCode() { + return Objects.hash(this.className, this.methodName, this.methodDescriptor); + } + } + + /** + * Encapsulates information about a given method call using the name + line number. + */ + final class MethodCallByLine { + private final String className; + private final String methodName; + private final int lineNumber; + + public MethodCallByLine(String className, String methodName, int lineNumber) { + this.className = className; + this.methodName = methodName; + this.lineNumber = lineNumber; + } + + public String getClassName() { + return this.className; + } + + public String getMethodName() { + return this.methodName; + } + + public int getLineNumber() { + return this.lineNumber; + } + + @Override + public String toString() { + return this.className + ";" + this.lineNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MethodCallByLine)) return false; + MethodCallByLine that = (MethodCallByLine) o; + return this.lineNumber == that.lineNumber && this.className.equals(that.className); + } + + @Override + public int hashCode() { + return Objects.hash(this.className, this.lineNumber); + } } } diff --git a/spark-common/src/main/proto/spark/spark_sampler.proto b/spark-common/src/main/proto/spark/spark_sampler.proto index 8d9512a..f670ddf 100644 --- a/spark-common/src/main/proto/spark/spark_sampler.proto +++ b/spark-common/src/main/proto/spark/spark_sampler.proto @@ -11,6 +11,8 @@ message SamplerData { SamplerMetadata metadata = 1; repeated ThreadNode threads = 2; map class_sources = 3; // optional + map method_sources = 4; // optional + map line_sources = 5; // optional } message SamplerMetadata { diff --git a/spark-fabric/build.gradle b/spark-fabric/build.gradle index 30b1ff6..fce859a 100644 --- a/spark-fabric/build.gradle +++ b/spark-fabric/build.gradle @@ -66,6 +66,10 @@ processResources { } } +license { + exclude '**/smap/SourceMap.java' +} + shadowJar { archiveFileName = "spark-fabric-${project.pluginVersion}-dev.jar" configurations = [project.configurations.shade] @@ -74,12 +78,16 @@ shadowJar { relocate 'net.kyori.examination', 'me.lucko.spark.lib.adventure.examination' relocate 'net.bytebuddy', 'me.lucko.spark.lib.bytebuddy' relocate 'com.google.protobuf', 'me.lucko.spark.lib.protobuf' - relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm' +// relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm' relocate 'one.profiler', 'me.lucko.spark.lib.asyncprofiler' exclude 'module-info.class' exclude 'META-INF/maven/**' exclude 'META-INF/proguard/**' + + dependencies { + exclude(dependency('org.ow2.asm::')) + } } task remappedShadowJar(type: RemapJarTask) { diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java index 7030680..9ffac18 100644 --- a/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java +++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java @@ -22,18 +22,35 @@ package me.lucko.spark.fabric; import com.google.common.collect.ImmutableMap; +import me.lucko.spark.common.util.ClassFinder; import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.fabric.smap.MixinUtils; +import me.lucko.spark.fabric.smap.SourceMap; +import me.lucko.spark.fabric.smap.SourceMapProvider; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.objectweb.asm.Type; +import org.spongepowered.asm.mixin.FabricUtil; +import org.spongepowered.asm.mixin.extensibility.IMixinConfig; +import org.spongepowered.asm.mixin.transformer.Config; +import org.spongepowered.asm.mixin.transformer.meta.MixinMerged; + +import java.lang.reflect.Method; +import java.net.URI; import java.nio.file.Path; import java.util.Collection; import java.util.Map; public class FabricClassSourceLookup extends ClassSourceLookup.ByCodeSource { + + private final ClassFinder classFinder = new ClassFinder(); + private final SourceMapProvider smapProvider = new SourceMapProvider(); + private final Path modsDirectory; - private final Map pathToModMap; + private final Map pathToModMap; public FabricClassSourceLookup() { FabricLoader loader = FabricLoader.getInstance(); @@ -43,7 +60,7 @@ public class FabricClassSourceLookup extends ClassSourceLookup.ByCodeSource { @Override public String identifyFile(Path path) { - String id = this.pathToModMap.get(path); + String id = this.pathToModMap.get(path.toAbsolutePath().normalize().toString()); if (id != null) { return id; } @@ -55,11 +72,141 @@ public class FabricClassSourceLookup extends ClassSourceLookup.ByCodeSource { return super.identifyFileName(this.modsDirectory.relativize(path).toString()); } - private static Map constructPathToModIdMap(Collection mods) { - ImmutableMap.Builder builder = ImmutableMap.builder(); + @Override + public @Nullable String identify(MethodCall methodCall) throws Exception { + String className = methodCall.getClassName(); + String methodName = methodCall.getMethodName(); + String methodDesc = methodCall.getMethodDescriptor(); + + if (className.equals("native") || methodName.equals("") || methodName.equals("")) { + return null; + } + + Class clazz = this.classFinder.findClass(className); + if (clazz == null) { + return null; + } + + Class[] params = getParameterTypesForMethodDesc(methodDesc); + Method reflectMethod = clazz.getDeclaredMethod(methodName, params); + + MixinMerged mixinMarker = reflectMethod.getDeclaredAnnotation(MixinMerged.class); + if (mixinMarker == null) { + return null; + } + + return modIdFromMixinClass(mixinMarker.mixin()); + } + + @Override + public @Nullable String identify(MethodCallByLine methodCall) throws Exception { + String className = methodCall.getClassName(); + String methodName = methodCall.getMethodName(); + int lineNumber = methodCall.getLineNumber(); + + if (className.equals("native") || methodName.equals("") || methodName.equals("")) { + return null; + } + + SourceMap smap = this.smapProvider.getSourceMap(className); + if (smap == null) { + return null; + } + + int[] inputLineInfo = smap.getReverseLineMapping().get(lineNumber); + if (inputLineInfo == null || inputLineInfo.length == 0) { + return null; + } + + for (int fileInfoIds : inputLineInfo) { + SourceMap.FileInfo inputFileInfo = smap.getFileInfo().get(fileInfoIds); + if (inputFileInfo == null) { + continue; + } + + String path = inputFileInfo.path(); + if (path.endsWith(".java")) { + path = path.substring(0, path.length() - 5); + } + + String possibleMixinClassName = path.replace('/', '.'); + if (possibleMixinClassName.equals(className)) { + continue; + } + + return modIdFromMixinClass(possibleMixinClassName); + } + + return null; + } + + private static String modIdFromMixinClass(String mixinClassName) { + for (Config config : MixinUtils.getMixinConfigs().values()) { + IMixinConfig mixinConfig = config.getConfig(); + if (mixinClassName.startsWith(mixinConfig.getMixinPackage())) { + return mixinConfig.getDecoration(FabricUtil.KEY_MOD_ID); + } + } + return null; + } + + private Class[] getParameterTypesForMethodDesc(String methodDesc) { + Type methodType = Type.getMethodType(methodDesc); + Class[] params = new Class[methodType.getArgumentTypes().length]; + Type[] argumentTypes = methodType.getArgumentTypes(); + + for (int i = 0, argumentTypesLength = argumentTypes.length; i < argumentTypesLength; i++) { + Type argumentType = argumentTypes[i]; + params[i] = getClassFromType(argumentType); + } + + return params; + } + + private Class getClassFromType(Type type) { + return switch (type.getSort()) { + case Type.VOID -> void.class; + case Type.BOOLEAN -> boolean.class; + case Type.CHAR -> char.class; + case Type.BYTE -> byte.class; + case Type.SHORT -> short.class; + case Type.INT -> int.class; + case Type.FLOAT -> float.class; + case Type.LONG -> long.class; + case Type.DOUBLE -> double.class; + case Type.ARRAY -> { + final Class classFromType = getClassFromType(type.getElementType()); + Class result = classFromType; + if (classFromType != null) { + for (int i = 0; i < type.getDimensions(); i++) { + result = result.arrayType(); + } + } + yield result; + } + case Type.OBJECT -> this.classFinder.findClass(type.getClassName()); + default -> null; + }; + } + + private static Map constructPathToModIdMap(Collection mods) { + ImmutableMap.Builder builder = ImmutableMap.builder(); for (ModContainer mod : mods) { - Path path = mod.getRootPath().toAbsolutePath().normalize(); - builder.put(path, mod.getMetadata().getId()); + String modId = mod.getMetadata().getId(); + if (modId.equals("java")) { + continue; + } + + for (Path path : mod.getRootPaths()) { + URI uri = path.toUri(); + if (uri.getScheme().equals("jar") && path.toString().equals("/")) { // ZipFileSystem + String zipFilePath = path.getFileSystem().toString(); + builder.put(zipFilePath, modId); + } else { + builder.put(path.toAbsolutePath().normalize().toString(), modId); + } + + } } return builder.build(); } diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkMixinPlugin.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkMixinPlugin.java new file mode 100644 index 0000000..cfc8c95 --- /dev/null +++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkMixinPlugin.java @@ -0,0 +1,71 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.fabric.plugin; + +import me.lucko.spark.fabric.smap.SourceDebugCache; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; +import org.spongepowered.asm.mixin.transformer.IMixinTransformer; +import org.spongepowered.asm.mixin.transformer.ext.Extensions; +import org.spongepowered.asm.mixin.transformer.ext.IExtension; +import org.spongepowered.asm.mixin.transformer.ext.ITargetClassContext; + +import java.util.List; +import java.util.Set; + +public class FabricSparkMixinPlugin implements IMixinConfigPlugin, IExtension { + + private static final Logger LOGGER = LogManager.getLogger("spark"); + + @Override + public void onLoad(String mixinPackage) { + Object activeTransformer = MixinEnvironment.getCurrentEnvironment().getActiveTransformer(); + if (activeTransformer instanceof IMixinTransformer transformer && transformer.getExtensions() instanceof Extensions extensions) { + extensions.add(this); + } else { + LOGGER.error( + "Failed to initialize SMAP parser for spark profiler. " + + "Mod information for mixin injected methods is now only available with the async-profiler engine." + ); + } + } + + @Override + public void export(MixinEnvironment env, String name, boolean force, ClassNode classNode) { + SourceDebugCache.put(name, classNode); + } + + // noop + @Override public String getRefMapperConfig() { return null; } + @Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { return true; } + @Override public void acceptTargets(Set myTargets, Set otherTargets) { } + @Override public List getMixins() { return null; } + @Override public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { } + @Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { } + @Override public boolean checkActive(MixinEnvironment environment) { return true; } + @Override public void preApply(ITargetClassContext context) { } + @Override public void postApply(ITargetClassContext context) { } + +} diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/MixinUtils.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/MixinUtils.java new file mode 100644 index 0000000..ebf2766 --- /dev/null +++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/MixinUtils.java @@ -0,0 +1,52 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.fabric.smap; + +import org.spongepowered.asm.mixin.transformer.Config; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +public enum MixinUtils { + ; + + private static final Map MIXIN_CONFIGS; + + static { + Map configs; + try { + Field allConfigsField = Config.class.getDeclaredField("allConfigs"); + allConfigsField.setAccessible(true); + + //noinspection unchecked + configs = (Map) allConfigsField.get(null); + } catch (Exception e) { + e.printStackTrace(); + configs = new HashMap<>(); + } + MIXIN_CONFIGS = configs; + } + + public static Map getMixinConfigs() { + return MIXIN_CONFIGS; + } +} diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceDebugCache.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceDebugCache.java new file mode 100644 index 0000000..88adae6 --- /dev/null +++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceDebugCache.java @@ -0,0 +1,87 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.fabric.smap; + +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.service.IClassBytecodeProvider; +import org.spongepowered.asm.service.MixinService; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Caches the lookup of class -> source debug info for classes loaded on the JVM. + * + * The {@link me.lucko.spark.fabric.plugin.FabricSparkMixinPlugin} also supplements this cache with + * extra information as classes are exported. + */ +public enum SourceDebugCache { + ; + + // class name -> smap + private static final Map CACHE = new ConcurrentHashMap<>(); + + public static void put(String className, ClassNode node) { + if (className == null || node == null) { + return; + } + className = className.replace('/', '.'); + CACHE.put(className, SmapValue.of(node.sourceDebug)); + } + + public static String getSourceDebugInfo(String className) { + SmapValue cached = CACHE.get(className); + if (cached != null) { + return cached.value(); + } + + try { + IClassBytecodeProvider provider = MixinService.getService().getBytecodeProvider(); + ClassNode classNode = provider.getClassNode(className.replace('.', '/')); + + if (classNode != null) { + put(className, classNode); + return classNode.sourceDebug; + } + + } catch (Exception e) { + // ignore + } + + CACHE.put(className, SmapValue.NULL); + return null; + } + + private record SmapValue(String value) { + static final SmapValue NULL = new SmapValue(null); + + static SmapValue of(String value) { + if (value == null) { + return NULL; + } else { + return new SmapValue(value); + } + } + + } + +} diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMap.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMap.java new file mode 100644 index 0000000..5105a26 --- /dev/null +++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMap.java @@ -0,0 +1,133 @@ +/* + * SMAPSourceDebugExtension.java - Parse source debug extensions and + * enhance stack traces. + * + * Copyright (c) 2012 Michael Schierl + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither name of the copyright holders nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND THE CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDERS OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package me.lucko.spark.fabric.smap; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class to parse "SMAP" (source map) information from loaded Java classes. + * + * @author Michael Schierl + */ +public class SourceMap { + + private final String generatedFileName; + private final String firstStratum; + private final Map fileinfo = new HashMap<>(); + private final Map reverseLineMapping = new HashMap<>(); + + private static final Pattern LINE_INFO_PATTERN = Pattern.compile("([0-9]+)(?:#([0-9]+))?(?:,([0-9]+))?:([0-9]+)(?:,([0-9]+))?"); + + public SourceMap(String value) { + String[] lines = value.split("\n"); + if (!lines[0].equals("SMAP") || !lines[3].startsWith("*S ") || !lines[4].equals("*F")) { + throw new IllegalArgumentException(value); + } + + this.generatedFileName = lines[1]; + this.firstStratum = lines[3].substring(3); + + int idx = 5; + while (!lines[idx].startsWith("*")) { + String infoline = lines[idx++]; + String path = null; + + if (infoline.startsWith("+ ")) { + path = lines[idx++]; + infoline = infoline.substring(2); + } + + int pos = infoline.indexOf(" "); + int filenum = Integer.parseInt(infoline.substring(0, pos)); + String name = infoline.substring(pos + 1); + + this.fileinfo.put(filenum, new FileInfo(name, path == null ? name : path)); + } + + if (lines[idx].equals("*L")) { + idx++; + int lastLFI = 0; + + while (!lines[idx].startsWith("*")) { + Matcher m = LINE_INFO_PATTERN.matcher(lines[idx++]); + if (!m.matches()) { + throw new IllegalArgumentException(lines[idx - 1]); + } + + int inputStartLine = Integer.parseInt(m.group(1)); + int lineFileID = m.group(2) == null ? lastLFI : Integer.parseInt(m.group(2)); + int repeatCount = m.group(3) == null ? 1 : Integer.parseInt(m.group(3)); + int outputStartLine = Integer.parseInt(m.group(4)); + int outputLineIncrement = m.group(5) == null ? 1 : Integer.parseInt(m.group(5)); + + for (int i = 0; i < repeatCount; i++) { + int[] inputMapping = new int[] { lineFileID, inputStartLine + i }; + int baseOL = outputStartLine + i * outputLineIncrement; + + for (int ol = baseOL; ol < baseOL + outputLineIncrement; ol++) { + if (!this.reverseLineMapping.containsKey(ol)) { + this.reverseLineMapping.put(ol, inputMapping); + } + } + } + + lastLFI = lineFileID; + } + } + } + + public String getGeneratedFileName() { + return this.generatedFileName; + } + + public String getFirstStratum() { + return this.firstStratum; + } + + public Map getFileInfo() { + return this.fileinfo; + } + + public Map getReverseLineMapping() { + return this.reverseLineMapping; + } + + public record FileInfo(String name, String path) { } +} \ No newline at end of file diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMapProvider.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMapProvider.java new file mode 100644 index 0000000..1a4f246 --- /dev/null +++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMapProvider.java @@ -0,0 +1,53 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.fabric.smap; + +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class SourceMapProvider { + private final Map cache = new HashMap<>(); + + public @Nullable SourceMap getSourceMap(String className) { + if (this.cache.containsKey(className)) { + return this.cache.get(className); + } + + SourceMap smap = null; + try { + String value = SourceDebugCache.getSourceDebugInfo(className); + if (value != null) { + value = value.replaceAll("\r\n?", "\n"); + if (value.startsWith("SMAP\n")) { + smap = new SourceMap(value); + } + } + } catch (Exception e) { + // ignore + } + + this.cache.put(className, smap); + return smap; + } + +} diff --git a/spark-fabric/src/main/resources/spark.mixins.json b/spark-fabric/src/main/resources/spark.mixins.json index e75b34f..63c1078 100644 --- a/spark-fabric/src/main/resources/spark.mixins.json +++ b/spark-fabric/src/main/resources/spark.mixins.json @@ -11,5 +11,6 @@ "server": [ "ServerEntityManagerAccessor", "ServerWorldAccessor" - ] + ], + "plugin": "me.lucko.spark.fabric.plugin.FabricSparkMixinPlugin" } \ No newline at end of file -- cgit From 7079484d428321c9b3db09394577efda4d591a4e Mon Sep 17 00:00:00 2001 From: Luck Date: Mon, 19 Sep 2022 18:57:02 +0100 Subject: Provide extra metadata about sources in sampler data --- .../spark/bukkit/BukkitClassSourceLookup.java | 2 +- .../me/lucko/spark/bukkit/BukkitSparkPlugin.java | 16 +- .../bungeecord/BungeeCordClassSourceLookup.java | 2 +- .../spark/bungeecord/BungeeCordSparkPlugin.java | 14 +- .../java/me/lucko/spark/common/SparkPlatform.java | 2 +- .../java/me/lucko/spark/common/SparkPlugin.java | 14 +- .../common/command/modules/SamplerModule.java | 3 +- .../spark/common/sampler/AbstractSampler.java | 10 +- .../me/lucko/spark/common/sampler/Sampler.java | 2 +- .../spark/common/sampler/async/AsyncSampler.java | 2 +- .../spark/common/sampler/java/JavaSampler.java | 2 +- .../common/sampler/source/ClassSourceLookup.java | 463 +++++++++++++++++++++ .../common/sampler/source/SourceMetadata.java | 81 ++++ .../lucko/spark/common/util/ClassSourceLookup.java | 452 -------------------- .../src/main/proto/spark/spark_sampler.proto | 6 + .../spark/fabric/FabricClassSourceLookup.java | 2 +- .../spark/fabric/plugin/FabricSparkPlugin.java | 19 +- .../lucko/spark/forge/ForgeClassSourceLookup.java | 2 +- .../lucko/spark/forge/plugin/ForgeSparkPlugin.java | 16 +- .../spark/minestom/MinestomClassSourceLookup.java | 2 +- .../lucko/spark/minestom/MinestomSparkPlugin.java | 14 +- .../spark/nukkit/NukkitClassSourceLookup.java | 2 +- .../me/lucko/spark/nukkit/NukkitSparkPlugin.java | 2 +- .../spark/sponge/Sponge7ClassSourceLookup.java | 2 +- .../me/lucko/spark/sponge/Sponge7SparkPlugin.java | 2 +- .../spark/sponge/Sponge8ClassSourceLookup.java | 2 +- .../me/lucko/spark/sponge/Sponge8SparkPlugin.java | 17 +- .../spark/velocity/VelocityClassSourceLookup.java | 2 +- .../lucko/spark/velocity/VelocitySparkPlugin.java | 14 +- .../spark/velocity/Velocity4ClassSourceLookup.java | 4 +- .../lucko/spark/velocity/Velocity4SparkPlugin.java | 14 +- .../spark/waterdog/WaterdogClassSourceLookup.java | 2 +- .../lucko/spark/waterdog/WaterdogSparkPlugin.java | 14 +- 33 files changed, 721 insertions(+), 482 deletions(-) create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/source/ClassSourceLookup.java create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/source/SourceMetadata.java delete mode 100644 spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java (limited to 'spark-common/src/main/java/me/lucko/spark/common/util') diff --git a/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitClassSourceLookup.java b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitClassSourceLookup.java index 6d8afda..f9c0c0b 100644 --- a/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitClassSourceLookup.java +++ b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitClassSourceLookup.java @@ -20,7 +20,7 @@ package me.lucko.spark.bukkit; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import org.bukkit.plugin.java.JavaPlugin; diff --git a/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitSparkPlugin.java b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitSparkPlugin.java index 5737d3d..87490ea 100644 --- a/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitSparkPlugin.java +++ b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitSparkPlugin.java @@ -30,9 +30,10 @@ import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.common.platform.serverconfig.ServerConfigProvider; import me.lucko.spark.common.platform.world.WorldInfoProvider; import me.lucko.spark.common.sampler.ThreadDumper; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.tick.TickReporter; -import me.lucko.spark.common.util.ClassSourceLookup; import net.kyori.adventure.platform.bukkit.BukkitAudiences; @@ -40,10 +41,13 @@ import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; import org.bukkit.plugin.ServicePriority; import org.bukkit.plugin.java.JavaPlugin; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.logging.Level; import java.util.stream.Stream; @@ -180,6 +184,16 @@ public class BukkitSparkPlugin extends JavaPlugin implements SparkPlugin { return new BukkitClassSourceLookup(); } + @Override + public Collection getKnownSources() { + return SourceMetadata.gather( + Arrays.asList(getServer().getPluginManager().getPlugins()), + Plugin::getName, + plugin -> plugin.getDescription().getVersion(), + plugin -> String.join(", ", plugin.getDescription().getAuthors()) + ); + } + @Override public PlayerPingProvider createPlayerPingProvider() { if (BukkitPlayerPingProvider.isSupported()) { diff --git a/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordClassSourceLookup.java b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordClassSourceLookup.java index e601f87..2024d54 100644 --- a/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordClassSourceLookup.java +++ b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordClassSourceLookup.java @@ -20,7 +20,7 @@ package me.lucko.spark.bungeecord; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import net.md_5.bungee.api.plugin.PluginDescription; diff --git a/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordSparkPlugin.java b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordSparkPlugin.java index e259adc..71beddb 100644 --- a/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordSparkPlugin.java +++ b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordSparkPlugin.java @@ -24,7 +24,8 @@ import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.platform.PlatformInfo; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import net.kyori.adventure.platform.bungeecord.BungeeAudiences; import net.md_5.bungee.api.CommandSender; @@ -33,6 +34,7 @@ import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.TabExecutor; import java.nio.file.Path; +import java.util.Collection; import java.util.logging.Level; import java.util.stream.Stream; @@ -91,6 +93,16 @@ public class BungeeCordSparkPlugin extends Plugin implements SparkPlugin { return new BungeeCordClassSourceLookup(); } + @Override + public Collection getKnownSources() { + return SourceMetadata.gather( + getProxy().getPluginManager().getPlugins(), + plugin -> plugin.getDescription().getName(), + plugin -> plugin.getDescription().getVersion(), + plugin -> plugin.getDescription().getAuthor() + ); + } + @Override public PlayerPingProvider createPlayerPingProvider() { return new BungeeCordPlayerPingProvider(getProxy()); diff --git a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java index f92abf3..1969206 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java +++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java @@ -45,10 +45,10 @@ import me.lucko.spark.common.monitor.ping.PingStatistics; import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.monitor.tick.TickStatistics; import me.lucko.spark.common.platform.PlatformStatisticsProvider; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.tick.TickReporter; import me.lucko.spark.common.util.BytebinClient; -import me.lucko.spark.common.util.ClassSourceLookup; import me.lucko.spark.common.util.Configuration; import me.lucko.spark.common.util.TemporaryFiles; diff --git a/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java b/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java index 1116b04..e2a2dbd 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java +++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java @@ -27,11 +27,14 @@ import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.common.platform.serverconfig.ServerConfigProvider; import me.lucko.spark.common.platform.world.WorldInfoProvider; import me.lucko.spark.common.sampler.ThreadDumper; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.tick.TickReporter; -import me.lucko.spark.common.util.ClassSourceLookup; import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; import java.util.logging.Level; import java.util.stream.Stream; @@ -132,6 +135,15 @@ public interface SparkPlugin { return ClassSourceLookup.NO_OP; } + /** + * Gets a list of known sources (plugins/mods) on the platform. + * + * @return a list of sources + */ + default Collection getKnownSources() { + return Collections.emptyList(); + } + /** * Creates a player ping provider function. * diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java index 0a80c31..2afed64 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java @@ -38,6 +38,7 @@ import me.lucko.spark.common.sampler.ThreadGrouper; import me.lucko.spark.common.sampler.ThreadNodeOrder; import me.lucko.spark.common.sampler.async.AsyncSampler; import me.lucko.spark.common.sampler.node.MergeMode; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.util.MethodDisambiguator; import me.lucko.spark.proto.SparkSamplerProtos; @@ -303,7 +304,7 @@ public class SamplerModule implements CommandModule { } private void handleUpload(SparkPlatform platform, CommandResponseHandler resp, Sampler sampler, ThreadNodeOrder threadOrder, String comment, MergeMode mergeMode, boolean saveToFileFlag) { - SparkSamplerProtos.SamplerData output = sampler.toProto(platform, resp.sender(), threadOrder, comment, mergeMode, platform.createClassSourceLookup()); + SparkSamplerProtos.SamplerData output = sampler.toProto(platform, resp.sender(), threadOrder, comment, mergeMode, ClassSourceLookup.create(platform)); boolean saveToFile = false; if (saveToFileFlag) { diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java index 3cfef0b..7b57504 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java @@ -27,13 +27,16 @@ import me.lucko.spark.common.platform.serverconfig.ServerConfigProvider; import me.lucko.spark.common.sampler.aggregator.DataAggregator; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import me.lucko.spark.common.tick.TickHook; -import me.lucko.spark.common.util.ClassSourceLookup; import me.lucko.spark.proto.SparkSamplerProtos.SamplerData; import me.lucko.spark.proto.SparkSamplerProtos.SamplerMetadata; +import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -150,6 +153,11 @@ public abstract class AbstractSampler implements Sampler { e.printStackTrace(); } + Collection knownSources = platform.getPlugin().getKnownSources(); + for (SourceMetadata source : knownSources) { + metadata.putSources(source.getName().toLowerCase(Locale.ROOT), source.toProto()); + } + proto.setMetadata(metadata); } diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java index 84f2da1..98281de 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java @@ -24,7 +24,7 @@ import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.command.sender.CommandSender; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.proto.SparkSamplerProtos.SamplerData; import java.util.Comparator; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java index dae3852..37ccd96 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java @@ -30,7 +30,7 @@ import me.lucko.spark.common.sampler.ThreadGrouper; import me.lucko.spark.common.sampler.async.jfr.JfrReader; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.util.TemporaryFiles; import me.lucko.spark.proto.SparkSamplerProtos.SamplerData; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java index 913faee..0f73a9f 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java @@ -29,8 +29,8 @@ import me.lucko.spark.common.sampler.ThreadDumper; import me.lucko.spark.common.sampler.ThreadGrouper; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.tick.TickHook; -import me.lucko.spark.common.util.ClassSourceLookup; import me.lucko.spark.proto.SparkSamplerProtos.SamplerData; import java.lang.management.ManagementFactory; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/source/ClassSourceLookup.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/source/ClassSourceLookup.java new file mode 100644 index 0000000..66b41d2 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/source/ClassSourceLookup.java @@ -0,0 +1,463 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.common.sampler.source; + +import me.lucko.spark.common.SparkPlatform; +import me.lucko.spark.common.sampler.node.StackTraceNode; +import me.lucko.spark.common.sampler.node.ThreadNode; +import me.lucko.spark.common.util.ClassFinder; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * A function which defines the source of given {@link Class}es or (Mixin) method calls. + */ +public interface ClassSourceLookup { + + /** + * Identify the given class. + * + * @param clazz the class + * @return the source of the class + */ + @Nullable String identify(Class clazz) throws Exception; + + /** + * Identify the given method call. + * + * @param methodCall the method call info + * @return the source of the method call + */ + default @Nullable String identify(MethodCall methodCall) throws Exception { + return null; + } + + /** + * Identify the given method call. + * + * @param methodCall the method call info + * @return the source of the method call + */ + default @Nullable String identify(MethodCallByLine methodCall) throws Exception { + return null; + } + + /** + * A no-operation {@link ClassSourceLookup}. + */ + ClassSourceLookup NO_OP = new ClassSourceLookup() { + @Override + public @Nullable String identify(Class clazz) { + return null; + } + }; + + static ClassSourceLookup create(SparkPlatform platform) { + try { + return platform.createClassSourceLookup(); + } catch (Exception e) { + e.printStackTrace(); + return NO_OP; + } + } + + /** + * A {@link ClassSourceLookup} which identifies classes based on their {@link ClassLoader}. + */ + abstract class ByClassLoader implements ClassSourceLookup { + + public abstract @Nullable String identify(ClassLoader loader) throws Exception; + + @Override + public final @Nullable String identify(Class clazz) throws Exception { + ClassLoader loader = clazz.getClassLoader(); + while (loader != null) { + String source = identify(loader); + if (source != null) { + return source; + } + loader = loader.getParent(); + } + return null; + } + } + + /** + * A {@link ClassSourceLookup} which identifies classes based on URL. + */ + interface ByUrl extends ClassSourceLookup { + + default String identifyUrl(URL url) throws URISyntaxException, MalformedURLException { + Path path = null; + + String protocol = url.getProtocol(); + if (protocol.equals("file")) { + path = Paths.get(url.toURI()); + } else if (protocol.equals("jar")) { + URL innerUrl = new URL(url.getPath()); + path = Paths.get(innerUrl.getPath().split("!")[0]); + } + + if (path != null) { + return identifyFile(path.toAbsolutePath().normalize()); + } + + return null; + } + + default String identifyFile(Path path) { + return identifyFileName(path.getFileName().toString()); + } + + default String identifyFileName(String fileName) { + return fileName.endsWith(".jar") ? fileName.substring(0, fileName.length() - 4) : null; + } + } + + /** + * A {@link ClassSourceLookup} which identifies classes based on the first URL in a {@link URLClassLoader}. + */ + class ByFirstUrlSource extends ClassSourceLookup.ByClassLoader implements ClassSourceLookup.ByUrl { + @Override + public @Nullable String identify(ClassLoader loader) throws IOException, URISyntaxException { + if (loader instanceof URLClassLoader) { + URLClassLoader urlClassLoader = (URLClassLoader) loader; + URL[] urls = urlClassLoader.getURLs(); + if (urls.length == 0) { + return null; + } + return identifyUrl(urls[0]); + } + return null; + } + } + + /** + * A {@link ClassSourceLookup} which identifies classes based on their {@link ProtectionDomain#getCodeSource()}. + */ + class ByCodeSource implements ClassSourceLookup, ClassSourceLookup.ByUrl { + @Override + public @Nullable String identify(Class clazz) throws URISyntaxException, MalformedURLException { + ProtectionDomain protectionDomain = clazz.getProtectionDomain(); + if (protectionDomain == null) { + return null; + } + CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource == null) { + return null; + } + + URL url = codeSource.getLocation(); + return url == null ? null : identifyUrl(url); + } + } + + interface Visitor { + void visit(ThreadNode node); + + boolean hasClassSourceMappings(); + + Map getClassSourceMapping(); + + boolean hasMethodSourceMappings(); + + Map getMethodSourceMapping(); + + boolean hasLineSourceMappings(); + + Map getLineSourceMapping(); + } + + static Visitor createVisitor(ClassSourceLookup lookup) { + if (lookup == ClassSourceLookup.NO_OP) { + return NoOpVisitor.INSTANCE; // don't bother! + } + return new VisitorImpl(lookup); + } + + enum NoOpVisitor implements Visitor { + INSTANCE; + + @Override + public void visit(ThreadNode node) { + + } + + @Override + public boolean hasClassSourceMappings() { + return false; + } + + @Override + public Map getClassSourceMapping() { + return Collections.emptyMap(); + } + + @Override + public boolean hasMethodSourceMappings() { + return false; + } + + @Override + public Map getMethodSourceMapping() { + return Collections.emptyMap(); + } + + @Override + public boolean hasLineSourceMappings() { + return false; + } + + @Override + public Map getLineSourceMapping() { + return Collections.emptyMap(); + } + } + + /** + * Visitor which scans {@link StackTraceNode}s and accumulates class/method call identities. + */ + class VisitorImpl implements Visitor { + private final ClassSourceLookup lookup; + private final ClassFinder classFinder = new ClassFinder(); + + private final SourcesMap classSources = new SourcesMap<>(Function.identity()); + private final SourcesMap methodSources = new SourcesMap<>(MethodCall::toString); + private final SourcesMap lineSources = new SourcesMap<>(MethodCallByLine::toString); + + VisitorImpl(ClassSourceLookup lookup) { + this.lookup = lookup; + } + + @Override + public void visit(ThreadNode node) { + for (StackTraceNode child : node.getChildren()) { + visitStackNode(child); + } + } + + private void visitStackNode(StackTraceNode node) { + this.classSources.computeIfAbsent( + node.getClassName(), + className -> { + Class clazz = this.classFinder.findClass(className); + if (clazz == null) { + return null; + } + return this.lookup.identify(clazz); + }); + + if (node.getMethodDescription() != null) { + MethodCall methodCall = new MethodCall(node.getClassName(), node.getMethodName(), node.getMethodDescription()); + this.methodSources.computeIfAbsent(methodCall, this.lookup::identify); + } else { + MethodCallByLine methodCall = new MethodCallByLine(node.getClassName(), node.getMethodName(), node.getLineNumber()); + this.lineSources.computeIfAbsent(methodCall, this.lookup::identify); + } + + // recursively + for (StackTraceNode child : node.getChildren()) { + visitStackNode(child); + } + } + + @Override + public boolean hasClassSourceMappings() { + return this.classSources.hasMappings(); + } + + @Override + public Map getClassSourceMapping() { + return this.classSources.export(); + } + + @Override + public boolean hasMethodSourceMappings() { + return this.methodSources.hasMappings(); + } + + @Override + public Map getMethodSourceMapping() { + return this.methodSources.export(); + } + + @Override + public boolean hasLineSourceMappings() { + return this.lineSources.hasMappings(); + } + + @Override + public Map getLineSourceMapping() { + return this.lineSources.export(); + } + } + + final class SourcesMap { + // --> identifier (plugin name) + private final Map map = new HashMap<>(); + private final Function keyToStringFunction; + + private SourcesMap(Function keyToStringFunction) { + this.keyToStringFunction = keyToStringFunction; + } + + public void computeIfAbsent(T key, ComputeSourceFunction function) { + if (!this.map.containsKey(key)) { + try { + this.map.put(key, function.compute(key)); + } catch (Throwable e) { + this.map.put(key, null); + } + } + } + + public boolean hasMappings() { + this.map.values().removeIf(Objects::isNull); + return !this.map.isEmpty(); + } + + public Map export() { + this.map.values().removeIf(Objects::isNull); + if (this.keyToStringFunction.equals(Function.identity())) { + //noinspection unchecked + return (Map) this.map; + } else { + return this.map.entrySet().stream().collect(Collectors.toMap( + e -> this.keyToStringFunction.apply(e.getKey()), + Map.Entry::getValue + )); + } + } + + private interface ComputeSourceFunction { + String compute(T key) throws Exception; + } + } + + /** + * Encapsulates information about a given method call using the name + method description. + */ + final class MethodCall { + private final String className; + private final String methodName; + private final String methodDescriptor; + + public MethodCall(String className, String methodName, String methodDescriptor) { + this.className = className; + this.methodName = methodName; + this.methodDescriptor = methodDescriptor; + } + + public String getClassName() { + return this.className; + } + + public String getMethodName() { + return this.methodName; + } + + public String getMethodDescriptor() { + return this.methodDescriptor; + } + + @Override + public String toString() { + return this.className + ";" + this.methodName + ";" + this.methodDescriptor; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MethodCall)) return false; + MethodCall that = (MethodCall) o; + return this.className.equals(that.className) && + this.methodName.equals(that.methodName) && + this.methodDescriptor.equals(that.methodDescriptor); + } + + @Override + public int hashCode() { + return Objects.hash(this.className, this.methodName, this.methodDescriptor); + } + } + + /** + * Encapsulates information about a given method call using the name + line number. + */ + final class MethodCallByLine { + private final String className; + private final String methodName; + private final int lineNumber; + + public MethodCallByLine(String className, String methodName, int lineNumber) { + this.className = className; + this.methodName = methodName; + this.lineNumber = lineNumber; + } + + public String getClassName() { + return this.className; + } + + public String getMethodName() { + return this.methodName; + } + + public int getLineNumber() { + return this.lineNumber; + } + + @Override + public String toString() { + return this.className + ";" + this.lineNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MethodCallByLine)) return false; + MethodCallByLine that = (MethodCallByLine) o; + return this.lineNumber == that.lineNumber && this.className.equals(that.className); + } + + @Override + public int hashCode() { + return Objects.hash(this.className, this.lineNumber); + } + } + +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/source/SourceMetadata.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/source/SourceMetadata.java new file mode 100644 index 0000000..0808d66 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/source/SourceMetadata.java @@ -0,0 +1,81 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.common.sampler.source; + +import com.google.common.collect.ImmutableList; + +import me.lucko.spark.proto.SparkSamplerProtos.SamplerMetadata; + +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +/** + * A "source" is a plugin or mod on the platform that may be identified + * as a source of a method call in a profile. + */ +public class SourceMetadata { + + public static List gather(Collection sources, Function nameFunction, Function versionFunction, Function authorFunction) { + ImmutableList.Builder builder = ImmutableList.builder(); + + for (T source : sources) { + String name = nameFunction.apply(source); + String version = versionFunction.apply(source); + String author = authorFunction.apply(source); + + SourceMetadata metadata = new SourceMetadata(name, version, author); + builder.add(metadata); + } + + return builder.build(); + } + + private final String name; + private final String version; + private final String author; + + public SourceMetadata(String name, String version, String author) { + this.name = name; + this.version = version; + this.author = author; + } + + public String getName() { + return this.name; + } + + public String getVersion() { + return this.version; + } + + public String getAuthor() { + return this.author; + } + + public SamplerMetadata.SourceMetadata toProto() { + return SamplerMetadata.SourceMetadata.newBuilder() + .setName(this.name) + .setVersion(this.version) + .build(); + } + +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java b/spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java deleted file mode 100644 index 668f31a..0000000 --- a/spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java +++ /dev/null @@ -1,452 +0,0 @@ -/* - * This file is part of spark. - * - * Copyright (c) lucko (Luck) - * Copyright (c) contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package me.lucko.spark.common.util; - -import me.lucko.spark.common.sampler.node.StackTraceNode; -import me.lucko.spark.common.sampler.node.ThreadNode; - -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.CodeSource; -import java.security.ProtectionDomain; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * A function which defines the source of given {@link Class}es or (Mixin) method calls. - */ -public interface ClassSourceLookup { - - /** - * Identify the given class. - * - * @param clazz the class - * @return the source of the class - */ - @Nullable String identify(Class clazz) throws Exception; - - /** - * Identify the given method call. - * - * @param methodCall the method call info - * @return the source of the method call - */ - default @Nullable String identify(MethodCall methodCall) throws Exception { - return null; - } - - /** - * Identify the given method call. - * - * @param methodCall the method call info - * @return the source of the method call - */ - default @Nullable String identify(MethodCallByLine methodCall) throws Exception { - return null; - } - - /** - * A no-operation {@link ClassSourceLookup}. - */ - ClassSourceLookup NO_OP = new ClassSourceLookup() { - @Override - public @Nullable String identify(Class clazz) { - return null; - } - }; - - /** - * A {@link ClassSourceLookup} which identifies classes based on their {@link ClassLoader}. - */ - abstract class ByClassLoader implements ClassSourceLookup { - - public abstract @Nullable String identify(ClassLoader loader) throws Exception; - - @Override - public final @Nullable String identify(Class clazz) throws Exception { - ClassLoader loader = clazz.getClassLoader(); - while (loader != null) { - String source = identify(loader); - if (source != null) { - return source; - } - loader = loader.getParent(); - } - return null; - } - } - - /** - * A {@link ClassSourceLookup} which identifies classes based on URL. - */ - interface ByUrl extends ClassSourceLookup { - - default String identifyUrl(URL url) throws URISyntaxException, MalformedURLException { - Path path = null; - - String protocol = url.getProtocol(); - if (protocol.equals("file")) { - path = Paths.get(url.toURI()); - } else if (protocol.equals("jar")) { - URL innerUrl = new URL(url.getPath()); - path = Paths.get(innerUrl.getPath().split("!")[0]); - } - - if (path != null) { - return identifyFile(path.toAbsolutePath().normalize()); - } - - return null; - } - - default String identifyFile(Path path) { - return identifyFileName(path.getFileName().toString()); - } - - default String identifyFileName(String fileName) { - return fileName.endsWith(".jar") ? fileName.substring(0, fileName.length() - 4) : null; - } - } - - /** - * A {@link ClassSourceLookup} which identifies classes based on the first URL in a {@link URLClassLoader}. - */ - class ByFirstUrlSource extends ByClassLoader implements ByUrl { - @Override - public @Nullable String identify(ClassLoader loader) throws IOException, URISyntaxException { - if (loader instanceof URLClassLoader) { - URLClassLoader urlClassLoader = (URLClassLoader) loader; - URL[] urls = urlClassLoader.getURLs(); - if (urls.length == 0) { - return null; - } - return identifyUrl(urls[0]); - } - return null; - } - } - - /** - * A {@link ClassSourceLookup} which identifies classes based on their {@link ProtectionDomain#getCodeSource()}. - */ - class ByCodeSource implements ClassSourceLookup, ByUrl { - @Override - public @Nullable String identify(Class clazz) throws URISyntaxException, MalformedURLException { - ProtectionDomain protectionDomain = clazz.getProtectionDomain(); - if (protectionDomain == null) { - return null; - } - CodeSource codeSource = protectionDomain.getCodeSource(); - if (codeSource == null) { - return null; - } - - URL url = codeSource.getLocation(); - return url == null ? null : identifyUrl(url); - } - } - - interface Visitor { - void visit(ThreadNode node); - - boolean hasClassSourceMappings(); - - Map getClassSourceMapping(); - - boolean hasMethodSourceMappings(); - - Map getMethodSourceMapping(); - - boolean hasLineSourceMappings(); - - Map getLineSourceMapping(); - } - - static Visitor createVisitor(ClassSourceLookup lookup) { - if (lookup == ClassSourceLookup.NO_OP) { - return NoOpVisitor.INSTANCE; // don't bother! - } - return new VisitorImpl(lookup); - } - - enum NoOpVisitor implements Visitor { - INSTANCE; - - @Override - public void visit(ThreadNode node) { - - } - - @Override - public boolean hasClassSourceMappings() { - return false; - } - - @Override - public Map getClassSourceMapping() { - return Collections.emptyMap(); - } - - @Override - public boolean hasMethodSourceMappings() { - return false; - } - - @Override - public Map getMethodSourceMapping() { - return Collections.emptyMap(); - } - - @Override - public boolean hasLineSourceMappings() { - return false; - } - - @Override - public Map getLineSourceMapping() { - return Collections.emptyMap(); - } - } - - /** - * Visitor which scans {@link StackTraceNode}s and accumulates class/method call identities. - */ - class VisitorImpl implements Visitor { - private final ClassSourceLookup lookup; - private final ClassFinder classFinder = new ClassFinder(); - - private final SourcesMap classSources = new SourcesMap<>(Function.identity()); - private final SourcesMap methodSources = new SourcesMap<>(MethodCall::toString); - private final SourcesMap lineSources = new SourcesMap<>(MethodCallByLine::toString); - - VisitorImpl(ClassSourceLookup lookup) { - this.lookup = lookup; - } - - @Override - public void visit(ThreadNode node) { - for (StackTraceNode child : node.getChildren()) { - visitStackNode(child); - } - } - - private void visitStackNode(StackTraceNode node) { - this.classSources.computeIfAbsent( - node.getClassName(), - className -> { - Class clazz = this.classFinder.findClass(className); - if (clazz == null) { - return null; - } - return this.lookup.identify(clazz); - }); - - if (node.getMethodDescription() != null) { - MethodCall methodCall = new MethodCall(node.getClassName(), node.getMethodName(), node.getMethodDescription()); - this.methodSources.computeIfAbsent(methodCall, this.lookup::identify); - } else { - MethodCallByLine methodCall = new MethodCallByLine(node.getClassName(), node.getMethodName(), node.getLineNumber()); - this.lineSources.computeIfAbsent(methodCall, this.lookup::identify); - } - - // recursively - for (StackTraceNode child : node.getChildren()) { - visitStackNode(child); - } - } - - @Override - public boolean hasClassSourceMappings() { - return this.classSources.hasMappings(); - } - - @Override - public Map getClassSourceMapping() { - return this.classSources.export(); - } - - @Override - public boolean hasMethodSourceMappings() { - return this.methodSources.hasMappings(); - } - - @Override - public Map getMethodSourceMapping() { - return this.methodSources.export(); - } - - @Override - public boolean hasLineSourceMappings() { - return this.lineSources.hasMappings(); - } - - @Override - public Map getLineSourceMapping() { - return this.lineSources.export(); - } - } - - final class SourcesMap { - // --> identifier (plugin name) - private final Map map = new HashMap<>(); - private final Function keyToStringFunction; - - private SourcesMap(Function keyToStringFunction) { - this.keyToStringFunction = keyToStringFunction; - } - - public void computeIfAbsent(T key, ComputeSourceFunction function) { - if (!this.map.containsKey(key)) { - try { - this.map.put(key, function.compute(key)); - } catch (Throwable e) { - this.map.put(key, null); - } - } - } - - public boolean hasMappings() { - this.map.values().removeIf(Objects::isNull); - return !this.map.isEmpty(); - } - - public Map export() { - this.map.values().removeIf(Objects::isNull); - if (this.keyToStringFunction.equals(Function.identity())) { - //noinspection unchecked - return (Map) this.map; - } else { - return this.map.entrySet().stream().collect(Collectors.toMap( - e -> this.keyToStringFunction.apply(e.getKey()), - Map.Entry::getValue - )); - } - } - - private interface ComputeSourceFunction { - String compute(T key) throws Exception; - } - } - - /** - * Encapsulates information about a given method call using the name + method description. - */ - final class MethodCall { - private final String className; - private final String methodName; - private final String methodDescriptor; - - public MethodCall(String className, String methodName, String methodDescriptor) { - this.className = className; - this.methodName = methodName; - this.methodDescriptor = methodDescriptor; - } - - public String getClassName() { - return this.className; - } - - public String getMethodName() { - return this.methodName; - } - - public String getMethodDescriptor() { - return this.methodDescriptor; - } - - @Override - public String toString() { - return this.className + ";" + this.methodName + ";" + this.methodDescriptor; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof MethodCall)) return false; - MethodCall that = (MethodCall) o; - return this.className.equals(that.className) && - this.methodName.equals(that.methodName) && - this.methodDescriptor.equals(that.methodDescriptor); - } - - @Override - public int hashCode() { - return Objects.hash(this.className, this.methodName, this.methodDescriptor); - } - } - - /** - * Encapsulates information about a given method call using the name + line number. - */ - final class MethodCallByLine { - private final String className; - private final String methodName; - private final int lineNumber; - - public MethodCallByLine(String className, String methodName, int lineNumber) { - this.className = className; - this.methodName = methodName; - this.lineNumber = lineNumber; - } - - public String getClassName() { - return this.className; - } - - public String getMethodName() { - return this.methodName; - } - - public int getLineNumber() { - return this.lineNumber; - } - - @Override - public String toString() { - return this.className + ";" + this.lineNumber; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof MethodCallByLine)) return false; - MethodCallByLine that = (MethodCallByLine) o; - return this.lineNumber == that.lineNumber && this.className.equals(that.className); - } - - @Override - public int hashCode() { - return Objects.hash(this.className, this.lineNumber); - } - } - -} diff --git a/spark-common/src/main/proto/spark/spark_sampler.proto b/spark-common/src/main/proto/spark/spark_sampler.proto index f670ddf..e4c2481 100644 --- a/spark-common/src/main/proto/spark/spark_sampler.proto +++ b/spark-common/src/main/proto/spark/spark_sampler.proto @@ -28,6 +28,7 @@ message SamplerMetadata { map server_configurations = 10; int64 end_time = 11; int32 number_of_ticks = 12; + map sources = 13; message ThreadDumper { Type type = 1; @@ -58,6 +59,11 @@ message SamplerMetadata { AS_ONE = 2; } } + + message SourceMetadata { + string name = 1; + string version = 2; + } } message ThreadNode { diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java index 9ffac18..51834fc 100644 --- a/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java +++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java @@ -22,8 +22,8 @@ package me.lucko.spark.fabric; import com.google.common.collect.ImmutableMap; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.util.ClassFinder; -import me.lucko.spark.common.util.ClassSourceLookup; import me.lucko.spark.fabric.smap.MixinUtils; import me.lucko.spark.fabric.smap.SourceMap; import me.lucko.spark.fabric.smap.SourceMapProvider; diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkPlugin.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkPlugin.java index 3126f28..9a03b4e 100644 --- a/spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkPlugin.java +++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkPlugin.java @@ -34,11 +34,14 @@ import com.mojang.brigadier.tree.LiteralCommandNode; 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.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import me.lucko.spark.common.util.SparkThreadFactory; import me.lucko.spark.fabric.FabricClassSourceLookup; import me.lucko.spark.fabric.FabricSparkMod; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.metadata.Person; import net.minecraft.server.command.CommandOutput; import org.apache.logging.log4j.LogManager; @@ -46,10 +49,12 @@ import org.apache.logging.log4j.Logger; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collection; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.logging.Level; +import java.util.stream.Collectors; public abstract class FabricSparkPlugin implements SparkPlugin { @@ -110,6 +115,18 @@ public abstract class FabricSparkPlugin implements SparkPlugin { return new FabricClassSourceLookup(); } + @Override + public Collection getKnownSources() { + return SourceMetadata.gather( + FabricLoader.getInstance().getAllMods(), + mod -> mod.getMetadata().getId(), + mod -> mod.getMetadata().getVersion().getFriendlyString(), + mod -> mod.getMetadata().getAuthors().stream() + .map(Person::getName) + .collect(Collectors.joining(", ")) + ); + } + protected CompletableFuture generateSuggestions(CommandSender sender, String[] args, SuggestionsBuilder builder) { SuggestionsBuilder suggestions; diff --git a/spark-forge/src/main/java/me/lucko/spark/forge/ForgeClassSourceLookup.java b/spark-forge/src/main/java/me/lucko/spark/forge/ForgeClassSourceLookup.java index 7900bc3..82d66ca 100644 --- a/spark-forge/src/main/java/me/lucko/spark/forge/ForgeClassSourceLookup.java +++ b/spark-forge/src/main/java/me/lucko/spark/forge/ForgeClassSourceLookup.java @@ -20,7 +20,7 @@ package me.lucko.spark.forge; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import cpw.mods.modlauncher.TransformingClassLoader; diff --git a/spark-forge/src/main/java/me/lucko/spark/forge/plugin/ForgeSparkPlugin.java b/spark-forge/src/main/java/me/lucko/spark/forge/plugin/ForgeSparkPlugin.java index 36a7ce8..56061b9 100644 --- a/spark-forge/src/main/java/me/lucko/spark/forge/plugin/ForgeSparkPlugin.java +++ b/spark-forge/src/main/java/me/lucko/spark/forge/plugin/ForgeSparkPlugin.java @@ -34,18 +34,22 @@ import com.mojang.brigadier.tree.LiteralCommandNode; 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.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import me.lucko.spark.common.util.SparkThreadFactory; import me.lucko.spark.forge.ForgeClassSourceLookup; import me.lucko.spark.forge.ForgeSparkMod; import net.minecraft.commands.CommandSource; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.forgespi.language.IModInfo; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collection; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -110,6 +114,16 @@ public abstract class ForgeSparkPlugin implements SparkPlugin { return new ForgeClassSourceLookup(); } + @Override + public Collection getKnownSources() { + return SourceMetadata.gather( + ModList.get().getMods(), + IModInfo::getModId, + mod -> mod.getVersion().toString(), + mod -> null // ? + ); + } + protected CompletableFuture generateSuggestions(CommandSender sender, String[] args, SuggestionsBuilder builder) { SuggestionsBuilder suggestions; diff --git a/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomClassSourceLookup.java b/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomClassSourceLookup.java index 252060e..ca44eea 100644 --- a/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomClassSourceLookup.java +++ b/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomClassSourceLookup.java @@ -20,7 +20,7 @@ package me.lucko.spark.minestom; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import net.minestom.server.MinecraftServer; import net.minestom.server.extensions.Extension; diff --git a/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomSparkPlugin.java b/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomSparkPlugin.java index 2b43cae..9014476 100644 --- a/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomSparkPlugin.java +++ b/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomSparkPlugin.java @@ -24,9 +24,10 @@ import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.platform.PlatformInfo; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.tick.TickReporter; -import me.lucko.spark.common.util.ClassSourceLookup; import net.minestom.server.MinecraftServer; import net.minestom.server.command.CommandSender; @@ -45,6 +46,7 @@ import org.jetbrains.annotations.NotNull; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collection; import java.util.logging.Level; import java.util.stream.Stream; @@ -117,6 +119,16 @@ public class MinestomSparkPlugin extends Extension implements SparkPlugin { return new MinestomClassSourceLookup(); } + @Override + public Collection getKnownSources() { + return SourceMetadata.gather( + MinecraftServer.getExtensionManager().getExtensions(), + extension -> extension.getOrigin().getName(), + extension -> extension.getOrigin().getVersion(), + extension -> String.join(", ", extension.getOrigin().getAuthors()) + ); + } + @Override public PlayerPingProvider createPlayerPingProvider() { return new MinestomPlayerPingProvider(); diff --git a/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitClassSourceLookup.java b/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitClassSourceLookup.java index 4fed396..180e0af 100644 --- a/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitClassSourceLookup.java +++ b/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitClassSourceLookup.java @@ -20,7 +20,7 @@ package me.lucko.spark.nukkit; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import cn.nukkit.plugin.PluginClassLoader; diff --git a/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitSparkPlugin.java b/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitSparkPlugin.java index 87d9f09..ae21241 100644 --- a/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitSparkPlugin.java +++ b/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitSparkPlugin.java @@ -25,7 +25,7 @@ import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.platform.PlatformInfo; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import cn.nukkit.command.Command; import cn.nukkit.command.CommandSender; diff --git a/spark-sponge7/src/main/java/me/lucko/spark/sponge/Sponge7ClassSourceLookup.java b/spark-sponge7/src/main/java/me/lucko/spark/sponge/Sponge7ClassSourceLookup.java index 90f3b8f..899ce58 100644 --- a/spark-sponge7/src/main/java/me/lucko/spark/sponge/Sponge7ClassSourceLookup.java +++ b/spark-sponge7/src/main/java/me/lucko/spark/sponge/Sponge7ClassSourceLookup.java @@ -20,7 +20,7 @@ package me.lucko.spark.sponge; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import org.spongepowered.api.Game; diff --git a/spark-sponge7/src/main/java/me/lucko/spark/sponge/Sponge7SparkPlugin.java b/spark-sponge7/src/main/java/me/lucko/spark/sponge/Sponge7SparkPlugin.java index e6c9a04..0e3f4eb 100644 --- a/spark-sponge7/src/main/java/me/lucko/spark/sponge/Sponge7SparkPlugin.java +++ b/spark-sponge7/src/main/java/me/lucko/spark/sponge/Sponge7SparkPlugin.java @@ -29,8 +29,8 @@ import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.common.platform.world.WorldInfoProvider; import me.lucko.spark.common.sampler.ThreadDumper; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.tick.TickHook; -import me.lucko.spark.common.util.ClassSourceLookup; import org.slf4j.Logger; import org.spongepowered.api.Game; diff --git a/spark-sponge8/src/main/java/me/lucko/spark/sponge/Sponge8ClassSourceLookup.java b/spark-sponge8/src/main/java/me/lucko/spark/sponge/Sponge8ClassSourceLookup.java index fa4ac45..7f02e75 100644 --- a/spark-sponge8/src/main/java/me/lucko/spark/sponge/Sponge8ClassSourceLookup.java +++ b/spark-sponge8/src/main/java/me/lucko/spark/sponge/Sponge8ClassSourceLookup.java @@ -22,7 +22,7 @@ package me.lucko.spark.sponge; import com.google.common.collect.ImmutableMap; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import org.spongepowered.api.Game; import org.spongepowered.plugin.PluginCandidate; diff --git a/spark-sponge8/src/main/java/me/lucko/spark/sponge/Sponge8SparkPlugin.java b/spark-sponge8/src/main/java/me/lucko/spark/sponge/Sponge8SparkPlugin.java index 83b2ec2..b1d31e9 100644 --- a/spark-sponge8/src/main/java/me/lucko/spark/sponge/Sponge8SparkPlugin.java +++ b/spark-sponge8/src/main/java/me/lucko/spark/sponge/Sponge8SparkPlugin.java @@ -30,8 +30,9 @@ import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.common.platform.world.WorldInfoProvider; import me.lucko.spark.common.sampler.ThreadDumper; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import me.lucko.spark.common.tick.TickHook; -import me.lucko.spark.common.util.ClassSourceLookup; import net.kyori.adventure.text.Component; @@ -52,8 +53,10 @@ import org.spongepowered.api.event.lifecycle.StartedEngineEvent; import org.spongepowered.api.event.lifecycle.StoppingEngineEvent; import org.spongepowered.plugin.PluginContainer; import org.spongepowered.plugin.builtin.jvm.Plugin; +import org.spongepowered.plugin.metadata.model.PluginContributor; import java.nio.file.Path; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutorService; @@ -177,6 +180,18 @@ public class Sponge8SparkPlugin implements SparkPlugin { return new Sponge8ClassSourceLookup(this.game); } + @Override + public Collection getKnownSources() { + return SourceMetadata.gather( + this.game.pluginManager().plugins(), + plugin -> plugin.metadata().id(), + plugin -> plugin.metadata().version().toString(), + plugin -> plugin.metadata().contributors().stream() + .map(PluginContributor::name) + .collect(Collectors.joining(", ")) + ); + } + @Override public PlayerPingProvider createPlayerPingProvider() { if (this.game.isServerAvailable()) { diff --git a/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocityClassSourceLookup.java b/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocityClassSourceLookup.java index bcb8176..9b697c3 100644 --- a/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocityClassSourceLookup.java +++ b/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocityClassSourceLookup.java @@ -23,7 +23,7 @@ package me.lucko.spark.velocity; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginManager; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocitySparkPlugin.java b/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocitySparkPlugin.java index 7d9ced8..4a89a4e 100644 --- a/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocitySparkPlugin.java +++ b/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocitySparkPlugin.java @@ -34,11 +34,13 @@ import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.platform.PlatformInfo; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import org.slf4j.Logger; import java.nio.file.Path; +import java.util.Collection; import java.util.List; import java.util.logging.Level; import java.util.stream.Stream; @@ -133,6 +135,16 @@ public class VelocitySparkPlugin implements SparkPlugin, SimpleCommand { return new VelocityClassSourceLookup(this.proxy.getPluginManager()); } + @Override + public Collection getKnownSources() { + return SourceMetadata.gather( + this.proxy.getPluginManager().getPlugins(), + plugin -> plugin.getDescription().getId(), + plugin -> plugin.getDescription().getVersion().orElse("unspecified"), + plugin -> String.join(", ", plugin.getDescription().getAuthors()) + ); + } + @Override public PlayerPingProvider createPlayerPingProvider() { return new VelocityPlayerPingProvider(this.proxy); diff --git a/spark-velocity4/src/main/java/me/lucko/spark/velocity/Velocity4ClassSourceLookup.java b/spark-velocity4/src/main/java/me/lucko/spark/velocity/Velocity4ClassSourceLookup.java index c5c22c3..84840d2 100644 --- a/spark-velocity4/src/main/java/me/lucko/spark/velocity/Velocity4ClassSourceLookup.java +++ b/spark-velocity4/src/main/java/me/lucko/spark/velocity/Velocity4ClassSourceLookup.java @@ -23,7 +23,7 @@ package me.lucko.spark.velocity; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginManager; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import org.checkerframework.checker.nullness.qual.Nullable; @@ -48,7 +48,7 @@ public class Velocity4ClassSourceLookup extends ClassSourceLookup.ByClassLoader for (PluginContainer plugin : pluginManager.plugins()) { Object instance = plugin.instance(); if (instance != null) { - this.classLoadersToPlugin.put(instance.getClass().getClassLoader(), plugin.description().name()); + this.classLoadersToPlugin.put(instance.getClass().getClassLoader(), plugin.description().id()); } } } diff --git a/spark-velocity4/src/main/java/me/lucko/spark/velocity/Velocity4SparkPlugin.java b/spark-velocity4/src/main/java/me/lucko/spark/velocity/Velocity4SparkPlugin.java index 0c57689..b638246 100644 --- a/spark-velocity4/src/main/java/me/lucko/spark/velocity/Velocity4SparkPlugin.java +++ b/spark-velocity4/src/main/java/me/lucko/spark/velocity/Velocity4SparkPlugin.java @@ -34,11 +34,13 @@ import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.platform.PlatformInfo; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import org.slf4j.Logger; import java.nio.file.Path; +import java.util.Collection; import java.util.List; import java.util.logging.Level; import java.util.stream.Stream; @@ -133,6 +135,16 @@ public class Velocity4SparkPlugin implements SparkPlugin, SimpleCommand { return new Velocity4ClassSourceLookup(this.proxy.pluginManager()); } + @Override + public Collection getKnownSources() { + return SourceMetadata.gather( + this.proxy.pluginManager().plugins(), + plugin -> plugin.description().id(), + plugin -> plugin.description().version(), + plugin -> String.join(", ", plugin.description().authors()) + ); + } + @Override public PlayerPingProvider createPlayerPingProvider() { return new Velocity4PlayerPingProvider(this.proxy); diff --git a/spark-waterdog/src/main/java/me/lucko/spark/waterdog/WaterdogClassSourceLookup.java b/spark-waterdog/src/main/java/me/lucko/spark/waterdog/WaterdogClassSourceLookup.java index 36e6a57..2207c9e 100644 --- a/spark-waterdog/src/main/java/me/lucko/spark/waterdog/WaterdogClassSourceLookup.java +++ b/spark-waterdog/src/main/java/me/lucko/spark/waterdog/WaterdogClassSourceLookup.java @@ -20,7 +20,7 @@ package me.lucko.spark.waterdog; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; import dev.waterdog.waterdogpe.ProxyServer; import dev.waterdog.waterdogpe.plugin.Plugin; diff --git a/spark-waterdog/src/main/java/me/lucko/spark/waterdog/WaterdogSparkPlugin.java b/spark-waterdog/src/main/java/me/lucko/spark/waterdog/WaterdogSparkPlugin.java index 07b153a..1a64a98 100644 --- a/spark-waterdog/src/main/java/me/lucko/spark/waterdog/WaterdogSparkPlugin.java +++ b/spark-waterdog/src/main/java/me/lucko/spark/waterdog/WaterdogSparkPlugin.java @@ -24,7 +24,8 @@ import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.platform.PlatformInfo; -import me.lucko.spark.common.util.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.ClassSourceLookup; +import me.lucko.spark.common.sampler.source.SourceMetadata; import dev.waterdog.waterdogpe.ProxyServer; import dev.waterdog.waterdogpe.command.Command; @@ -32,6 +33,7 @@ import dev.waterdog.waterdogpe.command.CommandSender; import dev.waterdog.waterdogpe.plugin.Plugin; import java.nio.file.Path; +import java.util.Collection; import java.util.logging.Level; import java.util.stream.Stream; @@ -100,6 +102,16 @@ public class WaterdogSparkPlugin extends Plugin implements SparkPlugin { return new WaterdogClassSourceLookup(getProxy()); } + @Override + public Collection getKnownSources() { + return SourceMetadata.gather( + getProxy().getPluginManager().getPlugins(), + Plugin::getName, + plugin -> plugin.getDescription().getVersion(), + plugin -> plugin.getDescription().getAuthor() + ); + } + @Override public PlayerPingProvider createPlayerPingProvider() { return new WaterdogPlayerPingProvider(getProxy()); -- cgit From a42dda9eebdc8db6c310978d138708c367f95096 Mon Sep 17 00:00:00 2001 From: Luck Date: Mon, 19 Sep 2022 20:08:10 +0100 Subject: Fix issues with temporary files going missing (#225) --- .../java/me/lucko/spark/common/SparkPlatform.java | 8 ++- .../lucko/spark/common/sampler/SamplerBuilder.java | 2 +- .../common/sampler/async/AsyncProfilerAccess.java | 30 +++++--- .../spark/common/sampler/async/AsyncSampler.java | 7 +- .../me/lucko/spark/common/util/TemporaryFiles.java | 81 +++++++++++++++++++--- 5 files changed, 103 insertions(+), 25 deletions(-) (limited to 'spark-common/src/main/java/me/lucko/spark/common/util') diff --git a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java index 1969206..2790a3c 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java +++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java @@ -89,6 +89,7 @@ public class SparkPlatform { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); private final SparkPlugin plugin; + private final TemporaryFiles temporaryFiles; private final Configuration configuration; private final String viewerUrl; private final BytebinClient bytebinClient; @@ -109,6 +110,7 @@ public class SparkPlatform { public SparkPlatform(SparkPlugin plugin) { this.plugin = plugin; + this.temporaryFiles = new TemporaryFiles(this.plugin.getPluginDirectory().resolve("tmp")); this.configuration = new Configuration(this.plugin.getPluginDirectory().resolve("config.json")); this.viewerUrl = this.configuration.getString("viewerUrl", "https://spark.lucko.me/"); @@ -192,13 +194,17 @@ public class SparkPlatform { SparkApi.unregister(); - TemporaryFiles.deleteTemporaryFiles(); + this.temporaryFiles.deleteTemporaryFiles(); } public SparkPlugin getPlugin() { return this.plugin; } + public TemporaryFiles getTemporaryFiles() { + return this.temporaryFiles; + } + public Configuration getConfiguration() { return this.configuration; } diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java index 88b9d91..52a7387 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java @@ -98,7 +98,7 @@ public class SamplerBuilder { Sampler sampler; if (this.ticksOver != -1 && this.tickHook != null) { sampler = new JavaSampler(platform, intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative, this.tickHook, this.ticksOver); - } else if (this.useAsyncProfiler && !(this.threadDumper instanceof ThreadDumper.Regex) && AsyncProfilerAccess.INSTANCE.checkSupported(platform)) { + } else if (this.useAsyncProfiler && !(this.threadDumper instanceof ThreadDumper.Regex) && AsyncProfilerAccess.getInstance(platform).checkSupported(platform)) { sampler = new AsyncSampler(platform, intervalMicros, this.threadDumper, this.threadGrouper, this.timeout); } else { sampler = new JavaSampler(platform, intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative); diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncProfilerAccess.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncProfilerAccess.java index ef2c035..abde21d 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncProfilerAccess.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncProfilerAccess.java @@ -22,9 +22,9 @@ package me.lucko.spark.common.sampler.async; import com.google.common.collect.ImmutableTable; import com.google.common.collect.Table; +import com.google.common.io.ByteStreams; import me.lucko.spark.common.SparkPlatform; -import me.lucko.spark.common.util.TemporaryFiles; import one.profiler.AsyncProfiler; import one.profiler.Events; @@ -32,19 +32,29 @@ import one.profiler.Events; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.Locale; +import java.util.Objects; import java.util.logging.Level; import java.util.stream.Collectors; /** * Provides a bridge between spark and async-profiler. */ -public enum AsyncProfilerAccess { - INSTANCE; +public class AsyncProfilerAccess { + private static AsyncProfilerAccess instance; + + // singleton, needs a SparkPlatform for first init + public static synchronized AsyncProfilerAccess getInstance(SparkPlatform platform) { + if (instance == null) { + Objects.requireNonNull(platform, "platform"); + instance = new AsyncProfilerAccess(platform); + } + return instance; + } /** An instance of the async-profiler Java API. */ private final AsyncProfiler profiler; @@ -55,13 +65,13 @@ public enum AsyncProfilerAccess { /** If profiler is null, contains the reason why setup failed */ private final Exception setupException; - AsyncProfilerAccess() { + AsyncProfilerAccess(SparkPlatform platform) { AsyncProfiler profiler; ProfilingEvent profilingEvent = null; Exception setupException = null; try { - profiler = load(); + profiler = load(platform); if (isEventSupported(profiler, ProfilingEvent.CPU, false)) { profilingEvent = ProfilingEvent.CPU; } else if (isEventSupported(profiler, ProfilingEvent.WALL, true)) { @@ -106,7 +116,7 @@ public enum AsyncProfilerAccess { return this.profiler != null; } - private static AsyncProfiler load() throws Exception { + private static AsyncProfiler load(SparkPlatform platform) throws Exception { // check compatibility String os = System.getProperty("os.name").toLowerCase(Locale.ROOT).replace(" ", ""); String arch = System.getProperty("os.arch").toLowerCase(Locale.ROOT); @@ -135,10 +145,10 @@ public enum AsyncProfilerAccess { throw new IllegalStateException("Could not find " + resource + " in spark jar file"); } - Path extractPath = TemporaryFiles.create("spark-", "-libasyncProfiler.so.tmp"); + Path extractPath = platform.getTemporaryFiles().create("spark-", "-libasyncProfiler.so.tmp"); - try (InputStream in = profilerResource.openStream()) { - Files.copy(in, extractPath, StandardCopyOption.REPLACE_EXISTING); + try (InputStream in = profilerResource.openStream(); OutputStream out = Files.newOutputStream(extractPath)) { + ByteStreams.copy(in, out); } // get an instance of async-profiler diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java index 37ccd96..7d9cb81 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java @@ -31,7 +31,6 @@ import me.lucko.spark.common.sampler.async.jfr.JfrReader; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; import me.lucko.spark.common.sampler.source.ClassSourceLookup; -import me.lucko.spark.common.util.TemporaryFiles; import me.lucko.spark.proto.SparkSamplerProtos.SamplerData; import one.profiler.AsyncProfiler; @@ -67,7 +66,7 @@ public class AsyncSampler extends AbstractSampler { public AsyncSampler(SparkPlatform platform, int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime) { super(platform, interval, threadDumper, endTime); - this.profiler = AsyncProfilerAccess.INSTANCE.getProfiler(); + this.profiler = AsyncProfilerAccess.getInstance(platform).getProfiler(); this.dataAggregator = new AsyncDataAggregator(threadGrouper); } @@ -93,12 +92,12 @@ public class AsyncSampler extends AbstractSampler { super.start(); try { - this.outputFile = TemporaryFiles.create("spark-profile-", ".jfr.tmp"); + this.outputFile = this.platform.getTemporaryFiles().create("spark-", "-profile-data.jfr.tmp"); } catch (IOException e) { throw new RuntimeException("Unable to create temporary output file", e); } - String command = "start,event=" + AsyncProfilerAccess.INSTANCE.getProfilingEvent() + ",interval=" + this.interval + "us,threads,jfr,file=" + this.outputFile.toString(); + String command = "start,event=" + AsyncProfilerAccess.getInstance(this.platform).getProfilingEvent() + ",interval=" + this.interval + "us,threads,jfr,file=" + this.outputFile.toString(); if (this.threadDumper instanceof ThreadDumper.Specific) { command += ",filter"; } diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/TemporaryFiles.java b/spark-common/src/main/java/me/lucko/spark/common/util/TemporaryFiles.java index 8a4a621..91a474c 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/TemporaryFiles.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/TemporaryFiles.java @@ -20,10 +20,18 @@ package me.lucko.spark.common.util; +import com.google.common.collect.ImmutableList; + import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.Collections; +import java.util.EnumSet; import java.util.HashSet; import java.util.Iterator; import java.util.Set; @@ -32,23 +40,47 @@ import java.util.Set; * Utility for handling temporary files. */ public final class TemporaryFiles { - private TemporaryFiles() {} - private static final Set DELETE_SET = Collections.synchronizedSet(new HashSet<>()); + public static final FileAttribute[] OWNER_ONLY_FILE_PERMISSIONS; + + static { + boolean isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + if (isPosix) { + OWNER_ONLY_FILE_PERMISSIONS = new FileAttribute[]{PosixFilePermissions.asFileAttribute(EnumSet.of( + PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE + ))}; + } else { + OWNER_ONLY_FILE_PERMISSIONS = new FileAttribute[0]; + } + } + + private final Path tmpDirectory; + private final Set files = Collections.synchronizedSet(new HashSet<>()); - public static Path create(String prefix, String suffix) throws IOException { - return register(Files.createTempFile(prefix, suffix)); + public TemporaryFiles(Path tmpDirectory) { + this.tmpDirectory = tmpDirectory; } - public static Path register(Path path) { + public Path create(String prefix, String suffix) throws IOException { + Path file; + if (ensureDirectoryIsReady()) { + String name = prefix + Long.toHexString(System.nanoTime()) + suffix; + file = Files.createFile(this.tmpDirectory.resolve(name), OWNER_ONLY_FILE_PERMISSIONS); + } else { + file = Files.createTempFile(prefix, suffix); + } + return register(file); + } + + public Path register(Path path) { path.toFile().deleteOnExit(); - DELETE_SET.add(path); + this.files.add(path); return path; } - public static void deleteTemporaryFiles() { - synchronized (DELETE_SET) { - for (Iterator iterator = DELETE_SET.iterator(); iterator.hasNext(); ) { + public void deleteTemporaryFiles() { + synchronized (this.files) { + for (Iterator iterator = this.files.iterator(); iterator.hasNext(); ) { Path path = iterator.next(); try { Files.deleteIfExists(path); @@ -60,4 +92,35 @@ public final class TemporaryFiles { } } + private boolean ensureDirectoryIsReady() { + if (Boolean.parseBoolean(System.getProperty("spark.useOsTmpDir", "false"))) { + return false; + } + + if (Files.isDirectory(this.tmpDirectory)) { + return true; + } + + try { + Files.createDirectories(this.tmpDirectory); + + Files.write(this.tmpDirectory.resolve("about.txt"), ImmutableList.of( + "# What is this directory?", + "", + "* In order to perform certain functions, spark sometimes needs to write temporary data to the disk. ", + "* Previously, a temporary directory provided by the operating system was used for this purpose. ", + "* However, this proved to be unreliable in some circumstances, so spark now stores temporary data here instead!", + "", + "spark will automatically cleanup the contents of this directory. " , + "(but if for some reason it doesn't, if the server is stopped, you can freely delete any files ending in .tmp)", + "", + "tl;dr: spark uses this folder to store some temporary data." + ), StandardCharsets.UTF_8); + + return true; + } catch (IOException e) { + return false; + } + } + } -- cgit From 5af2e6fb4cbd21f836c7ad56100b3c4535a831de Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 13 Nov 2022 12:30:59 +0000 Subject: Remove recursion in protobuf data --- .../spark/common/sampler/node/StackTraceNode.java | 6 +- .../spark/common/sampler/node/ThreadNode.java | 81 +++++++++++++++++++++- .../spark/common/util/IndexedListBuilder.java | 43 ++++++++++++ .../src/main/proto/spark/spark_sampler.proto | 7 +- 4 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 spark-common/src/main/java/me/lucko/spark/common/util/IndexedListBuilder.java (limited to 'spark-common/src/main/java/me/lucko/spark/common/util') diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java index ed938d5..c0dcc5b 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java @@ -65,7 +65,7 @@ public final class StackTraceNode extends AbstractNode { return this.description.parentLineNumber; } - public SparkSamplerProtos.StackTraceNode toProto(MergeMode mergeMode, ProtoTimeEncoder timeEncoder) { + public SparkSamplerProtos.StackTraceNode toProto(MergeMode mergeMode, ProtoTimeEncoder timeEncoder, Iterable childrenRefs) { SparkSamplerProtos.StackTraceNode.Builder proto = SparkSamplerProtos.StackTraceNode.newBuilder() .setClassName(this.description.className) .setMethodName(this.description.methodName); @@ -91,9 +91,7 @@ public final class StackTraceNode extends AbstractNode { .ifPresent(proto::setMethodDesc); } - for (StackTraceNode child : exportChildren(mergeMode)) { - proto.addChildren(child.toProto(mergeMode, timeEncoder)); - } + proto.addAllChildrenRefs(childrenRefs); return proto.build(); } diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java index 1dce523..9faece6 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java @@ -21,8 +21,14 @@ package me.lucko.spark.common.sampler.node; import me.lucko.spark.common.sampler.window.ProtoTimeEncoder; +import me.lucko.spark.common.util.IndexedListBuilder; import me.lucko.spark.proto.SparkSamplerProtos; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; + /** * The root of a sampling stack for a given thread / thread group. */ @@ -92,10 +98,83 @@ public final class ThreadNode extends AbstractNode { proto.addTimes(time); } + // When converting to a proto, we change the data structure from a recursive tree to an array. + // Effectively, instead of: + // + // { + // data: 'one', + // children: [ + // { + // data: 'two', + // children: [{ data: 'four' }] + // }, + // { data: 'three' } + // ] + // } + // + // we transmit: + // + // [ + // { data: 'one', children: [1, 2] }, + // { data: 'two', children: [3] } + // { data: 'three', children: [] } + // { data: 'four', children: [] } + // ] + // + + // the flattened array of nodes + IndexedListBuilder nodesArray = new IndexedListBuilder<>(); + + // Perform a depth-first post order traversal of the tree + Deque stack = new ArrayDeque<>(); + + // push the thread node's children to the stack + List childrenRefs = new LinkedList<>(); for (StackTraceNode child : exportChildren(mergeMode)) { - proto.addChildren(child.toProto(mergeMode, timeEncoder)); + stack.push(new Node(child, childrenRefs)); + } + + Node node; + while (!stack.isEmpty()) { + node = stack.peek(); + + // on the first visit, just push this node's children and leave it on the stack + if (node.firstVisit) { + for (StackTraceNode child : node.stackTraceNode.exportChildren(mergeMode)) { + stack.push(new Node(child, node.childrenRefs)); + } + node.firstVisit = false; + continue; + } + + // convert StackTraceNode to a proto + // - at this stage, we have already visited this node's children + // - the refs for each child are stored in node.childrenRefs + SparkSamplerProtos.StackTraceNode childProto = node.stackTraceNode.toProto(mergeMode, timeEncoder, node.childrenRefs); + + // add the child proto to the nodes array, and record the ref in the parent + int childIndex = nodesArray.add(childProto); + node.parentChildrenRefs.add(childIndex); + + // pop from the stack + stack.pop(); } + proto.addAllChildrenRefs(childrenRefs); + proto.addAllChildren(nodesArray.build()); + return proto.build(); } + + private static final class Node { + private final StackTraceNode stackTraceNode; + private boolean firstVisit = true; + private final List childrenRefs = new LinkedList<>(); + private final List parentChildrenRefs; + + private Node(StackTraceNode node, List parentChildrenRefs) { + this.stackTraceNode = node; + this.parentChildrenRefs = parentChildrenRefs; + } + } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/IndexedListBuilder.java b/spark-common/src/main/java/me/lucko/spark/common/util/IndexedListBuilder.java new file mode 100644 index 0000000..b2315f9 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/util/IndexedListBuilder.java @@ -0,0 +1,43 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.common.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * List builder that returns the index of the inserted element. + * + * @param generic type + */ +public class IndexedListBuilder { + private int i = 0; + private final List nodes = new ArrayList<>(); + + public int add(T node) { + this.nodes.add(node); + return this.i++; + } + + public List build() { + return this.nodes; + } +} diff --git a/spark-common/src/main/proto/spark/spark_sampler.proto b/spark-common/src/main/proto/spark/spark_sampler.proto index 2cb08f1..245da37 100644 --- a/spark-common/src/main/proto/spark/spark_sampler.proto +++ b/spark-common/src/main/proto/spark/spark_sampler.proto @@ -78,18 +78,19 @@ message ThreadNode { repeated StackTraceNode children = 3; repeated double times = 4; + repeated int32 children_refs = 5; } message StackTraceNode { // replaced - reserved 1; - reserved "time"; + reserved 1, 2; + reserved "time", "children"; - repeated StackTraceNode children = 2; string class_name = 3; string method_name = 4; int32 parent_line_number = 5; // optional int32 line_number = 6; // optional string method_desc = 7; // optional repeated double times = 8; + repeated int32 children_refs = 9; } -- cgit From f2d77d875f32f107987c93da1f90529fc6812444 Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 13 Nov 2022 21:24:57 +0000 Subject: Background profiler --- .../java/me/lucko/spark/common/SparkPlatform.java | 82 ++++++++++----- .../me/lucko/spark/common/command/Arguments.java | 11 +- .../me/lucko/spark/common/command/Command.java | 58 ++++++++++- .../common/command/modules/GcMonitoringModule.java | 22 +--- .../common/command/modules/SamplerModule.java | 115 +++++++++++++++------ .../spark/common/sampler/AbstractSampler.java | 19 ++-- .../me/lucko/spark/common/sampler/Sampler.java | 7 ++ .../lucko/spark/common/sampler/SamplerBuilder.java | 28 ++--- .../spark/common/sampler/SamplerContainer.java | 9 ++ .../spark/common/sampler/SamplerSettings.java | 61 +++++++++++ .../spark/common/sampler/async/AsyncSampler.java | 14 +-- .../spark/common/sampler/java/JavaSampler.java | 20 ++-- .../spark/common/sampler/node/ThreadNode.java | 1 + .../sampler/window/WindowStatisticsCollector.java | 5 + .../me/lucko/spark/common/util/Configuration.java | 10 ++ .../me/lucko/spark/common/util/FormatUtil.java | 20 ++++ 16 files changed, 362 insertions(+), 120 deletions(-) create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerSettings.java (limited to 'spark-common/src/main/java/me/lucko/spark/common/util') diff --git a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java index a015e42..5461ed4 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java +++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java @@ -45,7 +45,10 @@ import me.lucko.spark.common.monitor.ping.PingStatistics; import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.monitor.tick.TickStatistics; import me.lucko.spark.common.platform.PlatformStatisticsProvider; +import me.lucko.spark.common.sampler.Sampler; +import me.lucko.spark.common.sampler.SamplerBuilder; import me.lucko.spark.common.sampler.SamplerContainer; +import me.lucko.spark.common.sampler.ThreadGrouper; import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.tick.TickReporter; @@ -64,6 +67,7 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -74,7 +78,6 @@ import java.util.stream.Collectors; import static net.kyori.adventure.text.Component.space; import static net.kyori.adventure.text.Component.text; -import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; import static net.kyori.adventure.text.format.NamedTextColor.GOLD; import static net.kyori.adventure.text.format.NamedTextColor.GRAY; import static net.kyori.adventure.text.format.NamedTextColor.RED; @@ -139,7 +142,7 @@ public class SparkPlatform { this.activityLog = new ActivityLog(plugin.getPluginDirectory().resolve("activity.json")); this.activityLog.load(); - this.samplerContainer = new SamplerContainer(); + this.samplerContainer = new SamplerContainer(this.configuration.getBoolean("backgroundProfiler", true)); this.tickHook = plugin.createTickHook(); this.tickReporter = plugin.createTickReporter(); @@ -179,6 +182,16 @@ public class SparkPlatform { SparkApi api = new SparkApi(this); this.plugin.registerApi(api); SparkApi.register(api); + + if (this.samplerContainer.isBackgroundProfilerEnabled()) { + this.plugin.log(Level.INFO, "Starting background profiler..."); + try { + startBackgroundProfiler(); + this.plugin.log(Level.INFO, "... done!"); + } catch (Exception e) { + e.printStackTrace(); + } + } } public void disable() { @@ -196,6 +209,8 @@ public class SparkPlatform { module.close(); } + this.samplerContainer.close(); + SparkApi.unregister(); this.temporaryFiles.deleteTemporaryFiles(); @@ -269,6 +284,17 @@ public class SparkPlatform { return this.serverNormalOperationStartTime; } + public void startBackgroundProfiler() { + Sampler sampler = new SamplerBuilder() + .background(true) + .threadDumper(this.plugin.getDefaultThreadDumper()) + .threadGrouper(ThreadGrouper.BY_POOL) + .samplingInterval(this.configuration.getInteger("backgroundProfilerInterval", 10)) + .start(this); + + this.samplerContainer.setActiveSampler(sampler); + } + public Path resolveSaveFile(String prefix, String extension) { Path pluginFolder = this.plugin.getPluginDirectory(); try { @@ -394,7 +420,7 @@ public class SparkPlatform { if (command.aliases().contains(alias)) { resp.setCommandPrimaryAlias(command.primaryAlias()); try { - command.executor().execute(this, sender, resp, new Arguments(rawArgs)); + command.executor().execute(this, sender, resp, new Arguments(rawArgs, command.allowSubCommand())); } catch (Arguments.ParseException e) { resp.replyPrefixed(text(e.getMessage(), RED)); } @@ -442,32 +468,38 @@ public class SparkPlatform { ); for (Command command : commands) { String usage = "/" + getPlugin().getCommandName() + " " + command.primaryAlias(); - ClickEvent clickEvent = ClickEvent.suggestCommand(usage); - sender.reply(text() - .append(text(">", GOLD, BOLD)) - .append(space()) - .append(text().content(usage).color(GRAY).clickEvent(clickEvent).build()) - .build() - ); - for (Command.ArgumentInfo arg : command.arguments()) { - if (arg.requiresParameter()) { + + if (command.allowSubCommand()) { + Map> argumentsBySubCommand = command.arguments().stream() + .collect(Collectors.groupingBy(Command.ArgumentInfo::subCommandName, LinkedHashMap::new, Collectors.toList())); + + argumentsBySubCommand.forEach((subCommand, arguments) -> { + String subCommandUsage = usage + " " + subCommand; + sender.reply(text() - .content(" ") - .append(text("[", DARK_GRAY)) - .append(text("--" + arg.argumentName(), GRAY)) + .append(text(">", GOLD, BOLD)) .append(space()) - .append(text("<" + arg.parameterDescription() + ">", DARK_GRAY)) - .append(text("]", DARK_GRAY)) - .build() - ); - } else { - sender.reply(text() - .content(" ") - .append(text("[", DARK_GRAY)) - .append(text("--" + arg.argumentName(), GRAY)) - .append(text("]", DARK_GRAY)) + .append(text().content(subCommandUsage).color(GRAY).clickEvent(ClickEvent.suggestCommand(subCommandUsage)).build()) .build() ); + + for (Command.ArgumentInfo arg : arguments) { + if (arg.argumentName().isEmpty()) { + continue; + } + sender.reply(arg.toComponent(" ")); + } + }); + } else { + sender.reply(text() + .append(text(">", GOLD, BOLD)) + .append(space()) + .append(text().content(usage).color(GRAY).clickEvent(ClickEvent.suggestCommand(usage)).build()) + .build() + ); + + for (Command.ArgumentInfo arg : command.arguments()) { + sender.reply(arg.toComponent(" ")); } } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/Arguments.java b/spark-common/src/main/java/me/lucko/spark/common/command/Arguments.java index 17c49e2..ad8c777 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/Arguments.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/Arguments.java @@ -38,8 +38,9 @@ public class Arguments { private final List rawArgs; private final SetMultimap parsedArgs; + private String parsedSubCommand = null; - public Arguments(List rawArgs) { + public Arguments(List rawArgs, boolean allowSubCommand) { this.rawArgs = rawArgs; this.parsedArgs = HashMultimap.create(); @@ -52,7 +53,9 @@ public class Arguments { Matcher matcher = FLAG_REGEX.matcher(arg); boolean matches = matcher.matches(); - if (flag == null || matches) { + if (i == 0 && allowSubCommand && !matches) { + this.parsedSubCommand = arg; + } else if (flag == null || matches) { if (!matches) { throw new ParseException("Expected flag at position " + i + " but got '" + arg + "' instead!"); } @@ -80,6 +83,10 @@ public class Arguments { return this.rawArgs; } + public String subCommand() { + return this.parsedSubCommand; + } + public int intFlag(String key) { Iterator it = this.parsedArgs.get(key).iterator(); if (it.hasNext()) { diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/Command.java b/spark-common/src/main/java/me/lucko/spark/common/command/Command.java index dad15e6..c6871a9 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/Command.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/Command.java @@ -25,10 +25,17 @@ import com.google.common.collect.ImmutableList; import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.command.sender.CommandSender; +import net.kyori.adventure.text.Component; + import java.util.Collections; import java.util.List; import java.util.Objects; +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; + public class Command { public static Builder builder() { @@ -39,12 +46,14 @@ public class Command { private final List arguments; private final Executor executor; private final TabCompleter tabCompleter; + private final boolean allowSubCommand; - private Command(List aliases, List arguments, Executor executor, TabCompleter tabCompleter) { + private Command(List aliases, List arguments, Executor executor, TabCompleter tabCompleter, boolean allowSubCommand) { this.aliases = aliases; this.arguments = arguments; this.executor = executor; this.tabCompleter = tabCompleter; + this.allowSubCommand = allowSubCommand; } public List aliases() { @@ -67,11 +76,16 @@ public class Command { return this.aliases.get(0); } + public boolean allowSubCommand() { + return this.allowSubCommand; + } + public static final class Builder { private final ImmutableList.Builder aliases = ImmutableList.builder(); private final ImmutableList.Builder arguments = ImmutableList.builder(); private Executor executor = null; private TabCompleter tabCompleter = null; + private boolean allowSubCommand = false; Builder() { @@ -82,8 +96,13 @@ public class Command { return this; } + public Builder argumentUsage(String subCommandName, String argumentName, String parameterDescription) { + this.arguments.add(new ArgumentInfo(subCommandName, argumentName, parameterDescription)); + return this; + } + public Builder argumentUsage(String argumentName, String parameterDescription) { - this.arguments.add(new ArgumentInfo(argumentName, parameterDescription)); + this.arguments.add(new ArgumentInfo("", argumentName, parameterDescription)); return this; } @@ -97,6 +116,11 @@ public class Command { return this; } + public Builder allowSubCommand(boolean allowSubCommand) { + this.allowSubCommand = allowSubCommand; + return this; + } + public Command build() { List aliases = this.aliases.build(); if (aliases.isEmpty()) { @@ -108,7 +132,7 @@ public class Command { if (this.tabCompleter == null) { this.tabCompleter = TabCompleter.empty(); } - return new Command(aliases, this.arguments.build(), this.executor, this.tabCompleter); + return new Command(aliases, this.arguments.build(), this.executor, this.tabCompleter, this.allowSubCommand); } } @@ -127,14 +151,20 @@ public class Command { } public static final class ArgumentInfo { + private final String subCommandName; private final String argumentName; private final String parameterDescription; - public ArgumentInfo(String argumentName, String parameterDescription) { + public ArgumentInfo(String subCommandName, String argumentName, String parameterDescription) { + this.subCommandName = subCommandName; this.argumentName = argumentName; this.parameterDescription = parameterDescription; } + public String subCommandName() { + return this.subCommandName; + } + public String argumentName() { return this.argumentName; } @@ -146,6 +176,26 @@ public class Command { public boolean requiresParameter() { return this.parameterDescription != null; } + + public Component toComponent(String padding) { + if (requiresParameter()) { + return text() + .content(padding) + .append(text("[", DARK_GRAY)) + .append(text("--" + argumentName(), GRAY)) + .append(space()) + .append(text("<" + parameterDescription() + ">", DARK_GRAY)) + .append(text("]", DARK_GRAY)) + .build(); + } else { + return text() + .content(padding) + .append(text("[", DARK_GRAY)) + .append(text("--" + argumentName(), GRAY)) + .append(text("]", DARK_GRAY)) + .build(); + } + } } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/GcMonitoringModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/GcMonitoringModule.java index 2ce83fd..a2da0a0 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/GcMonitoringModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/GcMonitoringModule.java @@ -123,7 +123,7 @@ public class GcMonitoringModule implements CommandModule { ); report.add(text() .content(" ") - .append(text(formatTime((long) averageFrequency), WHITE)) + .append(text(FormatUtil.formatSeconds((long) averageFrequency / 1000), WHITE)) .append(text(" avg frequency", GRAY)) .build() ); @@ -153,26 +153,6 @@ public class GcMonitoringModule implements CommandModule { ); } - private static String formatTime(long millis) { - if (millis <= 0) { - return "0s"; - } - - long second = millis / 1000; - long minute = second / 60; - second = second % 60; - - StringBuilder sb = new StringBuilder(); - if (minute != 0) { - sb.append(minute).append("m "); - } - if (second != 0) { - sb.append(second).append("s "); - } - - return sb.toString().trim(); - } - private static class ReportingGcMonitor extends GarbageCollectionMonitor implements GarbageCollectionMonitor.Listener { private final SparkPlatform platform; private final CommandResponseHandler resp; diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java index 00bf1a9..6a76748 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java @@ -39,6 +39,7 @@ import me.lucko.spark.common.sampler.async.AsyncSampler; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.tick.TickHook; +import me.lucko.spark.common.util.FormatUtil; import me.lucko.spark.common.util.MethodDisambiguator; import me.lucko.spark.proto.SparkSamplerProtos; @@ -72,31 +73,36 @@ public class SamplerModule implements CommandModule { public void registerCommands(Consumer consumer) { consumer.accept(Command.builder() .aliases("profiler", "sampler") - .argumentUsage("info", null) - .argumentUsage("stop", null) - .argumentUsage("timeout", "timeout seconds") - .argumentUsage("thread *", null) - .argumentUsage("thread", "thread name") - .argumentUsage("only-ticks-over", "tick length millis") - .argumentUsage("interval", "interval millis") + .allowSubCommand(true) + .argumentUsage("info", "", null) + .argumentUsage("start", "timeout", "timeout seconds") + .argumentUsage("start", "thread *", null) + .argumentUsage("start", "thread", "thread name") + .argumentUsage("start", "only-ticks-over", "tick length millis") + .argumentUsage("start", "interval", "interval millis") + .argumentUsage("stop", "", null) + .argumentUsage("cancel", "", null) .executor(this::profiler) .tabCompleter((platform, sender, arguments) -> { - if (arguments.contains("--info") || arguments.contains("--cancel")) { - return Collections.emptyList(); + List opts = Collections.emptyList(); + + if (arguments.size() > 0) { + String subCommand = arguments.get(0); + if (subCommand.equals("stop") || subCommand.equals("upload")) { + opts = new ArrayList<>(Arrays.asList("--comment", "--save-to-file")); + opts.removeAll(arguments); + } + if (subCommand.equals("start")) { + opts = new ArrayList<>(Arrays.asList("--timeout", "--regex", "--combine-all", + "--not-combined", "--interval", "--only-ticks-over", "--force-java-sampler")); + opts.removeAll(arguments); + opts.add("--thread"); // allowed multiple times + } } - if (arguments.contains("--stop") || arguments.contains("--upload")) { - return TabCompleter.completeForOpts(arguments, "--comment", "--save-to-file"); - } - - List opts = new ArrayList<>(Arrays.asList("--info", "--stop", "--cancel", - "--timeout", "--regex", "--combine-all", "--not-combined", "--interval", - "--only-ticks-over", "--force-java-sampler")); - opts.removeAll(arguments); - opts.add("--thread"); // allowed multiple times - return TabCompleter.create() - .from(0, CompletionSupplier.startsWith(opts)) + .at(0, CompletionSupplier.startsWith(Arrays.asList("info", "start", "stop", "cancel"))) + .from(1, CompletionSupplier.startsWith(opts)) .complete(arguments); }) .build() @@ -104,28 +110,48 @@ public class SamplerModule implements CommandModule { } private void profiler(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) { - if (arguments.boolFlag("info")) { + String subCommand = arguments.subCommand() == null ? "" : arguments.subCommand(); + + if (subCommand.equals("info") || arguments.boolFlag("info")) { profilerInfo(platform, resp); return; } - if (arguments.boolFlag("cancel")) { + if (subCommand.equals("cancel") || arguments.boolFlag("cancel")) { profilerCancel(platform, resp); return; } - if (arguments.boolFlag("stop") || arguments.boolFlag("upload")) { + if (subCommand.equals("stop") || arguments.boolFlag("stop") || arguments.boolFlag("upload")) { profilerStop(platform, sender, resp, arguments); return; } - profilerStart(platform, sender, resp, arguments); + if (subCommand.equals("start") || arguments.boolFlag("start")) { + profilerStart(platform, sender, resp, arguments); + return; + } + + if (arguments.raw().isEmpty()) { + profilerInfo(platform, resp); + } else { + profilerStart(platform, sender, resp, arguments); + } } private void profilerStart(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) { - if (platform.getSamplerContainer().getActiveSampler() != null) { - profilerInfo(platform, resp); - return; + Sampler previousSampler = platform.getSamplerContainer().getActiveSampler(); + if (previousSampler != null) { + if (previousSampler.isRunningInBackground()) { + // there is a background profiler running - stop that first + resp.replyPrefixed(text("Stopping the background profiler before starting... please wait")); + previousSampler.stop(); + platform.getSamplerContainer().unsetActiveSampler(previousSampler); + } else { + // there is a non-background profiler running - tell the user + profilerInfo(platform, resp); + return; + } } int timeoutSeconds = arguments.intFlag("timeout"); @@ -212,9 +238,9 @@ public class SamplerModule implements CommandModule { if (timeoutSeconds == -1) { resp.broadcastPrefixed(text("It will run in the background until it is stopped by an admin.")); resp.broadcastPrefixed(text("To stop the profiler and upload the results, run:")); - resp.broadcastPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler --stop")); + resp.broadcastPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler stop")); } else { - resp.broadcastPrefixed(text("The results will be automatically returned after the profiler has been running for " + timeoutSeconds + " seconds.")); + resp.broadcastPrefixed(text("The results will be automatically returned after the profiler has been running for " + FormatUtil.formatSeconds(timeoutSeconds) + ".")); } CompletableFuture future = sampler.getFuture(); @@ -248,24 +274,34 @@ public class SamplerModule implements CommandModule { if (sampler == null) { resp.replyPrefixed(text("The profiler isn't running!")); resp.replyPrefixed(text("To start a new one, run:")); - resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler")); + resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler start")); } else { resp.replyPrefixed(text("Profiler is already running!", GOLD)); long runningTime = (System.currentTimeMillis() - sampler.getStartTime()) / 1000L; - resp.replyPrefixed(text("So far, it has profiled for " + runningTime + " seconds.")); + + if (sampler.isRunningInBackground()) { + resp.replyPrefixed(text() + .append(text("It was started ")) + .append(text("automatically", WHITE)) + .append(text(" when spark enabled and has been running in the background for " + FormatUtil.formatSeconds(runningTime) + ".")) + .build() + ); + } else { + resp.replyPrefixed(text("So far, it has profiled for " + FormatUtil.formatSeconds(runningTime) + ".")); + } long timeout = sampler.getAutoEndTime(); if (timeout == -1) { resp.replyPrefixed(text("To stop the profiler and upload the results, run:")); - resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler --stop")); + resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler stop")); } else { long timeoutDiff = (timeout - System.currentTimeMillis()) / 1000L; - resp.replyPrefixed(text("It is due to complete automatically and upload results in " + timeoutDiff + " seconds.")); + resp.replyPrefixed(text("It is due to complete automatically and upload results in " + FormatUtil.formatSeconds(timeoutDiff) + ".")); } resp.replyPrefixed(text("To cancel the profiler without uploading the results, run:")); - resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler --cancel")); + resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler cancel")); } } @@ -299,6 +335,17 @@ public class SamplerModule implements CommandModule { MethodDisambiguator methodDisambiguator = new MethodDisambiguator(); MergeMode mergeMode = arguments.boolFlag("separate-parent-calls") ? MergeMode.separateParentCalls(methodDisambiguator) : MergeMode.sameMethod(methodDisambiguator); handleUpload(platform, resp, sampler, comment, mergeMode, saveToFile); + + // if the previous sampler was running in the background, create a new one + if (platform.getSamplerContainer().isBackgroundProfilerEnabled()) { + platform.startBackgroundProfiler(); + + resp.broadcastPrefixed(text() + .append(text("Restarted the background profiler. ")) + .append(text("(If you don't want this to happen, run: /" + platform.getPlugin().getCommandName() + " profiler cancel)", DARK_GRAY)) + .build() + ); + } } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java index c650738..feefd66 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java @@ -32,8 +32,6 @@ import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.sampler.source.SourceMetadata; import me.lucko.spark.common.sampler.window.ProtoTimeEncoder; import me.lucko.spark.common.sampler.window.WindowStatisticsCollector; -import me.lucko.spark.common.tick.TickHook; -import me.lucko.spark.proto.SparkProtos; import me.lucko.spark.proto.SparkSamplerProtos.SamplerData; import me.lucko.spark.proto.SparkSamplerProtos.SamplerMetadata; @@ -64,6 +62,9 @@ public abstract class AbstractSampler implements Sampler { /** The unix timestamp (in millis) when this sampler should automatically complete. */ protected final long autoEndTime; // -1 for nothing + /** If the sampler is running in the background */ + protected boolean background; + /** Collects statistics for each window in the sample */ protected final WindowStatisticsCollector windowStatisticsCollector; @@ -73,11 +74,12 @@ public abstract class AbstractSampler implements Sampler { /** The garbage collector statistics when profiling started */ protected Map initialGcStats; - protected AbstractSampler(SparkPlatform platform, int interval, ThreadDumper threadDumper, long autoEndTime) { + protected AbstractSampler(SparkPlatform platform, SamplerSettings settings) { this.platform = platform; - this.interval = interval; - this.threadDumper = threadDumper; - this.autoEndTime = autoEndTime; + this.interval = settings.interval(); + this.threadDumper = settings.threadDumper(); + this.autoEndTime = settings.autoEndTime(); + this.background = settings.runningInBackground(); this.windowStatisticsCollector = new WindowStatisticsCollector(platform); } @@ -94,6 +96,11 @@ public abstract class AbstractSampler implements Sampler { return this.autoEndTime; } + @Override + public boolean isRunningInBackground() { + return this.background; + } + @Override public CompletableFuture getFuture() { return this.future; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java index e06cba6..5d2026d 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java @@ -57,6 +57,13 @@ public interface Sampler { */ long getAutoEndTime(); + /** + * If this sampler is running in the background. (wasn't started by a specific user) + * + * @return true if the sampler is running in the background + */ + boolean isRunningInBackground(); + /** * Gets a future to encapsulate the completion of the sampler * diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java index 382950a..ec635ef 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java @@ -38,7 +38,8 @@ public class SamplerBuilder { private boolean ignoreSleeping = false; private boolean ignoreNative = false; private boolean useAsyncProfiler = true; - private long timeout = -1; + private long autoEndTime = -1; + private boolean background = false; private ThreadDumper threadDumper = ThreadDumper.ALL; private ThreadGrouper threadGrouper = ThreadGrouper.BY_NAME; @@ -57,7 +58,12 @@ public class SamplerBuilder { if (timeout <= 0) { throw new IllegalArgumentException("timeout > 0"); } - this.timeout = System.currentTimeMillis() + unit.toMillis(timeout); + this.autoEndTime = System.currentTimeMillis() + unit.toMillis(timeout); + return this; + } + + public SamplerBuilder background(boolean background) { + this.background = background; return this; } @@ -95,26 +101,22 @@ public class SamplerBuilder { public Sampler start(SparkPlatform platform) { boolean onlyTicksOverMode = this.ticksOver != -1 && this.tickHook != null; boolean canUseAsyncProfiler = this.useAsyncProfiler && + !onlyTicksOverMode && !(this.ignoreSleeping || this.ignoreNative) && !(this.threadDumper instanceof ThreadDumper.Regex) && AsyncProfilerAccess.getInstance(platform).checkSupported(platform); int intervalMicros = (int) (this.samplingInterval * 1000d); + SamplerSettings settings = new SamplerSettings(intervalMicros, this.threadDumper, this.threadGrouper, this.autoEndTime, this.background); Sampler sampler; - if (onlyTicksOverMode) { - sampler = new JavaSampler(platform, intervalMicros, this.threadDumper, - this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative, - this.tickHook, this.ticksOver); - - } else if (canUseAsyncProfiler) { - sampler = new AsyncSampler(platform, intervalMicros, this.threadDumper, - this.threadGrouper, this.timeout); - + if (canUseAsyncProfiler) { + sampler = new AsyncSampler(platform, settings); + } else if (onlyTicksOverMode) { + sampler = new JavaSampler(platform, settings, this.ignoreSleeping, this.ignoreNative, this.tickHook, this.ticksOver); } else { - sampler = new JavaSampler(platform, intervalMicros, this.threadDumper, - this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative); + sampler = new JavaSampler(platform, settings, this.ignoreSleeping, this.ignoreNative); } sampler.start(); diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java index 55913d8..f56dee5 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java @@ -28,6 +28,11 @@ import java.util.concurrent.atomic.AtomicReference; public class SamplerContainer implements AutoCloseable { private final AtomicReference activeSampler = new AtomicReference<>(); + private final boolean backgroundProfilerEnabled; + + public SamplerContainer(boolean backgroundProfilerEnabled) { + this.backgroundProfilerEnabled = backgroundProfilerEnabled; + } /** * Gets the active sampler, or null if a sampler is not active. @@ -68,6 +73,10 @@ public class SamplerContainer implements AutoCloseable { } } + public boolean isBackgroundProfilerEnabled() { + return this.backgroundProfilerEnabled; + } + @Override public void close() { stopActiveSampler(); diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerSettings.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerSettings.java new file mode 100644 index 0000000..6e55a43 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerSettings.java @@ -0,0 +1,61 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.common.sampler; + +/** + * Base settings for all samplers + */ +public class SamplerSettings { + + private final int interval; + private final ThreadDumper threadDumper; + private final ThreadGrouper threadGrouper; + private final long autoEndTime; + private final boolean runningInBackground; + + public SamplerSettings(int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long autoEndTime, boolean runningInBackground) { + this.interval = interval; + this.threadDumper = threadDumper; + this.threadGrouper = threadGrouper; + this.autoEndTime = autoEndTime; + this.runningInBackground = runningInBackground; + } + + public int interval() { + return this.interval; + } + + public ThreadDumper threadDumper() { + return this.threadDumper; + } + + public ThreadGrouper threadGrouper() { + return this.threadGrouper; + } + + public long autoEndTime() { + return this.autoEndTime; + } + + public boolean runningInBackground() { + return this.runningInBackground; + } +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java index cbc81c7..d6cfd4f 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java @@ -25,8 +25,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.command.sender.CommandSender; import me.lucko.spark.common.sampler.AbstractSampler; -import me.lucko.spark.common.sampler.ThreadDumper; -import me.lucko.spark.common.sampler.ThreadGrouper; +import me.lucko.spark.common.sampler.SamplerSettings; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.sampler.window.ProfilingWindowUtils; @@ -36,6 +35,7 @@ import me.lucko.spark.proto.SparkSamplerProtos.SamplerData; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.function.IntPredicate; /** * A sampler implementation using async-profiler. @@ -55,10 +55,10 @@ public class AsyncSampler extends AbstractSampler { /** The executor used for scheduling and management */ private ScheduledExecutorService scheduler; - public AsyncSampler(SparkPlatform platform, int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime) { - super(platform, interval, threadDumper, endTime); + public AsyncSampler(SparkPlatform platform, SamplerSettings settings) { + super(platform, settings); this.profilerAccess = AsyncProfilerAccess.getInstance(platform); - this.dataAggregator = new AsyncDataAggregator(threadGrouper); + this.dataAggregator = new AsyncDataAggregator(settings.threadGrouper()); this.scheduler = Executors.newSingleThreadScheduledExecutor( new ThreadFactoryBuilder().setNameFormat("spark-asyncsampler-worker-thread").build() ); @@ -124,7 +124,9 @@ public class AsyncSampler extends AbstractSampler { previousJob.aggregate(this.dataAggregator); // prune data older than the history size - this.dataAggregator.pruneData(ProfilingWindowUtils.keepHistoryBefore(window)); + IntPredicate predicate = ProfilingWindowUtils.keepHistoryBefore(window); + this.dataAggregator.pruneData(predicate); + this.windowStatisticsCollector.pruneStatistics(predicate); } } catch (Throwable e) { e.printStackTrace(); diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java index 6aad5e3..95c3508 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java @@ -25,8 +25,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.command.sender.CommandSender; import me.lucko.spark.common.sampler.AbstractSampler; -import me.lucko.spark.common.sampler.ThreadDumper; -import me.lucko.spark.common.sampler.ThreadGrouper; +import me.lucko.spark.common.sampler.SamplerSettings; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.sampler.window.ProfilingWindowUtils; @@ -42,6 +41,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntPredicate; /** * A sampler implementation using Java (WarmRoast). @@ -66,14 +66,14 @@ public class JavaSampler extends AbstractSampler implements Runnable { /** The last window that was profiled */ private final AtomicInteger lastWindow = new AtomicInteger(); - public JavaSampler(SparkPlatform platform, int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime, boolean ignoreSleeping, boolean ignoreNative) { - super(platform, interval, threadDumper, endTime); - this.dataAggregator = new SimpleDataAggregator(this.workerPool, threadGrouper, interval, ignoreSleeping, ignoreNative); + public JavaSampler(SparkPlatform platform, SamplerSettings settings, boolean ignoreSleeping, boolean ignoreNative) { + super(platform, settings); + this.dataAggregator = new SimpleDataAggregator(this.workerPool, settings.threadGrouper(), settings.interval(), ignoreSleeping, ignoreNative); } - public JavaSampler(SparkPlatform platform, int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime, boolean ignoreSleeping, boolean ignoreNative, TickHook tickHook, int tickLengthThreshold) { - super(platform, interval, threadDumper, endTime); - this.dataAggregator = new TickedDataAggregator(this.workerPool, threadGrouper, interval, ignoreSleeping, ignoreNative, tickHook, tickLengthThreshold); + public JavaSampler(SparkPlatform platform, SamplerSettings settings, boolean ignoreSleeping, boolean ignoreNative, TickHook tickHook, int tickLengthThreshold) { + super(platform, settings); + this.dataAggregator = new TickedDataAggregator(this.workerPool, settings.threadGrouper(), settings.interval(), ignoreSleeping, ignoreNative, tickHook, tickLengthThreshold); } @Override @@ -151,7 +151,9 @@ public class JavaSampler extends AbstractSampler implements Runnable { JavaSampler.this.windowStatisticsCollector.measureNow(previousWindow); // prune data older than the history size - JavaSampler.this.dataAggregator.pruneData(ProfilingWindowUtils.keepHistoryBefore(this.window)); + IntPredicate predicate = ProfilingWindowUtils.keepHistoryBefore(this.window); + JavaSampler.this.dataAggregator.pruneData(predicate); + JavaSampler.this.windowStatisticsCollector.pruneStatistics(predicate); } } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java index 5035046..37ff359 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java @@ -130,6 +130,7 @@ public final class ThreadNode extends AbstractNode { } } + removeTimeWindows(predicate); return getTimeWindows().isEmpty(); } diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/window/WindowStatisticsCollector.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/window/WindowStatisticsCollector.java index 47f739d..7da62fa 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/window/WindowStatisticsCollector.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/window/WindowStatisticsCollector.java @@ -30,6 +30,7 @@ import me.lucko.spark.proto.SparkProtos; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntPredicate; /** * Collects statistics for each profiling window. @@ -116,6 +117,10 @@ public class WindowStatisticsCollector { } } + public void pruneStatistics(IntPredicate predicate) { + this.stats.keySet().removeIf(predicate::test); + } + public Map export() { return this.stats; } diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java b/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java index 7588645..ce63878 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java @@ -67,4 +67,14 @@ public final class Configuration { return val.isBoolean() ? val.getAsBoolean() : def; } + public int getInteger(String path, int def) { + JsonElement el = this.root.get(path); + if (el == null || !el.isJsonPrimitive()) { + return def; + } + + JsonPrimitive val = el.getAsJsonPrimitive(); + return val.isBoolean() ? val.getAsInt() : def; + } + } diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java b/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java index c4a3d66..1ee3b0f 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java @@ -62,4 +62,24 @@ public enum FormatUtil { .append(Component.text(unit)) .build(); } + + public static String formatSeconds(long seconds) { + if (seconds <= 0) { + return "0s"; + } + + long second = seconds; + long minute = second / 60; + second = second % 60; + + StringBuilder sb = new StringBuilder(); + if (minute != 0) { + sb.append(minute).append("m "); + } + if (second != 0) { + sb.append(second).append("s "); + } + + return sb.toString().trim(); + } } -- cgit From fc1e371d67551e9548491e9bf50534d91ce5d170 Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 27 Nov 2022 23:38:21 +0000 Subject: Temporary solution to async-profiler JVM crashing issues (#271, #273, #274) --- .../java/me/lucko/spark/common/SparkPlatform.java | 36 ++----- .../common/command/modules/SamplerModule.java | 4 +- .../common/sampler/BackgroundSamplerManager.java | 116 +++++++++++++++++++++ .../spark/common/sampler/SamplerContainer.java | 9 -- .../me/lucko/spark/common/util/Configuration.java | 60 +++++++++-- 5 files changed, 179 insertions(+), 46 deletions(-) create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/BackgroundSamplerManager.java (limited to 'spark-common/src/main/java/me/lucko/spark/common/util') diff --git a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java index 2574443..dae04ff 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java +++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java @@ -44,12 +44,9 @@ import me.lucko.spark.common.monitor.net.NetworkMonitor; import me.lucko.spark.common.monitor.ping.PingStatistics; import me.lucko.spark.common.monitor.ping.PlayerPingProvider; import me.lucko.spark.common.monitor.tick.TickStatistics; -import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.common.platform.PlatformStatisticsProvider; -import me.lucko.spark.common.sampler.Sampler; -import me.lucko.spark.common.sampler.SamplerBuilder; +import me.lucko.spark.common.sampler.BackgroundSamplerManager; import me.lucko.spark.common.sampler.SamplerContainer; -import me.lucko.spark.common.sampler.ThreadGrouper; import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.tick.TickReporter; @@ -104,6 +101,7 @@ public class SparkPlatform { private final ReentrantLock commandExecuteLock = new ReentrantLock(true); private final ActivityLog activityLog; private final SamplerContainer samplerContainer; + private final BackgroundSamplerManager backgroundSamplerManager; private final TickHook tickHook; private final TickReporter tickReporter; private final TickStatistics tickStatistics; @@ -143,10 +141,8 @@ public class SparkPlatform { this.activityLog = new ActivityLog(plugin.getPluginDirectory().resolve("activity.json")); this.activityLog.load(); - this.samplerContainer = new SamplerContainer(this.configuration.getBoolean( - "backgroundProfiler", - plugin.getPlatformInfo().getType() == PlatformInfo.Type.SERVER - )); + this.samplerContainer = new SamplerContainer(); + this.backgroundSamplerManager = new BackgroundSamplerManager(this, this.configuration); this.tickHook = plugin.createTickHook(); this.tickReporter = plugin.createTickReporter(); @@ -187,14 +183,7 @@ public class SparkPlatform { this.plugin.registerApi(api); SparkApi.register(api); - if (this.samplerContainer.isBackgroundProfilerEnabled()) { - this.plugin.log(Level.INFO, "Starting background profiler..."); - try { - startBackgroundProfiler(); - } catch (Throwable e) { - e.printStackTrace(); - } - } + this.backgroundSamplerManager.initialise(); } public void disable() { @@ -255,6 +244,10 @@ public class SparkPlatform { return this.samplerContainer; } + public BackgroundSamplerManager getBackgroundSamplerManager() { + return this.backgroundSamplerManager; + } + public TickHook getTickHook() { return this.tickHook; } @@ -287,17 +280,6 @@ public class SparkPlatform { return this.serverNormalOperationStartTime; } - public void startBackgroundProfiler() { - Sampler sampler = new SamplerBuilder() - .background(true) - .threadDumper(this.plugin.getDefaultThreadDumper()) - .threadGrouper(ThreadGrouper.BY_POOL) - .samplingInterval(this.configuration.getInteger("backgroundProfilerInterval", 10)) - .start(this); - - this.samplerContainer.setActiveSampler(sampler); - } - public Path resolveSaveFile(String prefix, String extension) { Path pluginFolder = this.plugin.getPluginDirectory(); try { diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java index f576eac..cd00f0d 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java @@ -337,9 +337,7 @@ public class SamplerModule implements CommandModule { handleUpload(platform, resp, sampler, comment, mergeMode, saveToFile); // if the previous sampler was running in the background, create a new one - if (platform.getSamplerContainer().isBackgroundProfilerEnabled()) { - platform.startBackgroundProfiler(); - + if (platform.getBackgroundSamplerManager().restartBackgroundSampler()) { resp.broadcastPrefixed(text() .append(text("Restarted the background profiler. ")) .append(text("(If you don't want this to happen, run: /" + platform.getPlugin().getCommandName() + " profiler cancel)", DARK_GRAY)) diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/BackgroundSamplerManager.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/BackgroundSamplerManager.java new file mode 100644 index 0000000..d655739 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/BackgroundSamplerManager.java @@ -0,0 +1,116 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.common.sampler; + +import me.lucko.spark.common.SparkPlatform; +import me.lucko.spark.common.platform.PlatformInfo; +import me.lucko.spark.common.util.Configuration; + +import java.util.logging.Level; + +public class BackgroundSamplerManager { + + private static final String OPTION_ENABLED = "backgroundProfiler"; + private static final String OPTION_ENGINE = "backgroundProfilerEngine"; + private static final String OPTION_INTERVAL = "backgroundProfilerInterval"; + + private static final String MARKER_FAILED = "_marker_background_profiler_failed"; + + private final SparkPlatform platform; + private final Configuration configuration; + private final boolean enabled; + + public BackgroundSamplerManager(SparkPlatform platform, Configuration configuration) { + this.platform = platform; + this.configuration = configuration; + this.enabled = this.configuration.getBoolean( + OPTION_ENABLED, + this.platform.getPlugin().getPlatformInfo().getType() == PlatformInfo.Type.SERVER + ); + } + + public void initialise() { + if (!this.enabled) { + return; + } + + // are we enabling the background profiler by default for the first time? + boolean didEnableByDefault = false; + if (!this.configuration.contains(OPTION_ENABLED)) { + this.configuration.setBoolean(OPTION_ENABLED, true); + didEnableByDefault = true; + } + + // did the background profiler fail to start on the previous attempt? + if (this.configuration.getBoolean(MARKER_FAILED, false)) { + this.platform.getPlugin().log(Level.WARNING, "It seems the background profiler failed to start when spark was last enabled. Sorry about that!"); + this.platform.getPlugin().log(Level.WARNING, "In the future, spark will try to use the built-in Java profiling engine instead."); + + this.configuration.remove(MARKER_FAILED); + this.configuration.setString(OPTION_ENGINE, "java"); + this.configuration.save(); + } + + this.platform.getPlugin().log(Level.INFO, "Starting background profiler..."); + + if (didEnableByDefault) { + // set the failed marker and save before we try to start the profiler, + // then remove the marker afterwards if everything goes ok! + this.configuration.setBoolean(MARKER_FAILED, true); + this.configuration.save(); + } + + try { + startSampler(); + + if (didEnableByDefault) { + this.configuration.remove(MARKER_FAILED); + this.configuration.save(); + } + + } catch (Throwable e) { + e.printStackTrace(); + } + } + + public boolean restartBackgroundSampler() { + if (this.enabled) { + startSampler(); + return true; + } + return false; + } + + private void startSampler() { + boolean forceJavaEngine = this.configuration.getString(OPTION_ENGINE, "async").equals("java"); + + Sampler sampler = new SamplerBuilder() + .background(true) + .threadDumper(this.platform.getPlugin().getDefaultThreadDumper()) + .threadGrouper(ThreadGrouper.BY_POOL) + .samplingInterval(this.configuration.getInteger(OPTION_INTERVAL, 10)) + .forceJavaSampler(forceJavaEngine) + .start(this.platform); + + this.platform.getSamplerContainer().setActiveSampler(sampler); + } + +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java index d55909c..15b1029 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java @@ -28,11 +28,6 @@ import java.util.concurrent.atomic.AtomicReference; public class SamplerContainer implements AutoCloseable { private final AtomicReference activeSampler = new AtomicReference<>(); - private final boolean backgroundProfilerEnabled; - - public SamplerContainer(boolean backgroundProfilerEnabled) { - this.backgroundProfilerEnabled = backgroundProfilerEnabled; - } /** * Gets the active sampler, or null if a sampler is not active. @@ -73,10 +68,6 @@ public class SamplerContainer implements AutoCloseable { } } - public boolean isBackgroundProfilerEnabled() { - return this.backgroundProfilerEnabled; - } - @Override public void close() { stopActiveSampler(true); diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java b/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java index ce63878..32f3bc6 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java @@ -20,32 +20,58 @@ package me.lucko.spark.common.util; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; public final class Configuration { - private static final JsonParser PARSER = new JsonParser(); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - private final JsonObject root; + private final Path file; + private JsonObject root; public Configuration(Path file) { + this.file = file; + load(); + } + + public void load() { JsonObject root = null; - if (Files.exists(file)) { - try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { - root = PARSER.parse(reader).getAsJsonObject(); + if (Files.exists(this.file)) { + try (BufferedReader reader = Files.newBufferedReader(this.file, StandardCharsets.UTF_8)) { + root = GSON.fromJson(reader, JsonObject.class); } catch (IOException e) { e.printStackTrace(); } } - this.root = root != null ? root : new JsonObject(); + if (root == null) { + root = new JsonObject(); + root.addProperty("_header", "spark configuration file - https://spark.lucko.me/docs/Configuration"); + } + this.root = root; + } + + public void save() { + try { + Files.createDirectories(this.file.getParent()); + } catch (IOException e) { + // ignore + } + + try (BufferedWriter writer = Files.newBufferedWriter(this.file, StandardCharsets.UTF_8)) { + GSON.toJson(this.root, writer); + } catch (IOException e) { + e.printStackTrace(); + } } public String getString(String path, String def) { @@ -77,4 +103,24 @@ public final class Configuration { return val.isBoolean() ? val.getAsInt() : def; } + public void setString(String path, String value) { + this.root.add(path, new JsonPrimitive(value)); + } + + public void setBoolean(String path, boolean value) { + this.root.add(path, new JsonPrimitive(value)); + } + + public void setInteger(String path, int value) { + this.root.add(path, new JsonPrimitive(value)); + } + + public boolean contains(String path) { + return this.root.has(path); + } + + public void remove(String path) { + this.root.remove(path); + } + } -- cgit