This commit is contained in:
David Grieve 2021-01-30 11:27:15 -05:00
Родитель f0d67a5c99
Коммит 38942cea08
10 изменённых файлов: 264 добавлений и 171 удалений

12
CONTRIBUTING.md Normal file
Просмотреть файл

@ -0,0 +1,12 @@
This project welcomes contributions and suggestions. Most contributions require you to
agree to a Contributor License Agreement (CLA) declaring that you have the right to,
and actually do, grant us the rights to use your contribution. For details, visit
https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need
to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the
instructions provided by the bot. You will only need to do this once across all repositories using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

Просмотреть файл

@ -43,18 +43,3 @@ The build is vanilla Maven.
`mvn compile` - compile the source code
`mvn test` - run unit tests (this project uses TestNG)
`mvn package` - build the .jar file
# Contribute
This project welcomes contributions and suggestions. Most contributions require you to
agree to a Contributor License Agreement (CLA) declaring that you have the right to,
and actually do, grant us the rights to use your contribution. For details, visit
https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need
to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the
instructions provided by the bot. You will only need to do this once across all repositories using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

Просмотреть файл

@ -60,12 +60,12 @@ public class FlightRecorderConnection {
* @return A {@code FlightRecorderConnection}.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws InstanceNotFoundException The FlightRecorder MBean is not registered on the target JVM.
* @throws java.lang.InternalError Wraps a {@code javax.management.MalformedObjectNameException}
* @throws JfrStreamingException Wraps a {@code javax.management.MalformedObjectNameException}
* and indicates a bug in this class.
* @throws NullPointerException The {@code mBeanServerConnection} parameter is {@code null}.
*/
public static FlightRecorderConnection connect(MBeanServerConnection mBeanServerConnection)
throws IOException, InstanceNotFoundException
throws IOException, InstanceNotFoundException, JfrStreamingException
{
Objects.requireNonNull(mBeanServerConnection);
try {
@ -75,7 +75,7 @@ public class FlightRecorderConnection {
} catch (MalformedObjectNameException e) {
// Not expected to happen. This exception comes from the ObjectName constructor. If
// JFR_OBJECT_NAME is malformed, then this is an internal bug.
throw new InternalError(JFR_OBJECT_NAME, e);
throw new JfrStreamingException(JFR_OBJECT_NAME, e);
}
}
@ -102,32 +102,26 @@ public class FlightRecorderConnection {
* the {@link #newRecording(RecordingOptions, RecordingConfiguration)} method
* @return The id of the recording.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws java.lang.InternalError Wraps an {@code javax.management.InstanceNotFoundException},
* @throws JfrStreamingException Wraps an {@code javax.management.InstanceNotFoundException},
* a {@code javax.management.MBeanException} or a {@code javax.management.ReflectionException}
* and indicates an issue with the FlightRecorderMXBean in the JVM.
* The cause may also be a {@code javax.management.openmbean.OpenDataException}
* which indicates a bug in the code of this class.
*/
long startRecording(RecordingOptions recordingOptions, RecordingConfiguration recordingConfiguration)
throws IOException {
/* package-scoped */ long startRecording(RecordingOptions recordingOptions, RecordingConfiguration recordingConfiguration)
throws IOException, JfrStreamingException {
try {
final long id = (long) connection.invoke(
flightRecorder,
"newRecording",
new Object[]{},
new String[]{}
);
Object[] args = new Object[]{};
String[] argTypes = new String[]{};
final long id = (long) connection.invoke(flightRecorder, "newRecording", args, argTypes);
if (recordingConfiguration != null) {
String predefinedConfiguration = recordingConfiguration.getName();
if (predefinedConfiguration != null && predefinedConfiguration.trim().length() > 0) {
connection.invoke(
flightRecorder,
"setPredefinedConfiguration",
new Object[]{id, predefinedConfiguration},
new String[]{long.class.getName(), String.class.getName()}
);
args = new Object[]{id, predefinedConfiguration};
argTypes = new String[]{long.class.getName(), String.class.getName()};
connection.invoke(flightRecorder, "setPredefinedConfiguration", args, argTypes);
}
}
@ -135,26 +129,20 @@ public class FlightRecorderConnection {
Map<String,String> options = recordingOptions.getRecordingOptions();
if (options != null && !options.isEmpty()) {
TabularData recordingOptionsParam = makeOpenData(options);
connection.invoke(
flightRecorder,
"setRecordingOptions",
new Object[]{id, recordingOptionsParam},
new String[]{long.class.getName(), TabularData.class.getName()}
);
args = new Object[]{id, recordingOptionsParam};
argTypes = new String[]{long.class.getName(), TabularData.class.getName()};
connection.invoke(flightRecorder, "setRecordingOptions", args, argTypes);
}
}
connection.invoke(
flightRecorder,
"startRecording",
new Object[]{id},
new String[]{long.class.getName()}
);
args = new Object[]{id};
argTypes = new String[]{long.class.getName()};
connection.invoke(flightRecorder, "startRecording", args, argTypes);
return id;
} catch (OpenDataException|InstanceNotFoundException|MBeanException|ReflectionException e) {
// In theory, we should never get these.
throw new InternalError(e.getMessage(), e);
throw new JfrStreamingException(e.getMessage(), e);
}
}
@ -162,15 +150,17 @@ public class FlightRecorderConnection {
* Stop a recording. This method is called from the {@link Recording#stop()} method.
* @param id The id of the recording.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws java.lang.InternalError Wraps an {@code javax.management.InstanceNotFoundException},
* @throws JfrStreamingException Wraps an {@code javax.management.InstanceNotFoundException},
* a {@code javax.management.MBeanException} or a {@code javax.management.ReflectionException}
* and indicates an issue with the FlightRecorderMXBean in the JVM.
*/
void stopRecording(long id) throws IOException {
/* package-scoped */ void stopRecording(long id) throws IOException, JfrStreamingException {
try {
connection.invoke(flightRecorder, "stopRecording", new Object[]{id}, new String[]{long.class.getName()});
Object[] args = new Object[]{id};
String[] argTypes = new String[]{long.class.getName()};
connection.invoke(flightRecorder, "stopRecording", args, argTypes);
} catch (InstanceNotFoundException|MBeanException|ReflectionException e) {
throw new InternalError(e.getMessage(), e);
throw new JfrStreamingException(e.getMessage(), e);
}
}
@ -180,13 +170,15 @@ public class FlightRecorderConnection {
* @param id The id of the recording.
* @param outputFile the system-dependent file name where data is written, not {@code null}
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws InternalError Wraps a {@code javax.management.JMException}. See {@link InternalError}.
* @throws JfrStreamingException Wraps a {@code javax.management.JMException}. See {@link JfrStreamingException}.
*/
void dumpRecording(long id, String outputFile) throws IOException {
/* package-scoped */ void dumpRecording(long id, String outputFile) throws IOException, JfrStreamingException {
try {
connection.invoke(flightRecorder, "copyTo", new Object[]{id,outputFile}, new String[]{long.class.getName(),String.class.getName()});
Object[] args = new Object[]{id,outputFile};
String[] argTypes = new String[]{long.class.getName(),String.class.getName()};
connection.invoke(flightRecorder, "copyTo", args, argTypes);
} catch (InstanceNotFoundException|MBeanException|ReflectionException e) {
throw new InternalError(e.getMessage(), e);
throw new JfrStreamingException(e.getMessage(), e);
}
}
@ -197,13 +189,15 @@ public class FlightRecorderConnection {
* @param id The id of the recording being cloned.
* @param stop Whether to stop the cloned recording.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws InternalError Wraps a {@code javax.management.JMException}. See {@link InternalError}.
* @throws JfrStreamingException Wraps a {@code javax.management.JMException}. See {@link JfrStreamingException}.
*/
long cloneRecording(long id, boolean stop) throws IOException {
/* package-scoped */ long cloneRecording(long id, boolean stop) throws IOException, JfrStreamingException {
try {
return (long)connection.invoke(flightRecorder, "cloneRecording", new Object[]{id,stop}, new String[]{long.class.getName(),boolean.class.getName()});
Object[] args = new Object[]{id,stop};
String[] argTypes = new String[]{long.class.getName(),boolean.class.getName()};
return (long)connection.invoke(flightRecorder, "cloneRecording", args, argTypes);
} catch (InstanceNotFoundException|MBeanException|ReflectionException e) {
throw new InternalError(e.getMessage(), e);
throw new JfrStreamingException(e.getMessage(), e);
}
}
@ -227,14 +221,14 @@ public class FlightRecorderConnection {
* @param blockSize The number of bytes to read at a time.
* @return A {@code InputStream} of the Java Flight Recording data.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws InternalError Wraps an {@code javax.management.InstanceNotFoundException},
* @throws JfrStreamingException Wraps an {@code javax.management.InstanceNotFoundException},
* a {@code javax.management.MBeanException} or a {@code javax.management.ReflectionException}
* and indicates an issue with the FlightRecorderMXBean in the JVM.
* The cause may also be a {@code javax.management.openmbean.OpenDataException}
* which indicates a bug in the code of this class.
*/
InputStream getStream(long id, Instant startTime, Instant endTime, long blockSize)
throws IOException {
/* package-scoped */ InputStream getStream(long id, Instant startTime, Instant endTime, long blockSize)
throws IOException, JfrStreamingException {
Map<String,String> options = new HashMap<>();
if (startTime != null) options.put("startTime", startTime.toString());
if (endTime != null) options.put("endTime", endTime.toString());
@ -242,10 +236,12 @@ public class FlightRecorderConnection {
try {
TabularData streamOptions = makeOpenData(options);
long streamId = (long) connection.invoke(flightRecorder, "openStream", new Object[]{id, streamOptions}, new String[]{long.class.getName(), TabularData.class.getName()});
Object[] args = new Object[]{id, streamOptions};
String[] argTypes = new String[]{long.class.getName(), TabularData.class.getName()};
long streamId = (long) connection.invoke(flightRecorder, "openStream", args, argTypes);
return new JfrStream(connection, flightRecorder, streamId);
} catch(OpenDataException|InstanceNotFoundException|MBeanException|ReflectionException e) {
throw new InternalError(e.getMessage(), e);
throw new JfrStreamingException(e.getMessage(), e);
}
}
@ -253,15 +249,17 @@ public class FlightRecorderConnection {
* Close the recording. This method is called from the {@link Recording#close()} method.
* @param id The id of the recording.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws java.lang.InternalError Wraps an {@code javax.management.InstanceNotFoundException},
* @throws JfrStreamingException Wraps an {@code javax.management.InstanceNotFoundException},
* a {@code javax.management.MBeanException} or a {@code javax.management.ReflectionException}
* and indicates an issue with the FlightRecorderMXBean in the JVM.
*/
void closeRecording(long id) throws IOException {
/* package-scoped */ void closeRecording(long id) throws IOException, JfrStreamingException {
try {
connection.invoke(flightRecorder, "closeRecording", new Object[]{id}, new String[]{long.class.getName()});
Object[] args = new Object[]{id};
String[] argTypes = new String[]{long.class.getName()};
connection.invoke(flightRecorder, "closeRecording", args, argTypes);
} catch (InstanceNotFoundException|MBeanException|ReflectionException e) {
throw new InternalError(e.getMessage(), e);
throw new JfrStreamingException(e.getMessage(), e);
}
}

Просмотреть файл

@ -15,7 +15,6 @@ import javax.management.ReflectionException;
*/
class JfrStream extends InputStream {
/* A default value for blockSize used by FlightRecorderMXBean#readStream(long) */
private static final long DEFAULT_BLOCKSIZE = Long.getLong("jfr.stream.blocksize", 50000L);
@ -29,6 +28,8 @@ class JfrStream extends InputStream {
private byte[] buffer;
private int index = 0;
private boolean EOF = false;
// There is a recording id and an id you get from the recording for the stream.
// streamId is the id for the stream.
private final long streamid;
private final MBeanServerConnection connection;
private final ObjectName flightRecorder;

Просмотреть файл

@ -3,12 +3,11 @@ package com.microsoft.jfr;
import javax.management.JMException;
/**
* An InternalError is a wrapper around specific {@code javax.management.JMException}
* An JfrStreamingException is a wrapper around specific {@code javax.management.JMException}
* instances which might be thrown, either directly or indirectly, by methods of this package.
* Exceptions of this type are not expected from a well behaved JVM. This class is
* a subclass of {@code java.lang.Error} and may be treated as an unchecked exception.
* Exceptions of this type are not expected from a well behaved JVM.
*
* The cause of an {@code InternalError} will be one of the following:
* The cause of an {@code JfrStreamingException} will be one of the following:
* <dl>
* <dt><em>javax.management.InstanceNotFoundException</em></dt>
* <dd>The FlightRecorderMXBean is not found on the MBean server. This could happen
@ -32,9 +31,9 @@ import javax.management.JMException;
* were not met. This cause indicates a bug in the com.microsoft.censum.jfr package code.</dd>
* </dl>
*/
public class InternalError extends Error {
public class JfrStreamingException extends Exception {
InternalError(String message, JMException cause) {
JfrStreamingException(String message, JMException cause) {
super(message, cause);
}

Просмотреть файл

@ -20,7 +20,15 @@ public class Recording implements AutoCloseable {
/**
* A {@code Recording} may be in one of these states. Note that a {@code Recording} is
* no longer usable once it is in the {@code CLOSED} state.
* no longer usable once it is in the {@code CLOSED} state. Valid state transitions are
* <code>
* NEW -> [RECORDING, STOPPED, CLOSED]
* RECORDING -> [RECORDING, STOPPED, CLOSED]
* STOPPED -> [RECORDING, STOPPED, CLOSED]
* CLOSED -> [CLOSED]
* </code>
* Calling a method on {@code Recording} that would cause an invalid transition
* will raise an IllegalStateException.
*/
public enum State {
/**
@ -43,7 +51,17 @@ public class Recording implements AutoCloseable {
}
// Format for IllegalStateException that this class might throw
// {0} is the state the code is trying to transition to.
// {1} are the states that the instance could be in for a valid transition.
private final static MessageFormat illegalStateFormat = new MessageFormat("Recording state {0} not in [{1}]");
/**
* Helper for formatting the message for an IllegalStateException that may be thrown by methods of this class.
* @param actual This is the state that the Recording is in currently
* @param expected This is the state that the Recording should be in for a valid transition to occur
* @param others Additional <em>expected</em> states
* @return
*/
private static String createIllegalStateExceptionMessage(State actual, State expected, State... others) {
String[] args = new String[]{actual.name(), expected.name()};
if (others != null) {
@ -67,7 +85,7 @@ public class Recording implements AutoCloseable {
* @param recordingOptions The options to be used for the recording
* @param recordingConfiguration The settings for events to be collected by the recording
*/
/*package*/ Recording(
/* package-scoped */ Recording(
FlightRecorderConnection connection,
RecordingOptions recordingOptions,
RecordingConfiguration recordingConfiguration) {
@ -81,7 +99,7 @@ public class Recording implements AutoCloseable {
* Get the recording id. The recording does not have an id until the recording is started.
* @return The recording id, or {@code -1} if the recording was never started.
*/
public long getId() throws IllegalStateException {
public long getId() {
return id;
}
@ -89,11 +107,11 @@ public class Recording implements AutoCloseable {
* Start a recording. A recording may not be started after it is closed.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws IllegalStateException This {@code Recording} is closed.
* @throws InternalError Wraps a {@code javax.management.JMException}. See {@link InternalError}.
* @throws JfrStreamingException Wraps a {@code javax.management.JMException}. See {@link JfrStreamingException}.
* @return The recording id.
*/
public long start() throws IOException, IllegalStateException {
// state machine transitions: NEW -> RECORDING or STOPPED -> RECORDING, otherwise remain in state
public long start() throws IOException, IllegalStateException, JfrStreamingException {
// state transitions: NEW -> RECORDING or STOPPED -> RECORDING, otherwise remain in state
State oldState = state.getAndUpdate(s -> s == State.NEW || s == State.STOPPED ? State.RECORDING : s);
if (oldState == State.NEW || oldState == State.STOPPED) {
@ -108,10 +126,10 @@ public class Recording implements AutoCloseable {
* Stop a recording.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws IllegalStateException If the {@code Recording} is closed.
* @throws InternalError Wraps a {@code javax.management.JMException}. See {@link InternalError}.
* @throws JfrStreamingException Wraps a {@code javax.management.JMException}. See {@link JfrStreamingException}.
*/
public void stop() throws IOException, IllegalStateException {
// state machine transitions: RECORDING -> STOPPED, otherwise remain in state
public void stop() throws IOException, IllegalStateException, JfrStreamingException {
// state transitions: RECORDING -> STOPPED, otherwise remain in state
State oldState = state.getAndUpdate(s -> s == State.RECORDING ? State.STOPPED : s);
if (oldState == State.RECORDING) {
connection.stopRecording(id);
@ -126,10 +144,10 @@ public class Recording implements AutoCloseable {
* @param outputFile the system-dependent file name where data is written, not {@code null}
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws IllegalStateException If the {@code Recording} has not been started, or has been closed.
* @throws InternalError Wraps a {@code javax.management.JMException}. See {@link InternalError}.
* @throws JfrStreamingException Wraps a {@code javax.management.JMException}. See {@link JfrStreamingException}.
* @throws NullPointerException If the {@code outputFile} argument is null.
*/
public void dump(String outputFile) throws IOException, IllegalStateException {
public void dump(String outputFile) throws IOException, IllegalStateException, JfrStreamingException {
Objects.requireNonNull(outputFile, "outputFile may not be null");
State currentState = state.get();
if (currentState == State.RECORDING || currentState == State.STOPPED) {
@ -147,9 +165,9 @@ public class Recording implements AutoCloseable {
* @return The cloned recording.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws IllegalStateException If the {@code Recording} has not been started, or has been closed.
* @throws InternalError Wraps a {@code javax.management.JMException}. See {@link InternalError}.
* @throws JfrStreamingException Wraps a {@code javax.management.JMException}. See {@link JfrStreamingException}.
*/
public Recording clone(boolean stop) throws IOException {
public Recording clone(boolean stop) throws IOException, JfrStreamingException {
State currentState = state.get();
if (currentState == State.RECORDING || currentState == State.STOPPED) {
long newId = connection.cloneRecording(id, stop);
@ -170,10 +188,11 @@ public class Recording implements AutoCloseable {
* @return An {@code InputStream}, or {@code null} if no data is available in the interval.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws IllegalStateException If the {@code Recording} has not been stopped.
* @throws InternalError Wraps a {@code javax.management.JMException}. See {@link InternalError}.
* @throws JfrStreamingException Wraps a {@code javax.management.JMException}. See {@link JfrStreamingException}.
* @see JfrStream#getDefaultBlockSize()
*/
public InputStream getStream(Instant startTime, Instant endTime) throws IOException, IllegalStateException {
public InputStream getStream(Instant startTime, Instant endTime)
throws IOException, IllegalStateException, JfrStreamingException {
return getStream(startTime, endTime, JfrStream.getDefaultBlockSize());
}
@ -190,11 +209,11 @@ public class Recording implements AutoCloseable {
* @return An {@code InputStream}, or {@code null} if no data is available in the interval.
* @throws IOException A communication problem occurred when talking to the MBean server.
* @throws IllegalStateException If the {@code Recording} has not been stopped.
* @throws InternalError Wraps a {@code javax.management.JMException}. See {@link InternalError}.
* @throws JfrStreamingException Wraps a {@code javax.management.JMException}. See {@link JfrStreamingException}.
*/
public InputStream getStream(Instant startTime, Instant endTime, long blockSize)
throws IOException, IllegalStateException {
// state machine transitions: remain in state
throws IOException, IllegalStateException, JfrStreamingException {
// state transitions: remain in state
State currentState = state.get();
if (currentState == State.STOPPED) {
return connection.getStream(id, startTime, endTime, blockSize);
@ -213,7 +232,7 @@ public class Recording implements AutoCloseable {
@Override
public void close() throws IOException {
// state machine transitions: any -> CLOSED
// state transitions: any -> CLOSED
State oldState = state.getAndSet(State.CLOSED);
if (oldState == State.RECORDING) {
try {

Просмотреть файл

@ -18,6 +18,22 @@ import java.util.stream.Stream;
* {@link FlightRecorderConnection#newRecording(RecordingOptions, RecordingConfiguration)}. A {@code Configuration}
* is immutable which prevents attempts at changing the configuration while a recording
* is in progress.
*
* <b>A note on the API</b>
* It is enticing to want the Builder to take a {@code java.time.Duration} instead of a String for
* the {@code maxAge} or {@code duration} API, or have the {@code maxAge} API take a long, or pass
* a boolean for the others. The problem with this is twofold. First, it makes it difficult for the user of
* the library to set values through system properties since the String value has to be converted. This is
* not really an issue for a long or boolean arg, but it adds a lot of code for handling a Duration. And
* it makes it difficult to document what value a user should provide for a system property. These are not
* insurmountable problems. But they do add error-prone complexity. Secondly, the arguments to
* FlightRecorderMXBean are String, so there would need to be a conversion from the Duration/boolean/long to
* a String anyway. And then what API would you have for Recording? If Recording#getDuration returns a
* Duration and this is called from the underlying code, then the underlying code has to do the conversion
* and this creates a tight coupling between Recording and the underlying code. If Builder takes a Duration
* and Recording returns a String, then the two APIs are not parallel (violates the rule of least surprise).
* Sticking to a String based API resolves these issues. But it does mean that the Builder needs to validate
* the args and potentially throw IllegalArgumentException. String makes the overall code so much simpler.
*/
public class RecordingOptions {
@ -35,7 +51,7 @@ public class RecordingOptions {
DISK("disk", "false"),
DURATION("duration", NO_LIMIT);
private Option(String name, String defaultValue) {
Option(String name, String defaultValue) {
this.name = name;
this.defaultValue = defaultValue;
}
@ -43,22 +59,20 @@ public class RecordingOptions {
private final String defaultValue; /* not null! */
}
/* If the arg is null or an empty String, return the Option's default. */
private static String normalize(String arg, Option option) {
return arg == null || (arg = arg.trim()).isEmpty() ? option.defaultValue : arg;
}
/**
* Builder for {@link RecordingOptions}.
* Builder for {@link RecordingOptions}. The builder builds up a hash table of options.
* These options correspond to the recording options for a
* {@code jdk.management.jfr.FlightRecorderMXBean}.
*/
public static class Builder {
// The builder builds up a hash table of options. These options
// correspond to the recording options for a FlightRecorderMXBean
// Even though the input to the builder methods are not all String,
// the option value is converted to a String. This simplifies
// the code in the RecordingOption constructor, but makes the
// RecordingOption getter methods a bit strange since they have to
// covert from the String back to the raw value. The getter methods
// could return String, but then you have the builder taking one type
// and the getter returning a different type, which would be surprising.
private final Map<Option, String> options = new HashMap<>();
/**
* Constructor for a {@code Builder}.
*/
@ -75,7 +89,7 @@ public class RecordingOptions {
public Builder name(String name) {
options.put(
Option.NAME,
name != null ? name.trim() : Option.NAME.defaultValue
normalize(name,Option.NAME)
);
return this;
}
@ -125,7 +139,7 @@ public class RecordingOptions {
public Builder maxSize(String maxSize) throws IllegalArgumentException {
long value = 0L;
try {
String numVal = maxSize == null || maxSize.trim().length() == 0 ? Option.MAX_SIZE.defaultValue : maxSize.trim();
String numVal = normalize(maxSize, Option.MAX_SIZE);
value = Long.parseLong(numVal);
if (value < 0L) {
throw new IllegalArgumentException("maxSize: " + value + " < 0");
@ -169,9 +183,7 @@ public class RecordingOptions {
public Builder destination(String destination) {
options.put(
Option.DESTINATION,
destination != null
? destination.trim()
: Option.DESTINATION.defaultValue
normalize(destination,Option.DESTINATION)
);
return this;
}
@ -186,7 +198,7 @@ public class RecordingOptions {
public Builder disk(String disk) {
options.put(
Option.DISK,
Boolean.valueOf(disk).toString()
Boolean.valueOf(normalize(disk, Option.DISK)).toString()
);
return this;
}
@ -239,8 +251,10 @@ public class RecordingOptions {
* @param builder The builder that was used to parameterize the configuration
*/
private RecordingOptions(Builder builder) {
// Note we're converting from Map<Option<?>,String> to Map<String,String>
final Map<Option,String> options = builder.options;
//
// For each option,
// if the option is not the default value
@ -330,7 +344,7 @@ public class RecordingOptions {
* according to FlightRecorderMXBean.
* @return A read-only map of the recording options.
*/
/*package*/ Map<String,String> getRecordingOptions() {
/* package-scoped */ Map<String,String> getRecordingOptions() {
return recordingOptions;
}
@ -340,30 +354,30 @@ public class RecordingOptions {
private final Map<String,String> recordingOptions;
// format for FlightRecorderMXBean maxAge and duration recording options
private static final Pattern pattern = Pattern.compile("([-+]?\\d+)\\s*(\\w*)");
private static final Pattern durationPattern = Pattern.compile("([-+]?\\d+)\\s*(\\w*)");
// If the durationString arg is a valid format, return the arg properly formatted for FlightRecorderMXBean.
// The expected format is a positive number, followed by a space (optional),
// followed by the units (one of ns, us, ms, s, m, h, d).
private static String validateDuration(Option option, String durationString) throws IllegalArgumentException {
if (durationString == null || durationString.trim().length() == 0) {
if (durationString == null || durationString.trim().isEmpty()) {
return option.defaultValue;
}
Matcher matcher = pattern.matcher(durationString);
if (matcher.matches()) {
Matcher durationStringMatcher = durationPattern.matcher(durationString);
if (durationStringMatcher.matches()) {
final long value;
try {
value = Long.parseLong(matcher.group(1));
value = Long.parseLong(durationStringMatcher.group(1));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
}
if (value >= 0L) {
final String units = matcher.group(2);
final String units = durationStringMatcher.group(2);
switch (units) {
case "":
return Long.toString(value);

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
/**
* This package provides API for controlling Java Flight Recordings (JFR) through Java Management Extensions (JMX).
*
* JDK 9 introduced the {@code jdk.jfr} API which is not available in JDK 8. The
* {@code jdk.management.jfr.FlightRecorderMXBean} is available in Java 8 and higher.
* By relying on JMX and the {@code jdk.management.jfr.FlightRecorderMXBean},
* the {@code com.microsoft.jfr} package provides access to JFR on local or remote JVMs.
*/
package com.microsoft.jfr;

Просмотреть файл

@ -1,6 +1,7 @@
package com.microsoft.jfr;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.HashMap;
import java.util.Map;
@ -19,14 +20,14 @@ public class RecordingOptionsTest {
{null, ""}
};
}
@org.testng.annotations.Test(dataProvider = "nameValues")
@Test(dataProvider = "nameValues")
public void testGetName(String[] args) {
String expected = args[1];
RecordingOptions opts = new RecordingOptions.Builder().name(args[0]).build();
assertEquals(opts.getName(), expected);
}
@org.testng.annotations.Test
@Test
public void testGetNameDefault() {
String expected = "";
RecordingOptions opts = new RecordingOptions.Builder().build();
@ -50,13 +51,13 @@ public class RecordingOptionsTest {
{null, "0"}
};
}
@org.testng.annotations.Test(dataProvider = "maxAgeGoodValues")
@Test(dataProvider = "maxAgeGoodValues")
public void testGetMaxAge(String[] args) {
RecordingOptions opts = new RecordingOptions.Builder().maxAge(args[0]).build();
assertEquals(opts.getMaxAge(), args[1]);
}
@org.testng.annotations.Test
@Test
public void testGetMaxAgeDefault() {
String expected = "0";
RecordingOptions opts = new RecordingOptions.Builder().build();
@ -77,7 +78,7 @@ public class RecordingOptionsTest {
{"3 _ ms"}
};
}
@org.testng.annotations.Test(dataProvider = "maxAgeBadValues", expectedExceptions = {IllegalArgumentException.class})
@Test(dataProvider = "maxAgeBadValues", expectedExceptions = {IllegalArgumentException.class})
public void testGetMaxAgeNegative(String[] args) {
RecordingOptions opts = new RecordingOptions.Builder().maxAge(args[0]).build();
}
@ -95,13 +96,13 @@ public class RecordingOptionsTest {
{null, "0"}
};
}
@org.testng.annotations.Test(dataProvider = "maxSizeGoodValues")
@Test(dataProvider = "maxSizeGoodValues")
public void testGetMaxSize(String[] args) {
RecordingOptions opts = new RecordingOptions.Builder().maxSize(args[0]).build();
assertEquals(opts.getMaxSize(), args[1]);
}
@org.testng.annotations.Test
@Test
public void testGetMaxSizeDefault() {
String expected = "0";
RecordingOptions opts = new RecordingOptions.Builder().build();
@ -117,26 +118,26 @@ public class RecordingOptionsTest {
{"0xBEEF"}
};
}
@org.testng.annotations.Test(dataProvider = "maxSizeBadValues", expectedExceptions = {IllegalArgumentException.class})
@Test(dataProvider = "maxSizeBadValues", expectedExceptions = {IllegalArgumentException.class})
public void testGetMaxSizeNegative(String[] args) {
RecordingOptions opts = new RecordingOptions.Builder().maxSize(args[0]).build();
}
@org.testng.annotations.Test
@Test
public void testGetDumpOnExit() {
String expected = "true";
RecordingOptions opts = new RecordingOptions.Builder().dumpOnExit(expected).build();
assertEquals(opts.getDumpOnExit(), expected);
}
@org.testng.annotations.Test
@Test
public void testGetDumpOnExitDefault() {
String expected = "false";
RecordingOptions opts = new RecordingOptions.Builder().build();
assertEquals(opts.getDumpOnExit(), expected);
}
@org.testng.annotations.Test
@Test
public void testGetDumpOnExitBadValue() {
String expected = "false";
RecordingOptions opts = new RecordingOptions.Builder().dumpOnExit("BAD_VALUE").build();
@ -153,35 +154,35 @@ public class RecordingOptionsTest {
{null, ""}
};
}
@org.testng.annotations.Test(dataProvider = "destinationValues")
@Test(dataProvider = "destinationValues")
public void testGetDestination(String[] args) {
String expected = args[1];
RecordingOptions opts = new RecordingOptions.Builder().destination(args[0]).build();
assertEquals(opts.getDestination(), expected);
}
@org.testng.annotations.Test
@Test
public void testGetDestinationDefault() {
String expected = "";
RecordingOptions opts = new RecordingOptions.Builder().build();
assertEquals(opts.getDestination(), expected);
}
@org.testng.annotations.Test
@Test
public void testGetDisk() {
String expected = "true";
RecordingOptions opts = new RecordingOptions.Builder().disk(expected).build();
assertEquals(opts.getDisk(), expected);
}
@org.testng.annotations.Test
@Test
public void testGetDiskDefault() {
String expected = "false";
RecordingOptions opts = new RecordingOptions.Builder().build();
assertEquals(opts.getDisk(), expected);
}
@org.testng.annotations.Test
@Test
public void testGetDiskBadValue() {
String expected = "false";
RecordingOptions opts = new RecordingOptions.Builder().disk("BAD_VALUE").build();
@ -205,14 +206,14 @@ public class RecordingOptionsTest {
{null, "0"}
};
}
@org.testng.annotations.Test(dataProvider = "durationGoodValues")
@Test(dataProvider = "durationGoodValues")
public void testGetDuration(String[] args) {
String expected = args[1];
RecordingOptions opts = new RecordingOptions.Builder().duration(args[0]).build();
assertEquals(opts.getDuration(), expected);
}
@org.testng.annotations.Test
@Test
public void testGetDurationDefault() {
String expected = "0";
RecordingOptions opts = new RecordingOptions.Builder().build();
@ -233,12 +234,12 @@ public class RecordingOptionsTest {
{"3 _ ms"}
};
}
@org.testng.annotations.Test(dataProvider = "durationBadValues", expectedExceptions = {IllegalArgumentException.class})
@Test(dataProvider = "durationBadValues", expectedExceptions = {IllegalArgumentException.class})
public void testGetDurationNegative(String[] args) {
RecordingOptions opts = new RecordingOptions.Builder().duration(args[0]).build();
}
@org.testng.annotations.Test
@Test
public void testGetRecordingOptions() {
Map<String,String> expected = new HashMap<>();
expected.put("name", "test");
@ -260,7 +261,7 @@ public class RecordingOptionsTest {
assertEquals(opts.getRecordingOptions(), expected);
}
@org.testng.annotations.Test
@Test
public void testGetRecordingOptionsDefaults() {
Map<String,String> expected = new HashMap<>();
// Due to a bug, some JVMs default "disk=true". So include "disk=false" (the documented default)

Просмотреть файл

@ -39,7 +39,6 @@ import static org.testng.Assert.*;
public class RecordingTest {
FlightRecorderConnection flightRecorderConnection = null;
Set<Path> jfrFilesToSave = new HashSet<>();
@BeforeTest
public void setup() {
@ -51,6 +50,8 @@ public class RecordingTest {
} catch (IOException e) {
// possible that this can be thrown, but should not happen in this context
fail("IOException not expected", e);
} catch (JfrStreamingException reallyBad) {
fail ("something really bad happened", reallyBad);
}
}
@ -60,7 +61,6 @@ public class RecordingTest {
Path userDir = Paths.get(System.getProperty("user.dir"));
Files.list(userDir)
.filter(Files::isRegularFile)
.filter(path -> !jfrFilesToSave.contains(path))
.filter(path -> path.toString().endsWith(".jfr"))
.forEach(jfrFile -> {
try {
@ -76,7 +76,7 @@ public class RecordingTest {
}
@Test
public void testNewRecording() {
public void assertNewRecordingInitialValues() {
assert flightRecorderConnection != null;
Recording recording = flightRecorderConnection.newRecording(null, null);
assertEquals(recording.getState(), Recording.State.NEW);
@ -85,20 +85,20 @@ public class RecordingTest {
@Test
public void testRecordingStart() {
public void assertRecordingStartIdAndState() {
assert flightRecorderConnection != null;
Recording recording = flightRecorderConnection.newRecording(null, null);
try {
long id = recording.start();
assertEquals(recording.getId(), id);
assertEquals(recording.getState(), Recording.State.RECORDING);
} catch (IOException|IllegalStateException| InternalError e) {
fail("Recording start threw exception", e);
} catch (IOException|IllegalStateException| JfrStreamingException e) {
fail("assertRecordingStartIdAndState caught exception", e);
}
}
@Test
public void testRecordingStop() {
public void assertRecordingStopState() {
assert flightRecorderConnection != null;
Recording recording = flightRecorderConnection.newRecording(null, null);
try {
@ -106,13 +106,13 @@ public class RecordingTest {
assertEquals(recording.getId(), id);
recording.stop();
assertEquals(recording.getState(), Recording.State.STOPPED);
} catch (IOException|IllegalStateException| InternalError e) {
fail("Recording start threw exception", e);
} catch (IOException|IllegalStateException| JfrStreamingException e) {
fail("assertRecordingStopState caught exception", e);
}
}
@Test
public void testRecordingClose() {
public void assertRecordingCloseState() {
assert flightRecorderConnection != null;
Recording recording = flightRecorderConnection.newRecording(null, null);
try {
@ -120,8 +120,8 @@ public class RecordingTest {
assertEquals(recording.getId(), id);
recording.close();
assertEquals(recording.getState(), Recording.State.CLOSED);
} catch (IOException|IllegalStateException| InternalError e) {
fail("Recording start threw exception", e);
} catch (IOException|IllegalStateException| JfrStreamingException e) {
fail("assertRecordingCloseState caught exception", e);
}
}
@ -186,7 +186,7 @@ public class RecordingTest {
};
@Test(dataProvider = "validStateChanges")
public void testValidStateChanges(Object[] args) {
public void assertValidStateChangeNoException(Object[] args) {
assert flightRecorderConnection != null;
Recording recording = flightRecorderConnection.newRecording(null, null);
try {
@ -215,7 +215,7 @@ public class RecordingTest {
};
@Test(dataProvider = "invalidStateChanges", expectedExceptions = {IllegalStateException.class})
public void testInvalidStateChanges(Object[] args) {
public void assertInvalidStateChangeThrowsIllegalStateException(Object[] args) {
assert flightRecorderConnection != null;
Recording recording = flightRecorderConnection.newRecording(null, null);
try {
@ -241,8 +241,9 @@ public class RecordingTest {
{"name=test", "maxAge=30 s", "maxSize=1048576","dumpOnExit=true","destination=temp.jfr","disk=true","duration=30 s"},
};
}
@Test(dataProvider = "options")
public void testRecordingOptions(String[] options) {
public void assertRecordingOptionsAreSetInFlightRecorderMXBean(String[] options) {
try {
MBeanServerConnection mBeanServer = ManagementFactory.getPlatformMBeanServer();
ObjectName flightRecorder = new ObjectName("jdk.management.jfr:type=FlightRecorder");
@ -289,7 +290,7 @@ public class RecordingTest {
} catch (IOException ioe) {
// possible that this can be thrown, but should not happen in this context
fail("IOException not expected: ", ioe);
} catch (InternalError | ReflectionException| MBeanException badBean) {
} catch (JfrStreamingException | ReflectionException| MBeanException badBean) {
fail("Error thrown by MBean server or FlightRecorderMXBean: ", badBean);
} catch (MalformedObjectNameException badTest) {
fail("Error internal to the test: ", badTest);
@ -314,9 +315,10 @@ public class RecordingTest {
}
@Test
public void testRecordingDump() {
public void assertFileExistsAfterRecordingDump() {
Path dumpFile = null;
try {
final Path dumpFile = Paths.get(System.getProperty("user.dir"),"testRecordingDump_dumped.jfr");
dumpFile = Paths.get(System.getProperty("user.dir"),"testRecordingDump_dumped.jfr");
Files.deleteIfExists(dumpFile);
RecordingOptions recordingOptions = new RecordingOptions.Builder().disk("true").build();
@ -335,18 +337,70 @@ public class RecordingTest {
} catch (IOException ioe) {
// possible that this can be thrown, but should not happen in this context
fail("IOException not expected: ", ioe);
} catch (InternalError badBean) {
} catch (JfrStreamingException badBean) {
fail("Error thrown by MBean server or FlightRecorderMXBean: ", badBean);
} finally {
if (dumpFile != null) {
try {
Files.deleteIfExists(dumpFile);
} catch (IOException ignore) {
}
}
}
}
@Test
public void testRecordingStream() {
public void assertFileExistsAfterRecordingStream() {
Path streamedFile = null;
try {
final Path streamedFile = Paths.get(System.getProperty("user.dir"),"testRecordingStream_getStream.jfr");
final Path dumpedFile = Paths.get(System.getProperty("user.dir"),"testRecordingStream_dumped.jfr");
streamedFile = Paths.get(System.getProperty("user.dir"),"testRecordingStream_getStream.jfr");
Files.deleteIfExists(streamedFile);
RecordingOptions recordingOptions = new RecordingOptions.Builder().disk("true").build();
Recording recording = flightRecorderConnection.newRecording(recordingOptions, null);
long id = recording.start();
Instant now = Instant.now();
Instant then = now.plusSeconds(1);
while (Instant.now().compareTo(then) < 0) {
fib(Short.MAX_VALUE); // do something
}
recording.stop();
try (InputStream inputStream = recording.getStream(now, then); // get the whole thing.
OutputStream outputStream = new FileOutputStream(streamedFile.toFile())) {
int c = -1;
while ((c = inputStream.read()) != -1) outputStream.write(c);
} catch (IOException e) {
fail(e.getMessage(), e);
}
assertTrue(Files.exists(streamedFile));
} catch (IllegalArgumentException badData) {
fail("Issue in test data: " + badData.getMessage());
} catch (IOException ioe) {
// possible that this can be thrown, but should not happen in this context
fail("IOException not expected: ", ioe);
} catch (JfrStreamingException badBean) {
fail("Error thrown by MBean server or FlightRecorderMXBean: ", badBean);
} finally {
if (streamedFile != null) {
try {
Files.deleteIfExists(streamedFile);
} catch (IOException ignore) {
}
}
}
}
@Test
public void assertStreamedFileEqualsDumpedFile() {
Path streamedFile = null;
Path dumpedFile = null;
try {
streamedFile = Paths.get(System.getProperty("user.dir"),"testRecordingStream_getStream.jfr");
dumpedFile = Paths.get(System.getProperty("user.dir"),"testRecordingStream_dumped.jfr");
Files.deleteIfExists(streamedFile);
Files.deleteIfExists(dumpedFile);
RecordingOptions recordingOptions = new RecordingOptions.Builder().disk("true").build();
Recording recording = flightRecorderConnection.newRecording(recordingOptions, null);
@ -365,9 +419,7 @@ public class RecordingTest {
} catch (IOException e) {
fail(e.getMessage(), e);
}
// save these in case this test fails so we can do the diff.
jfrFilesToSave.add(streamedFile);
jfrFilesToSave.add(dumpedFile);
try (InputStream streamed = new FileInputStream(streamedFile.toFile());
InputStream dumped = new FileInputStream(dumpedFile.toFile())) {
int a = -1;
@ -381,21 +433,22 @@ public class RecordingTest {
fail(e.getMessage(), e);
}
// if we get here, then the files compare the same and there is no need to save them
jfrFilesToSave.remove(dumpedFile);
jfrFilesToSave.remove(streamedFile);
Files.deleteIfExists(dumpedFile);
Files.deleteIfExists(streamedFile);
} catch (IllegalArgumentException badData) {
fail("Issue in test data: " + badData.getMessage());
} catch (IOException ioe) {
// possible that this can be thrown, but should not happen in this context
fail("IOException not expected: ", ioe);
} catch (InternalError badBean) {
} catch (JfrStreamingException badBean) {
fail("Error thrown by MBean server or FlightRecorderMXBean: ", badBean);
}
}
@Test
public void testRecordingClone() {
public void assertRecordingCloneState() {
// Recording#clone returns a clone of the recording with the same state, but clone has its own id.
// Recording#clone with 'true' causes clone to close before returning.
try {
RecordingOptions recordingOptions = new RecordingOptions.Builder().disk("true").build();
@ -410,7 +463,7 @@ public class RecordingTest {
} catch (IOException ioe) {
// possible that this can be thrown, but should not happen in this context
fail("IOException not expected: ", ioe);
} catch (InternalError badBean) {
} catch (JfrStreamingException badBean) {
fail("Error thrown by MBean server or FlightRecorderMXBean: ", badBean);
}
}