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 
  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 
  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 
 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
 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 
 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 
 129     synchronized void finish(PlatformRecording recording) {
 130         if (recording.getState() == RecordingState.RUNNING) {
 131             recording.stop("Recording closed");
 132         }
 133         recordings.remove(recording);
 134     }
 135 
 136     public synchronized List<PlatformRecording> getRecordings() {
 137         return Collections.unmodifiableList(new ArrayList<PlatformRecording>(recordings));
 138     }
 139 
 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 
 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 
 163     static synchronized List<FlightRecorderListener> getListeners() {
 164         return new ArrayList<>(changeListeners);
 165     }
 166 
 167     Timer getTimer() {
 168         return timer;
 169     }
 170 
 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
 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 
 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 
 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 
 315     private void dumpMemoryToDestination(PlatformRecording recording)  {
 316         WriteableUserPath dest = recording.getDestination();
 317         if (dest != null) {
 318             MetadataRepository.getInstance().setOutput(dest.getText());
 319             recording.clearDestination();
 320         }
 321     }
 322     private void disableEvents() {
 323         MetadataRepository.getInstance().disableEvents();
 324     }
 325 
 326     void updateSettings() {
 327         updateSettingsButIgnoreRecording(null);
 328     }
 329 
 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 
 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 
 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 
 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 
 385     private void startDiskMonitor() {
 386         Thread t = SecuritySupport.createThreadWitNoPermissions("JFR Periodic Tasks", () -> periodicTask());
 387         SecuritySupport.setDaemonThread(t, true);
 388         t.start();
 389     }
 390 
 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 
 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.getText();
 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 
 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 
 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 }