--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecorder.java Tue May 15 20:24:34 2018 +0200
@@ -0,0 +1,563 @@
+/*
+ * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.jfr.internal;
+
+import static jdk.jfr.internal.LogLevel.INFO;
+import static jdk.jfr.internal.LogLevel.TRACE;
+import static jdk.jfr.internal.LogLevel.WARN;
+import static jdk.jfr.internal.LogTag.JFR;
+import static jdk.jfr.internal.LogTag.JFR_SYSTEM;
+
+import java.io.IOException;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import jdk.jfr.Enabled;
+import jdk.jfr.EventType;
+import jdk.jfr.FlightRecorder;
+import jdk.jfr.FlightRecorderListener;
+import jdk.jfr.Recording;
+import jdk.jfr.RecordingState;
+import jdk.jfr.events.ActiveRecordingEvent;
+import jdk.jfr.events.ActiveSettingEvent;
+import jdk.jfr.internal.SecuritySupport.SecureRecorderListener;
+import jdk.jfr.internal.instrument.JDKEvents;
+import jdk.jfr.internal.settings.CutoffSetting;
+import jdk.jfr.internal.test.WhiteBox;
+
+public final class PlatformRecorder {
+
+ private final List<PlatformRecording> recordings = new ArrayList<>();
+ private final static List<SecureRecorderListener> changeListeners = new ArrayList<>();
+ private final Repository repository;
+ private final Timer timer;
+ private final static JVM jvm = JVM.getJVM();
+ private final EventType activeRecordingEvent;
+ private final EventType activeSettingEvent;
+ private final Thread shutdownHook;
+
+ private long recordingCounter = 0;
+ private RepositoryChunk currentChunk;
+
+ public PlatformRecorder() throws Exception {
+ repository = Repository.getRepository();
+ Logger.log(JFR_SYSTEM, INFO, "Initialized disk repository");
+ repository.ensureRepository();
+ jvm.createNativeJFR();
+ Logger.log(JFR_SYSTEM, INFO, "Created native");
+ JDKEvents.initialize();
+ Logger.log(JFR_SYSTEM, INFO, "Registered JDK events");
+ JDKEvents.addInstrumentation();
+ startDiskMonitor();
+ SecuritySupport.registerEvent(ActiveRecordingEvent.class);
+ activeRecordingEvent = EventType.getEventType(ActiveRecordingEvent.class);
+ SecuritySupport.registerEvent(ActiveSettingEvent.class);
+ activeSettingEvent = EventType.getEventType(ActiveSettingEvent.class);
+ shutdownHook = SecuritySupport.createThreadWitNoPermissions("JFR: Shutdown Hook", new ShutdownHook(this));
+ SecuritySupport.setUncaughtExceptionHandler(shutdownHook, new ShutdownHook.ExceptionHandler());
+ SecuritySupport.registerShutdownHook(shutdownHook);
+ timer = createTimer();
+ }
+
+ private static Timer createTimer() {
+ try {
+ List<Timer> result = new CopyOnWriteArrayList<>();
+ Thread t = SecuritySupport.createThreadWitNoPermissions("Permissionless thread", ()-> {
+ result.add(new Timer("JFR Recording Scheduler", true));
+ });
+ t.start();
+ t.join();
+ return result.get(0);
+ } catch (InterruptedException e) {
+ throw new IllegalStateException("Not able to create timer task. " + e.getMessage(), e);
+ }
+ }
+
+ public synchronized PlatformRecording newRecording(Map<String, String> settings) {
+ PlatformRecording recording = new PlatformRecording(this, ++recordingCounter);
+ if (!settings.isEmpty()) {
+ recording.setSettings(settings);
+ }
+ recordings.add(recording);
+ return recording;
+ }
+
+ synchronized void finish(PlatformRecording recording) {
+ if (recording.getState() == RecordingState.RUNNING) {
+ recording.stop("Recording closed");
+ }
+ recordings.remove(recording);
+ }
+
+ public synchronized List<PlatformRecording> getRecordings() {
+ return Collections.unmodifiableList(new ArrayList<PlatformRecording>(recordings));
+ }
+
+ public synchronized static void addListener(FlightRecorderListener changeListener) {
+ AccessControlContext context = AccessController.getContext();
+ SecureRecorderListener sl = new SecureRecorderListener(context, changeListener);
+ boolean runInitialized;
+ synchronized (PlatformRecorder.class) {
+ runInitialized = FlightRecorder.isInitialized();
+ changeListeners.add(sl);
+ }
+ if (runInitialized) {
+ sl.recorderInitialized(FlightRecorder.getFlightRecorder());
+ }
+ }
+
+ public synchronized static boolean removeListener(FlightRecorderListener changeListener) {
+ for (SecureRecorderListener s : new ArrayList<>(changeListeners)) {
+ if (s.getChangeListener() == changeListener) {
+ changeListeners.remove(s);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static synchronized List<FlightRecorderListener> getListeners() {
+ return new ArrayList<>(changeListeners);
+ }
+
+ Timer getTimer() {
+ return timer;
+ }
+
+ public static void notifyRecorderInitialized(FlightRecorder recorder) {
+ Logger.log(JFR_SYSTEM, TRACE, "Notifying listeners that Flight Recorder is initialized");
+ for (FlightRecorderListener r : getListeners()) {
+ r.recorderInitialized(recorder);
+ }
+ }
+
+ // called by shutdown hook
+ synchronized void destroy() {
+ try {
+ timer.cancel();
+ } catch (Exception ex) {
+ Logger.log(JFR_SYSTEM, WARN, "Shutdown hook could not cancel timer");
+ }
+
+ for (PlatformRecording p : getRecordings()) {
+ if (p.getState() == RecordingState.RUNNING) {
+ try {
+ p.stop("Shutdown");
+ } catch (Exception ex) {
+ Logger.log(JFR, WARN, "Recording " + p.getName() + ":" + p.getId() + " could not be stopped");
+ }
+ }
+ }
+
+ JDKEvents.remove();
+
+ if (jvm.hasNativeJFR()) {
+ if (jvm.isRecording()) {
+ jvm.endRecording_();
+ }
+ jvm.destroyNativeJFR();
+ }
+ repository.clear();
+ }
+
+ synchronized void start(PlatformRecording recording) {
+ // State can only be NEW or DELAYED because of previous checks
+ Instant now = Instant.now();
+ recording.setStartTime(now);
+ recording.updateTimer();
+ Duration duration = recording.getDuration();
+ if (duration != null) {
+ recording.setStopTime(now.plus(duration));
+ }
+ boolean toDisk = recording.isToDisk();
+ boolean beginPhysical = true;
+ for (PlatformRecording s : getRecordings()) {
+ if (s.getState() == RecordingState.RUNNING) {
+ beginPhysical = false;
+ if (s.isToDisk()) {
+ toDisk = true;
+ }
+ }
+ }
+ if (beginPhysical) {
+ RepositoryChunk newChunk = null;
+ if (toDisk) {
+ newChunk = repository.newChunk(now);
+ MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
+ } else {
+ MetadataRepository.getInstance().setOutput(null);
+ }
+ currentChunk = newChunk;
+ jvm.beginRecording_();
+ recording.setState(RecordingState.RUNNING);
+ updateSettings();
+ writeMetaEvents();
+ } else {
+ RepositoryChunk newChunk = null;
+ if (toDisk) {
+ newChunk = repository.newChunk(now);
+ RequestEngine.doChunkEnd();
+ MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
+ }
+ recording.setState(RecordingState.RUNNING);
+ updateSettings();
+ writeMetaEvents();
+ if (currentChunk != null) {
+ finishChunk(currentChunk, now, recording);
+ }
+ currentChunk = newChunk;
+ }
+
+ RequestEngine.doChunkBegin();
+ }
+
+ synchronized void stop(PlatformRecording recording) {
+ RecordingState state = recording.getState();
+
+ if (Utils.isAfter(state, RecordingState.RUNNING)) {
+ throw new IllegalStateException("Can't stop an already stopped recording.");
+ }
+ if (Utils.isBefore(state, RecordingState.RUNNING)) {
+ throw new IllegalStateException("Recording must be started before it can be stopped.");
+ }
+ Instant now = Instant.now();
+ boolean toDisk = false;
+ boolean endPhysical = true;
+ for (PlatformRecording s : getRecordings()) {
+ RecordingState rs = s.getState();
+ if (s != recording && RecordingState.RUNNING == rs) {
+ endPhysical = false;
+ if (s.isToDisk()) {
+ toDisk = true;
+ }
+ }
+ }
+ emitOldObjectSamples(recording);
+
+ if (endPhysical) {
+ RequestEngine.doChunkEnd();
+ if (recording.isToDisk()) {
+ if (currentChunk != null) {
+ MetadataRepository.getInstance().setOutput(null);
+ finishChunk(currentChunk, now, null);
+ currentChunk = null;
+ }
+ } else {
+ // last memory
+ dumpMemoryToDestination(recording);
+ }
+ jvm.endRecording_();
+ disableEvents();
+ } else {
+ RepositoryChunk newChunk = null;
+ RequestEngine.doChunkEnd();
+ updateSettingsButIgnoreRecording(recording);
+ if (toDisk) {
+ newChunk = repository.newChunk(now);
+ MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
+ } else {
+ MetadataRepository.getInstance().setOutput(null);
+ }
+ writeMetaEvents();
+ if (currentChunk != null) {
+ finishChunk(currentChunk, now, null);
+ }
+ currentChunk = newChunk;
+ RequestEngine.doChunkBegin();
+ }
+ recording.setState(RecordingState.STOPPED);
+ }
+
+ // Only dump event if the recording that is being stopped
+ // has OldObjectSample enabled + cutoff from recording, not global value
+ private void emitOldObjectSamples(PlatformRecording recording) {
+ Map<String, String> settings = recording.getSettings();
+ String s = settings.get(Type.EVENT_NAME_PREFIX + "OldObjectSample#" + Enabled.NAME);
+ if ("true".equals(s)) {
+ long nanos = CutoffSetting.parseValueSafe(settings.get(Type.EVENT_NAME_PREFIX + "OldObjectSample#" + Cutoff.NAME));
+ long ticks = Utils.nanosToTicks(nanos);
+ JVM.getJVM().emitOldObjectSamples(ticks, WhiteBox.getWriteAllObjectSamples());
+ }
+ }
+
+ private void dumpMemoryToDestination(PlatformRecording recording) {
+ WriteableUserPath dest = recording.getDestination();
+ if (dest != null) {
+ MetadataRepository.getInstance().setOutput(dest.getText());
+ recording.clearDestination();
+ }
+ }
+
+ private void disableEvents() {
+ MetadataRepository.getInstance().disableEvents();
+ }
+
+ void updateSettings() {
+ updateSettingsButIgnoreRecording(null);
+ }
+
+ void updateSettingsButIgnoreRecording(PlatformRecording ignoreMe) {
+ List<PlatformRecording> recordings = getRunningRecordings();
+ List<Map<String, String>> list = new ArrayList<>(recordings.size());
+ for (PlatformRecording r : recordings) {
+ if (r != ignoreMe) {
+ list.add(r.getSettings());
+ }
+ }
+ MetadataRepository.getInstance().setSettings(list);
+ }
+
+ synchronized void rotateDisk() {
+ Instant now = Instant.now();
+ RepositoryChunk newChunk = repository.newChunk(now);
+ RequestEngine.doChunkEnd();
+ MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
+ writeMetaEvents();
+ if (currentChunk != null) {
+ finishChunk(currentChunk, now, null);
+ }
+ currentChunk = newChunk;
+ RequestEngine.doChunkBegin();
+ }
+
+ private List<PlatformRecording> getRunningRecordings() {
+ List<PlatformRecording> runningRecordings = new ArrayList<>();
+ for (PlatformRecording recording : getRecordings()) {
+ if (recording.getState() == RecordingState.RUNNING) {
+ runningRecordings.add(recording);
+ }
+ }
+ return runningRecordings;
+ }
+
+ private List<RepositoryChunk> makeChunkList(Instant startTime, Instant endTime) {
+ Set<RepositoryChunk> chunkSet = new HashSet<>();
+ for (PlatformRecording r : getRecordings()) {
+ chunkSet.addAll(r.getChunks());
+ }
+ if (chunkSet.size() > 0) {
+ List<RepositoryChunk> chunks = new ArrayList<>(chunkSet.size());
+ for (RepositoryChunk rc : chunkSet) {
+ if (rc.inInterval(startTime, endTime)) {
+ chunks.add(rc);
+ }
+ }
+ // n*log(n), should be able to do n*log(k) with a priority queue,
+ // where k = number of recordings, n = number of chunks
+ Collections.sort(chunks, RepositoryChunk.END_TIME_COMPARATOR);
+ return chunks;
+ }
+
+ return Collections.emptyList();
+ }
+
+ private void startDiskMonitor() {
+ Thread t = SecuritySupport.createThreadWitNoPermissions("JFR Periodic Tasks", () -> periodicTask());
+ SecuritySupport.setDaemonThread(t, true);
+ t.start();
+ }
+
+ private void finishChunk(RepositoryChunk chunk, Instant time, PlatformRecording ignoreMe) {
+ chunk.finish(time);
+ for (PlatformRecording r : getRecordings()) {
+ if (r != ignoreMe && r.getState() == RecordingState.RUNNING) {
+ r.appendChunk(chunk);
+ }
+ }
+ }
+
+ private void writeMetaEvents() {
+
+ if (activeRecordingEvent.isEnabled()) {
+ for (PlatformRecording r : getRecordings()) {
+ if (r.getState() == RecordingState.RUNNING && r.shouldWriteMetadataEvent()) {
+ ActiveRecordingEvent event = new ActiveRecordingEvent();
+ event.id = r.getId();
+ event.name = r.getName();
+ WriteableUserPath p = r.getDestination();
+ event.destination = p == null ? null : p.getText();
+ Duration d = r.getDuration();
+ event.recordingDuration = d == null ? Long.MAX_VALUE : d.toMillis();
+ Duration age = r.getMaxAge();
+ event.maxAge = age == null ? Long.MAX_VALUE : age.toMillis();
+ Long size = r.getMaxSize();
+ event.maxSize = size == null ? Long.MAX_VALUE : size;
+ Instant start = r.getStartTime();
+ event.recordingStart = start == null ? Long.MAX_VALUE : start.toEpochMilli();
+ event.commit();
+ }
+ }
+ }
+ if (activeSettingEvent.isEnabled()) {
+ for (EventControl ec : MetadataRepository.getInstance().getEventControls()) {
+ ec.writeActiveSettingEvent();
+ }
+ }
+ }
+
+ private void periodicTask() {
+ while (true) {
+ synchronized (this) {
+ if (!jvm.hasNativeJFR()) {
+ return;
+ }
+ if (currentChunk != null) {
+ try {
+ if (SecuritySupport.getFileSize(currentChunk.getUnfishedFile()) > Options.getMaxChunkSize()) {
+ rotateDisk();
+ }
+ } catch (IOException e) {
+ Logger.log(JFR_SYSTEM, WARN, "Could not check file size to determine chunk rotation");
+ }
+ }
+ }
+ long minDelta = RequestEngine.doPeriodic();
+ long wait = Math.min(minDelta, Options.getWaitInterval());
+ takeNap(wait);
+ }
+ }
+
+ private void takeNap(long duration) {
+ try {
+ synchronized (JVM.FILE_DELTA_CHANGE) {
+ JVM.FILE_DELTA_CHANGE.wait(duration < 10 ? 10 : duration);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ synchronized Recording newCopy(PlatformRecording r, boolean stop) {
+ Recording newRec = new Recording();
+ PlatformRecording copy = PrivateAccess.getInstance().getPlatformRecording(newRec);
+ copy.setSettings(r.getSettings());
+ copy.setMaxAge(r.getMaxAge());
+ copy.setMaxSize(r.getMaxSize());
+ copy.setDumpOnExit(r.getDumpOnExit());
+ copy.setName("Clone of " + r.getName());
+ copy.setToDisk(r.isToDisk());
+ copy.setInternalDuration(r.getDuration());
+ copy.setStartTime(r.getStartTime());
+ copy.setStopTime(r.getStopTime());
+
+ if (r.getState() == RecordingState.NEW) {
+ return newRec;
+ }
+ if (r.getState() == RecordingState.DELAYED) {
+ copy.scheduleStart(r.getStartTime());
+ return newRec;
+ }
+ copy.setState(r.getState());
+ // recording has started, copy chunks
+ for (RepositoryChunk c : r.getChunks()) {
+ copy.add(c);
+ }
+ if (r.getState() == RecordingState.RUNNING) {
+ if (stop) {
+ copy.stop("Stopped when cloning recording '" + r.getName() + "'");
+ } else {
+ if (r.getStopTime() != null) {
+ TimerTask stopTask = copy.createStopTask();
+ copy.setStopTask(copy.createStopTask());
+ getTimer().schedule(stopTask, r.getStopTime().toEpochMilli());
+ }
+ }
+ }
+ return newRec;
+ }
+
+ public synchronized Recording newSnapshot() {
+ boolean running = false;
+ boolean toDisk = false;
+ for (PlatformRecording r : recordings) {
+ if (r.getState() == RecordingState.RUNNING) {
+ running = true;
+ }
+ if (r.isToDisk()) {
+ toDisk = true;
+ }
+
+ }
+ // If needed, flush data from memory
+ if (running) {
+ if (toDisk) {
+ rotateDisk();
+ } else {
+ try (Recording snapshot = new Recording()) {
+ PlatformRecording internal = PrivateAccess.getInstance().getPlatformRecording(snapshot);
+ internal.setShouldWriteActiveRecordingEvent(false);
+ snapshot.start();
+ snapshot.stop();
+ return makeRecordingWithDiskChunks();
+ }
+ }
+ }
+ return makeRecordingWithDiskChunks();
+ }
+
+ private Recording makeRecordingWithDiskChunks() {
+ Recording snapshot = new Recording();
+ snapshot.setName("Snapshot");
+ PlatformRecording internal = PrivateAccess.getInstance().getPlatformRecording(snapshot);
+ for (RepositoryChunk c : makeChunkList(null, null)) {
+ internal.add(c);
+ }
+ internal.setState(RecordingState.STOPPED);
+ Instant startTime = null;
+ Instant endTime = null;
+
+ for (RepositoryChunk c : makeChunkList(null, null)) {
+ if (startTime == null || c.getStartTime().isBefore(startTime)) {
+ startTime = c.getStartTime();
+ }
+ if (endTime == null || c.getEndTime().isAfter(endTime)) {
+ endTime = c.getEndTime();
+ }
+ }
+ Instant now = Instant.now();
+ if (startTime == null) {
+ startTime = now;
+ }
+ if (endTime == null) {
+ endTime = now;
+ }
+ internal.setStartTime(startTime);
+ internal.setStopTime(endTime);
+ internal.setInternalDuration(Duration.between(startTime, endTime));
+ return snapshot;
+ }
+}