1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.hadoop.hbase.http;
19
20 import com.google.common.base.Joiner;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.lang.reflect.Method;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.atomic.AtomicInteger;
29 import java.util.concurrent.locks.Lock;
30 import java.util.concurrent.locks.ReentrantLock;
31
32 import javax.servlet.http.HttpServlet;
33 import javax.servlet.http.HttpServletRequest;
34 import javax.servlet.http.HttpServletResponse;
35
36 import org.apache.commons.logging.Log;
37 import org.apache.commons.logging.LogFactory;
38 import org.apache.hadoop.hbase.util.ProcessUtils;
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85 public class ProfileServlet extends HttpServlet {
86
87 private static final long serialVersionUID = 1L;
88 private static final Log LOG = LogFactory.getLog(ProfileServlet.class);
89
90 private static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
91 private static final String ALLOWED_METHODS = "GET";
92 private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
93 private static final String CONTENT_TYPE_TEXT = "text/plain; charset=utf-8";
94 private static final String ASYNC_PROFILER_HOME_ENV = "ASYNC_PROFILER_HOME";
95 private static final String ASYNC_PROFILER_HOME_SYSTEM_PROPERTY = "async.profiler.home";
96 private static final String PROFILER_SCRIPT = "/profiler.sh";
97 private static final int DEFAULT_DURATION_SECONDS = 10;
98 private static final AtomicInteger ID_GEN = new AtomicInteger(0);
99 static final String OUTPUT_DIR = System.getProperty("java.io.tmpdir") + "/prof-output";
100
101 enum Event {
102 CPU("cpu"),
103 ALLOC("alloc"),
104 LOCK("lock"),
105 PAGE_FAULTS("page-faults"),
106 CONTEXT_SWITCHES("context-switches"),
107 CYCLES("cycles"),
108 INSTRUCTIONS("instructions"),
109 CACHE_REFERENCES("cache-references"),
110 CACHE_MISSES("cache-misses"),
111 BRANCHES("branches"),
112 BRANCH_MISSES("branch-misses"),
113 BUS_CYCLES("bus-cycles"),
114 L1_DCACHE_LOAD_MISSES("L1-dcache-load-misses"),
115 LLC_LOAD_MISSES("LLC-load-misses"),
116 DTLB_LOAD_MISSES("dTLB-load-misses"),
117 MEM_BREAKPOINT("mem:breakpoint"),
118 TRACE_TRACEPOINT("trace:tracepoint"),;
119
120 private final String internalName;
121
122 Event(final String internalName) {
123 this.internalName = internalName;
124 }
125
126 public String getInternalName() {
127 return internalName;
128 }
129
130 public static Event fromInternalName(final String name) {
131 for (Event event : values()) {
132 if (event.getInternalName().equalsIgnoreCase(name)) {
133 return event;
134 }
135 }
136
137 return null;
138 }
139 }
140
141 enum Output {
142 SUMMARY,
143 TRACES,
144 FLAT,
145 COLLAPSED,
146 SVG,
147 TREE,
148 JFR
149 }
150
151 @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED",
152 justification = "This class is never serialized nor restored.")
153 private transient Lock profilerLock = new ReentrantLock();
154 private transient volatile Process process;
155 private String asyncProfilerHome;
156 private Integer pid;
157
158 public ProfileServlet() {
159 this.asyncProfilerHome = getAsyncProfilerHome();
160 this.pid = ProcessUtils.getPid();
161 LOG.info("Servlet process PID: " + pid + " asyncProfilerHome: " + asyncProfilerHome);
162 }
163
164 @Override
165 protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
166 throws IOException {
167 if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), req, resp)) {
168 resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
169 setResponseHeader(resp);
170 resp.getWriter().write("Unauthorized: Instrumentation access is not allowed!");
171 return;
172 }
173
174
175 if (asyncProfilerHome == null || asyncProfilerHome.trim().isEmpty()) {
176 resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
177 setResponseHeader(resp);
178 resp.getWriter().write("ASYNC_PROFILER_HOME env is not set.\n\n" +
179 "Please ensure the prerequsites for the Profiler Servlet have been installed and the\n" +
180 "environment is properly configured. For more information please see\n" +
181 "http://hbase.apache.org/book.html#profiler\n");
182 return;
183 }
184
185
186 pid = getInteger(req, "pid", pid);
187
188
189 if (pid == null) {
190 resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
191 setResponseHeader(resp);
192 resp.getWriter().write(
193 "'pid' query parameter unspecified or unable to determine PID of current process.");
194 return;
195 }
196
197 final int duration = getInteger(req, "duration", DEFAULT_DURATION_SECONDS);
198 final Output output = getOutput(req);
199 final Event event = getEvent(req);
200 final Long interval = getLong(req, "interval");
201 final Integer jstackDepth = getInteger(req, "jstackdepth", null);
202 final Long bufsize = getLong(req, "bufsize");
203 final boolean thread = req.getParameterMap().containsKey("thread");
204 final boolean simple = req.getParameterMap().containsKey("simple");
205 final Integer width = getInteger(req, "width", null);
206 final Integer height = getInteger(req, "height", null);
207 final Double minwidth = getMinWidth(req);
208 final boolean reverse = req.getParameterMap().containsKey("reverse");
209
210 if (process == null || !isAlive(process)) {
211 try {
212 int lockTimeoutSecs = 3;
213 if (profilerLock.tryLock(lockTimeoutSecs, TimeUnit.SECONDS)) {
214 try {
215 File outputFile = new File(OUTPUT_DIR, "async-prof-pid-" + pid + "-" +
216 event.name().toLowerCase() + "-" + ID_GEN.incrementAndGet() + "." +
217 output.name().toLowerCase());
218 List<String> cmd = new ArrayList<>();
219 cmd.add(asyncProfilerHome + PROFILER_SCRIPT);
220 cmd.add("-e");
221 cmd.add(event.getInternalName());
222 cmd.add("-d");
223 cmd.add("" + duration);
224 cmd.add("-o");
225 cmd.add(output.name().toLowerCase());
226 cmd.add("-f");
227 cmd.add(outputFile.getAbsolutePath());
228 if (interval != null) {
229 cmd.add("-i");
230 cmd.add(interval.toString());
231 }
232 if (jstackDepth != null) {
233 cmd.add("-j");
234 cmd.add(jstackDepth.toString());
235 }
236 if (bufsize != null) {
237 cmd.add("-b");
238 cmd.add(bufsize.toString());
239 }
240 if (thread) {
241 cmd.add("-t");
242 }
243 if (simple) {
244 cmd.add("-s");
245 }
246 if (width != null) {
247 cmd.add("--width");
248 cmd.add(width.toString());
249 }
250 if (height != null) {
251 cmd.add("--height");
252 cmd.add(height.toString());
253 }
254 if (minwidth != null) {
255 cmd.add("--minwidth");
256 cmd.add(minwidth.toString());
257 }
258 if (reverse) {
259 cmd.add("--reverse");
260 }
261 cmd.add(pid.toString());
262 process = ProcessUtils.runCmdAsync(cmd);
263
264
265 setResponseHeader(resp);
266 resp.setStatus(HttpServletResponse.SC_ACCEPTED);
267 String relativeUrl = "/prof-output/" + outputFile.getName();
268 resp.getWriter().write(
269 "Started [" + event.getInternalName() +
270 "] profiling. This page will automatically redirect to " +
271 relativeUrl + " after " + duration + " seconds.\n\ncommand:\n" +
272 Joiner.on(" ").join(cmd));
273
274
275
276 int refreshDelay = getInteger(req, "refreshDelay", 0);
277
278
279
280 resp.setHeader("Refresh", (duration + refreshDelay) + ";" + relativeUrl);
281 resp.getWriter().flush();
282 } finally {
283 profilerLock.unlock();
284 }
285 } else {
286 setResponseHeader(resp);
287 resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
288 resp.getWriter().write(
289 "Unable to acquire lock. Another instance of profiler might be running.");
290 LOG.warn("Unable to acquire lock in " + lockTimeoutSecs +
291 " seconds. Another instance of profiler might be running.");
292 }
293 } catch (InterruptedException e) {
294 LOG.warn("Interrupted while acquiring profile lock.", e);
295 resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
296 }
297 } else {
298 setResponseHeader(resp);
299 resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
300 resp.getWriter().write("Another instance of profiler is already running.");
301 }
302 }
303
304
305 private static Method isAliveMethod;
306 static {
307 try {
308 isAliveMethod = Process.class.getDeclaredMethod("isAlive");
309 } catch (Exception e) {
310 isAliveMethod = null;
311 }
312 }
313
314 private static boolean isAlive(Process process) {
315
316 if (isAliveMethod != null) {
317 try {
318 return (boolean) isAliveMethod.invoke(process);
319 } catch (Exception e) {
320 if (LOG.isTraceEnabled()) {
321 LOG.trace("Failed to invoke Process#isAlive on " + process, e);
322 }
323
324 }
325 }
326
327
328 try {
329 int exitValue = process.exitValue();
330 if (LOG.isTraceEnabled()) {
331 LOG.trace("Process " + process + " is dead with exitValue " + exitValue);
332 }
333 } catch (IllegalThreadStateException e) {
334
335 return true;
336 }
337
338 return false;
339 }
340
341 private Integer getInteger(final HttpServletRequest req, final String param,
342 final Integer defaultValue) {
343 final String value = req.getParameter(param);
344 if (value != null) {
345 try {
346 return Integer.valueOf(value);
347 } catch (NumberFormatException e) {
348 return defaultValue;
349 }
350 }
351 return defaultValue;
352 }
353
354 private Long getLong(final HttpServletRequest req, final String param) {
355 final String value = req.getParameter(param);
356 if (value != null) {
357 try {
358 return Long.valueOf(value);
359 } catch (NumberFormatException e) {
360 return null;
361 }
362 }
363 return null;
364 }
365
366 private Double getMinWidth(final HttpServletRequest req) {
367 final String value = req.getParameter("minwidth");
368 if (value != null) {
369 try {
370 return Double.valueOf(value);
371 } catch (NumberFormatException e) {
372 return null;
373 }
374 }
375 return null;
376 }
377
378 private Event getEvent(final HttpServletRequest req) {
379 final String eventArg = req.getParameter("event");
380 if (eventArg != null) {
381 Event event = Event.fromInternalName(eventArg);
382 return event == null ? Event.CPU : event;
383 }
384 return Event.CPU;
385 }
386
387 private Output getOutput(final HttpServletRequest req) {
388 final String outputArg = req.getParameter("output");
389 if (req.getParameter("output") != null) {
390 try {
391 return Output.valueOf(outputArg.trim().toUpperCase());
392 } catch (IllegalArgumentException e) {
393 return Output.SVG;
394 }
395 }
396 return Output.SVG;
397 }
398
399 private static void setResponseHeader(final HttpServletResponse response) {
400 response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, ALLOWED_METHODS);
401 response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
402 response.setContentType(CONTENT_TYPE_TEXT);
403 }
404
405 static String getAsyncProfilerHome() {
406 String asyncProfilerHome = System.getenv(ASYNC_PROFILER_HOME_ENV);
407
408 if (asyncProfilerHome == null || asyncProfilerHome.trim().isEmpty()) {
409 asyncProfilerHome = System.getProperty(ASYNC_PROFILER_HOME_SYSTEM_PROPERTY);
410 }
411
412 return asyncProfilerHome;
413 }
414
415 public static class DisabledServlet extends HttpServlet {
416
417 private static final long serialVersionUID = 1L;
418
419 @Override
420 protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
421 throws IOException {
422 resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
423 setResponseHeader(resp);
424 resp.getWriter().write("The profiler servlet was disabled at startup.\n\n" +
425 "Please ensure the prerequsites for the Profiler Servlet have been installed and the\n" +
426 "environment is properly configured. For more information please see\n" +
427 "http://hbase.apache.org/book.html#profiler\n");
428 return;
429 }
430
431 }
432
433 }