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 static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertTrue;
023
024import java.io.File;
025import java.io.IOException;
026import java.nio.file.Files;
027import java.nio.file.Path;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import javax.servlet.http.HttpServletRequest;
033import org.apache.hadoop.hbase.testclassification.MiscTests;
034import org.apache.hadoop.hbase.testclassification.SmallTests;
035import org.junit.jupiter.api.Tag;
036import org.junit.jupiter.api.Test;
037import org.junit.jupiter.api.io.TempDir;
038import org.mockito.Mockito;
039
040@Tag(MiscTests.TAG)
041@Tag(SmallTests.TAG)
042public class TestProfilerCommandMapper {
043
044  @TempDir
045  Path tempDir;
046
047  // ---- Library start command ----
048
049  @Test
050  public void testLibraryStartCommandDefaults() {
051    ProfileServlet.ProfileRequest req = parseRequest(Collections.emptyMap());
052    String cmd = ProfilerCommandMapper.toLibraryStartCommand(req);
053    assertTrue(cmd.startsWith("start"));
054    assertTrue(cmd.contains("event=cpu"));
055    assertFalse(cmd.contains("interval"));
056    assertFalse(cmd.contains("threads"));
057    assertFalse(cmd.contains("simple"));
058  }
059
060  @Test
061  public void testLibraryStartCommandAllOptions() {
062    Map<String, String[]> flags = new HashMap<>();
063    flags.put("thread", new String[] { "" });
064    flags.put("simple", new String[] { "" });
065
066    ProfileServlet.ProfileRequest req = parseRequest(flags, "event", "alloc", "interval", "1000",
067      "jstackdepth", "256", "bufsize", "100000");
068    String cmd = ProfilerCommandMapper.toLibraryStartCommand(req);
069    assertTrue(cmd.contains("event=alloc"));
070    assertTrue(cmd.contains("interval=1000"));
071    assertTrue(cmd.contains("jstackdepth=256"));
072    // bufsize= is not a recognized 4.x agent option and must not be emitted for LibraryBackend
073    assertFalse(cmd.contains("bufsize"));
074    assertTrue(cmd.contains("threads"));
075    assertTrue(cmd.contains("simple"));
076  }
077
078  // ---- Library stop command ----
079
080  @Test
081  public void testLibraryStopCommandHtml() throws IOException {
082    Map<String, String[]> flags = new HashMap<>();
083    flags.put("reverse", new String[] { "" });
084    ProfileServlet.ProfileRequest req =
085      parseRequest(flags, "output", "html", "width", "1200", "height", "16", "minwidth", "0.5");
086
087    File outputFile = File.createTempFile("prof", ".html");
088    outputFile.deleteOnExit();
089
090    String cmd = ProfilerCommandMapper.toLibraryStopCommand(req, outputFile);
091    assertTrue(cmd.startsWith("stop"));
092    assertTrue(cmd.contains("file=" + outputFile.getAbsolutePath()));
093    // html: format derived from .html extension — no format= key emitted
094    assertFalse(cmd.contains("format="));
095    // width/height not recognized by 4.x agent — must not be emitted
096    // (use ",width=" prefix to avoid matching ",minwidth=")
097    assertFalse(cmd.contains(",width="));
098    assertFalse(cmd.contains(",height="));
099    assertTrue(cmd.contains("minwidth=0.5"));
100    assertTrue(cmd.contains("reverse"));
101  }
102
103  @Test
104  public void testLibraryStopCommandTree() throws IOException {
105    ProfileServlet.ProfileRequest req = parseRequest(Collections.emptyMap(), "output", "tree");
106    File outputFile = File.createTempFile("prof", ".tree");
107    outputFile.deleteOnExit();
108
109    String cmd = ProfilerCommandMapper.toLibraryStopCommand(req, outputFile);
110    // text-based formats need the bare token
111    assertTrue(cmd.contains(",tree"));
112    assertFalse(cmd.contains("format="));
113  }
114
115  @Test
116  public void testLibraryStopCommandCollapsed() throws IOException {
117    ProfileServlet.ProfileRequest req = parseRequest(Collections.emptyMap(), "output", "collapsed");
118    File outputFile = File.createTempFile("prof", ".collapsed");
119    outputFile.deleteOnExit();
120
121    String cmd = ProfilerCommandMapper.toLibraryStopCommand(req, outputFile);
122    // collapsed must be passed as a bare token — .collapsed extension is not auto-detected by 4.x
123    assertTrue(cmd.contains(",collapsed"));
124    assertFalse(cmd.contains("format="));
125  }
126
127  @Test
128  public void testLibraryStopCommandSvgRemappedToHtml() throws IOException {
129    ProfileServlet.ProfileRequest req = parseRequest(Collections.emptyMap(), "output", "svg");
130    File outputFile = File.createTempFile("prof", ".html");
131    outputFile.deleteOnExit();
132
133    String cmd = ProfilerCommandMapper.toLibraryStopCommand(req, outputFile);
134    // SVG is obsolete — must be remapped to html
135    assertFalse(cmd.contains("svg"));
136    assertFalse(cmd.contains("format="));
137  }
138
139  // ---- CLI command ----
140
141  @Test
142  public void testCliCommandDefaultScript() throws IOException {
143    // Create bin/asprof so the primary script path exists
144    Path binDir = Files.createDirectories(tempDir.resolve("bin"));
145    Files.createFile(binDir.resolve("asprof"));
146
147    ProfileServlet.ProfileRequest req =
148      parseRequest(Collections.emptyMap(), "duration", "30", "output", "html");
149    File outputFile = File.createTempFile("prof", ".html");
150    outputFile.deleteOnExit();
151
152    List<String> cmd =
153      ProfilerCommandMapper.toCliCommand(req, outputFile, tempDir.toString(), 1234);
154    assertEquals(tempDir.resolve("bin/asprof").toString(), cmd.get(0));
155    assertTrue(cmd.contains("-e"));
156    assertTrue(cmd.contains("cpu"));
157    assertTrue(cmd.contains("-d"));
158    assertTrue(cmd.contains("30"));
159    assertTrue(cmd.contains("-o"));
160    assertTrue(cmd.contains("html"));
161    assertTrue(cmd.contains("-f"));
162    assertTrue(cmd.contains(outputFile.getAbsolutePath()));
163    assertTrue(cmd.contains("1234"));
164  }
165
166  @Test
167  public void testCliCommandSvgRemappedToHtml() throws IOException {
168    Path binDir = Files.createDirectories(tempDir.resolve("bin"));
169    Files.createFile(binDir.resolve("asprof"));
170
171    ProfileServlet.ProfileRequest req = parseRequest(Collections.emptyMap(), "output", "svg");
172    File outputFile = File.createTempFile("prof", ".html");
173    outputFile.deleteOnExit();
174
175    List<String> cmd =
176      ProfilerCommandMapper.toCliCommand(req, outputFile, tempDir.toString(), 1234);
177    // SVG is obsolete; toCliCommand must remap to html, not pass "-o svg"
178    assertFalse(cmd.contains("svg"));
179    assertTrue(cmd.contains("html"));
180  }
181
182  @Test
183  public void testCliCommandFallbackToOldScript() throws IOException {
184    // Do NOT create bin/asprof — only create profiler.sh as fallback
185    Files.createFile(tempDir.resolve("profiler.sh"));
186
187    ProfileServlet.ProfileRequest req = parseRequest(Collections.emptyMap());
188    File outputFile = File.createTempFile("prof", ".html");
189    outputFile.deleteOnExit();
190
191    List<String> cmd =
192      ProfilerCommandMapper.toCliCommand(req, outputFile, tempDir.toString(), 1234);
193    assertEquals(tempDir.resolve("profiler.sh").toString(), cmd.get(0));
194  }
195
196  @Test
197  public void testCliCommandAllOptions() throws IOException {
198    Path binDir = Files.createDirectories(tempDir.resolve("bin"));
199    Files.createFile(binDir.resolve("asprof"));
200
201    Map<String, String[]> flags = new HashMap<>();
202    flags.put("thread", new String[] { "" });
203    flags.put("simple", new String[] { "" });
204    flags.put("reverse", new String[] { "" });
205
206    ProfileServlet.ProfileRequest req = parseRequest(flags, "event", "alloc", "interval", "500",
207      "jstackdepth", "128", "bufsize", "50000", "width", "800", "height", "12", "minwidth", "1.0");
208    File outputFile = File.createTempFile("prof", ".html");
209    outputFile.deleteOnExit();
210
211    List<String> cmd = ProfilerCommandMapper.toCliCommand(req, outputFile, tempDir.toString(), 99);
212    assertTrue(cmd.contains("-e"));
213    assertTrue(cmd.contains("alloc"));
214    assertTrue(cmd.contains("-i"));
215    assertTrue(cmd.contains("500"));
216    assertTrue(cmd.contains("-j"));
217    assertTrue(cmd.contains("128"));
218    assertTrue(cmd.contains("-b"));
219    assertTrue(cmd.contains("50000"));
220    assertTrue(cmd.contains("-t"));
221    assertTrue(cmd.contains("-s"));
222    assertTrue(cmd.contains("--width"));
223    assertTrue(cmd.contains("800"));
224    assertTrue(cmd.contains("--height"));
225    assertTrue(cmd.contains("12"));
226    assertTrue(cmd.contains("--minwidth"));
227    assertTrue(cmd.contains("1.0"));
228    assertTrue(cmd.contains("--reverse"));
229  }
230
231  // ---- Format mapping ----
232
233  @Test
234  public void testOutputFormatMappingAllValues() {
235    assertEquals("summary", ProfilerCommandMapper.toFormatString(ProfileServlet.Output.SUMMARY));
236    assertEquals("traces", ProfilerCommandMapper.toFormatString(ProfileServlet.Output.TRACES));
237    assertEquals("flat", ProfilerCommandMapper.toFormatString(ProfileServlet.Output.FLAT));
238    assertEquals("collapsed",
239      ProfilerCommandMapper.toFormatString(ProfileServlet.Output.COLLAPSED));
240    assertEquals("tree", ProfilerCommandMapper.toFormatString(ProfileServlet.Output.TREE));
241    assertEquals("jfr", ProfilerCommandMapper.toFormatString(ProfileServlet.Output.JFR));
242    // SVG is obsolete in async-profiler 2.x+ — remapped to html
243    assertEquals("html", ProfilerCommandMapper.toFormatString(ProfileServlet.Output.SVG));
244    assertEquals("html", ProfilerCommandMapper.toFormatString(ProfileServlet.Output.HTML));
245  }
246
247  // ---- helpers ----
248
249  private ProfileServlet.ProfileRequest parseRequest(Map<String, String[]> paramMap,
250    String... kvPairs) {
251    ProfileServlet servlet = new ProfileServlet(null);
252    HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
253    Mockito.when(req.getParameterMap()).thenReturn(paramMap);
254    // defaults
255    String[] keys = { "pid", "duration", "output", "event", "interval", "jstackdepth", "bufsize",
256      "width", "height", "minwidth", "refreshDelay" };
257    for (String k : keys) {
258      Mockito.when(req.getParameter(k)).thenReturn(null);
259    }
260    for (int i = 0; i < kvPairs.length; i += 2) {
261      Mockito.when(req.getParameter(kvPairs[i])).thenReturn(kvPairs[i + 1]);
262    }
263    return servlet.parseProfileRequest(req);
264  }
265}