2323import org .graalvm .home .Version ;
2424import org .graalvm .tests .integration .utils .Apps ;
2525import org .graalvm .tests .integration .utils .Commands ;
26+ import org .graalvm .tests .integration .utils .ContainerNames ;
27+ import org .graalvm .tests .integration .utils .HyperfoilHelper ;
2628import org .graalvm .tests .integration .utils .Logs ;
2729import org .graalvm .tests .integration .utils .WebpageTester ;
30+ import org .graalvm .tests .integration .utils .thresholds .Thresholds ;
2831import org .graalvm .tests .integration .utils .versions .IfMandrelVersion ;
2932import org .graalvm .tests .integration .utils .versions .IfQuarkusVersion ;
3033import org .graalvm .tests .integration .utils .versions .QuarkusVersion ;
3134import org .graalvm .tests .integration .utils .versions .UsedVersion ;
3235import org .jboss .logging .Logger ;
36+ import org .json .JSONObject ;
3337import org .junit .jupiter .api .Tag ;
3438import org .junit .jupiter .api .Test ;
3539import org .junit .jupiter .api .TestInfo ;
5256import java .nio .file .Path ;
5357import java .nio .file .Paths ;
5458import java .nio .file .StandardOpenOption ;
59+ import java .time .Duration ;
5560import java .util .ArrayList ;
5661import java .util .Arrays ;
5762import java .util .HashMap ;
5863import java .util .List ;
5964import java .util .Map ;
65+ import java .util .Objects ;
6066import java .util .TreeMap ;
6167import java .util .concurrent .TimeUnit ;
6268import java .util .regex .Pattern ;
7278import static org .graalvm .tests .integration .utils .Commands .QUARKUS_VERSION ;
7379import static org .graalvm .tests .integration .utils .Commands .builderRoutine ;
7480import 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 ;
7583import static org .graalvm .tests .integration .utils .Commands .findExecutable ;
7684import static org .graalvm .tests .integration .utils .Commands .findFiles ;
7785import static org .graalvm .tests .integration .utils .Commands .getProperty ;
8795import static org .graalvm .tests .integration .utils .Commands .runJaegerContainer ;
8896import static org .graalvm .tests .integration .utils .Commands .waitForFileToMatch ;
8997import static org .graalvm .tests .integration .utils .Commands .waitForTcpClosed ;
98+ import static org .graalvm .tests .integration .utils .Logs .getLogsDir ;
9099import static org .graalvm .tests .integration .utils .Uploader .PERF_APP_REPORT ;
91100import static org .graalvm .tests .integration .utils .Uploader .postBuildtimePayload ;
92101import static org .graalvm .tests .integration .utils .Uploader .postRuntimePayload ;
95104import static org .jboss .resteasy .spi .HttpResponseCodes .SC_CREATED ;
96105import static org .jboss .resteasy .spi .HttpResponseCodes .SC_OK ;
97106import 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 ;
98110import 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.
0 commit comments