From d17423ca07e7445f4098db414afe175eb3b6cca6 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 22 Oct 2026 12:04:14 +0200 Subject: [PATCH 1/7] =?UTF-8?q?dashboard=20UI=20(zoom=20graphiques,=20pill?= =?UTF-8?q?s=20moteurs=20group=C3=A9s,=20filtrage=20SPARQL)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 23 +- build.gradle | 29 ++ python-utils/workflow.py | 62 +++++ site/app.js | 588 +++++++++++++++++++++++++++++++-------- site/index.html | 111 ++++---- site/styles.css | 348 ++++++++++++++++++++++- 6 files changed, 990 insertions(+), 171 deletions(-) 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/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..a270e04 100644 --- a/python-utils/workflow.py +++ b/python-utils/workflow.py @@ -79,6 +79,18 @@ 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).", +) args = parser.parse_args() current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -219,6 +231,9 @@ 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") 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) @@ -262,6 +277,53 @@ def merge_json_files(file_list, output_file): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") +# ── 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): + print(f"[resume] SKIP {engine} × {ds} — already done ({out_file})") + collected.append(out_file) + continue + 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(",")] diff --git a/site/app.js b/site/app.js index d856416..cfa4c3f 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 = {}; @@ -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: true, }, }; @@ -177,9 +192,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 +211,7 @@ function renderDatasetCards(runs, queries) { } return ` -
+
${meta.label} ${linkHtml} @@ -222,6 +237,56 @@ function renderDatasetCards(runs, queries) { }).join(''); } +function openDatasetModal(ds) { + const m = DATASET_META[ds.toLowerCase()] ?? { label: ds, description: '', url: null }; + const dsRuns = allRuns.filter(r => r.dataset === ds); + const files = [...new Set(dsRuns.map(r => r.file))]; + const engines = [...new Set(dsRuns.map(r => r.engine))]; + const totalTrip = engines.length ? dsRuns.reduce((s, r) => s + r.triples, 0) / engines.length : 0; + + document.getElementById('modal-ds-title').textContent = m.label; + const queriesHtml = metadataQueries && metadataQueries.length > 0 && m.hasSparql ? ` + ` : ''; + + document.getElementById('modal-ds-body').innerHTML = ` + + + ${queriesHtml} + `; + document.getElementById('dataset-modal').showModal(); +} + +function closeDatasetModal() { + document.getElementById('dataset-modal').close(); +} + // ── Table sort state ───────────────────────────────────────────────────────── let sortField = null; let sortAsc = true; @@ -249,25 +314,59 @@ 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 allCharts = {}; +let activeDatasets = new Set(); +let userRefEngine = null; +let historyData = []; +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))]; + 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}`; + const enginePills = document.getElementById('engine-pills'); + if (enginePills) { + // Sort engines by known order, unknown engines go at the end + const sorted = [ + ...KNOWN_ENGINE_ORDER.filter(e => engines.includes(e)), + ...engines.filter(e => !KNOWN_ENGINE_ORDER.includes(e)), + ]; + // Group by family (prefix before first dot) + const families = {}; + const familyOrder = []; + for (const e of sorted) { + const dot = e.indexOf('.'); + const family = dot >= 0 ? e.slice(0, dot) : e; + if (!families[family]) { families[family] = []; familyOrder.push(family); } + families[family].push(e); + } + enginePills.innerHTML = familyOrder.map(family => { + const vers = families[family]; + const p = pal(vers[0]); + const btns = vers.map(e => { + const label = e.includes('.') ? e.slice(e.indexOf('.') + 1) : e; + return ``; + }).join(''); + return `
+ ${family} + ${btns} +
`; + }).join(''); + } + + const dsPills = document.getElementById('dataset-filter-pills'); + if (dsPills) { + dsPills.innerHTML = datasets.map(d => { + const m = DATASET_META[d.toLowerCase()] ?? { label: d }; + return ``; + }).join(''); + } } // ── Run info bar ────────────────────────────────────────────────────────────── @@ -333,7 +432,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 +481,14 @@ function renderEngineCards(runs) { ${engine} - ${scoreDisplay} + + ${scoreDisplay} + +
@@ -408,20 +516,44 @@ function renderEngineCards(runs) { }).join(''); } +function syncEnginePills() { + document.querySelectorAll('.engine-pill[data-engine]').forEach(el => { + const e = el.dataset.engine; + const sign = el.querySelector('.pill-sign'); + if (activeEngines.size === 0) { + el.classList.remove('ep-active', 'ep-dimmed'); + if (sign) sign.textContent = ''; + } else if (activeEngines.has(e)) { + el.classList.add('ep-active'); + el.classList.remove('ep-dimmed'); + if (sign) sign.textContent = '−'; + } else { + el.classList.add('ep-dimmed'); + el.classList.remove('ep-active'); + if (sign) sign.textContent = '+'; + } + }); +} + 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(filtered); + 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(); } function clearEngineFilter() { @@ -429,8 +561,65 @@ function clearEngineFilter() { 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'; + syncEnginePills(); + updateCharts(); + updateTable(); +} + +function getFilteredRuns(runs) { + return runs.filter(r => + (activeEngines.size === 0 || activeEngines.has(r.engine)) && + (activeDatasets.size === 0 || activeDatasets.has(r.dataset)) + ); +} + +function clearDatasetFilter() { + activeDatasets.clear(); + document.querySelectorAll('.dataset-pill').forEach(el => el.classList.remove('active')); + updateCharts(); + updateTable(); + renderKPI(getFilteredRuns(allRuns)); + renderEngineCards(getFilteredRuns(allRuns)); + document.querySelectorAll('.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'; + updateQuerySectionVisibility(); +} + +function updateQuerySectionVisibility() { + const section = document.querySelector('.query-section'); + if (!section) return; + if (activeDatasets.size === 0) { section.style.display = ''; return; } + const hasSparql = [...activeDatasets].some(ds => DATASET_META[ds.toLowerCase()]?.hasSparql ?? false); + section.style.display = hasSparql ? '' : 'none'; +} + +function toggleDataset(ds) { + if (activeDatasets.has(ds)) activeDatasets.delete(ds); + else activeDatasets.add(ds); + document.querySelectorAll('.dataset-pill').forEach(el => { + el.classList.toggle('active', activeDatasets.has(el.dataset.ds)); + }); updateCharts(); updateTable(); + renderKPI(getFilteredRuns(allRuns)); + renderEngineCards(getFilteredRuns(allRuns)); + document.querySelectorAll('.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'; + updateQuerySectionVisibility(); +} + +function setRefEngine(engine) { + userRefEngine = userRefEngine === engine ? null : engine; + renderEngineCards(getFilteredRuns(allRuns)); + document.querySelectorAll('.suite-item[data-engine]').forEach(el => { + el.classList.toggle('active', activeEngines.has(el.dataset.engine)); + }); } // ── Engine card drag & drop ─────────────────────────────────────────────────── @@ -473,6 +662,7 @@ function dragDrop(e) { const order = [...grid.querySelectorAll('.suite-item[data-engine]')].map(el => el.dataset.engine); localStorage.setItem('engineOrder', JSON.stringify(order)); + updateCharts(); } function dragQueryDrop(e) { @@ -491,6 +681,7 @@ function dragQueryDrop(e) { if (srcIdx < tgtIdx) grid.insertBefore(dragSrc, target.nextSibling); else grid.insertBefore(dragSrc, target); + updateQueryChart(); } function dragEnd(e) { @@ -540,18 +731,19 @@ const CHART_OPT = (yLabel) => ({ }); 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 engines = getEngineOrder(); + 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)', + backgroundColor: visible ? p.bg : 'rgba(100,100,100,0.15)', borderColor: visible ? p.hex : 'rgba(100,100,100,0.3)', borderWidth: 1, borderRadius: 3, @@ -559,31 +751,81 @@ function buildDatasets(field) { }); } +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))]; +} + +function buildCombinedDatasets() { + const dsRuns = activeDatasets.size === 0 ? allRuns : allRuns.filter(r => activeDatasets.has(r.dataset)); + const engines = getEngineOrder(); + const files = [...new Set(dsRuns.map(r => r.file))]; + const datasets = []; + engines.forEach(engine => { + const p = pal(engine); + const visible = activeEngines.size === 0 || activeEngines.has(engine); + const col = visible ? p.hex : 'rgba(100,100,100,0.3)'; + const bgCol = visible ? p.bg : 'rgba(100,100,100,0.15)'; + // Barres : Loading time (axe gauche) + datasets.push({ + label: `${engine}`, + type: 'bar', + 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: bgCol, + borderColor: col, + borderWidth: 1, + borderRadius: 3, + }); + // 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: col, + backgroundColor: 'transparent', + borderWidth: 2, + borderDash: [5, 3], + pointRadius: 3, + tension: 0.3, + }); + }); + return datasets; +} + function initCharts() { - const files = [...new Set(allRuns.map(r => r.file))]; + 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); - 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: '%' }, + const defs = [ + { id: 'chart-loading', field: 'loadingTimeMs', unit: 'ms' }, + { id: 'chart-throughput', field: 'throughputTriplesPerSec', unit: 't/s' }, + { id: 'chart-memory', field: 'memoryDeltaMb', unit: 'MB' }, + { id: 'chart-cpu', field: 'cpuPercent', unit: '%' }, ]; - for (const { id, field, yLabel } of configs) { + 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(yLabel), + options: CHART_OPT(unit), }); allCharts[id]._field = field; } } function updateCharts() { + 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, chart] of Object.entries(allCharts)) { + chart.data.labels = labels; chart.data.datasets = buildDatasets(chart._field); - chart.update('none'); + chart.update(); } } @@ -593,9 +835,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 +911,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 +939,74 @@ 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] && + (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 => + (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) { @@ -835,13 +1107,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 @@ -858,6 +1124,10 @@ function renderQuerySection(queryRuns) { `; 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'), { type: 'bar', @@ -876,15 +1146,50 @@ function renderQuerySection(queryRuns) { }); } -// ── Tendance historique ─────────────────────────────────────────────────────── +// ── Sparklines (mini charts sur engine cards) ───────────────────────────────── +function renderSparklines(valid) { + document.querySelectorAll('.sparkline-canvas[data-engine]').forEach(canvas => { + const engine = canvas.dataset.engine; + const points = valid.map(run => { + const runs = run.data.runs.filter(r => r.engine === engine); + if (!runs.length) return null; + return Math.round(runs.reduce((s, r) => s + r.throughputTriplesPerSec, 0) / runs.length); + }).filter(v => v !== null); + + if (points.length < 2) { canvas.style.display = 'none'; return; } + + const p = pal(engine); + new Chart(canvas, { + type: 'line', + data: { + labels: points.map((_, i) => i), + datasets: [{ + data: points, + borderColor: p.hex, + borderWidth: 1.5, + pointRadius: 0, + fill: true, + backgroundColor: p.hex + '22', + tension: 0.4, + }], + }, + options: { + responsive: false, + animation: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { x: { display: false }, y: { display: false, beginAtZero: false } }, + }, + }); + }); +} + +// ── Tendance historique — heatmap régression ────────────────────────────────── 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'); @@ -907,67 +1212,121 @@ async function renderTrend() { if (valid.length < 2) return; } - // Engines: only those present in the current run (allRuns), to keep the chart focused + historyData = valid; + 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)); - // 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 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)}`; + const d = r.date; + return `${d.slice(6,8)}/${d.slice(4,6)} ${d.slice(9,11)}h${d.slice(11,13)}`; }); + function avgThru(run, engine) { + const runs = run.data.runs.filter(r => r.engine === engine); + if (!runs.length) return null; + return Math.round(runs.reduce((s, r) => s + r.throughputTriplesPerSec, 0) / runs.length); + } + + const rows = engines.map(engine => { + const p = pal(engine); + const cells = valid.map((run, i) => { + const val = avgThru(run, engine); + if (val === null) return `—`; + const prev = i > 0 ? avgThru(valid[i - 1], engine) : null; + let cls = 'hm-first', badge = ''; + if (prev !== null) { + const pct = (val - prev) / prev * 100; + cls = pct > 5 ? 'hm-good' : pct < -5 ? 'hm-bad' : 'hm-neutral'; + badge = `${pct > 0 ? '+' : ''}${pct.toFixed(1)}%`; + } + return ` + ${fmt(val, 0)}${badge} + `; + }).join(''); + return ` + ${engine} + ${cells} + `; + }).join(''); + const section = document.createElement('section'); section.innerHTML = `

