Skip to content

Commit

Permalink
Check for code usage fluctuations in native images
Browse files Browse the repository at this point in the history
This aims to aid in detecting code usage fluctuations as early as
possible. I set the threshold to 3% which might be a bit tight.

Ideally most (if not all) ITs being tested in native mode should include
such checks. This way when a dependency gets updated and brings in more
bloat, or a GraalVM change results in more code becoming reachable we
will be able to notice.

If significant fluctuations between GraalVM/Mandrel versions are
detected the annotations `@DisableIfBuiltWithGraalVMOlderThan` and
`@DisableIfBuiltWithGraalVMNewerThan` may be used to run tests with
different properties files.

The properties files can be generated by running the following script,
passing the build output json file as the first parameter and the path
to the target properties file as the second parameter (it will be
overwritten), after a failed run. This way enabling the check for a new
integration test is a matter of:

1. copying `ImageMetricsITCase.java` in the corresponding folder
2. running the test once 3. running the following script passing the
build output json file as the first parameter.

```bash

KEYS=( \
image_details.total_bytes \
analysis_results.types.reachable \
analysis_results.methods.reachable \
analysis_results.fields.reachable \
analysis_results.types.reflection \
analysis_results.methods.reflection \
analysis_results.fields.reflection \
analysis_results.types.jni \
analysis_results.methods.jni \
analysis_results.fields.jni \
)

echo "# Properties file used by ImageMetricsITCase" > $2
for i in $KEYS
do
  echo "$i=$(jq .$i $1) >> $2
  echo "$i.tolerance=3 >> $2
done
```
  • Loading branch information
zakkak committed Dec 14, 2023
1 parent ecdd3b2 commit d4d2392
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.it.jpa.postgresql;

import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.quarkus.test.junit.nativeimage.BuildOutput;

