Skip to content

Commit 20b07dd

Browse files
authored
Merge pull request #355 from patrikcerbak/serialgc-test
[WIP] Started implementing compareNativeAndJVMSerialGCTime test
2 parents 7886fc2 + 5ba6901 commit 20b07dd

File tree

10 files changed

+390
-12
lines changed

10 files changed

+390
-12
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: microprofile-hyperfoil
2+
http:
3+
host: http://localhost:8080
4+
sharedConnections: 200
5+
usersPerSec: 10
6+
duration: 5s
7+
scenario:
8+
- first:
9+
- httpRequest:
10+
# create the HTTP GET request
11+
GET: /data/hello
12+
# use different metric for webpages and other
13+
metric:
14+
- .*\.html -> pages
15+
- -> other
16+
# handler processes the response
17+
handler:
18+
# check that the response was successful
19+
status:
20+
range: 2xx
21+
# when completed, set the variable
22+
onCompletion:
23+
set: firstDone <- yes
24+
# wait for it to sync
25+
sync: true
26+
# wait for the variable to be set
27+
- awaitVar: firstDone
28+
29+
# repeat the same scenario for other endpoints...
30+
- second:
31+
- httpRequest:
32+
GET: /data/secured/test
33+
metric:
34+
- .*\.html -> pages
35+
- -> other
36+
handler:
37+
status:
38+
range: 2xx
39+
onCompletion:
40+
set: secondDone <- yes
41+
sync: true
42+
- awaitVar: secondDone
43+
44+
- third:
45+
- httpRequest:
46+
GET: /q/metrics
47+
metric:
48+
- .*\.html -> pages
49+
- -> other
50+
handler:
51+
status:
52+
range: 2xx
53+
onCompletion:
54+
set: thirdDone <- yes
55+
sync: true
56+
- awaitVar: thirdDone
57+
58+
- fourth:
59+
- httpRequest:
60+
GET: /q/health
61+
metric:
62+
- .*\.html -> pages
63+
- -> other
64+
handler:
65+
status:
66+
range: 2xx
67+
onCompletion:
68+
set: fourthDone <- yes
69+
sync: true
70+
- awaitVar: fourthDone
71+
72+
- fifth:
73+
- httpRequest:
74+
GET: /data/config/injected
75+
metric:
76+
- .*\.html -> pages
77+
- -> other
78+
handler:
79+
status:
80+
range: 2xx
81+
onCompletion:
82+
set: fifthDone <- yes
83+
sync: true
84+
- awaitVar: fifthDone
85+
86+
- sixth:
87+
- httpRequest:
88+
GET: /data/client/test/bonjour
89+
metric:
90+
- .*\.html -> pages
91+
- -> other
92+
handler:
93+
status:
94+
range: 2xx
95+
onCompletion:
96+
set: sixthDone <- yes
97+
sync: true
98+
- awaitVar: sixthDone
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
timeInGCs=10

