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