diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b04e80a..2f238b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,24 @@ jobs: find out/ -name "*.json" -mtime +30 -delete 2>/dev/null || true rm -rf build/ input/ + - name: Build corese.next JAR + env: + CORESE_COMMIT: 1215ab78adbfad7f0edc20e212446780a23182c1 + run: | + CORESE_DIR="$HOME/corese-next-build" + if [ ! -d "$CORESE_DIR/.git" ]; then + git clone https://github.com/corese-stack/corese-core.git "$CORESE_DIR" + fi + cd "$CORESE_DIR" + git fetch origin feature/corese-next + git checkout "$CORESE_COMMIT" + chmod +x gradlew + ./gradlew shadowJar --no-daemon -q + JAR=$(find build/libs -name "*-jar-with-dependencies.jar" | head -1) + mkdir -p "$GITHUB_WORKSPACE/libs" + cp "$JAR" "$GITHUB_WORKSPACE/libs/corese-core-local.jar" + echo "corese.next JAR built from $CORESE_COMMIT → libs/corese-core-local.jar" + - name: Set up conda environment run: | CONDA_SH=$(find $HOME -name "conda.sh" -path "*/profile.d/conda.sh" -not -path "*/pkgs/*" 2>/dev/null | head -1) @@ -95,8 +113,8 @@ jobs: source "$CONDA_SH" conda activate benchmarkenv python python-utils/workflow.py \ - --engines="corese.4.6.5,jena.6.2.0,rdf4j.5.3.2" \ - --datasets="dbpedia,bowlogna" + --engines="corese.next,corese.4.6.5,jena.6.2.0,rdf4j.5.3.2" \ + --datasets="dbpedia,bowlogna,lubm" - name: Check site/data output run: ls -la ./site/data/ @@ -128,7 +146,7 @@ jobs: - name: Build corese.next JAR if: contains(github.event.inputs.engines, 'corese.next') env: - CORESE_COMMIT: b8dbc158 + CORESE_COMMIT: 1215ab78adbfad7f0edc20e212446780a23182c1 run: | CORESE_DIR="$HOME/corese-next-build" if [ ! -d "$CORESE_DIR/.git" ]; then diff --git a/Makefile b/Makefile index a72fd47..eb8a052 100644 --- a/Makefile +++ b/Makefile @@ -9,17 +9,24 @@ DATASET ?= dbpedia DATASETS ?= dbpedia,bowlogna,bsbm,lubm SCALES ?= small -.PHONY: help env regression compare next bowlogna lubm bsbm all site clean-out +# Predefined version sets +ALL_VERSIONS ?= corese.next,corese.4.6.5,jena.6.2.0,jena.6.1.0,rdf4j.5.3.2,rdf4j.5.3.1 + +.PHONY: help env regression compare next bowlogna lubm bsbm all versions versions-resume corese-all quick site clean-out help: @echo "" @echo " make regression Detect a Corese regression (local vs 4.6.5)" @echo " make compare Compare corese + jena + rdf4j on dbpedia" @echo " make next Benchmark corese.next vs corese.4.6.5" + @echo " make versions All versions: corese(next+3) + jena(3) + rdf4j(3)" + @echo " make versions-resume Idem, reprend là où c'était arrêté" + @echo " make corese-all corese.next + corese.4.6.5 sur tous les datasets" @echo " make bowlogna Bowlogna dataset with SPARQL queries (corese.local vs 4.6.5)" @echo " make lubm LUBM dataset with SPARQL queries" @echo " make bsbm BSBM dataset" @echo " make all All datasets (dbpedia + bowlogna + bsbm + lubm) in one run" + @echo " make quick Quick run (1 rep instead of 3) — faster, less accurate" @echo " make site Serve the dashboard at http://localhost:8080" @echo " make env Create conda environment" @echo " make clean-out Delete all generated reports in out/" @@ -41,7 +48,19 @@ compare: $(PYTHON) $(WORKFLOW) --engines $(ENGINES) --dataset $(DATASET) next: - $(PYTHON) $(WORKFLOW) --engines corese.next,corese.4.6.5,jena.6.2.0,rdf4j.5.3.2 --datasets dbpedia,bowlogna --scales $(SCALES) + $(PYTHON) $(WORKFLOW) --engines corese.next,corese.4.6.5,jena.6.2.0,rdf4j.5.3.2 --datasets dbpedia,bowlogna,bsbm,lubm --scales $(SCALES) + +versions: + $(PYTHON) $(WORKFLOW) --engines $(ALL_VERSIONS) --datasets $(DATASETS) --scales $(SCALES) + +versions-resume: + $(PYTHON) $(WORKFLOW) --engines $(ALL_VERSIONS) --datasets $(DATASETS) --scales $(SCALES) --resume + +corese-all: + $(PYTHON) $(WORKFLOW) --engines corese.next,corese.4.6.5 --datasets dbpedia,bowlogna,bsbm,lubm --scales $(SCALES) + +quick: + $(PYTHON) $(WORKFLOW) --engines $(ENGINES) --datasets $(DATASETS) --scales $(SCALES) --quick bowlogna: $(PYTHON) $(WORKFLOW) --engines corese.local,corese.4.6.5 --dataset bowlogna --scales $(SCALES) diff --git a/README.md b/README.md index ec0fe89..7a81d8c 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ This repository benchmarks Java-based RDF triple stores on loading and SPARQL qu **Principles:** - Java 21 harness with a rigorous measurement protocol (warmup + 3 repetitions + median) - Memory measured as delta before/after loading with forced GC +- SPARQL query benchmark with per-query timeout and warmup - Automatic JSON + CSV output -- Run history for trend tracking - Python orchestration for dataset download, build, and deployment --- @@ -26,7 +26,19 @@ This repository benchmarks Java-based RDF triple stores on loading and SPARQL qu The interactive benchmark dashboard is published at: **[https://corese-stack.github.io/corese-benchmark/](https://corese-stack.github.io/corese-benchmark/)** -To view it locally, open `site/index.html` in a browser. +To view it locally: + +```bash +make site # serves at http://localhost:8080 +``` + +**Dashboard features:** +- **Scalability charts** — when a single dataset is selected, Performance charts switch to line charts (X = graph size in triples) showing how each engine scales +- **W3C Conformance** — live pass/fail results fetched from [corese-stack.github.io/corese-w3c](https://corese-stack.github.io/corese-w3c/data/w3c-report.json) (All mode only) +- **Shareable URLs** — state (dataset, engine filter, reference engine) is encoded in the URL (`?ds=dbpedia&engines=corese.4.6.5,jena.6.2.0&ref=corese.4.6.5`) +- **Export CSV** — exports the currently filtered results directly from the browser (Full results section) +- **Engine filter** — click engine cards to filter charts; click × on pills to remove from comparison +- **SPARQL section** — per-engine latency cards, median per query, color-coded comparison table --- @@ -120,7 +132,7 @@ python python-utils/workflow.py --coreseCommits a17f3d6,b089a03 ### E) Benchmark with SPARQL queries -For datasets that have associated queries (e.g. Bowlogna): +For datasets that have associated queries (Bowlogna, BSBM): ```bash python python-utils/workflow.py --engines corese.local,corese.4.6.5 --dataset bowlogna @@ -130,6 +142,39 @@ The **SPARQL Queries** section appears in the dashboard with: - Per-engine latency cards: avg, p50, p95, p99 - Median time per query and per engine - Color-coded comparison table (green = best, orange = ×5–19, red = ×20+) +- `—` indicates timeout or error + +--- + +### F) Resume mode + +Resume mode runs one engine × one dataset at a time and skips combinations whose output already contains a valid JSON report. Useful for long runs that may be interrupted: + +```bash +python python-utils/workflow.py \ + --engines corese.local,corese.4.6.5,jena.6.2.0,rdf4j.5.3.2 \ + --datasets dbpedia,bowlogna,bsbm,lubm \ + --resume +``` + +Partial or empty JSON files are automatically re-run (not skipped). + +--- + +### Workflow options + +| Option | Default | Description | +|--------|---------|-------------| +| `--engines` | — | Comma-separated engine list (e.g. `corese.4.6.5,jena.6.2.0`) | +| `--dataset` | `dbpedia` | Single dataset: `dbpedia`, `bowlogna`, `bsbm`, `lubm` | +| `--datasets` | — | Multiple datasets in one run (e.g. `dbpedia,bowlogna`) | +| `--scales` | `small,medium,large` | Scale tiers for synthetic datasets | +| `--resume` | off | Skip already-completed engine × dataset pairs | +| `--quick` | off | 1 repetition instead of 3 (faster, less accurate) | +| `--query-timeout` | `30` | Per-query SPARQL timeout in seconds | +| `--inference` | off | Enable RDFS inference for Corese engines | +| `--coreseLocalPath` | auto | Path to corese-core project or JAR | +| `--dataDir` | `./input` | Custom data directory | --- @@ -144,6 +189,9 @@ The **SPARQL Queries** section appears in the dashboard with: # Run (with queries) ./gradlew runBenchmark --args="input/dbpedia/unzip out/json/report.json corese.4.6.5 dbpedia queries.txt" + +# Run with custom query timeout (60s) +./gradlew runBenchmark --args="input/dbpedia/unzip out/json/report.json corese.4.6.5 dbpedia queries.txt --query-timeout 60" ``` --- @@ -158,15 +206,13 @@ Each run automatically generates: | `out/csv/_.csv` | Same data in CSV format (Excel, etc.) | | `site/data/benchmark-report.json` | Copy of the latest report (current dashboard) | | `site/data/benchmark-report.csv` | Copy of the latest CSV (download button) | -| `site/data/history/.json` | History entry (last 30 runs kept) | -| `site/data/history-data.js` | Inline history (trend chart) | - -The **historical trend chart** appears in the dashboard once 2 or more runs are available. --- ## Measurement Protocol +### Load benchmark + For each file and each engine: 1. **Warmup** — load → clear → GC (result discarded) @@ -177,6 +223,15 @@ For each file and each engine: - Record time and memory delta 3. **Keep the median** of the 3 repetitions +### SPARQL benchmark + +For each query and each engine: + +1. **Warmup** — one execution discarded (primes JIT); logs result count with `WARN` if 0 results +2. **3 repetitions** with per-execution timeout (default 30s, configurable via `--query-timeout`) +3. Each rep runs in a **daemon thread** via `ExecutorService` — a stuck query is abandoned without blocking subsequent queries +4. **Keep the median** of the 3 repetitions (`—` if all reps timed out) + This protocol ensures reproducible results by eliminating JIT compilation bias and GC timing noise. **Note on `clear()`:** RDF4J performs a full repository reinitialisation between repetitions (rather than just `conn.clear()`) to release internal SAIL memory structures and avoid negative memory delta artefacts caused by delayed GC. @@ -214,6 +269,8 @@ python python-utils/workflow.py --dataset dbpedia --engines corese.4.6.5,jena.6. Synthetic dataset describing student/university relations. Includes associated **SPARQL queries** for latency benchmarking. Source: [DICE Research / HOBBIT](https://files.dice-research.org/projects/HOBBIT/benchmarks-data/datasets-dumps/bowlogna-dump.zip) +Dashboard note: Bowlogna shows only the **SPARQL** section (no Performance/Load charts — the dataset is optimised for query benchmarking). + | Scale | Departments | Triples (approx.) | |-------|-------------|-------------------| | `small` | 1 | ~1.2 M | @@ -224,16 +281,6 @@ Synthetic dataset describing student/university relations. Includes associated * python python-utils/workflow.py --dataset bowlogna --scales small --engines corese.4.6.5,jena.6.2.0,rdf4j.5.3.2 ``` -### BSBM - -Berlin SPARQL Benchmark — synthetic e-commerce dataset. Generated with [bsbmtools 0.2](https://sourceforge.net/projects/bsbmtools/). - -| Scale | Products | Triples (approx.) | -|-------|---------|-------------------| -| `small` | 1,000 | ~50 K | -| `medium` | 50,000 | ~1 M | -| `large` | 500,000 | ~10 M | - ```bash python python-utils/workflow.py --dataset bsbm --scales small,medium --engines corese.4.6.5,jena.6.2.0,rdf4j.5.3.2 ``` diff --git a/build.gradle b/build.gradle index c488acb..8f3fc98 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,9 @@ def useLocalCorese = (project.hasProperty('coreseVersion') && project.coreseVers if (useLocalCorese && (!localJars.isEmpty() || project.hasProperty('coreseJar'))) { // corese-next build: exclude 4.x driver (incompatible RuleEngine API) sourceSets.main.java.exclude '**/CoreseDriver.java' + // Also exclude tests that directly instantiate CoreseDriver (won't compile without it) + sourceSets.test.java.exclude '**/CrossEngineConsistencyTest.java' + sourceSets.test.java.exclude '**/DriverFormatTest.java' if (project.hasProperty('coreseJar')) { dependencies { compileOnly files(project.coreseJar) } } else { @@ -60,12 +63,38 @@ dependencies { implementation "org.eclipse.rdf4j:rdf4j-sail-memory:${rdf4jVersion}" implementation "org.eclipse.rdf4j:rdf4j-repository-sail:${rdf4jVersion}" implementation "org.eclipse.rdf4j:rdf4j-rio:${rdf4jVersion}" + // RIO parsers — required for multi-format support (.nq, .ttl, .trig) + implementation "org.eclipse.rdf4j:rdf4j-rio-ntriples:${rdf4jVersion}" + implementation "org.eclipse.rdf4j:rdf4j-rio-nquads:${rdf4jVersion}" + implementation "org.eclipse.rdf4j:rdf4j-rio-turtle:${rdf4jVersion}" + implementation "org.eclipse.rdf4j:rdf4j-rio-trig:${rdf4jVersion}" // JSON output implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.0' // Logging implementation 'ch.qos.logback:logback-classic:1.4.14' + + // Tests + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'fr.inria.corese:corese-core:4.6.5' + def jenaVersionTest = project.hasProperty('jenaVersion') ? project.jenaVersion : '6.2.0' + testImplementation "org.apache.jena:jena-core:${jenaVersionTest}" + testImplementation "org.apache.jena:jena-arq:${jenaVersionTest}" + def rdf4jVersionTest = project.hasProperty('rdf4jVersion') ? project.rdf4jVersion : '5.3.2' + testImplementation "org.eclipse.rdf4j:rdf4j-repository:${rdf4jVersionTest}" + testImplementation "org.eclipse.rdf4j:rdf4j-sail-memory:${rdf4jVersionTest}" + testImplementation "org.eclipse.rdf4j:rdf4j-repository-sail:${rdf4jVersionTest}" + testImplementation "org.eclipse.rdf4j:rdf4j-rio:${rdf4jVersionTest}" + testImplementation "org.eclipse.rdf4j:rdf4j-rio-ntriples:${rdf4jVersionTest}" + testImplementation "org.eclipse.rdf4j:rdf4j-rio-nquads:${rdf4jVersionTest}" + testImplementation "org.eclipse.rdf4j:rdf4j-rio-turtle:${rdf4jVersionTest}" + testImplementation "org.eclipse.rdf4j:rdf4j-rio-trig:${rdf4jVersionTest}" +} + +test { + useJUnitPlatform() } application { diff --git a/python-utils/workflow.py b/python-utils/workflow.py index d2c2b2c..122e454 100644 --- a/python-utils/workflow.py +++ b/python-utils/workflow.py @@ -79,6 +79,25 @@ default=False, help="Enable RDFS inference for Corese engines (passed to Java harness).", ) +parser.add_argument( + "--resume", + action="store_true", + default=False, + help="Resume mode: run one engine×dataset at a time, skip if output already exists.", +) +parser.add_argument( + "--quick", + action="store_true", + default=False, + help="Quick mode: 1 repetition instead of 3 (faster, less accurate).", +) +parser.add_argument( + "--query-timeout", + type=int, + default=None, + metavar="SECONDS", + help="Per-query SPARQL timeout in seconds (default: 30). Overrides the Java harness default.", +) args = parser.parse_args() current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -199,7 +218,16 @@ def prepare_dataset(dataset_name): def gradle_build(extra_args=None): cmd = [gradle_wrapper, "clean", "build"] + (extra_args or []) print(f"Building: {' '.join(cmd)}") - subprocess.run(cmd, cwd=project_dir, check=True) + try: + subprocess.run(cmd, cwd=project_dir, check=True, timeout=600, stderr=subprocess.PIPE) + except subprocess.TimeoutExpired: + print("[ERROR] Gradle build timed out after 600s — check for stuck daemon", file=sys.stderr) + raise + except subprocess.CalledProcessError as e: + stderr_text = e.stderr.decode("utf-8", errors="replace").strip() if e.stderr else "" + if stderr_text: + print(f"[ERROR] Gradle build stderr:\n{stderr_text}", file=sys.stderr) + raise QUERIES_FILES = { @@ -219,9 +247,21 @@ def run_benchmark(engines_str, output_file, dataset_label, unzip_dir, gradle_pro if args.inference: args_str += " --inference" print(" Inference: enabled") + if args.quick: + args_str += " --quick" + print(" Quick mode: 1 repetition") + if args.query_timeout is not None: + args_str += f" --query-timeout {args.query_timeout}" + print(f" Query timeout: {args.query_timeout}s") cmd = [gradle_wrapper, "runBenchmark"] + (gradle_props or []) + [f"--args={args_str}"] print(f"Running benchmark: {' '.join(cmd)}") - subprocess.run(cmd, cwd=project_dir, check=True) + try: + subprocess.run(cmd, cwd=project_dir, check=True, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as e: + stderr_text = e.stderr.decode("utf-8", errors="replace").strip() if e.stderr else "" + if stderr_text: + print(f"[ERROR] Gradle stderr:\n{stderr_text}", file=sys.stderr) + raise def merge_json_files(file_list, output_file): @@ -262,6 +302,83 @@ def merge_json_files(file_list, output_file): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") +# ── Log to file (mirrors stdout+stderr) ────────────────────────────────────── +_log_dir = os.path.join(out_json_dir, "..", "logs") +os.makedirs(_log_dir, exist_ok=True) +_log_path = os.path.join(_log_dir, f"{timestamp}.log") + +class _Tee: + def __init__(self, *streams): + self._streams = streams + def write(self, data): + for s in self._streams: + s.write(data) + def flush(self): + for s in self._streams: + s.flush() + def fileno(self): + return self._streams[0].fileno() + +_log_fh = open(_log_path, "w", buffering=1, encoding="utf-8") +sys.stdout = _Tee(sys.__stdout__, _log_fh) +sys.stderr = _Tee(sys.__stderr__, _log_fh) +print(f"Logging to: {_log_path}") + +# ── Resume mode: one engine × one dataset at a time, skips existing results ── +if args.engines and args.resume: + engines_list = [e.strip() for e in args.engines.split(",")] + + def needs_local_jar_r(e): + return e in ('corese.next', 'corese.local') or e.startswith('corese.commit.') + + def build_gradle_props_r(engine): + parts = engine.split(".", 1) + family, ver = (parts[0], parts[1]) if len(parts) == 2 else (engine, None) + extra = [] + if family == 'corese': + extra += [f"-PcoreseVersion={'local' if ver in ('next','local') else ver}"] + elif family == 'jena' and ver: + extra += [f"-PjenaVersion={ver}"] + elif family == 'rdf4j' and ver: + extra += [f"-Prdf4jVersion={ver}"] + return extra + + resume_dir = os.path.join(out_json_dir, "resume") + os.makedirs(resume_dir, exist_ok=True) + collected = [] + last_build_props = None + + for engine in engines_list: + gradle_props = build_gradle_props_r(engine) + for ds in dataset_names: + safe_engine = engine.replace('.', '_') + out_file = os.path.join(resume_dir, f"{safe_engine}_{ds}.json") + if os.path.exists(out_file): + try: + with open(out_file) as _f: + _data = _json_early.load(_f) + if isinstance(_data.get("runs"), list) and len(_data["runs"]) > 0: + print(f"[resume] SKIP {engine} × {ds} — already done ({out_file})") + collected.append(out_file) + continue + else: + print(f"[resume] REDO {engine} × {ds} — output exists but has no valid runs") + except Exception as _e: + print(f"[resume] REDO {engine} × {ds} — output invalid ({_e})") + print(f"[resume] RUN {engine} × {ds}") + if gradle_props != last_build_props: + gradle_build(gradle_props) + last_build_props = gradle_props + run_benchmark(engine, out_file, ds, dataset_dirs[ds], gradle_props=gradle_props) + collected.append(out_file) + + if collected: + engines_tag = args.engines.replace(',', '_') + final_output = os.path.join(out_json_dir, f"{timestamp}_resume_{engines_tag}.json") + merge_json_files(list(collected), final_output) + print(f"\n[resume] Rapport final : {final_output}") + import sys; sys.exit(0) + # Case A: --engines (single build, all engines) if args.engines: engines_list = [e.strip() for e in args.engines.split(",")] @@ -411,34 +528,5 @@ def build_gradle_props(engine_sublist): f.write(f"window.__BENCHMARK_DATA__ = {data};") print("Generated report-data.js") - history_dir = os.path.join(site_data_dir, "history") - os.makedirs(history_dir, exist_ok=True) - hist_filename = f"{timestamp}.json" - shutil.copy(latest, os.path.join(history_dir, hist_filename)) - - index_path = os.path.join(history_dir, "index.json") - if os.path.exists(index_path): - with open(index_path) as f: - history_index = _json_early.load(f) - else: - history_index = [] - - history_index.append({"file": hist_filename, "date": timestamp}) - history_index = history_index[-30:] - - with open(index_path, "w") as f: - _json_early.dump(history_index, f, indent=2) - print(f"History updated: {len(history_index)} entries in {index_path}") - - history_data = [] - for entry in history_index: - hist_file = os.path.join(history_dir, entry["file"]) - if os.path.exists(hist_file): - with open(hist_file) as hf: - history_data.append({"date": entry["date"], "data": _json_early.load(hf)}) - hist_js = os.path.join(site_data_dir, "history-data.js") - with open(hist_js, "w") as f: - f.write(f"window.__HISTORY_DATA__ = {_json_early.dumps(history_data)};") - print(f"Generated history-data.js ({len(history_data)} entries)") else: print("No JSON report found in out/") diff --git a/site/app.js b/site/app.js index d856416..e0ea912 100644 --- a/site/app.js +++ b/site/app.js @@ -38,6 +38,13 @@ const AUTO_COLORS = [ { hex: '#FF5722', bg: 'rgba(255,87,34,0.75)' }, ]; +// Known engine versions (order preserved in pills, filtered to what's in data) +const KNOWN_ENGINE_ORDER = [ + 'corese.next', 'corese.local', 'corese.4.6.5', 'corese.4.6.4', 'corese.4.6.3', + 'jena.6.2.0', 'jena.6.1.0', 'jena.6.0.0', + 'rdf4j.5.3.2', 'rdf4j.5.3.1', 'rdf4j.5.3.0', +]; + // Dynamic resolution: builds engine → color map from data let ENGINE_COLOR_MAP = {}; @@ -79,7 +86,7 @@ async function loadData() { return await res.json(); } catch (_) { if (typeof window.__BENCHMARK_DATA__ !== 'undefined') return window.__BENCHMARK_DATA__; - throw new Error('Aucune donnée. Lancez le benchmark d\'abord.'); + throw new Error('No data. Run the benchmark first.'); } } @@ -130,21 +137,29 @@ const DATASET_META = { label: 'DBpedia', description: 'Real-world RDF dump — 5 N-Triples files from DBpedia 3.5.1', url: 'https://downloads.dbpedia.org/3.5.1/en/', + fileBaseUrl: 'https://downloads.dbpedia.org/3.5.1/en/', + hasSparql: false, }, bowlogna: { label: 'Bowlogna', description: 'Synthetic university dataset with SPARQL queries (HOBBIT benchmark)', url: 'https://files.dice-research.org/projects/HOBBIT/benchmarks-data/datasets-dumps/bowlogna-dump.zip', + fileBaseUrl: null, + hasSparql: true, }, bsbm: { label: 'BSBM', description: 'Berlin SPARQL Benchmark — synthetic e-commerce dataset', url: 'https://sourceforge.net/projects/bsbmtools/', + fileBaseUrl: null, + hasSparql: true, }, lubm: { label: 'LUBM', description: 'Lehigh University Benchmark — university domain with RDFS reasoning', url: 'https://swat.cse.lehigh.edu/projects/lubm/', + fileBaseUrl: null, + hasSparql: false, }, }; @@ -154,9 +169,6 @@ function renderDatasetCards(runs, queries) { if (!grid) return; const meta = document.getElementById('datasets-meta'); if (meta) meta.textContent = `${datasets.length} dataset${datasets.length > 1 ? 's' : ''}`; - // Collapse by default when more than one dataset (keeps the page compact) - const details = grid.closest('details'); - if (details && datasets.length > 1) details.removeAttribute('open'); grid.innerHTML = datasets.map(ds => { const meta = DATASET_META[ds.toLowerCase()] ?? { label: ds, description: '', url: null }; @@ -177,9 +189,9 @@ function renderDatasetCards(runs, queries) { `${shortFile(f)}` ).join(''); - // SPARQL queries collapsible (only if queries embedded in JSON) + // SPARQL queries collapsible (only if queries embedded in JSON et dataset hasSparql) let queriesHtml = ''; - if (queries && queries.length > 0) { + if (queries && queries.length > 0 && (DATASET_META[ds.toLowerCase()]?.hasSparql ?? false)) { const queryItems = queries.map((q, i) => `
Q${i + 1} @@ -196,7 +208,7 @@ function renderDatasetCards(runs, queries) { } return ` -
+
${meta.label} ${linkHtml} @@ -222,6 +234,7 @@ function renderDatasetCards(runs, queries) { }).join(''); } + // ── Table sort state ───────────────────────────────────────────────────────── let sortField = null; let sortAsc = true; @@ -249,25 +262,73 @@ function initTableSort() { // ── Engine filter state ─────────────────────────────────────────────────────── // activeEngines: Set of selected engine names. Empty = all engines shown. -let activeEngines = new Set(); -let allCharts = {}; +let activeEngines = new Set(); +let hiddenEngines = new Set(); // engines removed from comparator bar via × +let allCharts = {}; +let activeDatasets = new Set(); +let userRefEngine = localStorage.getItem('refEngine') ?? null; +let metadataQueries = null; // ── Header ──────────────────────────────────────────────────────────────────── function renderHeader(meta, runs) { const datasets = [...new Set(runs.map(r => r.dataset))]; - const badgeContainer = document.getElementById('hdr-datasets'); - if (badgeContainer) { - badgeContainer.innerHTML = datasets.map(d => `${d}`).join(''); + const engines = [...new Set(runs.map(r => r.engine))]; + + + const enginePills = document.getElementById('engine-pills'); + if (enginePills) { + const sorted = [ + ...KNOWN_ENGINE_ORDER.filter(e => engines.includes(e)), + ...engines.filter(e => !KNOWN_ENGINE_ORDER.includes(e)), + ]; + enginePills.innerHTML = sorted.map(engine => { + const dot = engine.indexOf('.'); + const family = dot >= 0 ? engine.slice(0, dot) : engine; + const version = dot >= 0 ? engine.slice(dot + 1) : engine; + const p = pal(engine); + return `
+ ${family} + ${version} + +
`; + }).join(''); + // Attach drag handlers to newly created pills + enginePills.querySelectorAll('.engine-pill[data-engine]').forEach(attachPillDragHandlers); } - document.getElementById('hdr-date').textContent = formatDate(meta.generatedAt); - document.getElementById('hdr-java').textContent = `Java ${meta.java}`; +} - const engines = [...new Set(runs.map(r => r.engine))]; - const engineHtml = engines.map(e => `${e}`).join(' · '); - const desc = document.getElementById('main-desc'); - if (desc) desc.innerHTML = - `Corese reference benchmark dashboard — Loading performance comparison : ${engineHtml}`; +function renderSidebarMeta(meta, engines, runs, queryRuns) { + // Badges + const datasets = [...new Set(runs.map(r => r.dataset))]; + const el = id => document.getElementById(id); + if (el('sidebar-badge-datasets')) el('sidebar-badge-datasets').textContent = datasets.length; + if (el('sidebar-badge-engines')) el('sidebar-badge-engines').textContent = engines.length; + if (el('sidebar-badge-results')) el('sidebar-badge-results').textContent = runs.length; + if (queryRuns && queryRuns.length > 0) { + const queryIds = [...new Set(queryRuns.map(r => r.queryId))]; + if (el('sidebar-badge-sparql')) el('sidebar-badge-sparql').textContent = queryIds.length + ' q'; + const sparqlNav = document.getElementById('sidebar-sparql-nav'); + if (sparqlNav) sparqlNav.style.display = ''; + } + // Footer meta + const box = document.getElementById('sidebar-meta'); + if (box) { + const rows = [ + ['Date', formatDate(meta.generatedAt)], + ['Java', `${meta.java ?? '—'}`], + ['OS', meta.os ?? '—'], + ['Protocol', meta.protocol ? `${meta.protocol.repetitions} reps + warmup` : '—'], + ]; + box.innerHTML = ` + Run context +
+ ${rows.map(([k, v]) => `
${k}
${v}
`).join('')} +
+ `; + } } // ── Run info bar ────────────────────────────────────────────────────────────── @@ -280,39 +341,153 @@ function renderRunInfo(meta) { } // ── KPIs ────────────────────────────────────────────────────────────────────── +let _overviewBarChart = null; +let _overviewRadarChart = null; + function renderKPI(runs) { - const engines = [...new Set(runs.map(r => r.engine))]; - const files = [...new Set(runs.map(r => r.file))]; - const totalTrip = runs.reduce((s, r) => s + r.triples, 0); - const bestR = runs.reduce((best, r) => - r.throughputTriplesPerSec > best.throughputTriplesPerSec ? r : best, runs[0]); - - const kpis = [ - { label: 'Engines tested', num: engines.length, note: engines.join(' · ') }, - { label: 'Files tested', num: files.length, note: '.nt (N-Triples) files' }, - { label: 'Total triples', num: fmt(totalTrip), note: 'loaded in total' }, - { label: 'Best throughput', num: fmt(bestR.throughputTriplesPerSec, 0), note: bestR.engine }, - ]; + if (!runs || runs.length === 0) return; + const engines = [...new Set(runs.map(r => r.engine))]; + const datasets = [...new Set(runs.map(r => r.dataset))]; + const files = [...new Set(runs.map(r => r.file))]; + + // Per-engine avg for the winner cards + const byEngine = {}; + engines.forEach(e => { + const er = runs.filter(r => r.engine === e); + byEngine[e] = { + avgThru: er.reduce((s,r) => s + r.throughputTriplesPerSec, 0) / er.length, + avgMem: er.reduce((s,r) => s + r.memoryDeltaMb, 0) / er.length, + avgTime: er.reduce((s,r) => s + r.loadingTimeMs, 0) / er.length, + }; + }); - document.getElementById('metrics-grid').innerHTML = kpis.map(k => ` -
-
${k.label}
-
${k.num}
-
${k.note}
-
- `).join(''); + const bestThruEngine = engines.reduce((a,b) => byEngine[a].avgThru > byEngine[b].avgThru ? a : b); + const bestMemEngine = engines.reduce((a,b) => byEngine[a].avgMem < byEngine[b].avgMem ? a : b); + const bestTimeEngine = engines.reduce((a,b) => byEngine[a].avgTime < byEngine[b].avgTime ? a : b); + + function winnerCard(label, engine, value, sub) { + const p = pal(engine); + return `
+
${label}
+
+ + ${engine} +
+
${value}
+
${sub}
+
`; + } + + const contextCard = `
+
${engines.length}engines
+
${datasets.length}datasets
+
${files.length}files
+
`; + + document.getElementById('metrics-grid').innerHTML = + winnerCard('Best throughput', bestThruEngine, + fmt(byEngine[bestThruEngine].avgThru, 0) + ' t/s', 'avg triples/s across all files') + + winnerCard('Best memory', bestMemEngine, + fmt(byEngine[bestMemEngine].avgMem, 1) + ' MB', 'avg memory Δ across all files') + + winnerCard('Best load time', bestTimeEngine, + fmt(byEngine[bestTimeEngine].avgTime, 0) + ' ms', 'avg loading time across all files') + + contextCard; + + const meta = document.getElementById('overview-meta'); + if (meta) meta.textContent = `${engines.length} engines · ${datasets.length} datasets · ${files.length} files`; + + _renderOverviewCharts(engines, byEngine); } -// ── Global meter (proportion bars per engine) ───────────────────────────────── -function renderGlobalMeter(runs) { - const engines = [...new Set(runs.map(r => r.engine))]; - const total = runs.length; - const meter = document.getElementById('global-meter'); - meter.innerHTML = engines.map(e => { - const count = runs.filter(r => r.engine === e).length; - const pct = (count / total * 100).toFixed(1); - return `
`; - }).join(''); +function _renderOverviewCharts(engines, byEngine) { + const isDark = document.documentElement.getAttribute('data-theme') !== 'light'; + const gridCol = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)'; + const textCol = isDark ? '#8b949e' : '#57606a'; + + // ── Bar: avg loading time ───────────────────────────────────────────── + const barCtx = document.getElementById('chart-overview-bar')?.getContext('2d'); + if (barCtx) { + if (_overviewBarChart) _overviewBarChart.destroy(); + _overviewBarChart = new Chart(barCtx, { + type: 'bar', + data: { + labels: engines, + datasets: [{ + label: 'Avg load time', + data: engines.map(e => Math.round(byEngine[e].avgTime)), + backgroundColor: engines.map(e => pal(e).bg), + borderColor: engines.map(e => pal(e).hex), + borderWidth: 1, + borderRadius: 4, + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { callbacks: { label: ctx => ` ${ctx.raw.toLocaleString()} ms` } }, + }, + scales: { + x: { grid: { display: false }, ticks: { color: textCol, font: { size: 10 } } }, + y: { beginAtZero: true, grid: { color: gridCol }, ticks: { color: textCol, font: { size: 10 }, callback: v => v.toLocaleString() } }, + }, + }, + }); + } + + // ── Radar: normalized scores ────────────────────────────────────────── + const radarCtx = document.getElementById('chart-overview-radar')?.getContext('2d'); + if (radarCtx) { + if (_overviewRadarChart) _overviewRadarChart.destroy(); + + const maxThru = Math.max(...engines.map(e => byEngine[e].avgThru)); + const maxTime = Math.max(...engines.map(e => byEngine[e].avgTime)); + const maxMem = Math.max(...engines.map(e => byEngine[e].avgMem)); + + const radarDatasets = engines.map(e => { + const p = pal(e); + // Throughput: higher = better → normalize directly + // Load time: lower = better → invert + // Memory: lower = better → invert + const scoreThru = maxThru > 0 ? Math.round(byEngine[e].avgThru / maxThru * 100) : 0; + const scoreTime = maxTime > 0 ? Math.round((1 - (byEngine[e].avgTime - Math.min(...engines.map(x=>byEngine[x].avgTime))) / maxTime) * 100) : 100; + const scoreMem = maxMem > 0 ? Math.round((1 - (byEngine[e].avgMem - Math.min(...engines.map(x=>byEngine[x].avgMem))) / maxMem) * 100) : 100; + return { + label: e, + data: [scoreThru, scoreTime, scoreMem], + borderColor: p.hex, + backgroundColor: p.hex + '22', + pointBackgroundColor: p.hex, + borderWidth: 1.8, + pointRadius: 3, + }; + }); + + _overviewRadarChart = new Chart(radarCtx, { + type: 'radar', + data: { + labels: ['Throughput', 'Load speed', 'Memory efficiency'], + datasets: radarDatasets, + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'bottom', labels: { boxWidth: 10, padding: 10, font: { size: 10 }, color: textCol } }, + }, + scales: { + r: { + min: 0, max: 100, + ticks: { display: false, stepSize: 25 }, + grid: { color: gridCol }, + angleLines: { color: gridCol }, + pointLabels: { font: { size: 11 }, color: textCol }, + }, + }, + }, + }); + } } // ── Engine cards ────────────────────────────────────────────────────────────── @@ -333,7 +508,9 @@ function renderEngineCards(runs) { // Reference: stable Corese version if present, otherwise the best engine const stableCorese = engines.find(isStableCorese); const maxAvgThru = Math.max(...Object.values(avgThruMap)); - const refEngine = stableCorese ?? engines.find(e => avgThruMap[e] === maxAvgThru); + const refEngine = userRefEngine && engines.includes(userRefEngine) + ? userRefEngine + : (stableCorese ?? engines.find(e => avgThruMap[e] === maxAvgThru)); const refThru = avgThruMap[refEngine]; document.getElementById('engine-grid').innerHTML = engines.map(engine => { @@ -380,7 +557,14 @@ function renderEngineCards(runs) { ${engine} - ${scoreDisplay} + + ${scoreDisplay} + +
@@ -408,33 +592,324 @@ function renderEngineCards(runs) { }).join(''); } +function syncEnginePills() { + document.querySelectorAll('.engine-pills-wrap .engine-pill[data-engine]').forEach(el => { + const e = el.dataset.engine; + el.classList.remove('ep-active', 'ep-dimmed'); + if (activeEngines.size > 0) { + if (activeEngines.has(e)) el.classList.add('ep-active'); + else el.classList.add('ep-dimmed'); + } + }); + // also sync engine cards in engine-grid + document.querySelectorAll('#engine-grid .suite-item[data-engine]').forEach(el => { + el.classList.toggle('active', activeEngines.has(el.dataset.engine)); + }); + const btn = document.getElementById('clear-engines-btn'); + if (btn) btn.style.display = activeEngines.size > 0 ? '' : 'none'; +} + +function getDatasetFilteredRuns() { + // Runs filtered only by dataset (not by engine) — used to keep all engine cards visible + return allRuns.filter(r => + !hiddenEngines.has(r.engine) && + (activeDatasets.size === 0 || activeDatasets.has(r.dataset)) + ); +} + function toggleEngine(engine) { if (activeEngines.has(engine)) { activeEngines.delete(engine); } else { activeEngines.add(engine); } - document.querySelectorAll('.suite-item[data-engine]').forEach(el => { - el.classList.toggle('active', activeEngines.has(el.dataset.engine)); - }); - // Show/hide clear button - const btn = document.getElementById('clear-engines-btn'); - if (btn) btn.style.display = activeEngines.size > 0 ? '' : 'none'; + const filtered = getFilteredRuns(allRuns); + renderKPI(filtered); + renderEngineCards(getDatasetFilteredRuns()); // toujours toutes les cartes + syncEnginePills(); updateCharts(); updateTable(); + // Sync SPARQL section + document.querySelectorAll('.query-engine-card[data-engine]').forEach(el => { + const e = el.dataset.engine; + el.style.display = (activeEngines.size === 0 || activeEngines.has(e)) ? '' : 'none'; + }); + updateQueryChart(); + updateQueryTable(); + pushStateToUrl(); } function clearEngineFilter() { activeEngines.clear(); - document.querySelectorAll('.suite-item[data-engine]').forEach(el => el.classList.remove('active')); const btn = document.getElementById('clear-engines-btn'); if (btn) btn.style.display = 'none'; + renderEngineCards(getDatasetFilteredRuns()); + syncEnginePills(); + updateCharts(); + updateTable(); + pushStateToUrl(); +} + +function getFilteredRuns(runs) { + return runs.filter(r => + !hiddenEngines.has(r.engine) && + (activeEngines.size === 0 || activeEngines.has(r.engine)) && + (activeDatasets.size === 0 || activeDatasets.has(r.dataset)) + ); +} + +function hidePillEngine(engine) { + hiddenEngines.add(engine); + activeEngines.delete(engine); + // Remove pill from the comparator bar + document.querySelectorAll(`.engine-pills-wrap .engine-pill[data-engine="${engine}"]`).forEach(el => el.remove()); + const filtered = getFilteredRuns(allRuns); + renderKPI(filtered); + renderEngineCards(getDatasetFilteredRuns()); + syncEnginePills(); + updateCharts(); + updateTable(); + document.querySelectorAll('.query-engine-card[data-engine]').forEach(el => { + el.style.display = hiddenEngines.has(el.dataset.engine) ? 'none' : ''; + }); + updateQueryChart(); + updateQueryTable(); + updatePillsScrollBtns(); + pushStateToUrl(); +} + +function clearDatasetFilter() { + activeDatasets.clear(); + syncDatasetPills(); updateCharts(); updateTable(); + renderKPI(getFilteredRuns(allRuns)); + renderEngineCards(getDatasetFilteredRuns()); + const btn = document.getElementById('clear-engines-btn'); + if (btn) btn.style.display = activeEngines.size > 0 ? '' : 'none'; + updateQuerySectionVisibility(); + pushStateToUrl(); +} + +function updateQuerySectionVisibility() { + const sparqlSection = document.getElementById('section-sparql'); + const sparqlNav = document.getElementById('sidebar-sparql-nav'); + const hasSparql = activeDatasets.size > 0 && + [...activeDatasets].some(ds => DATASET_META[ds.toLowerCase()]?.hasSparql ?? false); + if (sparqlSection) sparqlSection.style.display = hasSparql ? '' : 'none'; + if (sparqlNav) sparqlNav.style.display = hasSparql ? '' : 'none'; +} + +function renderDatasetPills(runs) { + const wrap = document.getElementById('dataset-pills'); + if (!wrap) return; + const datasets = [...new Set(runs.map(r => r.dataset))]; + wrap.innerHTML = + `` + + datasets.map(ds => { + const meta = DATASET_META[ds.toLowerCase()] ?? { label: ds }; + const label = meta.label ?? ds; + return ``; + }).join(''); +} + +function syncDatasetPills() { + document.querySelectorAll('.dataset-pill[data-ds]').forEach(el => { + const ds = el.dataset.ds; + el.classList.toggle('active', ds === '' ? activeDatasets.size === 0 : activeDatasets.has(ds)); + }); + // Dataset cards: show only selected one (or all in All mode) + document.querySelectorAll('.dataset-card[data-ds]').forEach(el => { + const ds = el.dataset.ds; + el.classList.toggle('active', activeDatasets.has(ds)); + el.style.display = (activeDatasets.size === 0 || activeDatasets.has(ds)) ? '' : 'none'; + }); + _syncDatasetSections(); +} + +function _syncDatasetSections() { + const isAll = activeDatasets.size === 0; + + // All mode: show Overview + Datasets + W3C, hide detail sections + // Dataset selected: hide Overview + W3C, show detail sections + const overviewSection = document.getElementById('section-kpi'); + if (overviewSection) overviewSection.style.display = isAll ? '' : 'none'; + + const w3cSection = document.getElementById('section-w3c'); + if (w3cSection && w3cSection.innerHTML.trim()) w3cSection.style.display = isAll ? '' : 'none'; + + const w3cNav = document.getElementById('sidebar-w3c-nav'); + if (w3cNav) w3cNav.style.display = isAll ? '' : 'none'; + + // Bowlogna is SPARQL-focused: hide Performance charts + Full results, show only Engines + SPARQL + const isBowlogna = activeDatasets.has('bowlogna'); + const hideCharts = isAll || isBowlogna; + const hideResults = isAll || isBowlogna; + + const enginesEl = document.getElementById('section-engines'); + if (enginesEl) enginesEl.style.display = isAll ? 'none' : ''; + const chartsEl = document.getElementById('section-charts'); + if (chartsEl) chartsEl.style.display = hideCharts ? 'none' : ''; + const resultsEl = document.getElementById('section-results'); + if (resultsEl) resultsEl.style.display = hideResults ? 'none' : ''; + + // Sidebar: Overview nav visible in All, hidden in dataset mode + document.querySelectorAll('.sidebar-nav-item[data-target="section-kpi"]').forEach(el => { + el.closest('li').style.display = isAll ? '' : 'none'; + }); + document.querySelectorAll('.sidebar-nav-item[data-target="section-engines"]').forEach(el => { + el.closest('li').style.display = isAll ? 'none' : ''; + }); + document.querySelectorAll('.sidebar-nav-item[data-target="section-charts"]').forEach(el => { + el.closest('li').style.display = hideCharts ? 'none' : ''; + }); + document.querySelectorAll('.sidebar-nav-item[data-target="section-results"]').forEach(el => { + el.closest('li').style.display = hideResults ? 'none' : ''; + }); + // Results group heading in sidebar + const resultsGroup = document.querySelector('.sidebar-group:last-of-type'); + if (resultsGroup) resultsGroup.style.display = isAll ? 'none' : ''; +} + +function toggleDataset(ds) { + if (activeDatasets.has(ds)) { + activeDatasets.clear(); // re-clic → tout afficher + } else { + activeDatasets.clear(); + activeDatasets.add(ds); // sélection exclusive + } + syncDatasetPills(); + Object.values(allCharts).forEach(c => c.destroy()); + allCharts = {}; + updateTable(); + renderKPI(getFilteredRuns(allRuns)); + renderEngineCards(getDatasetFilteredRuns()); + _refreshQuerySection(); + pushStateToUrl(); + if (activeDatasets.size > 0) { + requestAnimationFrame(() => requestAnimationFrame(() => { + initCharts(); + _updateChartsMeta(); + })); + } +} + +function _updateChartsMeta() { + const el = document.getElementById('charts-mode-meta'); + if (!el) return; + el.textContent = isScalabilityMode() + ? 'scalability view — X axis = graph size' + : ''; +} + +function _refreshQuerySection() { + // Always update visibility (hides SPARQL nav for datasets without SPARQL) + updateQuerySectionVisibility(); + if (!allQueryRuns || allQueryRuns.length === 0) return; + const hasSparql = activeDatasets.size > 0 && + [...activeDatasets].some(ds => DATASET_META[ds.toLowerCase()]?.hasSparql ?? false); + const filteredQueryRuns = hasSparql + ? allQueryRuns.filter(r => activeDatasets.has(r.dataset)) + : []; + const sparqlSection = document.getElementById('section-sparql'); + if (sparqlSection) sparqlSection.innerHTML = ''; + if (filteredQueryRuns.length > 0) { + renderQuerySection(filteredQueryRuns); + // Re-apply hidden engines (renderQuerySection re-renders from scratch) + document.querySelectorAll('.query-engine-card[data-engine]').forEach(el => { + el.style.display = hiddenEngines.has(el.dataset.engine) ? 'none' : ''; + }); + updateQueryChart(); + updateQueryTable(); + } +} + +function setRefEngine(engine) { + userRefEngine = userRefEngine === engine ? null : engine; + if (userRefEngine) localStorage.setItem('refEngine', userRefEngine); + else localStorage.removeItem('refEngine'); + renderEngineCards(getDatasetFilteredRuns()); + document.querySelectorAll('.suite-item[data-engine]').forEach(el => { + el.classList.toggle('active', activeEngines.has(el.dataset.engine)); + }); + pushStateToUrl(); } // ── Engine card drag & drop ─────────────────────────────────────────────────── -let dragSrc = null; +let dragSrc = null; +let pillDragSrc = null; + +// ── Pill drag-to-reorder ────────────────────────────────────────────────────── +function pillDragStart(e) { + pillDragSrc = e.currentTarget; + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', pillDragSrc.dataset.engine); + pillDragSrc.classList.add('pill-dragging'); +} + +function pillDragOver(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + const target = e.currentTarget; + if (target !== pillDragSrc) target.classList.add('pill-drag-over'); +} + +function pillDragLeave(e) { + e.currentTarget.classList.remove('pill-drag-over'); +} + +function pillDrop(e) { + e.preventDefault(); + const target = e.currentTarget; + target.classList.remove('pill-drag-over'); + if (!pillDragSrc || pillDragSrc === target) return; + const wrap = document.getElementById('engine-pills'); + if (!wrap || !wrap.contains(pillDragSrc) || !wrap.contains(target)) return; + + const pills = [...wrap.querySelectorAll('.engine-pill[data-engine]')]; + const srcIdx = pills.indexOf(pillDragSrc); + const tgtIdx = pills.indexOf(target); + + if (srcIdx < tgtIdx) wrap.insertBefore(pillDragSrc, target.nextSibling); + else wrap.insertBefore(pillDragSrc, target); + + // Sync engine cards order to match pills + const newOrder = [...wrap.querySelectorAll('.engine-pill[data-engine]')].map(el => el.dataset.engine); + syncCardsToOrder(newOrder); +} + +function pillDragEnd(e) { + e.currentTarget.classList.remove('pill-dragging'); + document.querySelectorAll('.engine-pill').forEach(el => el.classList.remove('pill-drag-over')); + pillDragSrc = null; + updatePillsScrollBtns(); +} + +function syncCardsToOrder(order) { + const grid = document.getElementById('engine-grid'); + if (!grid) return; + order.forEach(engine => { + const card = grid.querySelector(`.suite-item[data-engine="${engine}"]`); + if (card) grid.appendChild(card); + }); +} + +function syncPillsToOrder(order) { + const wrap = document.getElementById('engine-pills'); + if (!wrap) return; + order.forEach(engine => { + const pill = wrap.querySelector(`.engine-pill[data-engine="${engine}"]`); + if (pill) wrap.appendChild(pill); + }); +} + +function attachPillDragHandlers(pill) { + pill.addEventListener('dragstart', pillDragStart); + pill.addEventListener('dragover', pillDragOver); + pill.addEventListener('drop', pillDrop); + pill.addEventListener('dragend', pillDragEnd); + pill.addEventListener('dragleave', pillDragLeave); +} function dragStart(e) { dragSrc = e.currentTarget; @@ -473,6 +948,8 @@ function dragDrop(e) { const order = [...grid.querySelectorAll('.suite-item[data-engine]')].map(el => el.dataset.engine); localStorage.setItem('engineOrder', JSON.stringify(order)); + syncPillsToOrder(order); + updateCharts(); } function dragQueryDrop(e) { @@ -491,6 +968,7 @@ function dragQueryDrop(e) { if (srcIdx < tgtIdx) grid.insertBefore(dragSrc, target.nextSibling); else grid.insertBefore(dragSrc, target); + updateQueryChart(); } function dragEnd(e) { @@ -520,6 +998,7 @@ function applyStoredEngineOrder() { // ── Charts ──────────────────────────────────────────────────────────────────── let allRuns = []; +let currentData = null; const CHART_OPT = (yLabel) => ({ responsive: true, @@ -539,52 +1018,252 @@ const CHART_OPT = (yLabel) => ({ }, }); +// ── Scalability mode (single dataset → line charts by triple count) ─────────── +// Active when exactly one non-bowlogna dataset is selected. +function isScalabilityMode() { + return activeDatasets.size === 1 && !activeDatasets.has('bowlogna'); +} + +function fmtTriples(n) { + if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M'; + if (n >= 1_000) return Math.round(n / 1_000) + 'K'; + return String(n); +} + +// Returns files sorted by ascending average triple count across engines +function getScalabilityFiles() { + const dsRuns = allRuns.filter(r => activeDatasets.has(r.dataset)); + const files = [...new Set(dsRuns.map(r => r.file))]; + files.sort((a, b) => { + const avg = f => { + const rs = dsRuns.filter(r => r.file === f); + return rs.length ? rs.reduce((s, r) => s + r.triples, 0) / rs.length : 0; + }; + return avg(a) - avg(b); + }); + return files; +} + +// X-axis labels: triple count formatted (e.g. "4M", "800K") +function getScalabilityLabels(files) { + const dsRuns = allRuns.filter(r => activeDatasets.has(r.dataset)); + return files.map(f => { + const rs = dsRuns.filter(r => r.file === f); + const avg = rs.length ? rs.reduce((s, r) => s + r.triples, 0) / rs.length : 0; + return fmtTriples(Math.round(avg)); + }); +} + +const LINE_CHART_OPT = (yLabel, fileNames = []) => ({ + responsive: true, + maintainAspectRatio: false, + spanGaps: true, + plugins: { + legend: { position: 'bottom', labels: { boxWidth: 10, padding: 12, font: { size: 11, family: "'Inter'" } } }, + tooltip: { + mode: 'index', + intersect: false, + callbacks: { + title: (items) => { + const idx = items[0]?.dataIndex ?? 0; + const fname = fileNames[idx] ? shortFile(fileNames[idx]) : ''; + return fname ? `${items[0].label} — ${fname}` : items[0].label; + }, + }, + }, + }, + scales: { + x: { + grid: { display: false }, + ticks: { font: { size: 10 }, maxRotation: 20 }, + title: { display: true, text: 'Graph size (triples)', font: { size: 10 }, color: '#8b949e' }, + }, + y: { + grid: { color: 'rgba(255,255,255,0.05)' }, + ticks: { font: { size: 10 } }, + beginAtZero: true, + title: { display: true, text: yLabel, font: { size: 10 }, color: '#8b949e' }, + }, + }, +}); + +function buildLineDatasets(field) { + const dsRuns = allRuns.filter(r => activeDatasets.has(r.dataset)); + const allEngs = getEngineOrder(); + const engines = allEngs.filter(e => + !hiddenEngines.has(e) && (activeEngines.size === 0 || activeEngines.has(e)) + ); + const files = getScalabilityFiles(); + return engines.map(engine => { + const p = pal(engine); + return { + label: engine, + data: files.map(f => { + const r = dsRuns.find(x => x.engine === engine && x.file === f); + return r ? +r[field].toFixed(1) : null; + }), + borderColor: p.hex, + backgroundColor: p.bg, + borderWidth: 2, + tension: 0.3, + pointRadius: 4, + pointHoverRadius: 6, + fill: false, + }; + }); +} + function buildDatasets(field) { - const engines = [...new Set(allRuns.map(r => r.engine))]; - const files = [...new Set(allRuns.map(r => r.file))]; + const dsRuns = activeDatasets.size === 0 ? allRuns : allRuns.filter(r => activeDatasets.has(r.dataset)); + const allEngs = getEngineOrder(); + // Respect activeEngines filter (empty = all) + const engines = allEngs.filter(e => + !hiddenEngines.has(e) && (activeEngines.size === 0 || activeEngines.has(e)) + ); + const files = [...new Set(dsRuns.map(r => r.file))]; return engines.map(engine => { const p = pal(engine); - const visible = activeEngines.size === 0 || activeEngines.has(engine); return { label: engine, data: files.map(f => { - const r = allRuns.find(x => x.engine === engine && x.file === f); + const r = dsRuns.find(x => x.engine === engine && x.file === f); return r ? +r[field].toFixed(1) : null; }), - backgroundColor: visible ? p.bg : 'rgba(100,100,100,0.15)', - borderColor: visible ? p.hex : 'rgba(100,100,100,0.3)', + backgroundColor: p.bg, + borderColor: p.hex, borderWidth: 1, borderRadius: 3, }; }); } -function initCharts() { - const files = [...new Set(allRuns.map(r => r.file))]; - const labels = files.map(shortFile); - - const configs = [ - { id: 'chart-loading', field: 'loadingTimeMs', yLabel: 'ms' }, - { id: 'chart-throughput', field: 'throughputTriplesPerSec', yLabel: 'triples/s' }, - { id: 'chart-memory', field: 'memoryDeltaMb', yLabel: 'MB' }, - { id: 'chart-cpu', field: 'cpuPercent', yLabel: '%' }, - ]; +function getEngineOrder() { + const cardEls = document.querySelectorAll('#engine-grid .suite-item[data-engine]'); + return cardEls.length > 0 + ? [...cardEls].map(el => el.dataset.engine) + : [...new Set(allRuns.map(r => r.engine))]; +} - for (const { id, field, yLabel } of configs) { - allCharts[id] = new Chart(document.getElementById(id).getContext('2d'), { +function buildCombinedDatasets() { + const dsRuns = activeDatasets.size === 0 ? allRuns : allRuns.filter(r => activeDatasets.has(r.dataset)); + const allEngines = getEngineOrder(); + const engines = activeEngines.size === 0 ? allEngines : allEngines.filter(e => activeEngines.has(e)); + const files = [...new Set(dsRuns.map(r => r.file))]; + const datasets = []; + engines.forEach(engine => { + const p = pal(engine); + // Barres : Loading time (axe gauche) + datasets.push({ + label: `${engine}`, type: 'bar', - data: { labels, datasets: buildDatasets(field) }, - options: CHART_OPT(yLabel), + yAxisID: 'yLeft', + data: files.map(f => { const r = dsRuns.find(x => x.engine === engine && x.file === f); return r ? +r.loadingTimeMs.toFixed(0) : null; }), + backgroundColor: p.bg, + borderColor: p.hex, + borderWidth: 1, + borderRadius: 3, }); - allCharts[id]._field = field; + // Lignes : Memory (axe droit) + datasets.push({ + label: `${engine} (MB)`, + type: 'line', + yAxisID: 'yRight', + data: files.map(f => { const r = dsRuns.find(x => x.engine === engine && x.file === f); return r ? +r.memoryDeltaMb.toFixed(1) : null; }), + borderColor: p.hex, + backgroundColor: 'transparent', + borderWidth: 2, + borderDash: [5, 3], + pointRadius: 3, + tension: 0.3, + }); + }); + return datasets; +} + +function initCharts() { + const defs = [ + { id: 'chart-loading', field: 'loadingTimeMs', unit: 'ms' }, + { id: 'chart-memory', field: 'memoryDeltaMb', unit: 'MB' }, + { id: 'chart-cpu', field: 'cpuPercent', unit: '%' }, + ]; + + if (isScalabilityMode()) { + // Single dataset → line charts (X = triple count, ascending) + const files = getScalabilityFiles(); + const labels = getScalabilityLabels(files); + for (const { id, field, unit } of defs) { + allCharts[id] = new Chart(document.getElementById(id).getContext('2d'), { + type: 'line', + data: { labels, datasets: buildLineDatasets(field) }, + options: LINE_CHART_OPT(unit, files), + }); + allCharts[id]._field = field; + } + } else { + // All / multi-dataset → bar charts + const dsRuns = activeDatasets.size === 0 ? allRuns : allRuns.filter(r => activeDatasets.has(r.dataset)); + const files = [...new Set(dsRuns.map(r => r.file))]; + const labels = files.map(shortFile); + for (const { id, field, unit } of defs) { + allCharts[id] = new Chart(document.getElementById(id).getContext('2d'), { + type: 'bar', + data: { labels, datasets: buildDatasets(field) }, + options: CHART_OPT(unit), + }); + allCharts[id]._field = field; + } } } function updateCharts() { - for (const [id, chart] of Object.entries(allCharts)) { - chart.data.datasets = buildDatasets(chart._field); - chart.update('none'); + if (activeDatasets.size === 0) return; + if (Object.keys(allCharts).length === 0) return; // charts not yet created + + if (isScalabilityMode()) { + // Line mode: update labels + datasets in-place + const files = getScalabilityFiles(); + const labels = getScalabilityLabels(files); + for (const chart of Object.values(allCharts)) { + const field = chart._field; + if (!field) continue; + chart.data.labels = labels; + const newDs = buildLineDatasets(field); + chart.data.datasets.splice(0, chart.data.datasets.length, ...newDs); + chart.update('none'); + } + } else { + // Bar mode: update datasets in-place + const dsRuns = allRuns.filter(r => activeDatasets.has(r.dataset)); + const allEngs = getEngineOrder(); + const engines = allEngs.filter(e => + !hiddenEngines.has(e) && (activeEngines.size === 0 || activeEngines.has(e)) + ); + const files = [...new Set(dsRuns.map(r => r.file))]; + + for (const chart of Object.values(allCharts)) { + const field = chart._field; + if (!field) continue; + const newDs = engines.map(engine => { + const p = pal(engine); + return { + label: engine, + data: files.map(f => { + const r = dsRuns.find(x => x.engine === engine && x.file === f); + return r ? +r[field].toFixed(1) : null; + }), + backgroundColor: p.bg, + borderColor: p.hex, + borderWidth: 1, + borderRadius: 3, + }; + }); + chart.data.datasets.splice(0, chart.data.datasets.length, ...newDs); + chart.update('none'); + } } + + const filtered = getFilteredRuns(allRuns); + if (filtered.length > 0) renderKPI(filtered); } // ── Tableau ──────────────────────────────────────────────────────────────────── @@ -593,9 +1272,7 @@ function renderTable(runs) { } function updateTable(runsArg) { - let runs = (runsArg ?? allRuns).filter(r => - activeEngines.size === 0 || activeEngines.has(r.engine) - ); + let runs = getFilteredRuns(runsArg ?? allRuns); if (sortField) { runs = [...runs].sort((a, b) => { @@ -671,6 +1348,7 @@ function updateTable(runsArg) { let activeQueryEngines = new Set(); let queryChart = null; let allQueryRuns = []; +let queryChartDatasetsBase = {}; // datasets originaux, jamais modifiés function toggleQueryEngine(engine) { if (activeQueryEngines.has(engine)) { @@ -698,43 +1376,76 @@ function clearQueryEngineFilter() { function updateQueryChart() { if (!queryChart) return; - const engines = [...document.querySelectorAll('.query-engine-card[data-engine]')] + // Ordre depuis les cards (drag & drop) + const cardOrder = [...document.querySelectorAll('.query-engine-card[data-engine]')] .map(el => el.dataset.engine); - queryChart.data.datasets = queryChart.data.datasets.map(ds => { - const visible = activeQueryEngines.size === 0 || activeQueryEngines.has(ds.label); - const p = pal(ds.label); - return { ...ds, - backgroundColor: visible ? p.bg : 'rgba(100,100,100,0.15)', - borderColor: visible ? p.hex : 'rgba(100,100,100,0.3)', - }; - }); - queryChart.update('none'); + // Réordonner ET mettre à jour les couleurs + queryChart.data.datasets = cardOrder + .filter(e => queryChartDatasetsBase[e] && + !hiddenEngines.has(e) && + (activeQueryEngines.size === 0 || activeQueryEngines.has(e)) && + (activeEngines.size === 0 || activeEngines.has(e)) + ) + .map(e => { + const ds = queryChartDatasetsBase[e]; + const p = pal(e); + return { ...ds, + backgroundColor: p.bg, + borderColor: p.hex, + }; + }); + queryChart.update(); } function updateQueryTable() { const tbody = document.getElementById('query-results-body'); - if (!tbody) return; + if (!tbody || !allQueryRuns.length) return; const table = tbody.closest('table'); if (!table) return; - // Map engine → column index (0-based) from thead - const headers = [...table.querySelectorAll('thead th')]; - const engineColIndex = {}; - headers.forEach((th, i) => { - if (th.dataset.engine) engineColIndex[th.dataset.engine] = i; + const allEngines = [...new Set(allQueryRuns.map(r => r.engine))]; + const visibleEngines = allEngines.filter(e => + !hiddenEngines.has(e) && + (activeQueryEngines.size === 0 || activeQueryEngines.has(e)) && + (activeEngines.size === 0 || activeEngines.has(e)) + ); + + const queryIds = [...new Set(allQueryRuns.map(r => r.queryId))].sort( + (a, b) => parseInt(a.split('_')[1]) - parseInt(b.split('_')[1]) + ); + + // Masquer les colonnes non visibles dans le thead + table.querySelectorAll('thead th[data-engine]').forEach(th => { + th.style.display = visibleEngines.includes(th.dataset.engine) ? '' : 'none'; }); - // Apply opacity per cell using direct column index - const allRows = [...table.querySelectorAll('tr')]; - allRows.forEach(tr => { - const cells = [...tr.children]; - headers.forEach((th, i) => { - const engine = th.dataset.engine; - if (!engine) return; - const visible = activeQueryEngines.size === 0 || activeQueryEngines.has(engine); - if (cells[i]) cells[i].style.opacity = visible ? '' : '0.25'; + // Reconstruire le tbody avec best recalculé parmi les moteurs visibles uniquement + tbody.innerHTML = queryIds.map(qid => { + // Valeurs pour tous les moteurs (pour mapping colonne) + const rowData = allEngines.map(engine => { + const r = allQueryRuns.find(r => r.engine === engine && r.queryId === qid); + return r && r.medianMs >= 0 ? r.medianMs : null; }); - }); + + // Best parmi moteurs VISIBLES seulement + const visibleData = visibleEngines.map(e => rowData[allEngines.indexOf(e)]); + const validVisible = visibleData.filter(v => v !== null); + const minMs = validVisible.length ? Math.min(...validVisible) : null; + const winner = minMs !== null ? visibleEngines[visibleData.indexOf(minMs)] : null; + + const cells = allEngines.map((engine, i) => { + if (!visibleEngines.includes(engine)) return ``; + const ms = rowData[i]; + if (ms === null) return `—`; + return `${fmt(ms)} ms`; + }).join(''); + + const bestCell = winner + ? `${winner}` + : `—`; + + return `${qid}${cells}${bestCell}`; + }).join(''); } function renderQuerySection(queryRuns) { @@ -800,7 +1511,7 @@ function renderQuerySection(queryRuns) { ${queryIds.length} queries · ${engines.length} engines · ms, lower is better
-

Median execution time per query — 3 repetitions

+

Median execution time per query — 3 repetitions  ·  — = timeout or error

${cards}
@@ -835,13 +1546,7 @@ function renderQuerySection(queryRuns) { const cells = engines.map((engine, i) => { const ms = rowData[i]; if (ms === null) return `—`; - const isBest = ms === minMs; - const r = minMs > 0 ? ms / minMs : 1; - const isRed = !isBest && r >= 20; - const isOrange= !isBest && r >= 5 && r < 20; - const ratio = !isBest && r >= 2 ? ` ×${r.toFixed(1)}` : ''; - const cls = isBest ? 'query-best' : isRed ? 'query-worst' : isOrange ? 'query-slow' : ''; - return `${fmt(ms)} ms${ratio}`; + return `${fmt(ms)} ms`; }).join(''); const bestCell = winner @@ -856,7 +1561,18 @@ function renderQuerySection(queryRuns) {
`; - document.querySelector('.app-wrapper').appendChild(section); + const sparqlSection = document.getElementById('section-sparql'); + if (sparqlSection) { + // Show only if a specific dataset is selected (not in All mode) + sparqlSection.style.display = activeDatasets.size > 0 ? '' : 'none'; + sparqlSection.appendChild(section); + } else { + (document.querySelector('.app-content') || document.querySelector('.app-wrapper')).appendChild(section); + } + + // Stocker les datasets originaux pour pouvoir restaurer après filtre + queryChartDatasetsBase = {}; + datasets.forEach(ds => { queryChartDatasetsBase[ds.label] = ds; }); // Render chart — store reference for filter updates queryChart = new Chart(document.getElementById('chart-queries'), { @@ -876,98 +1592,55 @@ function renderQuerySection(queryRuns) { }); } -// ── Tendance historique ─────────────────────────────────────────────────────── -async function renderTrend() { - let valid; - // Priority: inline data (file:// compatible) - if (window.__HISTORY_DATA__ && window.__HISTORY_DATA__.length >= 2) { - valid = window.__HISTORY_DATA__; - } else { - // Fallback : fetch (fonctionne sur serveur HTTP) - let index; - try { - const res = await fetch('data/history/index.json'); - if (!res.ok) return; - index = await res.json(); - } catch (_) { return; } - if (!index || index.length < 2) return; - - const reports = await Promise.all( - index.map(async entry => { - try { - const r = await fetch(`data/history/${entry.file}`); - if (!r.ok) return null; - const d = await r.json(); - return { date: entry.date, data: d }; - } catch (_) { return null; } - }) - ); - valid = reports.filter(Boolean); - if (valid.length < 2) return; - } +// ── Chart zoom modal ────────────────────────────────────────────────────────── +let _zoomChart = null; - // Engines: only those present in the current run (allRuns), to keep the chart focused - const currentEngines = new Set(allRuns.map(r => r.engine)); - const engines = [...new Set(valid.flatMap(r => r.data.runs.map(run => run.engine)))] - .filter(e => currentEngines.has(e)); +function openChartZoom(chartId) { + const src = allCharts[chartId]; + if (!src) return; - // Time series: average throughput per engine - const datasets = engines.map(engine => { - const p = pal(engine); - return { - label: engine, - data: valid.map(r => { - const runs = r.data.runs.filter(run => run.engine === engine); - if (!runs.length) return null; - return Math.round(runs.reduce((s, run) => s + run.throughputTriplesPerSec, 0) / runs.length); - }), - borderColor: p.hex, - backgroundColor: p.bg, - tension: 0.3, - fill: false, - pointRadius: 4, - }; - }); + const dialog = document.getElementById('chart-zoom-modal'); + const titleEl = document.getElementById('chart-zoom-title'); + const pngBtn = document.getElementById('chart-zoom-png'); + const canvas = document.getElementById('chart-zoom-canvas'); - const labels = valid.map(r => { - const d = r.date; // format YYYYMMDD_HHMMSS - return `${d.slice(6,8)}/${d.slice(4,6)} ${d.slice(9,11)}:${d.slice(11,13)}`; - }); + // Titre = titre du chart-surface correspondant + const surface = document.getElementById(chartId)?.closest('.chart-surface'); + titleEl.textContent = surface?.querySelector('.chart-title')?.textContent ?? chartId; - const section = document.createElement('section'); - section.innerHTML = ` -
- -

Historical trend

- ${valid.length} runs · avg throughput per engine -
-
-
- Throughput over time - triples/s — higher is better -
- -
-
- `; - document.querySelector('.app-wrapper').appendChild(section); + // Détruire le chart zoom précédent + if (_zoomChart) { _zoomChart.destroy(); _zoomChart = null; } - new Chart(document.getElementById('chart-trend'), { - type: 'line', - data: { labels, datasets }, + // Cloner data + options du chart source + _zoomChart = new Chart(canvas.getContext('2d'), { + type: src.config.type, + data: JSON.parse(JSON.stringify(src.data)), options: { + ...src.config.options, responsive: true, + maintainAspectRatio: false, plugins: { - legend: { position: 'bottom', labels: { boxWidth: 10, padding: 12, font: { size: 11, family: "'Inter'" } } }, - tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${fmt(ctx.raw, 0)} t/s` } }, - }, - scales: { - x: { grid: { display: false }, ticks: { font: { size: 10 } } }, - y: { beginAtZero: false, ticks: { callback: v => fmt(v, 0) } }, + ...src.config.options?.plugins, + legend: { position: 'bottom', labels: { boxWidth: 12, padding: 16, font: { size: 13, family: "'Inter'" } } }, }, }, }); + + pngBtn.onclick = () => { + const a = document.createElement('a'); + a.download = `corese-benchmark-${chartId}.png`; + a.href = canvas.toDataURL('image/png'); + a.click(); + }; + + dialog.showModal(); +} + +function closeChartZoom() { + const dialog = document.getElementById('chart-zoom-modal'); + dialog.close(); + if (_zoomChart) { _zoomChart.destroy(); _zoomChart = null; } } // ── PNG export ──────────────────────────────────────────────────────────────── @@ -980,6 +1653,249 @@ function downloadChart(canvasId, name) { a.click(); } +// ── Pills scroll arrows ─────────────────────────────────────────────────────── +function scrollPills(dir) { + const wrap = document.getElementById('engine-pills'); + if (!wrap) return; + wrap.scrollBy({ left: dir * 180, behavior: 'smooth' }); +} + +function updatePillsScrollBtns() { + const wrap = document.getElementById('engine-pills'); + const btnL = document.getElementById('pills-scroll-left'); + const btnR = document.getElementById('pills-scroll-right'); + if (!wrap || !btnL || !btnR) return; + const canScrollL = wrap.scrollLeft > 4; + const canScrollR = wrap.scrollLeft < wrap.scrollWidth - wrap.clientWidth - 4; + btnL.style.opacity = canScrollL ? '1' : '0.25'; + btnL.style.pointerEvents = canScrollL ? '' : 'none'; + btnR.style.opacity = canScrollR ? '1' : '0.25'; + btnR.style.pointerEvents = canScrollR ? '' : 'none'; + // hide both if no overflow + const hasOverflow = wrap.scrollWidth > wrap.clientWidth + 4; + btnL.style.display = hasOverflow ? '' : 'none'; + btnR.style.display = hasOverflow ? '' : 'none'; +} + +// ── Sidebar scroll-spy ──────────────────────────────────────────────────────── +function initSidebar() { + // Click → smooth scroll + document.querySelectorAll('.sidebar-nav-item[data-target]').forEach(item => { + item.addEventListener('click', e => { + e.preventDefault(); + const target = document.getElementById(item.dataset.target); + if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + }); + + // Search filter + document.getElementById('sidebar-search-input')?.addEventListener('input', e => { + const q = e.target.value.toLowerCase(); + document.querySelectorAll('.sidebar-nav-item').forEach(item => { + const text = item.textContent.toLowerCase(); + item.closest('li').style.display = text.includes(q) ? '' : 'none'; + }); + }); + + // Scroll-spy via IntersectionObserver + const sections = document.querySelectorAll('.page-section[id]'); + const navItems = document.querySelectorAll('.sidebar-nav-item[data-target]'); + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + navItems.forEach(item => item.classList.remove('active')); + const active = document.querySelector(`.sidebar-nav-item[data-target="${entry.target.id}"]`); + if (active) active.classList.add('active'); + } + }); + }, { rootMargin: '-20% 0px -70% 0px', threshold: 0 }); + sections.forEach(s => observer.observe(s)); +} + +// ── Add engine modal ────────────────────────────────────────────────────────── +// Famille → préfixe avant le premier "." +function _engineFamily(e) { const d = e.indexOf('.'); return d >= 0 ? e.slice(0, d) : e; } + +function _populateVersionSelect(family, subset) { + const engines = subset ?? [...new Set(allRuns.map(r => r.engine))]; + const versions = engines + .filter(e => _engineFamily(e) === family) + .map(e => { const d = e.indexOf('.'); return d >= 0 ? e.slice(d + 1) : e; }); + const sel = document.getElementById('modal-select-version'); + if (!sel) return; + sel.innerHTML = versions.map(v => ``).join(''); +} + +function openAddEngineModal() { + const overlay = document.getElementById('modal-add-engine'); + if (!overlay) return; + + // Familles disponibles dans les données (uniquement moteurs cachés) + const allEngines = [...new Set(allRuns.map(r => r.engine))]; + const available = hiddenEngines.size > 0 + ? allEngines.filter(e => hiddenEngines.has(e)) + : allEngines; + const families = [...new Set(available.map(_engineFamily))]; + + const triplesel = document.getElementById('modal-select-triplestore'); + if (triplesel) { + const labels = { corese: 'Corese', jena: 'Apache Jena', rdf4j: 'Eclipse RDF4J' }; + triplesel.innerHTML = families.map(f => + `` + ).join(''); + _populateVersionSelect(triplesel.value, available); + triplesel.onchange = () => _populateVersionSelect(triplesel.value, available); + } + + overlay.classList.add('active'); + overlay.addEventListener('click', e => { if (e.target === overlay) closeAddEngineModal(); }, { once: true }); +} + +function closeAddEngineModal() { + document.getElementById('modal-add-engine')?.classList.remove('active'); +} + +// ── W3C Conformance ─────────────────────────────────────────────────────────── +async function loadW3CData() { + try { + const res = await fetch('https://corese-stack.github.io/corese-w3c/data/w3c-report.json'); + if (!res.ok) throw new Error(res.statusText); + return await res.json(); + } catch (e) { + console.warn('W3C data unavailable:', e); + return null; + } +} + +function renderW3CSection(data) { + const section = document.getElementById('section-w3c'); + if (!section) return; + + if (!data) { + section.innerHTML = `

