- SELECT (count(?s) as ?n)# ?p ?o
- WHERE {
- ?s ?p ?o .
- }
- #LIMIT 10
- """
-
- def tupleQuery = connection.prepareTupleQuery(query)
- def result = tupleQuery.evaluate()
-
- println "Query Results:"
- while (result.hasNext()) {
- def bindingSet = result.next()
- println bindingSet
- }
-} finally {
- // Step 5: Close the connection
- connection.close()
- repository.shutDown()
-}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/benchmark/Main.java b/src/main/java/fr/inria/corese/benchmark/Main.java
new file mode 100644
index 0000000..5f34898
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/Main.java
@@ -0,0 +1,112 @@
+package fr.inria.corese.benchmark;
+
+import fr.inria.corese.benchmark.driver.DriverFactory;
+import fr.inria.corese.benchmark.driver.TripleStoreDriver;
+import fr.inria.corese.benchmark.harness.BenchmarkHarness;
+import fr.inria.corese.benchmark.metrics.FileResult;
+import fr.inria.corese.benchmark.metrics.QueryResult;
+import fr.inria.corese.benchmark.report.BenchmarkReport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Entry point for the benchmark harness.
+ */
+public class Main {
+
+ private static final Logger log = LoggerFactory.getLogger(Main.class);
+
+ /**
+ * Parses command-line arguments, runs the load and (optionally) query benchmarks
+ * for each specified engine, then writes the combined report.
+ *
+ * Arguments:
+ *
+ * - {@code datasetDir} — directory containing {@code .nt} files
+ * - {@code outputFile} — path for the generated JSON report
+ * - {@code engines} — comma-separated engine names (e.g. {@code corese.4.6.3,jena.5.2.0})
+ * - {@code datasetName} — (optional) logical dataset label; defaults to {@code "dataset"}
+ * - {@code queriesFile} — (optional) path to a file with one SPARQL query per line
+ * - {@code --inference} — (optional flag) enables RDFS inference for Corese drivers
+ *
+ *
+ * If an engine fails, the error is logged and the harness continues with the next engine.
+ * The process exits with status 1 if the report cannot be written or if fewer than
+ * 3 arguments are supplied.
+ *
+ * @param args command-line arguments as described above
+ */
+ public static void main(String[] args) {
+ if (args.length < 3) {
+ log.error("Usage: benchmark [datasetName] [queriesFile]");
+ System.exit(1);
+ }
+
+ String datasetDir = args[0];
+ String outputFile = args[1];
+ String[] engines = args[2].split(",");
+ String datasetName = args.length > 3 ? args[3] : "dataset";
+
+ // Parse remaining args: optional queries file and --inference flag
+ String queriesFile = null;
+ boolean inference = false;
+ for (int i = 4; i < args.length; i++) {
+ if ("--inference".equals(args[i])) {
+ inference = true;
+ } else if (queriesFile == null) {
+ queriesFile = args[i];
+ }
+ }
+
+ List queries = new ArrayList<>();
+ if (queriesFile != null) {
+ try {
+ queries = Files.readAllLines(Path.of(queriesFile));
+ queries.removeIf(String::isBlank);
+ log.info("Loaded {} queries from: {}", queries.size(), queriesFile);
+ } catch (Exception e) {
+ log.error("Failed to read queries file {}: {}", queriesFile, e.getMessage());
+ System.exit(1);
+ }
+ }
+
+ List allFileResults = new ArrayList<>();
+ List allQueryResults = new ArrayList<>();
+
+ for (String engineName : engines) {
+ engineName = engineName.trim();
+ log.info("Starting engine: {}", engineName);
+ try {
+ TripleStoreDriver driver = DriverFactory.create(engineName, inference);
+ BenchmarkHarness harness = new BenchmarkHarness(driver, datasetName);
+
+ // Load benchmark
+ List fileResults = harness.run(datasetDir);
+ allFileResults.addAll(fileResults);
+
+ // Query benchmark (optional)
+ if (!queries.isEmpty()) {
+ List queryResults = harness.runQueryBenchmark(datasetDir, queries);
+ allQueryResults.addAll(queryResults);
+ }
+ } catch (OutOfMemoryError e) {
+ System.gc();
+ log.error("Engine {} ran out of memory — skipping to next engine", engineName, e);
+ } catch (Exception e) {
+ log.error("Engine {} failed: {} — skipping to next engine", engineName, e.getMessage(), e);
+ }
+ }
+
+ try {
+ BenchmarkReport.write(allFileResults, allQueryResults, queries, outputFile);
+ } catch (Exception e) {
+ log.error("Failed to write report: {}", e.getMessage(), e);
+ System.exit(1);
+ }
+ }
+}
diff --git a/src/main/java/fr/inria/corese/benchmark/driver/CoreseDriver.java b/src/main/java/fr/inria/corese/benchmark/driver/CoreseDriver.java
new file mode 100644
index 0000000..1c385f1
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/driver/CoreseDriver.java
@@ -0,0 +1,98 @@
+package fr.inria.corese.benchmark.driver;
+
+import fr.inria.corese.core.Graph;
+import fr.inria.corese.core.load.Load;
+import fr.inria.corese.core.api.Loader;
+import fr.inria.corese.core.query.QueryProcess;
+import fr.inria.corese.core.rule.RuleEngine;
+
+/**
+ * {@link TripleStoreDriver} implementation for Corese 4.x using the classic
+ * {@code Graph} / {@code Load} / {@code QueryProcess} API.
+ *
+ * Engine names handled: {@code corese.} and {@code corese.local}.
+ * For Corese 5 (new Repositories API) see {@code CoreseNextDriver}.
+ */
+public class CoreseDriver implements TripleStoreDriver {
+
+ private final String name;
+ private final boolean inference;
+ private Graph graph;
+
+ /**
+ * @param name engine label used in benchmark reports (e.g. {@code corese.4.6.5})
+ * @param inference {@code true} to apply OWL-RL reasoning after loading
+ */
+ public CoreseDriver(String name, boolean inference) {
+ this.name = name;
+ this.inference = inference;
+ }
+
+ /** Creates a new empty {@link Graph}. */
+ @Override
+ public void init() {
+ this.graph = Graph.create();
+ }
+
+ /**
+ * Loads an N-Triples file into the graph and, if inference is enabled,
+ * applies OWL-RL rules via {@link RuleEngine}.
+ *
+ * @param filePath absolute path to the {@code .nt} file
+ * @throws Exception if parsing or rule processing fails
+ */
+ @Override
+ public void load(String filePath) throws Exception {
+ Load loader = Load.create(graph);
+ loader.parse(filePath, Loader.format.NT_FORMAT);
+ applyInference();
+ }
+
+ /**
+ * Executes a SPARQL query and returns the elapsed time.
+ *
+ * @param sparql SPARQL query string (SELECT, ASK, CONSTRUCT, DESCRIBE)
+ * @return execution time in milliseconds
+ * @throws Exception if the query is malformed or execution fails
+ */
+ @Override
+ public long query(String sparql) throws Exception {
+ QueryProcess exec = QueryProcess.create(graph);
+ long start = System.currentTimeMillis();
+ exec.query(sparql);
+ return System.currentTimeMillis() - start;
+ }
+
+ /** @return number of triples in the graph */
+ @Override
+ public long getGraphSize() {
+ return graph.size();
+ }
+
+ /** Replaces the current graph with a new empty one, releasing all triples. */
+ @Override
+ public void clear() {
+ this.graph = Graph.create();
+ }
+
+ /** Releases the graph reference. */
+ @Override
+ public void close() {
+ this.graph = null;
+ }
+
+ /** @return the engine label passed at construction time */
+ @Override
+ public String getName() {
+ return name;
+ }
+
+
+ private void applyInference() throws Exception {
+ if (inference) {
+ RuleEngine re = RuleEngine.create(graph);
+ re.setProfile(RuleEngine.ProfileType.OWL_RL);
+ re.process();
+ }
+ }
+}
diff --git a/src/main/java/fr/inria/corese/benchmark/driver/CoreseNextDriver.java b/src/main/java/fr/inria/corese/benchmark/driver/CoreseNextDriver.java
new file mode 100644
index 0000000..b5c1ea1
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/driver/CoreseNextDriver.java
@@ -0,0 +1,193 @@
+package fr.inria.corese.benchmark.driver;
+
+import fr.inria.corese.core.next.data.api.io.format.RDFFormat;
+import fr.inria.corese.core.next.data.api.model.Model;
+import fr.inria.corese.core.next.io.CoreseIO;
+import fr.inria.corese.core.next.query.Repositories;
+import fr.inria.corese.core.next.query.api.repository.Repository;
+import fr.inria.corese.core.next.query.api.repository.RepositoryConnection;
+import fr.inria.corese.core.next.query.api.result.GraphQueryResult;
+import fr.inria.corese.core.next.query.api.result.TupleQueryResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Benchmark driver for the Corese 5 API (corese-next).
+ *
+ * Uses the new {@code Repositories} / {@code RepositoryConnection} / {@code CoreseIO}
+ * public API introduced in Corese 5. Requires the Corese 5 JAR in {@code libs/} and
+ * building with {@code -PcoreseVersion=local}.
+ *
+ * Engine name convention: {@code corese.next} (or any name starting with that prefix).
+ */
+@SuppressWarnings("unused")
+public class CoreseNextDriver implements TripleStoreDriver {
+
+ private static final Logger log = LoggerFactory.getLogger(CoreseNextDriver.class);
+ private final String name;
+ private Repository repository;
+ private RepositoryConnection connection;
+
+ /**
+ * Constructs a new driver with the given engine name.
+ *
+ * @param name human-readable engine name reported in benchmark results (e.g. {@code "corese.next"})
+ */
+ public CoreseNextDriver(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Initialises the Corese 5 in-memory repository and opens a connection.
+ */
+ @Override
+ public void init() {
+ repository = Repositories.create();
+ connection = repository.getConnection();
+ }
+
+ /**
+ * Loads an N-Triples file into the repository.
+ *
+ *
+ * Fallback: if the fast path throws (e.g. invalid IRIs in DBpedia {@code homepages_en.nt}),
+ * the file is re-read line by line, skipping and counting invalid lines.
+ *
+ * @param filePath absolute path to the {@code .nt} file to load
+ * @throws IOException if the file cannot be read
+ */
+ @Override
+ public void load(String filePath) throws IOException {
+ Path path = Path.of(filePath);
+ try {
+ connection.load(path, RDFFormat.NTRIPLES);
+ } catch (Exception e) {
+ log.debug("corese.next: fast load failed for {} ({}), switching to line-by-line", path.getFileName(), e.getMessage());
+ loadLineByLine(path);
+ }
+ }
+
+ /**
+ * Fallback loader: reads the file line by line, skipping lines with invalid IRIs.
+ *
+ * @param path path to the {@code .nt} file
+ * @throws IOException if the file cannot be read
+ */
+ private void loadLineByLine(Path path) throws IOException {
+ int skipped = 0;
+ try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ String trimmed = line.trim();
+ if (trimmed.isEmpty() || trimmed.startsWith("#")) continue;
+ try {
+ byte[] bytes = (trimmed + "\n").getBytes(StandardCharsets.UTF_8);
+ Model model = CoreseIO.read(new ByteArrayInputStream(bytes), RDFFormat.NTRIPLES);
+ connection.add(model);
+ } catch (Exception ex) {
+ skipped++;
+ }
+ }
+ }
+ if (skipped > 0) {
+ log.warn("corese.next: skipped {} line(s) with invalid IRIs in {}", skipped, path.getFileName());
+ }
+ }
+
+ /**
+ * Executes a SPARQL query and fully iterates the result set to ensure the engine
+ * has performed all work before the timer stops.
+ * Supports SELECT, ASK, CONSTRUCT, and DESCRIBE query forms.
+ *
+ * @param sparql the SPARQL query string
+ * @return execution time in milliseconds
+ */
+ @Override
+ public long query(String sparql) {
+ long start = System.currentTimeMillis();
+ switch (detectQueryType(sparql)) {
+ case SELECT -> {
+ try (TupleQueryResult result = connection.prepareTupleQuery(sparql).evaluate()) {
+ while (result.hasNext()) result.next();
+ }
+ }
+ case ASK -> connection.prepareBooleanQuery(sparql).evaluate();
+ default -> {
+ try (GraphQueryResult result = connection.prepareGraphQuery(sparql).evaluate()) {
+ while (result.hasNext()) result.next();
+ }
+ }
+ }
+ return System.currentTimeMillis() - start;
+ }
+
+ /**
+ * Returns the total number of statements currently held in the repository.
+ *
+ * @return triple count
+ */
+ @Override
+ public long getGraphSize() {
+ return connection.size();
+ }
+
+ /**
+ * Removes all statements from the repository connection.
+ */
+ @Override
+ public void clear() {
+ connection.clear();
+ }
+
+ /**
+ * Closes the repository connection and shuts down the repository, releasing
+ * all resources. Safe to call even if {@link #init()} was never called.
+ */
+ @Override
+ public void close() {
+ if (connection != null) {
+ connection.close();
+ connection = null;
+ }
+ if (repository != null) {
+ repository.close();
+ repository = null;
+ }
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+
+ private enum QueryType { SELECT, ASK, GRAPH }
+
+ /**
+ * Determines the SPARQL query form by inspecting the first keyword after any
+ * PREFIX/BASE declarations.
+ *
+ * @param sparql the SPARQL query string
+ * @return the detected {@link QueryType}
+ */
+ private QueryType detectQueryType(String sparql) {
+ String upper = sparql.trim().toUpperCase();
+ // Strip PREFIX and BASE declarations — stop if no newline to avoid infinite loop
+ int safety = 0;
+ while ((upper.startsWith("PREFIX") || upper.startsWith("BASE")) && safety++ < 50) {
+ int nl = upper.indexOf('\n');
+ if (nl < 0) break;
+ upper = upper.substring(nl + 1).stripLeading();
+ }
+ if (upper.startsWith("ASK")) return QueryType.ASK;
+ if (upper.startsWith("CONSTRUCT") || upper.startsWith("DESCRIBE")) return QueryType.GRAPH;
+ return QueryType.SELECT;
+ }
+}
diff --git a/src/main/java/fr/inria/corese/benchmark/driver/DriverFactory.java b/src/main/java/fr/inria/corese/benchmark/driver/DriverFactory.java
new file mode 100644
index 0000000..4d41a56
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/driver/DriverFactory.java
@@ -0,0 +1,68 @@
+package fr.inria.corese.benchmark.driver;
+
+/**
+ * Creates the appropriate TripleStoreDriver from an engine name.
+ * Format: "corese.4.6.5", "corese.local", "corese.next", "jena.5.2.0", "rdf4j.5.1.3"
+ */
+public class DriverFactory {
+
+ private static final String CORESE_NEXT_CLASS =
+ "fr.inria.corese.benchmark.driver.CoreseNextDriver";
+
+
+ /**
+ * Creates a driver for the given engine name.
+ *
+ * Engine name prefix determines the implementation:
+ *
+ * - {@code corese.next} — {@link CoreseNextDriver} loaded via reflection
+ * - {@code corese.*} — {@code CoreseDriver} with optional RDFS inference
+ * - {@code jena.*} — {@link JenaDriver}
+ * - {@code rdf4j.*} — {@link RDF4JDriver}
+ *
+ *
+ * @param engineName engine identifier string
+ * @param inference {@code true} to enable RDFS inference (only applies to Corese drivers)
+ * @return a ready-to-use {@link TripleStoreDriver} (not yet initialised)
+ * @throws IllegalArgumentException if the engine name is not recognised or if
+ * {@code corese.next} is requested but the Corese 5 JAR is absent
+ */
+ public static TripleStoreDriver create(String engineName, boolean inference) {
+ if ("corese.next".equals(engineName)) {
+ return createCoreseNext(engineName);
+ } else if (engineName.startsWith("corese.")) {
+ return new CoreseDriver(engineName, inference);
+ } else if (engineName.startsWith("jena.")) {
+ return new JenaDriver(engineName);
+ } else if (engineName.startsWith("rdf4j.")) {
+ return new RDF4JDriver(engineName);
+ } else {
+ throw new IllegalArgumentException("Unknown engine: " + engineName
+ + " (expected format: corese.X.X.X, corese.next, jena.X.X.X, rdf4j.X.X.X)");
+ }
+ }
+
+ /**
+ * Instantiates {@link CoreseNextDriver} via reflection so that the main classpath
+ * does not need a hard compile-time dependency on the Corese 5 JAR.
+ *
+ * @param engineName engine name passed through to the driver constructor
+ * @return a new {@link CoreseNextDriver} instance
+ * @throws IllegalArgumentException if the Corese 5 JAR is not on the classpath
+ * @throws RuntimeException if reflection-based instantiation fails for any other reason
+ */
+ @SuppressWarnings("unchecked")
+ private static TripleStoreDriver createCoreseNext(String engineName) {
+ try {
+ Class extends TripleStoreDriver> cls =
+ (Class extends TripleStoreDriver>) Class.forName(CORESE_NEXT_CLASS);
+ return cls.getDeclaredConstructor(String.class).newInstance(engineName);
+ } catch (ClassNotFoundException | NoClassDefFoundError e) {
+ throw new IllegalArgumentException(
+ "Engine 'corese.next' requires the Corese 5 JAR in libs/ "
+ + "— place the JAR in libs/ and rebuild with -PcoreseVersion=local", e);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException("Failed to instantiate CoreseNextDriver", e);
+ }
+ }
+}
diff --git a/src/main/java/fr/inria/corese/benchmark/driver/JenaDriver.java b/src/main/java/fr/inria/corese/benchmark/driver/JenaDriver.java
new file mode 100644
index 0000000..8fdffb7
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/driver/JenaDriver.java
@@ -0,0 +1,105 @@
+package fr.inria.corese.benchmark.driver;
+
+import org.apache.jena.query.*;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.ModelFactory;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.riot.Lang;
+
+/**
+ * Benchmark driver for Apache Jena 5.
+ *
+ * Stores triples in a Jena in-memory {@link Model} and executes SPARQL queries
+ * via {@link QueryExecution}. All four SPARQL query forms (SELECT, CONSTRUCT, ASK,
+ * DESCRIBE) are supported.
+ */
+public class JenaDriver implements TripleStoreDriver {
+
+ private final String name;
+ private Model model;
+
+ /**
+ * Constructs a new driver with the given engine name.
+ *
+ * @param name human-readable engine name reported in benchmark results (e.g. {@code "jena.5.2.0"})
+ */
+ public JenaDriver(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Initialises a fresh Jena default (in-memory) model.
+ */
+ @Override
+ public void init() {
+ this.model = ModelFactory.createDefaultModel();
+ }
+
+ /**
+ * Loads an N-Triples file into the Jena model using RIOT streaming parsing.
+ *
+ * @param filePath absolute path to the {@code .nt} file to load
+ */
+ @Override
+ public void load(String filePath) {
+ RDFDataMgr.read(model, filePath, Lang.NT);
+ }
+
+ /**
+ * Executes a SPARQL query and fully materialises the result to ensure all
+ * engine work is captured within the timed window.
+ * Supports SELECT, CONSTRUCT, ASK, and DESCRIBE query forms.
+ *
+ * @param sparql the SPARQL query string
+ * @return execution time in milliseconds
+ */
+ @Override
+ public long query(String sparql) {
+ long start = System.currentTimeMillis();
+ try (QueryExecution qe = QueryExecution.model(model).query(sparql).build()) {
+ if (qe.getQuery().isSelectType()) {
+ ResultSet rs = qe.execSelect();
+ while (rs.hasNext()) rs.next();
+ } else if (qe.getQuery().isConstructType()) {
+ qe.execConstruct();
+ } else if (qe.getQuery().isAskType()) {
+ qe.execAsk();
+ } else if (qe.getQuery().isDescribeType()) {
+ qe.execDescribe();
+ }
+ }
+ return System.currentTimeMillis() - start;
+ }
+
+ /**
+ * Returns the number of statements currently held in the Jena model.
+ *
+ * @return triple count
+ */
+ @Override
+ public long getGraphSize() {
+ return model.size();
+ }
+
+ /**
+ * Removes all statements from the model.
+ */
+ @Override
+ public void clear() {
+ model.removeAll();
+ }
+
+ /**
+ * Closes and discards the Jena model, releasing all associated resources.
+ */
+ @Override
+ public void close() {
+ model.close();
+ this.model = null;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/fr/inria/corese/benchmark/driver/RDF4JDriver.java b/src/main/java/fr/inria/corese/benchmark/driver/RDF4JDriver.java
new file mode 100644
index 0000000..3eb38de
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/driver/RDF4JDriver.java
@@ -0,0 +1,178 @@
+package fr.inria.corese.benchmark.driver;
+
+import org.eclipse.rdf4j.repository.sail.SailRepository;
+import org.eclipse.rdf4j.repository.RepositoryConnection;
+import org.eclipse.rdf4j.sail.memory.MemoryStore;
+import org.eclipse.rdf4j.query.TupleQuery;
+import org.eclipse.rdf4j.query.TupleQueryResult;
+import org.eclipse.rdf4j.query.QueryLanguage;
+import org.eclipse.rdf4j.rio.RDFFormat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Benchmark driver for Eclipse RDF4J 5.
+ *
+ * Stores triples in an RDF4J in-memory {@link SailRepository} backed by a
+ * {@link MemoryStore}. N-Triples files are parsed in batches of 10 000 lines;
+ * when a batch fails due to a malformed triple the driver falls back to
+ * line-by-line parsing so that valid triples are still loaded.
+ */
+public class RDF4JDriver implements TripleStoreDriver {
+
+ private static final Logger log = LoggerFactory.getLogger(RDF4JDriver.class);
+
+ private final String name;
+ private SailRepository repository;
+
+ /**
+ * Constructs a new driver with the given engine name.
+ *
+ * @param name human-readable engine name reported in benchmark results (e.g. {@code "rdf4j.5.1.3"})
+ */
+ public RDF4JDriver(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Initialises a fresh RDF4J in-memory {@link SailRepository}.
+ */
+ @Override
+ public void init() {
+ this.repository = new SailRepository(new MemoryStore());
+ this.repository.init();
+ }
+
+ /**
+ * Loads an N-Triples file into the repository using batched parsing.
+ * Batches of up to 10 000 lines are submitted together; any batch that triggers
+ * a parse error is retried line by line so that well-formed triples are preserved.
+ * Malformed individual lines are logged as warnings and skipped.
+ *
+ * @param filePath absolute path to the {@code .nt} file to load
+ * @throws Exception if the file cannot be opened or the transaction cannot be committed
+ */
+ @Override
+ public void load(String filePath) throws Exception {
+ // Parse in batches; on failure fall back to line-by-line to skip malformed triples.
+ // The NPE from GenericStatement (null IRI) is a RuntimeException and IS catchable.
+ try (RepositoryConnection conn = repository.getConnection();
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(filePath), StandardCharsets.UTF_8))) {
+ conn.begin();
+ StringBuilder batch = new StringBuilder();
+ String line;
+ int batchLineCount = 0;
+ while ((line = reader.readLine()) != null) {
+ String trimmed = line.trim();
+ if (trimmed.isEmpty() || trimmed.startsWith("#")) continue;
+ batch.append(trimmed).append('\n');
+ batchLineCount++;
+ if (batchLineCount >= 10_000) {
+ flushBatch(batch.toString(), conn);
+ batch.setLength(0);
+ batchLineCount = 0;
+ }
+ }
+ if (!batch.isEmpty()) {
+ flushBatch(batch.toString(), conn);
+ }
+ conn.commit();
+ }
+ }
+
+ /**
+ * Attempts to add a block of N-Triples content to the repository in one call.
+ * If that fails, falls back to adding each line individually, logging a warning
+ * for any line that cannot be parsed.
+ *
+ * @param content N-Triples text (one or more lines, newline-terminated)
+ * @param conn active repository connection; the caller manages its transaction
+ */
+ private void flushBatch(String content, RepositoryConnection conn) {
+ try {
+ conn.add(new StringReader(content), "", RDFFormat.NTRIPLES);
+ } catch (Exception batchEx) {
+ // Batch contains a malformed triple — fall back to line-by-line
+ for (String ln : content.split("\n")) {
+ if (ln.trim().isEmpty()) continue;
+ try {
+ conn.add(new StringReader(ln + "\n"), "", RDFFormat.NTRIPLES);
+ } catch (Exception lineEx) {
+ log.warn("Skipping malformed triple: {}", ln);
+ }
+ }
+ }
+ }
+
+ /**
+ * Executes a SPARQL SELECT query and fully iterates the result set.
+ * Note: only SELECT queries are currently supported by this driver.
+ *
+ * @param sparql the SPARQL SELECT query string
+ * @return execution time in milliseconds
+ */
+ @Override
+ public long query(String sparql) {
+ long start = System.currentTimeMillis();
+ try (RepositoryConnection conn = repository.getConnection()) {
+ TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, sparql);
+ try (TupleQueryResult result = q.evaluate()) {
+ while (result.hasNext()) result.next();
+ }
+ }
+ return System.currentTimeMillis() - start;
+ }
+
+ /**
+ * Returns the total number of statements held in the repository.
+ *
+ * @return triple count
+ */
+ @Override
+ public long getGraphSize() {
+ try (RepositoryConnection conn = repository.getConnection()) {
+ return conn.size();
+ }
+ }
+
+ /**
+ * Resets the repository by shutting it down and reinitialising a fresh
+ * {@link MemoryStore}. A full reinit is used instead of a simple
+ * {@code removeAll} to avoid negative memory-delta measurements caused
+ * by GC residuals from the previous repetition.
+ */
+ @Override
+ public void clear() {
+ // Full reinit to release internal SAIL structures — avoids negative memory deltas
+ // caused by GC collecting previous-rep residuals mid-measurement.
+ if (repository != null) {
+ repository.shutDown();
+ }
+ this.repository = new SailRepository(new MemoryStore());
+ this.repository.init();
+ }
+
+ /**
+ * Shuts down the repository and releases all resources.
+ * Safe to call even if {@link #init()} was never called.
+ */
+ @Override
+ public void close() {
+ if (repository != null) {
+ repository.shutDown();
+ this.repository = null;
+ }
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/fr/inria/corese/benchmark/driver/TripleStoreDriver.java b/src/main/java/fr/inria/corese/benchmark/driver/TripleStoreDriver.java
new file mode 100644
index 0000000..ac742bd
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/driver/TripleStoreDriver.java
@@ -0,0 +1,29 @@
+package fr.inria.corese.benchmark.driver;
+
+/**
+ * Generic interface for all triple store benchmark drivers.
+ * Each implementation wraps a specific triple store (Corese, Jena, RDF4J).
+ */
+public interface TripleStoreDriver {
+
+ /** Initialize the triple store (create repository, allocate resources). */
+ void init() throws Exception;
+
+ /** Load an RDF file (N-Triples format) into the store. */
+ void load(String filePath) throws Exception;
+
+ /** Execute a SPARQL query and return execution time in milliseconds. */
+ long query(String sparql) throws Exception;
+
+ /** Return the number of triples currently in the store. */
+ long getGraphSize() throws Exception;
+
+ /** Clear all triples from the store (used between repetitions). */
+ void clear() throws Exception;
+
+ /** Release all resources held by the store. */
+ void close() throws Exception;
+
+ /** Return a human-readable name for this driver (e.g. "corese.4.6.5"). */
+ String getName();
+}
diff --git a/src/main/java/fr/inria/corese/benchmark/harness/BenchmarkHarness.java b/src/main/java/fr/inria/corese/benchmark/harness/BenchmarkHarness.java
new file mode 100644
index 0000000..42f109c
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/harness/BenchmarkHarness.java
@@ -0,0 +1,214 @@
+package fr.inria.corese.benchmark.harness;
+
+import fr.inria.corese.benchmark.driver.TripleStoreDriver;
+import fr.inria.corese.benchmark.metrics.FileResult;
+import fr.inria.corese.benchmark.metrics.MetricsCollector;
+import fr.inria.corese.benchmark.metrics.QueryResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.*;
+
+/**
+ * Benchmark harness: orchestrates warmup + 3 repetitions + median for each file.
+ * Protocol per file:
+ * 1. Warmup: load → clear → gc (discarded)
+ * 2. Repeat 3 times:
+ * - gc + memory snapshot before
+ * - load
+ * - gc + memory snapshot after
+ * - record time and memory delta
+ * 3. Keep the median of 3 repetitions
+ */
+public class BenchmarkHarness {
+
+ private static final Logger log = LoggerFactory.getLogger(BenchmarkHarness.class);
+ private static final int REPETITIONS = 3;
+ private static final int QUERY_REPS = 3;
+ private static final long QUERY_TIMEOUT_S = 30; // seconds per query execution
+
+ private final TripleStoreDriver driver;
+ private final MetricsCollector metrics;
+ private final String datasetName;
+
+ /**
+ * Constructs a harness for the given driver and dataset.
+ *
+ * @param driver the triple-store driver to benchmark
+ * @param datasetName logical name of the dataset (recorded in result objects)
+ */
+ public BenchmarkHarness(TripleStoreDriver driver, String datasetName) {
+ this.driver = driver;
+ this.datasetName = datasetName;
+ this.metrics = new MetricsCollector();
+ }
+
+ /**
+ * Runs the load benchmark over all {@code .nt} files found in {@code datasetDir}.
+ * Files are sorted alphabetically. For each file the harness performs one warmup
+ * load followed by {@value #REPETITIONS} measured repetitions, keeping the median.
+ * The driver is initialised at the start and closed in a {@code finally} block.
+ *
+ * @param datasetDir path to the directory containing {@code .nt} files
+ * @return one {@link FileResult} per file, in sorted file order
+ * @throws IllegalArgumentException if no {@code .nt} files are found
+ * @throws Exception if the driver or file I/O raises an error
+ */
+ public List run(String datasetDir) throws Exception {
+ File dir = new File(datasetDir);
+ File[] files = dir.listFiles(f -> f.getName().endsWith(".nt"));
+ if (files == null || files.length == 0) {
+ throw new IllegalArgumentException("No .nt files found in: " + datasetDir);
+ }
+ Arrays.sort(files);
+
+ List results = new ArrayList<>();
+ driver.init();
+ try {
+ log.info("=== Benchmark: {} | Dataset: {} | {} file(s) ===", driver.getName(), datasetName, files.length);
+ for (int fi = 0; fi < files.length; fi++) {
+ File file = files[fi];
+ log.info(" [{}/{}] {}", fi + 1, files.length, file.getName());
+ try {
+ FileResult result = benchmarkFile(file);
+ results.add(result);
+ log.info(" [{}/{}] {}", fi + 1, files.length,
+ String.format(Locale.US, "%-45s | %6d ms | %8.1f MB | %,.0f triples/s",
+ file.getName(), result.loadingTimeMs(),
+ result.memoryDeltaMb(), result.throughputTriplesPerSec()));
+ } catch (OutOfMemoryError e) {
+ System.gc();
+ log.error(" [{}/{}] {} — OUT OF MEMORY, skipping file", fi + 1, files.length, file.getName(), e);
+ }
+ }
+ } finally {
+ driver.close();
+ }
+ return results;
+ }
+
+ /**
+ * Load all dataset files once, then run each SPARQL query QUERY_REPS times.
+ * Returns one QueryResult per (engine, query).
+ */
+ public List runQueryBenchmark(String datasetDir, List queries) throws Exception {
+ File dir = new File(datasetDir);
+ File[] files = dir.listFiles(f -> f.getName().endsWith(".nt"));
+ if (files == null || files.length == 0) {
+ throw new IllegalArgumentException("No .nt files found in: " + datasetDir);
+ }
+ Arrays.sort(files);
+
+ List results = new ArrayList<>();
+ driver.init();
+ try {
+ log.info("=== Query Benchmark: {} | Dataset: {} ===", driver.getName(), datasetName);
+
+ // Load all files once
+ log.info(" Loading {} file(s)...", files.length);
+ for (File file : files) {
+ driver.load(file.getAbsolutePath());
+ }
+ log.info(" {} triples loaded — running {} queries × {} reps",
+ driver.getGraphSize(), queries.size(), QUERY_REPS);
+
+ // Benchmark each query with per-execution timeout
+ try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
+ for (int qi = 0; qi < queries.size(); qi++) {
+ String sparql = queries.get(qi).trim();
+ if (sparql.isEmpty()) continue;
+ String queryId = "query_" + (qi + 1);
+
+ long[] times = new long[QUERY_REPS];
+ for (int i = 0; i < QUERY_REPS; i++) {
+ final String q = sparql;
+ Future future = executor.submit(() -> driver.query(q));
+ try {
+ times[i] = future.get(QUERY_TIMEOUT_S, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ future.cancel(true);
+ log.warn(" {} rep {} timed out after {}s", queryId, i + 1, QUERY_TIMEOUT_S);
+ times[i] = -1L;
+ } catch (ExecutionException e) {
+ log.warn(" {} rep {} failed: {}", queryId, i + 1, e.getCause().getMessage());
+ times[i] = -1L;
+ }
+ }
+ long medianMs = MetricsCollector.median(times);
+ results.add(new QueryResult(driver.getName(), datasetName, queryId, medianMs, times));
+ log.info(" {} | {} ms", queryId, medianMs >= 0 ? medianMs : "TIMEOUT");
+ }
+ }
+ } finally {
+ driver.close();
+ }
+ return results;
+ }
+
+ /**
+ * Benchmarks a single N-Triples file: one warmup followed by
+ * {@value #REPETITIONS} measured load cycles. Memory is sampled before and
+ * after each load (with forced GC), and CPU usage is sampled immediately after.
+ * Median values are computed across the repetitions and returned as a
+ * {@link FileResult}.
+ *
+ * @param file the N-Triples file to benchmark
+ * @return the aggregated {@link FileResult} for this file
+ * @throws Exception if the driver raises an error during load or measurement
+ */
+ private FileResult benchmarkFile(File file) throws Exception {
+ String path = file.getAbsolutePath();
+
+ // --- Warmup (discarded) ---
+ driver.clear();
+ System.gc();
+ driver.load(path);
+ driver.clear();
+ System.gc();
+
+ // --- 3 repetitions ---
+ long[] timesMs = new long[REPETITIONS];
+ double[] memBefore = new double[REPETITIONS];
+ double[] memAfter = new double[REPETITIONS];
+ double[] cpuSamples = new double[REPETITIONS];
+ long triples = 0;
+
+ for (int i = 0; i < REPETITIONS; i++) {
+ driver.clear();
+ memBefore[i] = metrics.getUsedMemoryMb();
+
+ long start = System.nanoTime();
+ driver.load(path);
+ timesMs[i] = (System.nanoTime() - start) / 1_000_000L;
+
+ cpuSamples[i] = metrics.getCpuPercent();
+ memAfter[i] = metrics.getUsedMemoryMb();
+ triples = driver.getGraphSize();
+ }
+
+ // --- Compute medians ---
+ long medianTimeMs = MetricsCollector.median(timesMs);
+
+ double[] deltas = new double[REPETITIONS];
+ for (int i = 0; i < REPETITIONS; i++) deltas[i] = memAfter[i] - memBefore[i];
+
+ return new FileResult(
+ driver.getName(),
+ datasetName,
+ file.getName(),
+ triples,
+ medianTimeMs,
+ MetricsCollector.throughput(triples, medianTimeMs),
+ MetricsCollector.medianDouble(memBefore),
+ MetricsCollector.medianDouble(memAfter),
+ MetricsCollector.medianDouble(deltas),
+ MetricsCollector.medianDouble(cpuSamples),
+ timesMs
+ );
+ }
+}
diff --git a/src/main/java/fr/inria/corese/benchmark/metrics/FileResult.java b/src/main/java/fr/inria/corese/benchmark/metrics/FileResult.java
new file mode 100644
index 0000000..f029210
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/metrics/FileResult.java
@@ -0,0 +1,39 @@
+package fr.inria.corese.benchmark.metrics;
+
+/**
+ * Benchmark result for a single file loaded by a single engine.
+ * All timing and memory values are the median of 3 repetitions.
+ *
+ * @param engine driver name (e.g. {@code "corese.4.6.5"})
+ * @param dataset logical dataset name (e.g. {@code "dbpedia"})
+ * @param file name of the {@code .nt} file (without path)
+ * @param triples number of triples loaded (from the last repetition)
+ * @param loadingTimeMs median load time across repetitions, in milliseconds
+ * @param throughputTriplesPerSec median triples loaded per second
+ * @param memoryBeforeMb median JVM heap usage before loading (after forced GC), in MB
+ * @param memoryAfterMb median JVM heap usage after loading, in MB
+ * @param memoryDeltaMb median heap increase during loading ({@code after - before}), in MB
+ * @param cpuPercent median JVM CPU usage during loading (0–100)
+ * @param repetitionsMs raw per-repetition load times in milliseconds (length = 3)
+ */
+public record FileResult(
+ String engine,
+ String dataset,
+ String file,
+ long triples,
+
+ // Loading metrics (median of repetitions)
+ long loadingTimeMs,
+ double throughputTriplesPerSec,
+
+ // Memory metrics (median delta: after - before, with forced GC)
+ double memoryBeforeMb,
+ double memoryAfterMb,
+ double memoryDeltaMb,
+
+ // CPU usage during loading
+ double cpuPercent,
+
+ // Raw repetition values (for transparency)
+ long[] repetitionsMs
+) {}
diff --git a/src/main/java/fr/inria/corese/benchmark/metrics/MetricsCollector.java b/src/main/java/fr/inria/corese/benchmark/metrics/MetricsCollector.java
new file mode 100644
index 0000000..83bc64f
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/metrics/MetricsCollector.java
@@ -0,0 +1,62 @@
+package fr.inria.corese.benchmark.metrics;
+
+import com.sun.management.OperatingSystemMXBean;
+import java.lang.management.ManagementFactory;
+
+/**
+ * Collects JVM memory and CPU metrics during benchmark execution.
+ */
+public class MetricsCollector {
+
+ private final OperatingSystemMXBean osBean;
+
+ public MetricsCollector() {
+ this.osBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
+ }
+
+ /** Force garbage collection and return used heap memory in MB. */
+ public double getUsedMemoryMb() {
+ System.gc();
+ System.gc();
+ Runtime rt = Runtime.getRuntime();
+ long used = rt.totalMemory() - rt.freeMemory();
+ return used / (1024.0 * 1024.0);
+ }
+
+ /** Return current JVM CPU usage as a percentage (0-100). */
+ public double getCpuPercent() {
+ double cpu = osBean.getProcessCpuLoad();
+ return cpu < 0 ? 0.0 : cpu * 100.0;
+ }
+
+ /**
+ * Compute median of an array of long values, ignoring sentinel -1 values
+ * (used to mark timeouts or failed repetitions).
+ * Returns -1 if all values are -1.
+ */
+ public static long median(long[] values) {
+ long[] valid = java.util.Arrays.stream(values).filter(v -> v >= 0).toArray();
+ if (valid.length == 0) return -1L;
+ java.util.Arrays.sort(valid);
+ int mid = valid.length / 2;
+ return valid.length % 2 == 0
+ ? (valid[mid - 1] + valid[mid]) / 2
+ : valid[mid];
+ }
+
+ /** Compute median of an array of double values. */
+ public static double medianDouble(double[] values) {
+ double[] sorted = values.clone();
+ java.util.Arrays.sort(sorted);
+ int mid = sorted.length / 2;
+ return sorted.length % 2 == 0
+ ? (sorted[mid - 1] + sorted[mid]) / 2.0
+ : sorted[mid];
+ }
+
+ /** Compute throughput in triples/second. */
+ public static double throughput(long triples, long timeMs) {
+ if (timeMs <= 0) return 0;
+ return triples / (timeMs / 1000.0);
+ }
+}
diff --git a/src/main/java/fr/inria/corese/benchmark/metrics/QueryResult.java b/src/main/java/fr/inria/corese/benchmark/metrics/QueryResult.java
new file mode 100644
index 0000000..a1643d1
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/metrics/QueryResult.java
@@ -0,0 +1,21 @@
+package fr.inria.corese.benchmark.metrics;
+
+/**
+ * Benchmark result for a single SPARQL query executed by a single engine.
+ * {@code medianMs} is the median of {@code QUERY_REPS} repetitions;
+ * a value of {@code -1} indicates that the query failed or timed out.
+ *
+ * @param engine driver name (e.g. {@code "corese.4.6.5"})
+ * @param dataset logical dataset name (e.g. {@code "dbpedia"})
+ * @param queryId identifier of the query (e.g. {@code "query_1"})
+ * @param medianMs median execution time in milliseconds, or {@code -1} on failure
+ * @param repetitionsMs raw per-repetition execution times in milliseconds; {@code -1} entries
+ * indicate failed repetitions
+ */
+public record QueryResult(
+ String engine,
+ String dataset,
+ String queryId,
+ long medianMs,
+ long[] repetitionsMs
+) {}
diff --git a/src/main/java/fr/inria/corese/benchmark/report/BenchmarkReport.java b/src/main/java/fr/inria/corese/benchmark/report/BenchmarkReport.java
new file mode 100644
index 0000000..f61b658
--- /dev/null
+++ b/src/main/java/fr/inria/corese/benchmark/report/BenchmarkReport.java
@@ -0,0 +1,131 @@
+package fr.inria.corese.benchmark.report;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import fr.inria.corese.benchmark.metrics.FileResult;
+import fr.inria.corese.benchmark.metrics.QueryResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * Generates benchmark-report.json consumed by the dashboard.
+ */
+public class BenchmarkReport {
+
+ private static final Logger log = LoggerFactory.getLogger(BenchmarkReport.class);
+
+ /**
+ * Writes benchmark results to a JSON file and a sibling CSV file.
+ *
+ * The JSON file is written to {@code outputPath}. The CSV file is placed in
+ * a {@code csv/} directory that is a sibling of the JSON file's parent directory,
+ * using the same filename with a {@code .csv} extension.
+ *
+ * The JSON structure contains a {@code metadata} block (timestamp, protocol,
+ * Java version, OS), a {@code runs} array of {@link FileResult} objects, and an
+ * optional {@code queryRuns} array of {@link QueryResult} objects.
+ *
+ * @param results load-benchmark results (may be empty but not {@code null})
+ * @param queryResults query-benchmark results; {@code null} or empty means the
+ * {@code queryRuns} section is omitted from the report
+ * @param outputPath path of the JSON output file; parent directories must exist
+ * @throws Exception if serialisation or file I/O fails
+ */
+ public static void write(List results, List queryResults, List queries, String outputPath) throws Exception {
+ Map report = new LinkedHashMap<>();
+
+ // Metadata
+ Map metadata = new LinkedHashMap<>();
+ metadata.put("generatedAt", Instant.now().toString());
+ Map protocol = new LinkedHashMap<>();
+ protocol.put("warmup", true);
+ protocol.put("repetitions", 3);
+ protocol.put("queryRepetitions", 3);
+ metadata.put("protocol", protocol);
+ metadata.put("java", System.getProperty("java.version"));
+ metadata.put("os", System.getProperty("os.name"));
+ // Embed SPARQL queries so the dashboard can display them without external files
+ if (queries != null && !queries.isEmpty()) {
+ metadata.put("queries", queries);
+ }
+ report.put("metadata", metadata);
+
+ // Load benchmark runs
+ report.put("runs", results);
+
+ // Query benchmark runs (optional)
+ if (queryResults != null && !queryResults.isEmpty()) {
+ report.put("queryRuns", queryResults);
+ }
+
+ Path jsonPath = Path.of(outputPath);
+
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.enable(SerializationFeature.INDENT_OUTPUT);
+ // Records have private fields with no JavaBean getters — expose all fields
+ mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ mapper.writeValue(jsonPath.toFile(), report);
+ log.info("Report written to: {}", outputPath);
+
+ // CSV — sibling csv/ directory alongside json/
+ Path csvDir = jsonPath.getParent().getParent().resolve("csv");
+ Files.createDirectories(csvDir);
+ Path csvPath = csvDir.resolve(jsonPath.getFileName().toString().replace(".json", ".csv"));
+ writeCsv(results, queryResults, csvPath.toString());
+ log.info("CSV written to: {}", csvPath);
+ }
+
+ /**
+ * Writes results to a CSV file with two labelled sections: {@code # LOAD BENCHMARK}
+ * and (optionally) {@code # QUERY BENCHMARK}.
+ *
+ * @param results load-benchmark results
+ * @param queryResults query-benchmark results; {@code null} or empty skips the query section
+ * @param csvPath absolute path of the CSV file to create or overwrite
+ * @throws Exception if file I/O fails
+ */
+ private static void writeCsv(List results, List queryResults, String csvPath) throws Exception {
+ try (PrintWriter pw = new PrintWriter(csvPath)) {
+ // Load benchmark section
+ pw.println("# LOAD BENCHMARK");
+ pw.println("engine,dataset,file,triples,loadingTimeMs,throughputTriplesPerSec,memoryBeforeMb,memoryAfterMb,memoryDeltaMb,cpuPercent,rep1Ms,rep2Ms,rep3Ms");
+ for (FileResult r : results) {
+ String reps = Arrays.stream(r.repetitionsMs())
+ .mapToObj(String::valueOf)
+ .collect(Collectors.joining(","));
+ pw.printf(Locale.US, "%s,%s,%s,%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%s%n",
+ r.engine(), r.dataset(), r.file(), r.triples(),
+ r.loadingTimeMs(), r.throughputTriplesPerSec(),
+ r.memoryBeforeMb(), r.memoryAfterMb(), r.memoryDeltaMb(),
+ r.cpuPercent(), reps);
+ }
+
+ // Query benchmark section (optional)
+ if (queryResults != null && !queryResults.isEmpty()) {
+ pw.println();
+ pw.println("# QUERY BENCHMARK");
+ pw.println("engine,dataset,queryId,medianMs,rep1Ms,rep2Ms,rep3Ms");
+ for (QueryResult r : queryResults) {
+ String reps = Arrays.stream(r.repetitionsMs())
+ .mapToObj(String::valueOf)
+ .collect(Collectors.joining(","));
+ pw.printf(Locale.US, "%s,%s,%s,%d,%s%n",
+ r.engine(), r.dataset(), r.queryId(), r.medianMs(), reps);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 4eceec9..4fce1a2 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -5,11 +5,12 @@
-
+
+
+
+
-
-
diff --git a/src/main/resources/lubm/lubm-queries.txt b/src/main/resources/lubm/lubm-queries.txt
new file mode 100644
index 0000000..5b610e0
--- /dev/null
+++ b/src/main/resources/lubm/lubm-queries.txt
@@ -0,0 +1,42 @@
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X WHERE { ?X rdf:type ub:GraduateStudent . ?X ub:takesCourse }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X ?Y ?Z WHERE { ?X rdf:type ub:GraduateStudent . ?Y rdf:type ub:University . ?Z rdf:type ub:Department . ?X ub:memberOf ?Z . ?Z ub:subOrganizationOf ?Y . ?X ub:undergraduateDegreeFrom ?Y }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X WHERE { ?X rdf:type ub:Publication . ?X ub:publicationAuthor }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X ?Y1 ?Y2 ?Y3 WHERE { ?X rdf:type ub:Professor . ?X ub:worksFor . ?X ub:name ?Y1 . ?X ub:emailAddress ?Y2 . ?X ub:telephone ?Y3 }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X WHERE { ?X rdf:type ub:Person . ?X ub:memberOf }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X WHERE { ?X rdf:type ub:Student . ?X ub:takesCourse }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X ?Y WHERE { ?X rdf:type ub:Student . ?Y rdf:type ub:Department . ?X ub:memberOf ?Y . ?Y ub:subOrganizationOf . ?X ub:emailAddress ?Z }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X ?Y ?Z WHERE { ?X rdf:type ub:Student . ?Y rdf:type ub:Department . ?Z rdf:type ub:Publication . ?X ub:memberOf ?Y . ?Y ub:subOrganizationOf . ?Z ub:publicationAuthor ?X }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X ?Y ?Z WHERE { ?X rdf:type ub:Student . ?Y rdf:type ub:Faculty . ?Z rdf:type ub:Course . ?X ub:advisor ?Y . ?X ub:takesCourse ?Z . ?Y ub:teacherOf ?Z }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X WHERE { ?X rdf:type ub:Student . ?X ub:takesCourse . ?X ub:takesCourse }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X WHERE { ?X rdf:type ub:ResearchGroup . ?X ub:subOrganizationOf }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X ?Y WHERE { ?X rdf:type ub:Chair . ?Y rdf:type ub:Department . ?X ub:worksFor ?Y . ?Y ub:subOrganizationOf }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X WHERE { ?X rdf:type ub:Person . ?X ub:hasAlumnus }
+PREFIX rdf:
+PREFIX ub:
+SELECT ?X WHERE { ?X rdf:type ub:Student . ?X ub:takesCourse }