44import java .io .FileInputStream ;
55import java .io .FileOutputStream ;
66import java .io .IOException ;
7+ import java .nio .charset .StandardCharsets ;
78import java .security .CodeSource ;
89import java .text .SimpleDateFormat ;
910import java .util .ArrayList ;
2627import com .taobao .arthas .core .shell .cli .CompletionUtils ;
2728import com .taobao .arthas .core .shell .command .AnnotatedCommand ;
2829import com .taobao .arthas .core .shell .command .CommandProcess ;
30+ import com .taobao .arthas .core .util .FileUtils ;
2931import com .taobao .middleware .cli .annotations .Argument ;
3032import com .taobao .middleware .cli .annotations .DefaultValue ;
3133import com .taobao .middleware .cli .annotations .Description ;
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