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.internal;
27 
28 import static jdk.jfr.internal.LogLevel.INFO;
29 import static jdk.jfr.internal.LogLevel.TRACE;
30 import static jdk.jfr.internal.LogLevel.WARN;
31 import static jdk.jfr.internal.LogTag.JFR;
32 import static jdk.jfr.internal.LogTag.JFR_SYSTEM;
33 
34 import java.io.IOException;
35 import java.security.AccessControlContext;
36 import java.security.AccessController;
37 import java.time.Duration;
38 import java.time.Instant;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.HashMap;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.Timer;
47 import java.util.TimerTask;
48 import java.util.concurrent.CopyOnWriteArrayList;
49 
50 import jdk.jfr.EventType;
51 import jdk.jfr.FlightRecorder;
52 import jdk.jfr.FlightRecorderListener;
53 import jdk.jfr.Recording;
54 import jdk.jfr.RecordingState;
55 import jdk.jfr.events.ActiveRecordingEvent;
56 import jdk.jfr.events.ActiveSettingEvent;
57 import jdk.jfr.internal.SecuritySupport.SecureRecorderListener;
58 import jdk.jfr.internal.instrument.JDKEvents;
59 
60 public final class PlatformRecorder {
61 
62     private final List<PlatformRecording> recordings = new ArrayList<>();
63     private final static List<SecureRecorderListener> changeListeners = new ArrayList<>();
64     private final Repository repository;
65     private final Timer timer;
66     private final static JVM jvm = JVM.getJVM();
67     private final EventType activeRecordingEvent;
68     private final EventType activeSettingEvent;
69     private final Thread shutdownHook;
70 
71     private long recordingCounter = 0;
72     private RepositoryChunk currentChunk;
73 
PlatformRecorder()74     public PlatformRecorder() throws Exception {
75         repository = Repository.getRepository();
76         Logger.log(JFR_SYSTEM, INFO, "Initialized disk repository");
77         repository.ensureRepository();
78         jvm.createNativeJFR();
79         Logger.log(JFR_SYSTEM, INFO, "Created native");
80         JDKEvents.initialize();
81         Logger.log(JFR_SYSTEM, INFO, "Registered JDK events");
82         JDKEvents.addInstrumentation();
83         startDiskMonitor();
84         activeRecordingEvent = EventType.getEventType(ActiveRecordingEvent.class);
85         activeSettingEvent = EventType.getEventType(ActiveSettingEvent.class);
86         shutdownHook = SecuritySupport.createThreadWitNoPermissions("JFR: Shutdown Hook", new ShutdownHook(this));
87         SecuritySupport.setUncaughtExceptionHandler(shutdownHook, new ShutdownHook.ExceptionHandler());
88         SecuritySupport.registerShutdownHook(shutdownHook);
89         timer = createTimer();
90     }
91 
92 
createTimer()93     private static Timer createTimer() {
94         try {
95             List<Timer> result = new CopyOnWriteArrayList<>();
96             Thread t = SecuritySupport.createThreadWitNoPermissions("Permissionless thread", ()-> {
97                 result.add(new Timer("JFR Recording Scheduler", true));
98             });
99             t.start();
100             t.join();
101             return result.get(0);
102         } catch (InterruptedException e) {
103             throw new IllegalStateException("Not able to create timer task. " + e.getMessage(), e);
104         }
105     }
106 
newRecording(Map<String, String> settings)107     public synchronized PlatformRecording newRecording(Map<String, String> settings) {
108         return newRecording(settings, ++recordingCounter);
109     }
110 
111     // To be used internally when doing dumps.
112     // Caller must have recorder lock and close recording before releasing lock
newTemporaryRecording()113     public PlatformRecording newTemporaryRecording() {
114         if(!Thread.holdsLock(this)) {
115             throw new InternalError("Caller must have recorder lock");
116         }
117         return newRecording(new HashMap<>(), 0);
118     }
119 
newRecording(Map<String, String> settings, long id)120     private synchronized PlatformRecording newRecording(Map<String, String> settings, long id) {
121         PlatformRecording recording = new PlatformRecording(this, id);
122         if (!settings.isEmpty()) {
123             recording.setSettings(settings);
124         }
125         recordings.add(recording);
126         return recording;
127     }
128 
finish(PlatformRecording recording)129     synchronized void finish(PlatformRecording recording) {
130         if (recording.getState() == RecordingState.RUNNING) {
131             recording.stop("Recording closed");
132         }
133         recordings.remove(recording);
134     }
135 
getRecordings()136     public synchronized List<PlatformRecording> getRecordings() {
137         return Collections.unmodifiableList(new ArrayList<PlatformRecording>(recordings));
138     }
139 
addListener(FlightRecorderListener changeListener)140     public synchronized static void addListener(FlightRecorderListener changeListener) {
141         AccessControlContext context = AccessController.getContext();
142         SecureRecorderListener sl = new SecureRecorderListener(context, changeListener);
143         boolean runInitialized;
144         synchronized (PlatformRecorder.class) {
145             runInitialized = FlightRecorder.isInitialized();
146             changeListeners.add(sl);
147         }
148         if (runInitialized) {
149             sl.recorderInitialized(FlightRecorder.getFlightRecorder());
150         }
151     }
152 
removeListener(FlightRecorderListener changeListener)153     public synchronized static boolean removeListener(FlightRecorderListener changeListener) {
154         for (SecureRecorderListener s : new ArrayList<>(changeListeners)) {
155             if (s.getChangeListener() == changeListener) {
156                 changeListeners.remove(s);
157                 return true;
158             }
159         }
160         return false;
161     }
162 
getListeners()163     static synchronized List<FlightRecorderListener> getListeners() {
164         return new ArrayList<>(changeListeners);
165     }
166 
getTimer()167     Timer getTimer() {
168         return timer;
169     }
170 
notifyRecorderInitialized(FlightRecorder recorder)171     public static void notifyRecorderInitialized(FlightRecorder recorder) {
172         Logger.log(JFR_SYSTEM, TRACE, "Notifying listeners that Flight Recorder is initialized");
173         for (FlightRecorderListener r : getListeners()) {
174             r.recorderInitialized(recorder);
175         }
176     }
177 
178     // called by shutdown hook
destroy()179     synchronized void destroy() {
180         try {
181             timer.cancel();
182         } catch (Exception ex) {
183             Logger.log(JFR_SYSTEM, WARN, "Shutdown hook could not cancel timer");
184         }
185 
186         for (PlatformRecording p : getRecordings()) {
187             if (p.getState() == RecordingState.RUNNING) {
188                 try {
189                     p.stop("Shutdown");
190                 } catch (Exception ex) {
191                     Logger.log(JFR, WARN, "Recording " + p.getName() + ":" + p.getId() + " could not be stopped");
192                 }
193             }
194         }
195 
196         JDKEvents.remove();
197 
198         if (jvm.hasNativeJFR()) {
199             if (jvm.isRecording()) {
200                 jvm.endRecording_();
201             }
202             jvm.destroyNativeJFR();
203         }
204         repository.clear();
205     }
206 
start(PlatformRecording recording)207     synchronized void start(PlatformRecording recording) {
208         // State can only be NEW or DELAYED because of previous checks
209         Instant now = Instant.now();
210         recording.setStartTime(now);
211         recording.updateTimer();
212         Duration duration = recording.getDuration();
213         if (duration != null) {
214             recording.setStopTime(now.plus(duration));
215         }
216         boolean toDisk = recording.isToDisk();
217         boolean beginPhysical = true;
218         for (PlatformRecording s : getRecordings()) {
219             if (s.getState() == RecordingState.RUNNING) {
220                 beginPhysical = false;
221                 if (s.isToDisk()) {
222                     toDisk = true;
223                 }
224             }
225         }
226         if (beginPhysical) {
227             RepositoryChunk newChunk = null;
228             if (toDisk) {
229                 newChunk = repository.newChunk(now);
230                 MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
231             } else {
232                 MetadataRepository.getInstance().setOutput(null);
233             }
234             currentChunk = newChunk;
235             jvm.beginRecording_();
236             recording.setState(RecordingState.RUNNING);
237             updateSettings();
238             writeMetaEvents();
239         } else {
240             RepositoryChunk newChunk = null;
241             if (toDisk) {
242                 newChunk = repository.newChunk(now);
243                 RequestEngine.doChunkEnd();
244                 MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
245             }
246             recording.setState(RecordingState.RUNNING);
247             updateSettings();
248             writeMetaEvents();
249             if (currentChunk != null) {
250                 finishChunk(currentChunk, now, recording);
251             }
252             currentChunk = newChunk;
253         }
254 
255         RequestEngine.doChunkBegin();
256     }
257 
stop(PlatformRecording recording)258     synchronized void stop(PlatformRecording recording) {
259         RecordingState state = recording.getState();
260 
261         if (Utils.isAfter(state, RecordingState.RUNNING)) {
262             throw new IllegalStateException("Can't stop an already stopped recording.");
263         }
264         if (Utils.isBefore(state, RecordingState.RUNNING)) {
265             throw new IllegalStateException("Recording must be started before it can be stopped.");
266         }
267         Instant now = Instant.now();
268         boolean toDisk = false;
269         boolean endPhysical = true;
270         for (PlatformRecording s : getRecordings()) {
271             RecordingState rs = s.getState();
272             if (s != recording && RecordingState.RUNNING == rs) {
273                 endPhysical = false;
274                 if (s.isToDisk()) {
275                     toDisk = true;
276                 }
277             }
278         }
279         OldObjectSample.emit(recording);
280 
281         if (endPhysical) {
282             RequestEngine.doChunkEnd();
283             if (recording.isToDisk()) {
284                 if (currentChunk != null) {
285                     MetadataRepository.getInstance().setOutput(null);
286                     finishChunk(currentChunk, now, null);
287                     currentChunk = null;
288                 }
289             } else {
290                 // last memory
291                 dumpMemoryToDestination(recording);
292             }
293             jvm.endRecording_();
294             disableEvents();
295         } else {
296             RepositoryChunk newChunk = null;
297             RequestEngine.doChunkEnd();
298             updateSettingsButIgnoreRecording(recording);
299             if (toDisk) {
300                 newChunk = repository.newChunk(now);
301                 MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
302             } else {
303                 MetadataRepository.getInstance().setOutput(null);
304             }
305             writeMetaEvents();
306             if (currentChunk != null) {
307                 finishChunk(currentChunk, now, null);
308             }
309             currentChunk = newChunk;
310             RequestEngine.doChunkBegin();
311         }
312         recording.setState(RecordingState.STOPPED);
313     }
314 
dumpMemoryToDestination(PlatformRecording recording)315     private void dumpMemoryToDestination(PlatformRecording recording)  {
316         WriteableUserPath dest = recording.getDestination();
317         if (dest != null) {
318             MetadataRepository.getInstance().setOutput(dest.getRealPathText());
319             recording.clearDestination();
320         }
321     }
disableEvents()322     private void disableEvents() {
323         MetadataRepository.getInstance().disableEvents();
324     }
325 
updateSettings()326     void updateSettings() {
327         updateSettingsButIgnoreRecording(null);
328     }
329 
updateSettingsButIgnoreRecording(PlatformRecording ignoreMe)330     void updateSettingsButIgnoreRecording(PlatformRecording ignoreMe) {
331         List<PlatformRecording> recordings = getRunningRecordings();
332         List<Map<String, String>> list = new ArrayList<>(recordings.size());
333         for (PlatformRecording r : recordings) {
334             if (r != ignoreMe) {
335                 list.add(r.getSettings());
336             }
337         }
338         MetadataRepository.getInstance().setSettings(list);
339     }
340 
rotateDisk()341     synchronized void rotateDisk() {
342         Instant now = Instant.now();
343         RepositoryChunk newChunk = repository.newChunk(now);
344         RequestEngine.doChunkEnd();
345         MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
346         writeMetaEvents();
347         if (currentChunk != null) {
348             finishChunk(currentChunk, now, null);
349         }
350         currentChunk = newChunk;
351         RequestEngine.doChunkBegin();
352     }
353 
getRunningRecordings()354     private List<PlatformRecording> getRunningRecordings() {
355         List<PlatformRecording> runningRecordings = new ArrayList<>();
356         for (PlatformRecording recording : getRecordings()) {
357             if (recording.getState() == RecordingState.RUNNING) {
358                 runningRecordings.add(recording);
359             }
360         }
361         return runningRecordings;
362     }
363 
makeChunkList(Instant startTime, Instant endTime)364     private List<RepositoryChunk> makeChunkList(Instant startTime, Instant endTime) {
365         Set<RepositoryChunk> chunkSet = new HashSet<>();
366         for (PlatformRecording r : getRecordings()) {
367             chunkSet.addAll(r.getChunks());
368         }
369         if (chunkSet.size() > 0) {
370             List<RepositoryChunk> chunks = new ArrayList<>(chunkSet.size());
371             for (RepositoryChunk rc : chunkSet) {
372                 if (rc.inInterval(startTime, endTime)) {
373                     chunks.add(rc);
374                 }
375             }
376             // n*log(n), should be able to do n*log(k) with a priority queue,
377             // where k = number of recordings, n = number of chunks
378             Collections.sort(chunks, RepositoryChunk.END_TIME_COMPARATOR);
379             return chunks;
380         }
381 
382         return Collections.emptyList();
383     }
384 
startDiskMonitor()385     private void startDiskMonitor() {
386         Thread t = SecuritySupport.createThreadWitNoPermissions("JFR Periodic Tasks", () -> periodicTask());
387         SecuritySupport.setDaemonThread(t, true);
388         t.start();
389     }
390 
finishChunk(RepositoryChunk chunk, Instant time, PlatformRecording ignoreMe)391     private void finishChunk(RepositoryChunk chunk, Instant time, PlatformRecording ignoreMe) {
392         chunk.finish(time);
393         for (PlatformRecording r : getRecordings()) {
394             if (r != ignoreMe && r.getState() == RecordingState.RUNNING) {
395                 r.appendChunk(chunk);
396             }
397         }
398     }
399 
writeMetaEvents()400     private void writeMetaEvents() {
401 
402         if (activeRecordingEvent.isEnabled()) {
403             for (PlatformRecording r : getRecordings()) {
404                 if (r.getState() == RecordingState.RUNNING && r.shouldWriteMetadataEvent()) {
405                     ActiveRecordingEvent event = new ActiveRecordingEvent();
406                     event.id = r.getId();
407                     event.name = r.getName();
408                     WriteableUserPath p = r.getDestination();
409                     event.destination = p == null ? null : p.getRealPathText();
410                     Duration d = r.getDuration();
411                     event.recordingDuration = d == null ? Long.MAX_VALUE : d.toMillis();
412                     Duration age = r.getMaxAge();
413                     event.maxAge = age == null ? Long.MAX_VALUE : age.toMillis();
414                     Long size = r.getMaxSize();
415                     event.maxSize = size == null ? Long.MAX_VALUE : size;
416                     Instant start = r.getStartTime();
417                     event.recordingStart = start == null ? Long.MAX_VALUE : start.toEpochMilli();
418                     event.commit();
419                 }
420             }
421         }
422         if (activeSettingEvent.isEnabled()) {
423             for (EventControl ec : MetadataRepository.getInstance().getEventControls()) {
424                 ec.writeActiveSettingEvent();
425             }
426         }
427     }
428 
periodicTask()429     private void periodicTask() {
430         if (!jvm.hasNativeJFR()) {
431             return;
432         }
433         while (true) {
434             synchronized (this) {
435                 if (jvm.shouldRotateDisk()) {
436                     rotateDisk();
437                 }
438             }
439             long minDelta = RequestEngine.doPeriodic();
440             long wait = Math.min(minDelta, Options.getWaitInterval());
441             takeNap(wait);
442         }
443     }
444 
takeNap(long duration)445     private void takeNap(long duration) {
446         try {
447             synchronized (JVM.FILE_DELTA_CHANGE) {
448                 JVM.FILE_DELTA_CHANGE.wait(duration < 10 ? 10 : duration);
449             }
450         } catch (InterruptedException e) {
451             e.printStackTrace();
452         }
453     }
454 
455     synchronized Recording newCopy(PlatformRecording r, boolean stop) {
456         Recording newRec = new Recording();
457         PlatformRecording copy = PrivateAccess.getInstance().getPlatformRecording(newRec);
458         copy.setSettings(r.getSettings());
459         copy.setMaxAge(r.getMaxAge());
460         copy.setMaxSize(r.getMaxSize());
461         copy.setDumpOnExit(r.getDumpOnExit());
462         copy.setName("Clone of " + r.getName());
463         copy.setToDisk(r.isToDisk());
464         copy.setInternalDuration(r.getDuration());
465         copy.setStartTime(r.getStartTime());
466         copy.setStopTime(r.getStopTime());
467 
468         if (r.getState() == RecordingState.NEW) {
469             return newRec;
470         }
471         if (r.getState() == RecordingState.DELAYED) {
472             copy.scheduleStart(r.getStartTime());
473             return newRec;
474         }
475         copy.setState(r.getState());
476         // recording has started, copy chunks
477         for (RepositoryChunk c : r.getChunks()) {
478             copy.add(c);
479         }
480         if (r.getState() == RecordingState.RUNNING) {
481             if (stop) {
482                 copy.stop("Stopped when cloning recording '" + r.getName() + "'");
483             } else {
484                 if (r.getStopTime() != null) {
485                     TimerTask stopTask = copy.createStopTask();
486                     copy.setStopTask(copy.createStopTask());
487                     getTimer().schedule(stopTask, r.getStopTime().toEpochMilli());
488                 }
489             }
490         }
491         return newRec;
492     }
493 
494     public synchronized void fillWithRecordedData(PlatformRecording target, Boolean pathToGcRoots) {
495         boolean running = false;
496         boolean toDisk = false;
497 
498         for (PlatformRecording r : recordings) {
499             if (r.getState() == RecordingState.RUNNING) {
500                 running = true;
501                 if (r.isToDisk()) {
502                     toDisk = true;
503                 }
504             }
505         }
506         // If needed, flush data from memory
507         if (running) {
508             if (toDisk) {
509                 OldObjectSample.emit(recordings, pathToGcRoots);
510                 rotateDisk();
511             } else {
512                 try (PlatformRecording snapshot = newTemporaryRecording()) {
513                     snapshot.setToDisk(true);
514                     snapshot.setShouldWriteActiveRecordingEvent(false);
515                     snapshot.start();
516                     OldObjectSample.emit(recordings, pathToGcRoots);
517                     snapshot.stop("Snapshot dump");
518                     fillWithDiskChunks(target);
519                 }
520                 return;
521             }
522         }
523         fillWithDiskChunks(target);
524     }
525 
526     private void fillWithDiskChunks(PlatformRecording target) {
527         for (RepositoryChunk c : makeChunkList(null, null)) {
528             target.add(c);
529         }
530         target.setState(RecordingState.STOPPED);
531         Instant startTime = null;
532         Instant endTime = null;
533 
534         for (RepositoryChunk c : target.getChunks()) {
535             if (startTime == null || c.getStartTime().isBefore(startTime)) {
536                 startTime = c.getStartTime();
537             }
538             if (endTime == null || c.getEndTime().isAfter(endTime)) {
539                 endTime = c.getEndTime();
540             }
541         }
542         Instant now = Instant.now();
543         if (startTime == null) {
544             startTime = now;
545         }
546         if (endTime == null) {
547             endTime = now;
548         }
549         target.setStartTime(startTime);
550         target.setStopTime(endTime);
551         target.setInternalDuration(Duration.between(startTime, endTime));
552     }
553 }
554