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.dump(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 
RecordingSettings(Recording r, String identifier)70         RecordingSettings(Recording r, String identifier) {
71             this.recording = r;
72             this.identifier = identifier;
73         }
74 
RecordingSettings(Recording r, Class<? extends Event> eventClass)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
with(String name, String value)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
toMap()89         public Map<String, String> toMap() {
90             return recording.getSettings();
91         }
92     }
93 
94     private final PlatformRecording internal;
95 
Recording(Map<String, String> settings)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      */
Recording()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      */
Recording(Configuration configuration)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      */
start()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      */
scheduleStart(Duration delay)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      */
stop()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      */
getSettings()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      */
getSize()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      */
getStopTime()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      */
getStartTime()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      */
getMaxSize()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      */
getMaxAge()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      */
getName()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      */
setSettings(Map<String, String> settings)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      */
getState()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
close()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      */
copy(boolean stop)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      */
dump(Path destination)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      */
isToDisk()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      */
setMaxSize(long maxSize)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      */
setMaxAge(Duration maxAge)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      */
setDestination(Path destination)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      */
getDestination()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      */
getId()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      */
setName(String name)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      */
setDumpOnExit(boolean dumpOnExit)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      */
getDumpOnExit()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      */
setToDisk(boolean disk)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      */
getStream(Instant start, Instant end)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      */
getDuration()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      */
setDuration(Duration duration)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      */
enable(String name)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      */
disable(String name)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      */
enable(Class<? extends Event> eventClass)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      */
disable(Class<? extends Event> eventClass)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
getInternal()666     PlatformRecording getInternal() {
667         return internal;
668     }
669 
setSetting(String id, String value)670     private void setSetting(String id, String value) {
671         Objects.requireNonNull(id);
672         Objects.requireNonNull(value);
673         internal.setSetting(id, value);
674     }
675 
676 }
677