1 /* 2 * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.jfr; 27 28 import java.io.Closeable; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.nio.file.Path; 32 import java.time.Duration; 33 import java.time.Instant; 34 import java.util.Collections; 35 import java.util.HashMap; 36 import java.util.Map; 37 import java.util.Objects; 38 39 import jdk.jfr.internal.PlatformRecording; 40 import jdk.jfr.internal.Type; 41 import jdk.jfr.internal.Utils; 42 import jdk.jfr.internal.WriteableUserPath; 43 44 /** 45 * Provides means to configure, start, stop and dump recording data to disk. 46 * <p> 47 * Example, 48 * 49 * <pre> 50 * <code> 51 * Configuration c = Configuration.getConfiguration("default"); 52 * Recording r = new Recording(c); 53 * r.start(); 54 * System.gc(); 55 * Thread.sleep(5000); 56 * r.stop(); 57 * r.copyTo(Files.createTempFile("my-recording", ".jfr")); 58 * </code> 59 * </pre> 60 * 61 * @since 9 62 */ 63 public final class Recording implements Closeable { 64 65 private static class RecordingSettings extends EventSettings { 66 67 private final Recording recording; 68 private final String identifier; 69 70 RecordingSettings(Recording r, String identifier) { 71 this.recording = r; 72 this.identifier = identifier; 73 } 74 75 RecordingSettings(Recording r, Class<? extends Event> eventClass) { 76 Utils.ensureValidEventSubclass(eventClass); 77 this.recording = r; 78 this.identifier = String.valueOf(Type.getTypeId(eventClass)); 79 } 80 81 @Override 82 public EventSettings with(String name, String value) { 83 Objects.requireNonNull(value); 84 recording.setSetting(identifier + "#" + name, value); 85 return this; 86 } 87 88 @Override 89 public Map<String, String> toMap() { 90 return recording.getSettings(); 91 } 92 } 93 94 private final PlatformRecording internal; 95 96 private Recording(PlatformRecording internal) { 97 this.internal = internal; 98 this.internal.setRecording(this); 99 if (internal.getRecording() != this) { 100 throw new InternalError("Internal recording not properly setup"); 101 } 102 } 103 104 /** 105 * Creates a recording without any settings. 106 * <p> 107 * A newly created recording will be in {@link RecordingState#NEW}. To start 108 * the recording invoke {@link Recording#start()}. 109 * 110 * @throws IllegalStateException if the platform Flight Recorder couldn't be 111 * created, for instance if commercial features isn't unlocked, or 112 * if the file repository can't be created or accessed. 113 * 114 * @throws SecurityException If a security manager is used and 115 * FlightRecorderPermission "accessFlightRecorder" is not set. 116 */ 117 public Recording() { 118 this(FlightRecorder.getFlightRecorder().newInternalRecording(new HashMap<String, String>())); 119 } 120 121 /** 122 * Creates a recording with settings from a configuration. 123 * <p> 124 * Example, 125 * 126 * <pre> 127 * <code> 128 * Recording r = new Recording(Configuration.getConfiguration("default")); 129 * </code> 130 * </pre> 131 * 132 * The newly created recording will be in {@link RecordingState#NEW}. To 133 * start the recording invoke {@link Recording#start()}. 134 * 135 * @param configuration configuration containing settings to be used, not 136 * {@code null} 137 * 138 * @throws IllegalStateException if the platform Flight Recorder couldn't be 139 * created, for instance if commercial features isn't unlocked, or 140 * if the file repository can't be created or accessed. 141 * 142 * @throws SecurityException If a security manager is used and 143 * FlightRecorderPermission "accessFlightRecorder" is not set. 144 * 145 * @see Configuration 146 */ 147 public Recording(Configuration configuration) { 148 this(FlightRecorder.getFlightRecorder().newInternalRecording(configuration.getSettings())); 149 } 150 151 /** 152 * Starts this recording. 153 * <p> 154 * It's recommended that recording options and event settings are configured 155 * before calling this method, since it guarantees a more consistent state 156 * when analyzing the recorded data. It may also have performance benefits, 157 * since the configuration can be applied atomically. 158 * <p> 159 * After a successful invocation of this method, the state of this recording will be 160 * in {@code RUNNING} state. 161 * 162 * @throws IllegalStateException if recording has already been started or is 163 * in {@code CLOSED} state 164 */ 165 public void start() { 166 internal.start(); 167 } 168 169 /** 170 * Starts this recording after a delay. 171 * <p> 172 * After a successful invocation of this method, the state of this recording will be 173 * in the {@code DELAYED} state. 174 * 175 * @param delay the time to wait before starting this recording, not 176 * {@code null} 177 * @throws IllegalStateException if already started or is in {@code CLOSED} 178 * state 179 */ 180 public void scheduleStart(Duration delay) { 181 Objects.requireNonNull(delay); 182 internal.scheduleStart(delay); 183 } 184 185 /** 186 * Stops this recording. 187 * <p> 188 * Once a recording has been stopped it can't be restarted. If this 189 * recording has a destination, data will be written to that destination and 190 * the recording closed. Once a recording is closed, the data is no longer 191 * available. 192 * <p> 193 * After a successful invocation of this method, the state of this recording will be 194 * in {@code STOPPED} state. 195 * 196 * @return {@code true} if recording was stopped, {@code false} otherwise 197 * 198 * @throws IllegalStateException if not started or already been stopped 199 * 200 * @throws SecurityException if a security manager exists and the caller 201 * doesn't have {@code FilePermission} to write at the destination 202 * path 203 * 204 * @see #setDestination(Path) 205 * 206 */ 207 public boolean stop() { 208 return internal.stop("Stopped by user"); 209 } 210 211 /** 212 * Returns settings used by this recording. 213 * <p> 214 * Modifying the returned map will not change settings for this recording. 215 * <p> 216 * If no settings have been set for this recording, an empty map is 217 * returned. 218 * 219 * @return recording settings, not {@code null} 220 */ 221 public Map<String, String> getSettings() { 222 return new HashMap<>(internal.getSettings()); 223 } 224 225 /** 226 * Returns the current size of this recording in the disk repository, 227 * measured in bytes. 228 * 229 * Size is updated when recording buffers are flushed. If the recording is 230 * not to disk the returned is always {@code 0}. 231 * 232 * @return amount of recorded data, measured in bytes, or {@code 0} if the 233 * recording is not to disk 234 */ 235 public long getSize() { 236 return internal.getSize(); 237 } 238 239 /** 240 * Returns the time when this recording was stopped. 241 * 242 * @return the the time, or {@code null} if this recording hasn't been 243 * stopped 244 */ 245 public Instant getStopTime() { 246 return internal.getStopTime(); 247 } 248 249 /** 250 * Returns the time when this recording was started. 251 * 252 * @return the the time, or {@code null} if this recording hasn't been 253 * started 254 */ 255 public Instant getStartTime() { 256 return internal.getStartTime(); 257 } 258 259 /** 260 * Returns the max size, measured in bytes, at which data will no longer be 261 * kept in the disk repository. 262 * 263 * @return max size in bytes, or {@code 0} if no max size has been set 264 */ 265 public long getMaxSize() { 266 return internal.getMaxSize(); 267 } 268 269 /** 270 * Returns how long data is to be kept in the disk repository before it is 271 * thrown away. 272 * 273 * @return max age, or {@code null} if no maximum age has been set 274 */ 275 public Duration getMaxAge() { 276 return internal.getMaxAge(); 277 } 278 279 /** 280 * Returns the name of this recording. 281 * <p> 282 * By default the name is the same as the recording id. 283 * 284 * @return recording name, not {@code null} 285 */ 286 public String getName() { 287 return internal.getName(); 288 } 289 290 /** 291 * Replaces all settings for this recording, 292 * <p> 293 * Example, 294 * 295 * <pre> 296 * <code> 297 * Map{@literal <}String, String{@literal >} settings = new HashMap{@literal <}{@literal >}(); 298 * settings.putAll(EventSettings.enabled("com.oracle.jdk.CPUSample").withPeriod(Duration.ofSeconds(2)).toMap()); 299 * settings.putAll(EventSettings.enabled(MyEvent.class).withThreshold(Duration.ofSeconds(2)).withoutStackTrace().toMap()); 300 * settings.put("com.oracle.jdk.ExecutionSample#period", "10 ms"); 301 * recording.setSettings(settings); 302 * </code> 303 * </pre> 304 * 305 * To merge with existing settings do. 306 * 307 * <pre> 308 * {@code 309 * Map<String, String> settings = recording.getSettings(); 310 * settings.putAll(additionalSettings)); 311 * recording.setSettings(settings); 312 * } 313 * </pre> 314 * 315 * @param settings the settings to set, not {@code null} 316 */ 317 public void setSettings(Map<String, String> settings) { 318 Objects.requireNonNull(settings); 319 Map<String, String> sanitized = Utils.sanitizeNullFreeStringMap(settings); 320 internal.setSettings(sanitized); 321 } 322 323 /** 324 * Returns the <code>RecordingState</code> this recording is currently in. 325 * 326 * @return the recording state, not {@code null} 327 * 328 * @see RecordingState 329 */ 330 public RecordingState getState() { 331 return internal.getState(); 332 } 333 334 /** 335 * Releases all data associated with this recording. 336 * <p> 337 * After a successful invocation of this method, the state of this recording will be 338 * in the {@code CLOSED} state. 339 */ 340 @Override 341 public void close() { 342 internal.close(); 343 } 344 345 /** 346 * Returns a clone of this recording, but with a new recording id and name. 347 * 348 * Clones are useful for dumping data without stopping the recording. After 349 * a clone is created the amount of data to be copied out can be constrained 350 * with the {@link #setMaxAge(Duration)} and {@link #setMaxSize(long)}. 351 * 352 * @param stop {@code true} if the newly created copy should be stopped 353 * immediately, {@code false} otherwise 354 * @return the recording copy, not {@code null} 355 */ 356 public Recording copy(boolean stop) { 357 return internal.newCopy(stop); 358 } 359 360 /** 361 * Writes recording data to a file. 362 * <p> 363 * Recording must be started, but not necessarily stopped. 364 * 365 * @param destination where recording data should be written, not 366 * {@code null} 367 * 368 * @throws IOException if recording can't be copied to {@code path} 369 * 370 * @throws SecurityException if a security manager exists and the caller 371 * doesn't have {@code FilePermission} to write at the destination 372 * path 373 */ 374 public void dump(Path destination) throws IOException { 375 Objects.requireNonNull(destination); 376 internal.copyTo(new WriteableUserPath(destination), "Dumped by user", Collections.emptyMap()); 377 } 378 379 /** 380 * Returns if this recording is to disk. 381 * <p> 382 * If no value has been set, {@code true} is returned. 383 * 384 * @return {@code true} if recording is to disk, {@code false} otherwise 385 */ 386 public boolean isToDisk() { 387 return internal.isToDisk(); 388 } 389 390 /** 391 * Determines how much data should be kept in disk repository. 392 * <p> 393 * In order to control the amount of recording data stored on disk, and not 394 * having it grow indefinitely, Max size can be used to limit the amount of 395 * data retained. When the {@code maxSize} limit has been exceeded, the JVM 396 * will remove the oldest chunk to make room for a more recent chunk. 397 * 398 * @param maxSize or {@code 0} if infinite 399 * 400 * @throws IllegalArgumentException if <code>maxSize</code> is negative 401 * 402 * @throws IllegalStateException if the recording is in {@code CLOSED} state 403 */ 404 public void setMaxSize(long maxSize) { 405 if (maxSize < 0) { 406 throw new IllegalArgumentException("Max size of recording can't be negative"); 407 } 408 internal.setMaxSize(maxSize); 409 } 410 411 /** 412 * Determines how far back data should be kept in disk repository. 413 * <p> 414 * In order to control the amount of recording data stored on disk, and not 415 * having it grow indefinitely, {@code maxAge} can be used to limit the 416 * amount of data retained. Data stored on disk which is older than 417 * {@code maxAge} will be removed by Flight Recorder. 418 * 419 * @param maxAge how long data can be kept, or {@code null} if infinite 420 * 421 * @throws IllegalArgumentException if <code>maxAge</code> is negative 422 * 423 * @throws IllegalStateException if the recording is in closed state 424 */ 425 public void setMaxAge(Duration maxAge) { 426 if (maxAge != null && maxAge.isNegative()) { 427 throw new IllegalArgumentException("Max age of recording can't be negative"); 428 } 429 internal.setMaxAge(maxAge); 430 } 431 432 /** 433 * Sets a location where data will be written on recording stop, or 434 * {@code null} if data should not be dumped automatically. 435 * <p> 436 * If a destination is set, this recording will be closed automatically 437 * after data has been copied successfully to the destination path. 438 * <p> 439 * If a destination is <em>not</em> set, Flight Recorder will hold on to 440 * recording data until this recording is closed. Use {@link #dump(Path)} to 441 * write data to a file manually. 442 * 443 * @param destination destination path, or {@code null} if recording should 444 * not be dumped at stop 445 * 446 * @throws IllegalStateException if recording is in {@code ETOPPED} or 447 * {@code CLOSED} state. 448 * 449 * @throws SecurityException if a security manager exists and the caller 450 * doesn't have {@code FilePermission} to read, write and delete the 451 * {@code destination} file 452 * 453 * @throws IOException if path is not writable 454 */ 455 public void setDestination(Path destination) throws IOException { 456 internal.setDestination(destination != null ? new WriteableUserPath(destination) : null); 457 } 458 459 /** 460 * Returns destination file, where recording data will be written when the 461 * recording stops, or {@code null} if no destination has been set. 462 * 463 * @return the destination file, or {@code null} if not set. 464 */ 465 public Path getDestination() { 466 WriteableUserPath usp = internal.getDestination(); 467 if (usp == null) { 468 return null; 469 } else { 470 return usp.getPotentiallyMaliciousOriginal(); 471 } 472 } 473 474 /** 475 * Returns a unique identifier for this recording. 476 * 477 * @return the recording identifier 478 */ 479 public long getId() { 480 return internal.getId(); 481 } 482 483 /** 484 * Sets a human-readable name, such as {@code "My Recording"}. 485 * 486 * @param name the recording name, not {@code null} 487 * 488 * @throws IllegalStateException if the recording is in closed state 489 */ 490 public void setName(String name) { 491 Objects.requireNonNull(name); 492 internal.setName(name); 493 } 494 495 /** 496 * Sets if this recording should be dumped to disk when the JVM exits. 497 * 498 * @param dumpOnExit if recording should be dumped on JVM exit 499 */ 500 public void setDumpOnExit(boolean dumpOnExit) { 501 internal.setDumpOnExit(dumpOnExit); 502 } 503 504 /** 505 * Returns if this recording should be dumped to disk when the JVM exits. 506 * <p> 507 * If dump on exit has not been set, {@code false} is returned. 508 * 509 * @return {@code true} if recording should be dumped on exit, {@code false} 510 * otherwise. 511 */ 512 public boolean getDumpOnExit() { 513 return internal.getDumpOnExit(); 514 } 515 516 /** 517 * Determines if this recording should be flushed to disk continuously or if 518 * data should be constrained to what is available in memory buffers. 519 * 520 * @param disk {@code true} if recording should be written to disk, 521 * {@code false} if in-memory 522 * 523 */ 524 public void setToDisk(boolean disk) { 525 internal.setToDisk(disk); 526 } 527 528 /** 529 * Creates a data stream for a specified interval. 530 * 531 * The stream may contain some data outside the specified range. 532 * 533 * @param start start time for the stream, or {@code null} to get data from 534 * start time of the recording 535 * 536 * @param end end time for the stream, or {@code null} to get data until the 537 * present time. 538 * 539 * @return an input stream, or {@code null} if no data is available in the 540 * interval. 541 * 542 * @throws IllegalArgumentException if {@code end} happens before 543 * {@code start} 544 * 545 * @throws IOException if a stream can't be opened 546 */ 547 public InputStream getStream(Instant start, Instant end) throws IOException { 548 if (start != null && end != null && end.isBefore(start)) { 549 throw new IllegalArgumentException("End time of requested stream must not be before start time"); 550 } 551 return internal.open(start, end); 552 } 553 554 /** 555 * Returns the desired duration for this recording, or {@code null} if no 556 * duration has been set. 557 * <p> 558 * The duration can only be set when the recording state is 559 * {@link RecordingState#NEW}. 560 * 561 * @return the desired duration of the recording, or {@code null} if no 562 * duration has been set. 563 */ 564 public Duration getDuration() { 565 return internal.getDuration(); 566 } 567 568 /** 569 * Sets a duration for how long a recording should run before it's stopped. 570 * <p> 571 * By default a recording has no duration (<code>null</code>). 572 * 573 * @param duration the duration or {@code null} if no duration should be 574 * used 575 * 576 * @throws IllegalStateException if recording is in stopped or closed state 577 */ 578 public void setDuration(Duration duration) { 579 internal.setDuration(duration); 580 } 581 582 /** 583 * Enables the event with the specified name. 584 * <p> 585 * If there are multiple events with same name, which can be the case if the 586 * same class is loaded in different class loaders, all events matching the 587 * name will be enabled. To enable a specific class, use the 588 * {@link #enable(Class)} method or a String representation of the event 589 * type id. 590 * 591 * @param name the settings for the event, not {@code null} 592 * 593 * @return an event setting for further configuration, not {@code null} 594 * 595 * @see EventType 596 */ 597 public EventSettings enable(String name) { 598 Objects.requireNonNull(name); 599 RecordingSettings rs = new RecordingSettings(this, name); 600 rs.with("enabled", "true"); 601 return rs; 602 } 603 604 /** 605 * Disables event with the specified name. 606 * <p> 607 * If there are multiple events with same name, which can be the case if the 608 * same class is loaded in different class loaders, all events matching the 609 * name will be disabled. To disable a specific class, use the 610 * {@link #disable(Class)} method or a String representation of the event 611 * type id. 612 * 613 * @param name the settings for the event, not {@code null} 614 * 615 * @return an event setting for further configuration, not {@code null} 616 * 617 */ 618 public EventSettings disable(String name) { 619 Objects.requireNonNull(name); 620 RecordingSettings rs = new RecordingSettings(this, name); 621 rs.with("enabled", "false"); 622 return rs; 623 } 624 625 /** 626 * Enables event. 627 * 628 * @param eventClass the event to enable, not {@code null} 629 * 630 * @throws IllegalArgumentException if {@code eventClass} is an abstract 631 * class or not a subclass of {@link Event} 632 * 633 * @return an event setting for further configuration, not {@code null} 634 */ 635 public EventSettings enable(Class<? extends Event> eventClass) { 636 Objects.requireNonNull(eventClass); 637 RecordingSettings rs = new RecordingSettings(this, eventClass); 638 rs.with("enabled", "true"); 639 return rs; 640 } 641 642 /** 643 * Disables event. 644 * 645 * @param eventClass the event to enable, not {@code null} 646 * 647 * @throws IllegalArgumentException if {@code eventClass} is an abstract 648 * class or not a subclass of {@link Event} 649 * 650 * @return an event setting for further configuration, not {@code null} 651 * 652 */ 653 public EventSettings disable(Class<? extends Event> eventClass) { 654 Objects.requireNonNull(eventClass); 655 RecordingSettings rs = new RecordingSettings(this, eventClass); 656 rs.with("enabled", "false"); 657 return rs; 658 } 659 660 // package private 661 PlatformRecording getInternal() { 662 return internal; 663 } 664 665 private void setSetting(String id, String value) { 666 Objects.requireNonNull(id); 667 Objects.requireNonNull(value); 668 internal.setSetting(id, value); 669 } 670 671 }