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