mirror of https://github.com/alibaba/arthas.git
add jfr command. #2262
parent
586e953e6c
commit
c536748f20
@ -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<Long, Recording> recordings = new ConcurrentHashMap<Long, Recording>();
|
||||
|
||||
@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<Recording> recordingList;
|
||||
if (state != null) {
|
||||
recordingList = findRecordingByState(state);
|
||||
} else {
|
||||
recordingList = new ArrayList<Recording>(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<Recording> findRecordingByState(String state) {
|
||||
List<Recording> resultRecordingList = new ArrayList<Recording>();
|
||||
Collection<Recording> 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<CliToken> tokens = completion.lineTokens();
|
||||
String token = tokens.get(tokens.size() - 1).value();
|
||||
|
||||
if (token.startsWith("-")) {
|
||||
super.complete(completion);
|
||||
return;
|
||||
}
|
||||
List<String> cmd = Arrays.asList("start", "status", "dump", "stop");
|
||||
CompletionUtils.complete(completion, cmd);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<JFRModel>{
|
||||
@Override
|
||||
public void draw(CommandProcess process, JFRModel result) {
|
||||
writeln(process, result.getJfrOutput());
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@ -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`是一个很简单的程序,它随机生成整数,再执行因式分解,把结果打印出来。如果生成的随机数是负数,则会打印提示信息。
|
@ -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"
|
||||
}
|
||||
}
|
@ -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.
|
@ -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)
|
@ -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"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue