Skip to content

Commit 7896551

Browse files
committed
GH-5685 config, benchmark and tests
1 parent ea83bd7 commit 7896551

5 files changed

Lines changed: 204 additions & 99 deletions

File tree

core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/config/LmdbStoreConfig.java

Lines changed: 72 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@
88
*
99
* SPDX-License-Identifier: BSD-3-Clause
1010
*******************************************************************************/
11+
// Some portions generated by Codex
1112
package org.eclipse.rdf4j.sail.lmdb.config;
1213

1314
import java.time.Duration;
15+
import java.util.function.Consumer;
16+
import java.util.function.IntConsumer;
17+
import java.util.function.LongConsumer;
1418

19+
import org.eclipse.rdf4j.model.IRI;
1520
import org.eclipse.rdf4j.model.Model;
1621
import org.eclipse.rdf4j.model.Resource;
1722
import org.eclipse.rdf4j.model.ValueFactory;
@@ -75,6 +80,8 @@ public class LmdbStoreConfig extends BaseSailConfig {
7580

7681
private long valueEvictionInterval = Duration.ofSeconds(60).toMillis();
7782

83+
private boolean pageCardinalityEstimator = true;
84+
7885
/*--------------*
7986
* Constructors *
8087
*--------------*/
@@ -190,6 +197,15 @@ public LmdbStoreConfig setValueEvictionInterval(long valueEvictionInterval) {
190197
return this;
191198
}
192199

200+
public boolean getPageCardinalityEstimator() {
201+
return pageCardinalityEstimator;
202+
}
203+
204+
public LmdbStoreConfig setPageCardinalityEstimator(boolean pageCardinalityEstimator) {
205+
this.pageCardinalityEstimator = pageCardinalityEstimator;
206+
return this;
207+
}
208+
193209
@Override
194210
public Resource export(Model m) {
195211
Resource implNode = super.export(m);
@@ -226,6 +242,9 @@ public Resource export(Model m) {
226242
if (valueEvictionInterval != Duration.ofSeconds(60).toMillis()) {
227243
m.add(implNode, LmdbStoreSchema.VALUE_EVICTION_INTERVAL, vf.createLiteral(valueEvictionInterval));
228244
}
245+
if (!pageCardinalityEstimator) {
246+
m.add(implNode, LmdbStoreSchema.PAGE_CARDINALITY_ESTIMATOR, vf.createLiteral(false));
247+
}
229248
return implNode;
230249
}
231250

@@ -234,104 +253,61 @@ public void parse(Model m, Resource implNode) throws SailConfigException {
234253
super.parse(m, implNode);
235254

236255
try {
237-
Models.objectLiteral(m.getStatements(implNode, LmdbStoreSchema.TRIPLE_INDEXES, null))
238-
.ifPresent(lit -> setTripleIndexes(lit.getLabel()));
239-
240-
Models.objectLiteral(m.getStatements(implNode, LmdbStoreSchema.TRIPLE_DB_SIZE, null))
241-
.ifPresent(lit -> {
242-
try {
243-
setTripleDBSize(lit.longValue());
244-
} catch (NumberFormatException e) {
245-
throw new SailConfigException(
246-
"Long value required for " + LmdbStoreSchema.TRIPLE_DB_SIZE
247-
+ " property, found " + lit);
248-
}
249-
});
250-
251-
Models.objectLiteral(m.getStatements(implNode, LmdbStoreSchema.VALUE_DB_SIZE, null))
252-
.ifPresent(lit -> {
253-
try {
254-
setValueDBSize(lit.longValue());
255-
} catch (NumberFormatException e) {
256-
throw new SailConfigException(
257-
"Long value required for " + LmdbStoreSchema.VALUE_DB_SIZE
258-
+ " property, found " + lit);
259-
}
260-
});
261-
262-
Models.objectLiteral(m.getStatements(implNode, LmdbStoreSchema.FORCE_SYNC, null)).ifPresent(lit -> {
263-
try {
264-
setForceSync(lit.booleanValue());
265-
} catch (IllegalArgumentException e) {
266-
throw new SailConfigException(
267-
"Boolean value required for " + LmdbStoreSchema.FORCE_SYNC + " property, found " + lit);
268-
}
269-
});
270-
271-
Models.objectLiteral(m.getStatements(implNode, LmdbStoreSchema.VALUE_CACHE_SIZE, null)).ifPresent(lit -> {
272-
try {
273-
setValueCacheSize(lit.intValue());
274-
} catch (NumberFormatException e) {
275-
throw new SailConfigException(
276-
"Integer value required for " + LmdbStoreSchema.VALUE_CACHE_SIZE + " property, found "
277-
+ lit);
278-
}
279-
});
280-
281-
Models.objectLiteral(m.getStatements(implNode, LmdbStoreSchema.VALUE_ID_CACHE_SIZE, null))
282-
.ifPresent(lit -> {
283-
try {
284-
setValueIDCacheSize(lit.intValue());
285-
} catch (NumberFormatException e) {
286-
throw new SailConfigException(
287-
"Integer value required for " + LmdbStoreSchema.VALUE_ID_CACHE_SIZE
288-
+ " property, found " + lit);
289-
}
290-
});
291-
292-
Models.objectLiteral(m.getStatements(implNode, LmdbStoreSchema.NAMESPACE_CACHE_SIZE, null))
293-
.ifPresent(lit -> {
294-
try {
295-
setNamespaceCacheSize(lit.intValue());
296-
} catch (NumberFormatException e) {
297-
throw new SailConfigException(
298-
"Integer value required for " + LmdbStoreSchema.NAMESPACE_CACHE_SIZE
299-
+ " property, found " + lit);
300-
}
301-
});
302-
303-
Models.objectLiteral(m.getStatements(implNode, LmdbStoreSchema.NAMESPACE_ID_CACHE_SIZE, null))
304-
.ifPresent(lit -> {
305-
try {
306-
setNamespaceIDCacheSize(lit.intValue());
307-
} catch (NumberFormatException e) {
308-
throw new SailConfigException(
309-
"Integer value required for " + LmdbStoreSchema.NAMESPACE_ID_CACHE_SIZE
310-
+ " property, found " + lit);
311-
}
312-
});
313-
314-
Models.objectLiteral(m.getStatements(implNode, LmdbStoreSchema.AUTO_GROW, null)).ifPresent(lit -> {
315-
try {
316-
setAutoGrow(lit.booleanValue());
317-
} catch (IllegalArgumentException e) {
318-
throw new SailConfigException(
319-
"Boolean value required for " + LmdbStoreSchema.AUTO_GROW + " property, found " + lit);
320-
}
321-
});
322-
323-
Models.objectLiteral(m.getStatements(implNode, LmdbStoreSchema.VALUE_EVICTION_INTERVAL, null))
324-
.ifPresent(lit -> {
325-
try {
326-
setValueEvictionInterval(lit.longValue());
327-
} catch (NumberFormatException e) {
328-
throw new SailConfigException(
329-
"Long value required for " + LmdbStoreSchema.VALUE_EVICTION_INTERVAL
330-
+ " property, found " + lit);
331-
}
332-
});
256+
parseStringLiteral(m, implNode, LmdbStoreSchema.TRIPLE_INDEXES, this::setTripleIndexes);
257+
parseLongLiteral(m, implNode, LmdbStoreSchema.TRIPLE_DB_SIZE, this::setTripleDBSize);
258+
parseLongLiteral(m, implNode, LmdbStoreSchema.VALUE_DB_SIZE, this::setValueDBSize);
259+
parseBooleanLiteral(m, implNode, LmdbStoreSchema.FORCE_SYNC, this::setForceSync);
260+
parseIntegerLiteral(m, implNode, LmdbStoreSchema.VALUE_CACHE_SIZE, this::setValueCacheSize);
261+
parseIntegerLiteral(m, implNode, LmdbStoreSchema.VALUE_ID_CACHE_SIZE, this::setValueIDCacheSize);
262+
parseIntegerLiteral(m, implNode, LmdbStoreSchema.NAMESPACE_CACHE_SIZE, this::setNamespaceCacheSize);
263+
parseIntegerLiteral(m, implNode, LmdbStoreSchema.NAMESPACE_ID_CACHE_SIZE, this::setNamespaceIDCacheSize);
264+
parseBooleanLiteral(m, implNode, LmdbStoreSchema.AUTO_GROW, this::setAutoGrow);
265+
parseLongLiteral(m, implNode, LmdbStoreSchema.VALUE_EVICTION_INTERVAL, this::setValueEvictionInterval);
266+
parseBooleanLiteral(m, implNode, LmdbStoreSchema.PAGE_CARDINALITY_ESTIMATOR,
267+
this::setPageCardinalityEstimator);
333268
} catch (ModelException e) {
334269
throw new SailConfigException(e.getMessage(), e);
335270
}
336271
}
272+
273+
private void parseStringLiteral(Model m, Resource implNode, IRI property, Consumer<String> setter) {
274+
Models.objectLiteral(m.getStatements(implNode, property, null))
275+
.ifPresent(lit -> setter.accept(lit.getLabel()));
276+
}
277+
278+
private void parseIntegerLiteral(Model m, Resource implNode, IRI property, IntConsumer setter) {
279+
Models.objectLiteral(m.getStatements(implNode, property, null))
280+
.ifPresent(lit -> {
281+
try {
282+
setter.accept(lit.intValue());
283+
} catch (NumberFormatException e) {
284+
throw new SailConfigException(
285+
"Integer value required for " + property + " property, found " + lit);
286+
}
287+
});
288+
}
289+
290+
private void parseLongLiteral(Model m, Resource implNode, IRI property, LongConsumer setter) {
291+
Models.objectLiteral(m.getStatements(implNode, property, null))
292+
.ifPresent(lit -> {
293+
try {
294+
setter.accept(lit.longValue());
295+
} catch (NumberFormatException e) {
296+
throw new SailConfigException(
297+
"Long value required for " + property + " property, found " + lit);
298+
}
299+
});
300+
}
301+
302+
private void parseBooleanLiteral(Model m, Resource implNode, IRI property, Consumer<Boolean> setter) {
303+
Models.objectLiteral(m.getStatements(implNode, property, null))
304+
.ifPresent(lit -> {
305+
try {
306+
setter.accept(lit.booleanValue());
307+
} catch (IllegalArgumentException e) {
308+
throw new SailConfigException(
309+
"Boolean value required for " + property + " property, found " + lit);
310+
}
311+
});
312+
}
337313
}

core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/config/LmdbStoreSchema.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*
99
* SPDX-License-Identifier: BSD-3-Clause
1010
*******************************************************************************/
11+
// Some portions generated by Codex
1112
package org.eclipse.rdf4j.sail.lmdb.config;
1213

1314
import org.eclipse.rdf4j.model.IRI;
@@ -76,6 +77,11 @@ public class LmdbStoreSchema {
7677
*/
7778
public final static IRI VALUE_EVICTION_INTERVAL;
7879

80+
/**
81+
* <tt>http://rdf4j.org/config/sail/lmdb#pageCardinalityEstimator</tt>
82+
*/
83+
public final static IRI PAGE_CARDINALITY_ESTIMATOR;
84+
7985
static {
8086
ValueFactory factory = SimpleValueFactory.getInstance();
8187
TRIPLE_INDEXES = factory.createIRI(NAMESPACE, "tripleIndexes");
@@ -88,5 +94,6 @@ public class LmdbStoreSchema {
8894
NAMESPACE_ID_CACHE_SIZE = factory.createIRI(NAMESPACE, "namespaceIDCacheSize");
8995
AUTO_GROW = factory.createIRI(NAMESPACE, "autoGrow");
9096
VALUE_EVICTION_INTERVAL = factory.createIRI(NAMESPACE, "valueEvictionInterval");
97+
PAGE_CARDINALITY_ESTIMATOR = factory.createIRI(NAMESPACE, "pageCardinalityEstimator");
9198
}
9299
}

core/sail/lmdb/src/test/java/org/eclipse/rdf4j/sail/lmdb/benchmark/ThemeQueryBenchmark.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,26 @@
4949
import org.openjdk.jmh.runner.options.OptionsBuilder;
5050

5151
@State(Scope.Benchmark)
52-
@Warmup(iterations = 2, batchSize = 1, timeUnit = TimeUnit.SECONDS, time = 3)
52+
@Warmup(iterations = 1, batchSize = 1, timeUnit = TimeUnit.SECONDS, time = 30)
5353
@BenchmarkMode({ Mode.AverageTime })
5454
@Fork(value = 1, jvmArgs = { "-Xms32G", "-Xmx32G" })
55-
@Measurement(iterations = 2, batchSize = 1, timeUnit = TimeUnit.MILLISECONDS, time = 100)
55+
@Measurement(iterations = 1, batchSize = 1, timeUnit = TimeUnit.SECONDS, time = 10)
5656
@OutputTimeUnit(TimeUnit.MILLISECONDS)
5757
public class ThemeQueryBenchmark {
5858

59-
@Param({ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" })
59+
@Param({
60+
"0",
61+
"1",
62+
"2",
63+
"3",
64+
"4",
65+
"5",
66+
"6",
67+
"7",
68+
"8",
69+
"9",
70+
"10"
71+
})
6072
public int z_queryIndex;
6173

6274
@Param({
@@ -152,6 +164,7 @@ public void testQueryCounts() throws IOException {
152164
}
153165

154166
@Test
167+
@Disabled
155168
public void testQueryExplanation() throws IOException {
156169
String[] queryIndexes = paramValues("z_queryIndex");
157170
String[] themeNames = paramValues("themeName");
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Eclipse RDF4J contributors.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Distribution License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/org/documents/edl-v10.php.
8+
*
9+
* SPDX-License-Identifier: BSD-3-Clause
10+
*******************************************************************************/
11+
// Some portions generated by Codex
12+
package org.eclipse.rdf4j.sail.lmdb.benchmark;
13+
14+
import static org.junit.jupiter.api.Assertions.assertFalse;
15+
16+
import java.io.File;
17+
import java.io.IOException;
18+
import java.nio.charset.StandardCharsets;
19+
import java.nio.file.Files;
20+
import java.util.Arrays;
21+
22+
import org.apache.commons.io.FileUtils;
23+
import org.eclipse.rdf4j.benchmark.common.ThemeQueryCatalog;
24+
import org.eclipse.rdf4j.benchmark.rio.util.ThemeDataSetGenerator;
25+
import org.eclipse.rdf4j.benchmark.rio.util.ThemeDataSetGenerator.Theme;
26+
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
27+
import org.eclipse.rdf4j.query.explanation.Explanation;
28+
import org.eclipse.rdf4j.repository.sail.SailRepository;
29+
import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection;
30+
import org.eclipse.rdf4j.repository.util.RDFInserter;
31+
import org.eclipse.rdf4j.sail.lmdb.LmdbStore;
32+
import org.eclipse.rdf4j.sail.lmdb.config.LmdbStoreConfig;
33+
import org.junit.jupiter.api.Test;
34+
35+
class ThemeQueryBenchmarkExplanationTest {
36+
37+
@Test
38+
void recordOptimizedQueryExplanationWithAndWithoutPageEstimator() throws IOException {
39+
Theme[] themes = Theme.values();
40+
File outputDir = new File("target/theme-query-explanations");
41+
FileUtils.forceMkdir(outputDir);
42+
for (boolean pageEstimatorEnabled : new boolean[] { false, true }) {
43+
File outputFile = new File(outputDir, "optimized-explanations-page-estimator-"
44+
+ (pageEstimatorEnabled ? "enabled" : "disabled") + ".txt");
45+
StringBuilder recordedExplanations = new StringBuilder();
46+
recordedExplanations.append("pageCardinalityEstimator=").append(pageEstimatorEnabled).append('\n');
47+
recordedExplanations.append("themes=").append(Arrays.toString(themes)).append('\n');
48+
recordedExplanations.append("queryIndexes=0..").append(ThemeQueryCatalog.QUERY_COUNT - 1).append("\n\n");
49+
50+
for (Theme theme : themes) {
51+
String themeName = theme.name();
52+
File dataDir = Files.createTempDirectory("theme-query-explain").toFile();
53+
LmdbStoreConfig config = ConfigUtil.createConfig().setPageCardinalityEstimator(pageEstimatorEnabled);
54+
SailRepository repository = new SailRepository(new LmdbStore(dataDir, config));
55+
try {
56+
loadData(theme, repository);
57+
try (SailRepositoryConnection connection = repository.getConnection()) {
58+
for (int queryIndex = 0; queryIndex < ThemeQueryCatalog.QUERY_COUNT; queryIndex++) {
59+
String query = ThemeQueryCatalog.queryFor(theme, queryIndex);
60+
long expectedCount = ThemeQueryCatalog.expectedCountFor(theme, queryIndex);
61+
String explanation = connection
62+
.prepareTupleQuery(query)
63+
.explain(Explanation.Level.Optimized)
64+
.toString();
65+
assertFalse(explanation.isBlank(), "Missing explanation for theme " + themeName + " query "
66+
+ queryIndex + " with page estimator " + pageEstimatorEnabled);
67+
recordedExplanations.append("theme=")
68+
.append(themeName)
69+
.append(", queryIndex=")
70+
.append(queryIndex)
71+
.append(", expectedCount=")
72+
.append(expectedCount)
73+
.append('\n')
74+
.append(explanation)
75+
.append("\n\n");
76+
}
77+
}
78+
} finally {
79+
repository.shutDown();
80+
FileUtils.deleteDirectory(dataDir);
81+
}
82+
}
83+
FileUtils.writeStringToFile(outputFile, recordedExplanations.toString(), StandardCharsets.UTF_8);
84+
System.out.println("Wrote optimized query explanations to " + outputFile.getAbsolutePath());
85+
}
86+
}
87+
88+
private static void loadData(Theme theme, SailRepository repository) throws IOException {
89+
try (SailRepositoryConnection connection = repository.getConnection()) {
90+
connection.begin(IsolationLevels.NONE);
91+
RDFInserter inserter = new RDFInserter(connection);
92+
ThemeDataSetGenerator.generate(theme, inserter);
93+
connection.commit();
94+
}
95+
}
96+
}

core/sail/lmdb/src/test/java/org/eclipse/rdf4j/sail/lmdb/config/LmdbStoreConfigTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*
99
* SPDX-License-Identifier: BSD-3-Clause
1010
*******************************************************************************/
11+
// Some portions generated by Codex
1112
package org.eclipse.rdf4j.sail.lmdb.config;
1213

1314
import static org.assertj.core.api.Assertions.assertThat;
@@ -53,6 +54,18 @@ void testThatLmdbStoreConfigParseAndExportAutoGrow(final boolean autoGrow) {
5354
);
5455
}
5556

57+
@ParameterizedTest
58+
@ValueSource(booleans = { true, false })
59+
void testThatLmdbStoreConfigParseAndExportPageCardinalityEstimator(final boolean pageCardinalityEstimator) {
60+
testParseAndExport(
61+
LmdbStoreSchema.PAGE_CARDINALITY_ESTIMATOR,
62+
Values.literal(pageCardinalityEstimator),
63+
LmdbStoreConfig::getPageCardinalityEstimator,
64+
pageCardinalityEstimator,
65+
!pageCardinalityEstimator
66+
);
67+
}
68+
5669
@ParameterizedTest
5770
@ValueSource(ints = { 1, 205454, 0, -1231 })
5871
void testThatLmdbStoreConfigParseAndExportValueCacheSize(final int valueCacheSize) {

0 commit comments

Comments
 (0)