--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecording.java Tue May 15 20:24:34 2018 +0200
@@ -0,0 +1,696 @@
+/*
+ * 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.DEBUG;
+import static jdk.jfr.internal.LogLevel.WARN;
+import static jdk.jfr.internal.LogTag.JFR;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.channels.FileChannel;
+import java.nio.file.StandardOpenOption;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+import java.util.TimerTask;
+import java.util.TreeMap;
+
+import jdk.jfr.Configuration;
+import jdk.jfr.FlightRecorderListener;
+import jdk.jfr.Recording;
+import jdk.jfr.RecordingState;
+
+public final class PlatformRecording implements AutoCloseable {
+
+ private final PlatformRecorder recorder;
+ private final long id;
+ // Recording settings
+ private Map<String, String> settings = new LinkedHashMap<>();
+ private Duration duration;
+ private Duration maxAge;
+ private long maxSize;
+
+ private WriteableUserPath destination;
+
+ private boolean toDisk = true;
+ private String name;
+ private boolean dumpOnExit;
+ // Timestamp information
+ private Instant stopTime;
+ private Instant startTime;
+
+ // Misc, information
+ private RecordingState state = RecordingState.NEW;
+ private long size;
+ private final LinkedList<RepositoryChunk> chunks = new LinkedList<>();
+ private volatile Recording recording;
+ private TimerTask stopTask;
+ private TimerTask startTask;
+ private AccessControlContext noDestinationDumpOnExitAccessControlContext;
+ private boolean shuoldWriteActiveRecordingEvent = true;
+
+ PlatformRecording(PlatformRecorder recorder, long id) {
+ // Typically the access control context is taken
+ // when you call dump(Path) or setDdestination(Path),
+ // but if no destination is set and dumponexit=true
+ // the control context of the recording is taken when the
+ // Recording object is constructed. This works well for
+ // -XX:StartFlightRecording and JFR.dump
+ this.noDestinationDumpOnExitAccessControlContext = AccessController.getContext();
+ this.id = id;
+ this.recorder = recorder;
+ this.name = String.valueOf(id);
+ }
+
+ public void start() {
+ RecordingState oldState;
+ RecordingState newState;
+ synchronized (recorder) {
+ oldState = recording.getState();
+ if (!Utils.isBefore(state, RecordingState.RUNNING)) {
+ throw new IllegalStateException("Recording can only be started once.");
+ }
+ if (startTask != null) {
+ startTask.cancel();
+ startTask = null;
+ startTime = null;
+ }
+ recorder.start(this);
+ Logger.log(LogTag.JFR, LogLevel.INFO, () -> {
+ // Only print non-default values so it easy to see
+ // which options were added
+ StringJoiner options = new StringJoiner(", ");
+ if (!toDisk) {
+ options.add("disk=false");
+ }
+ if (maxAge != null) {
+ options.add("maxage=" + Utils.formatTimespan(maxAge, ""));
+ }
+ if (maxSize != 0) {
+ options.add("maxsize=" + Utils.formatBytes(maxSize, ""));
+ }
+ if (dumpOnExit) {
+ options.add("dumponexit=true");
+ }
+ if (duration != null) {
+ options.add("duration=" + Utils.formatTimespan(duration, ""));
+ }
+ if (destination != null) {
+ options.add("filename=" + destination.getText());
+ }
+ String optionText = options.toString();
+ if (optionText.length() != 0) {
+ optionText = "{" + optionText + "}";
+ }
+ return "Started recording \"" + getName() + "\" (" + getId() + ") " + optionText;
+ });
+ newState = recording.getState();
+ }
+ notifyIfStateChanged(oldState, newState);
+ }
+
+ public boolean stop(String reason) {
+ return stop(reason, null);
+ }
+
+ public boolean stop(String reason, WriteableUserPath alternativePath) {
+ return stop(reason, alternativePath, Collections.emptyMap());
+ }
+
+ boolean stop(String reason, WriteableUserPath alternativePath, Map<String, String> overlaySettings) {
+ RecordingState oldState;
+ RecordingState newState;
+ synchronized (recorder) {
+ oldState = recording.getState();
+ if (stopTask != null) {
+ stopTask.cancel();
+ stopTask = null;
+ }
+ recorder.stop(this);
+ String endTExt = reason == null ? "" : ". Reason \"" + reason + "\".";
+ Logger.log(LogTag.JFR, LogLevel.INFO, "Stopped recording \"" + recording.getName() + "\" (" + recording.getId()+ ")" + endTExt);
+ this.stopTime = Instant.now();
+ newState = recording.getState();
+ }
+ WriteableUserPath dest = getDestination();
+ if (dest == null && alternativePath != null) {
+ dest = alternativePath;
+ }
+ if (dest != null) {
+ try {
+ copyTo(dest, reason, overlaySettings);
+ Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + recording.getName() + "\" (" + recording.getId()+ ") to " + dest.getText());
+ notifyIfStateChanged(newState, oldState);
+ close(); // remove if copied out
+ } catch (IOException e) {
+ // throw e; // BUG8925030
+ }
+ } else {
+ notifyIfStateChanged(newState, oldState);
+ }
+ return true;
+ }
+
+ public void scheduleStart(Duration delay) {
+ synchronized (recorder) {
+ ensureOkForSchedule();
+
+ startTime = Instant.now().plus(delay);
+ LocalDateTime now = LocalDateTime.now().plus(delay);
+ setState(RecordingState.DELAYED);
+ startTask = createStartTask();
+ recorder.getTimer().schedule(startTask, delay.toMillis());
+ Logger.log(LogTag.JFR, LogLevel.INFO, "Scheduled recording \"" + recording.getName() + "\" (" + recording.getId()+ ") to start at " + now);
+ }
+ }
+
+ private void ensureOkForSchedule() {
+ if (getState() != RecordingState.NEW) {
+ throw new IllegalStateException("Only a new recoridng can be scheduled for start");
+ }
+ }
+
+ private TimerTask createStartTask() {
+ // Taking ref. to recording here.
+ // Opens up for memory leaks.
+ return new TimerTask() {
+ @Override
+ public void run() {
+ synchronized (recorder) {
+ if (getState() != RecordingState.DELAYED) {
+ return;
+ }
+ start();
+ }
+ }
+ };
+ }
+
+ void scheduleStart(Instant startTime) {
+ synchronized (recorder) {
+ ensureOkForSchedule();
+ this.startTime = startTime;
+ setState(RecordingState.DELAYED);
+ startTask = createStartTask();
+ recorder.getTimer().schedule(startTask, startTime.toEpochMilli());
+ }
+ }
+
+ public Map<String, String> getSettings() {
+ synchronized (recorder) {
+ return settings;
+ }
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public Instant getStopTime() {
+ synchronized (recorder) {
+ return stopTime;
+ }
+ }
+
+ public Instant getStartTime() {
+ synchronized (recorder) {
+ return startTime;
+ }
+ }
+
+ public Long getMaxSize() {
+ synchronized (recorder) {
+ return maxSize;
+ }
+ }
+
+ public Duration getMaxAge() {
+ synchronized (recorder) {
+ return maxAge;
+ }
+ }
+
+ public String getName() {
+ synchronized (recorder) {
+ return name;
+ }
+ }
+
+
+ public RecordingState getState() {
+ synchronized (recorder) {
+ return state;
+ }
+ }
+
+ @Override
+ public void close() {
+ RecordingState oldState;
+ RecordingState newState;
+
+ synchronized (recorder) {
+ oldState = getState();
+ if (RecordingState.CLOSED != getState()) {
+ if (startTask != null) {
+ startTask.cancel();
+ startTask = null;
+ }
+ recorder.finish(this);
+ for (RepositoryChunk c : chunks) {
+ removed(c);
+ }
+ chunks.clear();
+ setState(RecordingState.CLOSED);
+ Logger.log(LogTag.JFR, LogLevel.INFO, "Closed recording \"" + getName() + "\" (" + getId()+ ")");
+ }
+ newState = getState();
+ }
+ notifyIfStateChanged(newState, oldState);
+ }
+
+ public void copyTo(WriteableUserPath path, String reason, Map<String, String> dumpSettings) throws IOException {
+ synchronized (recorder) {
+ RecordingState state = getState();
+ if (state == RecordingState.CLOSED) {
+ throw new IOException("Recording \"" + name + "\" (id=" + id + ") has been closed, no contents to write");
+ }
+ if (state == RecordingState.DELAYED || state == RecordingState.NEW) {
+ throw new IOException("Recording \"" + name + "\" (id=" + id + ") has not started, no contents to write");
+ }
+ if (state == RecordingState.STOPPED) {
+ // have all we need, just write it
+ dumpToFile(path, reason, getId());
+ return;
+ }
+
+ // Recording is RUNNING, create a clone
+ try(Recording r = new Recording()) {
+ PlatformRecording clone = PrivateAccess.getInstance().getPlatformRecording(r);
+ clone.setShouldWriteActiveRecordingEvent(false);
+ clone.setName(getName());
+ clone.setDestination(path);
+ clone.setToDisk(true);
+ // We purposely don't clone settings, since
+ // a union a == a
+ if (!isToDisk()) {
+ // force memory contents to disk
+ clone.start();
+ } else {
+ // using existing chunks on disk
+ for (RepositoryChunk c : chunks) {
+ clone.add(c);
+ }
+ clone.setState(RecordingState.RUNNING);
+ clone.setStartTime(getStartTime());
+ }
+ if (dumpSettings.isEmpty()) {
+ clone.setSettings(getSettings());
+ clone.stop(reason); // dumps to destination path here
+ } else {
+ // Risk of violating lock order here, since
+ // clone.stop() will take recorder lock inside
+ // metadata lock, but OK if we already
+ // have recorder lock when we entered metadata lock
+ Thread.holdsLock(recorder);
+ synchronized(MetadataRepository.getInstance()) {
+ Thread.holdsLock(recorder);
+ Map<String, String> oldSettings = getSettings();
+ Map<String, String> newSettings = new HashMap<>(oldSettings);
+ // replace with dump settings
+ newSettings.putAll(dumpSettings);
+ clone.setSettings(newSettings);
+ clone.stop(reason);
+ }
+ }
+ }
+ return;
+ }
+ }
+
+ private void dumpToFile(WriteableUserPath userPath, String reason, long id) throws IOException {
+ userPath.doPriviligedIO(() -> {
+ try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
+ cc.transferTo(fc);
+ fc.force(true);
+ }
+ return null;
+ });
+ }
+
+ public boolean isToDisk() {
+ synchronized (recorder) {
+ return toDisk;
+ }
+ }
+
+ public void setMaxSize(long maxSize) {
+ synchronized (recorder) {
+ if (getState() == RecordingState.CLOSED) {
+ throw new IllegalStateException("Can't set max age when recording is closed");
+ }
+ this.maxSize = maxSize;
+ trimToSize();
+ }
+ }
+
+ public void setDestination(WriteableUserPath userSuppliedPath) throws IOException {
+ synchronized (recorder) {
+ if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) {
+ throw new IllegalStateException("Destination can't be set on a recording that has been stopped/closed");
+ }
+ this.destination = userSuppliedPath;
+ }
+ }
+
+ public WriteableUserPath getDestination() {
+ synchronized (recorder) {
+ return destination;
+ }
+ }
+
+ void setState(RecordingState state) {
+ synchronized (recorder) {
+ this.state = state;
+ }
+ }
+
+ void setStartTime(Instant startTime) {
+ synchronized (recorder) {
+ this.startTime = startTime;
+ }
+ }
+
+ void setStopTime(Instant timeStamp) {
+ synchronized (recorder) {
+ stopTime = timeStamp;
+ }
+ }
+
+ public long getId() {
+ synchronized (recorder) {
+ return id;
+ }
+ }
+
+ public void setName(String name) {
+ synchronized (recorder) {
+ ensureNotClosed();
+ this.name = name;
+ }
+ }
+
+ private void ensureNotClosed() {
+ if (getState() == RecordingState.CLOSED) {
+ throw new IllegalStateException("Can't change name on a closed recording");
+ }
+ }
+
+ public void setDumpOnExit(boolean dumpOnExit) {
+ synchronized (recorder) {
+ this.dumpOnExit = dumpOnExit;
+ }
+ }
+
+ public boolean getDumpOnExit() {
+ synchronized (recorder) {
+ return dumpOnExit;
+ }
+ }
+
+ public void setToDisk(boolean toDisk) {
+ synchronized (recorder) {
+ if (Utils.isState(getState(), RecordingState.NEW, RecordingState.DELAYED)) {
+ this.toDisk = toDisk;
+ } else {
+ throw new IllegalStateException("Recording option disk can't be changed after recording has started");
+ }
+ }
+ }
+
+ public void setSetting(String id, String value) {
+ synchronized (recorder) {
+ this.settings.put(id, value);
+ if (getState() == RecordingState.RUNNING) {
+ recorder.updateSettings();
+ }
+ }
+ }
+
+ public void setSettings(Map<String, String> settings) {
+ setSettings(settings, true);
+ }
+
+ private void setSettings(Map<String, String> settings, boolean update) {
+ if (LogTag.JFR_SETTING.shouldLog(LogLevel.INFO.level) && update) {
+ TreeMap<String, String> ordered = new TreeMap<>(settings);
+ Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")");
+ for (Map.Entry<String, String> entry : ordered.entrySet()) {
+ String text = entry.getKey() + "=\"" + entry.getValue() + "\"";
+ Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text);
+ }
+ }
+ synchronized (recorder) {
+ this.settings = new LinkedHashMap<>(settings);
+ if (getState() == RecordingState.RUNNING && update) {
+ recorder.updateSettings();
+ }
+ }
+ }
+
+
+ private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) {
+ if (oldState == newState) {
+ return;
+ }
+ for (FlightRecorderListener cl : PlatformRecorder.getListeners()) {
+ try {
+ cl.recordingStateChanged(getRecording());
+ } catch (RuntimeException re) {
+ Logger.log(JFR, WARN, "Error notifying recorder listener:" + re.getMessage());
+ }
+ }
+ }
+
+ public void setRecording(Recording recording) {
+ this.recording = recording;
+ }
+
+ public Recording getRecording() {
+ return recording;
+ }
+
+ @Override
+ public String toString() {
+ return getName() + " (id=" + getId() + ") " + getState();
+ }
+
+ public void setConfiguration(Configuration c) {
+ setSettings(c.getSettings());
+ }
+
+ public void setMaxAge(Duration maxAge) {
+ synchronized (recorder) {
+ if (getState() == RecordingState.CLOSED) {
+ throw new IllegalStateException("Can't set max age when recording is closed");
+ }
+ this.maxAge = maxAge;
+ if (maxAge != null) {
+ trimToAge(Instant.now().minus(maxAge));
+ }
+ }
+ }
+
+ void appendChunk(RepositoryChunk chunk) {
+ if (!chunk.isFinished()) {
+ throw new Error("not finished chunk " + chunk.getStartTime());
+ }
+ synchronized (recorder) {
+ if (!toDisk) {
+ return;
+ }
+ if (maxAge != null) {
+ trimToAge(chunk.getEndTime().minus(maxAge));
+ }
+ chunks.addLast(chunk);
+ added(chunk);
+ trimToSize();
+ }
+ }
+
+ private void trimToSize() {
+ if (maxSize == 0) {
+ return;
+ }
+ while (size > maxSize && chunks.size() > 1) {
+ RepositoryChunk c = chunks.removeFirst();
+ removed(c);
+ }
+ }
+
+ private void trimToAge(Instant oldest) {
+ while (!chunks.isEmpty()) {
+ RepositoryChunk oldestChunk = chunks.peek();
+ if (oldestChunk.getEndTime().isAfter(oldest)) {
+ return;
+ }
+ chunks.removeFirst();
+ removed(oldestChunk);
+ }
+ }
+
+ void add(RepositoryChunk c) {
+ chunks.add(c);
+ added(c);
+ }
+
+ private void added(RepositoryChunk c) {
+ c.use();
+ size += c.getSize();
+ Logger.log(JFR, DEBUG, ()-> "Recording \"" + name + "\" (" + id + ") added chunk " + c.toString() + ", current size=" + size);
+ }
+
+ private void removed(RepositoryChunk c) {
+ size -= c.getSize();
+ Logger.log(JFR, DEBUG, ()-> "Recording \"" + name + "\" (" + id + ") removed chunk " + c.toString() + ", current size=" + size);
+ c.release();
+ }
+
+ public List<RepositoryChunk> getChunks() {
+ return chunks;
+ }
+
+ public InputStream open(Instant start, Instant end) throws IOException {
+ synchronized (recorder) {
+ if (getState() != RecordingState.STOPPED) {
+ throw new IOException("Recording must be stopped before it can be read.");
+ }
+ List<RepositoryChunk> chunksToUse = new ArrayList<RepositoryChunk>();
+ for (RepositoryChunk chunk : chunks) {
+ if (chunk.isFinished()) {
+ Instant chunkStart = chunk.getStartTime();
+ Instant chunkEnd = chunk.getEndTime();
+ if (start == null || !chunkEnd.isBefore(start)) {
+ if (end == null || !chunkStart.isAfter(end)) {
+ chunksToUse.add(chunk);
+ }
+ }
+ }
+ }
+ if (chunksToUse.isEmpty()) {
+ return null;
+ }
+ return new ChunkInputStream(chunksToUse);
+ }
+ }
+
+ public Duration getDuration() {
+ synchronized (recorder) {
+ return duration;
+ }
+ }
+
+ void setInternalDuration(Duration duration) {
+ this.duration = duration;
+ }
+
+ public void setDuration(Duration duration) {
+ synchronized (recorder) {
+ if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) {
+ throw new IllegalStateException("Duration can't be set after a recording has been stopped/closed");
+ }
+ setInternalDuration(duration);
+ if (getState() != RecordingState.NEW) {
+ updateTimer();
+ }
+ }
+ }
+
+ void updateTimer() {
+ if (stopTask != null) {
+ stopTask.cancel();
+ stopTask = null;
+ }
+ if (getState() == RecordingState.CLOSED) {
+ return;
+ }
+ if (duration != null) {
+ stopTask = createStopTask();
+ recorder.getTimer().schedule(stopTask, new Date(startTime.plus(duration).toEpochMilli()));
+ }
+ }
+
+ TimerTask createStopTask() {
+ return new TimerTask() {
+ @Override
+ public void run() {
+ try {
+ stop("End of duration reached");
+ } catch (Throwable t) {
+ // Prevent malicious user to propagate exception callback in the wrong context
+ Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not stop recording. " + t.getMessage());
+ }
+ }
+ };
+ }
+
+ public Recording newCopy(boolean stop) {
+ return recorder.newCopy(this, stop);
+ }
+
+ void setStopTask(TimerTask stopTask) {
+ synchronized (recorder) {
+ this.stopTask = stopTask;
+ }
+ }
+
+ void clearDestination() {
+ destination = null;
+ }
+
+ public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() {
+ return noDestinationDumpOnExitAccessControlContext;
+ }
+
+ void setShouldWriteActiveRecordingEvent(boolean shouldWrite) {
+ this.shuoldWriteActiveRecordingEvent = shouldWrite;
+ }
+
+ boolean shouldWriteMetadataEvent() {
+ return shuoldWriteActiveRecordingEvent;
+ }
+}