- include link to one-page executive summary in README - update dashboard section in evaluation HTML to reference summary - revise report metadata to reflect additional CSV tables generated
815 lines
53 KiB
HTML
815 lines
53 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Storage Format Evaluation - Tribology Lab Data</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
*{box-sizing:border-box}
|
|
html,body{margin:0;padding:0}
|
|
body{font-family:"IBM Plex Sans",system-ui,sans-serif;color:#16181d;background:#eef0f2;-webkit-font-smoothing:antialiased}
|
|
::-webkit-scrollbar{width:10px;height:10px}
|
|
::-webkit-scrollbar-thumb{background:#cfd4da;border-radius:6px;border:2px solid #eef0f2}
|
|
::-webkit-scrollbar-track{background:transparent}
|
|
.mono{font-family:"IBM Plex Mono",monospace}
|
|
|
|
.app{display:flex;flex-direction:column;height:100vh;width:100%;overflow:hidden;background:#eef0f2}
|
|
header.top{display:flex;align-items:center;gap:14px;height:56px;flex:none;padding:0 22px;background:#fff;border-bottom:1px solid #e2e5ea}
|
|
.logo{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:7px;background:#16181d;color:#fff;font-family:"IBM Plex Mono",monospace;font-weight:600;font-size:12px}
|
|
.htitle{display:flex;flex-direction:column;line-height:1.25;white-space:nowrap}
|
|
.htitle b{font-size:14.5px;font-weight:600;letter-spacing:-.01em}
|
|
.htitle span{font-size:11.5px;color:#838a95}
|
|
.hright{margin-left:auto;display:flex;align-items:center;gap:8px}
|
|
.chip{font-family:"IBM Plex Mono",monospace;font-size:11px;color:#838a95;background:#f3f5f7;border:1px solid #e6e8ec;padding:4px 10px;border-radius:6px}
|
|
.chip-ok{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:#0f7a45;background:#e7f6ee;padding:4px 10px;border-radius:6px;font-weight:600}
|
|
.chip-ok i{width:7px;height:7px;border-radius:7px;background:#18a05a}
|
|
|
|
.frame{display:flex;flex:1;min-height:0}
|
|
nav.side{width:252px;flex:none;background:#fff;border-right:1px solid #e2e5ea;padding:14px 12px;display:flex;flex-direction:column;overflow-y:auto}
|
|
.navcap{font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:.1em;color:#a4abb4;padding:6px 11px 8px}
|
|
nav.side a{display:flex;align-items:center;gap:11px;width:100%;padding:8px 11px;border-radius:8px;cursor:pointer;text-decoration:none;font-size:13.5px;margin:1px 0;color:#475059;font-weight:500}
|
|
nav.side a .tag{width:24px;height:19px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-family:"IBM Plex Mono",monospace;font-size:9px;font-weight:600;flex:none;background:#eceef1;color:#8a909b}
|
|
nav.side a.active{background:#eaf1fe;color:#16181d;font-weight:600}
|
|
nav.side a.active .tag{background:#1f5fdb;color:#fff}
|
|
.sidefoot{margin-top:auto;padding:14px 11px 6px;border-top:1px solid #eef0f2}
|
|
.sidefoot .cap{font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:.08em;color:#a4abb4;margin-bottom:8px}
|
|
.sidefoot .txt{font-size:12px;color:#6b727c;line-height:1.6}
|
|
|
|
main{flex:1;min-width:0;overflow-y:auto;padding:26px 30px 60px}
|
|
main section{display:none}
|
|
main section.on{display:block}
|
|
h1{margin:0;font-size:21px;font-weight:600;letter-spacing:-.02em}
|
|
.sub{margin:4px 0 18px;font-size:13px;color:#838a95;max-width:900px;line-height:1.55}
|
|
.card{background:#fff;border:1px solid #e6e8ec;border-radius:11px;padding:18px 20px;margin-bottom:14px}
|
|
.card h3{margin:0 0 10px;font-size:14px;font-weight:600}
|
|
.card p{margin:0 0 10px;font-size:13px;color:#3d424b;line-height:1.6;max-width:900px}
|
|
.card p:last-child{margin-bottom:0}
|
|
.card ul{margin:6px 0 4px;padding-left:20px}
|
|
.card li{font-size:13px;color:#3d424b;line-height:1.65;margin-bottom:6px;max-width:860px}
|
|
.muted{color:#838a95}
|
|
.kpis{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:16px}
|
|
.kpis.four{grid-template-columns:repeat(4,1fr)}
|
|
.kpi{background:#fff;border:1px solid #e6e8ec;border-radius:10px;padding:15px 16px}
|
|
.kpi .l{font-size:11px;color:#838a95;font-weight:500;text-transform:uppercase;letter-spacing:.04em}
|
|
.kpi .v{font-family:"IBM Plex Mono",monospace;font-size:24px;font-weight:600;margin-top:7px;line-height:1}
|
|
.kpi .s{font-size:11.5px;color:#a4abb4;margin-top:5px}
|
|
.v-bad{color:#b23b4e}.v-good{color:#0f7a45}.v-neutral{color:#16181d}
|
|
|
|
table.data{width:100%;border-collapse:collapse}
|
|
table.data th{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:#838a95;font-weight:600;text-align:left;padding:9px 12px;background:#f7f8fa;border-bottom:1px solid #eef0f2}
|
|
table.data td{font-size:12.5px;color:#3d424b;padding:10px 12px;border-bottom:1px solid #f4f5f7;vertical-align:top}
|
|
table.data th.num, table.data td.num{text-align:right;font-family:"IBM Plex Mono",monospace}
|
|
table.data td.mono{font-family:"IBM Plex Mono",monospace}
|
|
|
|
.hbars{display:flex;flex-direction:column;gap:11px;margin-top:6px}
|
|
.hbar{display:flex;align-items:center;gap:14px}
|
|
.hbar .lab{width:190px;flex:none;font-size:12.5px;color:#3d424b}
|
|
.hbar .lab small{display:block;color:#a4abb4;font-size:11px}
|
|
.hbar .trk{flex:1;background:#f1f3f6;border-radius:6px;overflow:hidden}
|
|
.hbar .fill{height:14px;border-radius:6px;min-width:2px}
|
|
.hbar .val{font-family:"IBM Plex Mono",monospace;font-size:12.5px;color:#16181d;width:130px;flex:none;text-align:right}
|
|
.legend{display:flex;gap:16px;flex-wrap:wrap;margin:2px 0 10px}
|
|
.legend span{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:#5b6472}
|
|
.legend i{width:10px;height:10px;border-radius:3px;display:inline-block}
|
|
|
|
.stack{display:flex;align-items:center;gap:14px;margin-bottom:11px}
|
|
.stack .lab{width:190px;flex:none;font-size:12.5px;color:#3d424b}
|
|
.stack .trk{flex:1;display:flex;background:#f1f3f6;border-radius:6px;overflow:hidden;height:16px}
|
|
.stack .seg{height:16px}
|
|
.stack .val{font-family:"IBM Plex Mono",monospace;font-size:12.5px;width:70px;flex:none;text-align:right;font-weight:600}
|
|
|
|
.tabs{display:inline-flex;gap:4px;background:#eceef1;border-radius:8px;padding:3px;margin:0 0 14px}
|
|
.tabs button{border:0;background:transparent;font-family:"IBM Plex Sans",system-ui,sans-serif;font-size:12px;font-weight:600;color:#5b6472;padding:5px 12px;border-radius:6px;cursor:pointer}
|
|
.tabs button.on{background:#fff;color:#16181d;box-shadow:0 1px 3px rgba(20,30,50,.08)}
|
|
|
|
.callout{border-left:3px solid #1f5fdb;background:#f5f8fe;border-radius:0 9px 9px 0;padding:12px 16px;font-size:13px;color:#2b3038;line-height:1.6;margin:12px 0;max-width:900px}
|
|
.callout.warn{border-left-color:#b23b4e;background:#fdf3f5}
|
|
.callout.good{border-left-color:#18a05a;background:#f0faf4}
|
|
.pill{display:inline-flex;align-items:center;padding:2px 9px;border-radius:20px;font-size:10.5px;font-weight:600;white-space:nowrap}
|
|
.pill.ok{color:#0f7a45;background:#e7f6ee}
|
|
.pill.imp{color:#a06a12;background:#fdf3e0}
|
|
.pill.fail{color:#b23b4e;background:#fbe9ec}
|
|
.pill.rec{color:#0f7a45;background:#e7f6ee}
|
|
.pill.base{color:#5b6472;background:#eef0f2}
|
|
|
|
pre.env{background:#16181d;color:#dfe3e9;border-radius:9px;padding:14px 16px;font-family:"IBM Plex Mono",monospace;font-size:11.5px;line-height:1.65;overflow-x:auto;margin:10px 0 0}
|
|
.tree{font-family:"IBM Plex Mono",monospace;font-size:12px;line-height:1.8;color:#3d424b;background:#f7f8fa;border:1px solid #eef0f2;border-radius:9px;padding:14px 18px;margin-top:8px}
|
|
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
|
@media(max-width:1100px){.grid2{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,1fr)}}
|
|
.foot{font-size:11.5px;color:#a4abb4;border-top:1px solid #e6e8ec;padding-top:14px;margin-top:30px;line-height:1.7}
|
|
.src a, a.ext{color:#1f5fdb;text-decoration:none;word-break:break-all}
|
|
.src a:hover, a.ext:hover{text-decoration:underline}
|
|
a.chip{text-decoration:none}
|
|
a.chip:hover{border-color:#1f5fdb;color:#1f5fdb}
|
|
svg text{font-family:"IBM Plex Sans",system-ui,sans-serif}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app">
|
|
|
|
<header class="top">
|
|
<div class="logo">SE</div>
|
|
<div class="htitle">
|
|
<b>Storage Format Evaluation</b>
|
|
<span>Tribology lab data · CSV vs JSON-LD vs SQLite vs PostgreSQL vs RDF</span>
|
|
</div>
|
|
<div class="hright">
|
|
<a class="chip" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation" target="_blank" rel="noopener">Git repository</a>
|
|
<span class="chip">7 queries x 5 formats · 35 cells</span>
|
|
<span class="chip">seed 20260711</span>
|
|
<span class="chip-ok"><i></i>Measured 2026-07-11 · all checksums OK</span>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="frame">
|
|
|
|
<nav class="side" id="sidenav">
|
|
<div class="navcap">NAVIGATE</div>
|
|
<a href="#dashboard" class="active"><span class="tag">DB</span><span>Dashboard</span></a>
|
|
<a href="#lab"><span class="tag">LM</span><span>Lab Model & Corpus</span></a>
|
|
<a href="#method"><span class="tag">ME</span><span>Methodology</span></a>
|
|
<a href="#storage"><span class="tag">SF</span><span>Storage Footprint</span></a>
|
|
<a href="#measured"><span class="tag">MR</span><span>Measured Results</span></a>
|
|
<a href="#proj"><span class="tag">PR</span><span>Projections 0.6-6 TB</span></a>
|
|
<a href="#hw"><span class="tag">HW</span><span>Hardware & Cost</span></a>
|
|
<a href="#score"><span class="tag">SC</span><span>Scoring & Verdict</span></a>
|
|
<a href="#about"><span class="tag">AB</span><span>About</span></a>
|
|
<div class="sidefoot">
|
|
<div class="cap">CONTEXT</div>
|
|
<div class="txt">Final results of the LabDataStorageEvaluation pipeline (tasks 01-11): a simulated Pt-Au tribology laboratory corpus benchmarked across five storage formats. Sources of truth: out/report/ and out/bench/.</div>
|
|
<div class="cap" style="margin-top:12px">AUTHOR · SOURCE</div>
|
|
<div class="txt">Mary Goncharenko - University of Florida / Sandia National Laboratories.<br>
|
|
<a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation" target="_blank" rel="noopener">git.boskadoff.com/Public/LabDataStorageEvaluation</a></div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main id="main">
|
|
|
|
<!-- ================= DASHBOARD ================= -->
|
|
<section id="dashboard" class="on">
|
|
<h1>Dashboard</h1>
|
|
<p class="sub">A 141.8 MiB physically plausible CSV corpus (the canonical raw format) was converted into four other storage formats and benchmarked on seven laboratory retrieval scenarios with subprocess-isolated resource metering; results were extrapolated to 600 GB, 1.2 TB and 6 TB. Every measured run validated its rows against canonical results (PostgreSQL cross-validated with SQLite, tolerance 1e-9). Short on time? The one-page management summary is <a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation/raw/branch/master/docs/research/Tribology_Storage_Decision.html" target="_blank" rel="noopener">Storage Decision</a>.</p>
|
|
<div class="kpis">
|
|
<div class="kpi"><div class="l">Benchmark cells</div><div class="v v-good">35 / 35</div><div class="s">OK; 0 invalid, 0 timeouts</div></div>
|
|
<div class="kpi"><div class="l">Corpus</div><div class="v v-neutral">141.8 MiB</div><div class="s">8,718 CSV files, byte-reproducible</div></div>
|
|
<div class="kpi"><div class="l">Winner @600 GB</div><div class="v v-good">58.6 / 80</div><div class="s">PostgreSQL weighted score</div></div>
|
|
<div class="kpi"><div class="l">Worst blow-up</div><div class="v v-bad">11.7x</div><div class="s">RDF store vs raw CSV size</div></div>
|
|
<div class="kpi"><div class="l">Session</div><div class="v v-neutral">916 s</div><div class="s">whole matrix, quiet machine</div></div>
|
|
</div>
|
|
<div class="callout good"><b>Recommendation (confirmed by data):</b> PostgreSQL as the system of record; CSV retained as the raw archive (best compressor, ~2.8x); hybrid JSON-LD for inter-lab exchange; RDF only as a virtual layer (e.g. Ontop OBDA) over PostgreSQL - a materialized triplestore fails at scale on speed, RAM and cost simultaneously.</div>
|
|
<div class="card">
|
|
<h3>Typical query time per format (geometric mean Q1-Q7)</h3>
|
|
<div id="dash-geo" class="hbars"></div>
|
|
<p class="muted" style="margin-top:12px">Left value: measured on the 150 MB corpus. Right value in parentheses: projected at 600 GB. Linear bars scaled within the measured values; every projected number in this document is labeled as projected.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Where each format breaks</h3>
|
|
<table class="data">
|
|
<thead><tr><th>Format</th><th>Strength</th><th>Breaking point</th><th>@600 GB flags</th></tr></thead>
|
|
<tbody id="dash-breaks"></tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= LAB MODEL ================= -->
|
|
<section id="lab">
|
|
<h1>Lab Model & Corpus</h1>
|
|
<p class="sub">A simulated tribology laboratory (Pt-Au LDRD study context, Sandia National Laboratories): Ti-6Al-4V coupons with a sputtered Cr adhesion layer and a Pt-Au composition-gradient coating, tested on a 6-probe tribometer in two environments. All data is generated deterministically from seed 20260711; regeneration is byte-identical (MANIFEST.csv sha256 proof).</p>
|
|
<div class="kpis four">
|
|
<div class="kpi"><div class="l">Deposition</div><div class="v v-neutral">4 · 12 · 588</div><div class="s">batches · wafers · coupons (7x7 grid)</div></div>
|
|
<div class="kpi"><div class="l">Friction testing</div><div class="v v-neutral">1,440</div><div class="s">tracks: 480 coupons x 3 replicates</div></div>
|
|
<div class="kpi"><div class="l">Bulk rows</div><div class="v v-neutral">4.32 M</div><div class="s">1.44M COF cycles + 2.88M loop points</div></div>
|
|
<div class="kpi"><div class="l">Characterization</div><div class="v v-neutral">235,200</div><div class="s">XRF points + nanoindentation, AFM, profilometry</div></div>
|
|
</div>
|
|
<div class="grid2">
|
|
<div class="card">
|
|
<h3>Data hierarchy</h3>
|
|
<div class="tree">batch (B721-B724, deposition matrix)
|
|
-> wafer (x3 per batch)
|
|
-> coupon (x49 per wafer; 480 friction + 108 reserve)
|
|
-> xrf_map (400 pts) · nanoindentation (25) · afm · profilometry (100)
|
|
-> track (x3, counterface rotation)
|
|
-> cof_vs_cycle (1,000 rows) · friction_loops (10x200) · wear</div>
|
|
<p style="margin-top:10px">Runs R1-R4 map one-to-one onto batches; two environments (lab air / dry N2). The Q1 benchmark target track is <span class="mono">B722-W2-C13-T2</span>.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Physical models behind the numbers</h3>
|
|
<ul>
|
|
<li>COF: exponential run-in to a steady state depending on Au wt% and environment; noise floor 0.12.</li>
|
|
<li>Hardness / modulus: linear in Au wt% with measurement noise (25 indents per coupon).</li>
|
|
<li>Wear: Archard model, k0 = 1.5e-7 (lab air) / 6e-8 (dry N2) mm3/(N*m), log-normal noise.</li>
|
|
<li>Au gradient across wafer position; batch-dependent center 8 / 25 / 10 / 60 wt%.</li>
|
|
</ul>
|
|
<p>Benchmark queries therefore return physically meaningful answers (e.g. Q2 selects the Au 10 +/- 0.5 wt% composition window), not random noise.</p>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>The five storage formats under test</h3>
|
|
<table class="data">
|
|
<thead><tr><th>Format</th><th>Variant benchmarked</th><th>How Q1-Q7 run</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><b>CSV</b> (canonical raw)</td><td>hierarchical file tree + MANIFEST</td><td>python csv module, streaming; Q1 = direct path read</td></tr>
|
|
<tr><td><b>JSON-LD</b></td><td>FULL (per-coupon files, 588); HYBRID kept for exchange</td><td>ijson event streaming with early exit</td></tr>
|
|
<tr><td><b>SQLite</b></td><td>single file, composite natural PKs WITHOUT ROWID, enforced FKs</td><td>SQL; precomputed track_summary for Q2-Q4/Q7</td></tr>
|
|
<tr><td><b>PostgreSQL 16</b></td><td>remote host, range-partitioned bulk tables (12+12), BRIN + b-tree</td><td>SQL; parallel scans (4 workers), track_summary matview</td></tr>
|
|
<tr><td><b>RDF</b></td><td>pyoxigraph on-disk store, 17,419,713 triples (compact modeling)</td><td>SPARQL; run-in/steady-state derived from raw cycles</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= METHODOLOGY ================= -->
|
|
<section id="method">
|
|
<h1>Methodology</h1>
|
|
<p class="sub">The product of this study is its measurements; the protocol is designed so a sloppy number cannot survive it (docs/rules/bench-methodology.md).</p>
|
|
<div class="grid2">
|
|
<div class="card">
|
|
<h3>Measurement protocol</h3>
|
|
<ul>
|
|
<li><b>One subprocess per run</b> - RSS / CPU / read bytes attribute to exactly that cell; psutil samples the whole process tree every 50 ms (the venv launcher stub alone would report 4 MB).</li>
|
|
<li><b>1 warm-up + 3 measured runs</b> per cell; medians reported, min/max kept in raw data.</li>
|
|
<li><b>Randomized cell order</b> (deterministic shuffle from the corpus seed); order recorded.</li>
|
|
<li><b>30-minute hard timeout</b> per run; a timeout is recorded as data, never retried.</li>
|
|
<li><b>Correctness gates speed:</b> every run's rows are compared to the canonical expected results (sorted rows, float tolerance 1e-9); a fast run that fails the checksum is an invalid measurement.</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Fairness and honesty</h3>
|
|
<ul>
|
|
<li>Identical result-set contract for all five implementations of each query; no format answers a simplified question.</li>
|
|
<li>Each format plays its idiomatic strength (CSV direct path read, SQL indexes, matviews) - but no LIMIT/sampling that changes the answer.</li>
|
|
<li><b>Q5 must scan raw cycles in every format</b>; the PostgreSQL EXPLAIN plan is checked to prove track_summary was not used.</li>
|
|
<li>Cold-cache pass skipped (Windows host has no page-cache drop) - marked, not faked.</li>
|
|
<li>PostgreSQL is remote: client wall times include the LAN round-trip; server-side cost captured via pg_stat_statements deltas and Prometheus host windows.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>The seven retrieval scenarios</h3>
|
|
<table class="data">
|
|
<thead><tr><th>ID</th><th>Scenario</th><th class="num">Result rows</th></tr></thead>
|
|
<tbody id="method-queries"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Environment (recorded per session)</h3>
|
|
<pre class="env" id="method-env"></pre>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= STORAGE ================= -->
|
|
<section id="storage">
|
|
<h1>Storage Footprint</h1>
|
|
<p class="sub">Working footprint is what the query engine reads; archival is the compressed form for shelving. All archival sizes converge to 53-57 MB - the information content is the same, the working overhead is not.</p>
|
|
<div class="kpis four">
|
|
<div class="kpi"><div class="l">Most compact</div><div class="v v-good">0.83x</div><div class="s">SQLite: 123 MB, indexes included</div></div>
|
|
<div class="kpi"><div class="l">PostgreSQL</div><div class="v v-neutral">2.67x</div><div class="s">397 MB live; 34.1% of it is indexes</div></div>
|
|
<div class="kpi"><div class="l">RDF store</div><div class="v v-bad">11.70x</div><div class="s">1.74 GB for the same data</div></div>
|
|
<div class="kpi"><div class="l">Best archive</div><div class="v v-good">52.8 MB</div><div class="s">csv tree tar.gz (2.8x compression)</div></div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Working footprint, measured on the 150 MB corpus</h3>
|
|
<div id="st-work" class="hbars"></div>
|
|
</div>
|
|
<div class="grid2">
|
|
<div class="card">
|
|
<h3>Archival (compressed) footprint</h3>
|
|
<div id="st-arch" class="hbars"></div>
|
|
<p class="muted" style="margin-top:10px">pg_dump -Fc is internally gzip-compressed; RDF also has a Turtle serialization (596 MB, not the archival form). Hybrid JSON-LD is 1.9 MB of metadata + sourceFile links.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Projected working footprint (linear scaling)</h3>
|
|
<table class="data">
|
|
<thead><tr><th>Format</th><th class="num">@600 GB</th><th class="num">@1.2 TB</th><th class="num">@6 TB</th></tr></thead>
|
|
<tbody id="st-proj"></tbody>
|
|
</table>
|
|
<p class="muted" style="margin-top:10px">All projected. Storage scales linearly in data volume; coefficients are the measured ratios above.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= MEASURED ================= -->
|
|
<section id="measured">
|
|
<h1>Measured Results <span class="pill base" style="vertical-align:3px">150 MB corpus · warm cache</span></h1>
|
|
<p class="sub">Median of 3 isolated runs per cell. Pick a query and a metric; bars are linear with the actual value on each bar. Client peak RSS below ~50 ms of runtime under-reports (sampling cadence is spec-fixed at 50 ms) - wall times are exact.</p>
|
|
<div class="tabs" id="mq-tabs"></div>
|
|
<div class="tabs" id="mm-tabs" style="margin-left:10px"></div>
|
|
<div class="card">
|
|
<h3 id="m-title"></h3>
|
|
<div id="m-bars" class="hbars"></div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Full median matrix - wall time</h3>
|
|
<table class="data"><thead id="m-mat-h"></thead><tbody id="m-mat"></tbody></table>
|
|
</div>
|
|
<div class="card">
|
|
<h3>PostgreSQL server side (remote host, 4 cores / 3.8 GiB)</h3>
|
|
<table class="data">
|
|
<thead><tr><th>Query</th><th class="num">Server exec per call</th><th class="num">Shared blocks hit</th><th class="num">Read from disk</th></tr></thead>
|
|
<tbody id="m-pgsrv"></tbody>
|
|
</table>
|
|
<p class="muted" style="margin-top:10px">pg_stat_statements deltas over the session (4 calls per query). Zero disk reads: the whole 397 MB database sits in the 1 GB shared_buffers. Host peak CPU during the session: 9.2%; RAM delta ~5 MB - the server is nowhere near its limits at this scale.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= PROJECTIONS ================= -->
|
|
<section id="proj">
|
|
<h1>Projections <span class="pill imp" style="vertical-align:3px">every number here is projected, not measured</span></h1>
|
|
<p class="sub">Per-cell scaling laws calibrated on the measured 150 MB point after subtracting each format's harness floor (interpreter start, imports, connection). Flags: IMPRACTICAL over 1 hour, FAIL over 24 hours. Pick a target scale.</p>
|
|
<div class="tabs" id="p-tabs"></div>
|
|
<div class="card">
|
|
<h3 id="p-title"></h3>
|
|
<table class="data"><thead id="p-mat-h"></thead><tbody id="p-mat"></tbody></table>
|
|
<p class="muted" style="margin-top:10px" id="p-note"></p>
|
|
</div>
|
|
<div class="grid2">
|
|
<div class="card">
|
|
<h3>Scaling law per access pattern</h3>
|
|
<table class="data">
|
|
<thead><tr><th>Law</th><th>Growth</th><th>Applied to</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="mono">O(1)</td><td>flat</td><td>CSV / JSON-LD Q1 (one file, size fixed)</td></tr>
|
|
<tr><td class="mono">O(log n)</td><td>index depth</td><td>SQLite / PostgreSQL / RDF Q1</td></tr>
|
|
<tr><td class="mono">O(log n + k)</td><td>~linear (result set grows)</td><td>relational + RDF Q2, Q4</td></tr>
|
|
<tr><td class="mono">O(k log n)</td><td>linear x depth</td><td>relational Q3, Q6; RDF Q6</td></tr>
|
|
<tr><td class="mono">O(n)</td><td>linear, 1 core</td><td>all CSV/JSON scans; SQLite Q5/Q7; RDF Q3/Q5/Q7</td></tr>
|
|
<tr><td class="mono">O(n / cores)</td><td>linear / parallel workers</td><td>PostgreSQL Q5, Q7 (partitioned scan)</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Full-scan degradation (Q5 + Q7 geometric mean)</h3>
|
|
<div id="p-degr"></div>
|
|
<p class="muted" style="margin-top:8px">First column measured, the rest projected. PostgreSQL rides its parallel partitioned scan; single-threaded engines grow linearly.</p>
|
|
</div>
|
|
</div>
|
|
<div class="callout">PostgreSQL is the only format with all seven queries below one hour at every scale - Q5 at 6 TB projects to 37 minutes on 16 cores. SQLite holds to 600 GB, then its single-threaded scans pass the hour mark. JSON-LD full scans reach 4.6 days at 6 TB; RDF Q5 reaches 27.8 days.</div>
|
|
</section>
|
|
|
|
<!-- ================= HARDWARE ================= -->
|
|
<section id="hw">
|
|
<h1>Hardware & Cost <span class="pill base" style="vertical-align:3px">street prices as of 2026-07-11</span></h1>
|
|
<p class="sub">Bill-of-materials costing: whole 64 GB RDIMM modules, whole 7.68 TB NVMe drives, a priced 1U chassis; disk includes 30% free-space headroom. RAM model: relational hot set = 10% of index size + 4 GB working set; RDF hot set = 20% of the store; streaming formats are flat. Single-node RAM ceiling: 1 TB.</p>
|
|
<div class="tabs" id="h-tabs"></div>
|
|
<div class="card">
|
|
<h3 id="h-title"></h3>
|
|
<table class="data">
|
|
<thead><tr><th>Format</th><th>Exact configuration</th><th class="num">Chassis</th><th class="num">RAM</th><th class="num">Disk</th><th class="num">Extra nodes</th><th class="num">Total</th></tr></thead>
|
|
<tbody id="h-bom"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="grid2">
|
|
<div class="card">
|
|
<h3>Total cost comparison</h3>
|
|
<div id="h-bars" class="hbars"></div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Component prices & sources <span class="pill base">retrieved 2026-07-11</span></h3>
|
|
<ul class="src">
|
|
<li>64 GB DDR5-5600 ECC RDIMM (A-Tech, 2x32GB): <b>$1,887</b><br><a href="https://atechmemory.com/collections/ddr5-memory-ram">atechmemory.com/collections/ddr5-memory-ram</a></li>
|
|
<li>7.68 TB NVMe U.2 1DWPD enterprise SSD (Cloud Ninjas): <b>$3,995</b><br><a href="https://cloudninjas.com/products/new-7-68tb-nvme-u-2-1dwpd-sie-2-5-enterprise-solid-state-drive-for-14th-15th-16th-gen-dell">cloudninjas.com/products/new-7-68tb-nvme-u-2-1dwpd...</a></li>
|
|
<li>1U AMD EPYC chassis (Broadberry starting configs): AS-1015CS-TNR <b>$4,618</b>, AS-1115CS-TNR <b>$6,272</b><br><a href="https://www.broadberry.com/amd-epyc-9004-supermicro-servers">broadberry.com/amd-epyc-9004-supermicro-servers</a></li>
|
|
<li>Market context - server DRAM 2025-2026 price surge:<br><a href="https://www.techpowerup.com/342331/server-dram-pricing-jumps-50-only-70-of-orders-getting-filled">techpowerup.com/342331/server-dram-pricing-jumps-50...</a></li>
|
|
</ul>
|
|
<p class="muted" style="margin-top:10px">Spot prices during a documented DRAM surge; re-cost by editing out/config/hw_prices.yaml and re-running tasks 10-11.</p>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Cost vs speed at 600 GB (projected)</h3>
|
|
<div id="h-scatter"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= SCORING ================= -->
|
|
<section id="score">
|
|
<h1>Scoring & Verdict</h1>
|
|
<p class="sub">Subscores are 10 x best/value per metric, weighted: search speed x5 (0-50, inverse geometric mean of Q1-Q7), RAM economy x2 (0-20), disk economy x1 (0-10); maximum 80. A FAIL projection at 600 GB zeroes the search subscore.</p>
|
|
<div class="grid2">
|
|
<div class="card">
|
|
<h3>Measured scale (150 MB)</h3>
|
|
<div class="legend"><span><i style="background:#1f5fdb"></i>search x5</span><span><i style="background:#e0892e"></i>RAM x2</span><span><i style="background:#0f9d8a"></i>disk x1</span></div>
|
|
<div id="sc-measured"></div>
|
|
<p class="muted" style="margin-top:8px">At laptop scale SQLite is the honest winner: near-instant indexed queries in one file with the smallest footprint.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Projected at 600 GB</h3>
|
|
<div class="legend"><span><i style="background:#1f5fdb"></i>search x5</span><span><i style="background:#e0892e"></i>RAM x2</span><span><i style="background:#0f9d8a"></i>disk x1</span></div>
|
|
<div id="sc-proj"></div>
|
|
<p class="muted" style="margin-top:8px">At production scale the ranking flips: parallel indexed retrieval dominates the weights, and PostgreSQL takes the lead.</p>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Use-case mapping</h3>
|
|
<table class="data">
|
|
<thead><tr><th>Use case</th><th>Recommended format</th></tr></thead>
|
|
<tbody>
|
|
<tr><td>Interactive analysis (joins, aggregations)</td><td><b>PostgreSQL</b>; SQLite acceptable single-user up to ~600 GB</td></tr>
|
|
<tr><td>Report generation (repeated summaries)</td><td><b>PostgreSQL</b> (track_summary materialized view)</td></tr>
|
|
<tr><td>Search / filtering</td><td><b>PostgreSQL or SQLite</b> (indexed); flat formats need full scans</td></tr>
|
|
<tr><td>Archiving</td><td><b>CSV tree + tar.gz</b> (canonical raw) plus pg_dump of the system of record</td></tr>
|
|
<tr><td>Inter-lab exchange</td><td><b>Hybrid JSON-LD</b> (metadata + sourceFile links to CSV)</td></tr>
|
|
<tr><td>Semantic / ontology queries</td><td><b>Virtual RDF layer</b> over PostgreSQL (e.g. Ontop OBDA), not a materialized triplestore</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Limitations</h3>
|
|
<ul>
|
|
<li>Single-node measurements on one Windows host + one remote PostgreSQL host; no cluster variance.</li>
|
|
<li>Simulated corpus (physically plausible, fixed seed); real instrument data may distribute differently.</li>
|
|
<li>Extrapolation is analytic, calibrated on one 150 MB point; no intermediate-scale validation runs.</li>
|
|
<li>Warm-cache only (no page-cache drop on Windows); harness floor assumed constant across scales.</li>
|
|
<li>RDF numbers reflect compact modeling and client-side derivation of run-in/steady-state; a SPARQL-side aggregation engine could shift, not remove, the scan penalty.</li>
|
|
<li>Hardware prices are spot street prices (2026-07-11) during a DRAM price surge.</li>
|
|
</ul>
|
|
</div>
|
|
<div class="foot">
|
|
Generated from the LabDataStorageEvaluation pipeline artifacts (run of 2026-07-11, seed 20260711): out/report/REPORT.md, out/bench/results_median.csv, extrapolation.csv, hardware_sizing.csv, storage_sizes.csv, out/config/hw_prices.yaml. The full decision document with charts is out/report/REPORT.md; this page is the interactive companion. Related study: <a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation/raw/branch/master/docs/research/Tribology_FK_Architecture.html" target="_blank" rel="noopener">Foreign-Key Architecture Study</a>.
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= ABOUT ================= -->
|
|
<section id="about">
|
|
<h1>About</h1>
|
|
<p class="sub">Attribution and provenance for this document. It is distributed as a single standalone HTML file; everything needed to verify or reproduce the numbers is referenced below.</p>
|
|
<div class="card">
|
|
<h3>Author</h3>
|
|
<p>This study was conducted by <b>Mary Goncharenko</b>, PhD student at the Tribology Laboratory, University of Florida, Gainesville, FL, during a research internship at the tribology laboratory of Sandia National Laboratories, with an assignment on laboratory ontology development.</p>
|
|
<p>The research was carried out with the technical assistance of her father, <b>Vasiliy Goncharenko</b>, Chief Technology Architect at SoftCreator, LLC (enterprise data architecture across relational and semantic stores; 35 years in technology).</p>
|
|
<p class="muted">Profiles: <a class="ext" href="https://www.linkedin.com/in/marygoncharenko/" target="_blank" rel="noopener">linkedin.com/in/marygoncharenko</a> · <a class="ext" href="https://linkedin.com/in/vasiliy-goncharenko" target="_blank" rel="noopener">linkedin.com/in/vasiliy-goncharenko</a></p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Source repository</h3>
|
|
<p>The complete evaluation pipeline - task specifications, binding conventions, source code, and the generated report - is publicly available at:</p>
|
|
<p class="mono"><a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation" target="_blank" rel="noopener">https://git.boskadoff.com/Public/LabDataStorageEvaluation</a></p>
|
|
<p>This page is the interactive companion to the repository's <span class="mono">out/report/REPORT.md</span>; all numbers embedded here come from the pipeline run of 2026-07-11. The corpus regenerates byte-identically from seed 20260711 (MANIFEST.csv sha256 proof), so every figure is independently reproducible from the repository alone.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Data statement</h3>
|
|
<p>The measurement corpus is simulated: physically plausible models of a Pt-Au LDRD tribology study context at Sandia National Laboratories, generated from a fixed random seed. It contains no experimental measurements and no export-controlled data. Hardware prices cited in the cost model are public street prices retrieved 2026-07-11 (sources in the Hardware & Cost section).</p>
|
|
<p>Companion pages in the same repository: <a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation/raw/branch/master/docs/research/Tribology_Storage_Decision.html" target="_blank" rel="noopener">Storage Decision (one-page executive summary)</a> · <a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation/raw/branch/master/docs/research/Tribology_FK_Architecture.html" target="_blank" rel="noopener">Foreign-Key Architecture Study</a>.</p>
|
|
</div>
|
|
<div class="foot">Standalone distributable file. If you received this document outside the repository, the canonical source is <a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation" target="_blank" rel="noopener">git.boskadoff.com/Public/LabDataStorageEvaluation</a>.</div>
|
|
</section>
|
|
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
"use strict";
|
|
|
|
/* ---------- embedded data (from the 2026-07-11 pipeline run) ---------- */
|
|
const FMT = ["csv","json","sqlite","pg","rdf"];
|
|
const NAME = {csv:"CSV", json:"JSON-LD", sqlite:"SQLite", pg:"PostgreSQL", rdf:"RDF triplestore"};
|
|
const COLOR = {csv:"#9aa6b6", json:"#e0892e", sqlite:"#0f9d8a", pg:"#1f5fdb", rdf:"#c0476b"};
|
|
const QN = [1,2,3,4,5,6,7];
|
|
const QDESC = {
|
|
1:["COF vs cycle curve for one track (B722-W2-C13-T2)",1000],
|
|
2:["Steady-state COF for coupons with mean Au = 10 +/- 0.5 wt%",33],
|
|
3:["Hardness vs steady-state COF across all batches",480],
|
|
4:["Anomaly filter: dry N2, 100 mN, cof_ss > 0.20",469],
|
|
5:["Mean run-in cycles per batch over ALL raw cycles (no summaries)",4],
|
|
6:["Wear volume vs load per coupon",480],
|
|
7:["Stribeck-style mean COF by (environment, speed x load)",2]
|
|
};
|
|
|
|
/* measured medians, per format: [q1..q7] */
|
|
const WALL = {
|
|
csv:[0.06,0.1661,1.1521,0.715,1.0948,0.2756,1.2735],
|
|
json:[0.1113,1.4788,9.9408,9.9218,9.9623,9.7927,10.0781],
|
|
sqlite:[0.0617,0.0607,0.1134,0.0604,0.7659,0.0612,0.7094],
|
|
pg:[0.2214,0.2206,0.2211,0.2232,0.4405,0.2212,0.3342],
|
|
rdf:[0.1131,2.8054,46.1273,19.8245,59.5884,0.1682,40.5534]
|
|
};
|
|
const RSS = {
|
|
csv:[3.9,20.5,21.1,20.9,20.7,20.2,21.0],
|
|
json:[22.2,23.9,24.4,24.5,24.4,24.1,24.1],
|
|
sqlite:[3.9,3.9,4.1,3.9,25.0,3.9,25.0],
|
|
pg:[42.2,42.1,42.0,41.9,42.0,41.6,42.1],
|
|
rdf:[31.6,85.4,263.9,167.9,264.0,45.9,263.6]
|
|
};
|
|
const READ = {
|
|
csv:[0,2.45,31.76,16.82,31.78,1.23,32.47],
|
|
json:[1.4,53.08,297.57,297.75,297.57,297.82,297.94],
|
|
sqlite:[0,0,0,0,117.91,0,92.97],
|
|
pg:[4.08,4.08,4.08,4.08,4.08,4.08,4.08],
|
|
rdf:[3.62,375.56,5445.1,2694.56,5390.46,15.92,5380.51]
|
|
};
|
|
|
|
/* projections: scale -> format -> [[seconds,flag] x7]; flag "" | "I" | "F" */
|
|
const PROJ = {
|
|
600: {
|
|
csv:[[0.06,""],[428.301,""],[4408.0,"I"],[2643.775,""],[4176.725,"I"],[870.266,""],[4897.995,"I"]],
|
|
json:[[0.111,""],[5519.622,"I"],[39673.993,"I"],[39597.306,"I"],[39760.772,"I"],[39076.231,"I"],[40228.164,"I"]],
|
|
sqlite:[[0.062,""],[1.271,""],[339.238,""],[0.06,""],[2847.603,""],[5.18,""],[2619.558,""]],
|
|
pg:[[0.222,""],[0.221,""],[3.42,""],[10.715,""],[222.111,""],[4.06,""],[114.849,""]],
|
|
rdf:[[0.113,""],[10866.789,"I"],[185722.881,"F"],[79559.375,"I"],[240054.647,"F"],[352.729,""],[163225.475,"F"]]
|
|
},
|
|
1200: {
|
|
csv:[[0.06,""],[856.543,""],[8815.94,"I"],[5287.489,"I"],[8353.391,"I"],[1740.472,""],[9795.931,"I"]],
|
|
json:[[0.111,""],[11039.133,"I"],[79347.876,"I"],[79194.5,"I"],[79521.432,"I"],[78152.351,"I"],[80456.218,"I"]],
|
|
sqlite:[[0.063,""],[2.482,""],[699.328,""],[0.06,""],[5695.146,"I"],[10.615,""],[5239.055,"I"]],
|
|
pg:[[0.222,""],[0.221,""],[6.817,""],[21.209,""],[444.001,""],[8.137,""],[229.477,""]],
|
|
rdf:[[0.113,""],[21733.465,"I"],[371445.649,"F"],[159118.636,"F"],[480109.181,"F"],[727.088,""],[326450.837,"F"]]
|
|
},
|
|
6000: {
|
|
csv:[[0.06,""],[4282.474,"I"],[44079.46,"I"],[26437.206,"I"],[41766.715,"I"],[8702.119,"I"],[48979.414,"I"]],
|
|
json:[[0.111,""],[55195.222,"I"],[396738.933,"F"],[395972.054,"F"],[397606.717,"F"],[390761.312,"F"],[402280.643,"F"]],
|
|
sqlite:[[0.063,""],[12.169,""],[3739.195,"I"],[0.06,""],[28475.49,"I"],[56.5,""],[26195.034,"I"]],
|
|
pg:[[0.222,""],[0.221,""],[35.495,""],[105.162,""],[2219.125,""],[42.55,""],[1146.503,""]],
|
|
rdf:[[0.113,""],[108666.874,"F"],[1857227.792,"F"],[795592.727,"F"],[2400545.453,"F"],[3887.402,"I"],[1632253.733,"F"]]
|
|
}
|
|
};
|
|
|
|
/* storage bytes */
|
|
const WORK = {csv:148654472, json:311300222, sqlite:123400192, pg:397467648, rdf:1739414085};
|
|
const ARCH = {csv:[52827471,"tar.gz of the tree"], json:[57162674,"tar.gz (full + hybrid)"], sqlite:[55149977,"gzip of tribo.db"], pg:[53054380,"pg_dump -Fc"], rdf:[126487650,"N-Triples gz"]};
|
|
const COEFF = {csv:1.0, json:2.09, sqlite:0.83, pg:2.67, rdf:11.70};
|
|
|
|
/* hardware sizing: scale -> format -> {modules,drives,nodes,cores,base,ram,disk,extra,total} */
|
|
const HW = {
|
|
600: {
|
|
csv:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
|
|
json:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
|
|
sqlite:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
|
|
pg:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
|
|
rdf:{m:22,d:2,n:2,c:16,base:4618,ram:41514,disk:7990,extra:4618,total:58740}
|
|
},
|
|
1200: {
|
|
csv:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
|
|
json:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
|
|
sqlite:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
|
|
pg:{m:2,d:1,n:1,c:16,base:4618,ram:3774,disk:3995,extra:0,total:12387},
|
|
rdf:{m:44,d:3,n:3,c:16,base:4618,ram:83028,disk:11985,extra:9236,total:108867}
|
|
},
|
|
6000: {
|
|
csv:{m:1,d:2,n:1,c:16,base:4618,ram:1887,disk:7990,extra:0,total:14495},
|
|
json:{m:1,d:3,n:1,c:16,base:4618,ram:1887,disk:11985,extra:0,total:18490},
|
|
sqlite:{m:3,d:1,n:1,c:16,base:4618,ram:5661,disk:3995,extra:0,total:14274},
|
|
pg:{m:9,d:3,n:1,c:16,base:4618,ram:16983,disk:11985,extra:0,total:33586},
|
|
rdf:{m:220,d:12,n:14,c:16,base:4618,ram:415140,disk:47940,extra:60034,total:527732}
|
|
}
|
|
};
|
|
|
|
/* weighted scores: [search, ram, disk, total] */
|
|
const SCORES = {
|
|
measured:{csv:[15.5,20.0,8.3,43.8], json:[1.7,17.2,4.0,22.9], sqlite:[50.0,16.9,10.0,76.9], pg:[26.2,10.0,3.1,39.3], rdf:[1.3,1.6,0.7,3.6]},
|
|
proj:{csv:[0.6,20.0,8.3,28.9], json:[0.1,20.0,4.0,24.0], sqlite:[21.8,15.2,10.0,47.0], pg:[50.0,5.5,3.1,58.6], rdf:[0.0,0.2,0.7,0.9]}
|
|
};
|
|
|
|
/* pg server per-call exec ms + shared blocks (session deltas / 4 calls) */
|
|
const PGSRV = {1:[0.42,72],2:[0.43,1312],3:[4.92,6500],4:[1.09,140],5:[237.26,189692],6:[1.68,172],7:[107.14,32456]};
|
|
|
|
const ENVIRONMENT = [
|
|
"benchmark_host_os: Windows-2025Server-10.0.26100-SP0",
|
|
"benchmark_host_cpu: Intel64 Family 6 Model 141 (4 cores physical/logical), 8 GiB RAM",
|
|
"benchmark_host_disks: VMware Virtual NVMe Disk / SSD / 100 GB",
|
|
"python: 3.12.10 · sqlite 3.49.1 · psycopg 3.3.4 · pyoxigraph 0.5.9 · ijson 3.5.1 · psutil 7.2.2",
|
|
"postgresql_server: PostgreSQL 16.14 (Ubuntu 24.04), remote host, 4 cores / 3.8 GiB",
|
|
"pg tuning: shared_buffers=1GB · work_mem=32MB · effective_cache_size=2500MB · max_parallel_workers_per_gather=4",
|
|
"cold_cache_pass: skipped (Windows host, no page-cache drop)",
|
|
"pg_backend_sampling: remote server - Prometheus windows + pg_stat_statements deltas instead"
|
|
].join("\n");
|
|
|
|
/* ---------- formatting helpers ---------- */
|
|
function fmtTime(s){
|
|
if(s < 10) return s.toFixed(2) + " s";
|
|
if(s < 120) return s.toFixed(0) + " s";
|
|
if(s < 7200) return (s/60).toFixed(1) + " min";
|
|
if(s < 172800) return (s/3600).toFixed(1) + " h";
|
|
return (s/86400).toFixed(1) + " d";
|
|
}
|
|
function fmtSize(b){
|
|
if(b >= 1e12) return (b/1e12).toFixed(2) + " TB";
|
|
if(b >= 1e9) return (b/1e9).toFixed(2) + " GB";
|
|
return (b/1e6).toFixed(0) + " MB";
|
|
}
|
|
function fmtMoney(v){ return "$" + v.toLocaleString("en-US"); }
|
|
function esc(t){ return String(t).replace(/&/g,"&").replace(/</g,"<"); }
|
|
function el(id){ return document.getElementById(id); }
|
|
function flagPill(f){
|
|
if(f === "F") return ' <span class="pill fail">FAIL</span>';
|
|
if(f === "I") return ' <span class="pill imp">> 1 h</span>';
|
|
return "";
|
|
}
|
|
function geomean(a){ return Math.exp(a.reduce((s,v)=>s+Math.log(v),0)/a.length); }
|
|
|
|
/* one horizontal bar row; linear scale against max */
|
|
function barRow(label, sub, frac, color, val){
|
|
return '<div class="hbar"><div class="lab">' + label + (sub ? '<small>' + sub + '</small>' : '') + '</div>' +
|
|
'<div class="trk"><div class="fill" style="width:' + Math.max(0.4, frac*100).toFixed(2) + '%;background:' + color + '"></div></div>' +
|
|
'<div class="val">' + val + '</div></div>';
|
|
}
|
|
function tabs(containerId, items, onPick){
|
|
const box = el(containerId);
|
|
box.innerHTML = items.map((t,i) => '<button data-i="' + i + '"' + (i===0 ? ' class="on"' : '') + '>' + t + '</button>').join("");
|
|
box.querySelectorAll("button").forEach(b => b.addEventListener("click", () => {
|
|
box.querySelectorAll("button").forEach(x => x.classList.remove("on"));
|
|
b.classList.add("on");
|
|
onPick(parseInt(b.dataset.i,10));
|
|
}));
|
|
}
|
|
|
|
/* ---------- navigation ---------- */
|
|
/* menu items only: the sidebar footer also holds a regular external link */
|
|
const navLinks = document.querySelectorAll('nav.side a[href^="#"]');
|
|
function show(id){
|
|
if(!document.getElementById(id)) return;
|
|
document.querySelectorAll("main section").forEach(s => s.classList.toggle("on", s.id === id));
|
|
navLinks.forEach(a => a.classList.toggle("active", a.getAttribute("href") === "#" + id));
|
|
el("main").scrollTop = 0;
|
|
}
|
|
navLinks.forEach(a => a.addEventListener("click", e => {
|
|
e.preventDefault();
|
|
const id = a.getAttribute("href").slice(1);
|
|
show(id);
|
|
history.replaceState(null, "", "#" + id);
|
|
}));
|
|
if(location.hash && document.getElementById(location.hash.slice(1))) show(location.hash.slice(1));
|
|
|
|
/* ---------- dashboard ---------- */
|
|
(function(){
|
|
const rows = FMT.map(f => {
|
|
const g150 = geomean(WALL[f]);
|
|
const g600 = geomean(PROJ[600][f].map(x => x[0]));
|
|
return [NAME[f], "geometric mean of Q1-Q7", g150, fmtTime(g150) + ' <span class="muted">(' + fmtTime(g600) + ' @600 GB)</span>', COLOR[f]];
|
|
});
|
|
const max = Math.max(...rows.map(r => r[2]));
|
|
el("dash-geo").innerHTML = rows.map(r => barRow(r[0], r[1], r[2]/max, r[4], r[3])).join("");
|
|
|
|
const breaks = {
|
|
csv:["Direct path reads; canonical archive; best compression","Full scans pass 1 h just above 600 GB"],
|
|
json:["Self-describing exchange files; hybrid variant is 1.9 MB","Every scan reads the whole corpus: hours at 600 GB"],
|
|
sqlite:["Fastest at 150 MB; one file; smallest footprint","Single-threaded scans pass 1 h at ~1.2 TB"],
|
|
pg:["Parallel partitioned scans + matviews; flat 0.2-0.4 s today","None found up to 6 TB (Q5 = 37 min)"],
|
|
rdf:["Native semantics, SPARQL","11.7x disk, 20% hot set in RAM, scans measured in days"]
|
|
};
|
|
el("dash-breaks").innerHTML = FMT.map(f => {
|
|
const fl = PROJ[600][f];
|
|
const nI = fl.filter(x => x[1]==="I").length, nF = fl.filter(x => x[1]==="F").length;
|
|
let flags = (nI+nF)===0 ? '<span class="pill ok">all 7 OK</span>' :
|
|
(nI ? '<span class="pill imp">' + nI + ' x >1 h</span> ' : '') + (nF ? '<span class="pill fail">' + nF + ' x FAIL</span>' : '');
|
|
return "<tr><td><b>" + NAME[f] + "</b></td><td>" + breaks[f][0] + "</td><td>" + breaks[f][1] + "</td><td>" + flags + "</td></tr>";
|
|
}).join("");
|
|
})();
|
|
|
|
/* ---------- methodology ---------- */
|
|
el("method-queries").innerHTML = QN.map(q =>
|
|
'<tr><td class="mono">Q' + q + '</td><td>' + QDESC[q][0] + '</td><td class="num">' + QDESC[q][1].toLocaleString("en-US") + '</td></tr>').join("");
|
|
el("method-env").innerHTML = ENVIRONMENT;
|
|
|
|
/* ---------- storage ---------- */
|
|
(function(){
|
|
const workMax = WORK.rdf;
|
|
el("st-work").innerHTML = FMT.map(f =>
|
|
barRow(NAME[f], COEFF[f].toFixed(2) + "x vs raw CSV", WORK[f]/workMax, COLOR[f], fmtSize(WORK[f]))).join("");
|
|
const archMax = ARCH.rdf[0];
|
|
el("st-arch").innerHTML = FMT.map(f =>
|
|
barRow(NAME[f], ARCH[f][1], ARCH[f][0]/archMax, COLOR[f], fmtSize(ARCH[f][0]))).join("");
|
|
el("st-proj").innerHTML = FMT.map(f =>
|
|
"<tr><td><b>" + NAME[f] + "</b></td>" + [600,1200,6000].map(s =>
|
|
'<td class="num">' + fmtSize(COEFF[f]*s*1e9) + "</td>").join("") + "</tr>").join("");
|
|
})();
|
|
|
|
/* ---------- measured results ---------- */
|
|
(function(){
|
|
const METRICS = [
|
|
["Wall time", WALL, fmtTime],
|
|
["Client peak RSS", RSS, v => v.toFixed(0) + " MB"],
|
|
["Read bytes", READ, v => v >= 1000 ? (v/1000).toFixed(2) + " GB" : v.toFixed(1) + " MB"]
|
|
];
|
|
let q = 0, m = 0;
|
|
function render(){
|
|
const [mname, data, fmt] = METRICS[m];
|
|
el("m-title").textContent = "Q" + QN[q] + " - " + QDESC[QN[q]][0] + " - " + mname.toLowerCase() + " (median of 3 runs)";
|
|
const max = Math.max(...FMT.map(f => data[f][q])) || 1;
|
|
el("m-bars").innerHTML = FMT.map(f =>
|
|
barRow(NAME[f], "", (data[f][q])/max, COLOR[f], fmt(data[f][q]))).join("");
|
|
}
|
|
tabs("mq-tabs", QN.map(n => "Q" + n), i => { q = i; render(); });
|
|
tabs("mm-tabs", METRICS.map(x => x[0]), i => { m = i; render(); });
|
|
render();
|
|
|
|
el("m-mat-h").innerHTML = "<tr><th>Format</th>" + QN.map(n => '<th class="num">Q' + n + "</th>").join("") + "</tr>";
|
|
el("m-mat").innerHTML = FMT.map(f =>
|
|
"<tr><td><b>" + NAME[f] + "</b></td>" + WALL[f].map(v => '<td class="num">' + fmtTime(v) + "</td>").join("") + "</tr>").join("");
|
|
|
|
el("m-pgsrv").innerHTML = QN.map(n =>
|
|
'<tr><td class="mono">Q' + n + "</td><td class=\"num\">" + PGSRV[n][0].toFixed(2) + " ms</td><td class=\"num\">" +
|
|
PGSRV[n][1].toLocaleString("en-US") + '</td><td class="num">0</td></tr>').join("");
|
|
})();
|
|
|
|
/* ---------- projections ---------- */
|
|
(function(){
|
|
const SCALES = [600, 1200, 6000];
|
|
const SLBL = {600:"600 GB", 1200:"1.2 TB", 6000:"6 TB"};
|
|
function render(scale){
|
|
el("p-title").textContent = "Projected wall times at " + SLBL[scale] + " of raw data";
|
|
el("p-mat-h").innerHTML = "<tr><th>Format</th>" + QN.map(n => '<th class="num">Q' + n + "</th>").join("") + "</tr>";
|
|
el("p-mat").innerHTML = FMT.map(f =>
|
|
"<tr><td><b>" + NAME[f] + "</b></td>" + PROJ[scale][f].map(x =>
|
|
'<td class="num">' + fmtTime(x[0]) + flagPill(x[1]) + "</td>").join("") + "</tr>").join("");
|
|
const total = FMT.reduce((acc,f) => {
|
|
PROJ[scale][f].forEach(x => { if(x[1]==="I") acc.i++; if(x[1]==="F") acc.f++; });
|
|
return acc;
|
|
}, {i:0, f:0});
|
|
el("p-note").textContent = "Flags at this scale: " + total.i + " x IMPRACTICAL (over 1 hour), " + total.f +
|
|
" x FAIL (over 24 hours), " + (35 - total.i - total.f) + " x OK. Scaling ratio vs the measured corpus: x" +
|
|
Math.round(scale*1e9/WORK.csv).toLocaleString("en-US") + ".";
|
|
}
|
|
tabs("p-tabs", SCALES.map(s => SLBL[s]), i => render(SCALES[i]));
|
|
render(600);
|
|
|
|
/* degradation mini-table: full scan class geometric mean */
|
|
const cols = [["measured 150 MB", f => geomean([WALL[f][4], WALL[f][6]])]]
|
|
.concat(SCALES.map(s => ["projected " + SLBL[s], f => geomean([PROJ[s][f][4][0], PROJ[s][f][6][0]])]));
|
|
let html = '<table class="data"><thead><tr><th>Format</th>' +
|
|
cols.map(c => '<th class="num">' + c[0] + "</th>").join("") + "</tr></thead><tbody>";
|
|
html += FMT.map(f => "<tr><td><b>" + NAME[f] + "</b></td>" +
|
|
cols.map(c => '<td class="num">' + fmtTime(c[1](f)) + "</td>").join("") + "</tr>").join("");
|
|
html += "</tbody></table>";
|
|
el("p-degr").innerHTML = html;
|
|
})();
|
|
|
|
/* ---------- hardware ---------- */
|
|
(function(){
|
|
const SCALES = [600, 1200, 6000];
|
|
const SLBL = {600:"600 GB", 1200:"1.2 TB", 6000:"6 TB"};
|
|
function config(h){
|
|
return h.n + " x 1U Supermicro AS-1015CS-TNR (" + h.c + " cores) · " +
|
|
h.m + " x 64 GB DDR5 ECC RDIMM · " + h.d + " x 7.68 TB NVMe U.2";
|
|
}
|
|
function render(scale){
|
|
el("h-title").textContent = "Exact configuration and cost breakdown at " + SLBL[scale] + " (projected sizing)";
|
|
el("h-bom").innerHTML = FMT.map(f => {
|
|
const h = HW[scale][f];
|
|
return "<tr><td><b>" + NAME[f] + "</b></td><td>" + config(h) + "</td>" +
|
|
[h.base, h.ram, h.disk, h.extra, h.total].map((v,i) =>
|
|
'<td class="num"' + (i===4 ? ' style="font-weight:600"' : '') + ">" + fmtMoney(v) + "</td>").join("") + "</tr>";
|
|
}).join("");
|
|
const max = Math.max(...FMT.map(f => HW[scale][f].total));
|
|
el("h-bars").innerHTML = FMT.map(f => {
|
|
const h = HW[scale][f];
|
|
return barRow(NAME[f], h.n > 1 ? h.n + " nodes" : "single node", h.total/max, COLOR[f], fmtMoney(h.total));
|
|
}).join("");
|
|
scatter(scale);
|
|
}
|
|
function scatter(scale){
|
|
const W = 860, H = 340, L = 70, R = 30, T = 20, B = 46;
|
|
const pts = FMT.map(f => ({f, cost: HW[scale][f].total, geo: geomean(PROJ[scale][f].map(x => x[0]))}));
|
|
const maxC = Math.max(...pts.map(p => p.cost)) * 1.15;
|
|
const maxG = Math.max(...pts.map(p => p.geo)) * 1.15;
|
|
const X = c => L + (W-L-R) * c / maxC;
|
|
const Y = g => T + (H-T-B) * (1 - g / maxG);
|
|
let s = '<svg viewBox="0 0 ' + W + ' ' + H + '" style="width:100%;max-width:900px">';
|
|
/* axes + linear ticks in real units */
|
|
for(let i = 1; i <= 4; i++){
|
|
const c = maxC * i / 4, x = X(c);
|
|
s += '<line x1="' + x + '" y1="' + T + '" x2="' + x + '" y2="' + (H-B) + '" stroke="#eef0f2"/>';
|
|
s += '<text x="' + x + '" y="' + (H-B+18) + '" font-size="11" fill="#838a95" text-anchor="middle">$' + Math.round(c/1000) + 'k</text>';
|
|
const g = maxG * i / 4, y = Y(g);
|
|
s += '<line x1="' + L + '" y1="' + y + '" x2="' + (W-R) + '" y2="' + y + '" stroke="#eef0f2"/>';
|
|
s += '<text x="' + (L-8) + '" y="' + (y+4) + '" font-size="11" fill="#838a95" text-anchor="end">' + fmtTime(g) + '</text>';
|
|
}
|
|
s += '<line x1="' + L + '" y1="' + (H-B) + '" x2="' + (W-R) + '" y2="' + (H-B) + '" stroke="#cfd4da"/>';
|
|
s += '<line x1="' + L + '" y1="' + T + '" x2="' + L + '" y2="' + (H-B) + '" stroke="#cfd4da"/>';
|
|
s += '<text x="' + ((L+W-R)/2) + '" y="' + (H-6) + '" font-size="11.5" fill="#5b6472" text-anchor="middle">estimated hardware cost (bill of materials, linear)</text>';
|
|
s += '<text x="14" y="' + ((T+H-B)/2) + '" font-size="11.5" fill="#5b6472" text-anchor="middle" transform="rotate(-90 14 ' + ((T+H-B)/2) + ')">typical query time, projected (linear)</text>';
|
|
/* better = lower-left cue */
|
|
s += '<text x="' + (L+16) + '" y="' + (H-B-10) + '" font-size="11.5" fill="#0f7a45" font-weight="600">better: lower-left</text>';
|
|
const OFF = {csv:[12,-8], json:[12,4], sqlite:[12,14], pg:[12,-6], rdf:[-8,-12]};
|
|
const ANCH = {csv:"start", json:"start", sqlite:"start", pg:"start", rdf:"end"};
|
|
pts.forEach(p => {
|
|
s += '<circle cx="' + X(p.cost) + '" cy="' + Y(p.geo) + '" r="7" fill="' + COLOR[p.f] + '"/>';
|
|
s += '<text x="' + (X(p.cost)+OFF[p.f][0]) + '" y="' + (Y(p.geo)+OFF[p.f][1]) + '" font-size="11.5" fill="#3d424b" text-anchor="' + ANCH[p.f] + '">' +
|
|
NAME[p.f] + ': ' + fmtMoney(p.cost) + ', ' + fmtTime(p.geo) + '</text>';
|
|
});
|
|
s += "</svg>";
|
|
el("h-scatter").innerHTML = s;
|
|
}
|
|
tabs("h-tabs", SCALES.map(s => SLBL[s]), i => render(SCALES[i]));
|
|
render(600);
|
|
})();
|
|
|
|
/* ---------- scoring ---------- */
|
|
function renderScores(containerId, board){
|
|
const order = FMT.slice().sort((a,b) => board[b][3] - board[a][3]);
|
|
el(containerId).innerHTML = order.map(f => {
|
|
const [se, ra, di, total] = board[f];
|
|
return '<div class="stack"><div class="lab">' + NAME[f] + '</div><div class="trk">' +
|
|
'<div class="seg" style="width:' + (se/80*100) + '%;background:#1f5fdb"></div>' +
|
|
'<div class="seg" style="width:' + (ra/80*100) + '%;background:#e0892e"></div>' +
|
|
'<div class="seg" style="width:' + (di/80*100) + '%;background:#0f9d8a"></div>' +
|
|
'</div><div class="val">' + total.toFixed(1) + '</div></div>';
|
|
}).join("");
|
|
}
|
|
renderScores("sc-measured", SCORES.measured);
|
|
renderScores("sc-proj", SCORES.proj);
|
|
</script>
|
|
</body>
|
|
</html>
|