blob: 458a00f5e408d7248853715a8dcb7f71a9bd2fd7 [file] [log] [blame]
/*
* Copyright (c) 2016, 2019, 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.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;
import jdk.jfr.internal.SecuritySupport.SafePath;
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;
private SafePath dumpOnExitDirectory = new SafePath(".");
// 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;
private Duration flushInterval = Duration.ofSeconds(1);
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 long start() {
RecordingState oldState;
RecordingState newState;
long startNanos = -1;
synchronized (recorder) {
oldState = 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;
}
startNanos = 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.formatBytesCompact(maxSize));
}
if (dumpOnExit) {
options.add("dumponexit=true");
}
if (duration != null) {
options.add("duration=" + Utils.formatTimespan(duration, ""));
}
if (destination != null) {
options.add("filename=" + destination.getRealPathText());
}
String optionText = options.toString();
if (optionText.length() != 0) {
optionText = "{" + optionText + "}";
}
return "Started recording \"" + getName() + "\" (" + getId() + ") " + optionText;
});
newState = getState();
}
notifyIfStateChanged(oldState, newState);
return startNanos;
}
public boolean stop(String reason) {
RecordingState oldState;
RecordingState newState;
synchronized (recorder) {
oldState = 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 \"" + getName() + "\" (" + getId() + ")" + endText);
this.stopTime = Instant.now();
newState = getState();
}
WriteableUserPath dest = getDestination();
if (dest != null) {
try {
dumpStopped(dest);
Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + getName() + "\" (" + getId() + ") to " + dest.getRealPathText());
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 \"" + getName() + "\" (" + 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);
}
// To be used internally when doing dumps.
// Caller must have recorder lock and close recording before releasing lock
public PlatformRecording newSnapshotClone(String reason, Boolean pathToGcRoots) throws IOException {
if(!Thread.holdsLock(recorder)) {
throw new InternalError("Caller must have recorder lock");
}
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) {
PlatformRecording clone = recorder.newTemporaryRecording();
for (RepositoryChunk r : chunks) {
clone.add(r);
}
return clone;
}
// Recording is RUNNING, create a clone
PlatformRecording clone = recorder.newTemporaryRecording();
clone.setShouldWriteActiveRecordingEvent(false);
clone.setName(getName());
clone.setToDisk(true);
// We purposely don't clone settings here, 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 (pathToGcRoots == null) {
clone.setSettings(getSettings()); // needed for old object sample
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
synchronized (MetadataRepository.getInstance()) {
clone.setSettings(OldObjectSample.createSettingsForSnapshot(this, pathToGcRoots));
clone.stop(reason);
}
}
return clone;
}
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) {
checkSetDestination(userSuppliedPath);
this.destination = userSuppliedPath;
}
}
public void checkSetDestination(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");
}
}
}
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 (Logger.shouldLog(LogTag.JFR_SETTING, LogLevel.INFO) && 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.");
}
}
};
}
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;
}
// Dump running and stopped recordings
public void dump(WriteableUserPath writeableUserPath) throws IOException {
synchronized (recorder) {
try(PlatformRecording p = newSnapshotClone("Dumped by user", null)) {
p.dumpStopped(writeableUserPath);
}
}
}
public void dumpStopped(WriteableUserPath userPath) throws IOException {
synchronized (recorder) {
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 void filter(Instant begin, Instant end, Long maxSize) {
synchronized (recorder) {
List<RepositoryChunk> result = removeAfter(end, removeBefore(begin, new ArrayList<>(chunks)));
if (maxSize != null) {
if (begin != null && end == null) {
result = reduceFromBeginning(maxSize, result);
} else {
result = reduceFromEnd(maxSize, result);
}
}
int size = 0;
for (RepositoryChunk r : result) {
size += r.getSize();
r.use();
}
this.size = size;
for (RepositoryChunk r : chunks) {
r.release();
}
chunks.clear();
chunks.addAll(result);
}
}
private static List<RepositoryChunk> removeBefore(Instant time, List<RepositoryChunk> input) {
if (time == null) {
return input;
}
List<RepositoryChunk> result = new ArrayList<>(input.size());
for (RepositoryChunk r : input) {
if (!r.getEndTime().isBefore(time)) {
result.add(r);
}
}
return result;
}
private static List<RepositoryChunk> removeAfter(Instant time, List<RepositoryChunk> input) {
if (time == null) {
return input;
}
List<RepositoryChunk> result = new ArrayList<>(input.size());
for (RepositoryChunk r : input) {
if (!r.getStartTime().isAfter(time)) {
result.add(r);
}
}
return result;
}
private static List<RepositoryChunk> reduceFromBeginning(Long maxSize, List<RepositoryChunk> input) {
if (maxSize == null || input.isEmpty()) {
return input;
}
List<RepositoryChunk> result = new ArrayList<>(input.size());
long total = 0;
for (RepositoryChunk r : input) {
total += r.getSize();
if (total > maxSize) {
break;
}
result.add(r);
}
// always keep at least one chunk
if (result.isEmpty()) {
result.add(input.get(0));
}
return result;
}
private static List<RepositoryChunk> reduceFromEnd(Long maxSize, List<RepositoryChunk> input) {
Collections.reverse(input);
List<RepositoryChunk> result = reduceFromBeginning(maxSize, input);
Collections.reverse(result);
return result;
}
public void setDumpOnExitDirectory(SafePath directory) {
this.dumpOnExitDirectory = directory;
}
public SafePath getDumpOnExitDirectory() {
return this.dumpOnExitDirectory;
}
public void setFlushInterval(Duration interval) {
synchronized (recorder) {
if (getState() == RecordingState.CLOSED) {
throw new IllegalStateException("Can't set stream interval when recording is closed");
}
this.flushInterval = interval;
}
}
public Duration getFlushInterval() {
synchronized (recorder) {
return flushInterval;
}
}
public long getStreamIntervalMillis() {
synchronized (recorder) {
if (flushInterval != null) {
return flushInterval.toMillis();
}
return Long.MAX_VALUE;
}
}
}