--- /dev/null 2017-11-09 09:38:01.297999907 +0100 +++ new/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecording.java 2018-04-09 16:02:51.395912502 +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 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 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 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 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 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 oldSettings = getSettings(); + Map 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 settings) { + setSettings(settings, true); + } + + private void setSettings(Map settings, boolean update) { + if (LogTag.JFR_SETTING.shouldLog(LogLevel.INFO.level) && update) { + TreeMap ordered = new TreeMap<>(settings); + Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")"); + for (Map.Entry 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 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 chunksToUse = new ArrayList(); + 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; + } +}