2424import java .util .Locale ;
2525import java .util .Map ;
2626import java .util .Objects ;
27+ import java .util .concurrent .TimeUnit ;
2728
2829import org .eclipse .rdf4j .benchmark .common .BenchmarkQuery ;
2930import org .eclipse .rdf4j .benchmark .common .ThemeQueryCatalog ;
@@ -56,6 +57,9 @@ public final class QueryPlanSnapshotCli {
5657 "optimized" ,
5758 "executed" );
5859 private static final String MANUAL_QUERY_ID_ENTRY = "<manual entry>" ;
60+ private static final long EXECUTION_REPEAT_SOFT_LIMIT_NANOS = TimeUnit .SECONDS .toNanos (60 );
61+ private static final int EXECUTION_REPEAT_MIN_RUNS = 2 ;
62+ private static final int EXECUTION_REPEAT_MAX_RUNS = 128 ;
5963
6064 public static void main (String [] args ) throws Exception {
6165 QueryPlanSnapshotCliOptions options = parseArgs (args );
@@ -164,6 +168,7 @@ private void runSingleQueryCapture(QueryPlanSnapshotCliOptions options,
164168
165169 QueryPlanSnapshot currentSnapshot ;
166170 Path snapshotPath = null ;
171+ QueryExecutionVerification executionVerification ;
167172 try (SailRepositoryConnection connection = storeRuntime .repository .getConnection ()) {
168173 if (options .persist ) {
169174 snapshotPath = capture .captureAndWrite (context , () -> connection .prepareTupleQuery (queryText ));
@@ -173,10 +178,12 @@ private void runSingleQueryCapture(QueryPlanSnapshotCliOptions options,
173178 currentSnapshot = capture .capture (context , () -> connection .prepareTupleQuery (queryText ));
174179 output .println ("Snapshot captured in-memory only (--persist=false)." );
175180 }
181+ executionVerification = verifyRepeatedExecution (connection , queryText );
176182 }
177183
178184 printResultsSection (options , queryId , queryText );
179185 printPrettyExplanations (currentSnapshot );
186+ printExecutionVerification (executionVerification );
180187
181188 if (options .compareLatest ) {
182189 compareWithLatest (outputDirectory , queryId , currentSnapshot , snapshotPath , capture , options .diffMode );
@@ -212,6 +219,7 @@ private void runAllThemeQueriesCapture(QueryPlanSnapshotCliOptions options,
212219
213220 QueryPlanSnapshot currentSnapshot ;
214221 Path snapshotPath = null ;
222+ QueryExecutionVerification executionVerification ;
215223 try (SailRepositoryConnection connection = storeRuntime .repository .getConnection ()) {
216224 if (options .persist ) {
217225 snapshotPath = capture .captureAndWrite (context , () -> connection .prepareTupleQuery (queryText ));
@@ -221,6 +229,7 @@ private void runAllThemeQueriesCapture(QueryPlanSnapshotCliOptions options,
221229 currentSnapshot = capture .capture (context , () -> connection .prepareTupleQuery (queryText ));
222230 output .println ("Snapshot captured in-memory only (--persist=false)." );
223231 }
232+ executionVerification = verifyRepeatedExecution (connection , queryText );
224233 }
225234
226235 output .println ();
@@ -229,6 +238,7 @@ private void runAllThemeQueriesCapture(QueryPlanSnapshotCliOptions options,
229238 "Theme=" + theme + ", QueryIndex=" + queryIndex + ", QueryName=" + benchmarkQuery .getName ());
230239 printResultsSection (perQueryOptions , queryId , queryText );
231240 printPrettyExplanations (currentSnapshot );
241+ printExecutionVerification (executionVerification );
232242
233243 if (options .compareLatest ) {
234244 compareWithLatest (outputDirectory , queryId , currentSnapshot , snapshotPath , capture ,
@@ -942,6 +952,84 @@ private void printExplanation(String levelKey, QueryPlanExplanation explanation)
942952 }
943953 }
944954
955+ private QueryExecutionVerification verifyRepeatedExecution (SailRepositoryConnection connection , String queryText ) {
956+ long elapsedNanos = 0 ;
957+ long stableResultCount = Long .MIN_VALUE ;
958+ int runs = 0 ;
959+ boolean softLimitReached = false ;
960+
961+ while (runs < EXECUTION_REPEAT_MAX_RUNS ) {
962+ if (runs >= EXECUTION_REPEAT_MIN_RUNS ) {
963+ long averageNanos = Math .max (1L , elapsedNanos / runs );
964+ if (elapsedNanos + averageNanos > EXECUTION_REPEAT_SOFT_LIMIT_NANOS ) {
965+ softLimitReached = true ;
966+ break ;
967+ }
968+ } else if (elapsedNanos >= EXECUTION_REPEAT_SOFT_LIMIT_NANOS ) {
969+ softLimitReached = true ;
970+ break ;
971+ }
972+
973+ long startedAt = System .nanoTime ();
974+ long currentResultCount = connection .prepareTupleQuery (queryText ).evaluate ().stream ().count ();
975+ long runNanos = Math .max (1L , System .nanoTime () - startedAt );
976+ elapsedNanos += runNanos ;
977+ runs ++;
978+
979+ if (stableResultCount == Long .MIN_VALUE ) {
980+ stableResultCount = currentResultCount ;
981+ } else if (stableResultCount != currentResultCount ) {
982+ throw new IllegalStateException ("Result count changed between repeated runs: expected "
983+ + stableResultCount + " but got " + currentResultCount + " on run " + runs );
984+ }
985+ }
986+
987+ boolean maxRunsReached = runs >= EXECUTION_REPEAT_MAX_RUNS ;
988+ if (runs == 0 ) {
989+ return new QueryExecutionVerification (0 , 0 , 0 , softLimitReached , maxRunsReached );
990+ }
991+
992+ return new QueryExecutionVerification (runs , elapsedNanos , stableResultCount , softLimitReached ,
993+ maxRunsReached );
994+ }
995+
996+ private void printExecutionVerification (QueryExecutionVerification executionVerification ) {
997+ output .println ();
998+ output .println ("=== Execution Verification ===" );
999+ if (executionVerification .runs == 0 ) {
1000+ output .println ("No repeated runs executed." );
1001+ return ;
1002+ }
1003+
1004+ long totalMillis = TimeUnit .NANOSECONDS .toMillis (executionVerification .elapsedNanos );
1005+ long averageMillis = TimeUnit .NANOSECONDS .toMillis (
1006+ executionVerification .elapsedNanos / executionVerification .runs );
1007+ output .println ("runs=" + executionVerification .runs
1008+ + ", totalMillis=" + totalMillis
1009+ + ", averageMillis=" + averageMillis
1010+ + ", resultCount=" + executionVerification .resultCount
1011+ + ", softLimitMillis=" + TimeUnit .NANOSECONDS .toMillis (EXECUTION_REPEAT_SOFT_LIMIT_NANOS )
1012+ + ", softLimitReached=" + executionVerification .softLimitReached
1013+ + ", maxRunsReached=" + executionVerification .maxRunsReached );
1014+ }
1015+
1016+ private static final class QueryExecutionVerification {
1017+ private final int runs ;
1018+ private final long elapsedNanos ;
1019+ private final long resultCount ;
1020+ private final boolean softLimitReached ;
1021+ private final boolean maxRunsReached ;
1022+
1023+ private QueryExecutionVerification (int runs , long elapsedNanos , long resultCount , boolean softLimitReached ,
1024+ boolean maxRunsReached ) {
1025+ this .runs = runs ;
1026+ this .elapsedNanos = elapsedNanos ;
1027+ this .resultCount = resultCount ;
1028+ this .softLimitReached = softLimitReached ;
1029+ this .maxRunsReached = maxRunsReached ;
1030+ }
1031+ }
1032+
9451033 static QueryPlanSnapshotCliOptions parseArgs (String [] args ) {
9461034 return QueryPlanSnapshotCliOptions .parseArgs (args );
9471035 }
0 commit comments