Skip to content

Commit

Permalink
Add automatic analysis from JMC. Provide some example assertions wrt …
Browse files Browse the repository at this point in the history
…rules that have fired and tests based on severity
  • Loading branch information
johnaohara committed Oct 20, 2021
1 parent c650198 commit 2b4a3f9
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 4 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,50 @@ As you can see you can use custom DSL when checking the expected state.
* The `RecordedEvent` itself is extended with dynamic properties so you can just use `event.time` or `event.bytesWritten` etc.
This might be handy when you need an aggregation like this `jfrEvents['jdk.FileWrite']*.bytesWritten.sum() == expectedBytes`

## Automatic Analysis


Java Mission Control provides an Automatic Analysis tool that performs pattern analysis on recorded JFR events to determine common performance issues with Java applications.

It is possible to write assertions against the Automatic Analysis results to verify that unit tests against common performance issues:

```java
import dev.morling.jfrunit.*;

import static dev.morling.jfrunit.JfrEventsAssert.*;
import static dev.morling.jfrunit.ExpectedEvent.*;

@Test
@EnableConfiguration("profile")
public void automaticAnalysisTest() throws Exception {
System.gc();
Thread.sleep(1000);

jfrEvents.awaitEvents();

JfrAnalysisResults analysisResults = jfrEvents.automaticAnalysis();

//Inspect rules that fired
assertThat(analysisResults).contains(FullGcRule.class);
assertThat(analysisResults).doesNotContain(HeapDumpRule.class);

//Inspect severity of rule
assertThat(analysisResults).hasSeverity(FullGcRule.class, Severity.WARNING);

//Inspect score of rule
assertThat(analysisResults)
.contains(FullGcRule.class)
.scoresLessThan(80);
}
```