Historical trend

- ${valid.length} runs · avg throughput per engine + ${valid.length} runs · heatmap avg débit
-
-
- Throughput over time - triples/s — higher is better +

+ ▮ amélioration >5% + ▮ régression >5% + ▮ stable +

+
+ + + + ${labels.map(l => ``).join('')} + + ${rows} +
Engine${l}
- -
`; document.querySelector('.app-wrapper').appendChild(section); - new Chart(document.getElementById('chart-trend'), { - type: 'line', - data: { labels, datasets }, + renderSparklines(valid); +} + +// ── Chart zoom modal ────────────────────────────────────────────────────────── +let _zoomChart = null; + +function openChartZoom(chartId) { + const src = allCharts[chartId]; + if (!src) return; + + 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'); + + // Titre = titre du chart-surface correspondant + const surface = document.getElementById(chartId)?.closest('.chart-surface'); + titleEl.textContent = surface?.querySelector('.chart-title')?.textContent ?? chartId; + + // Détruire le chart zoom précédent + if (_zoomChart) { _zoomChart.destroy(); _zoomChart = null; } + + // 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 ──────────────────────────────────────────────────────────────── @@ -1000,16 +1359,15 @@ async function main() { } allRuns = data.runs; + 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); renderHeader(data.metadata, allRuns); - renderRunInfo(data.metadata); renderKPI(allRuns); - renderGlobalMeter(allRuns); - renderDatasetCards(allRuns, data.metadata.queries ?? null); + renderDatasetCards(allRuns, metadataQueries); renderEngineCards(allRuns); applyStoredEngineOrder(); initCharts(); @@ -1023,8 +1381,6 @@ async function main() { renderQuerySection(data.queryRuns); } - // Historical trend (async, non-blocking) - renderTrend(); } main(); diff --git a/site/index.html b/site/index.html index 79fcfd4..00025d7 100644 --- a/site/index.html +++ b/site/index.html @@ -18,8 +18,7 @@

Corese

- Benchmark - +
@@ -49,6 +48,13 @@

Corese

CSV + + + W3C Tests +
-

- Corese reference benchmark dashboard — Loading performance comparison -

+
+
+
- -
- Run info - Generated— - Java— - OS— - Protocol— -
-
- -
-
- -
-
-
@@ -118,50 +108,48 @@

Engines tested

- -

Resource usage

-
+ +

Performance

+
-
+
- Memory used - ΔMB — lower is better - + Loading time + ms — lower is better + +
- +
-
+
- CPU - % — lower is better - + Throughput + triples/s — higher is better + +
- +
-
- - -

Performance

-
- -
+
- Throughput - triples/s — higher is better - + Memory + ΔMB — lower is better + +
- +
-
+
- Loading time - ms — lower is better - + CPU + % — lower is better + +
- +
@@ -193,6 +181,29 @@

Full results

+ +
+
+ +
+ + +
+
+ +
+
+ + + + +