content: package com.taobao.arthas.core.command.monitor200;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  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 java.util.concurrent.TimeUnit;
  
  import com.alibaba.arthas.deps.org.slf4j.Logger;
  import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
  import com.taobao.arthas.common.IOUtils;
  import com.taobao.arthas.common.OSUtils;
  import com.taobao.arthas.core.command.Constants;
  import com.taobao.arthas.core.command.model.ProfilerModel;
  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.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 arthas.VmTool;
  import one.profiler.AsyncProfiler;
  import one.profiler.Counter;
  
  /**
   * https://github.com/async-profiler/async-profiler/blob/master/docs/ProfilerOptions.md 具体参数说明，以及哪些参数可以传递给 async-profiler agent
   * @author hengyunabc 2019-10-31
   *
   */
  //@formatter:off
  @Name("profiler")
  @Summary("Async Profiler. https://github.com/jvm-profiling-tools/async-profiler")
  @Description(Constants.EXAMPLE
          + "  profiler start\n"
          + "  profiler stop\n"
          + "  profiler list                # list all supported events\n"
          + "  profiler actions             # list all supported actions\n"
          + "  profiler start --event alloc\n"
          + "  profiler start --timeout 300s"
          + "  profiler start --loop 300s -f /tmp/result-%t.html"
          + "  profiler start --duration 300"
          + "  profiler stop --format html   # output file format, support flat[=N]|traces[=N]|collapsed|flamegraph|tree|jfr\n"
          + "  profiler stop --file /tmp/result.html\n"
          + "  profiler stop --threads \n"
          + "  profiler stop --include 'java/*' --include 'com/demo/*' --exclude '*Unsafe.park*'\n"
          + "  profiler status\n"
          + "  profiler resume              # Start or resume profiling without resetting collected data.\n"
          + "  profiler getSamples          # Get the number of samples collected during the profiling session\n"
          + "  profiler dumpFlat            # Dump flat profile, i.e. the histogram of the hottest methods\n"
          + "  profiler dumpCollapsed       # Dump profile in 'collapsed stacktraces' format\n"
          + "  profiler dumpTraces          # Dump collected stack traces\n"
          + "  profiler execute 'stop,file=/tmp/result.html'   # Execute an agent-compatible profiling command\n"
          + Constants.WIKI + Constants.WIKI_HOME + "profiler")
  //@formatter:on
  public class ProfilerCommand extends AnnotatedCommand {
      private static final Logger logger = LoggerFactory.getLogger(ProfilerCommand.class);
      // Track if a file was specified during profiler start
      private static String fileSpecifiedAtStart = null;
  
      // TODO start 时，没指定 file， 是否在  stop 时，能生成 html 或者 jfr 不？ 
      private String action;
      private String actionArg;
  
      /**
       * which event to trace (cpu, wall, cache-misses, etc.)
       */
      private String event;
  
      /**
       * profile allocations with BYTES interval
       * according to async-profiler README, alloc may contains non-numeric characters
       */
      private String alloc;
  
      /**
       * build allocation profile from live objects only
       */
      private boolean live;
  
      /**
       * profile contended locks longer than DURATION ns
       * according to async-profiler README, alloc may contains non-numeric characters
       */
      private String lock;
  
      /**
       * start Java Flight Recording with the given config along with the profiler
       */
      private String jfrsync;
  
      /**
       * output file name for dumping
       */
      private String file;
  
      /**
       * output file format, default value is html.
       */
      private String format;
  
      /**
       * sampling interval in ns (default: 10'000'000, i.e. 10 ms)
       */
      private Long interval;
  
      /**
       * maximum Java stack depth (default: 2048)
       */
      private Integer jstackdepth;
  
      /**
       * wall clock profiling interval
       */
      private Long wall;
  
      /**
       * profile different threads separately
       */
      private boolean threads;
  
      /**
       * group threads by scheduling policy
       */
      private boolean sched;
  
      /**
       * how to collect C stack frames in addition to Java stack
       * MODE is 'fp' (Frame Pointer), 'dwarf', 'lbr' (Last Branch Record) or 'no'
       */
      private String cstack;
  
      /**
       * use simple class names instead of FQN
       */
      private boolean simple;
  
      /**
       * print method signatures
       */
      private boolean sig;
  
      /**
       * annotate Java methods
       */
      private boolean ann;
  
      /**
       * prepend library names
       */
      private boolean lib;
  
      /**
       * include only user-mode events
       */
      private boolean alluser;
  
      /**
       * run profiling for <duration> seconds
       */
      private Long duration;
  
      /**
       * include stack traces containing PATTERN
       */
      private List<String> includes;
  
      /**
       * exclude stack traces containing PATTERN
       */
      private List<String> excludes;
  
      /**
       * automatically start profiling when the specified native function is executed.
       */
      private String begin;
  
      /**
       * automatically stop profiling when the specified native function is executed.
       */
      private String end;
  
      /**
       * time-to-safepoint profiling.
       * An alias for --begin SafepointSynchronize::begin --end RuntimeService::record_safepoint_synchronized
       */
      private boolean ttsp;
  
      /**
       * FlameGraph title
       */
      private String title;
  
      /**
       * FlameGraph minimum frame width in percent
       */
      private String minwidth;
  
      /**
       * generate stack-reversed FlameGraph / Call tree
       */
      private boolean reverse;
  
      /**
       * count the total value (time, bytes, etc.) instead of samples
       */
      private boolean total;
  
      /**
       * approximate size of JFR chunk in bytes (default: 100 MB)
       */
      private String chunksize;
  
      /**
       * duration of JFR chunk in seconds (default: 1 hour)
       */
      private String chunktime;
  
      /**
       * run profiler in a loop (continuous profiling)
       */
      private String loop;
  
      /**
       * automatically stop profiler at TIME (absolute or relative)
       */
      private String timeout;
  
      /**
       * Features enabled for profiling
       */
      private String features;
  
      /**
       * Profiling signal to use
       */
      private String signal;
  
      /*
       * Clock source for sampling timestamps: monotonic or tsc
       */
      private String clock;
  
      /*
       * Normalize method names by removing unique numerical suffixes from lambda classes.
       */
      private boolean norm;
  
      private static String libPath;
      private static AsyncProfiler profiler = null;
  
      static {
          String profilerSoPath = null;
          if (OSUtils.isMac()) {
              // FAT_BINARY support both x86_64/arm64
              profilerSoPath = "async-profiler/libasyncProfiler-mac.dylib";
          }
          if (OSUtils.isLinux()) {
              if (OSUtils.isX86_64()) {
                  profilerSoPath = "async-profiler/libasyncProfiler-linux-x64.so";
              }  else if (OSUtils.isArm64()) {
                  profilerSoPath = "async-profiler/libasyncProfiler-linux-arm64.so";
              }
          }
  
          if (profilerSoPath != 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(), profilerSoPath);
                      if (soFile.exists()) {
                          libPath = soFile.getAbsolutePath();
                      }
                  } catch (Throwable e) {
                      logger.error("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 = "j", longName = "jstackdepth")
      @Description("maximum Java stack depth (default: 2048)")
      public void setJstackdepth(int jstackdepth) {
          this.jstackdepth = jstackdepth;
      }
  
      @Option(shortName = "f", longName = "file")
      @Description("dump output to <filename>, if ends with html or jfr, content format can be infered")
      public void setFile(String file) {
          this.file = file;
      }
  
      @Option(shortName = "o", longName = "format")
      @Description("dump output content format(flat[=N]|traces[=N]|collapsed|flamegraph|tree|jfr)")
      public void setFormat(String format) {
          // only for backward compatibility
          if ("html".equals(format)) {
              format = "flamegraph";
          }
          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 = "alloc")
      @Description("allocation profiling interval in bytes")
      public void setAlloc(String alloc) {
          this.alloc = alloc;
      }
  
      @Option(longName = "live", flag = true)
      @Description("build allocation profile from live objects only")
      public void setLive(boolean live) {
          this.live = live;
      }
  
      @Option(longName = "lock")
      @Description("lock profiling threshold in nanoseconds")
      public void setLock(String lock) {
          this.lock = lock;
      }
  
      @Option(longName = "jfrsync")
      @Description("Start Java Flight Recording with the given config along with the profiler. "
              + "Accepts a predefined profile name, a path to a .jfc file, or a list of JFR events starting with '+'. ")
      public void setJfrsync(String jfrsync) {
          this.jfrsync = jfrsync;
      }
  
      @Option(longName = "wall")
      @Description("wall clock profiling interval in milliseconds(recommended: 200)")
      public void setWall(Long wall) {
          this.wall = wall;
      }
  
      @Option(shortName = "t", longName = "threads", flag = true)
      @Description("profile different threads separately")
      public void setThreads(boolean threads) {
          this.threads = threads;
      }
  
      @Option(shortName = "F", longName = "features")
      @Description("Features enabled for profiling")
      public void setFeatures(String features) {
          this.features = features;
      }
  
      @Option(longName = "signal")
      @Description("Set the profiling signal to use")
      public void setSignal(String signal) {
          this.signal = signal;
      }
  
      @Option(longName = "clock")
      @Description("Clock source for sampling timestamps: monotonic or tsc")
      public void setClock(String clock) {
          this.clock = clock;
      }
  
      @Option(longName = "norm", flag = true)
      @Description("Normalize method names by removing unique numerical suffixes from lambda classes.")
      public void setNorm(boolean norm) {
          this.norm = norm;
      }
  
      @Option(longName = "sched", flag = true)
      @Description("group threads by scheduling policy")
      public void setSched(boolean sched) {
          this.sched = sched;
      }
  
      @Option(longName = "cstack")
      @Description("how to traverse C stack: fp|dwarf|lbr|no")
      public void setCstack(String cstack) {
          this.cstack = cstack;
      }
  
      @Option(shortName = "s", flag = true)
      @Description("use simple class names instead of FQN")
      public void setSimple(boolean simple) {
          this.simple = simple;
      }
  
      @Option(shortName = "g", flag = true)
      @Description("print method signatures")
      public void setSig(boolean sig) {
          this.sig = sig;
      }
  
      @Option(shortName = "a", flag = true)
      @Description("annotate Java methods")
      public void setAnn(boolean ann) {
          this.ann = ann;
      }
  
      @Option(shortName = "l", flag = true)
      @Description("prepend library names")
      public void setLib(boolean lib) {
          this.lib = lib;
      }
  
      @Option(longName = "all-user", flag = true)
      @Description("include only user-mode events")
      public void setAlluser(boolean alluser) {
          this.alluser = alluser;
      }
  
      @Option(shortName = "d", longName = "duration")
      @Description("run profiling for <duration> seconds")
      public void setDuration(long duration) {
          this.duration = duration;
      }
  
      @Option(shortName = "I", longName = "include")
      @Description("include stack traces containing PATTERN, for example: 'java/*'")
      public void setInclude(List<String> includes) {
          this.includes = includes;
      }
  
      @Option(shortName = "X", longName = "exclude")
      @Description("exclude stack traces containing PATTERN, for example: '*Unsafe.park*'")
      public void setExclude(List<String> excludes) {
          this.excludes = excludes;
      }
  
      @Option(longName = "begin")
      @Description("automatically start profiling when the specified native function is executed")
      public void setBegin(String begin) {
          this.begin = begin;
      }
  
      @Option(longName = "end")
      @Description("automatically stop profiling when the specified native function is executed")
      public void setEnd(String end) {
          this.end = end;
      }
  
      @Option(longName = "ttsp", flag = true)
      @Description("time-to-safepoint profiling. "
          + "An alias for --begin SafepointSynchronize::begin --end RuntimeService::record_safepoint_synchronized")
      public void setTtsp(boolean ttsp) {
          this.ttsp = ttsp;
      }
  
      @Option(longName = "title")
      @Description("FlameGraph title")
      public void setTitle(String title) {
          // escape HTML special characters
          // and escape comma to avoid conflicts with JVM TI
          title = title.replace("&", "&amp;")
                  .replace("<", "&lt;")
                  .replace(">", "&gt;")
                  .replace("\"", "&quot;")
                  .replace("'", "&apos;")
                  .replace(",", "&#44;");
          this.title = title;
      }
  
      @Option(longName = "minwidth")
      @Description("FlameGraph minimum frame width in percent")
      public void setMinwidth(String minwidth) {
          this.minwidth = minwidth;
      }
  
      @Option(longName = "reverse", flag = true)
      @Description("generate stack-reversed FlameGraph / Call tree")
      public void setReverse(boolean reverse) {
          this.reverse = reverse;
      }
  
      @Option(longName = "total", flag = true)
      @Description("count the total value (time, bytes, etc.) instead of samples")
      public void setTotal(boolean total) {
          this.total = total;
      }
  
      @Option(longName = "chunksize")
      @Description("approximate size limits for a single JFR chunk in bytes (default: 100 MB) or other units")
      public void setChunksize(String chunksize) {
          this.chunksize = chunksize;
      }
  
      @Option(longName = "chunktime")
      @Description("approximate time limits for a single JFR chunk in second (default: 1 hour) or other units")
      public void setChunktime(String chunktime) {
          this.chunktime = chunktime;
      }
  
      @Option(longName = "loop")
      @Description("run profiler in a loop (continuous profiling)")
      public void setLoop(String loop) {
          this.loop = loop;
      }
  
      @Option(longName = "timeout")
      @Description("automatically stop profiler at TIME (absolute or relative)")
      public void setTimeout(String timeout) {
          this.timeout = timeout;
      }
  
  
      private AsyncProfiler profilerInstance() {
          if (profiler != null) {
              return profiler;
          }
  
          // try to load from special path
          if (ProfilerAction.load.toString().equals(action)) {
              profiler = AsyncProfiler.getInstance(this.actionArg);
          }
  
          if (libPath != null) {
              // load from arthas directory
              // 尝试把lib文件复制到临时文件里，避免多次attach时出现 Native Library already loaded in another classloader
              FileOutputStream tmpLibOutputStream = null;
              FileInputStream libInputStream = null;
              try {
                  File tmpLibFile = File.createTempFile(VmTool.JNI_LIBRARY_NAME, null);
                  tmpLibOutputStream = new FileOutputStream(tmpLibFile);
                  libInputStream = new FileInputStream(libPath);
  
                  IOUtils.copy(libInputStream, tmpLibOutputStream);
                  libPath = tmpLibFile.getAbsolutePath();
                  logger.debug("copy {} to {}", libPath, tmpLibFile);
              } catch (Throwable e) {
                  logger.error("try to copy lib error! libPath: {}", libPath, e);
              } finally {
                  IOUtils.close(libInputStream);
                  IOUtils.close(tmpLibOutputStream);
              }
              profiler = AsyncProfiler.getInstance(libPath);
          } else {
              if (OSUtils.isLinux() || OSUtils.isMac()) {
                  throw new IllegalStateException("Can not find libasyncProfiler so, please check the arthas directory.");
              } else {
                  throw new IllegalStateException("Current OS do not support AsyncProfiler, Only support Linux/Mac.");
              }
          }
  
          return profiler;
      }
  
      /**
       * https://github.com/async-profiler/async-profiler/blob/v3.0/src/arguments.cpp#L131
       */
      public enum ProfilerAction {
          // start, resume, stop, dump, check, status, meminfo, list,
          start, resume, stop, dump, check, status, meminfo, list,
          version,
  
          load,
          execute,
          dumpCollapsed, dumpFlat, dumpTraces, getSamples,
          actions
      }
  
      private String executeArgs(ProfilerAction action) {
          StringBuilder sb = new StringBuilder();
          final char COMMA = ',';
  
          // start - start profiling
          // resume - start or resume profiling without resetting collected data
          // stop - stop profiling
          sb.append(action).append(COMMA);
  
          if (this.event != null) {
              sb.append("event=").append(this.event).append(COMMA);
          }
          if (this.alloc!= null) {
              sb.append("alloc=").append(this.alloc).append(COMMA);
          }
          if (this.live) {
              sb.append("live").append(COMMA);
          }
          if (this.lock!= null) {
              sb.append("lock=").append(this.lock).append(COMMA);
          }
          if (this.jfrsync != null) {
              this.format = "jfr";
              sb.append("jfrsync=").append(this.jfrsync).append(COMMA);
          }
          if (this.file != null) {
              sb.append("file=").append(this.file).append(COMMA);
          }
          if (this.format != null) {
              sb.append(this.format).append(COMMA);
          }
          if (this.interval != null) {
              sb.append("interval=").append(this.interval).append(COMMA);
          }
          if (this.features != null) {
              sb.append("features=").append(this.features).append(COMMA);
          }
          if (this.signal != null) {
              sb.append("signal=").append(this.signal).append(COMMA);
          }
          if (this.clock != null) {
              sb.append("clock=").append(this.clock).append(COMMA);
          }
          if (this.jstackdepth != null) {
              sb.append("jstackdepth=").append(this.jstackdepth).append(COMMA);
          }
          if (this.threads) {
              sb.append("threads").append(COMMA);
          }
          if (this.sched) {
              sb.append("sched").append(COMMA);
          }
          if (this.cstack != null) {
              sb.append("cstack=").append(this.cstack).append(COMMA);
          }
          if (this.simple) {
              sb.append("simple").append(COMMA);
          }
          if (this.sig) {
              sb.append("sig").append(COMMA);
          }
          if (this.ann) {
              sb.append("ann").append(COMMA);
          }
          if (this.lib) {
              sb.append("lib").append(COMMA);
          }
          if (this.alluser) {
              sb.append("alluser").append(COMMA);
          }
          if (this.norm) {
              sb.append("norm").append(COMMA);
          }
          if (this.includes != null) {
              for (String include : includes) {
                  sb.append("include=").append(include).append(COMMA);
              }
          }
          if (this.excludes != null) {
              for (String exclude : excludes) {
                  sb.append("exclude=").append(exclude).append(COMMA);
              }
          }
          if (this.ttsp) {
              this.begin = "SafepointSynchronize::begin";
              this.end = "RuntimeService::record_safepoint_synchronized";
          }
          if (this.begin != null) {
              sb.append("begin=").append(this.begin).append(COMMA);
          }
          if (this.end != null) {
              sb.append("end=").append(this.end).append(COMMA);
          }
          if (this.wall != null) {
              sb.append("wall=").append(this.wall).append(COMMA);
          }
          if (this.title != null) {
              sb.append("title=").append(this.title).append(COMMA);
          }
          if (this.minwidth != null) {
              sb.append("minwidth=").append(this.minwidth).append(COMMA);
          }
          if (this.reverse) {
              sb.append("reverse").append(COMMA);
          }
          if (this.total) {
              sb.append("total").append(COMMA);
          }
          if (this.chunksize != null) {
              sb.append("chunksize=").append(this.chunksize).append(COMMA);
          }
          if (this.chunktime!= null) {
              sb.append("chunktime=").append(this.chunktime).append(COMMA);
          }
          if (this.loop != null) {
              sb.append("loop=").append(this.loop).append(COMMA);
          }
          if (this.timeout != null) {
              sb.append("timeout=").append(this.timeout).append(COMMA);
          }
  
          return sb.toString();
      }
  
      private static String execute(AsyncProfiler asyncProfiler, String arg)
              throws IllegalArgumentException, IOException {
          logger.info("profiler execute args: {}", arg);
          String result = asyncProfiler.execute(arg);
          if (!result.endsWith("\n")) {
              result += "\n";
          }
          return result;
      }
  
      @Override
      public void process(final CommandProcess process) {
          try {
              ProfilerAction profilerAction = ProfilerAction.valueOf(action);
  
              if (ProfilerAction.actions.equals(profilerAction)) {
                  process.appendResult(new ProfilerModel(actions()));
                  process.end();
                  return;
              }
  
              final AsyncProfiler asyncProfiler = this.profilerInstance();
  
              if (ProfilerAction.execute.equals(profilerAction)) {
                  if (actionArg == null) {
                      process.end(1, "actionArg can not be empty.");
                      return;
                  }
                  String result = execute(asyncProfiler, this.actionArg);
                  appendExecuteResult(process, result);
              } else if (ProfilerAction.start.equals(profilerAction)) {
                  // Track if file parameter was specified during start
                  boolean autoGeneratedFile = false;
                  if (this.file != null) {
                      fileSpecifiedAtStart = this.file;
                      logger.debug("File specified during profiler start: {}", fileSpecifiedAtStart);
                  } else if (this.timeout != null) {
                      // Auto-generate file if timeout is specified but file is not
                      try {
                          this.file = outputFile();
                          logger.debug("Auto-generated file for timeout: {}", this.file);
                          fileSpecifiedAtStart = this.file;
                          autoGeneratedFile = true;
                      } catch (IOException e) {
                          logger.warn("Failed to auto-generate file for timeout", e);
                      }
                  }
  
                  if (this.duration == null) {
                      String executeArgs = executeArgs(ProfilerAction.start);
                      String result = execute(asyncProfiler, executeArgs);
                      ProfilerModel profilerModel = createProfilerModel(result);
  
                      // Add information about auto-generated file for timeout
                      if (autoGeneratedFile && this.file != null) {
                          profilerModel.setOutputFile(this.file);
                          profilerModel.setExecuteResult(profilerModel.getExecuteResult()
                                  + "\nAuto-generated output file will be: " + this.file + "\n");
                      }
  
                      process.appendResult(profilerModel);
                  } else { // 设置延时执行 stop
                      final String outputFile = outputFile();
                      String executeArgs = executeArgs(ProfilerAction.start);
                      String result = execute(asyncProfiler, executeArgs);
                      ProfilerModel profilerModel = createProfilerModel(result);
                      profilerModel.setOutputFile(outputFile);
                      profilerModel.setDuration(duration);
  
                      // 延时执行stop
                      ArthasBootstrap.getInstance().getScheduledExecutorService().schedule(new Runnable() {
                          @Override
                          public void run() {
                              // 在异步线程执行，profiler命令已经结束，不能输出到客户端
                              try {
                                  logger.info("stopping profiler ...");
                                  ProfilerModel model = processStop(asyncProfiler, ProfilerAction.stop);
                                  logger.info("profiler output file: " + model.getOutputFile());
                                  logger.info("stop profiler successfully.");
                              } catch (Throwable e) {
                                  logger.error("stop profiler failure", e);
                              }
                          }
                      }, this.duration, TimeUnit.SECONDS);
                      process.appendResult(profilerModel);
                  }
  
              } else if (ProfilerAction.stop.equals(profilerAction)) {
                  ProfilerModel profilerModel = processStop(asyncProfiler, profilerAction);
                  process.appendResult(profilerModel);
              } else if (ProfilerAction.dump.equals(profilerAction)) {
                  ProfilerModel profilerModel = processStop(asyncProfiler, profilerAction);
                  process.appendResult(profilerModel);
              } else if (ProfilerAction.resume.equals(profilerAction)) {
                  String executeArgs = executeArgs(ProfilerAction.resume);
                  String result = execute(asyncProfiler, executeArgs);
                  appendExecuteResult(process, result);
              } else if (ProfilerAction.check.equals(profilerAction)) {
                  String executeArgs = executeArgs(ProfilerAction.check);
                  String result = execute(asyncProfiler, executeArgs);
                  appendExecuteResult(process, result);
              } else if (ProfilerAction.version.equals(profilerAction)) {
                  String result = asyncProfiler.execute("version=full");
                  appendExecuteResult(process, result);
              } else if (ProfilerAction.status.equals(profilerAction)
                      || ProfilerAction.meminfo.equals(profilerAction)
                      || ProfilerAction.list.equals(profilerAction)) {
                  String result = asyncProfiler.execute(profilerAction.toString());
                  appendExecuteResult(process, result);
              } else if (ProfilerAction.dumpCollapsed.equals(profilerAction)) {
                  if (actionArg == null) {
                      actionArg = "TOTAL";
                  }
                  actionArg = actionArg.toUpperCase();
                  if ("TOTAL".equals(actionArg) || "SAMPLES".equals(actionArg)) {
                      String result = asyncProfiler.dumpCollapsed(Counter.valueOf(actionArg));
                      appendExecuteResult(process, result);
                  } else {
                      process.end(1, "ERROR: dumpCollapsed argumment should be TOTAL or SAMPLES. ");
                      return;
                  }
              } else if (ProfilerAction.dumpFlat.equals(profilerAction)) {
                  int maxMethods = 0;
                  if (actionArg != null) {
                      maxMethods = Integer.valueOf(actionArg);
                  }
                  String result = asyncProfiler.dumpFlat(maxMethods);
                  appendExecuteResult(process, result);
              } else if (ProfilerAction.dumpTraces.equals(profilerAction)) {
                  int maxTraces = 0;
                  if (actionArg != null) {
                      maxTraces = Integer.valueOf(actionArg);
                  }
                  String result = asyncProfiler.dumpTraces(maxTraces);
                  appendExecuteResult(process, result);
              } else if (ProfilerAction.getSamples.equals(profilerAction)) {
                  String result = "" + asyncProfiler.getSamples() + "\n";
                  appendExecuteResult(process, result);
              }
              process.end();
          } catch (Throwable e) {
              logger.error("AsyncProfiler error", e);
              process.end(1, "AsyncProfiler error: "+e.getMessage());
          }
      }
  
      private ProfilerModel processStop(AsyncProfiler asyncProfiler, ProfilerAction profilerAction) throws IOException {
          String outputFile = null;
  
          // If we're stopping and a file was specified during start, don't generate a new
          // output file
          if (profilerAction == ProfilerAction.stop && fileSpecifiedAtStart != null) {
              outputFile = fileSpecifiedAtStart;
              // Reset the tracking variable after stop
              logger.debug("Using file specified during start: {}", fileSpecifiedAtStart);
              fileSpecifiedAtStart = null;
          } else {
              // Otherwise generate or use the specified output file
              outputFile = outputFile();
          }
  
          String executeArgs = executeArgs(profilerAction);
          String result = execute(asyncProfiler, executeArgs);
  
          ProfilerModel profilerModel = createProfilerModel(result);
          if (outputFile != null) {
              profilerModel.setOutputFile(outputFile);
          }
          return profilerModel;
      }
  
      private String outputFile() throws IOException {
          if (this.file == null) {
              String fileExt = outputFileExt();
              File outputPath = ArthasBootstrap.getInstance().getOutputPath();
              if (outputPath != null) {
                  this.file = new File(outputPath,
                          new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date()) + "." + fileExt)
                                  .getAbsolutePath();
              } else {
                  this.file = File.createTempFile("arthas-output", "." + fileExt).getAbsolutePath();
              }
          }
          return file;
      }
  
      /**
       * This method should only be called when {@code this.file == null} is true.
       */
      private String outputFileExt() {
          String fileExt = "";
          if (this.format == null) {
              fileExt = "html";
          } else if (this.format.startsWith("flat") || this.format.startsWith("traces") 
                  || this.format.equals("collapsed")) {
              fileExt = "txt";
          } else if (this.format.equals("flamegraph") || this.format.equals("tree")) {
              fileExt = "html";
          } else if (this.format.equals("jfr")) {
              fileExt = "jfr";
          } else {
              // illegal -o option makes async-profiler use flat
              fileExt = "txt";
          }
          return fileExt;
      }
  
      private void appendExecuteResult(CommandProcess process, String result) {
          ProfilerModel profilerModel = createProfilerModel(result);
          process.appendResult(profilerModel);
      }
  
      private ProfilerModel createProfilerModel(String result) {
          ProfilerModel profilerModel = new ProfilerModel();
          profilerModel.setAction(action);
          profilerModel.setActionArg(actionArg);
          profilerModel.setExecuteResult(result);
          return profilerModel;
      }
  
      private List<String> events() {
          List<String> result = new ArrayList<String>();
  
          String execute;
          try {
              /**
               * <pre>
                 Basic events:
                    cpu
                    alloc
                    lock
                    wall
                    itimer
               * </pre>
               */
              execute = this.profilerInstance().execute("list");
          } catch (Throwable e) {
              // ignore
              return result;
          }
          String lines[] = execute.split("\\r?\\n");
  
          for (String line : lines) {
              if (line.startsWith(" ")) {
                  result.add(line.trim());
              }
          }
          return result;
      }
  
      private Set<String> actions() {
          Set<String> values = new HashSet<String>();
          for (ProfilerAction action : ProfilerAction.values()) {
              values.add(action.toString());
          }
          return values;
      }
  
      @Override
      public void complete(Completion completion) {
          List<CliToken> 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("html", "jfr"));
                      return;
                  }
              }
          }
  
          if (token.startsWith("-")) {
              super.complete(completion);
              return;
          }
  
          CompletionUtils.complete(completion, actions());
      }
  
  }