1 /*
   2  * Copyright (c) 2016, 2018, 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.HashMap;
  35 import java.util.Map;
  36 import java.util.Objects;
  37 
  38 import jdk.jfr.internal.PlatformRecorder;
  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  * The following example shows how configure, start, stop and dump recording data to disk.
  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     public Recording(Map<String, String> settings) {
  97         PlatformRecorder r = FlightRecorder.getFlightRecorder().getInternal();
  98         synchronized (r) {
  99             this.internal = r.newRecording(settings);
 100             this.internal.setRecording(this);
 101             if (internal.getRecording() != this) {
 102                 throw new InternalError("Internal recording not properly setup");
 103             }
 104         }
 105     }
 106 
 107     /**
 108      * Creates a recording without any settings.
 109      * <p>
 110      * A newly created recording is in the {@link RecordingState#NEW} state. To start
 111      * the recording, invoke the {@link Recording#start()} method.
 112      *
 113      * @throws IllegalStateException if Flight Recorder can't be created (for
 114      *         example, if the Java Virtual Machine (JVM) lacks Flight Recorder
 115      *         support, or if the file repository can't be created or accessed)
 116      *
 117      * @throws SecurityException If a security manager is used and
 118      *         FlightRecorderPermission "accessFlightRecorder" is not set.
 119      */
 120     public Recording() {
 121         this(new HashMap<String, String>());
 122      }
 123 
 124     /**
 125      * Creates a recording with settings from a configuration.
 126      * <p>
 127      * The following example shows how create a recording that uses a predefined configuration.
 128      *
 129      * <pre>
 130      * <code>
 131      * Recording r = new Recording(Configuration.getConfiguration("default"));
 132      * </code>
 133      * </pre>
 134      *
 135      * The newly created recording is in the {@link RecordingState#NEW} state. To
 136      * start the recording, invoke the {@link Recording#start()} method.
 137      *
 138      * @param configuration configuration that contains the settings to be use, not
 139      *        {@code null}
 140      *
 141      * @throws IllegalStateException if Flight Recorder can't be created (for
 142      *         example, if the Java Virtual Machine (JVM) lacks Flight Recorder
 143      *         support, or if the file repository can't be created or accessed)
 144      *
 145      * @throws SecurityException if a security manager is used and
 146      *         FlightRecorderPermission "accessFlightRecorder" is not set.
 147      *
 148      * @see Configuration
 149      */
 150     public Recording(Configuration configuration) {
 151         this(configuration.getSettings());
 152     }
 153 
 154     /**
 155      * Starts this recording.
 156      * <p>
 157      * It's recommended that the recording options and event settings are configured
 158      * before calling this method. The benefits of doing so are a more consistent
 159      * state when analyzing the recorded data, and improved performance because the
 160      * configuration can be applied atomically.
 161      * <p>
 162      * After a successful invocation of this method, this recording is in the
 163      * {@code RUNNING} state.
 164      *
 165      * @throws IllegalStateException if recording is already started or is in the
 166      *         {@code CLOSED} state
 167      */
 168     public void start() {
 169         internal.start();
 170     }
 171 
 172     /**
 173      * Starts this recording after a delay.
 174      * <p>
 175      * After a successful invocation of this method, this recording is in the
 176      * {@code DELAYED} state.
 177      *
 178      * @param delay the time to wait before starting this recording, not
 179      *        {@code null}
 180      * @throws IllegalStateException if the recording is not it the {@code NEW} state
 181      */
 182     public void scheduleStart(Duration delay) {
 183         Objects.requireNonNull(delay);
 184         internal.scheduleStart(delay);
 185     }
 186 
 187     /**
 188      * Stops this recording.
 189      * <p>
 190      * When a recording is stopped it can't be restarted. If this
 191      * recording has a destination, data is written to that destination and
 192      * the recording is closed. After a recording is closed, the data is no longer
 193      * available.
 194      * <p>
 195      * After a successful invocation of this method, this recording will be
 196      * in the {@code STOPPED} state.
 197      *
 198      * @return {@code true} if recording is stopped, {@code false} otherwise
 199      *
 200      * @throws IllegalStateException if the recording is not started or is already stopped
 201      *
 202      * @throws SecurityException if a security manager exists and the caller
 203      *         doesn't have {@code FilePermission} to write to the destination
 204      *         path
 205      *
 206      * @see #setDestination(Path)
 207      *
 208      */
 209     public boolean stop() {
 210         return internal.stop("Stopped by user");
 211     }
 212 
 213     /**
 214      * Returns settings used by this recording.
 215      * <p>
 216      * Modifying the returned {@code Map} will not change the settings for this recording.
 217      * <p>
 218      * If no settings are set for this recording, an empty {@code Map} is
 219      * returned.
 220      *
 221      * @return recording settings, not {@code null}
 222      */
 223     public Map<String, String> getSettings() {
 224         return new HashMap<>(internal.getSettings());
 225     }
 226 
 227     /**
 228      * Returns the current size of this recording in the disk repository,
 229      * measured in bytes.
 230      * <p>
 231      * The size is updated when recording buffers are flushed. If the recording is
 232      * not written to the disk repository the returned size is always {@code 0}.
 233      *
 234      * @return amount of recorded data, measured in bytes, or {@code 0} if the
 235      *         recording is not written to the disk repository
 236      */
 237     public long getSize() {
 238         return internal.getSize();
 239     }
 240 
 241     /**
 242      * Returns the time when this recording was stopped.
 243      *
 244      * @return the time, or {@code null} if this recording is not stopped
 245      */
 246     public Instant getStopTime() {
 247         return internal.getStopTime();
 248     }
 249 
 250     /**
 251      * Returns the time when this recording was started.
 252      *
 253      * @return the the time, or {@code null} if this recording is not started
 254      */
 255     public Instant getStartTime() {
 256         return internal.getStartTime();
 257     }
 258 
 259     /**
 260      * Returns the maximum size, measured in bytes, at which data is no longer kept in the disk repository.
 261      *
 262      * @return maximum size in bytes, or {@code 0} if no maximum size is set
 263      */
 264     public long getMaxSize() {
 265         return internal.getMaxSize();
 266     }
 267 
 268     /**
 269      * Returns the length of time that the data is kept in the disk repository
 270      * before it is removed.
 271      *
 272      * @return maximum length of time, or {@code null} if no maximum length of time
 273      *         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 the 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      * The following example shows how to set event settings for a recording.
 294      *
 295      * <pre>
 296      * <code>
 297      *     Map{@literal <}String, String{@literal >} settings = new HashMap{@literal <}{@literal >}();
 298      *     settings.putAll(EventSettings.enabled("jdk.CPUSample").withPeriod(Duration.ofSeconds(2)).toMap());
 299      *     settings.putAll(EventSettings.enabled(MyEvent.class).withThreshold(Duration.ofSeconds(2)).withoutStackTrace().toMap());
 300      *     settings.put("jdk.ExecutionSample#period", "10 ms");
 301      *     recording.setSettings(settings);
 302      * </code>
 303      * </pre>
 304      *
 305      * The following example shows how to merge settings.
 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 recording state that 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 that is associated with this recording.
 336      * <p>
 337      * After a successful invocation of this method, this recording is in the
 338      * {@code CLOSED} state.
 339      */
 340     @Override
 341     public void close() {
 342         internal.close();
 343     }
 344 
 345     /**
 346      * Returns a clone of this recording, 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 copy is constrained
 350      * with the {@link #setMaxAge(Duration)} method and the {@link #setMaxSize(long)}method.
 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 the location where recording data is written, not
 366      *        {@code null}
 367      *
 368      * @throws IOException if the recording can't be copied to the specified
 369      *         location
 370      *
 371      * @throws SecurityException if a security manager exists and the caller doesn't
 372      *         have {@code FilePermission} to write to the destination path
 373      */
 374     public void dump(Path destination) throws IOException {
 375         Objects.requireNonNull(destination);
 376         internal.dump(new WriteableUserPath(destination));
 377 
 378     }
 379 
 380     /**
 381      * Returns {@code true} if this recording uses the disk repository, {@code false} otherwise.
 382      * <p>
 383      * If no value is set, {@code true} is returned.
 384      *
 385      * @return {@code true} if the recording uses the disk repository, {@code false}
 386      *         otherwise
 387      */
 388     public boolean isToDisk() {
 389         return internal.isToDisk();
 390     }
 391 
 392     /**
 393      * Determines how much data is kept in the disk repository.
 394      * <p>
 395      * To control the amount of recording data that is stored on disk, the maximum
 396      * amount of data to retain can be specified. When the maximum limit is
 397      * exceeded, the Java Virtual Machine (JVM) removes the oldest chunk to make
 398      * room for a more recent chunk.
 399      * <p>
 400      * If neither maximum limit or the maximum age is set, the size of the
 401      * recording may grow indefinitely.
 402      *
 403      * @param maxSize the amount of data to retain, {@code 0} if infinite
 404      *
 405      * @throws IllegalArgumentException if <code>maxSize</code> is negative
 406      *
 407      * @throws IllegalStateException if the recording is in {@code CLOSED} state
 408      */
 409     public void setMaxSize(long maxSize) {
 410         if (maxSize < 0) {
 411             throw new IllegalArgumentException("Max size of recording can't be negative");
 412         }
 413         internal.setMaxSize(maxSize);
 414     }
 415 
 416     /**
 417      * Determines how far back data is kept in the disk repository.
 418      * <p>
 419      * To control the amount of recording data stored on disk, the maximum length of
 420      * time to retain the data can be specified. Data stored on disk that is older
 421      * than the specified length of time is removed by the Java Virtual Machine (JVM).
 422      * <p>
 423      * If neither maximum limit or the maximum age is set, the size of the
 424      * recording may grow indefinitely.
 425      *
 426      * @param maxAge the length of time that data is kept, or {@code null} if infinite
 427      *
 428      * @throws IllegalArgumentException if <code>maxAge</code> is negative
 429      *
 430      * @throws IllegalStateException if the recording is in the {@code CLOSED} state
 431      */
 432     public void setMaxAge(Duration maxAge) {
 433         if (maxAge != null && maxAge.isNegative()) {
 434             throw new IllegalArgumentException("Max age of recording can't be negative");
 435         }
 436         internal.setMaxAge(maxAge);
 437     }
 438 
 439     /**
 440      * Sets a location where data is written on recording stop, or
 441      * {@code null} if data is not to be dumped.
 442      * <p>
 443      * If a destination is set, this recording is automatically closed
 444      * after data is successfully copied to the destination path.
 445      * <p>
 446      * If a destination is <em>not</em> set, Flight Recorder retains the
 447      * recording data until this recording is closed. Use the {@link #dump(Path)} method to
 448      * manually write data to a file.
 449      *
 450      * @param destination the destination path, or {@code null} if recording should
 451      *        not be dumped at stop
 452      *
 453      * @throws IllegalStateException if recording is in the {@code STOPPED} or
 454      *         {@code CLOSED} state.
 455      *
 456      * @throws SecurityException if a security manager exists and the caller
 457      *         doesn't have {@code FilePermission} to read, write, and delete the
 458      *         {@code destination} file
 459      *
 460      * @throws IOException if the path is not writable
 461      */
 462     public void setDestination(Path destination) throws IOException {
 463         internal.setDestination(destination != null ? new WriteableUserPath(destination) : null);
 464     }
 465 
 466     /**
 467      * Returns the destination file, where recording data is written when the
 468      * recording stops, or {@code null} if no destination is set.
 469      *
 470      * @return the destination file, or {@code null} if not set.
 471      */
 472     public Path getDestination() {
 473         WriteableUserPath usp = internal.getDestination();
 474         if (usp == null) {
 475             return null;
 476         } else {
 477             return usp.getPotentiallyMaliciousOriginal();
 478         }
 479     }
 480 
 481     /**
 482      * Returns a unique ID for this recording.
 483      *
 484      * @return the recording ID
 485      */
 486     public long getId() {
 487         return internal.getId();
 488     }
 489 
 490     /**
 491      * Sets a human-readable name (for example, {@code "My Recording"}).
 492      *
 493      * @param name the recording name, not {@code null}
 494      *
 495      * @throws IllegalStateException if the recording is in {@code CLOSED} state
 496      */
 497     public void setName(String name) {
 498         Objects.requireNonNull(name);
 499         internal.setName(name);
 500     }
 501 
 502     /**
 503      * Sets whether this recording is dumped to disk when the JVM exits.
 504      *
 505      * @param dumpOnExit if this recording should be dumped when the JVM exits
 506      */
 507     public void setDumpOnExit(boolean dumpOnExit) {
 508         internal.setDumpOnExit(dumpOnExit);
 509     }
 510 
 511     /**
 512      * Returns whether this recording is dumped to disk when the JVM exits.
 513      * <p>
 514      * If dump on exit is not set, {@code false} is returned.
 515      *
 516      * @return {@code true} if the recording is dumped on exit, {@code false}
 517      *         otherwise.
 518      */
 519     public boolean getDumpOnExit() {
 520         return internal.getDumpOnExit();
 521     }
 522 
 523     /**
 524      * Determines whether this recording is continuously flushed to the disk
 525      * repository or data is constrained to what is available in memory buffers.
 526      *
 527      * @param disk {@code true} if this recording is written to disk,
 528      *        {@code false} if in-memory
 529      *
 530      */
 531     public void setToDisk(boolean disk) {
 532         internal.setToDisk(disk);
 533     }
 534 
 535     /**
 536      * Creates a data stream for a specified interval.
 537      * <p>
 538      * The stream may contain some data outside the specified range.
 539      *
 540      * @param the start start time for the stream, or {@code null} to get data from
 541      *        start time of the recording
 542      *
 543      * @param the end end time for the stream, or {@code null} to get data until the
 544      *        present time.
 545      *
 546      * @return an input stream, or {@code null} if no data is available in the
 547      *         interval.
 548      *
 549      * @throws IllegalArgumentException if {@code end} happens before
 550      *         {@code start}
 551      *
 552      * @throws IOException if a stream can't be opened
 553      */
 554     public InputStream getStream(Instant start, Instant end) throws IOException {
 555         if (start != null && end != null && end.isBefore(start)) {
 556             throw new IllegalArgumentException("End time of requested stream must not be before start time");
 557         }
 558         return internal.open(start, end);
 559     }
 560 
 561     /**
 562      * Returns the specified duration for this recording, or {@code null} if no
 563      * duration is set.
 564      * <p>
 565      * The duration can be set only when the recording is in the
 566      * {@link RecordingState#NEW} state.
 567      *
 568      * @return the desired duration of the recording, or {@code null} if no duration
 569      *         has been set.
 570      */
 571     public Duration getDuration() {
 572         return internal.getDuration();
 573     }
 574 
 575     /**
 576      * Sets a duration for how long a recording runs before it stops.
 577      * <p>
 578      * By default, a recording has no duration ({@code null}).
 579      *
 580      * @param duration the duration, or {@code null} if no duration is set
 581      *
 582      * @throws IllegalStateException if recording is in the {@code STOPPED} or {@code CLOSED} state
 583      */
 584     public void setDuration(Duration duration) {
 585         internal.setDuration(duration);
 586     }
 587 
 588     /**
 589      * Enables the event with the specified name.
 590      * <p>
 591      * If multiple events have the same name (for example, the same class is loaded
 592      * in different class loaders), then all events that match the name are enabled. To
 593      * enable a specific class, use the {@link #enable(Class)} method or a {@code String}
 594      * representation of the event type ID.
 595      *
 596      * @param name the settings for the event, not {@code null}
 597      *
 598      * @return an event setting for further configuration, not {@code null}
 599      *
 600      * @see EventType
 601      */
 602     public EventSettings enable(String name) {
 603         Objects.requireNonNull(name);
 604         RecordingSettings rs = new RecordingSettings(this, name);
 605         rs.with("enabled", "true");
 606         return rs;
 607     }
 608 
 609     /**
 610      * Disables event with the specified name.
 611      * <p>
 612      * If multiple events with same name (for example, the same class is loaded
 613      * in different class loaders), then all events that match the
 614      * name is disabled. To disable a specific class, use the
 615      * {@link #disable(Class)} method or a {@code String} representation of the event
 616      * type ID.
 617      *
 618      * @param name the settings for the event, not {@code null}
 619      *
 620      * @return an event setting for further configuration, not {@code null}
 621      *
 622      */
 623     public EventSettings disable(String name) {
 624         Objects.requireNonNull(name);
 625         RecordingSettings rs = new RecordingSettings(this, name);
 626         rs.with("enabled", "false");
 627         return rs;
 628     }
 629 
 630     /**
 631      * Enables event.
 632      *
 633      * @param eventClass the event to enable, not {@code null}
 634      *
 635      * @throws IllegalArgumentException if {@code eventClass} is an abstract
 636      *         class or not a subclass of {@link Event}
 637      *
 638      * @return an event setting for further configuration, not {@code null}
 639      */
 640     public EventSettings enable(Class<? extends Event> eventClass) {
 641         Objects.requireNonNull(eventClass);
 642         RecordingSettings rs = new RecordingSettings(this, eventClass);
 643         rs.with("enabled", "true");
 644         return rs;
 645     }
 646 
 647     /**
 648      * Disables event.
 649      *
 650      * @param eventClass the event to enable, not {@code null}
 651      *
 652      * @throws IllegalArgumentException if {@code eventClass} is an abstract
 653      *         class or not a subclass of {@link Event}
 654      *
 655      * @return an event setting for further configuration, not {@code null}
 656      *
 657      */
 658     public EventSettings disable(Class<? extends Event> eventClass) {
 659         Objects.requireNonNull(eventClass);
 660         RecordingSettings rs = new RecordingSettings(this, eventClass);
 661         rs.with("enabled", "false");
 662         return rs;
 663     }
 664 
 665     // package private
 666     PlatformRecording getInternal() {
 667         return internal;
 668     }
 669 
 670     private void setSetting(String id, String value) {
 671         Objects.requireNonNull(id);
 672         Objects.requireNonNull(value);
 673         internal.setSetting(id, value);
 674     }
 675 
 676 }