From c536748f20b985c511b000e4c378a186016d9d2f Mon Sep 17 00:00:00 2001 From: longxu0509 <108991596+longxu0509@users.noreply.github.com> Date: Thu, 25 Aug 2022 15:33:49 +0800 Subject: [PATCH] add jfr command. #2262 --- .../core/command/BuiltinCommandPack.java | 34 +- .../core/command/basic1000/JFRCommand.java | 413 ++++++++++++++++++ .../arthas/core/command/model/JFRModel.java | 22 + .../arthas/core/command/view/JFRView.java | 14 + .../core/command/view/ResultViewResolver.java | 1 + site/docs/.vuepress/configs/sidebar/en.js | 1 + site/docs/.vuepress/configs/sidebar/zh.js | 1 + .../public/doc/arthas-tutorials.html | 12 + .../public/images/arthas-output-recording.png | Bin 0 -> 20263 bytes site/docs/doc/commands.md | 1 + site/docs/doc/jfr.md | 115 +++++ site/docs/en/doc/commands.md | 3 +- site/docs/en/doc/jfr.md | 115 +++++ .../katacoda/command-jfr-cn/arthas-boot.md | 16 + .../katacoda/command-jfr-cn/arthas-demo.md | 10 + tutorials/katacoda/command-jfr-cn/finish.md | 11 + tutorials/katacoda/command-jfr-cn/index.json | 35 ++ tutorials/katacoda/command-jfr-cn/intro.md | 23 + tutorials/katacoda/command-jfr-cn/jfr.md | 126 ++++++ .../katacoda/command-jfr-en/arthas-boot.md | 16 + .../katacoda/command-jfr-en/arthas-demo.md | 11 + tutorials/katacoda/command-jfr-en/finish.md | 7 + tutorials/katacoda/command-jfr-en/index.json | 35 ++ tutorials/katacoda/command-jfr-en/intro.md | 39 ++ tutorials/katacoda/command-jfr-en/jfr.md | 126 ++++++ 25 files changed, 1165 insertions(+), 22 deletions(-) create mode 100644 core/src/main/java/com/taobao/arthas/core/command/basic1000/JFRCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/model/JFRModel.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/view/JFRView.java create mode 100644 site/docs/.vuepress/public/images/arthas-output-recording.png create mode 100644 site/docs/doc/jfr.md create mode 100644 site/docs/en/doc/jfr.md create mode 100644 tutorials/katacoda/command-jfr-cn/arthas-boot.md create mode 100644 tutorials/katacoda/command-jfr-cn/arthas-demo.md create mode 100644 tutorials/katacoda/command-jfr-cn/finish.md create mode 100644 tutorials/katacoda/command-jfr-cn/index.json create mode 100644 tutorials/katacoda/command-jfr-cn/intro.md create mode 100644 tutorials/katacoda/command-jfr-cn/jfr.md create mode 100644 tutorials/katacoda/command-jfr-en/arthas-boot.md create mode 100644 tutorials/katacoda/command-jfr-en/arthas-demo.md create mode 100644 tutorials/katacoda/command-jfr-en/finish.md create mode 100644 tutorials/katacoda/command-jfr-en/index.json create mode 100644 tutorials/katacoda/command-jfr-en/intro.md create mode 100644 tutorials/katacoda/command-jfr-en/jfr.md 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 2dcb9d22a..262793812 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 @@ -3,25 +3,10 @@ package com.taobao.arthas.core.command; import java.util.ArrayList; import java.util.List; -import com.taobao.arthas.core.command.basic1000.AuthCommand; -import com.taobao.arthas.core.command.basic1000.Base64Command; -import com.taobao.arthas.core.command.basic1000.CatCommand; -import com.taobao.arthas.core.command.basic1000.ClsCommand; -import com.taobao.arthas.core.command.basic1000.EchoCommand; -import com.taobao.arthas.core.command.basic1000.GrepCommand; -import com.taobao.arthas.core.command.basic1000.HelpCommand; -import com.taobao.arthas.core.command.basic1000.HistoryCommand; -import com.taobao.arthas.core.command.basic1000.KeymapCommand; -import com.taobao.arthas.core.command.basic1000.OptionsCommand; -import com.taobao.arthas.core.command.basic1000.PwdCommand; -import com.taobao.arthas.core.command.basic1000.ResetCommand; -import com.taobao.arthas.core.command.basic1000.SessionCommand; -import com.taobao.arthas.core.command.basic1000.StopCommand; -import com.taobao.arthas.core.command.basic1000.SystemEnvCommand; -import com.taobao.arthas.core.command.basic1000.SystemPropertyCommand; -import com.taobao.arthas.core.command.basic1000.TeeCommand; -import com.taobao.arthas.core.command.basic1000.VMOptionCommand; -import com.taobao.arthas.core.command.basic1000.VersionCommand; + +import com.alibaba.arthas.deps.org.slf4j.Logger; +import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; +import com.taobao.arthas.core.command.basic1000.*; import com.taobao.arthas.core.command.hidden.JulyCommand; import com.taobao.arthas.core.command.hidden.ThanksCommand; import com.taobao.arthas.core.command.klass100.ClassLoaderCommand; @@ -59,7 +44,7 @@ import com.taobao.middleware.cli.annotations.Name; * @author beiwei30 on 17/11/2016. */ public class BuiltinCommandPack implements CommandResolver { - + private static final Logger logger = LoggerFactory.getLogger(BuiltinCommandPack.class); private List commands = new ArrayList(); public BuiltinCommandPack(List disabledCommands) { @@ -72,7 +57,7 @@ public class BuiltinCommandPack implements CommandResolver { } private void initCommands(List disabledCommands) { - List> commandClassList = new ArrayList>(32); + List> commandClassList = new ArrayList>(33); commandClassList.add(HelpCommand.class); commandClassList.add(AuthCommand.class); commandClassList.add(KeymapCommand.class); @@ -120,6 +105,13 @@ public class BuiltinCommandPack implements CommandResolver { commandClassList.add(ProfilerCommand.class); commandClassList.add(VmToolCommand.class); commandClassList.add(StopCommand.class); + try { + if (ClassLoader.getSystemClassLoader().getResource("jdk/jfr/Recording.class") != null) { + commandClassList.add(JFRCommand.class); + } + } catch (Throwable e) { + logger.error("This jdk version not support jfr command"); + } for (Class clazz : commandClassList) { Name name = clazz.getAnnotation(Name.class); diff --git a/core/src/main/java/com/taobao/arthas/core/command/basic1000/JFRCommand.java b/core/src/main/java/com/taobao/arthas/core/command/basic1000/JFRCommand.java new file mode 100644 index 000000000..47e5cc6cd --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/basic1000/JFRCommand.java @@ -0,0 +1,413 @@ +package com.taobao.arthas.core.command.basic1000; + +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.command.model.JFRModel; +import com.taobao.arthas.core.server.ArthasBootstrap; +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.middleware.cli.annotations.*; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import jdk.jfr.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +@Name("jfr") +@Summary("Java Flight Recorder Command") +@Description(Constants.EXAMPLE + + " jfr start # start a new JFR recording\n" + + " jfr start -n myRecording --duration 60s -f /tmp/myRecording.jfr \n" + + " jfr status # list all recordings\n" + + " jfr status -r 1 # list recording id = 1 \n" + + " jfr status --state running # list recordings state = running\n" + + " jfr stop -r 1 # stop a JFR recording to default file\n" + + " jfr stop -r 1 -f /tmp/myRecording.jfr\n" + + " jfr dump -r 1 # copy contents of a JFR recording to default file\n" + + " jfr dump -r 1 -f /tmp/myRecording.jfr\n" + + Constants.WIKI + Constants.WIKI_HOME + "jfr") +public class JFRCommand extends AnnotatedCommand { + + private String cmd; + private String name; + private String settings; + private Boolean dumpOnExit; + private String delay; + private String duration; + private String filename; + private String maxAge; + private String maxSize; + private Long recording; + private String state; + private JFRModel result = new JFRModel(); + private static Map recordings = new ConcurrentHashMap(); + + @Argument(index = 0, argName = "cmd", required = true) + @Description("command name (start status stop dump)") + public void setCmd(String cmd) { + this.cmd = cmd; + } + + @Option(shortName = "n", longName = "name") + @Description("Name that can be used to identify recording, e.g. \"My Recording\"") + public void setName(String name) { + this.name = name; + } + + @Option(shortName = "s", longName = "settings") + @Description("Settings file(s), e.g. profile or default. See JRE_HOME/lib/jfr (STRING , default)") + public void setSettings(String settings) { + this.settings = settings; + } + + @Option(longName = "dumponexit") + @Description("Dump running recording when JVM shuts down (BOOLEAN, false)") + public void setDumpOnExit(Boolean dumpOnExit) { + this.dumpOnExit = dumpOnExit; + } + + @Option(shortName = "d", longName = "delay") + @Description("Delay recording start with (s)econds, (m)inutes), (h)ours), or (d)ays, e.g. 5h. (NANOTIME, 0)") + public void setDelay(String delay) { + this.delay = delay; + } + + @Option(longName = "duration") + @Description("Duration of recording in (s)econds, (m)inutes, (h)ours, or (d)ays, e.g. 300s. (NANOTIME, 0)") + public void setDuration(String duration) { + this.duration = duration; + } + + @Option(shortName = "f", longName = "filename") + @Description("Resulting recording filename, e.g. /tmp/MyRecording.jfr.") + public void setFilename(String filename) { + this.filename = filename; + } + + @Option(longName = "maxage") + @Description("Maximum time to keep recorded data (on disk) in (s)econds, (m)inutes, (h)ours, or (d)ays, e.g. 60m, or default for no limit (NANOTIME, 0)") + public void setMaxAge(String maxAge) { + this.maxAge = maxAge; + } + + @Option(longName = "maxsize") + @Description("Maximum amount of bytes to keep (on disk) in (k)B, (M)B or (G)B, e.g. 500M, 0 for no limit (MEMORY SIZE, 250MB)") + public void setMaxSize(String maxSize) { + this.maxSize = maxSize; + } + + @Option(shortName = "r", longName = "recording") + @Description("Recording number, or omit to see all recordings (LONG, -1)") + public void setRecording(Long recording) { + this.recording = recording; + } + + @Option(longName = "state") + @Description("Query recordings by sate (new, delay, running, stopped, closed)") + public void setState(String state) { + this.state = state; + } + + public String getCmd() { + return cmd; + } + + public String getName() { + return name; + } + + public String getSettings() { + return settings; + } + + public Boolean isDumpOnExit() { + return dumpOnExit; + } + + public String getDelay() { + return delay; + } + + public String getDuration() { + return duration; + } + + public String getFilename() { + return filename; + } + + public String getMaxAge() { + return maxAge; + } + + public String getMaxSize() { + return maxSize; + } + + public Long getRecording() { + return recording; + } + + public String getState() { + return state; + } + + @Override + public void process(CommandProcess process) { + + if (cmd.equals("start")) { + Configuration c = null; + try { + if (getSettings() == null) { + setSettings("default"); + } + c = Configuration.getConfiguration(settings); + } catch (Throwable e) { + process.end(-1, "Could not start recording, not able to read settings"); + } + Recording r = new Recording(c); + + if (getFilename() != null) { + try { + r.setDestination(Paths.get(getFilename())); + } catch (IOException e) { + r.close(); + process.end(-1, "Could not start recording, not able to write to file " + getFilename() + e.getMessage()); + } + } + + if (getMaxSize() != null) { + try { + r.setMaxSize(parseSize(getMaxSize())); + } catch (Exception e) { + process.end(-1, e.getMessage()); + } + } + + if (getMaxAge() != null) { + try { + r.setMaxAge(Duration.ofNanos(parseTimespan(getMaxAge()))); + } catch (Exception e) { + process.end(-1, e.getMessage()); + } + } + + if (isDumpOnExit() != false) { + r.setDumpOnExit(isDumpOnExit().booleanValue()); + } + + if (getDuration() != null) { + try { + r.setDuration(Duration.ofNanos(parseTimespan(getDuration()))); + } catch (Exception e) { + process.end(-1, e.getMessage()); + } + } + + if (getName() == null) { + r.setName("Recording-" + r.getId()); + } else { + r.setName(getName()); + } + + long id = r.getId(); + recordings.put(id, r); + + if (getDelay() != null) { + try { + r.scheduleStart(Duration.ofNanos(parseTimespan(getDelay()))); + } catch (Exception e) { + process.end(-1, e.getMessage()); + } + result.setJfrOutput("Recording " + r.getId() + " scheduled to start in " + getDelay()); + } else { + r.start(); + result.setJfrOutput("Started recording " + r.getId() + "."); + } + + if (duration == null && maxAge == null && maxSize == null) { + result.setJfrOutput(" No limit specified, using maxsize=250MB as default."); + r.setMaxSize(250*1024L*1024L); + } + + if (filename != null && duration != null) { + result.setJfrOutput(" The result will be written to:\n" + filename); + } + } else if (cmd.equals("status")) { + // list recording id = recording + if (getRecording() != null) { + Recording r = recordings.get(getRecording()); + if (r == null) + process.end(-1, "recording not exit"); + printRecording(r); + } else {// list all recordings + List recordingList; + if (state != null) { + recordingList = findRecordingByState(state); + } else { + recordingList = new ArrayList(recordings.values()); + } + if (recordingList.isEmpty()) { + process.end(-1, "No available recordings.\n Use jfr start to start a recording.\n"); + } else { + for (Recording recording : recordingList) { + printRecording(recording); + } + } + } + } else if (cmd.equals("dump")) { + if (recordings.isEmpty()) { + process.end(-1,"No recordings to dump. Use jfr start to start a recording."); + } + if (getRecording() != null) { + Recording r = recordings.get(getRecording()); + if (r == null) + process.end(-1, "recording not exit"); + if (getFilename() == null) { + try { + setFilename(outputFile()); + } catch (IOException e) { + process.end(-1, e.getMessage()); + } + } + + try { + r.dump(Paths.get(getFilename())); + } catch (IOException e) { + process.end(-1,"Could not to dump. "+ e.getMessage()); + } + result.setJfrOutput("Dump recording " + r.getId() + ", The result will be written to:\n" + getFilename()); + } else { + process.end(-1,"Failed to dump. Please input recording id"); + } + + } else if (cmd.equals("stop")) { + if (recordings.isEmpty()) { + process.end(-1,"No recordings to stop. Use jfr start to start a recording."); + } + if (getRecording() != null) { + Recording r = recordings.remove(getRecording()); + if (r == null) + process.end(-1, "recording not exit"); + if (r.getState().toString().equals("CLOSED") || r.getState().toString().equals("STOPPED")) + process.end(-1, "Failed to stop recording, state can not be closed/stopped"); + if (getFilename() == null) { + try { + setFilename(outputFile()); + } catch (IOException e) { + process.end(-1, e.getMessage()); + } + } + try { + r.setDestination(Paths.get(getFilename())); + } catch (IOException e) { + process.end(-1, "Failed to stop" + r.getName() +". Could not set destination for "+ filename+ "to file" + e.getMessage()); + } + + r.stop(); + result.setJfrOutput("Stop recording " + r.getId() + ", The result will be written to:\n" + getFilename()); + r.close(); + } else { + process.end(-1, "Failed to stop. please input recording id"); + } + } else { + process.end(-1, "Please input correct jfr command (start status stop dump)"); + } + + process.appendResult(result); + process.end(); + } + + public long parseSize(String s) throws Exception{ + s = s.toLowerCase(); + if (s.endsWith("b")) { + return Long.parseLong(s.substring(0, s.length() - 1).trim()); + } else if (s.endsWith("k")) { + return 1024 * Long.parseLong(s.substring(0, s.length() - 1).trim()); + } else if (s.endsWith("m")) { + return 1024 * 1024 * Long.parseLong(s.substring(0, s.length() - 1).trim()); + } else if (s.endsWith("g")) { + return 1024 * 1024 * 1024 * Long.parseLong(s.substring(0, s.length() - 1).trim()); + } else { + try { + return Long.parseLong(s); + } catch (Exception e) { + throw new NumberFormatException("'" + s + "' is not a valid size. Shoule be numeric value followed by a unit, i.e. 20M. Valid units k, M, G"); + } + } + } + + public long parseTimespan(String s) throws Exception { + s = s.toLowerCase(); + if (s.endsWith("s")) { + return TimeUnit.NANOSECONDS.convert(Long.parseLong(s.substring(0, s.length() - 1).trim()), TimeUnit.SECONDS); + } else if (s.endsWith("m")) { + return 60 * TimeUnit.NANOSECONDS.convert(Long.parseLong(s.substring(0, s.length() - 1).trim()), TimeUnit.SECONDS); + } else if (s.endsWith("h")) { + return 60 * 60 * TimeUnit.NANOSECONDS.convert(Long.parseLong(s.substring(0, s.length() - 1).trim()), TimeUnit.SECONDS); + } else if (s.endsWith("d")) { + return 24 * 60 * 60 * TimeUnit.NANOSECONDS.convert(Long.parseLong(s.substring(0, s.length() - 1).trim()), TimeUnit.SECONDS); + } else { + try { + return Long.parseLong(s); + } catch (NumberFormatException var2) { + throw new NumberFormatException("'" + s + "' is not a valid timespan. Shoule be numeric value followed by a unit, i.e. 20s. Valid units s, m, h and d."); + } + } + } + + private List findRecordingByState(String state) { + List resultRecordingList = new ArrayList(); + Collection recordingList = recordings.values(); + for (Recording recording : recordingList) { + if (recording.getState().toString().toLowerCase().equals(state)) + resultRecordingList.add(recording); + } + return resultRecordingList; + } + + private void printRecording(Recording recording) { + String format = "Recording: recording="+recording.getId()+" name="+recording.getName()+""; + result.setJfrOutput(format); + Duration duration = recording.getDuration(); + if (duration != null) { + result.setJfrOutput(" duration="+ duration.toString()); + } + result.setJfrOutput(" (" + recording.getState().toString().toLowerCase() + ")\n"); + } + + private String outputFile() throws IOException { + if (this.filename == null) { + File outputPath = ArthasBootstrap.getInstance().getOutputPath(); + if (outputPath != null) { + this.filename = new File(outputPath, + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date()) + ".jfr") + .getAbsolutePath(); + } else { + this.filename = File.createTempFile("arthas-output", ".jfr").getAbsolutePath(); + } + } + return filename; + } + + @Override + public void complete(Completion completion) { + List tokens = completion.lineTokens(); + String token = tokens.get(tokens.size() - 1).value(); + + if (token.startsWith("-")) { + super.complete(completion); + return; + } + List cmd = Arrays.asList("start", "status", "dump", "stop"); + CompletionUtils.complete(completion, cmd); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/model/JFRModel.java b/core/src/main/java/com/taobao/arthas/core/command/model/JFRModel.java new file mode 100644 index 000000000..c07b38d89 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/model/JFRModel.java @@ -0,0 +1,22 @@ +package com.taobao.arthas.core.command.model; + +/** + * @author xulong 2022/7/25 + */ +public class JFRModel extends ResultModel { + + private String jfrOutput = ""; + + @Override + public String getType() { + return "jfr"; + } + + public String getJfrOutput() { + return jfrOutput; + } + + public void setJfrOutput(String jfrOutput) { + this.jfrOutput += jfrOutput; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/JFRView.java b/core/src/main/java/com/taobao/arthas/core/command/view/JFRView.java new file mode 100644 index 000000000..900c76918 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/view/JFRView.java @@ -0,0 +1,14 @@ +package com.taobao.arthas.core.command.view; + +import com.taobao.arthas.core.command.model.JFRModel; +import com.taobao.arthas.core.shell.command.CommandProcess; + +/** + * @author longxu 2022/7/25 + */ +public class JFRView extends ResultView{ + @Override + public void draw(CommandProcess process, JFRModel result) { + writeln(process, result.getJfrOutput()); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java b/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java index fc9ffad07..947b60a1e 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java +++ b/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java @@ -79,6 +79,7 @@ public class ResultViewResolver { registerView(TraceView.class); registerView(WatchView.class); registerView(VmToolView.class); + registerView(JFRView.class); } catch (Throwable e) { logger.error("register result view failed", e); diff --git a/site/docs/.vuepress/configs/sidebar/en.js b/site/docs/.vuepress/configs/sidebar/en.js index 8dfd0fbb7..228b47d27 100644 --- a/site/docs/.vuepress/configs/sidebar/en.js +++ b/site/docs/.vuepress/configs/sidebar/en.js @@ -64,6 +64,7 @@ module.exports = { "/en/doc/tt.md", "/en/doc/watch.md", "/en/doc/profiler.md", + "/en/doc/jfr.md", "/en/doc/auth.md", "/en/doc/options.md", "/en/doc/base64.md", diff --git a/site/docs/.vuepress/configs/sidebar/zh.js b/site/docs/.vuepress/configs/sidebar/zh.js index 69e5e89b7..01d45f935 100644 --- a/site/docs/.vuepress/configs/sidebar/zh.js +++ b/site/docs/.vuepress/configs/sidebar/zh.js @@ -68,6 +68,7 @@ module.exports = { "/doc/tt.md", "/doc/watch.md", "/doc/profiler.md", + "/doc/jfr.md", "/doc/auth.md", "/doc/options.md", "/doc/base64.md", diff --git a/site/docs/.vuepress/public/doc/arthas-tutorials.html b/site/docs/.vuepress/public/doc/arthas-tutorials.html index 8df42b2b3..ac1439d6e 100644 --- a/site/docs/.vuepress/public/doc/arthas-tutorials.html +++ b/site/docs/.vuepress/public/doc/arthas-tutorials.html @@ -839,6 +839,18 @@ cn: "command-profiler-cn", }, }, + { + id: "command-jfr", + type: "COMMAND-ENHANCED", + names: { + en: "jfr", + cn: "jfr", + }, + ids: { + en: "command-jfr-en", + cn: "command-jfr-cn", + }, + }, { id: "case-web-console", type: "USERCASE", diff --git a/site/docs/.vuepress/public/images/arthas-output-recording.png b/site/docs/.vuepress/public/images/arthas-output-recording.png new file mode 100644 index 0000000000000000000000000000000000000000..9482abce5301990ae9fdba1c09abf5d41d0ff9a5 GIT binary patch literal 20263 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 literal 0 HcmV?d00001 diff --git a/site/docs/doc/commands.md b/site/docs/doc/commands.md index bbf22681b..41b87c368 100644 --- a/site/docs/doc/commands.md +++ b/site/docs/doc/commands.md @@ -43,6 +43,7 @@ ## profiler/火焰图 - [profiler](profiler.md) - 使用[async-profiler](https://github.com/jvm-profiling-tools/async-profiler)对应用采样,生成火焰图 +- [jfr](jfr.md) - 动态开启关闭 JFR 记录 ## 鉴权 diff --git a/site/docs/doc/jfr.md b/site/docs/doc/jfr.md new file mode 100644 index 000000000..9723d11ec --- /dev/null +++ b/site/docs/doc/jfr.md @@ -0,0 +1,115 @@ +# jfr + +[`jfr`在线教程](https://arthas.aliyun.com/doc/arthas-tutorials.html?language=cn&id=command-jfr) + +::: tip +Java Flight Recorder (JFR) 是一种用于收集有关正在运行的 Java 应用程序的诊断和分析数据的工具。它集成到 Java 虚拟机 (JVM) 中,几乎不会造成性能开销,因此即使在负载较重的生产环境中也可以使用。 +::: + +`jfr` 命令支持在程序动态运行过程中开启和关闭 JFR 记录。 记录收集有关 event 的数据。事件在特定时间点发生在 JVM 或 Java 应用程序中。每个事件都有一个名称、一个时间戳和一个可选的有效负载。负载是与事件相关的数据,例如 CPU 使用率、事件前后的 Java 堆大小、锁持有者的线程 ID 等。 + +`jfr` 命令基本运行结构是 `jfr cmd [actionArg]` + +> 注意: JDK8 的 8u262 版本之后才支持 jfr + +## 参数说明 + +| 参数名称 | 参数说明 | +| ------------: | :---------------------------------------------------------------------------------------- | +| _cmd_ | 要执行的操作 支持的命令【start,status,dump,stop】 | +| _actionArg_ | 属性名模式 | +| [n:] | 记录名称 | +| [r:] | 记录 id 值 | +| [dumponexit:] | 程序退出时,是否要 dump 出 .jfr 文件,默认为 false | +| [d:] | 延迟多久后启动 JFR 记录,支持带单位配置,eg: 60s, 2m, 5h, 3d. 不带单位就是秒,默认无延迟 | +| [duration:] | JFR 记录持续时间,支持单位配置,不带单位就是秒,默认一直记录 | +| [s:] | 采集 Event 的详细配置文件,默认是 default.jfc 位于 `$JAVA_HOME/lib/jfr/default.jfc` | +| [f:] | 将输出转储到指定路径 | +| [maxage:] | 缓冲区数据最大文件记录保存时间,支持单位配置,不带单位就是秒,默认是不限制 | +| [maxsize:] | 缓冲区的最大文件大小,支持单位配置, 不带单位是字节,m 或者 M 代表 MB,g 或者 G 代表 GB。 | +| [state:] | jfr 记录状态 | + +## 启动 JFR 记录 + +``` +$ jfr start +Started recording 1. No limit specified, using maxsize=250MB as default. +``` + +::: tip +默认情况下,开启的是默认参数的 jfr 记录 +::: + +启动 jfr 记录,指定记录名,记录持续时间,记录文件保存路径。 + +``` +$ jfr start -n myRecording --duration 60s -f /tmp/myRecording.jfr +Started recording 2. The result will be written to: +/tmp/myRecording.jfr +``` + +## 查看 JFR 记录状态 + +默认是查看所有 JFR 记录信息 + +```bash +$ jfr status +Recording: recording=1 name=Recording-1 (running) +Recording: recording=2 name=myRecording duration=PT1M (closed) +``` + +查看指定记录 id 的记录信息 + +```bash +$ jfr status -r 1 +Recording: recording=1 name=Recording-1 (running) +``` + +查看指定状态的记录信息 + +```bash +$ jfr status --state closed +Recording: recording=2 name=myRecording duration=PT1M (closed) +``` + +## dump jfr 记录 + +指定记录输出路径 + +```bash +$ jfr dump -r 1 -f /tmp/myRecording1.jfr +Dump recording 1, The result will be written to: +/tmp/myRecording1.jfr +``` + +不指定文件输出路径,默认是保存到`arthas-output`目录下 + +```bash +$ jfr dump -r 1 +Dump recording 1, The result will be written to: +/tmp/test/arthas-output/20220819-200915.jfr +``` + +## 停止 jfr 记录 + +不指定记录输出路径,默认是保存到`arthas-output`目录下 + +```bash +$ jfr stop -r 1 +Stop recording 1, The result will be written to: +/tmp/test/arthas-output/20220819-202049.jfr +``` + +> 注意一条记录只能停止一次。 + +也可以指定记录输出路径。 + +## 通过浏览器查看 arthas-output 下面 JFR 记录的结果 + +默认情况下,arthas 使用 8563 端口,则可以打开: [http://localhost:8563/arthas-output/](http://localhost:8563/arthas-output/) 查看到`arthas-output`目录下面的 JFR 记录结果: + +![](/images/arthas-output-recording.png) + +生成的结果可以用支持 jfr 格式的工具来查看。比如: + +- JDK Mission Control : https://github.com/openjdk/jmc diff --git a/site/docs/en/doc/commands.md b/site/docs/en/doc/commands.md index 76460b0d5..799b6418b 100644 --- a/site/docs/en/doc/commands.md +++ b/site/docs/en/doc/commands.md @@ -43,6 +43,7 @@ ## profiler/flame graph - [profiler](profiler.md) - use [async-profiler](https://github.com/jvm-profiling-tools/async-profiler) to generate flame graph +- [jfr](jfr.md) - dynamic opening and closing of jfr recordings ## authentication @@ -50,7 +51,7 @@ ## options -- [options](options.md) - check/set Arthas global options +- [options](options.md) - check/set Arthas global optionss ## pipe diff --git a/site/docs/en/doc/jfr.md b/site/docs/en/doc/jfr.md new file mode 100644 index 000000000..e1c58fb46 --- /dev/null +++ b/site/docs/en/doc/jfr.md @@ -0,0 +1,115 @@ +# jfr + +[`jfr` online tutorial](https://arthas.aliyun.com/doc/arthas-tutorials.html?language=en&id=command-jfr) + +::: tip +Java Flight Recorder (JFR) is a tool for collecting diagnostic and profiling data about a running Java application. It is integrated into the Java Virtual Machine (JVM) and causes almost no performance overhead, so it can be used even in heavily loaded production environments. +::: + +The `jfr` command supports starting and stopping JFR recordings during dynamic program running. Recording collects data about _events_. Events occur in the JVM or the Java application at a specific point in time. Each event has a name, a time stamp, and an optional _payload_. The payload is the data associated with an event, for example, the CPU usage, the Java heap size before and after the event, the thread ID of the lock holder, and so on. + +The basic usage of the `jfr` command is`jfr cmd [actionArg]` + +> Note: jfr is supported only after the 8u262 version of jdk8 + +### Supported Options + +| Name | Specification | +| ------------: | :------------------------------------------------------------------------------------------- | +| _cmd_ | Command to execute, support【start,status,dump,stop】 | +| _actionArg_ | Attribute name pattern | +| [n:] | Name of recording | +| [r:] | Recording id | +| [dumponexit:] | When the program exits, whether to dump the .jfr file. (boolean false) | +| [d:] | Duration of recording, i.e. 60s, 2m, 5h, 3d. default no delay | +| [duration:] | Duration of recording, default forever. | +| [s:] | Server-side template, The default is default.jfc located at `$JAVA_HOME/lib/jfr/default.jfc` | +| [f:] | Resulting recording filename | +| [maxage:] | Maximum age of buffer data | +| [maxsize:] | Maximum size of buffers in bytes | +| [state:] | Recording state | + +## Start jfr recording + +``` +$ jfr start +Started recording 1. No limit specified, using maxsize=250MB as default. +``` + +::: tip +The default JFR record is started. +::: + +Start the JFR recording, specify the recording name, duration, file saving path. + +``` +$ jfr start -n myRecording --duration 60s -f /tmp/myRecording.jfr +Started recording 2. The result will be written to: +/tmp/myRecording.jfr +``` + +##View jfr recordings status + +The default is to view all JFR recordings. + +```bash +$ jfr status +Recording: recording=1 name=Recording-1 (running) +Recording: recording=2 name=myRecording duration=PT1M (closed) +``` + +View the records of the specified recording ID. + +```bash +$ jfr status -r 1 +Recording: recording=1 name=Recording-1 (running) +``` + +View recordings in a specified state. + +```bash +$ jfr status --state closed +Recording: recording=2 name=myRecording duration=PT1M (closed) +``` + +## dump jfr recording + +Specifies the record output path. + +```bash +$ jfr dump -r 1 -f /tmp/myRecording1.jfr +Dump recording 1, The result will be written to: +/tmp/myRecording1.jfr +``` + +The file output path is not specified. By default, it is saved to the `arthas-output` directory + +```bash +$ jfr dump -r 1 +Dump recording 1, The result will be written to: +/tmp/test/arthas-output/20220819-200915.jfr +``` + +## Stop jfr recording + +No recording output path is specified, default is saved to `arthas-output` directory. + +```bash +$ jfr stop -r 1 +Stop recording 1, The result will be written to: +/tmp/test/arthas-output/20220819-202049.jfr +``` + +> notice: A recording can only be stopped once. + +You can also specify the record output path. + +## View JFR recording results under arthas-output via browser + +By default, arthas uses http port 8563 , which can be opened:[http://localhost:8563/arthas-output/](http://localhost:8563/arthas-output/) View the `arthas-output` directory below JFR recording results: + +![](/images/arthas-output-recording.png) + +The resulting results can be viewed with tools that support the JFR format. Such as: + +- JDK Mission Control : https://github.com/openjdk/jmc diff --git a/tutorials/katacoda/command-jfr-cn/arthas-boot.md b/tutorials/katacoda/command-jfr-cn/arthas-boot.md new file mode 100644 index 000000000..bd42b144e --- /dev/null +++ b/tutorials/katacoda/command-jfr-cn/arthas-boot.md @@ -0,0 +1,16 @@ + + + + +在新的`Terminal 2`里,下载`arthas-boot.jar`,再用`java -jar`命令启动: + +`wget https://arthas.aliyun.com/arthas-boot.jar +java -jar arthas-boot.jar --target-ip 0.0.0.0`{{execute T2}} + +`arthas-boot`是`Arthas`的启动程序,它启动后,会列出所有的Java进程,用户可以选择需要诊断的目标进程。 + +选择第一个进程,输入 `1`{{execute T2}} ,再`Enter/回车`: + +Attach成功之后,会打印Arthas LOGO。输入 `help`{{execute T2}} 可以获取到更多的帮助信息。 + +![Arthas Boot](/arthas/scenarios/common-resources/assets/arthas-boot.png) diff --git a/tutorials/katacoda/command-jfr-cn/arthas-demo.md b/tutorials/katacoda/command-jfr-cn/arthas-demo.md new file mode 100644 index 000000000..d55f26622 --- /dev/null +++ b/tutorials/katacoda/command-jfr-cn/arthas-demo.md @@ -0,0 +1,10 @@ + + + + +下载`math-game.jar`,再用`java -jar`命令启动: + +`wget https://arthas.aliyun.com/math-game.jar +java -jar math-game.jar`{{execute T1}} + +`math-game`是一个很简单的程序,它随机生成整数,再执行因式分解,把结果打印出来。如果生成的随机数是负数,则会打印提示信息。 diff --git a/tutorials/katacoda/command-jfr-cn/finish.md b/tutorials/katacoda/command-jfr-cn/finish.md new file mode 100644 index 000000000..d1e6fbadf --- /dev/null +++ b/tutorials/katacoda/command-jfr-cn/finish.md @@ -0,0 +1,11 @@ + +在“jfr”中,我们演示了了Arthas的jfr命令。如果有更多的技巧或者使用疑问,欢迎在Issue里提出。 + +* Issues: https://github.com/alibaba/arthas/issues +* 文档: https://arthas.aliyun.com/doc + +如果您在使用Arthas,请让我们知道。您的使用对我们非常重要:[查看](https://github.com/alibaba/arthas/issues/111) + +欢迎关注公众号,获取Arthas项目的信息、源码分析、案例实践。 + +![Arthas公众号](/arthas/scenarios/common-resources/assets/qrcode_gongzhonghao.jpg) diff --git a/tutorials/katacoda/command-jfr-cn/index.json b/tutorials/katacoda/command-jfr-cn/index.json new file mode 100644 index 000000000..94d0f78fe --- /dev/null +++ b/tutorials/katacoda/command-jfr-cn/index.json @@ -0,0 +1,35 @@ +{ + "title": "Arthas jfr命令", + "description": "Arthas jfr命令", + "difficulty": "精通者", + "time": "10-20 分钟", + "details": { + "steps": [ + { + "title": "启动arthas demo", + "text": "arthas-demo.md" + }, + { + "title": "启动arthas-boot", + "text": "arthas-boot.md" + }, + { + "title": "jfr命令", + "text": "jfr.md" + } + ], + "intro": { + "text": "intro.md" + }, + "finish": { + "text": "finish.md" + } + }, + "environment": { + "uilayout": "terminal" + }, + "backend": { + "imageid": "openjdk:15", + "environmentsprotocol": "http" + } +} \ No newline at end of file diff --git a/tutorials/katacoda/command-jfr-cn/intro.md b/tutorials/katacoda/command-jfr-cn/intro.md new file mode 100644 index 000000000..4f0c42a14 --- /dev/null +++ b/tutorials/katacoda/command-jfr-cn/intro.md @@ -0,0 +1,23 @@ + + + +![Arthas](https://arthas.aliyun.com/doc/_images/arthas.png) + +`Arthas` 是Alibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。 + +`Arthas` 支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 `Tab` 自动补全功能,进一步方便进行问题的定位和诊断。 + +当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决: + +- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception? +- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了? +- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗? +- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现! +- 是否有一个全局视角来查看系统的运行状况? +- 有什么办法可以监控到JVM的实时运行状态? +- 怎么快速定位应用的热点,生成火焰图? + +本教程会以一个简单的应用为例,演示jfr命令。 + +* Github: https://github.com/alibaba/arthas +* 文档: https://arthas.aliyun.com/doc/ diff --git a/tutorials/katacoda/command-jfr-cn/jfr.md b/tutorials/katacoda/command-jfr-cn/jfr.md new file mode 100644 index 000000000..430b32f6f --- /dev/null +++ b/tutorials/katacoda/command-jfr-cn/jfr.md @@ -0,0 +1,126 @@ +::: tip +Java Flight Recorder (JFR) 是一种用于收集有关正在运行的 Java 应用程序的诊断和分析数据的工具。它集成到 Java 虚拟机 (JVM) 中,几乎不会造成性能开销,因此即使在负载较重的生产环境中也可以使用。 +::: + +`jfr` 命令支持在程序动态运行过程中开启和关闭JFR记录。 记录收集有关event的数据。事件在特定时间点发生在 JVM 或 Java 应用程序中。每个事件都有一个名称、一个时间戳和一个可选的有效负载。负载是与事件相关的数据,例如 CPU 使用率、事件前后的 Java 堆大小、锁持有者的线程 ID 等。 + +`jfr` 命令基本运行结构是 `jfr cmd [actionArg]` +> 注意: JDK8的8u262 版本之后才支持 jfr +## 参数说明 + +| 参数名称 | 参数说明 | +| ------------: | :----------------------------------------------------------- | +| _cmd_ | 要执行的命令,支持的命令【start,status,dump,stop】 | +| _actionArg_ | 属性名模式 | +| [n:] | 记录名称 | +| [r:] | 记录id值 | +| [dumponexit:] | 程序退出时,是否要dump出 .jfr文件,默认为false | +| [d:] | 延迟多久后启动 JFR 记录,支持带单位配置,eg: 60s,2m,5h,3d. 不带单位就是秒,默认无延迟 | +| [duration:] | JFR 记录持续时间,支持单位配置,不带单位就是秒,默认一直记录 | +| [s:] | 采集 Event 的详细配置文件,默认是default.jfc 位于 `$JAVA_HOME/lib/jfr/default.jfc` | +| [f:] | 将输出转储到指定路径 | +| [maxage:] | 缓冲区数据最大文件记录保存时间,支持单位配置,不带单位就是秒,默认是不限制 | +| [maxsize:] | 缓冲区的最大文件大小,支持单位配置, 不带单位是字节,m或者M代表MB,g或者G代表GB。 | +| [state:] | jfr记录状态 | + +## 启动 JFR 记录 + +`jfr start`{{execute T2}} + +``` +$ jfr start +Started recording 1. No limit specified, using maxsize=250MB as default. +``` + +::: tip +开启的是默认参数的jfr记录 +::: + +启动jfr记录,指定记录名,记录持续时间,记录文件保存路径。 + +`jfr start -n myRecording --duration 60s -f /tmp/myRecording.jfr`{{execute T2}} + +``` +$ jfr start -n myRecording --duration 60s -f /tmp/myRecording.jfr +Started recording 2. The result will be written to: +/tmp/myRecording.jfr +``` + +## 查看 JFR 记录状态 + +默认是查看所有JFR记录信息 + +`jfr status`{{execute T2}} + +```bash +$ jfr status +Recording: recording=1 name=Recording-1 (running) +Recording: recording=2 name=myRecording duration=PT1M (closed) +``` + +查看指定记录id的记录信息 + +`jfr status -r 1`{{execute T2}} + +```bash +$ jfr status -r 1 +Recording: recording=1 name=Recording-1 (running) +``` + +查看指定状态的记录信息 + +`jfr status --state closed`{{execute T2}} + +```bash +$ jfr status --state closed +Recording: recording=2 name=myRecording duration=PT1M (closed) +``` + +## dump jfr 记录 + +指定记录输出路径 + +`$ jfr dump -r 1 -f /tmp/myRecording1.jfr`{{execute T2}} + +```bash +$ jfr dump -r 1 -f /tmp/myRecording1.jfr +Dump recording 1, The result will be written to: +/tmp/myRecording1.jfr +``` + +不指定文件输出路径,默认是保存到`arthas-output`目录下 + +`jfr dump -r 1`{{execute T2}} + +```bash +$ jfr dump -r 1 +Dump recording 1, The result will be written to: +/tmp/test/arthas-output/20220819-200915.jfr +``` + +## 停止 jfr 记录 + +不指定记录输出路径,默认是保存到`arthas-output`目录下 + +`jfr stop -r 1`{{execute T2}} + +```bash +$ jfr stop -r 1 +Stop recording 1, The result will be written to: +/tmp/test/arthas-output/20220819-202049.jfr +``` + +> 注意一条记录只能停止一次。 + +也可以指定记录输出路径。 + +## 通过浏览器查看 arthas-output 下面JFR记录的结果 + +默认情况下,arthas 使用 8563 端口,则可以打开: [http://localhost:8563/arthas-output/](http://localhost:8563/arthas-output/) 查看到`arthas-output`目录下面的 JFR 记录结果: + +![](/images/arthas-output-recording.png) + + +生成的结果可以用支持 jfr 格式的工具来查看。比如: + +- JDK Mission Control : https://github.com/openjdk/jmc diff --git a/tutorials/katacoda/command-jfr-en/arthas-boot.md b/tutorials/katacoda/command-jfr-en/arthas-boot.md new file mode 100644 index 000000000..1726fe727 --- /dev/null +++ b/tutorials/katacoda/command-jfr-en/arthas-boot.md @@ -0,0 +1,16 @@ + + + + +In the new `Terminal 2`, download `arthas-boot.jar` and start with the `java -jar` command: + +`wget https://arthas.aliyun.com/arthas-boot.jar +java -jar arthas-boot.jar --target-ip 0.0.0.0`{{execute T2}} + +`arthas-boot` is the launcher for `Arthas`. It lists all the Java processes, and the user can select the target process to be diagnosed. + +Select the first process, type `1`{{execute T2}} ,then type `Enter`: + +After the Attach is successful, Arthas LOGO is printed. Enter `help`{{execute T2}} for more help. + +![Arthas Boot](/arthas/scenarios/common-resources/assets/arthas-boot.png) diff --git a/tutorials/katacoda/command-jfr-en/arthas-demo.md b/tutorials/katacoda/command-jfr-en/arthas-demo.md new file mode 100644 index 000000000..dcc6454ad --- /dev/null +++ b/tutorials/katacoda/command-jfr-en/arthas-demo.md @@ -0,0 +1,11 @@ + + + + +Download `math-game.jar` and start with the `java -jar` command: + +`wget https://arthas.aliyun.com/math-game.jar +java -jar math-game.jar`{{execute T1}} + +`math-game` is a very simple program that randomly generates integers, performs factorization, and prints the results. +If the generated random number is negative, a error message will be printed. diff --git a/tutorials/katacoda/command-jfr-en/finish.md b/tutorials/katacoda/command-jfr-en/finish.md new file mode 100644 index 000000000..25d62564d --- /dev/null +++ b/tutorials/katacoda/command-jfr-en/finish.md @@ -0,0 +1,7 @@ + +The `jfr Tutorial` demonstrates the usage of profiler. If you have more tips or questions, please feel free to tell or ask in Issue. + +* Issues: https://github.com/alibaba/arthas/issues +* Documentation: https://arthas.aliyun.com/doc/en + +If you are using Arthas, please let us know that. Your use is very important to us: [View](https://github.com/alibaba/arthas/issues/111) diff --git a/tutorials/katacoda/command-jfr-en/index.json b/tutorials/katacoda/command-jfr-en/index.json new file mode 100644 index 000000000..d6065c43b --- /dev/null +++ b/tutorials/katacoda/command-jfr-en/index.json @@ -0,0 +1,35 @@ +{ + "title": "Arthas jfr Command", + "description": "Arthas jfr Command", + "difficulty": "master", + "time": "10-20 minutes", + "details": { + "steps": [ + { + "title": "Arthas demo", + "text": "arthas-demo.md" + }, + { + "title": "Start arthas-boot", + "text": "arthas-boot.md" + }, + { + "title": "jfr Command", + "text": "jfr.md" + } + ], + "intro": { + "text": "intro.md" + }, + "finish": { + "text": "finish.md" + } + }, + "environment": { + "uilayout": "terminal" + }, + "backend": { + "imageid": "openjdk:15", + "environmentsprotocol": "http" + } +} diff --git a/tutorials/katacoda/command-jfr-en/intro.md b/tutorials/katacoda/command-jfr-en/intro.md new file mode 100644 index 000000000..948575e6f --- /dev/null +++ b/tutorials/katacoda/command-jfr-en/intro.md @@ -0,0 +1,39 @@ + + + +![Arthas](https://arthas.aliyun.com/doc/_images/arthas.png) + +`Arthas` is a Java diagnostic tool open-sourced by Alibaba middleware team. Arthas helps developers in trouble-shooting issues in production environment for Java based applications without modifying code or restarting servers. + +`Arthas` supports JDK 6+, supports Linux/Mac/Windows. + +## Background + +Oftentimes the production system network is inaccessible from local development environment. If issues are encountered in production systems, it is impossible to use IDE to debug the application remotely. What’s even worse, debugging in production environment is unacceptable, as it will suspend all the threads, leading to services downtime. + +Developers could always try to reproduce the same issue on the test/staging environment. However, this is tricky as some issues cannot be reproduced easily in a different environment, or even disappear once restarted. + +And if you’re thinking of adding some logs to your code to help trouble-shoot the issue, you will have to go through the following lifecycle: test, staging, and then to production. Time is money! This approach is inefficient! Worse still, the issue may not be fixed since it might be irreproducible once the JVM is restarted, as described above. + +Arthas is built to solve these issues. A developer can troubleshoot production issues on the fly. No JVM restart, no additional code changes. Arthas works as an observer, that is, it will never suspend your running threads. + +## Key features + +- Check whether a class is loaded? Or where the class is loaded from? (Useful for trouble-shooting jar file conflicts) +- Decompile a class to ensure the code is running as expected. +- Check classloader statistics, e.g. the number of classloaders, the number of classes loaded per classloader, the classloader hierarchy, possible classloader leaks, etc. +- Check the method invocation details, e.g. method parameter, returned values, exceptions and etc. +- Check the stack trace of specified method invocation. This is useful when a developer wants to know the caller of the method. +- Trace the method invocation to find slow sub-invocations. +- Monitor method invocation statistics, e.g. QPS (Query Per Second), RT (Return Time), success rate and etc. +- Monitor system metrics, thread states and CPU usage, GC statistics and etc. +- Supports command line interactive mode, with auto-complete feature enabled. +- Supports telnet and WebSocket, which enables both local and remote diagnostics with command line and browsers. +- Supports profiler/Flame Graph +- Supports JDK 6+ +- Supports Linux/Mac/Windows + +This tutorial takes a simple application as an example to demonstrate the the usage of jfr. + +* Github: https://github.com/alibaba/arthas +* Docs: https://arthas.aliyun.com/doc/en diff --git a/tutorials/katacoda/command-jfr-en/jfr.md b/tutorials/katacoda/command-jfr-en/jfr.md new file mode 100644 index 000000000..b4ea7a01c --- /dev/null +++ b/tutorials/katacoda/command-jfr-en/jfr.md @@ -0,0 +1,126 @@ +::: tip +Java Flight Recorder (JFR) is a tool for collecting diagnostic and profiling data about a running Java application. It is integrated into the Java Virtual Machine (JVM) and causes almost no performance overhead, so it can be used even in heavily loaded production environments. +::: + +The `jfr` command supports starting and stopping JFR recordings during dynamic program running. Recording collects data about *events*. Events occur in the JVM or the Java application at a specific point in time. Each event has a name, a time stamp, and an optional *payload*. The payload is the data associated with an event, for example, the CPU usage, the Java heap size before and after the event, the thread ID of the lock holder, and so on. + +The basic usage of the `jfr` command is`jfr cmd [actionArg]` +> Note: jfr is supported only after the 8u262 version of jdk8 +### Supported Options + +| Name | Specification | +| ------------: | :----------------------------------------------------------- | +| _cmd_ | Command to execute, support【start,status,dump,stop】 | +| _actionArg_ | Attribute name pattern | +| [n:] | Name of recording | +| [r:] | Recording id | +| [dumponexit:] | When the program exits, whether to dump the .jfr file. (boolean false) | +| [d:] | Duration of recording, i.e. 60s, 2m, 5h, 3d. default no delay | +| [duration:] | Duration of recording, default forever. | +| [s:] | Server-side template, The default is default.jfc located at `$JAVA_HOME/lib/jfr/default.jfc` | +| [f:] | Resulting recording filename | +| [maxage:] | Maximum age of buffer data | +| [maxsize:] | Maximum size of buffers in bytes | +| [state:] | Recording state | + +## Start jfr recording + +`jfr start`{{execute T2}} + +``` +$ jfr start +Started recording 1. No limit specified, using maxsize=250MB as default. +``` + +::: tip +The default JFR record is started. +::: + +Start the JFR recording, specify the recording name, duration, file saving path. + +`jfr start -n myRecording --duration 60s -f /tmp/myRecording.jfr`{{execute T2}} + +``` +$ jfr start -n myRecording --duration 60s -f /tmp/myRecording.jfr +Started recording 2. The result will be written to: +/tmp/myRecording.jfr +``` + +##View jfr recordings status + +The default is to view all JFR recordings. + +`jfr status`{{execute T2}} + +```bash +$ jfr status +Recording: recording=1 name=Recording-1 (running) +Recording: recording=2 name=myRecording duration=PT1M (closed) +``` + +View the records of the specified recording ID. + +`jfr status -r 1`{{execute T2}} + +```bash +$ jfr status -r 1 +Recording: recording=1 name=Recording-1 (running) +``` + +View recordings in a specified state. + +`jfr status --state closed`{{execute T2}} + +```bash +$ jfr status --state closed +Recording: recording=2 name=myRecording duration=PT1M (closed) +``` + +## dump jfr recording + +Specifies the record output path. + +`$ jfr dump -r 1 -f /tmp/myRecording1.jfr`{{execute T2}} + +```bash +$ jfr dump -r 1 -f /tmp/myRecording1.jfr +Dump recording 1, The result will be written to: +/tmp/myRecording1.jfr +``` + +The file output path is not specified. By default, it is saved to the `arthas-output` directory + +`jfr dump -r 1`{{execute T2}} + +```bash +$ jfr dump -r 1 +Dump recording 1, The result will be written to: +/tmp/test/arthas-output/20220819-200915.jfr +``` + +## Stop jfr recording + +No recording output path is specified, default is saved to `arthas-output` directory. + +`jfr stop -r 1`{{execute T2}} + +```bash +$ jfr stop -r 1 +Stop recording 1, The result will be written to: +/tmp/test/arthas-output/20220819-202049.jfr +``` + +> notice: A recording can only be stopped once. + +You can also specify the record output path. + +## View JFR recording results under arthas-output via browser + +By default, arthas uses http port 8563 , which can be opened:[http://localhost:8563/arthas-output/](http://localhost:8563/arthas-output/) View the `arthas-output` directory below JFR recording results: + +![](/images/arthas-output-recording.png) + + +The resulting results can be viewed with tools that support the JFR format. Such as: + +- JDK Mission Control : https://github.com/openjdk/jmc