W3C Conformance

+

Data unavailable — check your connection.

`; + section.style.display = ''; + return; + } + + const { metadata, summary, suites } = data; + const engineVersion = metadata?.coreseVersion ?? metadata?.version ?? '—'; + const runDate = metadata?.date ?? metadata?.generatedAt ?? null; + + // ── Summary cards ── + const pct = summary.passRate?.toFixed(2) ?? '—'; + const exePct = summary.executedPassRate?.toFixed(2) ?? null; + + const summaryCards = [ + { label: 'Pass rate', value: `${pct}%`, sub: exePct ? `${exePct}% executed` : '', cls: 'w3c-card-pass' }, + { label: 'Passed', value: fmt(summary.passed), sub: `/ ${fmt(summary.total)} tests`, cls: '' }, + { label: 'Failed', value: fmt(summary.failed), sub: 'tests failed', cls: summary.failed > 0 ? 'w3c-card-fail' : '' }, + { label: 'Inapplicable', value: fmt(summary.inapplicable ?? summary.skipped ?? 0), sub: 'inapplicable / skipped', cls: '' }, + { label: 'Cannot tell', value: fmt(summary.cantTell ?? 0), sub: 'uncertain', cls: '' }, + ].map(c => ` +
+
${c.label}
+
${c.value}
+ ${c.sub ? `
${c.sub}
` : ''} +
`).join(''); + + // ── Pass rate bar ── + const barPct = Math.min(parseFloat(pct), 100); + const barColor = barPct >= 99 ? '#3fb950' : barPct >= 95 ? '#d29922' : '#f85149'; + const passBar = ` +
+
+
+
+ ${pct}% +
`; + + // ── Suite table ── + const suiteRows = suites.map(s => { + const suitePct = s.total > 0 ? (s.passed / s.total * 100).toFixed(1) : '—'; + const failCls = s.failed > 0 ? 'w3c-fail-cell' : ''; + return ` + ${s.name ?? s.id} + ${s.specification ?? s.component ?? '—'} + ${fmt(s.total)} + ${fmt(s.passed)} + ${fmt(s.failed)} + ${s.inapplicable ?? 0} + ${suitePct}% + `; + }).join(''); + + // ── Failed tests detail ── + const failedTests = suites.flatMap(s => + (s.tests ?? []).filter(t => t.status === 'FAILED' || t.outcome === 'FAILED') + .map(t => ({ suite: s.name ?? s.id, ...t })) + ); + const failedHtml = failedTests.length === 0 ? '' : ` +
+ + ${failedTests.length} failed test${failedTests.length > 1 ? 's' : ''} + +
+
+ + + + + + ${failedTests.map(t => ` + + + + + `).join('')} + +
SuiteTestDescriptionError
${t.suite}${t.name ?? t.testUri ?? '—'}${t.description ?? '—'}${t.errorMessage ?? '—'}
+
+
+
`; + + section.innerHTML = ` +
+

W3C Conformance

+ +
+ ${passBar} +
${summaryCards}
+
+
+ Results by suite + ${suites.length} suites +
+
+ + + + + + + + + + ${suiteRows} +
SuiteSpecificationTotalPassedFailedN/APass %
+
+
+ ${failedHtml} + `; + + section.style.display = ''; + const nav = document.getElementById('sidebar-w3c-nav'); + if (nav) { + nav.style.display = ''; + const badge = document.getElementById('sidebar-badge-w3c'); + if (badge) badge.textContent = `${pct}%`; + } +} + // ── Main ───────────────────────────────────────────────────────────────────── async function main() { let data; @@ -1000,17 +1916,32 @@ async function main() { } allRuns = data.runs; + currentData = data; + metadataQueries = data.metadata.queries ?? null; // Build color map from engines present in the data const engines = [...new Set(allRuns.map(r => r.engine))]; buildColorMap(engines); + // Restore state from URL before rendering (shareable links) + applyUrlState(); + renderHeader(data.metadata, allRuns); - renderRunInfo(data.metadata); + // Pills scroll arrows — listen once after pills are rendered + const pillsWrap = document.getElementById('engine-pills'); + if (pillsWrap) { + pillsWrap.addEventListener('scroll', updatePillsScrollBtns, { passive: true }); + new ResizeObserver(updatePillsScrollBtns).observe(pillsWrap); + } + updatePillsScrollBtns(); + renderDatasetPills(allRuns); + _syncDatasetSections(); + updateQuerySectionVisibility(); renderKPI(allRuns); - renderGlobalMeter(allRuns); - renderDatasetCards(allRuns, data.metadata.queries ?? null); + renderDatasetCards(allRuns, metadataQueries); renderEngineCards(allRuns); + renderSidebarMeta(data.metadata, engines, allRuns, data.queryRuns); + initSidebar(); applyStoredEngineOrder(); initCharts(); initTableSort(); @@ -1023,8 +1954,170 @@ async function main() { renderQuerySection(data.queryRuns); } - // Historical trend (async, non-blocking) - renderTrend(); + // W3C Conformance — async, non-blocking + loadW3CData().then(renderW3CSection); + + document.getElementById('btn-open-add-engine')?.addEventListener('click', openAddEngineModal); + document.getElementById('btn-modal-cancel')?.addEventListener('click', closeAddEngineModal); + document.getElementById('btn-modal-submit')?.addEventListener('click', () => { + const family = document.getElementById('modal-select-triplestore')?.value; + const version = document.getElementById('modal-select-version')?.value; + if (!family || !version) return; + const engineId = `${family}.${version}`; + if (hiddenEngines.has(engineId)) { + hiddenEngines.delete(engineId); + // Re-add pill to comparator bar + const wrap = document.getElementById('engine-pills'); + if (wrap && !wrap.querySelector(`[data-engine="${engineId}"]`)) { + const dot = engineId.indexOf('.'); + const fam = dot >= 0 ? engineId.slice(0, dot) : engineId; + const ver = dot >= 0 ? engineId.slice(dot + 1) : engineId; + const pill = document.createElement('div'); + pill.className = `engine-pill family-${fam}`; + pill.dataset.engine = engineId; + pill.title = engineId; + pill.draggable = true; + pill.style.cursor = 'grab'; + pill.onclick = () => toggleEngine(engineId); + pill.addEventListener('dragstart', pillDragStart); + pill.addEventListener('dragover', pillDragOver); + pill.addEventListener('drop', pillDrop); + pill.addEventListener('dragend', pillDragEnd); + pill.addEventListener('dragleave', pillDragLeave); + pill.innerHTML = `${fam}${ver} + `; + wrap.appendChild(pill); + } + syncEnginePills(); + const filtered = getFilteredRuns(allRuns); + renderKPI(filtered); + renderEngineCards(getDatasetFilteredRuns()); + updateCharts(); + updateTable(); + } + closeAddEngineModal(); + }); + +} + +// ── Copy URL ────────────────────────────────────────────────────────────────── +// ── URL shareable state ─────────────────────────────────────────────────────── +// Encodes: ?ds=dbpedia&engines=corese.4.6.5,jena.6.2.0&ref=corese.4.6.5 +// Called after any state change that should be reflected in the URL. + +function pushStateToUrl() { + const params = new URLSearchParams(); + if (activeDatasets.size > 0) { + params.set('ds', [...activeDatasets].join(',')); + } + const visibleEngines = getEngineOrder().filter(e => !hiddenEngines.has(e)); + const allEngines = [...new Set(allRuns.map(r => r.engine))]; + // Only encode if different from "all engines" + if (hiddenEngines.size > 0) { + params.set('engines', visibleEngines.join(',')); + } + if (activeEngines.size > 0) { + params.set('filter', [...activeEngines].join(',')); + } + if (userRefEngine) { + params.set('ref', userRefEngine); + } + const qs = params.toString(); + const newUrl = qs ? `${location.pathname}?${qs}` : location.pathname; + history.replaceState(null, '', newUrl); +} + +function applyUrlState() { + const params = new URLSearchParams(location.search); + + // Dataset + const ds = params.get('ds'); + if (ds) { + activeDatasets.clear(); + ds.split(',').forEach(d => d && activeDatasets.add(d)); + } + + // Hidden engines (engines= lists visible ones; those absent are hidden) + const enginesParam = params.get('engines'); + if (enginesParam) { + const visible = new Set(enginesParam.split(',').filter(Boolean)); + const all = [...new Set(allRuns.map(r => r.engine))]; + hiddenEngines.clear(); + all.forEach(e => { if (!visible.has(e)) hiddenEngines.add(e); }); + } + + // Engine filter (active selection) + const filter = params.get('filter'); + if (filter) { + activeEngines.clear(); + filter.split(',').forEach(e => e && activeEngines.add(e)); + } + + // Reference engine + const ref = params.get('ref'); + if (ref) userRefEngine = ref; +} + +// ── Export JSON (filtered view) ─────────────────────────────────────────────── + +function exportFilteredJson() { + const runs = getFilteredRuns(allRuns); + const queryRuns = (allQueryRuns ?? []).filter(r => + !hiddenEngines.has(r.engine) && + (activeEngines.size === 0 || activeEngines.has(r.engine)) && + (activeDatasets.size === 0 || activeDatasets.has(r.dataset)) + ); + if (runs.length === 0 && queryRuns.length === 0) return; + + const payload = { metadata: currentData?.metadata ?? {}, runs }; + if (queryRuns.length > 0) payload.queryRuns = queryRuns; + + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `benchmark-filtered-${new Date().toISOString().slice(0,10)}.json`; + a.click(); + URL.revokeObjectURL(url); +} + +// ── Export CSV (filtered view) ──────────────────────────────────────────────── + +function exportFilteredCsv() { + const runs = getFilteredRuns(allRuns); + const queryRuns = (allQueryRuns ?? []).filter(r => + !hiddenEngines.has(r.engine) && + (activeEngines.size === 0 || activeEngines.has(r.engine)) && + (activeDatasets.size === 0 || activeDatasets.has(r.dataset)) + ); + if (runs.length === 0 && queryRuns.length === 0) return; + + const escape = v => `"${String(v ?? '').replace(/"/g, '""')}"`; + const lines = []; + + if (runs.length > 0) { + const hdr = ['engine', 'dataset', 'file', 'triples', 'loadingTimeMs', 'memoryDeltaMb', 'cpuPercent']; + lines.push('# LOAD BENCHMARK'); + lines.push(hdr.join(',')); + runs.forEach(r => lines.push(hdr.map(h => escape(r[h])).join(','))); + } + + if (queryRuns.length > 0) { + if (lines.length) lines.push(''); + lines.push('# QUERY BENCHMARK'); + lines.push(['engine', 'dataset', 'queryId', 'medianMs'].join(',')); + queryRuns.forEach(r => + lines.push([escape(r.engine), escape(r.dataset), escape(r.queryId), escape(r.medianMs)].join(',')) + ); + } + + const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `benchmark-filtered-${new Date().toISOString().slice(0,10)}.csv`; + a.click(); + URL.revokeObjectURL(url); } main(); diff --git a/site/index.html b/site/index.html index 79fcfd4..ecc171f 100644 --- a/site/index.html +++ b/site/index.html @@ -15,183 +15,323 @@ - -
- Run info - Generated— - Java— - OS— - Protocol— -
- - -
- -
- - -
-
- -
-
+ +
- -
- -

Datasets

- -
-
- -
-
+ + -
+ +
- -

Performance

-
+ +
+
+

Overview

+ +
+ +
+ +
+ +
+
+
+ Avg loading time by engine + ms — lower is better +
+ +
+
+
+ Performance profile + normalized score — higher is better +
+ +
+
+
-
-
- Throughput - triples/s — higher is better - -
- -
+ +
+
+

Datasets

+ +
+
+ +
+
+ + +
+
+

Engines tested

+ +
+

Click one or more engines to filter the charts

+
+ +
+
+ + +
+
+

Performance

+ +
+
+ +
+
+ Loading time + ms — lower is better + + +
+ +
+ +
+
+ Memory + ΔMB — lower is better + + +
+ +
+ +
+
+ CPU + % — lower is better + + +
+ +
+ +
+
-
-
- Loading time - ms — lower is better - + +
+
+

Full results

+ +
+ + +
+
+
+
+ + + + + + + + + + + + + + +
EngineFileTriplesLoading (ms)Throughput (t/s)Memory ΔMBCPU %Repetitions (ms)
+
+
+
+ + + + + + + +
+
+ + +
+
+ +
+ + +
- +
+
-
- -
- -

Full results

- -
-
-
- - - - - - - - - - - - - - -
EngineFileTriplesLoading (ms)Throughput (t/s)Memory ΔMBCPU %Repetitions (ms)
+ +
+
@@ -204,7 +344,6 @@

Full results

- diff --git a/site/styles.css b/site/styles.css index 4fff00e..043d0bc 100644 --- a/site/styles.css +++ b/site/styles.css @@ -212,7 +212,7 @@ body { /* ── Metrics grid (KPI) ───────────────────────────────────────────────────── */ .metrics-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px; } @@ -229,6 +229,18 @@ body { border-color: var(--text-faint); } +/* Winner cards */ +.kpi-winner-card { display: flex; flex-direction: column; gap: 4px; } +.kpi-winner-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-faint); } +.kpi-winner-engine { display: flex; align-items: center; gap: 6px; font-size: 13px; margin-top: 4px; } +.kpi-winner-value { font-size: 20px; font-weight: 700; color: var(--text-main); margin-top: 2px; } + +/* Context card */ +.kpi-context-card { display: flex; flex-direction: column; justify-content: center; gap: 8px; } +.kpi-context-row { display: flex; align-items: baseline; gap: 6px; } +.kpi-context-num { font-size: 22px; font-weight: 700; color: var(--text-main); } +.kpi-context-lbl { font-size: 11px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.4px; } + .metric-label { font-size: 11px; font-weight: 700; @@ -290,7 +302,7 @@ body { /* ── Engine cards (suite-item style) ─────────────────────────────────────── */ .suites-list { display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; } @@ -350,7 +362,8 @@ body { .suite-item.dragging { opacity: 0.4; } .suite-item.drag-over { outline: 2px dashed var(--color-accent); outline-offset: 2px; } -.dataset-card { cursor: default; } +.dataset-card { cursor: pointer; } +.dataset-card.active { border-color: var(--accent); background: var(--bg-surface-active); } .dataset-card:hover { transform: none; box-shadow: none; } .dataset-desc { @@ -497,9 +510,6 @@ body { gap: 12px; } -.charts-grid--sm .chart-surface canvas { - max-height: 180px; -} @media (max-width: 800px) { .charts-grid { grid-template-columns: 1fr; } } @@ -555,7 +565,8 @@ body { overflow: hidden; } -.table-scroll { overflow-x: auto; } +.table-scroll { overflow-x: auto; overflow-y: auto; max-height: 520px; } +#section-results .table-scroll { max-height: 60vh; } .report-table { width: 100%; @@ -564,6 +575,9 @@ body { } .report-table thead th { + position: sticky; + top: 0; + z-index: 2; background: var(--bg-surface-hover); padding: 9px 12px; text-align: left; @@ -717,9 +731,870 @@ body { .page-footer a { color: var(--accent); text-decoration: none; } .page-footer a:hover { text-decoration: underline; } +/* ── Dataset filter pills (header) ───────────────────────────────────────── */ +.header-pills-row { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; + margin-top: 6px; +} +.engine-pills { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} +.engine-family-group { + display: flex; + align-items: center; + gap: 5px; +} +.engine-family-label { + font-size: 11px; + font-weight: 600; + font-family: var(--font-mono); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-right: 2px; + white-space: nowrap; +} +.engine-pill { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 12px; + color: var(--text-muted); + font-family: var(--font-mono); + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 3px 10px; + cursor: pointer; + transition: all 0.15s; + user-select: none; +} +.engine-pill:hover { + border-color: var(--accent); + color: var(--text-main); +} +.engine-pill.ep-active { + border-color: var(--accent); + background: var(--bg-surface-active); + color: var(--text-main); +} +.engine-pill.ep-dimmed { + opacity: 0.4; +} +.pill-sign { + font-size: 13px; + font-weight: 700; + line-height: 1; + margin-left: 2px; + color: var(--accent); +} +.engine-pill.ep-dimmed .pill-sign { color: var(--text-faint); } +.dataset-filter-pills { + display: flex; + gap: 6px; + flex-wrap: wrap; +} +.dataset-pill { + display: inline-flex; + align-items: center; + font-size: 11px; + font-weight: 600; + padding: 3px 12px; + border-radius: 12px; + border: 1px solid var(--border-color); + background: var(--bg-surface); + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s; +} +.dataset-pill:hover { + background: var(--bg-surface-hover); + border-color: var(--accent); + color: var(--text-main); +} +.dataset-pill.active { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +/* ── Chart zoom modal ─────────────────────────────────────────────────────── */ +.chart-zoom-dialog { + position: fixed; + top: 50%; left: 50%; + transform: translate(-50%, -50%); + width: min(92vw, 1100px); + max-height: 90vh; + margin: 0; + padding: 0; + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: 10px; + box-shadow: 0 12px 48px rgba(0,0,0,0.6); + overflow: hidden; +} +.chart-zoom-dialog::backdrop { + background: rgba(0,0,0,0.65); +} +.chart-zoom-inner { + display: flex; + flex-direction: column; + height: 80vh; + padding: 16px 20px 20px; + box-sizing: border-box; +} +.chart-zoom-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + flex-shrink: 0; +} +.chart-zoom-title { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); +} +.chart-zoom-actions { + display: flex; + align-items: center; + gap: 8px; +} +#chart-zoom-canvas { + flex: 1; + min-height: 0; +} + +/* ── Dataset modal ────────────────────────────────────────────────────────── */ +.dataset-modal-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 0; + min-width: 380px; + max-width: 540px; + width: 90vw; + color: var(--text-main); + box-shadow: 0 8px 32px rgba(0,0,0,0.4); +} +.dataset-modal-dialog::backdrop { + background: rgba(0,0,0,0.55); +} +.modal-inner { + padding: 20px 24px 24px; +} +.modal-top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + border-bottom: 1px solid var(--border-subtle); + padding-bottom: 12px; +} +.modal-title { + font-size: 15px; + font-weight: 700; +} +.modal-close { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 16px; + line-height: 1; + padding: 2px 6px; + border-radius: 4px; + transition: color 0.15s; +} +.modal-close:hover { color: var(--text-main); } +.modal-info-grid { + display: flex; + flex-direction: column; + gap: 10px; +} +.info-row { + display: grid; + grid-template-columns: 100px 1fr; + gap: 8px; + font-size: 12px; + align-items: start; +} +.info-label { + font-weight: 700; + color: var(--text-faint); + text-transform: uppercase; + font-size: 10px; + letter-spacing: .05em; + padding-top: 2px; +} +.info-code { + font-family: var(--font-mono); + font-size: 11px; +} +.modal-link { + color: var(--accent); + font-size: 11px; + word-break: break-all; + text-decoration: none; +} +.modal-link:hover { text-decoration: underline; } + +.modal-queries-details { + margin-top: 14px; + border-top: 1px solid var(--border-subtle); + padding-top: 10px; +} +.modal-queries-summary { + list-style: none; + cursor: pointer; + font-size: 12px; + color: var(--accent); + display: flex; + align-items: center; + gap: 5px; + user-select: none; +} +.modal-queries-summary::-webkit-details-marker { display: none; } +.modal-queries-summary svg { transition: transform 0.2s; } +.modal-queries-details[open] .modal-queries-summary svg { transform: rotate(90deg); } +.modal-queries-list { + margin-top: 8px; + display: flex; + flex-direction: column; + gap: 6px; + max-height: 320px; + overflow-y: auto; +} + +.modal-files-list { + display: flex; + flex-direction: column; + gap: 2px; + margin-top: 14px; + border-top: 1px solid var(--border-subtle); + padding-top: 12px; +} +.modal-file-link { + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--accent); + font-size: 12px; + font-family: var(--font-mono); + text-decoration: none; + padding: 4px 0; + transition: color 0.15s; +} +.modal-file-link:hover { color: var(--accent-hover); text-decoration: underline; } +.modal-file-link--plain { color: var(--text-muted); cursor: default; } +.modal-file-link--plain:hover { text-decoration: none; color: var(--text-muted); } + +/* ── Reference engine button ──────────────────────────────────────────────── */ +.btn-set-ref { + background: none; + border: none; + color: var(--text-faint); + cursor: pointer; + font-size: 15px; + line-height: 1; + padding: 0 4px; + transition: color 0.15s; +} +.btn-set-ref:hover { color: var(--text-main); } +.btn-set-ref.is-ref { color: #f1c232; } + +.dataset-card { cursor: pointer !important; } + +/* ── W3C Conformance section ─────────────────────────────────────────────── */ +.w3c-pass-bar-wrap { + display: flex; + align-items: center; + gap: 12px; + margin-top: 14px; +} +.w3c-pass-bar-track { + flex: 1; + height: 8px; + background: var(--border-color); + border-radius: 4px; + overflow: hidden; +} +.w3c-pass-bar-fill { + height: 100%; + border-radius: 4px; + transition: width 0.6s ease; +} +.w3c-pass-bar-label { + font-size: 13px; + font-weight: 700; + font-family: var(--font-mono); + min-width: 52px; + text-align: right; +} +.w3c-card-pass .kpi-winner-value { color: #3fb950; } +.w3c-card-fail .kpi-winner-value { color: #f85149; } +.w3c-fail-cell { color: #f85149; font-weight: 600; } +.w3c-pct-cell { font-weight: 600; font-family: var(--font-mono); } +.text-muted-cell { color: var(--text-muted); font-size: 11px; white-space: nowrap; } +#section-w3c .report-table td, #section-w3c .report-table th { white-space: nowrap; } +.w3c-failed-details { margin-top: 16px; } + /* ── Responsive ───────────────────────────────────────────────────────────── */ @media (max-width: 768px) { .header-top-row { flex-direction: column; align-items: flex-start; } .metrics-grid { grid-template-columns: repeat(2, 1fr); } + .metrics-grid > :nth-child(4) { grid-column: span 2; } .run-info-bar { flex-direction: column; align-items: flex-start; gap: 8px; } } + +/* ── New layout: header / sidebar / content ─── */ +.app-wrapper { + /* override: remove old max-width + padding — layout now uses sidebar */ + max-width: none; + padding: 0; + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.page-header { + display: flex; + flex-direction: column; + padding: 0 20px; + position: sticky; + top: 0; + z-index: 100; + background-color: var(--bg-surface); + border-bottom: 1px solid var(--border-color); + padding-bottom: 0; + margin-bottom: 0; +} + +.header-top-row { + display: flex; + align-items: center; + gap: 16px; + height: 44px; +} + +.header-bottom-row { + display: flex; + align-items: stretch; + padding: 0; + border-top: 1px solid var(--border-color); + height: 34px; +} + +.header-brand { flex-shrink: 0; } +.brand-title { font-size: 15px; font-weight: 700; letter-spacing: -0.2px; white-space: nowrap; } + +.comparator-bar { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 0; + background: var(--bg-page); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 3px 10px; + overflow: hidden; +} + +.dataset-filter-bar { + display: flex; + align-items: stretch; + gap: 0; + background: transparent; + border: none; + padding: 0; + flex: 1; + min-width: 0; +} + +.dataset-pills-wrap { + display: flex; + align-items: stretch; + gap: 0; +} + +.dataset-pill { + display: inline-flex; + align-items: center; + padding: 0 16px; + font-size: 12px; + font-weight: 500; + border: none; + border-right: 1px solid var(--border-color); + background: transparent; + color: var(--text-muted); + cursor: pointer; + white-space: nowrap; + transition: color 0.15s, background 0.15s; + position: relative; +} +.dataset-pill:first-child { border-left: 1px solid var(--border-color); } +.dataset-pill:hover { color: var(--text-main); background: var(--bg-surface-hover); } +.dataset-pill.active { + color: var(--text-main); + background: var(--bg-surface-active); + font-weight: 600; +} +.dataset-pill.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: var(--accent); +} + +.comparator-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + color: var(--text-faint); + letter-spacing: 0.5px; + white-space: nowrap; +} + +.engine-pills-wrap { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: nowrap; + overflow-x: auto; + flex: 1; + min-width: 0; + scrollbar-width: none; +} +.engine-pills-wrap::-webkit-scrollbar { display: none; } + +.pills-scroll-btn { + flex-shrink: 0; + display: none; /* shown by JS when overflow */ + background: none; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-secondary); + font-size: 16px; + line-height: 1; + padding: 1px 6px 3px; + cursor: pointer; + transition: opacity 0.15s, color 0.15s; +} +.pills-scroll-btn:hover { color: var(--text-main); } + +/* engine pills in comparator bar — reuse .engine-pill class */ +.engine-pills-wrap .engine-pill { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 8px; + border-radius: var(--radius-sm); + font-size: 11px; + font-weight: 600; + border: 1px solid transparent; + white-space: nowrap; + cursor: pointer; + transition: opacity 0.15s, transform 0.15s; + user-select: none; +} +.engine-pills-wrap .engine-pill:hover { transform: translateY(-1px); } +.engine-pills-wrap .engine-pill.ep-dimmed { opacity: 0.35; } +.engine-pills-wrap .engine-pill.ep-active { box-shadow: 0 0 0 2px currentColor; } +.engine-pills-wrap .engine-pill.pill-dragging { opacity: 0.4; cursor: grabbing; } +.engine-pills-wrap .engine-pill.pill-drag-over { outline: 2px dashed currentColor; outline-offset: 2px; } + +.engine-pills-wrap .pill-version { + font-family: var(--font-mono); + font-size: 10px; + background: rgba(0,0,0,0.08); + padding: 1px 4px; + border-radius: 2px; +} +[data-theme="dark"] .engine-pills-wrap .pill-version, +:root:not([data-theme="light"]) .engine-pills-wrap .pill-version { background: rgba(255,255,255,0.12); } + +.engine-pills-wrap .pill-remove-btn { + background: none; + border: none; + cursor: pointer; + color: inherit; + opacity: 0.55; + font-size: 13px; + line-height: 1; + padding: 0 1px; + display: flex; + align-items: center; +} +.engine-pills-wrap .pill-remove-btn:hover { opacity: 1; } + +.btn-add-engine-trigger { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 9px; + border: 1px dashed var(--border-color); + border-radius: var(--radius-sm); + background: var(--bg-surface); + color: var(--text-muted); + font-size: 11px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + flex-shrink: 0; + transition: all 0.15s; +} +.btn-add-engine-trigger:hover { border-color: var(--accent); color: var(--accent); } + +.header-right { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; +} + +/* ── App body: sidebar + content ─── */ +.app-body { + display: flex; + flex: 1; + min-height: 0; +} + +.app-sidebar { + width: 220px; + flex-shrink: 0; + background: var(--bg-surface); + border-right: 1px solid var(--border-color); + padding: 16px 12px; + display: flex; + flex-direction: column; + gap: 16px; + position: sticky; + top: 56px; + height: calc(100vh - 56px); + overflow-y: auto; +} + +.sidebar-search-box input { + width: 100%; + padding: 6px 10px; + background: var(--bg-page); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-main); + font-size: 12px; + font-family: var(--font-sans); + outline: none; + transition: border-color 0.15s; +} +.sidebar-search-box input:focus { border-color: var(--accent); } + +.sidebar-group { display: flex; flex-direction: column; gap: 2px; } + +.sidebar-heading { + font-size: 10.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--text-faint); + padding: 0 8px; + margin-bottom: 4px; +} + +.sidebar-nav-list { list-style: none; display: flex; flex-direction: column; gap: 1px; } + +.sidebar-nav-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + border-radius: var(--radius-sm); + color: var(--text-muted); + font-size: 12px; + font-weight: 500; + cursor: pointer; + border-left: 2px solid transparent; + text-decoration: none; + transition: all 0.1s; + user-select: none; +} +.sidebar-nav-item:hover { background: var(--bg-surface-hover); color: var(--text-main); } +.sidebar-nav-item.active { + background: var(--bg-surface-active); + color: var(--accent); + font-weight: 600; + border-left-color: var(--accent); +} + +.sidebar-item-badge { + font-family: var(--font-mono); + font-size: 10px; + padding: 1px 5px; + border-radius: var(--radius-sm); + background: var(--bg-page); + border: 1px solid var(--border-color); + color: var(--text-faint); + white-space: nowrap; +} + +.sidebar-footer-box { + margin-top: auto; + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + background: var(--bg-page); + font-size: 11px; + color: var(--text-muted); + line-height: 1.6; +} +.sidebar-footer-box strong { color: var(--text-main); display: block; margin-bottom: 3px; font-size: 11px; } +.sidebar-footer-box code { font-family: var(--font-mono); font-size: 10px; color: var(--accent); } + +.meta-box-title { + display: block; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .06em; + color: var(--text-muted); + margin-bottom: 6px; +} +.meta-box-grid { + display: grid; + grid-template-columns: auto 1fr; + column-gap: 10px; + row-gap: 3px; + margin: 0; + padding: 0; +} +.meta-box-grid dt { + color: var(--text-muted); + font-weight: 500; + white-space: nowrap; +} +.meta-box-grid dd { + margin: 0; + color: var(--text-main); + word-break: break-word; +} +.meta-box-grid code { font-family: var(--font-mono); font-size: 10px; color: var(--accent); } + +/* ── Main content area ─── */ +.app-content { + flex: 1; + min-width: 0; + padding: 28px 32px 60px; + overflow-y: auto; +} + +.page-section { + margin-bottom: 48px; + scroll-margin-top: 72px; /* header height + 16 */ +} + +.section-head-bar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; + padding-bottom: 10px; + border-bottom: 1px solid var(--border-color); +} + +.section-meta { + font-size: 11px; + color: var(--text-faint); + font-family: var(--font-mono); +} + +/* ── Add engine modal overlay ─── */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + backdrop-filter: blur(2px); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s; +} +.modal-overlay.active { opacity: 1; pointer-events: auto; } + +.modal-card { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + width: 100%; + max-width: 400px; + padding: 20px 22px; + box-shadow: 0 8px 24px rgba(0,0,0,0.25); +} +.modal-card-title { font-size: 15px; font-weight: 700; color: var(--text-main); margin-bottom: 4px; } +.modal-card-desc { font-size: 12px; color: var(--text-muted); margin-bottom: 14px; } + +.modal-engine-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 6px; + max-height: 300px; + overflow-y: auto; + margin-bottom: 16px; +} +.modal-engine-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + background: var(--bg-page); + cursor: pointer; + transition: all 0.12s; +} +.modal-engine-item:hover { border-color: var(--accent); background: var(--bg-surface-hover); } +.modal-engine-item.active { border-color: var(--accent); background: var(--bg-surface-active); } +.modal-engine-name { font-size: 12px; font-weight: 600; color: var(--text-main); display: flex; align-items: center; gap: 8px; } +.modal-engine-status { font-size: 10px; font-family: var(--font-mono); color: var(--text-faint); } +.modal-engine-item.active .modal-engine-status { color: var(--accent); } + +.modal-form-group { margin-bottom: 14px; } +.modal-form-label { + display: block; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-faint); + margin-bottom: 6px; +} +.modal-form-select { + width: 100%; + padding: 7px 10px; + background: var(--bg-page); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-main); + font-size: 13px; + font-family: var(--font-sans); + outline: none; + cursor: pointer; + transition: border-color 0.15s; +} +.modal-form-select:focus { border-color: var(--accent); } +.modal-card-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; } +.btn-modal-cancel { + padding: 6px 14px; + background: var(--bg-page); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-main); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.12s; +} +.btn-modal-cancel:hover { background: var(--bg-surface-hover); } +.btn-modal-submit { + padding: 6px 16px; + background: var(--accent); + border: 1px solid transparent; + border-radius: var(--radius-sm); + color: #fff; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.12s; +} +.btn-modal-submit:hover { opacity: 0.88; } + +/* engine pill colors in comparator bar */ +.engine-pills-wrap .engine-pill.family-corese { background: var(--corese-bg); border-color: var(--corese-color); color: var(--corese-color); } +.engine-pills-wrap .engine-pill.family-jena { background: var(--jena-bg); border-color: var(--jena-color); color: var(--jena-color); } +.engine-pills-wrap .engine-pill.family-rdf4j { background: var(--rdf4j-bg); border-color: var(--rdf4j-color); color: var(--rdf4j-color); } + +/* ── Overview section ─────────────────────────────────────────────────────── */ +.metric-sub-label { + font-size: 11px; + color: var(--text-faint); + margin-bottom: 10px; +} + +.kpi-breakdown { + display: flex; + flex-direction: column; + gap: 6px; +} + +.kpi-breakdown-row { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; +} + +.kpi-breakdown-name { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-muted); + font-weight: 500; +} + +.kpi-breakdown-val { + font-family: var(--font-mono); + font-weight: 600; + font-size: 12px; + color: var(--text-main); +} + +.kpi-best { + color: #3fb950; + background: rgba(63,185,80,0.1); + border: 1px solid rgba(63,185,80,0.25); + padding: 1px 6px; + border-radius: var(--radius-sm); +} + +[data-theme="light"] .kpi-best { + color: #1a7f37; + background: rgba(26,127,55,0.08); + border-color: rgba(26,127,55,0.2); +} + +.kpi-single { gap: 0; } +.kpi-big { + font-size: 28px; + font-weight: 700; + font-family: var(--font-mono); + line-height: 1; + color: var(--text-main); +} + +.overview-charts-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; + margin-top: 16px; +} + +@media (max-width: 750px) { + .overview-charts-grid { grid-template-columns: 1fr; } +} diff --git a/src/main/java/fr/inria/corese/benchmark/Main.java b/src/main/java/fr/inria/corese/benchmark/Main.java index 5f34898..b5409f9 100644 --- a/src/main/java/fr/inria/corese/benchmark/Main.java +++ b/src/main/java/fr/inria/corese/benchmark/Main.java @@ -52,12 +52,18 @@ public static void main(String[] args) { 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; + // Parse remaining args: optional queries file and flags + String queriesFile = null; + boolean inference = false; + boolean quick = false; + int queryTimeoutSecs = -1; // -1 = use harness default for (int i = 4; i < args.length; i++) { if ("--inference".equals(args[i])) { inference = true; + } else if ("--quick".equals(args[i])) { + quick = true; + } else if ("--query-timeout".equals(args[i]) && i + 1 < args.length) { + try { queryTimeoutSecs = Integer.parseInt(args[++i]); } catch (NumberFormatException ignored) {} } else if (queriesFile == null) { queriesFile = args[i]; } @@ -83,7 +89,10 @@ public static void main(String[] args) { log.info("Starting engine: {}", engineName); try { TripleStoreDriver driver = DriverFactory.create(engineName, inference); - BenchmarkHarness harness = new BenchmarkHarness(driver, datasetName); + int reps = quick ? 1 : 3; + BenchmarkHarness harness = queryTimeoutSecs > 0 + ? new BenchmarkHarness(driver, datasetName, reps, queryTimeoutSecs) + : new BenchmarkHarness(driver, datasetName, reps); // Load benchmark List fileResults = harness.run(datasetDir); @@ -96,7 +105,7 @@ public static void main(String[] args) { } } catch (OutOfMemoryError e) { System.gc(); - log.error("Engine {} ran out of memory — skipping to next engine", engineName, e); + log.error("Engine {} ran out of memory on dataset '{}' — skipping to next engine", engineName, datasetName, e); } catch (Exception e) { log.error("Engine {} failed: {} — skipping to next engine", engineName, e.getMessage(), e); } diff --git a/src/main/java/fr/inria/corese/benchmark/driver/CoreseDriver.java b/src/main/java/fr/inria/corese/benchmark/driver/CoreseDriver.java index 69c38f5..cc149f4 100644 --- a/src/main/java/fr/inria/corese/benchmark/driver/CoreseDriver.java +++ b/src/main/java/fr/inria/corese/benchmark/driver/CoreseDriver.java @@ -63,6 +63,16 @@ public long query(String sparql) throws Exception { return System.currentTimeMillis() - start; } + /** Executes a SPARQL query and returns {@code [timeMs, resultCount]}. */ + @Override + public long[] queryWithCount(String sparql) throws Exception { + QueryProcess exec = QueryProcess.create(graph); + long start = System.currentTimeMillis(); + var maps = exec.query(sparql); + long elapsed = System.currentTimeMillis() - start; + return new long[]{ elapsed, maps != null ? maps.size() : -1L }; + } + /** @return number of triples in the graph */ @Override public long getGraphSize() { diff --git a/src/main/java/fr/inria/corese/benchmark/driver/CoreseNextDriver.java b/src/main/java/fr/inria/corese/benchmark/driver/CoreseNextDriver.java index b5c1ea1..8d3bd37 100644 --- a/src/main/java/fr/inria/corese/benchmark/driver/CoreseNextDriver.java +++ b/src/main/java/fr/inria/corese/benchmark/driver/CoreseNextDriver.java @@ -128,6 +128,28 @@ public long query(String sparql) { return System.currentTimeMillis() - start; } + /** Executes a SPARQL query and returns {@code [timeMs, resultCount]} (SELECT only). */ + @Override + public long[] queryWithCount(String sparql) { + long start = System.currentTimeMillis(); + long count = -1L; + switch (detectQueryType(sparql)) { + case SELECT -> { + try (TupleQueryResult result = connection.prepareTupleQuery(sparql).evaluate()) { + count = 0; + while (result.hasNext()) { result.next(); count++; } + } + } + case ASK -> connection.prepareBooleanQuery(sparql).evaluate(); + default -> { + try (GraphQueryResult result = connection.prepareGraphQuery(sparql).evaluate()) { + while (result.hasNext()) result.next(); + } + } + } + return new long[]{ System.currentTimeMillis() - start, count }; + } + /** * Returns the total number of statements currently held in the repository. * diff --git a/src/main/java/fr/inria/corese/benchmark/driver/JenaDriver.java b/src/main/java/fr/inria/corese/benchmark/driver/JenaDriver.java index 8fdffb7..a39da61 100644 --- a/src/main/java/fr/inria/corese/benchmark/driver/JenaDriver.java +++ b/src/main/java/fr/inria/corese/benchmark/driver/JenaDriver.java @@ -71,6 +71,27 @@ public long query(String sparql) { return System.currentTimeMillis() - start; } + /** Executes a SPARQL SELECT query and returns {@code [timeMs, resultCount]}. */ + @Override + public long[] queryWithCount(String sparql) { + long start = System.currentTimeMillis(); + long count = -1L; + try (QueryExecution qe = QueryExecution.model(model).query(sparql).build()) { + if (qe.getQuery().isSelectType()) { + ResultSet rs = qe.execSelect(); + count = 0; + while (rs.hasNext()) { rs.next(); count++; } + } else if (qe.getQuery().isConstructType()) { + qe.execConstruct(); + } else if (qe.getQuery().isAskType()) { + qe.execAsk(); + } else if (qe.getQuery().isDescribeType()) { + qe.execDescribe(); + } + } + return new long[]{ System.currentTimeMillis() - start, count }; + } + /** * Returns the number of statements currently held in the Jena model. * diff --git a/src/main/java/fr/inria/corese/benchmark/driver/RDF4JDriver.java b/src/main/java/fr/inria/corese/benchmark/driver/RDF4JDriver.java index 3eb38de..4d753c8 100644 --- a/src/main/java/fr/inria/corese/benchmark/driver/RDF4JDriver.java +++ b/src/main/java/fr/inria/corese/benchmark/driver/RDF4JDriver.java @@ -130,6 +130,21 @@ public long query(String sparql) { return System.currentTimeMillis() - start; } + /** Executes a SPARQL SELECT query and returns {@code [timeMs, resultCount]}. */ + @Override + public long[] queryWithCount(String sparql) { + long start = System.currentTimeMillis(); + long count = -1L; + try (RepositoryConnection conn = repository.getConnection()) { + TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, sparql); + try (TupleQueryResult result = q.evaluate()) { + count = 0; + while (result.hasNext()) { result.next(); count++; } + } + } + return new long[]{ System.currentTimeMillis() - start, count }; + } + /** * Returns the total number of statements held in the repository. * diff --git a/src/main/java/fr/inria/corese/benchmark/driver/TripleStoreDriver.java b/src/main/java/fr/inria/corese/benchmark/driver/TripleStoreDriver.java index ac742bd..6f1673d 100644 --- a/src/main/java/fr/inria/corese/benchmark/driver/TripleStoreDriver.java +++ b/src/main/java/fr/inria/corese/benchmark/driver/TripleStoreDriver.java @@ -15,6 +15,17 @@ public interface TripleStoreDriver { /** Execute a SPARQL query and return execution time in milliseconds. */ long query(String sparql) throws Exception; + /** + * Execute a SPARQL query and return both execution time and result row count. + * Returns {@code long[]{timeMs, resultCount}} where {@code resultCount} is -1 + * if the driver does not support counting (e.g. non-SELECT queries). + * Default implementation calls {@link #query(String)} and returns count -1. + * Override to provide real counts for warmup validation. + */ + default long[] queryWithCount(String sparql) throws Exception { + return new long[]{ query(sparql), -1L }; + } + /** Return the number of triples currently in the store. */ long getGraphSize() throws Exception; diff --git a/src/main/java/fr/inria/corese/benchmark/harness/BenchmarkHarness.java b/src/main/java/fr/inria/corese/benchmark/harness/BenchmarkHarness.java index 7eae723..c0f8d0d 100644 --- a/src/main/java/fr/inria/corese/benchmark/harness/BenchmarkHarness.java +++ b/src/main/java/fr/inria/corese/benchmark/harness/BenchmarkHarness.java @@ -28,9 +28,11 @@ public class BenchmarkHarness { private static final Logger log = LoggerFactory.getLogger(BenchmarkHarness.class); - private static final int REPETITIONS = 3; + private static final int DEFAULT_REPETITIONS = 3; + private final int repetitions; private static final int QUERY_REPS = 3; - private static final long QUERY_TIMEOUT_S = 30; // seconds per query execution + private static final long DEFAULT_QUERY_TIMEOUT_S = 30; // seconds per query execution + private final long queryTimeoutS; private final TripleStoreDriver driver; private final MetricsCollector metrics; @@ -43,8 +45,18 @@ public class BenchmarkHarness { * @param datasetName logical name of the dataset (recorded in result objects) */ public BenchmarkHarness(TripleStoreDriver driver, String datasetName) { + this(driver, datasetName, DEFAULT_REPETITIONS, DEFAULT_QUERY_TIMEOUT_S); + } + + public BenchmarkHarness(TripleStoreDriver driver, String datasetName, int repetitions) { + this(driver, datasetName, repetitions, DEFAULT_QUERY_TIMEOUT_S); + } + + public BenchmarkHarness(TripleStoreDriver driver, String datasetName, int repetitions, long queryTimeoutS) { this.driver = driver; this.datasetName = datasetName; + this.repetitions = repetitions; + this.queryTimeoutS = queryTimeoutS; this.metrics = new MetricsCollector(); } @@ -77,14 +89,19 @@ public List run(String datasetDir) throws Exception { FileResult result = benchmarkFile(file); results.add(result); long[] reps = result.repetitionsMs(); - String repStr = String.format("[%d, %d, %d]", reps[0], reps[1], reps[2]); + StringBuilder sb = new StringBuilder("["); + for (int r = 0; r < reps.length; r++) { if (r > 0) sb.append(", "); sb.append(reps[r]); } + sb.append("]"); + String repStr = sb.toString(); log.info(" [{}/{}] {}", fi + 1, files.length, String.format(Locale.US, "%-45s | %6d ms %s | %,9d triples | %8.1f MB | %,.0f triples/s", file.getName(), result.loadingTimeMs(), repStr, result.triples(), result.memoryDeltaMb(), result.throughputTriplesPerSec())); } catch (OutOfMemoryError e) { System.gc(); - log.error(" [{}/{}] {} — OUT OF MEMORY, skipping file", fi + 1, files.length, file.getName(), e); + log.error(" [{}/{}] {} — OUT OF MEMORY (engine={}, dataset={}, file={}), skipping file", + fi + 1, files.length, file.getName(), + driver.getName(), datasetName, file.getAbsolutePath(), e); } } } finally { @@ -118,32 +135,76 @@ public List runQueryBenchmark(String datasetDir, List queri 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; - } + // Warmup: one discarded run per query to prime the JIT and validate results. + // Skipped in quick mode (repetitions == 1) to save time. + if (repetitions == 1) log.info(" Quick mode — skipping query warmup"); + if (repetitions > 1) log.info(" Warming up {} queries (1 run discarded)...", queries.size()); + for (int qi = 0; repetitions > 1 && qi < queries.size(); qi++) { + String warmupSparql = queries.get(qi).trim(); + if (warmupSparql.isEmpty()) continue; + String warmupId = "query_" + (qi + 1); + final String wq = warmupSparql; + ExecutorService warmupExec = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "warmup-" + warmupId); + t.setDaemon(true); + return t; + }); + Future wf = warmupExec.submit(() -> driver.queryWithCount(wq)); + try { + long[] res = wf.get(queryTimeoutS, TimeUnit.SECONDS); + long count = res[1]; + if (count == 0) { + log.warn(" {} warmup: 0 results — query may be incorrect or data not loaded", warmupId); + } else if (count > 0) { + log.debug(" {} warmup: {} result(s)", warmupId, count); + } + warmupExec.shutdown(); + } catch (TimeoutException e) { + wf.cancel(true); + warmupExec.shutdownNow(); + log.warn(" {} warmup timed out after {}s — skipping", warmupId, queryTimeoutS); + } catch (Exception e) { + warmupExec.shutdown(); + log.warn(" {} warmup failed: {}", warmupId, + e.getCause() != null ? e.getCause().getMessage() : e.getMessage()); + } + } + + // Benchmark each query with per-execution timeout. + // A fresh daemon-thread executor is created per query so that a stuck thread + // (one that ignores interrupt) is abandoned rather than blocking all subsequent queries. + 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; + final int rep = i; + ExecutorService executor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "query-runner-" + queryId + "-rep" + rep); + t.setDaemon(true); // JVM won't wait for this thread on exit + return t; + }); + Future future = executor.submit(() -> driver.query(q)); + try { + times[i] = future.get(queryTimeoutS, TimeUnit.SECONDS); + executor.shutdown(); + } catch (TimeoutException e) { + future.cancel(true); + executor.shutdownNow(); // abandon — daemon thread won't block JVM + log.warn(" {} rep {} timed out after {}s", queryId, i + 1, queryTimeoutS); + times[i] = -1L; + } catch (ExecutionException e) { + executor.shutdown(); + 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"); } + 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(); @@ -165,21 +226,23 @@ public List runQueryBenchmark(String datasetDir, List queri private FileResult benchmarkFile(File file) throws Exception { String path = file.getAbsolutePath(); - // --- Warmup (discarded) --- - driver.clear(); - System.gc(); - driver.load(path); - driver.clear(); - System.gc(); + // --- Warmup (discarded) — skipped in quick mode (repetitions == 1) --- + if (repetitions > 1) { + 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[] 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++) { + for (int i = 0; i < repetitions; i++) { driver.clear(); memBefore[i] = metrics.getUsedMemoryMb(); @@ -195,8 +258,8 @@ private FileResult benchmarkFile(File file) throws Exception { // --- 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]; + double[] deltas = new double[repetitions]; + for (int i = 0; i < repetitions; i++) deltas[i] = memAfter[i] - memBefore[i]; return new FileResult( driver.getName(), diff --git a/src/main/resources/lubm/lubm-queries.txt b/src/main/resources/lubm/lubm-queries.txt index 5b610e0..b92eb61 100644 --- a/src/main/resources/lubm/lubm-queries.txt +++ b/src/main/resources/lubm/lubm-queries.txt @@ -1,42 +1,14 @@ -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 } +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 }