A full list of JMC Analysis rules can be found [here](https://docs.oracle.com/en/java/java-components/jdk-mission-control/8/jmc-flightrecorder-rules-jdk/org.openjdk.jmc.flightrecorder.rules.jdk/module-summary.html).

## Build

This project requires OpenJDK 14 or later for its build.
Apache Maven is used for the build.

Run the following to build the project:

```shell
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<maven.compiler.parameters>true</maven.compiler.parameters>
<quarkus.platform.version>2.3.0.Final</quarkus.platform.version>
<version.surefire.plugin>3.0.0-M5</version.surefire.plugin>
<jmc.version>8.0.1</jmc.version>
</properties>

<scm>
Expand Down Expand Up @@ -113,6 +114,11 @@
<type>pom</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmc</groupId>
<artifactId>flightrecorder.rules.jdk</artifactId>
<version>${jmc.version}</version>
</dependency>
</dependencies>

<build>
Expand Down
85 changes: 85 additions & 0 deletions src/main/java/org/moditect/jfrunit/JfrAnalysis.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2020 - 2021 The JfrUnit authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.moditect.jfrunit;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Future;

import org.openjdk.jmc.common.item.IItemCollection;
import org.openjdk.jmc.flightrecorder.CouldNotLoadRecordingException;
import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit;
import org.openjdk.jmc.flightrecorder.rules.IResult;
import org.openjdk.jmc.flightrecorder.rules.IRule;
import org.openjdk.jmc.flightrecorder.rules.RuleRegistry;
import org.openjdk.jmc.flightrecorder.rules.Severity;
import org.openjdk.jmc.flightrecorder.rules.util.RulesToolkit;

public class JfrAnalysis {

private static final System.Logger LOGGER = System.getLogger(JfrAnalysis.class.getName());

public static List<IResult> analysisRecording(String fileName, Severity minSeverity) {
return JfrAnalysis.analysisRecording(fileName, minSeverity, false);
}

public static List<IResult> analysisRecording(String fileName, Severity minSeverity, boolean verbose) {
try {
File file = new File(fileName);

IItemCollection events;
try {
events = JfrLoaderToolkit.loadEvents(file);
}
catch (IOException | CouldNotLoadRecordingException e) {
LOGGER.log(System.Logger.Level.ERROR, "Unable to analyse jfr recording: " + e.getLocalizedMessage());
return null;
}

// TODO: Provide configuration
Map<IRule, Future<IResult>> resultFutures = RulesToolkit.evaluateParallel(RuleRegistry.getRules(), events,
null, 0);
List<Map.Entry<IRule, Future<IResult>>> resultFutureList = new ArrayList<>(resultFutures.entrySet());
Collections.sort(resultFutureList, Comparator.comparing(o -> o.getKey().getId()));

List<IResult> analysisResults = new ArrayList();

for (Map.Entry<IRule, Future<IResult>> resultEntry : resultFutureList) {
IResult result;
try {
result = resultEntry.getValue().get();
}
catch (Throwable t) {
LOGGER.log(System.Logger.Level.WARNING, "Unable to analyse analysis result: " + t.getLocalizedMessage());
continue;
}

if (result != null && result.getSeverity().compareTo(minSeverity) >= 0) {
// TODO: further results processing
analysisResults.add(result);
}
}
return analysisResults;
}
catch (Throwable t) {
System.err.println("Got exception when creating report for " + fileName); //$NON-NLS-1$
throw t;
}
}
}
124 changes: 124 additions & 0 deletions src/main/java/org/moditect/jfrunit/JfrAnalysisAssert.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2020 - 2021 The JfrUnit authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.moditect.jfrunit;

import java.util.Optional;

import org.assertj.core.api.AbstractAssert;
import org.openjdk.jmc.common.unit.IQuantity;
import org.openjdk.jmc.flightrecorder.rules.IResult;
import org.openjdk.jmc.flightrecorder.rules.IRule;
import org.openjdk.jmc.flightrecorder.rules.Severity;
import org.openjdk.jmc.flightrecorder.rules.TypedResult;

public class JfrAnalysisAssert extends AbstractAssert<JfrAnalysisAssert, JfrAnalysisResults> {

private IResult foundResult;

public JfrAnalysisAssert(JfrAnalysisResults results) {
super(results, JfrAnalysisAssert.class);
}

public static JfrAnalysisAssert assertThat(JfrAnalysisResults results) {
return new JfrAnalysisAssert(results);
}

public JfrAnalysisAssert doesNotContain(Class<? extends IRule> expectedRule) {
return findRule(expectedRule, true, "JMC Analysis result contains rule of type <%s>");
}

public JfrAnalysisAssert contains(Class<? extends IRule> expectedRule) {
return findRule(expectedRule);
}

private JfrAnalysisAssert findRule(Class<? extends IRule> expectedRule) {
return findRule(expectedRule, false, "No JMC Analysis result rule of type <%s>");
}

private JfrAnalysisAssert findRule(Class<? extends IRule> expectedRule, boolean negate, String failureMsg) {
isNotNull();

Optional<IResult> optionalIResult = actual.getResults().stream()
.filter(re -> re.getRule().getClass().equals(expectedRule))
.findAny();

boolean found = optionalIResult
.isPresent();

if (negate ? found : !found) {
failWithMessage(failureMsg, expectedRule.getName());
}
else {
if (!negate) {
this.foundResult = optionalIResult.get();
}
}

return this;

}

public JfrAnalysisAssert hasSeverity(Class<? extends IRule> expectedRule, Severity expectedSeverity) {
Optional<IResult> resultOptional = findResult(expectedRule);

if (!resultOptional.isPresent()) {
failWithMessage("No analysis type for <%s>", expectedRule.getName());
}
else {
IResult result = resultOptional.get();

if (result.getSeverity().getLimit() < expectedSeverity.getLimit()) {
failWithMessage("Analysis result not required severity <%s>", expectedSeverity);

}
}
return this;
}

public JfrAnalysisAssert scoresLessThan(Class<? extends IRule> expectedRule, double expectedScore) {

findRule(expectedRule);

return scoresLessThan(expectedScore);
}

public JfrAnalysisAssert scoresLessThan(double expectedScore) {
IQuantity resultScore = this.foundResult.getResult(TypedResult.SCORE);
double score = 0;
if (resultScore != null) {
score = resultScore.doubleValue();
}
else if (this.foundResult.getSeverity().getLimit() != 0.0d) {
score = this.foundResult.getSeverity().getLimit();
}

if (score > expectedScore) {
failWithMessage("Analysis result score exceeds threshold: actual <%.1f>, threshold <%.1f>", score, expectedScore);
}
return this;

}

private Optional<IResult> findResult(Class<? extends IRule> expectedRule) {
return actual.getResults().stream()
.filter(re -> re.getRule().getClass().equals(expectedRule))
.findFirst();

}
}
38 changes: 38 additions & 0 deletions src/main/java/org/moditect/jfrunit/JfrAnalysisResults.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2020 - 2021 The JfrUnit authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.moditect.jfrunit;

import java.util.List;

import org.openjdk.jmc.flightrecorder.rules.IResult;

public class JfrAnalysisResults {
private List<IResult> results;

public JfrAnalysisResults(List<IResult> analysisRecording) {
this.results = analysisRecording;
}

public List<IResult> getResults() {
return this.results;
}

public int size() {
return this.results.size();
}
}
Loading

0 comments on commit 2b4a3f9

Please sign in to comment.