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.io.IOException; 022import java.nio.charset.StandardCharsets; 023import java.nio.file.Files; 024import java.nio.file.Path; 025import java.nio.file.Paths; 026import java.nio.file.StandardOpenOption; 027import java.nio.file.attribute.PosixFilePermissions; 028import java.time.Instant; 029import java.util.concurrent.TimeUnit; 030import java.util.concurrent.atomic.AtomicInteger; 031import java.util.concurrent.locks.Lock; 032import java.util.concurrent.locks.ReentrantLock; 033import javax.servlet.http.HttpServlet; 034import javax.servlet.http.HttpServletRequest; 035import javax.servlet.http.HttpServletResponse; 036import org.apache.yetus.audience.InterfaceAudience; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040/** 041 * Servlet that runs async-profiler as a web endpoint. 042 * <p> 043 * Query parameters: 044 * <ul> 045 * <li>{@code event} - profiling event: cpu|alloc|lock|cache-misses etc. (default: cpu)</li> 046 * <li>{@code duration} - run profiling for N seconds, clamped to [1, 047 * {@value #MAX_DURATION_SECONDS}] (default: 10)</li> 048 * <li>{@code interval} - sampling interval in nanoseconds (long)</li> 049 * <li>{@code jstackdepth} - maximum Java stack depth (integer)</li> 050 * <li>{@code bufsize} - frame buffer size (long); honored only by BinaryBackend</li> 051 * <li>{@code thread} - profile different threads separately (flag)</li> 052 * <li>{@code simple} - simple class names instead of FQN (flag)</li> 053 * <li>{@code output} - output format: summary|traces|flat|collapsed|tree|jfr|html (default: 054 * html)</li> 055 * <li>{@code width} - flame graph width in pixels; honored only by BinaryBackend</li> 056 * <li>{@code height} - flame graph frame height in pixels; honored only by BinaryBackend</li> 057 * <li>{@code minwidth} - skip frames smaller than this width in pixels (double)</li> 058 * <li>{@code reverse} - generate stack-reversed FlameGraph / Call tree (flag)</li> 059 * <li>{@code pid} - target process ID; LibraryBackend only supports the current JVM (returns 400 060 * for other PIDs), BinaryBackend supports external PIDs</li> 061 * <li>{@code refreshDelay} - extra seconds added to the auto-refresh delay (integer)</li> 062 * <li>{@code last} - instead of starting a new session, redirect to the most recently completed 063 * profiling result. Returns 404 if no result is cached yet. The last result is kept in memory for 064 * the lifetime of the JVM.</li> 065 * </ul> 066 * <p> 067 * Examples: 068 * 069 * <pre> 070 * # 30-second CPU profile (default) 071 * curl "http://localhost:10002/prof" 072 * 073 * # 1-minute allocation profile in tree format 074 * curl "http://localhost:10002/prof?event=alloc&output=tree&duration=60" 075 * 076 * # Redirect to the most recent profiling result 077 * curl "http://localhost:10002/prof?last" 078 * </pre> 079 * <p> 080 * Profiling is single-flight: only one session runs at a time. A second request while a session is 081 * active returns HTTP 409 Conflict with the URL of the last completed result (if any). Closing the 082 * browser tab does not cancel a running session — the stopper thread runs to completion on the 083 * server. 084 */ 085@InterfaceAudience.Private 086public class ProfileServlet extends HttpServlet { 087 088 private static final long serialVersionUID = 1L; 089 private static final Logger LOG = LoggerFactory.getLogger(ProfileServlet.class); 090 091 private static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; 092 private static final String ALLOWED_METHODS = "GET"; 093 private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; 094 private static final String CONTENT_TYPE_TEXT = "text/plain; charset=utf-8"; 095 private static final int DEFAULT_DURATION_SECONDS = 10; 096 static final int MAX_DURATION_SECONDS = 3600; 097 private static final AtomicInteger ID_GEN = new AtomicInteger(0); 098 static final String OUTPUT_DIR = System.getProperty("java.io.tmpdir") + "/prof-output-hbase"; 099 // ProfileOutputServlet considers a file complete when its size exceeds this threshold. 100 // Error messages written to the output file must be padded past this limit. 101 static final int PROF_OUTPUT_MIN_BYTES = 100; 102 103 private static final String ASYNC_PROFILER_HOME_ENV = "ASYNC_PROFILER_HOME"; 104 private static final String ASYNC_PROFILER_HOME_SYSTEM_PROPERTY = "async.profiler.home"; 105 106 /** Immutable record of a completed profiling session. */ 107 static final class ProfileResult { 108 final String relativeUrl; 109 final String event; 110 final int durationSeconds; 111 final Instant completedAt; 112 113 ProfileResult(String relativeUrl, String event, int durationSeconds, Instant completedAt) { 114 this.relativeUrl = relativeUrl; 115 this.event = event; 116 this.durationSeconds = durationSeconds; 117 this.completedAt = completedAt; 118 } 119 } 120 121 // Last completed profiling result — static so it survives servlet reloads within the same JVM. 122 private static volatile ProfileResult lastResult = null; 123 124 // Cached backend detection result — computed once at class-load time so that isAvailable() 125 // and the default constructor do not each pay the reflective detection cost. 126 private static final ProfilerBackend DETECTED_BACKEND = 127 ProfilerBackend.detect(getAsyncProfilerHome()); 128 129 enum Event { 130 CPU("cpu"), 131 WALL("wall"), 132 ALLOC("alloc"), 133 LOCK("lock"), 134 PAGE_FAULTS("page-faults"), 135 CONTEXT_SWITCHES("context-switches"), 136 CYCLES("cycles"), 137 INSTRUCTIONS("instructions"), 138 CACHE_REFERENCES("cache-references"), 139 CACHE_MISSES("cache-misses"), 140 BRANCHES("branches"), 141 BRANCH_MISSES("branch-misses"), 142 BUS_CYCLES("bus-cycles"), 143 L1_DCACHE_LOAD_MISSES("L1-dcache-load-misses"), 144 LLC_LOAD_MISSES("LLC-load-misses"), 145 DTLB_LOAD_MISSES("dTLB-load-misses"), 146 MEM_BREAKPOINT("mem:breakpoint"), 147 TRACE_TRACEPOINT("trace:tracepoint"),; 148 149 private final String internalName; 150 151 Event(final String internalName) { 152 this.internalName = internalName; 153 } 154 155 public String getInternalName() { 156 return internalName; 157 } 158 159 public static Event fromInternalName(final String name) { 160 for (Event event : values()) { 161 if (event.getInternalName().equalsIgnoreCase(name)) { 162 return event; 163 } 164 } 165 166 return null; 167 } 168 } 169 170 enum Output { 171 SUMMARY, 172 TRACES, 173 FLAT, 174 COLLAPSED, 175 // SVG dropped in async-profiler 2.0 (HBASE-25685); remapped to HTML by ProfilerCommandMapper. 176 SVG, 177 TREE, 178 JFR, 179 // In 2.x asyncprofiler, this is how you get flamegraphs. 180 HTML 181 } 182 183 // Static so a second ProfileServlet instance (servlet reload, test harness) shares the same 184 // lock and flag as the first — both gate the same JVM-global AsyncProfiler singleton. 185 private static final Lock profilerLock = new ReentrantLock(); 186 private static volatile boolean profiling; 187 private final long currentPid = ProcessHandle.current().pid(); 188 @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "SE_BAD_FIELD", 189 justification = "This class is never serialized nor restored.") 190 private final ProfilerBackend backend; 191 192 @InterfaceAudience.Private 193 public static final class ProfileRequest { 194 private final int duration; 195 private final Output output; 196 private final Event event; 197 private final Long interval; 198 private final Integer jstackDepth; 199 private final Long bufsize; 200 private final boolean thread; 201 private final boolean simple; 202 private final Integer width; 203 private final Integer height; 204 private final Double minwidth; 205 private final boolean reverse; 206 private final int refreshDelay; 207 private final Integer pid; 208 209 private ProfileRequest(int duration, Output output, Event event, Long interval, 210 Integer jstackDepth, Long bufsize, boolean thread, boolean simple, Integer width, 211 Integer height, Double minwidth, boolean reverse, int refreshDelay, Integer pid) { 212 this.duration = duration; 213 this.output = output; 214 this.event = event; 215 this.interval = interval; 216 this.jstackDepth = jstackDepth; 217 this.bufsize = bufsize; 218 this.thread = thread; 219 this.simple = simple; 220 this.width = width; 221 this.height = height; 222 this.minwidth = minwidth; 223 this.reverse = reverse; 224 this.refreshDelay = refreshDelay; 225 this.pid = pid; 226 } 227 228 public int getDuration() { 229 return duration; 230 } 231 232 public Output getOutput() { 233 return output; 234 } 235 236 public Event getEvent() { 237 return event; 238 } 239 240 public Long getInterval() { 241 return interval; 242 } 243 244 public Integer getJstackDepth() { 245 return jstackDepth; 246 } 247 248 public Long getBufsize() { 249 return bufsize; 250 } 251 252 public boolean isThread() { 253 return thread; 254 } 255 256 public boolean isSimple() { 257 return simple; 258 } 259 260 public Integer getWidth() { 261 return width; 262 } 263 264 public Integer getHeight() { 265 return height; 266 } 267 268 public Double getMinwidth() { 269 return minwidth; 270 } 271 272 public boolean isReverse() { 273 return reverse; 274 } 275 276 public int getRefreshDelay() { 277 return refreshDelay; 278 } 279 280 public Integer getPid() { 281 return pid; 282 } 283 } 284 285 public ProfileServlet() { 286 this.backend = DETECTED_BACKEND; 287 LOG.info("ProfileServlet initialized with backend: {}", 288 backend != null ? backend.getClass().getSimpleName() : "none"); 289 } 290 291 // visible for testing 292 ProfileServlet(ProfilerBackend backend) { 293 this.backend = backend; 294 } 295 296 @Override 297 public void init() throws javax.servlet.ServletException { 298 super.init(); 299 try { 300 ensureOutputDir(); 301 } catch (IOException e) { 302 // Log and continue rather than failing daemon startup — a read-only or full /tmp is not 303 // a reason to refuse to bring up Master/RegionServer. Profiling will fail at request time. 304 LOG.warn("Failed to create profiler output directory {}; profiling requests will fail. " 305 + "Check that java.io.tmpdir is writable.", OUTPUT_DIR, e); 306 } 307 } 308 309 /** 310 * Creates {@link #OUTPUT_DIR} with permissions 0700 (owner-only) when the filesystem supports 311 * POSIX permissions, so other local users cannot plant symlinks or read profiling output. No-ops 312 * if the directory already exists. Silently skips the permission step on non-POSIX filesystems 313 * (Windows, some container runtimes). 314 */ 315 static void ensureOutputDir() throws IOException { 316 Path dir = Paths.get(OUTPUT_DIR); 317 if (Files.exists(dir)) { 318 return; 319 } 320 try { 321 Files.createDirectories(dir, 322 PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); 323 } catch (UnsupportedOperationException e) { 324 // Non-POSIX filesystem (Windows, some container setups) — fall back to plain mkdir. 325 Files.createDirectories(dir); 326 } 327 } 328 329 static String getAsyncProfilerHome() { 330 String home = System.getenv(ASYNC_PROFILER_HOME_ENV); 331 if (home == null || home.trim().isEmpty()) { 332 home = System.getProperty(ASYNC_PROFILER_HOME_SYSTEM_PROPERTY); 333 } 334 return home; 335 } 336 337 /** 338 * Returns true if a profiler backend was detected at class-load time. Detection is a one-shot 339 * operation: a library added to the classpath after the JVM starts requires a restart. A backend 340 * that resolved successfully here may still fail on first use if the native binary is 341 * incompatible with the OS/kernel — that error surfaces at request time via the 342 * {@code catch(Error | RuntimeException)} block in {@link #doGet}. 343 */ 344 public static boolean isAvailable() { 345 return DETECTED_BACKEND != null; 346 } 347 348 public ProfileRequest parseProfileRequest(final HttpServletRequest req) { 349 // Note: when using in-process async-profiler Java API, we can only profile this JVM. 350 // We keep the pid parameter for API compatibility, but do not support external processes. 351 Integer requestedPid = getInteger(req, "pid", null); 352 353 final int duration = Math.min( 354 Math.max(getInteger(req, "duration", DEFAULT_DURATION_SECONDS), 1), MAX_DURATION_SECONDS); 355 final Output output = getOutput(req); 356 final Event event = getEvent(req); 357 final Long interval = getLong(req, "interval"); 358 final Integer jstackDepth = getInteger(req, "jstackdepth", null); 359 final Long bufsize = getLong(req, "bufsize"); 360 final boolean thread = req.getParameterMap().containsKey("thread"); 361 final boolean simple = req.getParameterMap().containsKey("simple"); 362 final Integer width = getInteger(req, "width", null); 363 final Integer height = getInteger(req, "height", null); 364 final Double minwidth = getMinWidth(req); 365 final boolean reverse = req.getParameterMap().containsKey("reverse"); 366 int refreshDelay = getInteger(req, "refreshDelay", 0); 367 368 return new ProfileRequest(duration, output, event, interval, jstackDepth, bufsize, thread, 369 simple, width, height, minwidth, reverse, refreshDelay, requestedPid); 370 } 371 372 protected String executeStart(ProfileRequest request, File outputFile) throws IOException { 373 return backend.executeStart(request, outputFile); 374 } 375 376 protected String executeStop(ProfileRequest request, File outputFile) throws IOException { 377 return backend.executeStop(request, outputFile); 378 } 379 380 @Override 381 protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) 382 throws IOException { 383 if (!checkInstrumentationAccess(req, resp)) { 384 return; 385 } 386 387 // ?last — redirect to the most recent completed profiling result. 388 if (req.getParameterMap().containsKey("last")) { 389 ProfileResult last = lastResult; 390 if (last == null) { 391 writeError(resp, HttpServletResponse.SC_NOT_FOUND, 392 "No profiling results available yet. Run /prof to start a session."); 393 return; 394 } 395 // The output file may have been removed by tmpwatch / fs cleanup or a JVM restart. 396 // Verify the file still exists before redirecting; otherwise clear the stale pointer. 397 String fileName = last.relativeUrl.substring(last.relativeUrl.lastIndexOf('/') + 1); 398 File outputFile = new File(OUTPUT_DIR, fileName); 399 if (!outputFile.exists()) { 400 lastResult = null; 401 writeError(resp, HttpServletResponse.SC_NOT_FOUND, 402 "The most recent profiling result (" + last.relativeUrl + ") no longer exists " 403 + "(may have been cleaned up by the OS). Run /prof to start a new session."); 404 return; 405 } 406 setResponseHeader(resp); 407 resp.sendRedirect(last.relativeUrl); 408 return; 409 } 410 411 final ProfileRequest request = parseProfileRequest(req); 412 413 // Reject non-positive PIDs regardless of backend — no valid process has pid <= 0. 414 if (request.getPid() != null && request.getPid() <= 0) { 415 writeError(resp, HttpServletResponse.SC_BAD_REQUEST, 416 "Invalid pid " + request.getPid() + ": must be a positive process ID."); 417 return; 418 } 419 420 // LibraryBackend can only profile the current JVM; BinaryBackend supports external PIDs. 421 if ( 422 request.getPid() != null && request.getPid().longValue() != currentPid 423 && backend instanceof LibraryBackend 424 ) { 425 LOG.warn("Rejected profiling request for PID {} (current PID: {}) — " 426 + "LibraryBackend only supports the current process", request.getPid(), currentPid); 427 writeError(resp, HttpServletResponse.SC_BAD_REQUEST, 428 "The 'pid' parameter is only supported for the current process when using the " 429 + "LibraryBackend (in-process async-profiler). Use ASYNC_PROFILER_HOME to enable " 430 + "the BinaryBackend for cross-process profiling."); 431 return; 432 } 433 434 boolean locked = false; 435 boolean thisRequestSetProfiling = false; 436 boolean stopperStarted = false; 437 File outputFile = null; 438 try { 439 locked = profilerLock.tryLock(100, TimeUnit.MILLISECONDS); 440 if (!locked) { 441 LOG.info("Profiler lock busy; returning 409 immediately."); 442 writeError(resp, HttpServletResponse.SC_CONFLICT, 443 "Another instance of profiler is already running or the lock is contended. " 444 + "Try again in a moment."); 445 return; 446 } 447 448 // Re-check under the lock to close the TOCTOU window. 449 if (profiling) { 450 StringBuilder msg = new StringBuilder("Another instance of profiler is already running."); 451 ProfileResult last = lastResult; 452 if (last != null) { 453 msg.append(" Last result: ").append(last.relativeUrl).append(" (").append(last.event) 454 .append(", ").append(last.durationSeconds).append("s, completed ") 455 .append(last.completedAt).append("). Use /prof?last to view it."); 456 } 457 writeError(resp, HttpServletResponse.SC_CONFLICT, msg.toString()); 458 return; 459 } 460 461 outputFile = createOutputFile(request); 462 final String relativeUrl = "/prof-output-hbase/" + outputFile.getName(); 463 // Write the placeholder before starting the profiler. Using CREATE_NEW (O_CREAT|O_EXCL) 464 // so a pre-planted symlink causes an immediate IOException rather than following the link 465 // and truncating the symlink target (symlink-attack mitigation). 466 Files.write(outputFile.toPath(), new byte[0], StandardOpenOption.CREATE_NEW); 467 executeStart(request, outputFile); 468 profiling = true; 469 thisRequestSetProfiling = true; 470 471 startStopperThread(request.getDuration(), request, outputFile, relativeUrl); 472 stopperStarted = true; 473 474 writeAcceptedResponse(resp, request, relativeUrl); 475 } catch (InterruptedException e) { 476 Thread.currentThread().interrupt(); 477 LOG.warn("Interrupted while acquiring profile lock.", e); 478 writeError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 479 "Interrupted while acquiring profile lock."); 480 } catch (IOException | Error | RuntimeException e) { 481 // Catches: 482 // - IOException: AsyncProfiler.execute() throws IOException for invalid agent commands 483 // - UnsatisfiedLinkError / other Error: native lib absent or incompatible OS/kernel 484 // - IllegalStateException / IllegalArgumentException (RuntimeException): double-start, 485 // unsupported event, rejected format from the profiler API 486 LOG.warn("Profiler failed to start or execute", e); 487 if (!resp.isCommitted()) { 488 writeError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 489 "Profiler error: " + e.getMessage() 490 + ". Check that the async-profiler native library is compatible with this OS/kernel."); 491 } 492 // If executeStart succeeded but startStopperThread failed (e.g. t.start() threw 493 // OutOfMemoryError), the LibraryBackend AsyncProfiler instance is still in "started" state 494 // with no stopper to drain it. A best-effort stop closes it so future requests don't hit 495 // IllegalStateException("profiler already started"). 496 if (thisRequestSetProfiling && !stopperStarted) { 497 try { 498 executeStop(request, outputFile); 499 } catch (Exception stopEx) { 500 LOG.warn("Best-effort stop after failed startStopperThread also failed", stopEx); 501 } 502 } 503 // Delete the placeholder whenever the stopper thread was never started — in that case 504 // no client received the output URL and the stopper will never write a result to the file. 505 // Guard on !stopperStarted (not !thisRequestSetProfiling) to also cover the rare path where 506 // executeStart succeeded but t.start() threw, leaving a 0-byte file with no owner. 507 if (!stopperStarted && outputFile != null) { 508 try { 509 Files.deleteIfExists(outputFile.toPath()); 510 } catch (IOException ioe) { 511 LOG.warn("Unable to delete orphan placeholder {}", outputFile.getName(), ioe); 512 } 513 } 514 } finally { 515 // Only reset the profiling flag if THIS request was the one that set it, and the stopper 516 // thread was never started (e.g. t.start() threw OutOfMemoryError). Using a separate flag 517 // avoids incorrectly clearing profiling=true for a concurrently-running session when this 518 // request exited early via the 409 conflict path. 519 if (thisRequestSetProfiling && !stopperStarted) { 520 profiling = false; 521 } 522 if (locked) { 523 profilerLock.unlock(); 524 } 525 } 526 } 527 528 private void startStopperThread(final int durationSeconds, final ProfileRequest request, 529 final File outputFile, final String relativeUrl) { 530 Thread t = new Thread(() -> { 531 boolean succeeded = false; 532 Throwable failure = null; 533 try { 534 TimeUnit.SECONDS.sleep(durationSeconds); 535 executeStop(request, outputFile); 536 // C5: executeStop may succeed but the profiler wrote nothing (e.g. zero samples collected). 537 // ProfileOutputServlet polls until size >= PROF_OUTPUT_MIN_BYTES, so pad a short/empty 538 // output file to unblock it rather than letting the browser auto-refresh forever. 539 if (outputFile.length() < PROF_OUTPUT_MIN_BYTES) { 540 String pad = "Profiling completed but output was empty or very short."; 541 while (pad.length() <= PROF_OUTPUT_MIN_BYTES) { 542 pad += " "; 543 } 544 Files.write(outputFile.toPath(), pad.getBytes(StandardCharsets.UTF_8), 545 java.nio.file.StandardOpenOption.APPEND); 546 } 547 lastResult = new ProfileResult(relativeUrl, request.getEvent().getInternalName(), 548 durationSeconds, Instant.now()); 549 succeeded = true; 550 } catch (InterruptedException e) { 551 Thread.currentThread().interrupt(); 552 failure = e; 553 LOG.warn("Profiler stopper thread interrupted; attempting best-effort stop.", e); 554 // C3: The LibraryBackend AsyncProfiler is still in "started" state after an interrupt. 555 // Best-effort stop so future /prof requests don't hit IllegalStateException. 556 try { 557 executeStop(request, outputFile); 558 } catch (Exception stopEx) { 559 LOG.warn("Best-effort stop after stopper interrupt also failed", stopEx); 560 } 561 } catch (Throwable e) { 562 failure = e; 563 LOG.warn("Profiler stop/dump failed", e); 564 } finally { 565 // C4: Reset profiling flag FIRST, before any I/O that could throw an Error and skip it. 566 profiling = false; 567 // If the session did not complete successfully, pad the output file to >100 bytes so 568 // ProfileOutputServlet's size check treats it as done and stops auto-refreshing. 569 if (!succeeded && failure != null) { 570 try { 571 String msg = (failure instanceof InterruptedException) 572 ? "Profiler session interrupted before stop completed." 573 : "Profiler stop/dump failed: " + failure.getMessage(); 574 // PROF_OUTPUT_MIN_BYTES is checked by ProfileOutputServlet to determine completion. 575 while (msg.length() <= PROF_OUTPUT_MIN_BYTES) { 576 msg += " "; 577 } 578 Files.write(outputFile.toPath(), msg.getBytes(StandardCharsets.UTF_8)); 579 } catch (IOException ioe) { 580 LOG.warn("Unable to write profiler error to output file", ioe); 581 } 582 } 583 } 584 }, "ProfileServlet-stopper"); 585 t.setDaemon(true); 586 t.start(); 587 } 588 589 private boolean checkInstrumentationAccess(final HttpServletRequest req, 590 final HttpServletResponse resp) throws IOException { 591 if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), req, resp)) { 592 resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 593 setResponseHeader(resp); 594 resp.getWriter().write("Unauthorized: Instrumentation access is not allowed!"); 595 return false; 596 } 597 return true; 598 } 599 600 @Override 601 public void destroy() { 602 if (backend != null) { 603 backend.destroy(); 604 } 605 super.destroy(); 606 } 607 608 private void writeError(final HttpServletResponse resp, final int status, final String message) 609 throws IOException { 610 resp.setStatus(status); 611 setResponseHeader(resp); 612 resp.getWriter().write(message); 613 } 614 615 private File createOutputFile(final ProfileRequest request) throws IOException { 616 final long pid = request.getPid() != null ? request.getPid().longValue() : currentPid; 617 // Use the remapped file extension so that (e.g.) SVG→HTML remap is reflected in the 618 // filename. toFileExtension is used here (not toFormatString) to avoid a duplicate 619 // LOG.warn — the warning is emitted once by toLibraryStopCommand or toCliCommand. 620 String ext = ProfilerCommandMapper.toFileExtension(request.getOutput()); 621 File outputFile = new File(OUTPUT_DIR, "async-prof-pid-" + pid + "-" 622 + request.getEvent().name().toLowerCase() + "-" + ID_GEN.incrementAndGet() + "." + ext); 623 return outputFile; 624 } 625 626 private void writeAcceptedResponse(final HttpServletResponse resp, final ProfileRequest request, 627 final String relativeUrl) throws IOException { 628 setResponseHeader(resp); 629 resp.setStatus(HttpServletResponse.SC_ACCEPTED); 630 StringBuilder body = new StringBuilder(); 631 body.append("Started [").append(request.getEvent().getInternalName()) 632 .append("] profiling. This page will automatically redirect to ").append(relativeUrl) 633 .append(" after ").append(request.getDuration()).append(" seconds. ") 634 .append("If empty diagram and Linux 4.6+, see 'Basic Usage' section on the Async ") 635 .append("Profiler Home Page, https://github.com/jvm-profiling-tools/async-profiler."); 636 if (request.getOutput() == Output.SVG) { 637 body.append("\nNote: output=svg is not supported in async-profiler 2.0+; serving html."); 638 } 639 if (backend instanceof LibraryBackend) { 640 if (request.getBufsize() != null) { 641 body.append("\nNote: bufsize= is not supported by the in-process LibraryBackend" 642 + " (async-profiler 4.x) and was ignored." 643 + " Set ASYNC_PROFILER_HOME to use BinaryBackend if you need -b support."); 644 } 645 if (request.getWidth() != null || request.getHeight() != null) { 646 body.append("\nNote: width= and height= are not supported by the in-process LibraryBackend" 647 + " (async-profiler 4.x) and were ignored." 648 + " Set ASYNC_PROFILER_HOME to use BinaryBackend if you need --width/--height support."); 649 } 650 } 651 resp.getWriter().write(body.toString()); 652 resp.setHeader("Refresh", 653 (request.getDuration() + request.getRefreshDelay()) + ";" + relativeUrl); 654 resp.getWriter().flush(); 655 } 656 657 private Integer getInteger(final HttpServletRequest req, final String param, 658 final Integer defaultValue) { 659 final String value = req.getParameter(param); 660 if (value != null) { 661 try { 662 return Integer.valueOf(value); 663 } catch (NumberFormatException e) { 664 return defaultValue; 665 } 666 } 667 return defaultValue; 668 } 669 670 private Long getLong(final HttpServletRequest req, final String param) { 671 final String value = req.getParameter(param); 672 if (value != null) { 673 try { 674 return Long.valueOf(value); 675 } catch (NumberFormatException e) { 676 return null; 677 } 678 } 679 return null; 680 } 681 682 private Double getMinWidth(final HttpServletRequest req) { 683 final String value = req.getParameter("minwidth"); 684 if (value != null) { 685 try { 686 return Double.valueOf(value); 687 } catch (NumberFormatException e) { 688 return null; 689 } 690 } 691 return null; 692 } 693 694 private Event getEvent(final HttpServletRequest req) { 695 final String eventArg = req.getParameter("event"); 696 if (eventArg != null) { 697 Event event = Event.fromInternalName(eventArg); 698 return event == null ? Event.CPU : event; 699 } 700 return Event.CPU; 701 } 702 703 private Output getOutput(final HttpServletRequest req) { 704 final String outputArg = req.getParameter("output"); 705 if (req.getParameter("output") != null) { 706 try { 707 return Output.valueOf(outputArg.trim().toUpperCase()); 708 } catch (IllegalArgumentException e) { 709 return Output.HTML; 710 } 711 } 712 return Output.HTML; 713 } 714 715 static void setResponseHeader(final HttpServletResponse response) { 716 response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, ALLOWED_METHODS); 717 response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); 718 response.setContentType(CONTENT_TYPE_TEXT); 719 } 720 721 public static class DisabledServlet extends HttpServlet { 722 723 private static final long serialVersionUID = 1L; 724 725 /** Init-param key for the human-readable disable reason. */ 726 static final String REASON_PARAM = "disabledReason"; 727 728 @Override 729 protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) 730 throws IOException { 731 resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 732 setResponseHeader(resp); 733 String reason = getInitParameter(REASON_PARAM); 734 if (reason == null || reason.isEmpty()) { 735 reason = "The profiler servlet was disabled at startup."; 736 } 737 resp.getWriter().write(reason + "\n\nFor more information please see " 738 + "https://hbase.apache.org/docs/profiler\n"); 739 } 740 741 } 742 743}