diff --git a/async-profiler/libasyncProfiler-linux-x64.so b/async-profiler/libasyncProfiler-linux-x64.so new file mode 100755 index 000000000..4f9232e11 Binary files /dev/null and b/async-profiler/libasyncProfiler-linux-x64.so differ diff --git a/async-profiler/libasyncProfiler-mac-x64.so b/async-profiler/libasyncProfiler-mac-x64.so new file mode 100755 index 000000000..9429b5143 Binary files /dev/null and b/async-profiler/libasyncProfiler-mac-x64.so differ diff --git a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java index a1a01a62f..7b86d1ff1 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java +++ b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java @@ -33,6 +33,7 @@ import com.taobao.arthas.core.command.monitor200.HeapDumpCommand; import com.taobao.arthas.core.command.monitor200.JvmCommand; import com.taobao.arthas.core.command.monitor200.MBeanCommand; import com.taobao.arthas.core.command.monitor200.MonitorCommand; +import com.taobao.arthas.core.command.monitor200.ProfilerCommand; import com.taobao.arthas.core.command.monitor200.StackCommand; import com.taobao.arthas.core.command.monitor200.ThreadCommand; import com.taobao.arthas.core.command.monitor200.TimeTunnelCommand; @@ -100,6 +101,7 @@ public class BuiltinCommandPack implements CommandResolver { commands.add(Command.create(CatCommand.class)); commands.add(Command.create(PwdCommand.class)); commands.add(Command.create(MBeanCommand.class)); - commands.add(Command.create(GrepCommand.class)); + commands.add(Command.create(GrepCommand.class)); + commands.add(Command.create(ProfilerCommand.class)); } } diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/ProfilerCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/ProfilerCommand.java new file mode 100644 index 000000000..dbf3490e4 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/ProfilerCommand.java @@ -0,0 +1,344 @@ +package com.taobao.arthas.core.command.monitor200; + +import java.io.File; +import java.io.IOException; +import java.security.CodeSource; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.taobao.arthas.common.OSUtils; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.cli.CompletionUtils; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.DefaultValue; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.middleware.logger.Logger; + +import one.profiler.AsyncProfiler; +import one.profiler.Counter; + +/** + * + * @author hengyunabc 2019-10-31 + * + */ +@Name("profiler") +@Summary("Profiler") +@Description("\nExamples:\n" + " mbean\n" + " mbean -m java.lang:type=Threading\n" + + " mbean java.lang:type=Threading\n" + " mbean java.lang:type=Threading *Count\n" + + " mbean -E java.lang:type=Threading PeakThreadCount|ThreadCount|DaemonThreadCount\n" + + " mbean -i 1000 java.lang:type=Threading *Count\n" + Constants.WIKI + Constants.WIKI_HOME + "profiler") +public class ProfilerCommand extends AnnotatedCommand { + private static final Logger logger = LogUtil.getArthasLogger(); + + private String action; + private String actionArg; + + private String event; + + private String file; + /** + * output file format, default value is svg. + */ + private String format; + + /** + * sampling interval in ns (default: 10'000'000, i.e. 10 ms) + */ + private Long interval; + + private boolean threads; + + private static String libPath; + private static AsyncProfiler profiler = null; + + static { + String profierSoPath = null; + if (OSUtils.isMac()) { + profierSoPath = "async-profiler/libasyncProfiler-mac-x64.so"; + } + if (OSUtils.isLinux()) { + profierSoPath = "async-profiler/libasyncProfiler-linux-x64.so"; + } + + if (profierSoPath != null) { + CodeSource codeSource = ProfilerCommand.class.getProtectionDomain().getCodeSource(); + if (codeSource != null) { + try { + File bootJarPath = new File(codeSource.getLocation().toURI().getSchemeSpecificPart()); + File soFile = new File(bootJarPath.getParentFile(), profierSoPath); + if (soFile.exists()) { + libPath = soFile.getAbsolutePath(); + } + } catch (Throwable e) { + logger.error("arthas", "can not find libasyncProfiler so", e); + } + } + } + + } + + @Argument(argName = "action", index = 0, required = true) + @Description("Action to execute") + public void setAction(String action) { + this.action = action; + } + + @Argument(argName = "actionArg", index = 1, required = false) + @Description("Attribute name pattern.") + public void setActionArg(String actionArg) { + this.actionArg = actionArg; + } + + @Option(shortName = "i", longName = "interval") + @Description("sampling interval in ns (default: 10'000'000, i.e. 10 ms)") + @DefaultValue("10000000") + public void setInterval(long interval) { + this.interval = interval; + } + + @Option(shortName = "f", longName = "file") + @Description("dump output to ") + public void setFile(String file) { + this.file = file; + } + + @Option(longName = "format") + @Description("dump output file format(svg, html, jfr), default valut is svg") + @DefaultValue("svg") + public void setFormat(String format) { + this.format = format; + } + + @Option(shortName = "e", longName = "event") + @Description("which event to trace (cpu, alloc, lock, cache-misses etc.), default value is cpu") + @DefaultValue("cpu") + public void setEvent(String event) { + this.event = event; + } + + @Option(longName = "threads") + @Description("profile different threads separately") + public void setThreads(boolean threads) { + this.threads = threads; + } + + private AsyncProfiler profilerInstance() { + if (profiler != null) { + return profiler; + } + + // try to load from special path + if ("load".equals(action)) { + profiler = AsyncProfiler.getInstance(this.actionArg); + } + + if (libPath != null) { + // load from arthas directory + profiler = AsyncProfiler.getInstance(libPath); + } else { + if (OSUtils.isLinux() || OSUtils.isLinux()) { + throw new IllegalStateException("Can not find libasyncProfiler so, please check the arthas directory."); + } else { + throw new IllegalStateException("Current OS do not support AsyncProfiler."); + } + } + + return profiler; + } + + enum ProfilerAction { + execute, start, stop, resume, list, version, status, + + dumpCollapsed, dumpFlat, dumpTraces, getSamples, + } + + private String executeArgs(ProfilerAction action) { + StringBuilder sb = new StringBuilder(); + + // start - start profiling + // resume - start or resume profiling without resetting collected data + // stop - stop profiling + sb.append(action).append(','); + + if (this.event != null) { + sb.append("event=").append(this.event).append(','); + } + if (this.file != null) { + sb.append("file=").append(this.file).append(','); + } + if (this.interval != null) { + sb.append("interval=").append(this.interval).append(','); + } + if (this.threads) { + sb.append("threads").append(','); + } + + return sb.toString(); + } + + private static String execute(AsyncProfiler asyncProfiler, String arg) + throws IllegalArgumentException, IOException { + String result = asyncProfiler.execute(arg); + if (!result.endsWith("\n")) { + result += "\n"; + } + return result; + } + + @Override + public void process(CommandProcess process) { + int status = 0; + try { + AsyncProfiler asyncProfiler = this.profilerInstance(); + + ProfilerAction profilerAction = ProfilerAction.valueOf(action); + + if (ProfilerAction.execute.equals(profilerAction)) { + String result = execute(asyncProfiler, this.actionArg); + process.write(result); + } else if (ProfilerAction.start.equals(profilerAction)) { + String executeArgs = executeArgs(ProfilerAction.start); + String result = execute(asyncProfiler, executeArgs); + process.write(result); + } else if (ProfilerAction.stop.equals(profilerAction)) { + if (this.file == null) { + this.file = new File("arthas-output", + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date()) + "." + this.format) + .getAbsolutePath(); + } + process.write("profiler output file: " + new File(this.file).getAbsolutePath() + "\n"); + String executeArgs = executeArgs(ProfilerAction.stop); + String result = execute(asyncProfiler, executeArgs); + process.write(result); + } else if (ProfilerAction.resume.equals(profilerAction)) { + String executeArgs = executeArgs(ProfilerAction.resume); + String result = execute(asyncProfiler, executeArgs); + process.write(result); + } else if (ProfilerAction.list.equals(profilerAction)) { + String result = asyncProfiler.execute("list"); + process.write(result); + } else if (ProfilerAction.version.equals(profilerAction)) { + String result = asyncProfiler.execute("version"); + process.write(result); + } else if (ProfilerAction.status.equals(profilerAction)) { + String result = asyncProfiler.execute("status"); + process.write(result); + } else if (ProfilerAction.dumpCollapsed.equals(profilerAction)) { + if (actionArg == null) { + actionArg = "TOTAL"; + } + actionArg = actionArg.toUpperCase(); + System.err.println("actionArg: " + actionArg + ", " + "TOTAL".equals(actionArg)); + if ("TOTAL".equals(actionArg) || "SAMPLES".equals(actionArg)) { + String result = asyncProfiler.dumpCollapsed(Counter.valueOf(actionArg)); + process.write(result); + } else { + process.write("ERROR: dumpCollapsed argumment should be TOTAL or SAMPLES. \n"); + status = 1; + } + } else if (ProfilerAction.dumpFlat.equals(profilerAction)) { + int maxMethods = 0; + if (actionArg != null) { + maxMethods = Integer.valueOf(actionArg); + } + String result = asyncProfiler.dumpFlat(maxMethods); + process.write(result); + } else if (ProfilerAction.dumpTraces.equals(profilerAction)) { + int maxTraces = 0; + if (actionArg != null) { + maxTraces = Integer.valueOf(actionArg); + } + String result = asyncProfiler.dumpTraces(maxTraces); + process.write(result); + } else if (ProfilerAction.getSamples.equals(profilerAction)) { + String result = "" + asyncProfiler.getSamples() + "\n"; + process.write(result); + } + } catch (Throwable e) { + process.write(e.getMessage()).write("\n"); + logger.error("arthas", "AsyncProfiler error", e); + status = 1; + } finally { + process.end(status); + } + } + + private List events() { + List result = new ArrayList(); + + String execute; + try { + /** + *
+               Basic events:
+                  cpu
+                  alloc
+                  lock
+                  wall
+                  itimer
+             * 
+ */ + execute = this.profilerInstance().execute("list"); + } catch (Throwable e) { + // ignore + return result; + } + String lines[] = execute.split("\\r?\\n"); + + if (lines != null) { + for (String line : lines) { + if (line.startsWith(" ")) { + result.add(line.trim()); + } + } + } + return result; + } + + @Override + public void complete(Completion completion) { + List tokens = completion.lineTokens(); + String token = tokens.get(tokens.size() - 1).value(); + + if (tokens.size() >= 2) { + CliToken cliToken_1 = tokens.get(tokens.size() - 1); + CliToken cliToken_2 = tokens.get(tokens.size() - 2); + if (cliToken_1.isBlank()) { + String token_2 = cliToken_2.value(); + if (token_2.equals("-e") || token_2.equals("--event")) { + CompletionUtils.complete(completion, events()); + return; + } else if (token_2.equals("-f") || token_2.equals("--format")) { + CompletionUtils.complete(completion, Arrays.asList("svg", "html", "jfr")); + return; + } + } + } + + if (token.startsWith("-")) { + super.complete(completion); + return; + } + + Set values = new HashSet(); + for (ProfilerAction action : ProfilerAction.values()) { + values.add(action.toString()); + } + CompletionUtils.complete(completion, values); + } + +} \ No newline at end of file diff --git a/core/src/main/java/one/profiler/AsyncProfiler.java b/core/src/main/java/one/profiler/AsyncProfiler.java new file mode 100644 index 000000000..17be8e56d --- /dev/null +++ b/core/src/main/java/one/profiler/AsyncProfiler.java @@ -0,0 +1,160 @@ +/* + * Copyright 2018 Andrei Pangin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package one.profiler; + +/** + * Java API for in-process profiling. Serves as a wrapper around + * async-profiler native library. This class is a singleton. + * The first call to {@link #getInstance()} initiates loading of + * libasyncProfiler.so. + */ +public class AsyncProfiler implements AsyncProfilerMXBean { + private static AsyncProfiler instance; + + private final String version; + + private AsyncProfiler() { + this.version = version0(); + } + + public static AsyncProfiler getInstance() { + return getInstance(null); + } + + public static synchronized AsyncProfiler getInstance(String libPath) { + if (instance != null) { + return instance; + } + + if (libPath == null) { + System.loadLibrary("asyncProfiler"); + } else { + System.load(libPath); + } + + instance = new AsyncProfiler(); + return instance; + } + + /** + * Start profiling + * + * @param event Profiling event, see {@link Events} + * @param interval Sampling interval, e.g. nanoseconds for Events.CPU + * @throws IllegalStateException If profiler is already running + */ + @Override + public void start(String event, long interval) throws IllegalStateException { + start0(event, interval, true); + } + + /** + * Start or resume profiling without resetting collected data. + * Note that event and interval may change since the previous profiling session. + * + * @param event Profiling event, see {@link Events} + * @param interval Sampling interval, e.g. nanoseconds for Events.CPU + * @throws IllegalStateException If profiler is already running + */ + @Override + public void resume(String event, long interval) throws IllegalStateException { + start0(event, interval, false); + } + + /** + * Stop profiling (without dumping results) + * + * @throws IllegalStateException If profiler is not running + */ + @Override + public void stop() throws IllegalStateException { + stop0(); + } + + /** + * Get the number of samples collected during the profiling session + * + * @return Number of samples + */ + @Override + public native long getSamples(); + + /** + * Get profiler agent version, e.g. "1.0" + * + * @return Version string + */ + @Override + public String getVersion() { + return version; + } + + /** + * Execute an agent-compatible profiling command - + * the comma-separated list of arguments described in arguments.cpp + * + * @param command Profiling command + * @return The command result + * @throws IllegalArgumentException If failed to parse the command + * @throws java.io.IOException If failed to create output file + */ + @Override + public String execute(String command) throws IllegalArgumentException, java.io.IOException { + return execute0(command); + } + + /** + * Dump profile in 'collapsed stacktraces' format + * + * @param counter Which counter to display in the output + * @return Textual representation of the profile + */ + @Override + public String dumpCollapsed(Counter counter) { + return dumpCollapsed0(counter.ordinal()); + } + + /** + * Dump collected stack traces + * + * @param maxTraces Maximum number of stack traces to dump. 0 means no limit + * @return Textual representation of the profile + */ + @Override + public String dumpTraces(int maxTraces) { + return dumpTraces0(maxTraces); + } + + /** + * Dump flat profile, i.e. the histogram of the hottest methods + * + * @param maxMethods Maximum number of methods to dump. 0 means no limit + * @return Textual representation of the profile + */ + @Override + public String dumpFlat(int maxMethods) { + return dumpFlat0(maxMethods); + } + + private native void start0(String event, long interval, boolean reset) throws IllegalStateException; + private native void stop0() throws IllegalStateException; + private native String execute0(String command) throws IllegalArgumentException, java.io.IOException; + private native String dumpCollapsed0(int counter); + private native String dumpTraces0(int maxTraces); + private native String dumpFlat0(int maxMethods); + private native String version0(); +} diff --git a/core/src/main/java/one/profiler/AsyncProfilerMXBean.java b/core/src/main/java/one/profiler/AsyncProfilerMXBean.java new file mode 100644 index 000000000..0bc24bf7e --- /dev/null +++ b/core/src/main/java/one/profiler/AsyncProfilerMXBean.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Andrei Pangin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package one.profiler; + +/** + * AsyncProfiler interface for JMX server. + * How to register AsyncProfiler MBean: + * + *
{@code
+ *     ManagementFactory.getPlatformMBeanServer().registerMBean(
+ *             AsyncProfiler.getInstance(),
+ *             new ObjectName("one.profiler:type=AsyncProfiler")
+ *     );
+ * }
+ */ +public interface AsyncProfilerMXBean { + void start(String event, long interval) throws IllegalStateException; + void resume(String event, long interval) throws IllegalStateException; + void stop() throws IllegalStateException; + + long getSamples(); + String getVersion(); + + String execute(String command) throws IllegalArgumentException, java.io.IOException; + + String dumpCollapsed(Counter counter); + String dumpTraces(int maxTraces); + String dumpFlat(int maxMethods); +} diff --git a/core/src/main/java/one/profiler/Counter.java b/core/src/main/java/one/profiler/Counter.java new file mode 100644 index 000000000..489053b72 --- /dev/null +++ b/core/src/main/java/one/profiler/Counter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Andrei Pangin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package one.profiler; + +/** + * Which metrics to use when generating profile in collapsed stack traces format. + */ +public enum Counter { + SAMPLES, + TOTAL +} diff --git a/core/src/main/java/one/profiler/Events.java b/core/src/main/java/one/profiler/Events.java new file mode 100644 index 000000000..1731122c7 --- /dev/null +++ b/core/src/main/java/one/profiler/Events.java @@ -0,0 +1,28 @@ +/* + * Copyright 2018 Andrei Pangin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package one.profiler; + +/** + * Predefined event names to use in {@link AsyncProfiler#start(String, long)} + */ +public class Events { + public static final String CPU = "cpu"; + public static final String ALLOC = "alloc"; + public static final String LOCK = "lock"; + public static final String WALL = "wall"; + public static final String ITIMER = "itimer"; +} diff --git a/core/src/main/java/one/profiler/package-info.java b/core/src/main/java/one/profiler/package-info.java new file mode 100644 index 000000000..8430f25e7 --- /dev/null +++ b/core/src/main/java/one/profiler/package-info.java @@ -0,0 +1,5 @@ + +/** + * from https://github.com/jvm-profiling-tools/async-profiler + */ +package one.profiler; diff --git a/packaging/src/main/assembly/assembly.xml b/packaging/src/main/assembly/assembly.xml index a871e65d4..e612888f4 100644 --- a/packaging/src/main/assembly/assembly.xml +++ b/packaging/src/main/assembly/assembly.xml @@ -50,4 +50,10 @@ ../bin/as-service.bat + + + + ../async-profiler + +