| /* |
| * 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; |
| } |
| } |
| } |