src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecording.java
changeset 50113 caf115bb98ad
child 50194 2ae4cd9d809d
--- /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;
+    }
+}