Skip to content

Commit 248c373

Browse files
committed
8351266: JFR: -XX:StartFlightRecording:report-on-exit
Reviewed-by: mgronlun
1 parent 03ef79c commit 248c373

File tree

15 files changed

+311
-17
lines changed

15 files changed

+311
-17
lines changed

src/java.base/share/man/java.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,12 @@ These `java` options control the runtime behavior of the Java HotSpot VM.
14471447
the potential leaking object was allocated is included in the
14481448
information collected.
14491449

1450+
`report-on-exit=`*identifier*
1451+
: Specifies the name of the view to display when the Java Virtual Machine
1452+
(JVM) shuts down. This option is not available if the disk option is set
1453+
to false. For a list of available views, see `jfr help view`. By default,
1454+
no report is generated.
1455+
14501456
`settings=`*path*
14511457
: Specifies the path and name of the event settings file (of type JFC).
14521458
By default, the `default.jfc` file is used, which is located in

src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecorder.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import jdk.jfr.events.ActiveSettingEvent;
5555
import jdk.jfr.internal.consumer.EventLog;
5656
import jdk.jfr.internal.periodic.PeriodicEvents;
57+
import jdk.jfr.internal.query.Report;
5758
import jdk.jfr.internal.util.Utils;
5859

5960
public final class PlatformRecorder {
@@ -63,7 +64,6 @@ public final class PlatformRecorder {
6364
private static final List<FlightRecorderListener> changeListeners = new ArrayList<>();
6465
private final Repository repository;
6566
private final Thread shutdownHook;
66-
6767
private Timer timer;
6868
private long recordingCounter = 0;
6969
private RepositoryChunk currentChunk;
@@ -154,6 +154,10 @@ synchronized void setInShutDown() {
154154
this.inShutdown = true;
155155
}
156156

157+
synchronized boolean isInShutDown() {
158+
return this.inShutdown;
159+
}
160+
157161
// called by shutdown hook
158162
synchronized void destroy() {
159163
try {
@@ -174,6 +178,7 @@ synchronized void destroy() {
174178
}
175179
}
176180

181+
writeReports();
177182
JDKEvents.remove();
178183

179184
if (JVMSupport.hasJFR()) {
@@ -185,6 +190,16 @@ synchronized void destroy() {
185190
repository.clear();
186191
}
187192

193+
private void writeReports() {
194+
for (PlatformRecording recording : getRecordings()) {
195+
if (recording.isToDisk() && recording.getState() == RecordingState.STOPPED) {
196+
for (Report report : recording.getReports()) {
197+
report.print(recording.getStartTime(), recording.getStopTime());
198+
}
199+
}
200+
}
201+
}
202+
188203
synchronized long start(PlatformRecording recording) {
189204
// State can only be NEW or DELAYED because of previous checks
190205
Instant startTime = null;

src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecording.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import jdk.jfr.FlightRecorderListener;
5757
import jdk.jfr.Recording;
5858
import jdk.jfr.RecordingState;
59+
import jdk.jfr.internal.query.Report;
5960
import jdk.jfr.internal.util.Utils;
6061
import jdk.jfr.internal.util.ValueFormatter;
6162

@@ -83,6 +84,7 @@ public final class PlatformRecording implements AutoCloseable {
8384
private RecordingState state = RecordingState.NEW;
8485
private long size;
8586
private final LinkedList<RepositoryChunk> chunks = new LinkedList<>();
87+
private final List<Report> reports = new ArrayList<>();
8688
private volatile Recording recording;
8789
private TimerTask stopTask;
8890
private TimerTask startTask;
@@ -171,7 +173,10 @@ public boolean stop(String reason) {
171173
dumpStopped(dest);
172174
Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + getName() + "\" (" + getId() + ") to " + dest.getRealPathText());
173175
notifyIfStateChanged(newState, oldState);
174-
close(); // remove if copied out
176+
boolean reportOnExit = recorder.isInShutDown() && !reports.isEmpty();
177+
if (!reportOnExit) {
178+
close(); // remove if copied out, unless we are in shutdown and there are reports to report.
179+
}
175180
} catch(IOException e) {
176181
Logger.log(LogTag.JFR, LogLevel.ERROR,
177182
"Unable to complete I/O operation when dumping recording \"" + getName() + "\" (" + getId() + ")");
@@ -922,4 +927,12 @@ void removeNonExistantPaths() {
922927
}
923928
}
924929
}
930+
931+
public void addReport(Report report) {
932+
reports.add(report);
933+
}
934+
935+
public List<Report> getReports() {
936+
return reports;
937+
}
925938
}

src/jdk.jfr/share/classes/jdk/jfr/internal/consumer/AbstractEventStream.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public abstract class AbstractEventStream implements EventStream {
5757
private final List<Configuration> configurations;
5858
private final ParserState parserState = new ParserState();
5959
private volatile boolean closeOnComplete = true;
60+
private volatile boolean waitForChunks = true;
6061
private Dispatcher dispatcher;
6162
private boolean daemon = false;
6263

@@ -109,6 +110,14 @@ public final void setCloseOnComplete(boolean closeOnComplete) {
109110
this.closeOnComplete = closeOnComplete;
110111
}
111112

113+
public final void setWaitForChunks(boolean wait) {
114+
waitForChunks = wait;
115+
}
116+
117+
protected final boolean getWaitForChunks() {
118+
return waitForChunks;
119+
}
120+
112121
@Override
113122
public final void setStartTime(Instant startTime) {
114123
Objects.requireNonNull(startTime, "startTime");

src/jdk.jfr/share/classes/jdk/jfr/internal/consumer/EventDirectoryStream.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ protected void processRecursionSafe() throws IOException {
206206
Logger.log(LogTag.JFR_SYSTEM_PARSER, LogLevel.INFO, "Unexpected chunk with 0 ns duration");
207207
}
208208
}
209-
path = repositoryFiles.nextPath(currentChunkStartNanos + durationNanos, true);
209+
path = repositoryFiles.nextPath(currentChunkStartNanos + durationNanos, getWaitForChunks());
210210
if (path == null) {
211211
logStreamEnd("no more chunk files found.");
212212
return;

src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/AbstractDCmd.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public final String[] execute(String source, String arg, char delimiter) throws
7676
if (log) {
7777
Logger.log(LogTag.JFR_DCMD, LogLevel.DEBUG, "Executing " + this.getClass().getSimpleName() + ": " + arg);
7878
}
79-
ArgumentParser parser = new ArgumentParser(getArgumentInfos(), arg, delimiter);
79+
ArgumentParser parser = new ArgumentParser(getParseArguments(source), arg, delimiter);
8080
parser.parse();
8181
if (log) {
8282
Logger.log(LogTag.JFR_DCMD, LogLevel.DEBUG, "DCMD options: " + parser.getOptions());
@@ -98,6 +98,10 @@ public final String[] execute(String source, String arg, char delimiter) throws
9898
}
9999
}
100100

101+
protected Argument[] getParseArguments(String source) {
102+
return getArgumentInfos();
103+
}
104+
101105
// Diagnostic commands that are meant to be used interactively
102106
// should turn off events to avoid noise in the output.
103107
protected boolean isInteractive() {

src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdQuery.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -52,6 +52,7 @@ protected void execute(ArgumentParser parser) throws DCmdException {
5252
Configuration configuration = new Configuration();
5353
configuration.output = getOutput();
5454
configuration.endTime = Instant.now().minusSeconds(1);
55+
configuration.verboseTimespan = true;
5556
Boolean verbose = parser.getOption("verbose");
5657
if (verbose != null) {
5758
configuration.verboseHeaders = verbose;

src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdStart.java

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import java.nio.file.Paths;
3232
import java.text.ParseException;
3333
import java.time.Duration;
34+
import java.util.Arrays;
35+
import java.util.HashMap;
3436
import java.util.HashSet;
3537
import java.util.LinkedHashMap;
3638
import java.util.List;
@@ -51,6 +53,7 @@
5153
import jdk.jfr.internal.jfc.model.JFCModel;
5254
import jdk.jfr.internal.jfc.model.JFCModelException;
5355
import jdk.jfr.internal.jfc.model.XmlInput;
56+
import jdk.jfr.internal.query.Report;
5457
import jdk.jfr.internal.util.Utils;
5558

5659
/**
@@ -82,6 +85,7 @@ public void execute(ArgumentParser parser) throws DCmdException {
8285
Long flush = parser.getOption("flush-interval");
8386
Boolean dumpOnExit = parser.getOption("dumponexit");
8487
Boolean pathToGcRoots = parser.getOption("path-to-gc-roots");
88+
List<String> reports = parser.getOption("report-on-exit");
8589

8690
if (name != null) {
8791
try {
@@ -156,6 +160,7 @@ public void execute(ArgumentParser parser) throws DCmdException {
156160
if (duration != null && path == null) {
157161
path = resolvePath(recording, null).toString();
158162
}
163+
PlatformRecording pr = PrivateAccess.getInstance().getPlatformRecording(recording);
159164

160165
if (path != null) {
161166
try {
@@ -168,7 +173,6 @@ public void execute(ArgumentParser parser) throws DCmdException {
168173
// Decide destination filename at dump time
169174
// Purposely avoid generating filename in Recording#setDestination due to
170175
// security concerns
171-
PlatformRecording pr = PrivateAccess.getInstance().getPlatformRecording(recording);
172176
pr.setDumpDirectory(p);
173177
} else {
174178
dumpPath = resolvePath(recording, path);
@@ -185,8 +189,7 @@ public void execute(ArgumentParser parser) throws DCmdException {
185189
}
186190

187191
if (flush != null) {
188-
PlatformRecording p = PrivateAccess.getInstance().getPlatformRecording(recording);
189-
p.setFlushInterval(Duration.ofNanos(flush));
192+
pr.setFlushInterval(Duration.ofNanos(flush));
190193
}
191194

192195
if (maxSize != null) {
@@ -201,6 +204,10 @@ public void execute(ArgumentParser parser) throws DCmdException {
201204
recording.setDumpOnExit(dumpOnExit);
202205
}
203206

207+
if (reports != null) {
208+
addReports(pr, reports);
209+
}
210+
204211
if (delay != null) {
205212
Duration dDelay = Duration.ofNanos(delay);
206213
recording.scheduleStart(dDelay);
@@ -236,6 +243,20 @@ public void execute(ArgumentParser parser) throws DCmdException {
236243
}
237244
}
238245

246+
// Add report-on-exit for -XX:StartFlightRecording
247+
@Override
248+
protected Argument[] getParseArguments(String source) {
249+
Argument[] argumentInfo = getArgumentInfos();;
250+
if (!"internal".equals(source)) {
251+
return argumentInfo;
252+
}
253+
Argument[] newArray = Arrays.copyOf(argumentInfo, argumentInfo.length + 1);
254+
newArray[argumentInfo.length] = new Argument("report-on-exit",
255+
"Display views on exit. See 'jfr help view' for available views to report.",
256+
"STRING SET", false, true, null, true);
257+
return newArray;
258+
}
259+
239260
private LinkedHashMap<String, String> configureStandard(String[] settings) throws DCmdException {
240261
LinkedHashMap<String, String> s = LinkedHashMap.newLinkedHashMap(settings.length);
241262
for (String configName : settings) {
@@ -248,6 +269,24 @@ private LinkedHashMap<String, String> configureStandard(String[] settings) throw
248269
return s;
249270
}
250271

272+
private void addReports(PlatformRecording recording, List<String> reportNames) throws DCmdException {
273+
if (!recording.isToDisk()) {
274+
throw new DCmdException("Option report-on-exit can only be used when recording to disk.");
275+
}
276+
Map<String, Report> reportLookup = new HashMap<>();
277+
for (Report report : Report.getReports()) {
278+
reportLookup.put(report.name(), report);
279+
}
280+
for (String name : reportNames) {
281+
Report report = reportLookup.get(name);
282+
if (report != null) {
283+
recording.addReport(report);
284+
} else {
285+
throw new DCmdException("Unknown view '" + name + "' specified for report-on-exit. Use 'jfr help view' to see a list of available views.");
286+
}
287+
}
288+
}
289+
251290
private LinkedHashMap<String, String> configureExtended(String[] settings, ArgumentParser parser) throws DCmdException {
252291
JFCModel model = new JFCModel(l -> logWarning(l));
253292
for (String setting : settings) {
@@ -322,6 +361,7 @@ public String[] getStartupHelp() {
322361
"$DELIMITER", ",",
323362
"$DELIMITER_NAME", "comma",
324363
"$DIRECTORY", exampleDirectory(),
364+
"$REPORT_ON_EXIT", reportOnExit(),
325365
"$JFC_OPTIONS", jfcOptions()
326366
);
327367
return Utils.format(helpTemplate(), parameters).lines().toArray(String[]::new);
@@ -336,6 +376,7 @@ public String[] getHelp() {
336376
"$DELIMITER", " ",
337377
"$DELIMITER_NAME", "whitespace",
338378
"$DIRECTORY", exampleDirectory(),
379+
"$REPORT_ON_EXIT", "",
339380
"$JFC_OPTIONS", jfcOptions()
340381
);
341382
return Utils.format(helpTemplate(), parameters).lines().toArray(String[]::new);
@@ -404,7 +445,7 @@ Virtual Machine (JVM) shuts down. If set to 'true' and no value
404445
'profile', then the information collected includes the stack
405446
trace from where the potential leaking object was allocated.
406447
(BOOLEAN, false)
407-
448+
$REPORT_ON_EXIT
408449
settings (Optional) Name of the settings file that identifies which events
409450
to record. To specify more than one file, use the settings
410451
parameter repeatedly. Include the path if the file is not in
@@ -456,6 +497,17 @@ Virtual Machine (JVM) shuts down. If set to 'true' and no value
456497
""";
457498
}
458499

500+
private static String reportOnExit() {
501+
return
502+
"""
503+
504+
report-on-exit Specifies the name of the view to display when the Java Virtual
505+
Machine (JVM) shuts down. This option is not available if the
506+
disk option is set to false. For a list of available views,
507+
see 'jfr help view'. By default, no report is generated.
508+
""";
509+
}
510+
459511
private static String jfcOptions() {
460512
try {
461513
StringBuilder sb = new StringBuilder();

src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/DCmdView.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -56,6 +56,7 @@ protected void execute(ArgumentParser parser) throws DCmdException {
5656
return;
5757
}
5858
Configuration configuration = new Configuration();
59+
configuration.verboseTimespan = true;
5960
configuration.output = getOutput();
6061
configuration.endTime = Instant.now().minusSeconds(1);
6162
String view = parser.getOption("view");

src/jdk.jfr/share/classes/jdk/jfr/internal/dcmd/QueryRecording.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -43,7 +43,7 @@
4343
* Helper class that holds recording chunks alive during a query. It also helps
4444
* out with configuration shared by DCmdView and DCmdQuery
4545
*/
46-
final class QueryRecording implements AutoCloseable {
46+
public final class QueryRecording implements AutoCloseable {
4747
private final long DEFAULT_MAX_SIZE = 32 * 1024 * 1024L;
4848
private final long DEFAULT_MAX_AGE = 60 * 10;
4949

@@ -52,6 +52,16 @@ final class QueryRecording implements AutoCloseable {
5252
private final EventStream eventStream;
5353
private final Instant endTime;
5454

55+
public QueryRecording(Instant startTime, Instant endTime) throws IOException {
56+
this.recorder = PrivateAccess.getInstance().getPlatformRecorder();
57+
this.endTime = endTime;
58+
this.chunks = acquireChunks(startTime);
59+
if (chunks.isEmpty()) {
60+
throw new IOException("No recording data found on disk.");
61+
}
62+
eventStream = makeStream(startTime);
63+
}
64+
5565
public QueryRecording(Configuration configuration, ArgumentParser parser) throws IOException, DCmdException, UserDataException {
5666
if (!FlightRecorder.isInitialized()) {
5767
throw new DCmdException("No recording data available. Start a recording with JFR.start.");

0 commit comments

Comments
 (0)