1 /*
2  * Copyright (c) 2016, 2020, 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.consumer;
27 
28 import java.io.Closeable;
29 import java.io.EOFException;
30 import java.io.File;
31 import java.io.IOException;
32 import java.nio.file.NoSuchFileException;
33 import java.nio.file.Path;
34 import java.util.ArrayList;
35 import java.util.HashSet;
36 import java.util.List;
37 
38 import jdk.jfr.EventType;
39 import jdk.jfr.internal.MetadataDescriptor;
40 import jdk.jfr.internal.Type;
41 import jdk.jfr.internal.consumer.ChunkHeader;
42 import jdk.jfr.internal.consumer.ChunkParser;
43 import jdk.jfr.internal.consumer.FileAccess;
44 import jdk.jfr.internal.consumer.RecordingInput;
45 
46 /**
47  * A recording file.
48  * <p>
49  * The following example shows how read and print all events in a recording file.
50  *
51  * <pre>{@literal
52  * try (RecordingFile recordingFile = new RecordingFile(Paths.get("recording.jfr"))) {
53  *   while (recordingFile.hasMoreEvents()) {
54  *     RecordedEvent event = recordingFile.readEvent();
55  *     System.out.println(event);
56  *   }
57  * }
58  * }</pre>
59  *
60  * @since 9
61  */
62 public final class RecordingFile implements Closeable {
63 
64     private boolean isLastEventInChunk;
65     private final File file;
66     private RecordingInput input;
67     private ChunkParser chunkParser;
68     private RecordedEvent nextEvent;
69     private boolean eof;
70 
71     /**
72      * Creates a recording file.
73      *
74      * @param file the path of the file to open, not {@code null}
75      * @throws IOException if it's not a valid recording file, or an I/O error
76      *         occurred
77      * @throws NoSuchFileException if the {@code file} can't be located
78      *
79      * @throws SecurityException if a security manager exists and its
80      *         {@code checkRead} method denies read access to the file.
81      */
RecordingFile(Path file)82     public RecordingFile(Path file) throws IOException {
83         this.file = file.toFile();
84         this.input = new RecordingInput(this.file, FileAccess.UNPRIVILEGED);
85         findNext();
86     }
87 
88     /**
89      * Reads the next event in the recording.
90      *
91      * @return the next event, not {@code null}
92      *
93      * @throws EOFException if no more events exist in the recording file
94      * @throws IOException if an I/O error occurs
95      *
96      * @see #hasMoreEvents()
97      */
readEvent()98     public RecordedEvent readEvent() throws IOException {
99         if (eof) {
100             ensureOpen();
101             throw new EOFException();
102         }
103         isLastEventInChunk = false;
104         RecordedEvent event = nextEvent;
105         nextEvent = chunkParser.readEvent();
106         while (nextEvent == ChunkParser.FLUSH_MARKER) {
107             nextEvent = chunkParser.readEvent();
108         }
109         if (nextEvent == null) {
110             isLastEventInChunk = true;
111             findNext();
112         }
113         return event;
114     }
115 
116     /**
117      * Returns {@code true} if unread events exist in the recording file,
118      * {@code false} otherwise.
119      *
120      * @return {@code true} if unread events exist in the recording, {@code false}
121      *         otherwise.
122      */
hasMoreEvents()123     public boolean hasMoreEvents() {
124         return !eof;
125     }
126 
127     /**
128      * Returns a list of all event types in this recording.
129      *
130      * @return a list of event types, not {@code null}
131      * @throws IOException if an I/O error occurred while reading from the file
132      *
133      * @see #hasMoreEvents()
134      */
readEventTypes()135     public List<EventType> readEventTypes() throws IOException {
136         ensureOpen();
137         MetadataDescriptor previous = null;
138         List<EventType> types = new ArrayList<>();
139         HashSet<Long> foundIds = new HashSet<>();
140         try (RecordingInput ri = new RecordingInput(file, FileAccess.UNPRIVILEGED)) {
141             ChunkHeader ch = new ChunkHeader(ri);
142             aggregateEventTypeForChunk(ch, null, types, foundIds);
143             while (!ch.isLastChunk()) {
144                 ch = ch.nextHeader();
145                 previous = aggregateEventTypeForChunk(ch, previous, types, foundIds);
146             }
147         }
148         return types;
149     }
150 
readTypes()151     List<Type> readTypes() throws IOException  {
152         ensureOpen();
153         MetadataDescriptor previous = null;
154         List<Type> types = new ArrayList<>();
155         HashSet<Long> foundIds = new HashSet<>();
156         try (RecordingInput ri = new RecordingInput(file, FileAccess.UNPRIVILEGED)) {
157             ChunkHeader ch = new ChunkHeader(ri);
158             ch.awaitFinished();
159             aggregateTypeForChunk(ch, null, types, foundIds);
160             while (!ch.isLastChunk()) {
161                 ch = ch.nextHeader();
162                 previous = aggregateTypeForChunk(ch, previous, types, foundIds);
163             }
164         }
165         return types;
166     }
167 
aggregateTypeForChunk(ChunkHeader ch, MetadataDescriptor previous, List<Type> types, HashSet<Long> foundIds)168     private MetadataDescriptor aggregateTypeForChunk(ChunkHeader ch, MetadataDescriptor previous, List<Type> types, HashSet<Long> foundIds) throws IOException {
169         MetadataDescriptor m = ch.readMetadata(previous);
170         for (Type t : m.getTypes()) {
171             if (!foundIds.contains(t.getId())) {
172                 types.add(t);
173                 foundIds.add(t.getId());
174             }
175         }
176         return m;
177     }
178 
aggregateEventTypeForChunk(ChunkHeader ch, MetadataDescriptor previous, List<EventType> types, HashSet<Long> foundIds)179     private static MetadataDescriptor aggregateEventTypeForChunk(ChunkHeader ch,  MetadataDescriptor previous, List<EventType> types, HashSet<Long> foundIds) throws IOException {
180         MetadataDescriptor m = ch.readMetadata(previous);
181         for (EventType t : m.getEventTypes()) {
182             if (!foundIds.contains(t.getId())) {
183                 types.add(t);
184                 foundIds.add(t.getId());
185             }
186         }
187         return m;
188     }
189 
190     /**
191      * Closes this recording file and releases any system resources that are
192      * associated with it.
193      *
194      * @throws IOException if an I/O error occurred
195      */
196     @Override
close()197     public void close() throws IOException {
198         if (input != null) {
199             eof = true;
200             input.close();
201             chunkParser = null;
202             input = null;
203             nextEvent = null;
204         }
205     }
206 
207     /**
208      * Returns a list of all events in a file.
209      * <p>
210      * This method is intended for simple cases where it's convenient to read all
211      * events in a single operation. It isn't intended for reading large files.
212      *
213      * @param path the path to the file, not {@code null}
214      *
215      * @return the events from the file as a {@code List} object; whether the
216      *         {@code List} is modifiable or not is implementation dependent and
217      *         therefore not specified, not {@code null}
218      *
219      * @throws IOException if an I/O error occurred, it's not a Flight Recorder
220      *         file or a version of a JFR file that can't be parsed
221      *
222      * @throws SecurityException if a security manager exists and its
223      *         {@code checkRead} method denies read access to the file.
224      */
readAllEvents(Path path)225     public static List<RecordedEvent> readAllEvents(Path path) throws IOException {
226         try (RecordingFile r = new RecordingFile(path)) {
227             List<RecordedEvent> list = new ArrayList<>();
228             while (r.hasMoreEvents()) {
229                 list.add(r.readEvent());
230             }
231             return list;
232         }
233     }
234 
235     // package protected
getFile()236     File getFile() {
237         return file;
238     }
239 
240     // package protected
isLastEventInChunk()241     boolean isLastEventInChunk() {
242         return isLastEventInChunk;
243     }
244 
245 
246     // either sets next to an event or sets eof to true
findNext()247     private void findNext() throws IOException {
248         while (nextEvent == null) {
249             if (chunkParser == null) {
250                 chunkParser = new ChunkParser(input);
251             } else if (!chunkParser.isLastChunk()) {
252                 chunkParser = chunkParser.nextChunkParser();
253             } else {
254                 eof = true;
255                 return;
256             }
257             nextEvent = chunkParser.readEvent();
258             while (nextEvent == ChunkParser.FLUSH_MARKER) {
259                 nextEvent = chunkParser.readEvent();
260             }
261         }
262     }
263 
ensureOpen()264     private void ensureOpen() throws IOException {
265         if (input == null) {
266             throw new IOException("Stream Closed");
267         }
268     }
269 }
270