├── .gitignore ├── JavaFlames.java ├── Profile.java ├── README.md ├── config.jfc └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | /JavaFlames.iml 2 | -------------------------------------------------------------------------------- /JavaFlames.java: -------------------------------------------------------------------------------- 1 | import com.sun.net.httpserver.HttpServer; 2 | import jdk.jfr.consumer.RecordedEvent; 3 | import jdk.jfr.consumer.RecordedFrame; 4 | import jdk.jfr.consumer.RecordedMethod; 5 | import jdk.jfr.consumer.RecordingFile; 6 | 7 | import java.awt.Desktop; 8 | import java.io.FileInputStream; 9 | import java.io.IOException; 10 | import java.io.UncheckedIOException; 11 | import java.net.InetSocketAddress; 12 | import java.net.URI; 13 | import java.nio.charset.StandardCharsets; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | import java.nio.file.Paths; 17 | import java.util.ArrayDeque; 18 | import java.util.List; 19 | import java.util.Objects; 20 | import java.util.function.Consumer; 21 | import java.util.function.Function; 22 | import java.util.function.Supplier; 23 | import java.util.stream.Collectors; 24 | import java.util.stream.Stream; 25 | 26 | public class JavaFlames { 27 | 28 | private static final int HTTP_PORT = 8090; 29 | private static final String PATH_TO_DATA = "data"; 30 | 31 | public static void main(String[] args) throws IOException { 32 | if (args.length != 1) { 33 | exit(1, "expected jfr input file as argument"); 34 | } 35 | var jfrFile = Paths.get(args[0]); 36 | if (!Files.exists(jfrFile)) { 37 | exit(2, jfrFile + " not found."); 38 | } 39 | startHttpServer(jfrFile); 40 | 41 | var url = "http://localhost:%d?baseLineInput=%s&baseLineTitle=%s".formatted(HTTP_PORT, PATH_TO_DATA, jfrFile.toFile().getName()); 42 | if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { 43 | Desktop.getDesktop().browse(URI.create(url)); 44 | } else { 45 | System.out.println("Done! Open a browser and point it to: " + url); 46 | } 47 | } 48 | 49 | private static void exit(int code, String message) { 50 | System.err.println(message); 51 | System.exit(code); 52 | } 53 | 54 | private static void startHttpServer(Path jfrFile) throws IOException { 55 | var httpServer = HttpServer.create(new InetSocketAddress("localhost", HTTP_PORT), 0); 56 | httpServer.createContext("/", exchange -> { 57 | final Path htmlPage = Paths.get("index.html"); 58 | exchange.sendResponseHeaders(200, Files.size(htmlPage)); 59 | try (var responseBody = exchange.getResponseBody(); var fis = new FileInputStream(htmlPage.toFile())) { 60 | fis.transferTo(responseBody); 61 | } 62 | }); 63 | httpServer.createContext("/" + PATH_TO_DATA, exchange -> { 64 | exchange.sendResponseHeaders(200, 0); 65 | try(var responseBody = exchange.getResponseBody()){ 66 | produceFlameGraphLog(jfrFile).forEach(io(line -> responseBody.write(line.getBytes(StandardCharsets.UTF_8)))); 67 | } 68 | System.exit(0); 69 | }); 70 | httpServer.start(); 71 | } 72 | 73 | public static Stream produceFlameGraphLog(final Path jfrRecording) throws IOException { 74 | var recordingFile = new RecordingFile(jfrRecording); 75 | return extractEvents(recordingFile) 76 | .filter(it -> "jdk.ExecutionSample".equalsIgnoreCase(it.getEventType().getName())) 77 | .map(event -> collapseFrames(event.getStackTrace().getFrames())) 78 | .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) 79 | .entrySet().stream().map(e -> "%s %d\n".formatted(e.getKey(), e.getValue())) 80 | .onClose(io(recordingFile::close)); 81 | } 82 | 83 | private static String collapseFrames(List frames) { 84 | var methodNames = new ArrayDeque(frames.size()); 85 | for (var frame : frames) { 86 | final RecordedMethod method = frame.getMethod(); 87 | methodNames.addFirst("%s::%s".formatted(method.getType().getName(), method.getName())); 88 | } 89 | return String.join(";", methodNames); 90 | } 91 | 92 | private static Stream extractEvents(RecordingFile recordingFile) { 93 | return Stream.generate(() -> 94 | recordingFile.hasMoreEvents() ? 95 | io(recordingFile::readEvent).get() : 96 | null 97 | ).takeWhile(Objects::nonNull); 98 | } 99 | 100 | // Helpers for dealing with checked IOException's in lambdas 101 | 102 | @FunctionalInterface 103 | interface IORunnable { 104 | void run() throws IOException; 105 | } 106 | 107 | @FunctionalInterface 108 | interface IOConsumer { 109 | void apply(T input) throws IOException; 110 | } 111 | 112 | @FunctionalInterface 113 | interface IOSupplier { 114 | T get() throws IOException; 115 | } 116 | 117 | private static Supplier io(IOSupplier supplier) { 118 | return () -> { 119 | try { 120 | return supplier.get(); 121 | } catch (final IOException e) { 122 | throw new UncheckedIOException(e); 123 | } 124 | }; 125 | } 126 | 127 | private static Consumer io(IOConsumer consumer) { 128 | return t -> { 129 | try { 130 | consumer.apply(t); 131 | } catch (final IOException e) { 132 | throw new UncheckedIOException(e); 133 | } 134 | }; 135 | } 136 | 137 | private static Runnable io(IORunnable runnable) { 138 | return () -> { 139 | try { 140 | runnable.run(); 141 | } catch (final IOException e) { 142 | throw new UncheckedIOException(e); 143 | } 144 | }; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Profile.java: -------------------------------------------------------------------------------- 1 | import java.io.BufferedReader; 2 | import java.io.IOException; 3 | import java.io.InputStreamReader; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | import java.text.SimpleDateFormat; 8 | import java.time.Duration; 9 | import java.time.LocalTime; 10 | import java.util.Date; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | public class Profile { 14 | private static final String NOW = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()); 15 | private static final Path LOG_FILE_NAME = Paths.get("log" + NOW + ".jfr").toAbsolutePath(); 16 | private static final Path CONFIG_FILE_NAME = Paths.get("config.jfc").toAbsolutePath(); 17 | private static final String PROFILING_NAME = "temp"; 18 | private static final Duration DEFAULT_DURATION = Duration.ofSeconds(30); 19 | 20 | public static void main(String[] args) { 21 | try { 22 | run(args); 23 | } catch (Exception throwable) { 24 | throwable.printStackTrace(); 25 | exit(99, "Encountered exception: " + throwable.getMessage()); 26 | } 27 | } 28 | 29 | private static void run(String[] args) throws IOException, InterruptedException { 30 | if (args.length < 1) { 31 | exit(1, "expected pid to profile"); 32 | } 33 | 34 | final int pid = findProcessPid(args[0]); 35 | 36 | var duration = args.length == 2 ? 37 | Duration.ofSeconds(Integer.parseInt(args[1])) : 38 | DEFAULT_DURATION; 39 | 40 | run("jcmd %d JFR.start name=%s settings=%s".formatted(pid, PROFILING_NAME, CONFIG_FILE_NAME)); 41 | println("Started profiling process %d for %d seconds".formatted(pid, duration.getSeconds())); 42 | final LocalTime deadline = LocalTime.now().plus(duration); 43 | 44 | while (LocalTime.now().isBefore(deadline)) { 45 | TimeUnit.SECONDS.sleep(1); 46 | System.out.print("."); 47 | } 48 | 49 | println("Stopping profiling"); 50 | run("jcmd %d JFR.dump name=%s filename=%s".formatted(pid, PROFILING_NAME, LOG_FILE_NAME)); 51 | run("jcmd %d JFR.stop name=%s".formatted(pid, PROFILING_NAME)); 52 | 53 | println("Starting JavaFlames"); 54 | run("java JavaFlames.java %s".formatted(LOG_FILE_NAME)); 55 | 56 | Files.deleteIfExists(LOG_FILE_NAME); 57 | } 58 | 59 | private static int findProcessPid(String javaProcess) throws IOException { 60 | if(javaProcess.matches("[0-9]+")){ 61 | return Integer.parseInt(javaProcess); 62 | } 63 | 64 | final Process jps = new ProcessBuilder("jps").start(); 65 | 66 | try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(jps.getInputStream()))) { 67 | String line; 68 | while ((line = bufferedReader.readLine()) != null) { 69 | if (line.endsWith(javaProcess)) { 70 | return Integer.parseInt(line.split(" ")[0]); 71 | } 72 | } 73 | } 74 | 75 | throw new IllegalStateException("Failed to find pid for process: " + javaProcess); 76 | } 77 | 78 | private static void println(String s) { 79 | System.out.println(s); 80 | } 81 | 82 | private static void run(String command) throws IOException, InterruptedException { 83 | println("> " + command); 84 | final int exitCode = new ProcessBuilder(command.split(" ")) 85 | .inheritIO() 86 | .start() 87 | .waitFor(); 88 | 89 | if (exitCode != 0) { 90 | throw new IllegalStateException("Command \"%s\" exited with code %d".formatted(command, exitCode)); 91 | } 92 | } 93 | 94 | private static void exit(int code, String message) { 95 | println(message); 96 | usage(); 97 | System.exit(code); 98 | } 99 | 100 | private static void usage() { 101 | println("Usage: java Profile.java []"); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flamegraph from JFR logs 2 | Simple one file Java script to generate flamegraphs from Java flight recordings without installing Perl and the [Brendan Gregg scripts](https://github.com/brendangregg/FlameGraph). 3 | 4 | It also comes with a separate script to start profiling a java process using FlightRecorder and then call the flamegraph script. 5 | ## Usage 6 | With a fairly recent Java version installed simply run: 7 | `java JavaFlames.java ` 8 | 9 | This launches an http server serving the `index.html` file and endpoint for the folded format extracted from the JFR logs. 10 | The script then opens up a browser showing the results. 11 | 12 | Alternatively if you don't have an existing JFR recording you can also start the `Profile.java` script to start profiling a java process generating the JFR file and then triggering flamegraph generation. 13 | 14 | `java Profile.java []` 15 | 16 | The default profiling duration unless specified is 30 seconds. 17 | 18 | ## Credits 19 | Brendan Gregg for introducing the concept of flamegraphs and https://github.com/spiermar/d3-flame-graph for the javascript/html implementation. 20 | -------------------------------------------------------------------------------- /config.jfc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | everyChunk 7 | 8 | 9 | 10 | false 11 | 1000 ms 12 | 13 | 14 | 15 | false 16 | everyChunk 17 | 18 | 19 | 20 | false 21 | 1000 ms 22 | 23 | 24 | 25 | false 26 | 27 | 28 | 29 | false 30 | 31 | 32 | 33 | false 34 | false 35 | 1 s 36 | 37 | 38 | 39 | false 40 | false 41 | 1 s 42 | 43 | 44 | 45 | false 46 | false 47 | 1 s 48 | 49 | 50 | 51 | false 52 | false 53 | 1 s 54 | 55 | 56 | 57 | false 58 | false 59 | 1 s 60 | 61 | 62 | 63 | false 64 | false 65 | 0 ms 66 | 67 | 68 | 69 | false 70 | false 71 | 0 ms 72 | 73 | 74 | 75 | false 76 | false 77 | 0 ms 78 | 79 | 80 | 81 | false 82 | false 83 | 84 | 85 | 86 | false 87 | false 88 | 0 ms 89 | 90 | 91 | 92 | false 93 | false 94 | 95 | 96 | 97 | false 98 | 99 | 100 | 101 | false 102 | beginChunk 103 | 104 | 105 | 106 | false 107 | beginChunk 108 | 109 | 110 | 111 | true 112 | 5 ms 113 | 114 | 115 | 116 | true 117 | 5 ms 118 | 119 | 120 | 121 | false 122 | 0 ms 123 | 124 | 125 | 126 | false 127 | 0 ms 128 | 129 | 130 | 131 | false 132 | 0 ms 133 | 134 | 135 | 136 | false 137 | 0 ms 138 | 139 | 140 | 141 | false 142 | 0 ms 143 | 144 | 145 | 146 | false 147 | 0 ms 148 | 149 | 150 | 151 | false 152 | 0 ms 153 | 154 | 155 | 156 | false 157 | true 158 | 159 | 160 | 161 | false 162 | 999 d 163 | 164 | 165 | 166 | false 167 | beginChunk 168 | 169 | 170 | 171 | false 172 | beginChunk 173 | 174 | 175 | 176 | false 177 | beginChunk 178 | 179 | 180 | 181 | false 182 | beginChunk 183 | 184 | 185 | 186 | false 187 | beginChunk 188 | 189 | 190 | 191 | false 192 | beginChunk 193 | 194 | 195 | 196 | false 197 | beginChunk 198 | 199 | 200 | 201 | false 202 | 203 | 204 | 205 | false 206 | 207 | 208 | 209 | false 210 | 211 | 212 | 213 | false 214 | 215 | 216 | 217 | false 218 | 219 | 220 | 221 | false 222 | 223 | 224 | 225 | false 226 | 227 | 228 | 229 | false 230 | everyChunk 231 | 232 | 233 | 234 | false 235 | everyChunk 236 | 237 | 238 | 239 | false 240 | beginChunk 241 | 242 | 243 | 244 | false 245 | beginChunk 246 | 247 | 248 | 249 | false 250 | beginChunk 251 | 252 | 253 | 254 | false 255 | beginChunk 256 | 257 | 258 | 259 | false 260 | 261 | 262 | 263 | false 264 | 265 | 266 | 267 | false 268 | 269 | 270 | 271 | false 272 | 273 | 274 | 275 | false 276 | 277 | 278 | 279 | false 280 | 281 | 282 | 283 | false 284 | true 285 | 286 | 287 | 288 | false 289 | true 290 | 291 | 292 | 293 | false 294 | 295 | 296 | 297 | false 298 | 0 ms 299 | 300 | 301 | 302 | false 303 | 0 ms 304 | 305 | 306 | 307 | false 308 | 0 ms 309 | 310 | 311 | 312 | false 313 | 0 ms 314 | 315 | 316 | 317 | false 318 | 0 ms 319 | 320 | 321 | 322 | false 323 | 0 ms 324 | 325 | 326 | 327 | false 328 | 0 ms 329 | 330 | 331 | 332 | false 333 | 0 ms 334 | 335 | 336 | 337 | false 338 | 0 ms 339 | 340 | 341 | 342 | false 343 | 0 ms 344 | 345 | 346 | 347 | false 348 | 0 ms 349 | 350 | 351 | 352 | false 353 | 354 | 355 | 356 | false 357 | 358 | 359 | 360 | false 361 | 362 | 363 | 364 | false 365 | 366 | 367 | 368 | false 369 | 370 | 371 | 372 | false 373 | 374 | 375 | 376 | false 377 | 378 | 379 | 380 | false 381 | 382 | 383 | 384 | false 385 | 386 | 387 | 388 | false 389 | 390 | 391 | 392 | false 393 | 394 | 395 | 396 | false 397 | 398 | 399 | 400 | false 401 | true 402 | 403 | 404 | 405 | false 406 | 407 | 408 | 409 | false 410 | everyChunk 411 | 412 | 413 | 414 | false 415 | 416 | 417 | 418 | false 419 | false 420 | 0 ns 421 | 422 | 423 | 424 | false 425 | beginChunk 426 | 427 | 428 | 429 | false 430 | 1000 ms 431 | 432 | 433 | 434 | false 435 | 100 ms 436 | 437 | 438 | 439 | false 440 | 10 s 441 | 442 | 443 | 444 | false 445 | 446 | 447 | 448 | false 449 | 450 | 451 | 452 | false 453 | beginChunk 454 | 455 | 456 | 457 | false 458 | everyChunk 459 | 460 | 461 | 462 | false 463 | 100 ms 464 | 465 | 466 | 467 | false 468 | beginChunk 469 | 470 | 471 | 472 | false 473 | everyChunk 474 | 475 | 476 | 477 | false 478 | 479 | 480 | 481 | false 482 | beginChunk 483 | 484 | 485 | 486 | false 487 | beginChunk 488 | 489 | 490 | 491 | false 492 | 10 s 493 | 494 | 495 | 496 | false 497 | 1000 ms 498 | 499 | 500 | 501 | false 502 | 10 s 503 | 504 | 505 | 506 | false 507 | beginChunk 508 | 509 | 510 | 511 | false 512 | endChunk 513 | 514 | 515 | 516 | false 517 | 5 s 518 | 519 | 520 | 521 | false 522 | beginChunk 523 | 524 | 525 | 526 | false 527 | everyChunk 528 | 529 | 530 | 531 | false 532 | true 533 | 534 | 535 | 536 | false 537 | true 538 | 539 | 540 | 541 | false 542 | everyChunk 543 | 544 | 545 | 546 | false 547 | true 548 | 1 s 549 | 550 | 551 | 552 | false 553 | true 554 | 1 s 555 | 556 | 557 | 558 | false 559 | true 560 | 1 s 561 | 562 | 563 | 564 | false 565 | true 566 | 1 s 567 | 568 | 569 | 570 | false 571 | true 572 | 1 s 573 | 574 | 575 | 576 | false 577 | true 578 | 579 | 580 | 581 | false 582 | true 583 | 584 | 585 | 586 | false 587 | 1000 ms 588 | 589 | 590 | 591 | false 592 | 593 | 594 | 595 | false 596 | 597 | 598 | 599 | false 600 | 601 | 602 | 603 | false 604 | 605 | 606 | 607 | false 608 | 10 ms 609 | 610 | 611 | 612 | false 613 | 0 ms 614 | 615 | 616 | 617 | 10 ms 618 | false 619 | 620 | 621 | 622 | false 623 | 10 ms 624 | 625 | 626 | 627 | 628 | 629 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 39 | 40 | Flames! 41 | 42 | 43 | 47 | 48 | 49 |
50 |
51 | 63 |
64 |
65 |
66 |
67 |
task: 68 |
69 | 77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 88 | 89 | 273 | 274 | --------------------------------------------------------------------------------