package jdk.jfr.internal;
import static jdk.jfr.internal.LogLevel.INFO;
import static jdk.jfr.internal.LogLevel.TRACE;
import static jdk.jfr.internal.LogLevel.WARN;
import static jdk.jfr.internal.LogTag.JFR;
import static jdk.jfr.internal.LogTag.JFR_SYSTEM;
import java.io.IOException;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
import jdk.jfr.EventType;
import jdk.jfr.FlightRecorder;
import jdk.jfr.FlightRecorderListener;
import jdk.jfr.Recording;
import jdk.jfr.RecordingState;
import jdk.jfr.events.ActiveRecordingEvent;
import jdk.jfr.events.ActiveSettingEvent;
import jdk.jfr.internal.SecuritySupport.SecureRecorderListener;
import jdk.jfr.internal.instrument.JDKEvents;
public final class PlatformRecorder {
private final List<PlatformRecording> recordings = new ArrayList<>();
private final static List<SecureRecorderListener> changeListeners = new ArrayList<>();
private final Repository repository;
private final Timer timer;
private final static JVM jvm = JVM.getJVM();
private final EventType activeRecordingEvent;
private final EventType activeSettingEvent;
private final Thread shutdownHook;
private long recordingCounter = 0;
private RepositoryChunk currentChunk;
public PlatformRecorder() throws Exception {
repository = Repository.getRepository();
Logger.log(JFR_SYSTEM, INFO, "Initialized disk repository");
repository.ensureRepository();
jvm.createNativeJFR();
Logger.log(JFR_SYSTEM, INFO, "Created native");
JDKEvents.initialize();
Logger.log(JFR_SYSTEM, INFO, "Registered JDK events");
JDKEvents.addInstrumentation();
startDiskMonitor();
activeRecordingEvent = EventType.getEventType(ActiveRecordingEvent.class);
activeSettingEvent = EventType.getEventType(ActiveSettingEvent.class);
shutdownHook = SecuritySupport.createThreadWitNoPermissions("JFR: Shutdown Hook", new ShutdownHook(this));
SecuritySupport.setUncaughtExceptionHandler(shutdownHook, new ShutdownHook.ExceptionHandler());
SecuritySupport.registerShutdownHook(shutdownHook);
timer = createTimer();
}
private static Timer createTimer() {
try {
List<Timer> result = new CopyOnWriteArrayList<>();
Thread t = SecuritySupport.createThreadWitNoPermissions("Permissionless thread", ()-> {
result.add(new Timer("JFR Recording Scheduler", true));
});
t.start();
t.join();
return result.get(0);
} catch (InterruptedException e) {
throw new IllegalStateException("Not able to create timer task. " + e.getMessage(), e);
}
}
public synchronized PlatformRecording newRecording(Map<String, String> settings) {
return newRecording(settings, ++recordingCounter);
}
public PlatformRecording newTemporaryRecording() {
if(!Thread.holdsLock(this)) {
throw new InternalError("Caller must have recorder lock");
}
return newRecording(new HashMap<>(), 0);
}
private synchronized PlatformRecording newRecording(Map<String, String> settings, long id) {
PlatformRecording recording = new PlatformRecording(this, id);
if (!settings.isEmpty()) {
recording.setSettings(settings);
}
recordings.add(recording);
return recording;
}
synchronized void finish(PlatformRecording recording) {
if (recording.getState() == RecordingState.RUNNING) {
recording.stop("Recording closed");
}
recordings.remove(recording);
}
public synchronized List<PlatformRecording> getRecordings() {
return Collections.unmodifiableList(new ArrayList<PlatformRecording>(recordings));
}
public synchronized static void addListener(FlightRecorderListener changeListener) {
AccessControlContext context = AccessController.getContext();
SecureRecorderListener sl = new SecureRecorderListener(context, changeListener);
boolean runInitialized;
synchronized (PlatformRecorder.class) {
runInitialized = FlightRecorder.isInitialized();
changeListeners.add(sl);
}
if (runInitialized) {
sl.recorderInitialized(FlightRecorder.getFlightRecorder());
}
}
public synchronized static boolean removeListener(FlightRecorderListener changeListener) {
for (SecureRecorderListener s : new ArrayList<>(changeListeners)) {
if (s.getChangeListener() == changeListener) {
changeListeners.remove(s);
return true;
}
}
return false;
}
static synchronized List<FlightRecorderListener> getListeners() {
return new ArrayList<>(changeListeners);
}
Timer getTimer() {
return timer;
}
public static void notifyRecorderInitialized(FlightRecorder recorder) {
Logger.log(JFR_SYSTEM, TRACE, "Notifying listeners that Flight Recorder is initialized");
for (FlightRecorderListener r : getListeners()) {
r.recorderInitialized(recorder);
}
}
synchronized void destroy() {
try {
timer.cancel();
} catch (Exception ex) {
Logger.log(JFR_SYSTEM, WARN, "Shutdown hook could not cancel timer");
}
for (PlatformRecording p : getRecordings()) {
if (p.getState() == RecordingState.RUNNING) {
try {
p.stop("Shutdown");
} catch (Exception ex) {
Logger.log(JFR, WARN, "Recording " + p.getName() + ":" + p.getId() + " could not be stopped");
}
}
}
JDKEvents.remove();
if (jvm.hasNativeJFR()) {
if (jvm.isRecording()) {
jvm.endRecording_();
}
jvm.destroyNativeJFR();
}
repository.clear();
}
synchronized void start(PlatformRecording recording) {
Instant now = Instant.now();
recording.setStartTime(now);
recording.updateTimer();
Duration duration = recording.getDuration();
if (duration != null) {
recording.setStopTime(now.plus(duration));
}
boolean toDisk = recording.isToDisk();
boolean beginPhysical = true;
for (PlatformRecording s : getRecordings()) {
if (s.getState() == RecordingState.RUNNING) {
beginPhysical = false;
if (s.isToDisk()) {
toDisk = true;
}
}
}
if (beginPhysical) {
RepositoryChunk newChunk = null;
if (toDisk) {
newChunk = repository.newChunk(now);
MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
} else {
MetadataRepository.getInstance().setOutput(null);
}
currentChunk = newChunk;
jvm.beginRecording_();
recording.setState(RecordingState.RUNNING);
updateSettings();
writeMetaEvents();
} else {
RepositoryChunk newChunk = null;
if (toDisk) {
newChunk = repository.newChunk(now);
RequestEngine.doChunkEnd();
MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
}
recording.setState(RecordingState.RUNNING);
updateSettings();
writeMetaEvents();
if (currentChunk != null) {
finishChunk(currentChunk, now, recording);
}
currentChunk = newChunk;
}
RequestEngine.doChunkBegin();
}
synchronized void stop(PlatformRecording recording) {
RecordingState state = recording.getState();
if (Utils.isAfter(state, RecordingState.RUNNING)) {
throw new IllegalStateException("Can't stop an already stopped recording.");
}
if (Utils.isBefore(state, RecordingState.RUNNING)) {
throw new IllegalStateException("Recording must be started before it can be stopped.");
}
Instant now = Instant.now();
boolean toDisk = false;
boolean endPhysical = true;
for (PlatformRecording s : getRecordings()) {
RecordingState rs = s.getState();
if (s != recording && RecordingState.RUNNING == rs) {
endPhysical = false;
if (s.isToDisk()) {
toDisk = true;
}
}
}
OldObjectSample.emit(recording);
if (endPhysical) {
RequestEngine.doChunkEnd();
if (recording.isToDisk()) {
if (currentChunk != null) {
MetadataRepository.getInstance().setOutput(null);
finishChunk(currentChunk, now, null);
currentChunk = null;
}
} else {
dumpMemoryToDestination(recording);
}
jvm.endRecording_();
disableEvents();
} else {
RepositoryChunk newChunk = null;
RequestEngine.doChunkEnd();
updateSettingsButIgnoreRecording(recording);
if (toDisk) {
newChunk = repository.newChunk(now);
MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
} else {
MetadataRepository.getInstance().setOutput(null);
}
writeMetaEvents();
if (currentChunk != null) {
finishChunk(currentChunk, now, null);
}
currentChunk = newChunk;
RequestEngine.doChunkBegin();
}
recording.setState(RecordingState.STOPPED);
}
private void dumpMemoryToDestination(PlatformRecording recording) {
WriteableUserPath dest = recording.getDestination();
if (dest != null) {
MetadataRepository.getInstance().setOutput(dest.getRealPathText());
recording.clearDestination();
}
}
private void disableEvents() {
MetadataRepository.getInstance().disableEvents();
}
void updateSettings() {
updateSettingsButIgnoreRecording(null);
}
void updateSettingsButIgnoreRecording(PlatformRecording ignoreMe) {
List<PlatformRecording> recordings = getRunningRecordings();
List<Map<String, String>> list = new ArrayList<>(recordings.size());
for (PlatformRecording r : recordings) {
if (r != ignoreMe) {
list.add(r.getSettings());
}
}
MetadataRepository.getInstance().setSettings(list);
}
synchronized void rotateDisk() {
Instant now = Instant.now();
RepositoryChunk newChunk = repository.newChunk(now);
RequestEngine.doChunkEnd();
MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
writeMetaEvents();
if (currentChunk != null) {
finishChunk(currentChunk, now, null);
}
currentChunk = newChunk;
RequestEngine.doChunkBegin();
}
private List<PlatformRecording> getRunningRecordings() {
List<PlatformRecording> runningRecordings = new ArrayList<>();
for (PlatformRecording recording : getRecordings()) {
if (recording.getState() == RecordingState.RUNNING) {
runningRecordings.add(recording);
}
}
return runningRecordings;
}
private List<RepositoryChunk> makeChunkList(Instant startTime, Instant endTime) {
Set<RepositoryChunk> chunkSet = new HashSet<>();
for (PlatformRecording r : getRecordings()) {
chunkSet.addAll(r.getChunks());
}
if (chunkSet.size() > 0) {
List<RepositoryChunk> chunks = new ArrayList<>(chunkSet.size());
for (RepositoryChunk rc : chunkSet) {
if (rc.inInterval(startTime, endTime)) {
chunks.add(rc);
}
}
Collections.sort(chunks, RepositoryChunk.END_TIME_COMPARATOR);
return chunks;
}
return Collections.emptyList();
}
private void startDiskMonitor() {
Thread t = SecuritySupport.createThreadWitNoPermissions("JFR Periodic Tasks", () -> periodicTask());
SecuritySupport.setDaemonThread(t, true);
t.start();
}
private void finishChunk(RepositoryChunk chunk, Instant time, PlatformRecording ignoreMe) {
chunk.finish(time);
for (PlatformRecording r : getRecordings()) {
if (r != ignoreMe && r.getState() == RecordingState.RUNNING) {
r.appendChunk(chunk);
}
}
}
private void writeMetaEvents() {
if (activeRecordingEvent.isEnabled()) {
for (PlatformRecording r : getRecordings()) {
if (r.getState() == RecordingState.RUNNING && r.shouldWriteMetadataEvent()) {
ActiveRecordingEvent event = new ActiveRecordingEvent();
event.id = r.getId();
event.name = r.getName();
WriteableUserPath p = r.getDestination();
event.destination = p == null ? null : p.getRealPathText();
Duration d = r.getDuration();
event.recordingDuration = d == null ? Long.MAX_VALUE : d.toMillis();
Duration age = r.getMaxAge();
event.maxAge = age == null ? Long.MAX_VALUE : age.toMillis();
Long size = r.getMaxSize();
event.maxSize = size == null ? Long.MAX_VALUE : size;
Instant start = r.getStartTime();
event.recordingStart = start == null ? Long.MAX_VALUE : start.toEpochMilli();
event.commit();
}
}
}
if (activeSettingEvent.isEnabled()) {
for (EventControl ec : MetadataRepository.getInstance().getEventControls()) {
ec.writeActiveSettingEvent();
}
}
}
private void periodicTask() {
if (!jvm.hasNativeJFR()) {
return;
}
while (true) {
synchronized (this) {
if (jvm.shouldRotateDisk()) {
rotateDisk();
}
}
long minDelta = RequestEngine.doPeriodic();
long wait = Math.min(minDelta, Options.getWaitInterval());
takeNap(wait);
}
}
private void takeNap(long duration) {
try {
synchronized (JVM.FILE_DELTA_CHANGE) {
JVM.FILE_DELTA_CHANGE.wait(duration < 10 ? 10 : duration);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized Recording newCopy(PlatformRecording r, boolean stop) {
Recording newRec = new Recording();
PlatformRecording copy = PrivateAccess.getInstance().getPlatformRecording(newRec);
copy.setSettings(r.getSettings());
copy.setMaxAge(r.getMaxAge());
copy.setMaxSize(r.getMaxSize());
copy.setDumpOnExit(r.getDumpOnExit());
copy.setName("Clone of " + r.getName());
copy.setToDisk(r.isToDisk());
copy.setInternalDuration(r.getDuration());
copy.setStartTime(r.getStartTime());
copy.setStopTime(r.getStopTime());
if (r.getState() == RecordingState.NEW) {
return newRec;
}
if (r.getState() == RecordingState.DELAYED) {
copy.scheduleStart(r.getStartTime());
return newRec;
}
copy.setState(r.getState());
for (RepositoryChunk c : r.getChunks()) {
copy.add(c);
}
if (r.getState() == RecordingState.RUNNING) {
if (stop) {
copy.stop("Stopped when cloning recording '" + r.getName() + "'");
} else {
if (r.getStopTime() != null) {
TimerTask stopTask = copy.createStopTask();
copy.setStopTask(copy.createStopTask());
getTimer().schedule(stopTask, r.getStopTime().toEpochMilli());
}
}
}
return newRec;
}
public synchronized void fillWithRecordedData(PlatformRecording target, Boolean pathToGcRoots) {
boolean running = false;
boolean toDisk = false;
for (PlatformRecording r : recordings) {
if (r.getState() == RecordingState.RUNNING) {
running = true;
if (r.isToDisk()) {
toDisk = true;
}
}
}
if (running) {
if (toDisk) {
OldObjectSample.emit(recordings, pathToGcRoots);
rotateDisk();
} else {
try (PlatformRecording snapshot = newTemporaryRecording()) {
snapshot.setToDisk(true);
snapshot.setShouldWriteActiveRecordingEvent(false);
snapshot.start();
OldObjectSample.emit(recordings, pathToGcRoots);
snapshot.stop("Snapshot dump");
fillWithDiskChunks(target);
}
return;
}
}
fillWithDiskChunks(target);
}
private void fillWithDiskChunks(PlatformRecording target) {
for (RepositoryChunk c : makeChunkList(null, null)) {
target.add(c);
}
target.setState(RecordingState.STOPPED);
Instant startTime = null;
Instant endTime = null;
for (RepositoryChunk c : target.getChunks()) {
if (startTime == null || c.getStartTime().isBefore(startTime)) {
startTime = c.getStartTime();
}
if (endTime == null || c.getEndTime().isAfter(endTime)) {
endTime = c.getEndTime();
}
}
Instant now = Instant.now();
if (startTime == null) {
startTime = now;
}
if (endTime == null) {
endTime = now;
}
target.setStartTime(startTime);
target.setStopTime(endTime);
target.setInternalDuration(Duration.between(startTime, endTime));
}
}