@QuarkusIntegrationTest
public class ImageMetricsITCase {
@Test
public void verifyImageMetrics() {
BuildOutput buildOutput = new BuildOutput();
buildOutput.verifyImageMetrics();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
image_details.total_bytes=81172328
image_details.total_bytes.tolerance=3
analysis_results.types.reachable=18854
analysis_results.types.reachable.tolerance=3
analysis_results.methods.reachable=93385
analysis_results.methods.reachable.tolerance=3
analysis_results.fields.reachable=28156
analysis_results.fields.reachable.tolerance=3
analysis_results.types.reflection=5978
analysis_results.types.reflection.tolerance=3
analysis_results.methods.reflection=4107
analysis_results.methods.reflection.tolerance=3
analysis_results.fields.reflection=127
analysis_results.fields.reflection.tolerance=3
analysis_results.types.jni=63
analysis_results.types.jni.tolerance=1
analysis_results.methods.jni=55
analysis_results.methods.jni.tolerance=1
analysis_results.fields.jni=68
analysis_results.fields.jni.tolerance=1
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.it.jpa.postgresql;

import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.quarkus.test.junit.nativeimage.BuildOutput;

@QuarkusIntegrationTest
public class ImageMetricsITCase {
@Test
public void verifyImageMetrics() {
BuildOutput buildOutput = new BuildOutput();
buildOutput.verifyImageMetrics();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
image_details.total_bytes=72185704
image_details.total_bytes.tolerance=3
analysis_results.types.reachable=17905
analysis_results.types.reachable.tolerance=3
analysis_results.methods.reachable=87856
analysis_results.methods.reachable.tolerance=3
analysis_results.fields.reachable=25370
analysis_results.fields.reachable.tolerance=3
analysis_results.types.reflection=5659
analysis_results.types.reflection.tolerance=3
analysis_results.methods.reflection=4200
analysis_results.methods.reflection.tolerance=3
analysis_results.fields.reflection=154
analysis_results.fields.reflection.tolerance=3
analysis_results.types.jni=63
analysis_results.types.jni.tolerance=1
analysis_results.methods.jni=55
analysis_results.methods.jni.tolerance=1
analysis_results.fields.jni=68
analysis_results.fields.jni.tolerance=1
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.it.main;

import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.quarkus.test.junit.nativeimage.BuildOutput;

@QuarkusIntegrationTest
public class ImageMetricsITCase {
@Test
public void verifyImageMetrics() {
BuildOutput buildOutput = new BuildOutput();
buildOutput.verifyImageMetrics();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
image_details.total_bytes=138778008
image_details.total_bytes.tolerance=3
# TODO: Switch to using analysis_results.types.reachable key once we drop support for GraalVM 22.3.0
analysis_results.classes.reachable=30005
analysis_results.classes.reachable.tolerance=3
analysis_results.methods.reachable=149440
analysis_results.methods.reachable.tolerance=3
analysis_results.fields.reachable=44161
analysis_results.fields.reachable.tolerance=3
# TODO: Switch to using analysis_results.types.reflection key once we drop support for GraalVM 22.3.0
analysis_results.classes.reflection=8966
analysis_results.classes.reflection.tolerance=3
analysis_results.methods.reflection=7346
analysis_results.methods.reflection.tolerance=3
analysis_results.fields.reflection=438
analysis_results.fields.reflection.tolerance=3
# TODO: Switch to using analysis_results.types.jni key once we drop support for GraalVM 22.3.0
analysis_results.classes.jni=64
analysis_results.classes.jni.tolerance=1
analysis_results.methods.jni=55
analysis_results.methods.jni.tolerance=1
analysis_results.fields.jni=70
analysis_results.fields.jni.tolerance=1
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package io.quarkus.test.junit.nativeimage;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Locale;
import java.util.Properties;

import jakarta.json.Json;
import jakarta.json.JsonObject;

import org.junit.jupiter.api.Assertions;

/**
* This is a general utility to assert via
* unit testing how many classes, methods, objects etc. have been included in a native-image.
* <p>
* For detailed information and explanations on the build output, visit
* <a href="https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/BuildOutput.md">the upstream GraalVM
* documentation</a>.
*/
public class BuildOutput {

private static final String IMAGE_METRICS_TEST_PROPERTIES = "image-metrics-test.properties";
private final JsonObject buildOutput;

public BuildOutput() {
this.buildOutput = getBuildOutput();
}

public void verifyImageMetrics() {
verifyImageMetrics(IMAGE_METRICS_TEST_PROPERTIES);
}

public void verifyImageMetrics(String propertiesFileName) {
Properties properties = getProperties(propertiesFileName);

properties.forEach((key, value) -> {
if (((String) key).endsWith(".tolerance")) {
return;
}
String[] keyParts = ((String) key).split("\\.");
String tolerance = properties.getProperty(key + ".tolerance");
assert tolerance != null : "tolerance not defined for " + key;
assertValueWithinRange(Integer.parseInt((String) value), Integer.parseInt(tolerance), keyParts);
});
}

private Properties getProperties(String propertiesFileName) {
Properties properties = new Properties();
try {
properties.load(getClass().getClassLoader().getResourceAsStream(propertiesFileName));
} catch (IOException e) {
Assertions.fail("Could not load properties from " + propertiesFileName, e);
}
return properties;
}

private void assertValueWithinRange(int expectedValue, int tolerancePercentage, String... key) {
JsonObject currentObject = buildOutput;
for (int i = 0; i < key.length - 1; i++) {
currentObject = currentObject.getJsonObject(key[i]);
}
String lastKey = key[key.length - 1];
int actualValue = currentObject.getInt(lastKey);
Assertions.assertTrue(isNumberWithinRange(expectedValue, actualValue, tolerancePercentage),
"Expected " + String.join(".", key) + " to be within range [" + expectedValue + " +- " + tolerancePercentage
+ "%] but was " + actualValue);
}

private boolean isNumberWithinRange(int expectedNumberOfClasses, int actualNumberOfClasses, int tolerancePercentage) {
final int lowerBound = expectedNumberOfClasses - (expectedNumberOfClasses * tolerancePercentage / 100);
final int upperBound = expectedNumberOfClasses + (expectedNumberOfClasses * tolerancePercentage / 100);
return actualNumberOfClasses >= lowerBound && actualNumberOfClasses <= upperBound;
}

private static JsonObject getBuildOutput() {
final Path buildOutputPath = getBuildOutputPath();
try (InputStream inputStream = Files.newInputStream(buildOutputPath)) {
return Json.createReader(inputStream).readObject();
} catch (Exception e) {
throw new RuntimeException("Could not load build output", e);
}
}

private static Path getBuildOutputPath() {
final Path buildDirectory = locateNativeImageBuildDirectory();
final File[] buildOutput = buildDirectory.toFile().listFiles((dir, name) -> name.toLowerCase(Locale.ROOT)
.endsWith("-build-output-stats.json"));
Assertions.assertNotNull(buildOutput, "Could not identify the native image build output");
Assertions.assertEquals(1, buildOutput.length, "Could not identify the native image build output");
return buildOutput[0].toPath();
}

private static Path locateNativeImageBuildDirectory() {
Path buildPath = Paths.get("target");
final File[] files = buildPath.toFile().listFiles((dir, name) -> name.toLowerCase(Locale.ROOT)
.endsWith("-native-image-source-jar"));
Assertions.assertNotNull(files, "Could not identify the native image build directory");
Assertions.assertEquals(1, files.length, "Could not identify the native image build directory");
return files[0].toPath();
}
}

0 comments on commit d4d2392

Please sign in to comment.