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