Skip to content

Commit 175fdb0

Browse files
committed
feat: add Markdown report generation for profiler output
1 parent c6efb82 commit 175fdb0

File tree

8 files changed

+688
-11
lines changed

8 files changed

+688
-11
lines changed

core/src/main/java/com/taobao/arthas/core/command/model/ProfilerModel.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ public class ProfilerModel extends ResultModel {
1010

1111
private String action;
1212
private String actionArg;
13+
/**
14+
* profiler stop/dump 输出格式(对应命令行 --format/-o)
15+
*/
16+
private String format;
1317
private String executeResult;
1418
private Collection<String> supportedActions;
1519
private String outputFile;
@@ -43,6 +47,14 @@ public void setActionArg(String actionArg) {
4347
this.actionArg = actionArg;
4448
}
4549

50+
public String getFormat() {
51+
return format;
52+
}
53+
54+
public void setFormat(String format) {
55+
this.format = format;
56+
}
57+
4658
public Collection<String> getSupportedActions() {
4759
return supportedActions;
4860
}

core/src/main/java/com/taobao/arthas/core/command/monitor200/ProfilerCommand.java

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.io.FileInputStream;
55
import java.io.FileOutputStream;
66
import java.io.IOException;
7+
import java.nio.charset.StandardCharsets;
78
import java.security.CodeSource;
89
import java.text.SimpleDateFormat;
910
import java.util.ArrayList;
@@ -26,6 +27,7 @@
2627
import com.taobao.arthas.core.shell.cli.CompletionUtils;
2728
import com.taobao.arthas.core.shell.command.AnnotatedCommand;
2829
import com.taobao.arthas.core.shell.command.CommandProcess;
30+
import com.taobao.arthas.core.util.FileUtils;
2931
import com.taobao.middleware.cli.annotations.Argument;
3032
import com.taobao.middleware.cli.annotations.DefaultValue;
3133
import com.taobao.middleware.cli.annotations.Description;
@@ -55,6 +57,7 @@
5557
+ " profiler start --loop 300s -f /tmp/result-%t.html"
5658
+ " profiler start --duration 300"
5759
+ " profiler stop --format html # output file format, support flat[=N]|traces[=N]|collapsed|flamegraph|tree|jfr\n"
60+
+ " profiler stop --format md # output Markdown report (LLM friendly), support md[=N]\n"
5861
+ " profiler stop --file /tmp/result.html\n"
5962
+ " profiler stop --threads \n"
6063
+ " profiler stop --include 'java/*' --include 'com/demo/*' --exclude '*Unsafe.park*'\n"
@@ -326,7 +329,7 @@ public void setFile(String file) {
326329
}
327330

328331
@Option(shortName = "o", longName = "format")
329-
@Description("dump output content format(flat[=N]|traces[=N]|collapsed|flamegraph|tree|jfr)")
332+
@Description("dump output content format(flat[=N]|traces[=N]|collapsed|flamegraph|tree|jfr|md[=N])")
330333
public void setFormat(String format) {
331334
// only for backward compatibility
332335
if ("html".equals(format)) {
@@ -335,6 +338,10 @@ public void setFormat(String format) {
335338
this.format = format;
336339
}
337340

341+
private boolean isMarkdownFormat() {
342+
return this.format != null && this.format.toLowerCase().startsWith("md");
343+
}
344+
338345
@Option(shortName = "e", longName = "event")
339346
@Description("which event to trace (cpu, alloc, lock, cache-misses etc.), default value is cpu")
340347
@DefaultValue("cpu")
@@ -619,10 +626,16 @@ private String executeArgs(ProfilerAction action) {
619626
this.format = "jfr";
620627
sb.append("jfrsync=").append(this.jfrsync).append(COMMA);
621628
}
622-
if (this.file != null) {
629+
boolean markdown = isMarkdownFormat();
630+
// md 是 Arthas 侧的后处理格式,不应传递给 async-profiler(避免识别失败/输出到文件导致数据丢失等问题)
631+
boolean passFile = this.file != null;
632+
if (markdown && (action == ProfilerAction.start || action == ProfilerAction.resume || action == ProfilerAction.check)) {
633+
passFile = false;
634+
}
635+
if (passFile) {
623636
sb.append("file=").append(this.file).append(COMMA);
624637
}
625-
if (this.format != null) {
638+
if (this.format != null && !markdown) {
626639
sb.append(this.format).append(COMMA);
627640
}
628641
if (this.interval != null) {
@@ -865,6 +878,15 @@ public void run() {
865878
}
866879

867880
private ProfilerModel processStop(AsyncProfiler asyncProfiler, ProfilerAction profilerAction) throws IOException {
881+
// profiler stop --file xxx.md:自动推断为 Markdown 输出
882+
if (this.format == null && this.file != null && this.file.toLowerCase().endsWith(".md")) {
883+
this.format = "md";
884+
}
885+
886+
if (isMarkdownFormat() && (profilerAction == ProfilerAction.stop || profilerAction == ProfilerAction.dump)) {
887+
return processStopMarkdown(asyncProfiler, profilerAction);
888+
}
889+
868890
String outputFile = null;
869891

870892
// If we're stopping and a file was specified during start, don't generate a new
@@ -889,6 +911,79 @@ private ProfilerModel processStop(AsyncProfiler asyncProfiler, ProfilerAction pr
889911
return profilerModel;
890912
}
891913

914+
private ProfilerModel processStopMarkdown(AsyncProfiler asyncProfiler, ProfilerAction profilerAction) throws IOException {
915+
// Markdown 输出:先让 async-profiler 输出 collapsed 文本,再在 Arthas 侧做结构化汇总。
916+
String userFormat = this.format;
917+
String userFile = this.file;
918+
int topN = mdTopN(userFormat);
919+
920+
// stop 时如果 start 阶段指定过 file,需要清理掉,避免影响后续 stop 行为
921+
if (profilerAction == ProfilerAction.stop) {
922+
fileSpecifiedAtStart = null;
923+
}
924+
925+
File collapsedFile = File.createTempFile("arthas-profiler-collapsed", ".txt");
926+
String collapsed;
927+
try {
928+
// 为避免 async-profiler 由于历史 file 配置导致返回 OK 而非 collapsed 文本,这里强制输出到临时文件后再读取。
929+
this.file = collapsedFile.getAbsolutePath();
930+
this.format = "collapsed";
931+
932+
String executeArgs = executeArgs(profilerAction);
933+
execute(asyncProfiler, executeArgs);
934+
935+
collapsed = FileUtils.readFileToString(collapsedFile, StandardCharsets.UTF_8);
936+
} finally {
937+
// best-effort cleanup
938+
try {
939+
collapsedFile.delete();
940+
} catch (Throwable ignore) {
941+
// ignore
942+
}
943+
this.format = userFormat;
944+
this.file = userFile;
945+
}
946+
947+
String markdown = ProfilerMarkdown.toMarkdown(new ProfilerMarkdown.Options()
948+
.action(profilerAction.name())
949+
.event(this.event)
950+
.threads(this.threads)
951+
.topN(topN)
952+
.collapsed(collapsed));
953+
954+
String outputFile = null;
955+
if (userFile != null && !userFile.trim().isEmpty()) {
956+
outputFile = userFile;
957+
FileUtils.writeByteArrayToFile(new File(outputFile), markdown.getBytes(StandardCharsets.UTF_8));
958+
}
959+
960+
ProfilerModel profilerModel = createProfilerModel(markdown);
961+
profilerModel.setFormat(userFormat);
962+
profilerModel.setOutputFile(outputFile);
963+
return profilerModel;
964+
}
965+
966+
private int mdTopN(String format) {
967+
final int defaultTopN = 10;
968+
if (format == null) {
969+
return defaultTopN;
970+
}
971+
String f = format.trim().toLowerCase();
972+
if (!f.startsWith("md")) {
973+
return defaultTopN;
974+
}
975+
int idx = f.indexOf('=');
976+
if (idx < 0 || idx == f.length() - 1) {
977+
return defaultTopN;
978+
}
979+
try {
980+
int n = Integer.parseInt(f.substring(idx + 1).trim());
981+
return n > 0 ? n : defaultTopN;
982+
} catch (Throwable e) {
983+
return defaultTopN;
984+
}
985+
}
986+
892987
private String outputFile() throws IOException {
893988
if (this.file == null) {
894989
String fileExt = outputFileExt();
@@ -911,6 +1006,8 @@ private String outputFileExt() {
9111006
String fileExt = "";
9121007
if (this.format == null) {
9131008
fileExt = "html";
1009+
} else if (this.format.toLowerCase().startsWith("md")) {
1010+
fileExt = "md";
9141011
} else if (this.format.startsWith("flat") || this.format.startsWith("traces")
9151012
|| this.format.equals("collapsed")) {
9161013
fileExt = "txt";
@@ -934,6 +1031,7 @@ private ProfilerModel createProfilerModel(String result) {
9341031
ProfilerModel profilerModel = new ProfilerModel();
9351032
profilerModel.setAction(action);
9361033
profilerModel.setActionArg(actionArg);
1034+
profilerModel.setFormat(format);
9371035
profilerModel.setExecuteResult(result);
9381036
return profilerModel;
9391037
}
@@ -989,8 +1087,12 @@ public void complete(Completion completion) {
9891087
if (token_2.equals("-e") || token_2.equals("--event")) {
9901088
CompletionUtils.complete(completion, events());
9911089
return;
992-
} else if (token_2.equals("-f") || token_2.equals("--format")) {
993-
CompletionUtils.complete(completion, Arrays.asList("html", "jfr"));
1090+
} else if (token_2.equals("-o") || token_2.equals("--format")) {
1091+
CompletionUtils.complete(completion, Arrays.asList(
1092+
"flamegraph", "tree", "jfr",
1093+
"flat", "traces", "collapsed",
1094+
"md", "md=10"
1095+
));
9941096
return;
9951097
}
9961098
}
@@ -1004,4 +1106,4 @@ public void complete(Completion completion) {
10041106
CompletionUtils.complete(completion, actions());
10051107
}
10061108

1007-
}
1109+
}

0 commit comments

Comments
 (0)