testsuite/src/it/java/org/graalvm/tests/integration/JFRTest.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.graalvm.tests.integration.utils.Apps;
2424
import org.graalvm.tests.integration.utils.Commands;
2525
import org.graalvm.tests.integration.utils.ContainerNames;
26+
import org.graalvm.tests.integration.utils.HyperfoilHelper;
2627
import org.graalvm.tests.integration.utils.LogBuilder;
2728
import org.graalvm.tests.integration.utils.Logs;
2829
import org.graalvm.tests.integration.utils.WebpageTester;
@@ -404,14 +405,7 @@ private Map<String, Integer> runBenchmarkForApp(Endpoint endpoint, int trials, A
404405

405406
// Upload the benchmark
406407
final HttpClient hc = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
407-
final HttpRequest uploadRequest = HttpRequest.newBuilder()
408-
.uri(new URI(app.urlContent.urlContent[1][0]))
409-
.header("Content-Type", "text/vnd.yaml")
410-
.POST(HttpRequest.BodyPublishers.ofFile(Path.of(appDir.getAbsolutePath() + "/benchmark.hf.yaml")))
411-
.build();
412-
final HttpResponse<String> releaseResponse = hc.send(uploadRequest, HttpResponse.BodyHandlers.ofString());
413-
assertEquals(204, releaseResponse.statusCode(), "App returned a non HTTP 204 response. The perf report is invalid.");
414-
LOGGER.info("Hyperfoil upload response code " + releaseResponse.statusCode());
408+
HyperfoilHelper.uploadBenchmark(app, appDir, app.urlContent.urlContent[1][0], hc);
415409

416410
// Run the benchmark
417411
disableTurbo();

testsuite/src/it/java/org/graalvm/tests/integration/PerfCheckTest.java

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@
2323
import org.graalvm.home.Version;
2424
import org.graalvm.tests.integration.utils.Apps;
2525
import org.graalvm.tests.integration.utils.Commands;
26+
import org.graalvm.tests.integration.utils.ContainerNames;
27+
import org.graalvm.tests.integration.utils.HyperfoilHelper;
2628
import org.graalvm.tests.integration.utils.Logs;
2729
import org.graalvm.tests.integration.utils.WebpageTester;
30+
import org.graalvm.tests.integration.utils.thresholds.Thresholds;
2831
import org.graalvm.tests.integration.utils.versions.IfMandrelVersion;
2932
import org.graalvm.tests.integration.utils.versions.IfQuarkusVersion;
3033
import org.graalvm.tests.integration.utils.versions.QuarkusVersion;
3134
import org.graalvm.tests.integration.utils.versions.UsedVersion;
3235
import org.jboss.logging.Logger;
36+
import org.json.JSONObject;
3337
import org.junit.jupiter.api.Tag;
3438
import org.junit.jupiter.api.Test;
3539
import org.junit.jupiter.api.TestInfo;
@@ -52,11 +56,13 @@
5256
import java.nio.file.Path;
5357
import java.nio.file.Paths;
5458
import java.nio.file.StandardOpenOption;
59+
import java.time.Duration;
5560
import java.util.ArrayList;
5661
import java.util.Arrays;
5762
import java.util.HashMap;
5863
import java.util.List;
5964
import java.util.Map;
65+
import java.util.Objects;
6066
import java.util.TreeMap;
6167
import java.util.concurrent.TimeUnit;
6268
import java.util.regex.Pattern;
@@ -72,6 +78,8 @@
7278
import static org.graalvm.tests.integration.utils.Commands.QUARKUS_VERSION;
7379
import static org.graalvm.tests.integration.utils.Commands.builderRoutine;
7480
import static org.graalvm.tests.integration.utils.Commands.cleanTarget;
81+
import static org.graalvm.tests.integration.utils.Commands.disableTurbo;
82+
import static org.graalvm.tests.integration.utils.Commands.enableTurbo;
7583
import static org.graalvm.tests.integration.utils.Commands.findExecutable;
7684
import static org.graalvm.tests.integration.utils.Commands.findFiles;
7785
import static org.graalvm.tests.integration.utils.Commands.getProperty;
@@ -87,6 +95,7 @@
8795
import static org.graalvm.tests.integration.utils.Commands.runJaegerContainer;
8896
import static org.graalvm.tests.integration.utils.Commands.waitForFileToMatch;
8997
import static org.graalvm.tests.integration.utils.Commands.waitForTcpClosed;
98+
import static org.graalvm.tests.integration.utils.Logs.getLogsDir;
9099
import static org.graalvm.tests.integration.utils.Uploader.PERF_APP_REPORT;
91100
import static org.graalvm.tests.integration.utils.Uploader.postBuildtimePayload;
92101
import static org.graalvm.tests.integration.utils.Uploader.postRuntimePayload;
@@ -95,6 +104,9 @@
95104
import static org.jboss.resteasy.spi.HttpResponseCodes.SC_CREATED;
96105
import static org.jboss.resteasy.spi.HttpResponseCodes.SC_OK;
97106
import static org.junit.jupiter.api.Assertions.assertEquals;
107+
import static org.junit.jupiter.api.Assertions.assertFalse;
108+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
109+
import static org.junit.jupiter.api.Assertions.assertNotNull;
98110
import static org.junit.jupiter.api.Assertions.assertTrue;
99111

100112
/**
@@ -111,6 +123,10 @@ public class PerfCheckTest {
111123
public static final int LIGHT_REQUESTS = Integer.parseInt(getProperty("PERFCHECK_TEST_LIGHT_REQUESTS", "100"));
112124
public static final int HEAVY_REQUESTS = Integer.parseInt(getProperty("PERFCHECK_TEST_HEAVY_REQUESTS", "2"));
113125
public static final int MX_HEAP_MB = Integer.parseInt(getProperty("PERFCHECK_TEST_REQUESTS_MX_HEAP_MB", "2560"));
126+
127+
// Making the heap smaller for GC tests
128+
public static final int GC_HEAP_MB = Integer.parseInt(getProperty("GC_TEST_HEAP_MB", "64"));
129+
114130
// Build time constraint
115131
public static final int NATIVE_IMAGE_XMX_GB = Integer.parseInt(getProperty("PERFCHECK_TEST_NATIVE_IMAGE_XMX_GB", "8"));
116132

@@ -552,6 +568,183 @@ public void testQuarkusFullMicroProfile(TestInfo testInfo) throws IOException, I
552568
}
553569
}
554570

571+
@Test
572+
@IfMandrelVersion(min = "21.3")
573+
public void compareNativeAndJVMSerialGCTime(TestInfo testInfo) throws IOException, InterruptedException, URISyntaxException {
574+
final Apps app = Apps.QUARKUS_FULL_MICROPROFILE_GC;
575+
LOGGER.info("Testing app: " + app);
576+
LOGGER.info("Comparing native and JVM SerialGC times.");
577+
578+
Process process = null;
579+
final File appDir = Path.of(BASE_DIR, app.dir).toFile();
580+
final File processLog = Path.of(appDir.getAbsolutePath(), "logs", "build-and-run.log").toFile();
581+
final String cn = testInfo.getTestClass().get().getCanonicalName();
582+
final String mn = testInfo.getTestMethod().get().getName();
583+
final List<Map<String, String>> reports = new ArrayList<>(2);
584+
585+
// apply patches, when necessary
586+
String patch = null;
587+
if (QUARKUS_VERSION.compareTo(QuarkusVersion.V_3_9_0) >= 0) {
588+
patch = "quarkus_3.9.x.patch";
589+
} else if (QUARKUS_VERSION.compareTo(QuarkusVersion.V_3_8_0) >= 0) {
590+
patch = "quarkus_3.8.x.patch";
591+
} else if (QUARKUS_VERSION.compareTo(QuarkusVersion.V_3_2_0) >= 0) {
592+
patch = "quarkus_3.2.x.patch";
593+
}
594+
595+
try {
596+
// cleanup before start
597+
cleanTarget(app);
598+
Files.createDirectories(Paths.get(appDir.getAbsolutePath(), "logs"));
599+
600+
if (patch != null) {
601+
runCommand(getRunCommand("git", "apply", patch), appDir);
602+
}
603+
604+
// build executables for testing
605+
builderRoutine(app, null, null, null, appDir, processLog, null, getSwitches3());
606+
607+
for (int i = 0; i < app.buildAndRunCmds.runCommands.length - 1; i++) {
608+
final Map<String, String> report = populateHeader(new TreeMap<>());
609+
report.replace("testApp", "https://github.com/Karm/mandrel-integration-tests/apps/quarkus-full-microprofile/");
610+
611+
// run the app
612+
final List<String> cmd = getRunCommand(app.buildAndRunCmds.runCommands[i]);
613+
Files.writeString(processLog.toPath(), String.join(" ", cmd) + '\n', StandardOpenOption.APPEND, StandardOpenOption.CREATE);
614+
process = runCommand(cmd, appDir, processLog, app);
615+
LOGGER.info("Running app with pid " + process.pid());
616+
617+
// create a request to teh app and measure the time
618+
final long timeToFirstOKRequestMs = WebpageTester.testWeb(app.urlContent.urlContent[0][0], 10, app.urlContent.urlContent[0][1], true);
619+
waitForFileToMatch(Pattern.compile(".*Events enabled.*"), processLog.toPath(), 0, 20, 1, TimeUnit.SECONDS);
620+
report.put("timeToFirstOKRequestMs", String.valueOf(timeToFirstOKRequestMs));
621+
622+
// generate some requests to the app with Hyperfoil
623+
generateRequestsWithHyperfoil(app, appDir, processLog, cn, mn, false);
624+
625+
// stop the app
626+
processStopper(process, false, true);
627+
assertTrue(waitForTcpClosed("localhost", parsePort(app.urlContent.urlContent[0][0]), 60),
628+
"Main port is still open.");
629+
630+
// parse the GC log (taken from testQuarkusFullMicroprofile)
631+
final String statsFor = Arrays.stream(app.buildAndRunCmds.runCommands[i]).collect(Collectors.joining(" ")).trim();
632+
final Commands.SerialGCLog l;
633+
if (!statsFor.contains("-jar")) {
634+
long executableSizeKb = Files.size(Path.of(appDir.getAbsolutePath(), statsFor.split(" ")[0])) / 1024L;
635+
report.put("executableSizeKb", String.valueOf(executableSizeKb));
636+
l = parseSerialGCLog(processLog.toPath(), statsFor, false);
637+
report.put("incrementalGCevents", String.valueOf(l.incrementalGCevents));
638+
report.put("fullGCevents", String.valueOf(l.fullGCevents));
639+
} else {
640+
l = parseSerialGCLog(processLog.toPath(), statsFor, true);
641+
report.put("incrementalGCevents", "-1");
642+
report.put("fullGCevents", "-1");
643+
report.put("executableSizeKb", "-1");
644+
}
645+
report.put("timeSpentInGCs", String.valueOf(l.timeSpentInGCs));
646+
report.put("testMethod", cn + "#" + mn);
647+
reports.add(report);
648+
}
649+
650+
// log the report
651+
final String reportPayload = mapToJSON(reports);
652+
LOGGER.info(reportPayload);
653+
654+
LOGGER.info("Wait till the ports close...");
655+
assertTrue(waitForTcpClosed("localhost", parsePort(app.urlContent.urlContent[0][0]), 60),
656+
"Main is still open.");
657+
Logs.checkLog(cn, mn, app, processLog);
658+
659+
// sanity check
660+
assertNotEquals("0.0", reports.get(0).get("timeSpentInGCs"), "Time spent in GCs is zero (JVM).");
661+
assertNotEquals("0.0", reports.get(1).get("timeSpentInGCs"), "Time spent in GCs is zero (native).");
662+
663+
// saving time spent in GCs values
664+
double jvmGCTime = Double.parseDouble(reports.get(1).get("timeSpentInGCs"));
665+
double nativeGCTime = Double.parseDouble(reports.get(1).get("timeSpentInGCs"));
666+
667+
// get threshold value
668+
final Path gcThresholds = appDir.toPath().resolve("gc_threshold.conf");
669+
Map<String, Long> thresholds = Thresholds.parseProperties(gcThresholds);
670+
671+
// assert that time spent in GCs in native is inside the threshold (ideally faster)
672+
double percentageDiff = getPercentageDifference(nativeGCTime, jvmGCTime);
673+
assertTrue(nativeGCTime < jvmGCTime || percentageDiff <= (double) thresholds.get("timeInGCs"),
674+
"Time spent in GCs is " + percentageDiff + "% slower in native than in JVM (threshold is " + thresholds.get("timeInGCs") + "%).");
675+
} finally {
676+
// final cleanup after the test is over
677+
if (process != null) {
678+
processStopper(process, true);
679+
}
680+
Logs.archiveLog(cn, mn, Path.of(appDir.getAbsolutePath(),
681+
"target", "quarkus-native-image-source-jar", "quarkus-json.json").toFile());
682+
Logs.archiveLog(cn, mn, processLog);
683+
cleanTarget(app);
684+
if (patch != null) {
685+
runCommand(getRunCommand("git", "apply", "-R", patch), appDir);
686+
}
687+
}
688+
}
689+
690+
private void generateRequestsWithHyperfoil(Apps app, File appDir, File processLog, String cn, String mn, boolean printResults)
691+
throws IOException, InterruptedException, URISyntaxException {
692+
try {
693+
removeContainer("hyperfoil-container");
694+
695+
// start Hyperfoil
696+
final List<String> getAndStartHyperfoil = getRunCommand(app.buildAndRunCmds.runCommands[2]);
697+
Process hyperfoilProcess = runCommand(getAndStartHyperfoil, appDir, processLog, app);
698+
assertNotNull(hyperfoilProcess, "Hyperfoil failed to run. Check " + getLogsDir(cn, mn) + File.separator + processLog.getName());
699+
LOGGER.info("Hyperfoil process started with pid " + hyperfoilProcess.pid());
700+
Commands.waitForContainerLogToMatch(ContainerNames.HYPERFOIL.name,
701+
Pattern.compile(".*Hyperfoil controller listening.*", Pattern.DOTALL), 600, 5, TimeUnit.SECONDS);
702+
WebpageTester.testWeb(app.urlContent.urlContent[1][0], 15, app.urlContent.urlContent[1][1], false);
703+
704+
// upload the benchmark
705+
final HttpClient hc = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
706+
HyperfoilHelper.uploadBenchmark(app, appDir, app.urlContent.urlContent[2][0], hc);
707+
708+
// run the benchmark
709+
disableTurbo();
710+
final HttpRequest benchmarkRequest = HttpRequest.newBuilder()
711+
.uri(new URI(app.urlContent.urlContent[3][0]))
712+
.GET()
713+
.build();
714+
final HttpResponse<String> benchmarkResponse = hc.send(benchmarkRequest, HttpResponse.BodyHandlers.ofString());
715+
final JSONObject benchmarkResponseJson = new JSONObject(benchmarkResponse.body());
716+
final String id = benchmarkResponseJson.getString("id");
717+
718+
LOGGER.info("Running Hyperfoil benchmark with id " + id);
719+
720+
// wait for benchmark to complete
721+
Commands.waitForContainerLogToMatch(ContainerNames.HYPERFOIL.name,
722+
Pattern.compile(".*Successfully persisted run.*", Pattern.DOTALL), 30, 2, TimeUnit.SECONDS);
723+
enableTurbo();
724+
725+
// get and print the benchmark results (if needed)
726+
if (printResults) {
727+
final HttpRequest resultsRequest = HttpRequest.newBuilder()
728+
.uri(new URI("http://localhost:8090/run/" + id + "/stats/all/json"))
729+
.GET()
730+
.timeout(Duration.ofSeconds(3)) // set timeout to allow for cleanup, otherwise will stall at first request above
731+
.build();
732+
final HttpResponse<String> resultsResponse = hc.send(resultsRequest, HttpResponse.BodyHandlers.ofString());
733+
LOGGER.info("Hyperfoil results response code " + resultsResponse.statusCode());
734+
final JSONObject resultsResponseJson = new JSONObject(resultsResponse.body());
735+
System.out.println(resultsResponseJson); // uses normal system print, because it's often very long
736+
}
737+
} finally {
738+
// cleanup
739+
removeContainer("hyperfoil-container");
740+
}
741+
742+
}
743+
744+
private double getPercentageDifference(double firstNumber, double secondNumber) {
745+
return Math.abs(firstNumber - secondNumber) * 100.0 / secondNumber;
746+
}
747+
555748
/**
556749
* This test builds and runs integration tests of a more complex Quarkus app,
557750
* including two databases, testcontainers etc.

testsuite/src/it/java/org/graalvm/tests/integration/utils/Apps.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public enum Apps {
5454
WhitelistLogLines.QUARKUS_FULL_MICROPROFILE,
5555
BuildAndRunCmds.QUARKUS_FULL_MICROPROFILE_PERF,
5656
ContainerNames.NONE),
57+
QUARKUS_FULL_MICROPROFILE_GC("apps" + File.separator + "quarkus-full-microprofile",
58+
URLContent.QUARKUS_FULL_MICROPROFILE_GC,
59+
WhitelistLogLines.QUARKUS_FULL_MICROPROFILE,
60+
BuildAndRunCmds.QUARKUS_FULL_MICROPROFILE_GC,
61+
ContainerNames.NONE),
5762
QUARKUS_MP_ORM_DBS_AWT("apps" + File.separator + "quarkus-mp-orm-dbs-awt",
5863
URLContent.NONE,
5964
WhitelistLogLines.QUARKUS_MP_ORM_DBS_AWT,

0 commit comments

Comments
 (0)