001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.http;
019
020import java.io.File;
021import java.nio.file.Files;
022import java.nio.file.Path;
023import java.nio.file.Paths;
024import java.util.ArrayList;
025import java.util.List;
026import org.apache.yetus.audience.InterfaceAudience;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030/**
031 * Utility class that maps {@link ProfileServlet.ProfileRequest} to async-profiler commands in both
032 * the in-process Java API format (comma-separated string) and the CLI format (argument list).
033 */
034@InterfaceAudience.Private
035final class ProfilerCommandMapper {
036
037  private static final Logger LOG = LoggerFactory.getLogger(ProfilerCommandMapper.class);
038
039  private static final String PROFILER_SCRIPT = "asprof";
040  private static final String OLD_PROFILER_SCRIPT = "profiler.sh";
041
042  private ProfilerCommandMapper() {
043  }
044
045  /**
046   * Builds the start command string for the async-profiler Java API. Format:
047   * {@code start,event=<event>[,interval=N][,jstackdepth=N][,threads][,simple]}
048   * <p>
049   * Note: {@code bufsize} is intentionally omitted — it is not a recognized option in the
050   * async-profiler 4.x agent grammar and is silently ignored. It remains supported by the
051   * BinaryBackend CLI path via {@code -b}.
052   */
053  static String toLibraryStartCommand(ProfileServlet.ProfileRequest request) {
054    StringBuilder sb = new StringBuilder("start");
055    sb.append(",event=").append(request.getEvent().getInternalName());
056    appendOption(sb, "interval", request.getInterval());
057    appendOption(sb, "jstackdepth", request.getJstackDepth());
058    if (request.isThread()) {
059      sb.append(",threads");
060    }
061    if (request.isSimple()) {
062      sb.append(",simple");
063    }
064    return sb.toString();
065  }
066
067  /**
068   * Builds the stop command string for the async-profiler Java API. Format:
069   * {@code stop,file=<path>[,<format-token>][,minwidth=N][,reverse]}
070   * <p>
071   * In async-profiler 4.x the output format is derived from the file extension for html/jfr, and
072   * via a bare token (e.g. {@code tree}, {@code flat}) for text-based formats. The {@code format=}
073   * key is not recognized. {@code width} and {@code height} are also not recognized by the 4.x
074   * agent grammar; they remain supported via the BinaryBackend CLI.
075   */
076  static String toLibraryStopCommand(ProfileServlet.ProfileRequest request, File outputFile) {
077    StringBuilder sb = new StringBuilder("stop");
078    sb.append(",file=").append(outputFile.getAbsolutePath());
079    String fmt = toFormatString(request.getOutput());
080    // html/jfr: format derived from file extension by async-profiler 4.x — no token needed.
081    // collapsed/tree/flat/traces/summary: must be passed as a bare token; the .collapsed
082    // extension is NOT auto-detected by detectOutputFormat in 4.x.
083    if (!fmt.equals("html") && !fmt.equals("jfr")) {
084      sb.append(",").append(fmt);
085    }
086    appendOption(sb, "minwidth", request.getMinwidth());
087    if (request.isReverse()) {
088      sb.append(",reverse");
089    }
090    return sb.toString();
091  }
092
093  /**
094   * Builds the CLI argument list for invoking the async-profiler binary (asprof / profiler.sh).
095   * Locates the script under {@code <profilerHome>/bin/asprof}, falling back to
096   * {@code <profilerHome>/profiler.sh} for older installations.
097   */
098  static List<String> toCliCommand(ProfileServlet.ProfileRequest request, File outputFile,
099    String profilerHome, Integer pid) {
100    List<String> cmd = new ArrayList<>();
101    Path profilerScriptPath = Paths.get(profilerHome, "bin", PROFILER_SCRIPT);
102    if (!Files.exists(profilerScriptPath)) {
103      LOG.info("async-profiler script {} does not exist, falling back to {}(version <= 2.9).",
104        PROFILER_SCRIPT, OLD_PROFILER_SCRIPT);
105      profilerScriptPath = Paths.get(profilerHome, OLD_PROFILER_SCRIPT);
106    }
107    cmd.add(profilerScriptPath.toString());
108    cmd.add("-e");
109    cmd.add(request.getEvent().getInternalName());
110    cmd.add("-d");
111    cmd.add(String.valueOf(request.getDuration()));
112    cmd.add("-o");
113    cmd.add(toFormatString(request.getOutput()));
114    cmd.add("-f");
115    cmd.add(outputFile.getAbsolutePath());
116    if (request.getInterval() != null) {
117      cmd.add("-i");
118      cmd.add(request.getInterval().toString());
119    }
120    if (request.getJstackDepth() != null) {
121      cmd.add("-j");
122      cmd.add(request.getJstackDepth().toString());
123    }
124    if (request.getBufsize() != null) {
125      cmd.add("-b");
126      cmd.add(request.getBufsize().toString());
127    }
128    if (request.isThread()) {
129      cmd.add("-t");
130    }
131    if (request.isSimple()) {
132      cmd.add("-s");
133    }
134    if (request.getWidth() != null) {
135      cmd.add("--width");
136      cmd.add(request.getWidth().toString());
137    }
138    if (request.getHeight() != null) {
139      cmd.add("--height");
140      cmd.add(request.getHeight().toString());
141    }
142    if (request.getMinwidth() != null) {
143      cmd.add("--minwidth");
144      cmd.add(request.getMinwidth().toString());
145    }
146    if (request.isReverse()) {
147      cmd.add("--reverse");
148    }
149    cmd.add(pid.toString());
150    return cmd;
151  }
152
153  /**
154   * Maps the {@link ProfileServlet.Output} enum to the format string used by both backends. Logs a
155   * deprecation warning when SVG is requested (it was removed in async-profiler 2.0, see
156   * HBASE-25685). Use {@link #toFileExtension} when only the file extension is needed and the
157   * warning has already been emitted.
158   */
159  static String toFormatString(ProfileServlet.Output output) {
160    if (output == ProfileServlet.Output.SVG) {
161      LOG.warn("output=svg is obsolete (HBASE-25685); redirecting to html (FlameGraph). "
162        + "Use output=html explicitly.");
163    }
164    return toFileExtension(output);
165  }
166
167  /**
168   * Maps the {@link ProfileServlet.Output} enum to a file extension / format token without emitting
169   * any log warnings. SVG is silently remapped to {@code "html"}.
170   */
171  static String toFileExtension(ProfileServlet.Output output) {
172    switch (output) {
173      case SUMMARY:
174        return "summary";
175      case TRACES:
176        return "traces";
177      case FLAT:
178        return "flat";
179      case COLLAPSED:
180        return "collapsed";
181      case TREE:
182        return "tree";
183      case JFR:
184        return "jfr";
185      case SVG:
186        // SVG was dropped in async-profiler 2.0 (HBASE-25685) and hard-errors in 4.x.
187        return "html";
188      case HTML:
189      default:
190        return "html";
191    }
192  }
193
194  private static void appendOption(StringBuilder sb, String key, Object value) {
195    if (value != null) {
196      sb.append(',').append(key).append('=').append(value);
197    }
198  }
199}