diff --git a/.gitignore b/.gitignore index 497a38e..e133570 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ benchmarks/results/ Thumbs.db .mvn/wrapper/maven-wrapper.jar +benchmarks/report/gen/ +benchmarks/report/report.pdf diff --git a/README.md b/README.md index 8bd6168..940043b 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ other hyperedges. HStore stores this structure directly instead of reifying it i relational join tables. The engine implements the design of *Native Persistent Hypergraph Storage Engine* together with its -higher-order extension. Membership and incidence are copy-on-write counted B+trees with monoid summaries. -Commits are atomic root swaps. The database layer adds a query language (HQL), hypergraph relational operators, -a semantic plane, multi-tenancy and a web console. +higher-order extension. Membership and incidence are copy-on-write counted B+trees with monoid summaries. A +commit logs only the keys it changed and publishes a new immutable generation; a background checkpoint later +writes the changed tree nodes to pages, once for all the commits since the last one. The database layer adds a +query language (HQL), hypergraph relational operators, a semantic plane, multi-tenancy and a web console. It is written in Java 25 and ships as one GraalVM native executable, `hstore`. @@ -21,6 +22,11 @@ It is written in Java 25 and ships as one GraalVM native executable, `hstore`. * **Transactions.** Snapshot isolation by default and `SERIALIZABLE` on request. Optimistic concurrency with commutative rebase of set operations. Group commit. Two WAL modes (page references or page images). Crash recovery that never exposes a partial commit. +* **Small commits.** A commit writes a log record of the keys it changed, a few hundred bytes, instead of new + pages. Checkpoints write each changed node once, and `write_buffer_mb` caps the memory held by nodes that + haven't been written yet. +* **Space reclamation.** Pages carry reference counts. Compaction moves the live pages out of mostly dead + segments without rewriting the trees that point at them, while readers keep running. * **Leapfrog incidence intersection.** Co-membership queries intersect sorted incidence trees and skip with subtree summaries. * **Branches.** `CREATE BRANCH`, isolated what-if writes, `DIFF`, three-way `MERGE … ON CONFLICT`. @@ -38,6 +44,27 @@ It is written in Java 25 and ships as one GraalVM native executable, `hstore`. spokes, hulls, ordered chains, time-travel slider), a schema browser, a live engine dashboard and access management. +## Performance + +HStore against HyperGraphDB 1.4 on Berkeley DB JE, both with 64 MiB caches, at scale 1 (50,000 nodes, 100,000 +hyperedges) with `async` durability. Each figure is the median of 5 runs on one laptop: + +| Workload | HStore | HyperGraphDB | | +|---|---:|---:|---:| +| Incidence set of a node | 6.26 M/s | 1.49 M/s | 4.2× faster | +| Hyperedges shared by two nodes | 610 k/s | 53.9 k/s | 11.3× faster | +| Value updates, 1,000 per transaction | 69.9 k/s | 13.0 k/s | 5.4× faster | +| Bytes written per single-update commit | 659 B | 6.9 KiB | 10.7× fewer | +| Commit latency, p99 | 296 µs | 466 µs | 1.6× lower | +| Disk after loading | 92 MiB | 346 MiB | 3.7× smaller | + +At scale 4 (200,000 nodes, 400,000 hyperedges), where the files of both stores outgrow their caches, the +leads widen: incidence sets 11.4×, value updates 33×, and listing a hyperedge's members 3.2× faster. +HyperGraphDB opens a store after a crash faster at scale 1, 76 ms against 128 ms. With `sync` durability, +single commits in both are bound by the cost of a forced write. The +[benchmark report](docs/benchmark-report.pdf) (PDF) covers every workload, configuration and caveat, and +[docs/benchmarks.md](docs/benchmarks.md) explains how to rerun it. + ## Quick start with Docker Images for linux/amd64 and linux/arm64 are published to the GitHub Container Registry with every release: @@ -173,6 +200,7 @@ The full index is [docs/README.md](docs/README.md). | Wire protocol | [docs/operations/wire-protocol.md](docs/operations/wire-protocol.md) | | HStore Studio | [docs/operations/studio.md](docs/operations/studio.md) | | Benchmarks against HyperGraphDB | [docs/benchmarks.md](docs/benchmarks.md) | +| Benchmark report (PDF) | [docs/benchmark-report.pdf](docs/benchmark-report.pdf) | | Development | [docs/development.md](docs/development.md) | ## Contributing diff --git a/benchmarks/report/gen.py b/benchmarks/report/gen.py new file mode 100644 index 0000000..ecf1887 --- /dev/null +++ b/benchmarks/report/gen.py @@ -0,0 +1,608 @@ +#!/usr/bin/env python3 +import json +import glob +import os +import statistics +import sys + +BENCH = sys.argv[1] +OUT = sys.argv[2] +os.makedirs(OUT, exist_ok=True) + +CONFIGS = { + "s1-async": "Scale 1, asynchronous commits", + "s1-sync": "Scale 1, synchronous commits", + "s4-async": "Scale 4, asynchronous commits", +} +ENGINES = ("hstore", "hypergraphdb") +KEYS = {"s1-async": "A", "s1-sync": "B", "s4-async": "C"} +PCT = {"p50": "med", "p99": "ptwo", "p99.9": "pthree", "max": "max"} + +THROUGHPUT = [ + ("ingest.nodes", "Insert nodes (1,000 per txn)"), + ("ingest.edges", "Insert hyperedges (1,000 per txn)"), + ("write.update", "Update node values (1,000 per txn)"), + ("latency.commit", "Single-update transactions"), + ("churn.delete", "Delete hyperedges (1,000 per txn)"), + ("churn.remove", "Remove one member (1,000 per txn)"), + ("large.ingest", "Build one giant hyperedge"), + ("read.incidence", "Incidence set of a node"), + ("read.members", "Members of a hyperedge"), + ("read.comembership", "Co-membership count of a pair"), + ("read.twohop", "Two-hop neighbourhood"), + ("read.incidence.parallel", "Incidence sets, 8 threads"), + ("latency.read", "One lookup per read txn"), + ("read.incidence.cold", "Incidence sets after reopen"), + ("read.incidence.churned", "Incidence sets after deletes"), + ("large.scan", "Scan the giant hyperedge"), + ("large.probe", "Membership probe, giant hyperedge"), + ("mixed.read", "Reads beside a 1,000 updates/s writer"), +] +TIMES = [("reopen", "Clean reopen"), ("recover", "Open after SIGKILL")] +SIZES = [("disk", "After load"), ("disk.churned", "After deletes"), ("disk.compacted", "After compaction")] +LATENCY = [("latency.read", "Read transaction"), ("latency.commit", "Commit"), + ("mixed.read", "Read beside writer"), ("mixed.write", "Commit beside readers"), + ("compaction.baseline", "Commit, no compaction"), ("compaction", "Commit during compaction")] +WRITES = ["ingest.nodes", "ingest.edges", "write.update", "latency.commit", "mixed.write", + "large.ingest", "churn.delete", "churn.remove"] +READS_IO = ["read.incidence.cold"] + + +def load(cfg): + runs = {} + for engine in ENGINES: + files = sorted(glob.glob(f"{BENCH}/{cfg}/{engine}-*.json")) + runs[engine] = [json.load(open(f)) for f in files] + return runs + + +def results(run): + return {r["workload"]: r for r in run["results"]} + + +def series(runs, workload, metric): + values = [] + for run in runs: + r = results(run).get(workload) + if r is not None: + v = metric(r) + if v is not None: + values.append(v) + return values + + +def ops(r): + return r["operations"] / (r["millis"] / 1000.0) + + +def med(values): + return statistics.median(values) if values else None + + +def fmt_rate(v): + if v >= 1e9: + return f"{v / 1e9:.2f}\\,G" + if v >= 1e6: + return f"{v / 1e6:.2f}\\,M" + if v >= 1e3: + return f"{v / 1e3:.1f}\\,k" + return f"{v:.0f}" + + +def fmt_int(v): + return f"{v:,.0f}".replace(",", "{,}") + + +def fmt_us(v): + if v >= 1000: + return f"{v / 1000:.2f}\\,ms" + if v >= 10: + return f"{v:.0f}\\,\\textmu s" + return f"{v:.1f}\\,\\textmu s" + + +def fmt_bytes(v): + if v < 0: + return "$-$" + fmt_bytes(-v) + if v >= 1 << 30: + return f"{v / (1 << 30):.2f}\\,GiB" + if v >= 1 << 20: + return f"{v / (1 << 20):.1f}\\,MiB" + if v >= 1 << 10: + return f"{v / (1 << 10):.1f}\\,KiB" + return f"{v:.0f}\\,B" + + +def fmt_ratio(v): + if v is None: + return "--" + if v >= 100: + return f"{v:.0f}$\\times$" + if v >= 10: + return f"{v:.1f}$\\times$" + return f"{v:.2f}$\\times$" + + +def agree(runs, workload): + sums = set() + for engine in ENGINES: + for run in runs[engine]: + r = results(run).get(workload) + if r is None: + return "--" + if r.get("checked") is False: + return "n/a" + sums.add(r["checksum"]) + return "yes" if len(sums) == 1 else "\\textbf{no}" + + +def rng(values, f): + return f"{f(min(values))}--{f(max(values))}" + + +macros = {} + + +def macro(name, value): + macros[name] = value + + +def main_table(cfg, runs): + rows = [] + for workload, label in THROUGHPUT: + h = series(runs["hstore"], workload, ops) + g = series(runs["hypergraphdb"], workload, ops) + if not h or not g: + continue + ratio = med(h) / med(g) + rows.append((workload, label, fmt_rate(med(h)), fmt_rate(med(g)), ratio, agree(runs, workload))) + for workload, label in TIMES: + h = series(runs["hstore"], workload, lambda r: r["millis"]) + g = series(runs["hypergraphdb"], workload, lambda r: r["millis"]) + if not h or not g: + continue + rows.append((workload, label, fmt_int(med(h)) + "\\,ms", fmt_int(med(g)) + "\\,ms", med(g) / med(h), agree(runs, workload))) + for workload, label in SIZES: + h = series(runs["hstore"], workload, lambda r: r["operations"]) + g = series(runs["hypergraphdb"], workload, lambda r: r["operations"]) + if not h or not g: + continue + rows.append((workload, "Disk, " + label.lower(), fmt_bytes(med(h)), fmt_bytes(med(g)), med(g) / med(h), "n/a")) + return rows + + +def write_main(cfg, rows): + key = KEYS[cfg] + with open(f"{OUT}/main-{cfg}.tex", "w") as f: + f.write("\\begin{tabular}{@{}llrrrc@{}}\n\\toprule\n") + f.write("Workload & Operation & HStore & HyperGraphDB & Ratio & Agree \\\\\n\\midrule\n") + section = None + for workload, label, h, g, ratio, ok in rows: + kind = "write" if workload in ("ingest.nodes", "ingest.edges", "write.update", "latency.commit", "churn.delete", "churn.remove", "large.ingest") \ + else "time" if workload in ("reopen", "recover") else "size" if workload.startswith("disk") else "read" + if section is not None and kind != section: + f.write("\\midrule\n") + section = kind + win = "\\textbf{" + fmt_ratio(ratio) + "}" if ratio >= 1 else fmt_ratio(ratio) + f.write(f"\\texttt{{{workload}}} & {label} & {h} & {g} & {win} & {ok} \\\\\n") + f.write("\\bottomrule\n\\end{tabular}\n") + for workload, label, h, g, ratio, ok in rows: + macro(f"ratio{key}{workload.replace('.', '')}", fmt_ratio(ratio)) + macro(f"h{key}{workload.replace('.', '')}", h) + macro(f"g{key}{workload.replace('.', '')}", g) + + +def write_ratio_figure(cfg, rows): + with open(f"{OUT}/ratios-{cfg}.tex", "w") as f: + names = [r[0] for r in rows] + f.write("\\def\\ratiosymbols{" + ",".join(names) + "}\n") + f.write("\\def\\ratiocoords{" + " ".join(f"({r[4]:.4f},{r[0]})" for r in rows) + "}\n") + + +def latency_table(cfg, runs): + key = KEYS[cfg] + with open(f"{OUT}/latency-{cfg}.tex", "w") as f: + f.write("\\begin{tabular}{@{}llrrrrrrrr@{}}\n\\toprule\n") + f.write(" & & \\multicolumn{4}{c}{HStore} & \\multicolumn{4}{c}{HyperGraphDB} \\\\\n") + f.write("\\cmidrule(lr){3-6}\\cmidrule(l){7-10}\n") + f.write("Workload & Operation & p50 & p99 & p99.9 & max & p50 & p99 & p99.9 & max \\\\\n\\midrule\n") + for workload, label in LATENCY: + cells = [] + for engine in ENGINES: + for p in ("p50", "p99", "p99.9", "max"): + v = series(runs[engine], workload, lambda r, p=p: r.get("latency", {}).get(p)) + cells.append(fmt_us(med(v)) if v else "--") + if v: + macro(f"lat{key}{engine.replace('hypergraphdb', 'hgdb')}{workload.replace('.', '')}{PCT[p]}", fmt_us(med(v))) + if p == "max": + macro(f"lat{key}{engine.replace('hypergraphdb', 'hgdb')}{workload.replace('.', '')}worst", fmt_us(max(v))) + if all(c == "--" for c in cells): + continue + f.write(f"\\texttt{{{workload}}} & {label} & " + " & ".join(cells) + " \\\\\n") + f.write("\\bottomrule\n\\end{tabular}\n") + with open(f"{OUT}/cdf-{cfg}.tex", "w") as f: + for workload in ("latency.read", "latency.commit", "mixed.write"): + for engine in ENGINES: + pts = [] + for i, p in enumerate(("p50", "p99", "p99.9", "max")): + v = series(runs[engine], workload, lambda r, p=p: r.get("latency", {}).get(p)) + if v: + pts.append(f"({i},{med(v):.4f})") + name = f"lat{workload.replace('.', '')}{engine}" + f.write(f"\\def\\{name.replace('hypergraphdb', 'hgdb')}{{{' '.join(pts)}}}\n") + + +def resources_table(cfg, runs): + key = KEYS[cfg] + rows = [] + for workload in WRITES: + cells = [] + for engine in ENGINES: + per = series(runs[engine], workload, lambda r: r["bytesWritten"] / r["operations"] if r.get("bytesWritten") is not None else None) + amp = series(runs[engine], workload, lambda r: r["bytesWritten"] / r["logicalWritten"] if r.get("logicalWritten") else None) + cells.append((med(per), med(amp))) + if cells[0][0] is None or cells[1][0] is None: + continue + rows.append((workload, cells)) + macro(f"bpo{key}hstore{workload.replace('.', '')}", fmt_bytes(cells[0][0])) + macro(f"bpo{key}hgdb{workload.replace('.', '')}", fmt_bytes(cells[1][0])) + with open(f"{OUT}/writes-{cfg}.tex", "w") as f: + f.write("\\begin{tabular}{@{}lrrrrr@{}}\n\\toprule\n") + f.write(" & \\multicolumn{2}{c}{Bytes written per operation} & \\multicolumn{2}{c}{Write amplification} & \\\\\n") + f.write("\\cmidrule(lr){2-3}\\cmidrule(lr){4-5}\n") + f.write("Workload & HStore & HyperGraphDB & HStore & HyperGraphDB & Ratio \\\\\n\\midrule\n") + for workload, cells in rows: + (hb, ha), (gb, ga) = cells + f.write(f"\\texttt{{{workload}}} & {fmt_bytes(hb)} & {fmt_bytes(gb)} & {ha:.1f}$\\times$ & {ga:.1f}$\\times$ & \\textbf{{{fmt_ratio(gb / hb)}}} \\\\\n") + f.write("\\bottomrule\n\\end{tabular}\n") + with open(f"{OUT}/writes-fig-{cfg}.tex", "w") as f: + f.write("\\def\\writesymbols{" + ",".join(w for w, _ in rows) + "}\n") + f.write("\\def\\writeh{" + " ".join(f"({w},{c[0][0]:.1f})" for w, c in rows) + "}\n") + f.write("\\def\\writeg{" + " ".join(f"({w},{c[1][0]:.1f})" for w, c in rows) + "}\n") + + with open(f"{OUT}/memory-{cfg}.tex", "w") as f: + f.write("\\begin{tabular}{@{}lrrrr@{}}\n\\toprule\n") + f.write(" & \\multicolumn{2}{c}{Heap allocated per operation} & \\multicolumn{2}{c}{GC pause (ms)} \\\\\n") + f.write("\\cmidrule(lr){2-3}\\cmidrule(l){4-5}\n") + f.write("Workload & HStore & HyperGraphDB & HStore & HyperGraphDB \\\\\n\\midrule\n") + for workload, _ in THROUGHPUT + TIMES: + a = [series(runs[e], workload, lambda r: r["allocated"] / r["operations"] if r.get("allocated") is not None else None) for e in ENGINES] + g = [series(runs[e], workload, lambda r: r.get("gcMillis")) for e in ENGINES] + if not a[0] or not a[1]: + continue + f.write(f"\\texttt{{{workload}}} & {fmt_bytes(med(a[0]))} & {fmt_bytes(med(a[1]))} & {med(g[0]):.0f} & {med(g[1]):.0f} \\\\\n") + macro(f"alloc{key}hstore{workload.replace('.', '')}", fmt_bytes(med(a[0]))) + macro(f"alloc{key}hgdb{workload.replace('.', '')}", fmt_bytes(med(a[1]))) + f.write("\\bottomrule\n\\end{tabular}\n") + + retained = [med([run["retainedHeap"] for run in runs[e]]) for e in ENGINES] + total = [med([run["bytesWritten"] for run in runs[e]]) for e in ENGINES] + macro(f"retained{key}hstore", fmt_bytes(retained[0])) + macro(f"retained{key}hgdb", fmt_bytes(retained[1])) + macro(f"totalwritten{key}hstore", fmt_bytes(total[0])) + macro(f"totalwritten{key}hgdb", fmt_bytes(total[1])) + for e, short in zip(ENGINES, ("hstore", "hgdb")): + comp = series(runs[e], "compaction", lambda r: r.get("compactionMillis")) + recl = series(runs[e], "compaction", lambda r: r.get("reclaimed")) + lost = series(runs[e], "recover", lambda r: r.get("lost")) + ack = series(runs[e], "recover", lambda r: r.get("acknowledged")) + if comp: + macro(f"compms{key}{short}", fmt_int(med(comp)) + "\\,ms") + if recl: + macro(f"reclaimed{key}{short}", fmt_bytes(med(recl))) + if lost: + macro(f"lost{key}{short}", f"{max(lost):.0f}") + macro(f"ack{key}{short}", fmt_int(med(ack))) + mix = series(runs[e], "mixed.write", ops) + if mix: + macro(f"mixmin{key}{short}", fmt_int(min(mix))) + cold = series(runs[e], "read.incidence.cold", lambda r: r["bytesRead"] / r["operations"] if r.get("bytesRead") is not None else None) + if cold: + macro(f"coldread{key}{short}", fmt_bytes(med(cold))) + + +def runs_table(cfg, runs): + with open(f"{OUT}/runs-{cfg}.tex", "w") as f: + n = max(len(runs[e]) for e in ENGINES) + f.write("\\begin{longtable}{@{}ll" + "r" * n + "@{}}\n\\toprule\n") + f.write("Workload & Engine & " + " & ".join(f"Run {i + 1}" for i in range(n)) + " \\\\\n\\midrule\n\\endhead\n") + for workload, _ in THROUGHPUT + TIMES + SIZES: + for e, name in zip(ENGINES, ("HStore", "HyperGraphDB")): + cells = [] + for run in runs[e]: + r = results(run).get(workload) + if r is None: + cells.append("--") + elif workload in dict(TIMES): + cells.append(f"{r['millis']:.0f}\\,ms") + elif workload.startswith("disk"): + cells.append(fmt_bytes(r["operations"])) + else: + cells.append(fmt_rate(ops(r))) + cells += [""] * (n - len(cells)) + label = f"\\texttt{{{workload}}}" if e == "hstore" else "" + f.write(f"{label} & {name} & " + " & ".join(cells) + " \\\\\n") + f.write("\\addlinespace[2pt]\n") + f.write("\\bottomrule\n\\end{longtable}\n") + + +def ranges_table(cfg, runs): + with open(f"{OUT}/ranges-{cfg}.tex", "w") as f: + f.write("\\begin{tabular}{@{}lrrrr@{}}\n\\toprule\n") + f.write(" & \\multicolumn{2}{c}{HStore} & \\multicolumn{2}{c}{HyperGraphDB} \\\\\n") + f.write("\\cmidrule(lr){2-3}\\cmidrule(l){4-5}\n") + f.write("Workload & min--max & CV & min--max & CV \\\\\n\\midrule\n") + for workload, _ in THROUGHPUT: + cells = [] + for e in ENGINES: + v = series(runs[e], workload, ops) + if not v: + cells += ["--", "--"] + continue + cv = statistics.pstdev(v) / statistics.mean(v) * 100 if len(v) > 1 else 0 + cells += [rng(v, fmt_rate), f"{cv:.0f}\\%"] + f.write(f"\\texttt{{{workload}}} & " + " & ".join(cells) + " \\\\\n") + f.write("\\bottomrule\n\\end{tabular}\n") + + +def environment(cfg, runs): + key = KEYS[cfg] + h = runs["hstore"][0] + macro(f"nodes{key}", fmt_int(h["nodes"])) + macro(f"edges{key}", fmt_int(h["edges"])) + macro(f"incidences{key}", fmt_int(h["incidences"])) + macro(f"runs{key}", str(min(len(runs[e]) for e in ENGINES))) + + + +def metric_for(workload): + if workload in dict(TIMES): + return (lambda r: r["millis"]), True + if workload.startswith("disk"): + return (lambda r: r["operations"]), True + return ops, False + + +def summary(all_runs): + cfgs = [c for c in CONFIGS if c in all_runs] + names = {"s1-async": "S1 async", "s1-sync": "S1 sync", "s4-async": "S4 async"} + with open(f"{OUT}/summary.tex", "w") as f: + f.write("\\begin{tabular}{@{}l" + "r" * len(cfgs) + "@{}}\n\\toprule\n") + f.write("Workload & " + " & ".join(names[c] for c in cfgs) + " \\\\\n\\midrule\n") + groups = [THROUGHPUT, TIMES, SIZES] + for gi, group in enumerate(groups): + if gi: + f.write("\\midrule\n") + for workload, _ in group: + cells = [] + for c in cfgs: + runs = all_runs[c] + metric, lower = metric_for(workload) + h = series(runs["hstore"], workload, metric) + g = series(runs["hypergraphdb"], workload, metric) + if not h or not g: + cells.append("--") + continue + ratio = med(g) / med(h) if lower else med(h) / med(g) + disjoint = (max(h) < min(g) or min(h) > max(g)) + text = fmt_ratio(ratio) + if ratio >= 1: + text = "\\textbf{" + text + "}" + if not disjoint: + text += "$^{\\dagger}$" + cells.append(text) + macros[f"sum{KEYS[c]}{workload.replace('.', '')}"] = fmt_ratio(ratio) + macros[f"dis{KEYS[c]}{workload.replace('.', '')}"] = "yes" if disjoint else "no" + f.write(f"\\texttt{{{workload}}} & " + " & ".join(cells) + " \\\\\n") + f.write("\\bottomrule\n\\end{tabular}\n") + with open(f"{OUT}/summary-fig.tex", "w") as f: + rows = [w for w, _ in THROUGHPUT + TIMES + SIZES] + f.write("\\def\\sumsymbols{" + ",".join(rows) + "}\n") + for c in cfgs: + pts = [] + for workload in rows: + runs = all_runs[c] + metric, lower = metric_for(workload) + h = series(runs["hstore"], workload, metric) + g = series(runs["hypergraphdb"], workload, metric) + if h and g: + ratio = med(g) / med(h) if lower else med(h) / med(g) + pts.append(f"({ratio:.4f},{workload})") + f.write(f"\\def\\sum{KEYS[c]}{{{' '.join(pts)}}}\n") + +available = [] +all_runs = {} +for cfg in CONFIGS: + runs = load(cfg) + if not runs["hstore"] or not runs["hypergraphdb"]: + continue + available.append(cfg) + all_runs[cfg] = runs + rows = main_table(cfg, runs) + write_main(cfg, rows) + write_ratio_figure(cfg, rows) + latency_table(cfg, runs) + resources_table(cfg, runs) + runs_table(cfg, runs) + ranges_table(cfg, runs) + environment(cfg, runs) + +summary(all_runs) + +loads = [] +if os.path.exists(f"{BENCH}/progress.log"): + import re + for line in open(f"{BENCH}/progress.log"): + m = re.search(r"load \{ ([0-9.]+)", line) + if m: + loads.append(float(m.group(1))) +if loads: + macros["loadmin"] = f"{min(loads):.1f}" + macros["loadmax"] = f"{max(loads):.1f}" + macros["loadmed"] = f"{statistics.median(loads):.1f}" + +with open(f"{OUT}/macros.tex", "w") as f: + for name, value in sorted(macros.items()): + safe = "".join(c for c in name if c.isalpha()) + f.write(f"\\expandafter\\def\\csname {safe}\\endcsname{{{value}}}\n") +print("configs:", ", ".join(available), "macros:", len(macros)) + + +SHORT = { + "ingest.nodes": "ingest.nodes", "ingest.edges": "ingest.edges", "write.update": "write.update", + "latency.commit": "latency.commit", "churn.delete": "churn.delete", "churn.remove": "churn.remove", + "large.ingest": "large.ingest", "read.incidence": "read.incidence", "read.members": "read.members", + "read.comembership": "read.comemb.", "read.twohop": "read.twohop", "read.incidence.parallel": "read.inc.parallel", + "latency.read": "latency.read", "read.incidence.cold": "read.inc.cold", "read.incidence.churned": "read.inc.churned", + "large.scan": "large.scan", "large.probe": "large.probe", "mixed.read": "mixed.read", "reopen": "reopen", + "recover": "recover", "disk": "disk", "disk.churned": "disk.churned", "disk.compacted": "disk.compacted", +} + + +def ratio_of(runs, workload): + metric, lower = metric_for(workload) + h = series(runs["hstore"], workload, metric) + g = series(runs["hypergraphdb"], workload, metric) + if not h or not g: + return None + return med(g) / med(h) if lower else med(h) / med(g) + + +def fig_ratios(): + cfgs = [c for c in CONFIGS if c in all_runs] + rows = [w for w, _ in THROUGHPUT + TIMES + SIZES] + labels = ",".join(SHORT[w] for w in rows) + styles = {"s1-async": "fill=hs,draw=hs", "s1-sync": "fill=hs!45,draw=hs!45", "s4-async": "fill=hg,draw=hg!80!black"} + legend = {"s1-async": "S1 async", "s1-sync": "S1 sync", "s4-async": "S4 async"} + with open(f"{OUT}/fig-ratios.tex", "w") as f: + f.write("\\begin{tikzpicture}\n\\begin{axis}[report, ybar=0pt, bar width=2.6pt, width=\\textwidth, height=5.6cm,\n") + f.write(f" ymode=log, log origin=infty, ymin=0.25, ymax=200, symbolic x coords={{{labels}}}, xtick=data,\n") + f.write(" x tick label style={rotate=50, anchor=east, font=\\scriptsize\\ttfamily}, enlarge x limits=0.02,\n") + f.write(" ylabel={HStore advantage ($\\times$, log)}, ymajorgrids, yticklabel style={/pgf/number format/fixed},\n") + f.write(" ytick={0.5,1,2,5,10,20,50,100}, yticklabels={0.5,1,2,5,10,20,50,100},\n") + f.write(" legend style={at={(0.5,1.02)}, anchor=south, legend columns=-1, /tikz/every even column/.append style={column sep=6pt}}]\n") + for c in cfgs: + pts = [] + for w in rows: + r = ratio_of(all_runs[c], w) + if r is not None: + pts.append(f"({SHORT[w]},{r:.4f})") + f.write(f"\\addplot[{styles[c]}] coordinates {{{' '.join(pts)}}};\n\\addlegendentry{{{legend[c]}}}\n") + f.write("\\addplot[rule, densely dashed, sharp plot, update limits=false] coordinates {(ingest.nodes,1) (disk.compacted,1)};\n") + f.write("\\end{axis}\n\\end{tikzpicture}\n") + + +def fig_latency(): + panels = [("s1-async", "latency.commit", "Commit, async"), ("s1-sync", "latency.commit", "Commit, sync"), + ("s1-async", "latency.read", "Read transaction"), ("s1-async", "mixed.write", "Commit beside 7 readers")] + with open(f"{OUT}/fig-latency.tex", "w") as f: + for i, (c, w, title) in enumerate(panels): + if c not in all_runs: + continue + f.write("\\begin{tikzpicture}\n\\begin{axis}[report, width=0.27\\textwidth, height=3.9cm, ymode=log,\n") + f.write(" xtick={0,1,2,3}, xticklabels={p50,p99,p99.9,max}, xmin=-0.3, xmax=3.3, ymajorgrids,\n") + f.write(f" title={{\\footnotesize {title}}}, title style={{yshift=-4pt}}") + f.write(", ylabel={latency (\\textmu s)}" if i == 0 else "") + f.write(", legend style={at={(0.03,0.97)}, anchor=north west}" if i == 0 else "") + f.write("]\n") + for e, style, name in (("hstore", "hs, mark=*, mark size=1.4pt, thick", "HStore"), + ("hypergraphdb", "hg!70!black, mark=square*, mark size=1.3pt, thick", "HyperGraphDB")): + pts = [] + for k, p in enumerate(("p50", "p99", "p99.9", "max")): + v = series(all_runs[c][e], w, lambda r, p=p: r.get("latency", {}).get(p)) + if v: + pts.append(f"({k},{med(v):.3f})") + f.write(f"\\addplot[{style}] coordinates {{{' '.join(pts)}}};\n") + if i == 0: + f.write(f"\\addlegendentry{{{name}}}\n") + f.write("\\end{axis}\n\\end{tikzpicture}%\n") + if i < len(panels) - 1: + f.write("\\hfill\n") + + +def fig_writes(): + c = "s1-async" + if c not in all_runs: + return + rows = [w for w in WRITES if series(all_runs[c]["hstore"], w, lambda r: r.get("bytesWritten"))] + with open(f"{OUT}/fig-writes.tex", "w") as f: + f.write("\\begin{tikzpicture}\n\\begin{axis}[report, ybar=0pt, bar width=4pt, width=\\columnwidth, height=4.6cm,\n") + f.write(f" ymode=log, log origin=infty, symbolic x coords={{{','.join(rows)}}}, xtick=data,\n") + f.write(" x tick label style={rotate=40, anchor=east, font=\\scriptsize\\ttfamily}, enlarge x limits=0.08,\n") + f.write(" ylabel={bytes written per op}, ymajorgrids, legend style={at={(0.02,0.98)}, anchor=north west}]\n") + for e, style, name in (("hstore", "fill=hs,draw=hs", "HStore"), ("hypergraphdb", "fill=hg,draw=hg!80!black", "HyperGraphDB")): + pts = [] + for w in rows: + v = series(all_runs[c][e], w, lambda r: r["bytesWritten"] / r["operations"] if r.get("bytesWritten") is not None else None) + pts.append(f"({w},{med(v):.1f})") + f.write(f"\\addplot[{style}] coordinates {{{' '.join(pts)}}};\n\\addlegendentry{{{name}}}\n") + f.write("\\end{axis}\n\\end{tikzpicture}\n") + + +def fig_disk(): + cfgs = [c for c in ("s1-async", "s4-async") if c in all_runs] + with open(f"{OUT}/fig-disk.tex", "w") as f: + for i, c in enumerate(cfgs): + f.write("\\begin{tikzpicture}\n\\begin{axis}[report, ybar=0pt, bar width=7pt, width=0.55\\columnwidth, height=4.2cm,\n") + f.write(" symbolic x coords={load,deletes,compacted}, xtick=data, enlarge x limits=0.25, ymin=0, ymajorgrids,\n") + f.write(f" title={{\\footnotesize {'Scale 1' if c == 's1-async' else 'Scale 4'}}}, title style={{yshift=-4pt}}") + f.write(", ylabel={MiB on disk}" if i == 0 else "") + f.write(", legend style={at={(0.98,0.98)}, anchor=north east}" if i == 0 else "") + f.write("]\n") + for e, style, name in (("hstore", "fill=hs,draw=hs", "HStore"), ("hypergraphdb", "fill=hg,draw=hg!80!black", "HyperGraphDB")): + pts = [] + for w, label in (("disk", "load"), ("disk.churned", "deletes"), ("disk.compacted", "compacted")): + v = series(all_runs[c][e], w, lambda r: r["operations"]) + if v: + pts.append(f"({label},{med(v) / (1 << 20):.1f})") + f.write(f"\\addplot[{style}] coordinates {{{' '.join(pts)}}};\n") + if i == 0: + f.write(f"\\addlegendentry{{{name}}}\n") + f.write("\\end{axis}\n\\end{tikzpicture}%\n") + if i < len(cfgs) - 1: + f.write("\\hfill\n") + + +fig_ratios() +fig_latency() +fig_writes() +fig_disk() + + +def resources_summary(): + cfgs = [c for c in CONFIGS if c in all_runs] + names = {"s1-async": "S1 async", "s1-sync": "S1 sync", "s4-async": "S4 async"} + with open(f"{OUT}/resources.tex", "w") as f: + f.write("\\begin{tabular}{@{}l" + "rr" * len(cfgs) + "@{}}\n\\toprule\n") + f.write(" & " + " & ".join(f"\\multicolumn{{2}}{{c}}{{{names[c]}}}" for c in cfgs) + " \\\\\n") + f.write("".join(f"\\cmidrule(lr){{{2 + 2 * i}-{3 + 2 * i}}}" for i in range(len(cfgs))) + "\n") + f.write("Measure" + " & HS & HG" * len(cfgs) + " \\\\\n\\midrule\n") + + def row(label, fn, fmt): + cells = [] + for c in cfgs: + for e in ENGINES: + v = [x for x in (fn(run) for run in all_runs[c][e]) if x is not None] + cells.append(fmt(med(v)) if v else "--") + f.write(label + " & " + " & ".join(cells) + " \\\\\n") + + def wl(name, field): + return lambda run: results(run).get(name, {}).get(field) + + row("Retained heap after ingest", lambda run: run.get("retainedHeap"), fmt_bytes) + row("Bytes written, whole run", lambda run: run.get("bytesWritten"), fmt_bytes) + row("GC pause, whole run (ms)", lambda run: sum(r.get("gcMillis", 0) for r in run["results"]), lambda v: fmt_int(v)) + row("Reclamation time (ms)", wl("compaction", "compactionMillis"), lambda v: fmt_int(v)) + row("Space reclaimed", wl("compaction", "reclaimed"), fmt_bytes) + row("Acknowledged before kill", wl("recover", "acknowledged"), lambda v: fmt_int(v)) + lost = lambda run: results(run).get("recover", {}).get("lost") + cells = [] + for c in cfgs: + for e in ENGINES: + v = [x for x in (lost(run) for run in all_runs[c][e]) if x is not None] + cells.append(fmt_int(max(v)) if v else "--") + f.write("Acknowledged lost (max) & " + " & ".join(cells) + " \\\\\n") + f.write("\\bottomrule\n\\end{tabular}\n") + + +resources_summary() diff --git a/benchmarks/report/preamble.tex b/benchmarks/report/preamble.tex new file mode 100644 index 0000000..8610ec3 --- /dev/null +++ b/benchmarks/report/preamble.tex @@ -0,0 +1,49 @@ +\usepackage[letterpaper,margin=0.75in,columnsep=0.25in]{geometry} +\usepackage{libertinus} +\usepackage{microtype} +\usepackage{titlesec} +\titlespacing*{\paragraph}{0pt}{5pt plus 2pt minus 1pt}{0.6em} +\titleformat{\paragraph}[runin]{\normalfont\bfseries}{}{0pt}{} +\usepackage{booktabs} +\usepackage{longtable} +\usepackage{tabularx} +\usepackage{array} +\usepackage{multirow} +\usepackage{xcolor} +\usepackage{caption} +\usepackage{subcaption} +\usepackage{enumitem} +\usepackage{amsmath} +\usepackage{pgfplots} +\usepackage{tikz} +\usetikzlibrary{positioning,arrows.meta,fit,backgrounds,calc} +\pgfplotsset{compat=1.18} +\usepackage[hidelinks]{hyperref} +\usepackage{cleveref} + +\definecolor{hs}{HTML}{1F4E79} +\definecolor{hg}{HTML}{B4B4B4} +\definecolor{rule}{HTML}{555555} + +\captionsetup{font=small,labelfont=bf,skip=4pt} +\setlist{nosep,leftmargin=1.2em} +\setlength{\tabcolsep}{4pt} +\renewcommand{\arraystretch}{1.08} +\frenchspacing + +\newcommand{\wl}[1]{\texttt{#1}} +\newcommand{\M}[1]{\csname #1\endcsname} +\newcommand{\hstore}{HStore} +\newcommand{\hgdb}{HyperGraphDB} + +\pgfplotsset{ + report/.style={ + font=\footnotesize, + tick label style={font=\scriptsize}, + label style={font=\footnotesize}, + legend style={font=\scriptsize,draw=none,fill=none}, + axis line style={rule}, + tick style={rule}, + major grid style={gray!25}, + }, +} diff --git a/benchmarks/report/report.tex b/benchmarks/report/report.tex new file mode 100644 index 0000000..81011d2 --- /dev/null +++ b/benchmarks/report/report.tex @@ -0,0 +1,40 @@ +\documentclass[10pt,twocolumn]{article} +\input{preamble} +\input{gen/macros} +\input{gen/summary-fig} + +\title{\Large\bfseries HStore and HyperGraphDB:\\ a storage-level benchmark of two persistent hypergraph engines} +\author{Metacog Labs\\[2pt]\normalsize HStore 0.1.0, commit \texttt{9291e18}} +\date{October 2026} + +\begin{document} +\maketitle + +\begin{abstract} +\input{sections/abstract} +\end{abstract} + +\input{sections/introduction} +\input{sections/systems} +\input{sections/method} +\input{sections/results} +\input{sections/discussion} +\input{sections/validity} +\input{sections/conclusion} + +\bibliographystyle{plain} +\begin{thebibliography}{9}\small +\bibitem{hgdb} B.~Iordanov. HyperGraphDB: a generalized graph database. In \emph{Web-Age Information Management (WAIM) Workshops}, LNCS 6185, pages 25--36. Springer, 2010. +\bibitem{je} Oracle. \emph{Berkeley DB Java Edition Architecture}. Oracle white paper, 2006. +\bibitem{leapfrog} T.~L. Veldhuizen. Leapfrog triejoin: a simple, worst-case optimal join algorithm. In \emph{Proc.\ ICDT}, pages 96--106, 2014. +\bibitem{lmdb} H.~Chu. MDB: a memory-mapped database and backend for OpenLDAP. In \emph{Proc.\ LDAPCon}, 2011. +\bibitem{aries} C.~Mohan, D.~Haderle, B.~Lindsay, H.~Pirahesh, P.~Schwarz. ARIES: a transaction recovery method supporting fine-granularity locking and partial rollbacks using write-ahead logging. \emph{ACM TODS} 17(1):94--162, 1992. +\bibitem{lfs} M.~Rosenblum, J.~K. Ousterhout. The design and implementation of a log-structured file system. \emph{ACM TOCS} 10(1):26--52, 1992. +\end{thebibliography} + +\clearpage +\onecolumn +\appendix +\input{sections/appendix} + +\end{document} diff --git a/benchmarks/report/results/progress.log b/benchmarks/report/results/progress.log new file mode 100644 index 0000000..c3f270d --- /dev/null +++ b/benchmarks/report/results/progress.log @@ -0,0 +1,52 @@ +s1-async hstore run 1 start 04:40:47 load { 3.41 8.19 7.72 } +s1-async hstore run 1 exit 0 took 55s +s1-async hypergraphdb run 1 start 04:41:42 load { 5.33 8.06 7.71 } +s1-async hypergraphdb run 1 exit 0 took 77s +s1-async hstore run 2 start 04:42:59 load { 7.56 8.29 7.83 } +s1-async hstore run 2 exit 0 took 53s +s1-async hypergraphdb run 2 start 04:43:52 load { 7.07 8.17 7.82 } +s1-async hypergraphdb run 2 exit 0 took 68s +s1-async hstore run 3 start 04:45:00 load { 6.00 7.86 7.74 } +s1-async hstore run 3 exit 0 took 54s +s1-async hypergraphdb run 3 start 04:45:54 load { 5.68 7.55 7.64 } +s1-async hypergraphdb run 3 exit 0 took 68s +s1-async hstore run 4 start 04:47:02 load { 7.54 7.80 7.73 } +s1-async hstore run 4 exit 0 took 54s +s1-async hypergraphdb run 4 start 04:47:56 load { 5.59 7.23 7.52 } +s1-async hypergraphdb run 4 exit 0 took 70s +s1-async hstore run 5 start 04:49:06 load { 6.98 7.49 7.60 } +s1-async hstore run 5 exit 0 took 53s +s1-async hypergraphdb run 5 start 04:49:59 load { 5.41 7.01 7.42 } +s1-async hypergraphdb run 5 exit 0 took 80s +s1-sync hstore run 1 start 04:51:19 load { 6.04 6.81 7.30 } +s1-sync hstore run 1 exit 0 took 74s +s1-sync hypergraphdb run 1 start 04:52:33 load { 4.49 6.15 7.00 } +s1-sync hypergraphdb run 1 exit 0 took 90s +s1-sync hstore run 2 start 04:54:03 load { 5.99 6.46 7.05 } +s1-sync hstore run 2 exit 0 took 73s +s1-sync hypergraphdb run 2 start 04:55:16 load { 4.63 5.95 6.81 } +s1-sync hypergraphdb run 2 exit 0 took 92s +s1-sync hstore run 3 start 04:56:48 load { 5.95 6.22 6.83 } +s1-sync hstore run 3 exit 0 took 74s +s1-sync hypergraphdb run 3 start 04:58:02 load { 5.38 6.06 6.72 } +s1-sync hypergraphdb run 3 exit 0 took 92s +s1-sync hstore run 4 start 04:59:34 load { 6.11 6.33 6.77 } +s1-sync hstore run 4 exit 0 took 77s +s1-sync hypergraphdb run 4 start 05:00:51 load { 6.02 6.33 6.73 } +s1-sync hypergraphdb run 4 exit 0 took 99s +s1-sync hstore run 5 start 05:02:30 load { 8.15 7.06 6.97 } +s1-sync hstore run 5 exit 0 took 81s +s1-sync hypergraphdb run 5 start 05:03:51 load { 9.65 8.32 7.49 } +s1-sync hypergraphdb run 5 exit 0 took 102s +s4-async hstore run 1 start 05:05:33 load { 9.27 8.98 7.87 } +s4-async hstore run 1 exit 0 took 106s +s4-async hypergraphdb run 1 start 05:07:19 load { 7.57 8.94 8.01 } +s4-async hypergraphdb run 1 exit 0 took 1361s +s4-async hstore run 2 start 05:30:00 load { 10.90 9.07 9.32 } +s4-async hstore run 2 exit 0 took 92s +s4-async hypergraphdb run 2 start 05:31:32 load { 5.57 7.90 8.85 } +s4-async hypergraphdb run 2 exit 0 took 1893s +s4-async hstore run 3 start 06:03:05 load { 18.27 17.17 15.83 } +s4-async hstore run 3 exit 0 took 125s +s4-async hypergraphdb run 3 start 06:05:10 load { 15.86 16.83 15.90 } +s4-async hypergraphdb run 3 exit 0 took 1529s diff --git a/benchmarks/report/results/s1-async/hstore-1.json b/benchmarks/report/results/s1-async/hstore-1.json new file mode 100644 index 0000000..60e30d5 --- /dev/null +++ b/benchmarks/report/results/s1-async/hstore-1.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":1,"durability":"async","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4091543552,"retainedHeap":198474184,"bytesWritten":99703930,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":979.102541,"checksum":50000,"gcCount":1,"gcMillis":13,"allocated":1012632648,"bytesWritten":12608282,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":2361.876625,"checksum":362757,"gcCount":3,"gcMillis":55,"allocated":3882120208,"bytesWritten":38760172,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":15.964541,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":145728,"bytesRead":0,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":46.583333,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4026984,"bytesRead":0,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":32.77125,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":82692232,"bytesRead":0,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":27.086166,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":3729784,"bytesRead":0,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":26.8535,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":1415360},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":26.084917,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":26937560,"latency":{"p50":0.209,"p99":0.5,"p99.9":1.5,"max":91.916}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":305.719375,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":475855128,"bytesWritten":4471900,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":403.635041,"checksum":5000,"gcCount":1,"gcMillis":7,"allocated":398671576,"latency":{"p50":48.542,"p99":297.667,"p99.9":441.042,"max":7103.125},"bytesWritten":3295384,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":25420570,"millis":10000.125625,"checksum":0,"gcCount":1,"gcMillis":7,"allocated":1629705568,"latency":{"p50":1.542,"p99":10.375,"p99.9":30.666,"max":38283.292},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.125625,"checksum":0,"latency":{"p50":2478.708,"p99":14502.083,"p99.9":45643.75,"max":45643.75},"checked":false,"bytesWritten":3547066,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":174.389209,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":115764688,"bytesWritten":3411588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":4.531584,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":176736,"bytesRead":0,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":5.091792,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":16000,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":111.496458,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":26931424},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":28.911208,"checksum":831528,"gcCount":0,"gcMillis":0,"allocated":28056488,"bytesRead":2737618,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":96884225,"millis":1000.0,"checksum":96884225},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":991.621291,"checksum":20000,"gcCount":1,"gcMillis":87,"allocated":1061541856,"bytesWritten":15127084,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":463.443459,"checksum":20000,"gcCount":1,"gcMillis":43,"allocated":484267280,"bytesWritten":5929386,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":10.795459,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":125760,"bytesRead":0,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":119597544,"millis":1000.0,"checksum":119597544},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9982.07325,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":103065488,"latency":{"p50":3312.166,"p99":5419.333,"p99.9":7087.666,"max":7087.666},"checked":false,"bytesWritten":1789526,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9984.020666,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":99604520,"latency":{"p50":3440.666,"p99":5602.458,"p99.9":9286.083,"max":9286.083},"checked":false,"bytesWritten":10762850,"logicalWritten":16000,"compactionMillis":141,"reclaimed":25796186},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":95658103,"millis":1000.0,"checksum":95658103},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":127.91475,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":125736984,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-async/hstore-2.json b/benchmarks/report/results/s1-async/hstore-2.json new file mode 100644 index 0000000..6f9f32a --- /dev/null +++ b/benchmarks/report/results/s1-async/hstore-2.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":1,"durability":"async","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4114087936,"retainedHeap":200992232,"bytesWritten":101402368,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":719.336,"checksum":50000,"gcCount":1,"gcMillis":9,"allocated":989389576,"bytesWritten":12608282,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":1825.084208,"checksum":362757,"gcCount":3,"gcMillis":52,"allocated":3631105240,"bytesWritten":36645552,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":15.293542,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":140272,"bytesRead":0,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":19.499625,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4016336,"bytesRead":0,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":33.034416,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":81184048,"bytesRead":0,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":18.405458,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":3729808,"bytesRead":0,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":19.70775,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":1533816},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":22.939834,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":16253024,"latency":{"p50":0.208,"p99":0.334,"p99.9":0.459,"max":43.958}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":269.118833,"checksum":20000,"gcCount":1,"gcMillis":6,"allocated":512738464,"bytesWritten":4471900,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":431.66675,"checksum":5000,"gcCount":0,"gcMillis":0,"allocated":393578752,"latency":{"p50":54.25,"p99":295.625,"p99.9":672.042,"max":1372.708},"bytesWritten":3295384,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":24671256,"millis":10000.152084,"checksum":0,"gcCount":3,"gcMillis":33,"allocated":2423673848,"latency":{"p50":1.791,"p99":10.292,"p99.9":25.667,"max":33406.666},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.152084,"checksum":0,"latency":{"p50":2404.334,"p99":11794.875,"p99.9":20479.917,"max":20479.917},"checked":false,"bytesWritten":3547066,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":102.870916,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":174374056,"bytesWritten":3411588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":4.939542,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":185424,"bytesRead":0,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":5.809125,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":6415952,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":107.149916,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":26940120},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":24.140917,"checksum":831528,"gcCount":0,"gcMillis":0,"allocated":28043920,"bytesRead":2737618,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":94754971,"millis":1000.0,"checksum":94754971},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":816.675083,"checksum":20000,"gcCount":1,"gcMillis":24,"allocated":1088859816,"bytesWritten":14955062,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":417.530958,"checksum":20000,"gcCount":1,"gcMillis":32,"allocated":535310256,"bytesWritten":9910298,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":11.906834,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":4132936,"bytesRead":0,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":121305832,"millis":1000.0,"checksum":121305832},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9983.5945,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":102783528,"latency":{"p50":3349.0,"p99":5301.875,"p99.9":8799.75,"max":8799.75},"checked":false,"bytesWritten":1789526,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9983.838,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":99499904,"latency":{"p50":3382.459,"p99":5317.417,"p99.9":5929.084,"max":5929.084},"checked":false,"bytesWritten":10767018,"logicalWritten":16000,"compactionMillis":136,"reclaimed":27508122},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":95654455,"millis":1000.0,"checksum":95654455},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":152.832917,"checksum":1,"gcCount":1,"gcMillis":41,"allocated":127721952,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-async/hstore-3.json b/benchmarks/report/results/s1-async/hstore-3.json new file mode 100644 index 0000000..4a078b0 --- /dev/null +++ b/benchmarks/report/results/s1-async/hstore-3.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":1,"durability":"async","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4103077888,"retainedHeap":199512344,"bytesWritten":103744704,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":738.11075,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":974522832,"bytesWritten":12608282,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":1998.317583,"checksum":362757,"gcCount":3,"gcMillis":52,"allocated":3797253160,"bytesWritten":38936844,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":18.199292,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":134144,"bytesRead":0,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":15.850292,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4015904,"bytesRead":0,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":25.369208,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":81183808,"bytesRead":0,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":17.421166,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":3729856,"bytesRead":0,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":21.869917,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":1614312},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":41.423167,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":29297648,"latency":{"p50":0.334,"p99":0.875,"p99.9":3.209,"max":1458.0}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":397.962417,"checksum":20000,"gcCount":1,"gcMillis":8,"allocated":458383432,"bytesWritten":4471900,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":480.330459,"checksum":5000,"gcCount":0,"gcMillis":0,"allocated":400663080,"latency":{"p50":59.209,"p99":408.334,"p99.9":1064.25,"max":1850.083},"bytesWritten":3295384,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":24256450,"millis":10004.688875,"checksum":0,"gcCount":3,"gcMillis":36,"allocated":2893813872,"latency":{"p50":1.917,"p99":10.959,"p99.9":25.833,"max":28509.166},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10004.688875,"checksum":0,"latency":{"p50":2325.417,"p99":6227.417,"p99.9":22015.083,"max":22015.083},"checked":false,"bytesWritten":3547066,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":81.951042,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":115072032,"bytesWritten":3411588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":5.702834,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":176544,"bytesRead":0,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":5.569542,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":15664,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":111.559875,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":26931392},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":24.722833,"checksum":831528,"gcCount":0,"gcMillis":0,"allocated":28057904,"bytesRead":2737618,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":97063290,"millis":1000.0,"checksum":97063290},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":892.22825,"checksum":20000,"gcCount":1,"gcMillis":75,"allocated":1161214736,"bytesWritten":15000052,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":468.443542,"checksum":20000,"gcCount":1,"gcMillis":40,"allocated":624004168,"bytesWritten":9925897,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":12.087541,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":139576,"bytesRead":0,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":123673576,"millis":1000.0,"checksum":123673576},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9984.920375,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":105591776,"latency":{"p50":3241.417,"p99":5668.459,"p99.9":7184.875,"max":7184.875},"checked":false,"bytesWritten":1789526,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9983.31,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":102411824,"latency":{"p50":3358.208,"p99":5443.625,"p99.9":6480.042,"max":6480.042},"checked":false,"bytesWritten":10757473,"logicalWritten":16000,"compactionMillis":125,"reclaimed":29878490},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":95651798,"millis":1000.0,"checksum":95651798},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":129.437375,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":127222768,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-async/hstore-4.json b/benchmarks/report/results/s1-async/hstore-4.json new file mode 100644 index 0000000..7b5348b --- /dev/null +++ b/benchmarks/report/results/s1-async/hstore-4.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":1,"durability":"async","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4114087936,"retainedHeap":198646712,"bytesWritten":102847801,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":685.309792,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":978189216,"bytesWritten":12608282,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":2079.485292,"checksum":362757,"gcCount":3,"gcMillis":54,"allocated":3657551624,"bytesWritten":38257459,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":12.205333,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":146016,"bytesRead":0,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":16.353917,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4029744,"bytesRead":0,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":26.163666,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":81183832,"bytesRead":0,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":29.777875,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":3729808,"bytesRead":0,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":38.185917,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":1416048},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":38.934917,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":26918072,"latency":{"p50":0.333,"p99":0.875,"p99.9":1.75,"max":42.833}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":286.207042,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":519803216,"bytesWritten":4471900,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":391.8315,"checksum":5000,"gcCount":0,"gcMillis":0,"allocated":387807400,"latency":{"p50":51.958,"p99":295.708,"p99.9":421.625,"max":3631.417},"bytesWritten":3295384,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":24574903,"millis":10000.099292,"checksum":0,"gcCount":3,"gcMillis":19,"allocated":2737518992,"latency":{"p50":1.792,"p99":10.459,"p99.9":24.125,"max":26011.959},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.099292,"checksum":0,"latency":{"p50":2308.959,"p99":9654.334,"p99.9":21528.917,"max":21528.917},"checked":false,"bytesWritten":3547066,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":77.01275,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":112959136,"bytesWritten":3411588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":5.401625,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":176208,"bytesRead":0,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":5.921917,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":15856,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":110.852625,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":26939008},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":28.7235,"checksum":831528,"gcCount":0,"gcMillis":0,"allocated":27026264,"bytesRead":2737618,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":96377516,"millis":1000.0,"checksum":96377516},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":708.286833,"checksum":20000,"gcCount":1,"gcMillis":55,"allocated":1012562832,"bytesWritten":14864709,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":402.713417,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":476658608,"bytesWritten":9838345,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":12.734916,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":139600,"bytesRead":0,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":122763975,"millis":1000.0,"checksum":122763975},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9983.116292,"checksum":0,"gcCount":1,"gcMillis":34,"allocated":103038752,"latency":{"p50":3350.0,"p99":5651.375,"p99.9":16125.375,"max":16125.375},"checked":false,"bytesWritten":1789526,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9981.993166,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":99832912,"latency":{"p50":3182.833,"p99":5382.5,"p99.9":5584.333,"max":5584.333},"checked":false,"bytesWritten":10762850,"logicalWritten":16000,"compactionMillis":135,"reclaimed":28962586},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":95658134,"millis":1000.0,"checksum":95658134},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":105.829875,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":126893448,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-async/hstore-5.json b/benchmarks/report/results/s1-async/hstore-5.json new file mode 100644 index 0000000..0d91c17 --- /dev/null +++ b/benchmarks/report/results/s1-async/hstore-5.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":1,"durability":"async","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4093640704,"retainedHeap":199019744,"bytesWritten":103454943,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":809.52525,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":959535112,"bytesWritten":12608282,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":1958.658208,"checksum":362757,"gcCount":3,"gcMillis":57,"allocated":3717006136,"bytesWritten":38961762,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":26.04875,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":159032,"bytesRead":0,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":15.605167,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4029600,"bytesRead":0,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":35.694375,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":81184000,"bytesRead":0,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":17.815625,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":3728856,"bytesRead":0,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":21.883583,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":1399576},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":21.184833,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":29321504,"latency":{"p50":0.208,"p99":0.292,"p99.9":0.375,"max":21.958}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":185.181209,"checksum":20000,"gcCount":1,"gcMillis":10,"allocated":460237920,"bytesWritten":4471900,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":335.724708,"checksum":5000,"gcCount":0,"gcMillis":0,"allocated":390118080,"latency":{"p50":48.458,"p99":248.917,"p99.9":320.083,"max":368.167},"bytesWritten":3295384,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":23684964,"millis":10000.08025,"checksum":0,"gcCount":2,"gcMillis":15,"allocated":1837281264,"latency":{"p50":1.958,"p99":10.791,"p99.9":23.917,"max":26809.625},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.08025,"checksum":0,"latency":{"p50":2305.916,"p99":8403.333,"p99.9":24257.166,"max":24257.166},"checked":false,"bytesWritten":3547066,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":66.216042,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":124266488,"bytesWritten":3411588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":4.070875,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":185520,"bytesRead":0,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":10.389541,"checksum":100000,"gcCount":1,"gcMillis":1,"allocated":6420920,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":131.383042,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":26925816},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":24.179625,"checksum":831528,"gcCount":0,"gcMillis":0,"allocated":27012496,"bytesRead":2737618,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":97088769,"millis":1000.0,"checksum":97088769},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":668.516791,"checksum":20000,"gcCount":1,"gcMillis":49,"allocated":1115357056,"bytesWritten":14761514,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":512.802833,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":623618424,"bytesWritten":9837728,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":11.34475,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":133392,"bytesRead":0,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":123368488,"millis":1000.0,"checksum":123368488},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9981.852042,"checksum":0,"gcCount":1,"gcMillis":20,"allocated":105524400,"latency":{"p50":3148.833,"p99":5432.5,"p99.9":21670.083,"max":21670.083},"checked":false,"bytesWritten":1789526,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9982.379625,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":102277536,"latency":{"p50":3306.791,"p99":5571.416,"p99.9":6167.875,"max":6167.875},"checked":false,"bytesWritten":10769501,"logicalWritten":16000,"compactionMillis":136,"reclaimed":29567770},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":95657430,"millis":1000.0,"checksum":95657430},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":106.008625,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":126626976,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-async/hypergraphdb-1.json b/benchmarks/report/results/s1-async/hypergraphdb-1.json new file mode 100644 index 0000000..a3d294b --- /dev/null +++ b/benchmarks/report/results/s1-async/hypergraphdb-1.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":1,"durability":"async","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4199022592,"retainedHeap":124140736,"bytesWritten":4560098472,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1249.94025,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":933521920,"bytesWritten":52955141,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":2859.487375,"checksum":362757,"gcCount":3,"gcMillis":58,"allocated":2987110392,"bytesWritten":238357636,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":64.046042,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":9780880,"bytesRead":24615421,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":33.352334,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4947368,"bytesRead":16718180,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":8942.771,"checksum":20287,"gcCount":16,"gcMillis":133,"allocated":160143672,"bytesRead":3929815270,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":140.832166,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":18415888,"bytesRead":17593801,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":343.74275,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":500398744},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":231.4905,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":404496928,"latency":{"p50":1.583,"p99":11.667,"p99.9":38.167,"max":450.333}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":1046.197417,"checksum":20000,"gcCount":2,"gcMillis":13,"allocated":2342886240,"bytesWritten":116022737,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":203.156791,"checksum":5000,"gcCount":1,"gcMillis":7,"allocated":473593944,"latency":{"p50":28.792,"p99":247.125,"p99.9":546.708,"max":7362.292},"bytesWritten":28652809,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":11320098,"millis":10000.123875,"checksum":0,"gcCount":22,"gcMillis":151,"allocated":28878509976,"latency":{"p50":3.333,"p99":54.542,"p99.9":295.625,"max":28501.584},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.123875,"checksum":0,"latency":{"p50":2843.542,"p99":16937.875,"p99.9":34414.417,"max":34414.417},"checked":false,"bytesWritten":855484992,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":478.62375,"checksum":50000,"gcCount":1,"gcMillis":9,"allocated":1306875344,"bytesWritten":79615321,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":15.422292,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":462728,"bytesRead":6996097,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":48.629625,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":9786056,"bytesRead":26101120,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":1361.156958,"checksum":1,"gcCount":2,"gcMillis":9,"allocated":2819783688},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":1103.657167,"checksum":831528,"gcCount":2,"gcMillis":43,"allocated":573054024,"bytesRead":330238625,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":363228978,"millis":1000.0,"checksum":363228978},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":2190.502041,"checksum":20000,"gcCount":4,"gcMillis":28,"allocated":4780839720,"bytesWritten":199184041,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":1243.9325,"checksum":20000,"gcCount":2,"gcMillis":18,"allocated":2819573736,"bytesWritten":135815276,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":107.29025,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":9871544,"bytesRead":31027227,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":297276902,"millis":1000.0,"checksum":297276902},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9982.679042,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":340630232,"latency":{"p50":3363.083,"p99":5659.083,"p99.9":10355.25,"max":10355.25},"checked":false,"bytesWritten":4139954,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9983.8075,"checksum":0,"gcCount":1,"gcMillis":104,"allocated":1575473592,"latency":{"p50":3169.417,"p99":26950.875,"p99.9":106715.792,"max":106715.792},"checked":false,"bytesWritten":99282792,"logicalWritten":16000,"compactionMillis":48,"reclaimed":683041},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":263891698,"millis":1000.0,"checksum":263891698},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":72.812208,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":81774872,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-async/hypergraphdb-2.json b/benchmarks/report/results/s1-async/hypergraphdb-2.json new file mode 100644 index 0000000..652aeda --- /dev/null +++ b/benchmarks/report/results/s1-async/hypergraphdb-2.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":1,"durability":"async","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4193779712,"retainedHeap":124334272,"bytesWritten":3695948322,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1257.588084,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":934785256,"bytesWritten":53215798,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":3004.694291,"checksum":362757,"gcCount":3,"gcMillis":61,"allocated":3059859816,"bytesWritten":241380708,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":63.082209,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":9800488,"bytesRead":19947593,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":37.549959,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4947368,"bytesRead":12734755,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":260.927792,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":159768792,"bytesRead":92738505,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":67.223,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":18417152,"bytesRead":30730687,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":299.02625,"checksum":5852224,"gcCount":1,"gcMillis":4,"allocated":753207912},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":218.819458,"checksum":731528,"gcCount":1,"gcMillis":5,"allocated":714789544,"latency":{"p50":0.916,"p99":13.375,"p99.9":22.584,"max":4543.75}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":1491.963041,"checksum":20000,"gcCount":3,"gcMillis":24,"allocated":3460046920,"bytesWritten":155922140,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":369.183792,"checksum":5000,"gcCount":1,"gcMillis":6,"allocated":983344000,"latency":{"p50":45.458,"p99":473.042,"p99.9":834.375,"max":7200.25},"bytesWritten":49674267,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":11995998,"millis":10000.091625,"checksum":0,"gcCount":25,"gcMillis":152,"allocated":30784621496,"latency":{"p50":3.208,"p99":52.208,"p99.9":288.542,"max":16326.25},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.091625,"checksum":0,"latency":{"p50":2901.625,"p99":24367.958,"p99.9":39799.208,"max":39799.208},"checked":false,"bytesWritten":815552894,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":367.469417,"checksum":50000,"gcCount":1,"gcMillis":9,"allocated":772118832,"bytesWritten":55944741,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":20.270583,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":462728,"bytesRead":11011238,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":115.532834,"checksum":100000,"gcCount":1,"gcMillis":14,"allocated":10011784,"bytesRead":36479141,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":751.518875,"checksum":1,"gcCount":1,"gcMillis":6,"allocated":1848637424},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":1123.088042,"checksum":831528,"gcCount":2,"gcMillis":41,"allocated":603836712,"bytesRead":446186487,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":389884525,"millis":1000.0,"checksum":389884525},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":2249.2675,"checksum":20000,"gcCount":4,"gcMillis":29,"allocated":5370015880,"bytesWritten":220296444,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":1144.393,"checksum":20000,"gcCount":3,"gcMillis":23,"allocated":3084797176,"bytesWritten":144587387,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":39.730042,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":9740256,"bytesRead":9325195,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":223894057,"millis":1000.0,"checksum":223894057},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9984.715209,"checksum":0,"gcCount":3,"gcMillis":99,"allocated":2933573536,"latency":{"p50":3496.709,"p99":7805.417,"p99.9":62458.417,"max":62458.417},"checked":false,"bytesWritten":181049947,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9984.102958,"checksum":0,"gcCount":2,"gcMillis":92,"allocated":3180852928,"latency":{"p50":3241.083,"p99":11585.833,"p99.9":84258.042,"max":84258.042},"checked":false,"bytesWritten":200722311,"logicalWritten":16000,"compactionMillis":32,"reclaimed":-68910300},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":207769912,"millis":1000.0,"checksum":207769912},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":60.926875,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":82949592,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-async/hypergraphdb-3.json b/benchmarks/report/results/s1-async/hypergraphdb-3.json new file mode 100644 index 0000000..dab158a --- /dev/null +++ b/benchmarks/report/results/s1-async/hypergraphdb-3.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":1,"durability":"async","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4189061120,"retainedHeap":124149144,"bytesWritten":3738439496,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1524.189917,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":931160336,"bytesWritten":53062668,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":2534.5795,"checksum":362757,"gcCount":3,"gcMillis":51,"allocated":3031657008,"bytesWritten":239349960,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":71.457667,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":9836392,"bytesRead":19302912,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":61.691,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4947368,"bytesRead":19761727,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":317.626042,"checksum":20287,"gcCount":1,"gcMillis":40,"allocated":160143696,"bytesRead":103782131,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":79.107083,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":18420112,"bytesRead":29971805,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":356.0815,"checksum":5852224,"gcCount":1,"gcMillis":6,"allocated":790826568},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":181.182458,"checksum":731528,"gcCount":1,"gcMillis":4,"allocated":477481864,"latency":{"p50":0.917,"p99":12.0,"p99.9":19.292,"max":3956.084}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":1893.072041,"checksum":20000,"gcCount":3,"gcMillis":16,"allocated":4226775032,"bytesWritten":188286435,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":354.9145,"checksum":5000,"gcCount":1,"gcMillis":8,"allocated":942908112,"latency":{"p50":44.708,"p99":439.084,"p99.9":962.5,"max":9300.917},"bytesWritten":44842326,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":10599077,"millis":10000.160625,"checksum":0,"gcCount":23,"gcMillis":161,"allocated":28108262632,"latency":{"p50":3.291,"p99":57.791,"p99.9":364.958,"max":147391.042},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.160625,"checksum":0,"latency":{"p50":3086.25,"p99":139885.791,"p99.9":165602.291,"max":165602.291},"checked":false,"bytesWritten":749882709,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":399.888625,"checksum":50000,"gcCount":1,"gcMillis":6,"allocated":819828224,"bytesWritten":57779265,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":15.618083,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":462728,"bytesRead":7750724,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":54.803125,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":9809688,"bytesRead":21628724,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":975.502959,"checksum":1,"gcCount":2,"gcMillis":8,"allocated":2565388144},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":730.1095,"checksum":831528,"gcCount":2,"gcMillis":30,"allocated":567049136,"bytesRead":322712926,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":326196634,"millis":1000.0,"checksum":326196634},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":2530.311334,"checksum":20000,"gcCount":5,"gcMillis":54,"allocated":5384099024,"bytesWritten":224334633,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":1012.464375,"checksum":20000,"gcCount":2,"gcMillis":14,"allocated":2800505080,"bytesWritten":133159926,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":55.672084,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":9761064,"bytesRead":12490214,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":316780575,"millis":1000.0,"checksum":316780575},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9984.616625,"checksum":0,"gcCount":5,"gcMillis":29,"allocated":6554709552,"latency":{"p50":3318.417,"p99":8321.375,"p99.9":11598.209,"max":11598.209},"checked":false,"bytesWritten":394050363,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9982.609208,"checksum":0,"gcCount":4,"gcMillis":108,"allocated":4784539304,"latency":{"p50":3344.792,"p99":7604.667,"p99.9":66175.333,"max":66175.333},"checked":false,"bytesWritten":297236761,"logicalWritten":16000,"compactionMillis":31,"reclaimed":2720558},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":278592814,"millis":1000.0,"checksum":278592814},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":79.405,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":83649184,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-async/hypergraphdb-4.json b/benchmarks/report/results/s1-async/hypergraphdb-4.json new file mode 100644 index 0000000..ace1260 --- /dev/null +++ b/benchmarks/report/results/s1-async/hypergraphdb-4.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":1,"durability":"async","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4174381056,"retainedHeap":124079976,"bytesWritten":3320975034,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1327.543458,"checksum":50000,"gcCount":1,"gcMillis":10,"allocated":926300464,"bytesWritten":53183399,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":2677.496792,"checksum":362757,"gcCount":2,"gcMillis":48,"allocated":3003227200,"bytesWritten":239054275,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":67.53075,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":9763344,"bytesRead":17876481,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":41.583625,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4947368,"bytesRead":13222908,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":371.0615,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":160072776,"bytesRead":90876876,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":109.34825,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":18415520,"bytesRead":36581514,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":410.84625,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":743003576},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":268.631417,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":401879592,"latency":{"p50":1.375,"p99":16.041,"p99.9":100.458,"max":1062.208}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":1656.351208,"checksum":20000,"gcCount":4,"gcMillis":25,"allocated":3778102880,"bytesWritten":166603240,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":691.683042,"checksum":5000,"gcCount":0,"gcMillis":0,"allocated":727897176,"latency":{"p50":58.667,"p99":1526.042,"p99.9":3816.125,"max":9509.333},"bytesWritten":34928692,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":11774401,"millis":10007.38075,"checksum":0,"gcCount":26,"gcMillis":162,"allocated":30996626824,"latency":{"p50":3.375,"p99":56.708,"p99.9":305.458,"max":24409.209},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10007.38075,"checksum":0,"latency":{"p50":2887.292,"p99":11868.084,"p99.9":20565.292,"max":20565.292},"checked":false,"bytesWritten":820880919,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":595.760834,"checksum":50000,"gcCount":1,"gcMillis":9,"allocated":1322019136,"bytesWritten":79087904,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":17.810416,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":462728,"bytesRead":11176024,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":113.945625,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":10101576,"bytesRead":47264859,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":674.381791,"checksum":1,"gcCount":1,"gcMillis":4,"allocated":1714719848},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":862.313333,"checksum":831528,"gcCount":2,"gcMillis":26,"allocated":598469032,"bytesRead":390911507,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":378890768,"millis":1000.0,"checksum":378890768},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":3375.644541,"checksum":20000,"gcCount":5,"gcMillis":70,"allocated":5458748920,"bytesWritten":224546222,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":1237.864417,"checksum":20000,"gcCount":2,"gcMillis":14,"allocated":3086666600,"bytesWritten":145673663,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":41.935292,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":9736704,"bytesRead":18250194,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":230499453,"millis":1000.0,"checksum":230499453},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9982.402875,"checksum":0,"gcCount":1,"gcMillis":9,"allocated":1305772272,"latency":{"p50":3339.375,"p99":7502.833,"p99.9":9586.625,"max":9586.625},"checked":false,"bytesWritten":76527072,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9985.73675,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":652313288,"latency":{"p50":3378.125,"p99":6768.792,"p99.9":8027.125,"max":8027.125},"checked":false,"bytesWritten":25458656,"logicalWritten":16000,"compactionMillis":52,"reclaimed":3784011},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":205643161,"millis":1000.0,"checksum":205643161},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":87.820917,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":80360824,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-async/hypergraphdb-5.json b/benchmarks/report/results/s1-async/hypergraphdb-5.json new file mode 100644 index 0000000..9fe84bd --- /dev/null +++ b/benchmarks/report/results/s1-async/hypergraphdb-5.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":1,"durability":"async","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4119855104,"retainedHeap":124335776,"bytesWritten":4191296353,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1401.548958,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":937763024,"bytesWritten":52872178,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":2921.814042,"checksum":362757,"gcCount":3,"gcMillis":57,"allocated":2980397016,"bytesWritten":236792852,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":67.1495,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":9751112,"bytesRead":18084256,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":28.685625,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4947368,"bytesRead":5633180,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":8208.856541,"checksum":20287,"gcCount":14,"gcMillis":131,"allocated":159964584,"bytesRead":3242874253,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":101.756166,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":18415792,"bytesRead":30757673,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":366.530291,"checksum":5852224,"gcCount":1,"gcMillis":6,"allocated":543401680},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":179.108334,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":473265472,"latency":{"p50":0.917,"p99":12.667,"p99.9":21.583,"max":143.458}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":1540.132917,"checksum":20000,"gcCount":3,"gcMillis":29,"allocated":3112128128,"bytesWritten":153581110,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":336.733542,"checksum":5000,"gcCount":0,"gcMillis":0,"allocated":668142408,"latency":{"p50":46.417,"p99":465.5,"p99.9":936.083,"max":5275.25},"bytesWritten":35315761,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":11280912,"millis":10000.212834,"checksum":0,"gcCount":22,"gcMillis":151,"allocated":27869333104,"latency":{"p50":3.666,"p99":51.083,"p99.9":310.333,"max":33213.958},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.212834,"checksum":0,"latency":{"p50":2952.792,"p99":11922.334,"p99.9":19312.75,"max":19312.75},"checked":false,"bytesWritten":760874124,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":429.959208,"checksum":50000,"gcCount":1,"gcMillis":7,"allocated":807088064,"bytesWritten":60645518,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":33.881125,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":462728,"bytesRead":10785597,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":51.5335,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":9763680,"bytesRead":23212358,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":924.11575,"checksum":1,"gcCount":1,"gcMillis":11,"allocated":1271949984},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":1042.807083,"checksum":831528,"gcCount":1,"gcMillis":45,"allocated":556650944,"bytesRead":243621633,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":303626709,"millis":1000.0,"checksum":303626709},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":2588.547083,"checksum":20000,"gcCount":5,"gcMillis":38,"allocated":5056119480,"bytesWritten":216146927,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":1091.459333,"checksum":20000,"gcCount":2,"gcMillis":14,"allocated":2542923048,"bytesWritten":119755589,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":47.509708,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":9747744,"bytesRead":21590479,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":213056956,"millis":1000.0,"checksum":213056956},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9985.127584,"checksum":0,"gcCount":1,"gcMillis":9,"allocated":1475363824,"latency":{"p50":3436.375,"p99":6463.709,"p99.9":10520.5,"max":10520.5},"checked":false,"bytesWritten":87602328,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9981.490458,"checksum":0,"gcCount":2,"gcMillis":143,"allocated":1575255920,"latency":{"p50":3129.833,"p99":24152.25,"p99.9":102953.416,"max":102953.416},"checked":false,"bytesWritten":96471171,"logicalWritten":16000,"compactionMillis":82,"reclaimed":3498510},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":197227454,"millis":1000.0,"checksum":197227454},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":75.60775,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":80324712,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-sync/hstore-1.json b/benchmarks/report/results/s1-sync/hstore-1.json new file mode 100644 index 0000000..ed71a08 --- /dev/null +++ b/benchmarks/report/results/s1-sync/hstore-1.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":1,"durability":"sync","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4105699328,"retainedHeap":183681648,"bytesWritten":104290386,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":889.4455,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":966171496,"bytesWritten":12608282,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":2529.399291,"checksum":362757,"gcCount":3,"gcMillis":57,"allocated":3722827576,"bytesWritten":39661175,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":16.125083,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":134432,"bytesRead":0,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":18.439666,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":16072,"bytesRead":0,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":26.657083,"checksum":20287,"gcCount":1,"gcMillis":0,"allocated":81183808,"bytesRead":0,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":17.573042,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":3331504,"bytesRead":0,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":19.511792,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":1555480},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":21.729292,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":29301200,"latency":{"p50":0.208,"p99":0.292,"p99.9":0.417,"max":38.75}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":257.97575,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":466323584,"bytesWritten":4471900,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":19393.206042,"checksum":5000,"gcCount":1,"gcMillis":6,"allocated":659979368,"latency":{"p50":3979.875,"p99":5214.291,"p99.9":7932.625,"max":10432.417},"bytesWritten":3295384,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":23468102,"millis":10000.1035,"checksum":0,"gcCount":2,"gcMillis":29,"allocated":2419718320,"latency":{"p50":1.875,"p99":11.375,"p99.9":27.833,"max":35320.208},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.1035,"checksum":0,"latency":{"p50":4952.5,"p99":17012.834,"p99.9":32684.25,"max":32684.25},"checked":false,"bytesWritten":3547066,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":71.297167,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":113149120,"bytesWritten":3411588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":4.988833,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":176616,"bytesRead":0,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":5.800041,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":6416096,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":127.634209,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":27487624},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":24.209167,"checksum":831528,"gcCount":1,"gcMillis":3,"allocated":28057328,"bytesRead":2737618,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":97789208,"millis":1000.0,"checksum":97789208},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":742.727916,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":1052400216,"bytesWritten":14814165,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":488.701208,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":526517320,"bytesWritten":9925897,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":11.515333,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":139552,"bytesRead":0,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":124211718,"millis":1000.0,"checksum":124211718},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9984.638416,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":105721248,"latency":{"p50":6507.833,"p99":8583.958,"p99.9":10229.75,"max":10229.75},"checked":false,"bytesWritten":1789526,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9985.33575,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":102094672,"latency":{"p50":6380.667,"p99":8510.167,"p99.9":10334.458,"max":10334.458},"checked":false,"bytesWritten":10764711,"logicalWritten":16000,"compactionMillis":176,"reclaimed":30409306},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":95659124,"millis":1000.0,"checksum":95659124},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":111.02925,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":127410360,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-sync/hstore-2.json b/benchmarks/report/results/s1-sync/hstore-2.json new file mode 100644 index 0000000..0819267 --- /dev/null +++ b/benchmarks/report/results/s1-sync/hstore-2.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":1,"durability":"sync","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4117233664,"retainedHeap":181479520,"bytesWritten":104785481,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":821.754375,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":965311224,"bytesWritten":12608282,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":2300.713417,"checksum":362757,"gcCount":3,"gcMillis":54,"allocated":3639410776,"bytesWritten":40072321,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":11.784875,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":134480,"bytesRead":0,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":16.551416,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4029072,"bytesRead":0,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":32.0815,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":74898616,"bytesRead":0,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":34.925125,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":3729904,"bytesRead":0,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":29.828459,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":1739360},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":27.837375,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":15705328,"latency":{"p50":0.25,"p99":0.542,"p99.9":2.875,"max":36.833}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":312.07075,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":468644872,"bytesWritten":4471900,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":19219.532833,"checksum":5000,"gcCount":1,"gcMillis":22,"allocated":664234728,"latency":{"p50":3979.25,"p99":5094.667,"p99.9":7983.083,"max":26041.459},"bytesWritten":3295384,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":22911226,"millis":10000.086375,"checksum":0,"gcCount":2,"gcMillis":42,"allocated":2519979712,"latency":{"p50":1.959,"p99":11.875,"p99.9":26.333,"max":38666.25},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.086375,"checksum":0,"latency":{"p50":5085.416,"p99":17664.041,"p99.9":47127.208,"max":47127.208},"checked":false,"bytesWritten":3547066,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":74.842292,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":111636808,"bytesWritten":3411588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":5.185667,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":186048,"bytesRead":0,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":8.9815,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":14415952,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":119.047125,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":27485520},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":23.913333,"checksum":831528,"gcCount":0,"gcMillis":0,"allocated":28057544,"bytesRead":2737618,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":98204042,"millis":1000.0,"checksum":98204042},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":711.875167,"checksum":20000,"gcCount":1,"gcMillis":11,"allocated":1072380280,"bytesWritten":14911436,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":516.121875,"checksum":20000,"gcCount":1,"gcMillis":36,"allocated":625677160,"bytesWritten":9910298,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":15.202958,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":139576,"bytesRead":0,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":124709349,"millis":1000.0,"checksum":124709349},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9986.79625,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":103597168,"latency":{"p50":5956.166,"p99":8508.333,"p99.9":9889.25,"max":9889.25},"checked":false,"bytesWritten":1789526,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9984.694542,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":100400656,"latency":{"p50":6670.0,"p99":9015.375,"p99.9":13144.458,"max":13144.458},"checked":false,"bytesWritten":10766988,"logicalWritten":16000,"compactionMillis":162,"reclaimed":30904282},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":95661812,"millis":1000.0,"checksum":95661812},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":183.751458,"checksum":1,"gcCount":1,"gcMillis":61,"allocated":129273320,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-sync/hstore-3.json b/benchmarks/report/results/s1-sync/hstore-3.json new file mode 100644 index 0000000..b8a57ac --- /dev/null +++ b/benchmarks/report/results/s1-sync/hstore-3.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":1,"durability":"sync","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4040687616,"retainedHeap":199619840,"bytesWritten":102911942,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":895.566667,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":967973288,"bytesWritten":12608282,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":2698.263208,"checksum":362757,"gcCount":3,"gcMillis":59,"allocated":3654286840,"bytesWritten":38190214,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":45.830167,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":132008,"bytesRead":0,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":14.470041,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":16720,"bytesRead":0,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":27.452333,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":81183952,"bytesRead":0,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":17.382708,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":3729880,"bytesRead":0,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":95.880041,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":33411752},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":23.240917,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":22903576,"latency":{"p50":0.208,"p99":0.333,"p99.9":0.417,"max":28.792}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":290.766625,"checksum":20000,"gcCount":1,"gcMillis":7,"allocated":469508904,"bytesWritten":4471900,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":19785.433125,"checksum":5000,"gcCount":0,"gcMillis":0,"allocated":665137424,"latency":{"p50":3990.917,"p99":5861.875,"p99.9":8162.667,"max":11599.625},"bytesWritten":3295384,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":22833293,"millis":10000.075333,"checksum":0,"gcCount":3,"gcMillis":25,"allocated":2295545240,"latency":{"p50":1.875,"p99":11.125,"p99.9":25.583,"max":43435.792},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.075333,"checksum":0,"latency":{"p50":4735.041,"p99":19110.375,"p99.9":29531.208,"max":29531.208},"checked":false,"bytesWritten":3547066,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":81.749875,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":119577040,"bytesWritten":3411588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":6.208583,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":186192,"bytesRead":0,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":6.928084,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":6415520,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":121.544542,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":26930776},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":46.113,"checksum":831528,"gcCount":0,"gcMillis":0,"allocated":28051216,"bytesRead":2737618,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":96309433,"millis":1000.0,"checksum":96309433},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":736.499292,"checksum":20000,"gcCount":1,"gcMillis":35,"allocated":1125721328,"bytesWritten":14911436,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":542.980916,"checksum":20000,"gcCount":1,"gcMillis":36,"allocated":621681560,"bytesWritten":9927139,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":11.077375,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":1733200,"bytesRead":0,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":122830790,"millis":1000.0,"checksum":122830790},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9987.420833,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":105403592,"latency":{"p50":6325.125,"p99":8381.417,"p99.9":11080.5,"max":11080.5},"checked":false,"bytesWritten":1789526,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9988.448042,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":101904696,"latency":{"p50":6391.125,"p99":8575.542,"p99.9":10344.25,"max":10344.25},"checked":false,"bytesWritten":10758715,"logicalWritten":16000,"compactionMillis":151,"reclaimed":29034458},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":95653077,"millis":1000.0,"checksum":95653077},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":112.538792,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":125699008,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-sync/hstore-4.json b/benchmarks/report/results/s1-sync/hstore-4.json new file mode 100644 index 0000000..756b7a3 --- /dev/null +++ b/benchmarks/report/results/s1-sync/hstore-4.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":1,"durability":"sync","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4098359296,"retainedHeap":200650792,"bytesWritten":103453603,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1952.196916,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":983271008,"bytesWritten":12608282,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":3832.685709,"checksum":362757,"gcCount":3,"gcMillis":99,"allocated":3728308888,"bytesWritten":38465386,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":14.462667,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":146688,"bytesRead":0,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":38.452625,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4029672,"bytesRead":0,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":35.879916,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":81183832,"bytesRead":0,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":28.645792,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":3729904,"bytesRead":0,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":23.602708,"checksum":5852224,"gcCount":1,"gcMillis":1,"allocated":1521880},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":52.980833,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":34898568,"latency":{"p50":0.5,"p99":0.792,"p99.9":3.584,"max":46.375}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":373.525667,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":473216760,"bytesWritten":4471900,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":19355.302791,"checksum":5000,"gcCount":1,"gcMillis":16,"allocated":656892424,"latency":{"p50":3978.958,"p99":5751.125,"p99.9":7948.167,"max":20066.959},"bytesWritten":3295384,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":20675600,"millis":10000.182375,"checksum":0,"gcCount":2,"gcMillis":33,"allocated":2186305688,"latency":{"p50":1.708,"p99":13.458,"p99.9":59.917,"max":37079.583},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.182375,"checksum":0,"latency":{"p50":5179.5,"p99":88267.792,"p99.9":119202.584,"max":119202.584},"checked":false,"bytesWritten":3547066,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":115.390917,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":116816728,"bytesWritten":3411588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":4.74625,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":185808,"bytesRead":0,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":8.430791,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":15976,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":144.253875,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":26924888},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":34.138833,"checksum":831528,"gcCount":0,"gcMillis":0,"allocated":28739064,"bytesRead":2737618,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":96587514,"millis":1000.0,"checksum":96587514},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":1151.622333,"checksum":20000,"gcCount":1,"gcMillis":37,"allocated":1145573896,"bytesWritten":15159583,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":844.053791,"checksum":20000,"gcCount":1,"gcMillis":58,"allocated":636999664,"bytesWritten":9940313,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":11.846208,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":133368,"bytesRead":0,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":123372456,"millis":1000.0,"checksum":123372456},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9984.84525,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":105436776,"latency":{"p50":6084.416,"p99":8932.333,"p99.9":11384.583,"max":11384.583},"checked":false,"bytesWritten":1789526,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9987.767,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":102045536,"latency":{"p50":6595.084,"p99":12540.042,"p99.9":17732.334,"max":17732.334},"checked":false,"bytesWritten":10763883,"logicalWritten":16000,"compactionMillis":197,"reclaimed":29571034},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":95658134,"millis":1000.0,"checksum":95658134},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":107.126458,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":126674232,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-sync/hstore-5.json b/benchmarks/report/results/s1-sync/hstore-5.json new file mode 100644 index 0000000..09aaa0a --- /dev/null +++ b/benchmarks/report/results/s1-sync/hstore-5.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":1,"durability":"sync","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4084203520,"retainedHeap":199777848,"bytesWritten":113722342,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1327.796959,"checksum":50000,"gcCount":1,"gcMillis":12,"allocated":994631752,"bytesWritten":12608282,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":5923.996209,"checksum":362757,"gcCount":3,"gcMillis":122,"allocated":3661655760,"bytesWritten":40034088,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":12.613958,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":131672,"bytesRead":0,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":42.62075,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4029792,"bytesRead":0,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":88.511083,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":75835672,"bytesRead":0,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":47.112333,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":3331504,"bytesRead":0,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":56.045083,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":1378536},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":61.910083,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":10887184,"latency":{"p50":0.5,"p99":1.292,"p99.9":12.583,"max":143.084}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":470.962542,"checksum":20000,"gcCount":1,"gcMillis":11,"allocated":456226896,"bytesWritten":4471900,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":20460.727959,"checksum":5000,"gcCount":0,"gcMillis":0,"allocated":663921464,"latency":{"p50":4004.958,"p99":6844.875,"p99.9":10235.208,"max":12704.709},"bytesWritten":3295384,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":19363448,"millis":10000.208666,"checksum":0,"gcCount":3,"gcMillis":70,"allocated":2269785552,"latency":{"p50":1.708,"p99":14.125,"p99.9":98.167,"max":51888.375},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.208666,"checksum":0,"latency":{"p50":5572.0,"p99":41438.458,"p99.9":49934.375,"max":49934.375},"checked":false,"bytesWritten":3547066,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":151.191959,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":117723624,"bytesWritten":3411588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":18.762792,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":186408,"bytesRead":0,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":11.163084,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":15688,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":147.849042,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":25519592},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":44.666125,"checksum":831528,"gcCount":0,"gcMillis":0,"allocated":28618688,"bytesRead":2737618,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":98165599,"millis":1000.0,"checksum":98165599},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":1992.046416,"checksum":20000,"gcCount":1,"gcMillis":78,"allocated":1202385264,"bytesWritten":23866858,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":944.214041,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":591887680,"bytesWritten":9940313,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":14.089458,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":1733152,"bytesRead":0,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":133720132,"millis":1000.0,"checksum":133720132},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9986.304541,"checksum":0,"gcCount":1,"gcMillis":122,"allocated":104294040,"latency":{"p50":6830.125,"p99":51483.208,"p99.9":115646.833,"max":115646.833},"checked":false,"bytesWritten":1789526,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9985.086959,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":101066088,"latency":{"p50":6337.334,"p99":10259.709,"p99.9":11115.542,"max":11115.542},"checked":false,"bytesWritten":10756645,"logicalWritten":16000,"compactionMillis":159,"reclaimed":39925914},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":95650930,"millis":1000.0,"checksum":95650930},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":117.823042,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":131745504,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-sync/hypergraphdb-1.json b/benchmarks/report/results/s1-sync/hypergraphdb-1.json new file mode 100644 index 0000000..87a9bdc --- /dev/null +++ b/benchmarks/report/results/s1-sync/hypergraphdb-1.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":1,"durability":"sync","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4173332480,"retainedHeap":124083808,"bytesWritten":5843023254,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1611.107917,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":931767352,"bytesWritten":53000138,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":3287.637917,"checksum":362757,"gcCount":3,"gcMillis":61,"allocated":3021658144,"bytesWritten":239535989,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":118.032458,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":9856512,"bytesRead":25597167,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":39.525625,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4947368,"bytesRead":15732184,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":440.651666,"checksum":20287,"gcCount":1,"gcMillis":75,"allocated":160300016,"bytesRead":93683991,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":84.387084,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":18416128,"bytesRead":39514746,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":392.140458,"checksum":5852224,"gcCount":1,"gcMillis":9,"allocated":834795128},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":143.873375,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":480944144,"latency":{"p50":0.75,"p99":12.208,"p99.9":20.667,"max":47.916}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":1895.590708,"checksum":20000,"gcCount":4,"gcMillis":23,"allocated":4166934296,"bytesWritten":184624117,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":21069.598625,"checksum":5000,"gcCount":39,"gcMillis":164,"allocated":48429967296,"latency":{"p50":4003.167,"p99":8875.459,"p99.9":12157.084,"max":17925.792},"bytesWritten":2805376671,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":10483879,"millis":10000.161834,"checksum":0,"gcCount":20,"gcMillis":131,"allocated":25854422160,"latency":{"p50":3.791,"p99":65.125,"p99.9":308.208,"max":25439.5},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.161834,"checksum":0,"latency":{"p50":5977.459,"p99":38597.959,"p99.9":46267.334,"max":46267.334},"checked":false,"bytesWritten":783444513,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":459.684584,"checksum":50000,"gcCount":1,"gcMillis":6,"allocated":721318128,"bytesWritten":58334709,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":21.700459,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":462728,"bytesRead":10403146,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":79.004667,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":9793680,"bytesRead":24838390,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":858.051083,"checksum":1,"gcCount":1,"gcMillis":5,"allocated":1777046168},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":701.3615,"checksum":831528,"gcCount":1,"gcMillis":14,"allocated":522982280,"bytesRead":242907734,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":320410243,"millis":1000.0,"checksum":320410243},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":2689.505,"checksum":20000,"gcCount":4,"gcMillis":30,"allocated":4769745968,"bytesWritten":208673642,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":1406.442167,"checksum":20000,"gcCount":2,"gcMillis":15,"allocated":2813421432,"bytesWritten":136785669,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":86.205708,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":9782496,"bytesRead":18268909,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":209038719,"millis":1000.0,"checksum":209038719},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9985.308083,"checksum":0,"gcCount":2,"gcMillis":34,"allocated":1484053552,"latency":{"p50":6196.75,"p99":9871.792,"p99.9":21853.958,"max":21853.958},"checked":false,"bytesWritten":88973909,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9985.339917,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":39293224,"latency":{"p50":6410.333,"p99":8564.417,"p99.9":10871.125,"max":10871.125},"checked":false,"bytesWritten":1018724,"logicalWritten":16000,"compactionMillis":10,"reclaimed":-1047258},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":199131475,"millis":1000.0,"checksum":199131475},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":77.740208,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":85397392,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-sync/hypergraphdb-2.json b/benchmarks/report/results/s1-sync/hypergraphdb-2.json new file mode 100644 index 0000000..3913475 --- /dev/null +++ b/benchmarks/report/results/s1-sync/hypergraphdb-2.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":1,"durability":"sync","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4143448064,"retainedHeap":131246128,"bytesWritten":5604949183,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1637.017667,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":930132088,"bytesWritten":53164783,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":3117.694583,"checksum":362757,"gcCount":3,"gcMillis":55,"allocated":3049905048,"bytesWritten":240547277,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":100.285458,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":9815448,"bytesRead":22571555,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":29.83025,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4947368,"bytesRead":12584405,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":261.533417,"checksum":20287,"gcCount":0,"gcMillis":0,"allocated":158105056,"bytesRead":83782811,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":95.108125,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":18418232,"bytesRead":24841349,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":408.254541,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":636724216},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":228.41525,"checksum":731528,"gcCount":1,"gcMillis":4,"allocated":640615240,"latency":{"p50":1.0,"p99":14.333,"p99.9":24.25,"max":4625.75}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":2289.272834,"checksum":20000,"gcCount":3,"gcMillis":21,"allocated":3476316000,"bytesWritten":150782263,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":21069.064875,"checksum":5000,"gcCount":37,"gcMillis":192,"allocated":45094370816,"latency":{"p50":4003.292,"p99":8984.042,"p99.9":12141.875,"max":21161.375},"bytesWritten":2542330948,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":10661237,"millis":10000.139625,"checksum":0,"gcCount":19,"gcMillis":139,"allocated":24806961160,"latency":{"p50":3.375,"p99":62.375,"p99.9":397.333,"max":37101.958},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.139625,"checksum":0,"latency":{"p50":5921.583,"p99":122491.0,"p99.9":146777.083,"max":146777.083},"checked":false,"bytesWritten":708890089,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":729.91575,"checksum":50000,"gcCount":1,"gcMillis":11,"allocated":1272894208,"bytesWritten":81947024,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":18.505,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":462728,"bytesRead":9425898,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":81.322167,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":9856680,"bytesRead":30003149,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":1157.898958,"checksum":1,"gcCount":2,"gcMillis":10,"allocated":2376406072},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":915.863042,"checksum":831528,"gcCount":1,"gcMillis":14,"allocated":576764784,"bytesRead":268033158,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":332966565,"millis":1000.0,"checksum":332966565},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":3646.381584,"checksum":20000,"gcCount":4,"gcMillis":161,"allocated":4501732784,"bytesWritten":195375543,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":1532.368708,"checksum":20000,"gcCount":2,"gcMillis":15,"allocated":2844304112,"bytesWritten":138327141,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":55.567292,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":9732856,"bytesRead":10216789,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":204901044,"millis":1000.0,"checksum":204901044},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9988.001041,"checksum":0,"gcCount":1,"gcMillis":12,"allocated":1547996376,"latency":{"p50":6108.208,"p99":9968.0,"p99.9":16371.0,"max":16371.0},"checked":false,"bytesWritten":96081361,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9987.023125,"checksum":0,"gcCount":2,"gcMillis":139,"allocated":1485615784,"latency":{"p50":6038.833,"p99":45948.166,"p99.9":109892.375,"max":109892.375},"checked":false,"bytesWritten":90326662,"logicalWritten":16000,"compactionMillis":33,"reclaimed":9641282},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":191402299,"millis":1000.0,"checksum":191402299},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":75.823625,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":80541912,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-sync/hypergraphdb-3.json b/benchmarks/report/results/s1-sync/hypergraphdb-3.json new file mode 100644 index 0000000..aab1087 --- /dev/null +++ b/benchmarks/report/results/s1-sync/hypergraphdb-3.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":1,"durability":"sync","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4195352576,"retainedHeap":124260504,"bytesWritten":5664299953,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1980.428541,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":928481096,"bytesWritten":53148212,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":3381.191041,"checksum":362757,"gcCount":4,"gcMillis":82,"allocated":3074066272,"bytesWritten":242587226,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":71.240541,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":9771304,"bytesRead":19157916,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":42.043417,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4944168,"bytesRead":16130583,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":494.216542,"checksum":20287,"gcCount":1,"gcMillis":58,"allocated":160700984,"bytesRead":62746633,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":95.605,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":18494144,"bytesRead":38329000,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":398.353375,"checksum":5852224,"gcCount":1,"gcMillis":5,"allocated":837833872},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":180.239041,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":558816984,"latency":{"p50":0.875,"p99":13.542,"p99.9":22.25,"max":86.334}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":2057.038792,"checksum":20000,"gcCount":4,"gcMillis":25,"allocated":4307838448,"bytesWritten":195273842,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":21157.946792,"checksum":5000,"gcCount":37,"gcMillis":185,"allocated":45957411656,"latency":{"p50":4005.625,"p99":8932.542,"p99.9":11872.084,"max":18823.042},"bytesWritten":2640916229,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":10887597,"millis":10000.133,"checksum":0,"gcCount":20,"gcMillis":150,"allocated":26006911240,"latency":{"p50":3.458,"p99":59.625,"p99.9":351.5,"max":19932.542},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.133,"checksum":0,"latency":{"p50":5959.625,"p99":29097.708,"p99.9":38100.5,"max":38100.5},"checked":false,"bytesWritten":754545668,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":400.427166,"checksum":50000,"gcCount":1,"gcMillis":7,"allocated":686016848,"bytesWritten":55028588,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":21.67575,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":462728,"bytesRead":6764654,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":55.117791,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":9759672,"bytesRead":15492035,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":1244.882083,"checksum":1,"gcCount":2,"gcMillis":8,"allocated":2564070496},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":955.035125,"checksum":831528,"gcCount":1,"gcMillis":15,"allocated":545697736,"bytesRead":275903795,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":367889011,"millis":1000.0,"checksum":367889011},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":2411.24025,"checksum":20000,"gcCount":4,"gcMillis":46,"allocated":4162176824,"bytesWritten":172217816,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":2184.195542,"checksum":20000,"gcCount":2,"gcMillis":21,"allocated":2988443464,"bytesWritten":147364880,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":101.792209,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":9853624,"bytesRead":7920897,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":284700691,"millis":1000.0,"checksum":284700691},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9985.149125,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":614642768,"latency":{"p50":6235.208,"p99":61457.375,"p99.9":78476.333,"max":78476.333},"checked":false,"bytesWritten":25214301,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9987.603208,"checksum":0,"gcCount":2,"gcMillis":23,"allocated":2427023088,"latency":{"p50":6392.167,"p99":11262.083,"p99.9":16176.042,"max":16176.042},"checked":false,"bytesWritten":146046512,"logicalWritten":16000,"compactionMillis":996,"reclaimed":53919081},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":290573969,"millis":1000.0,"checksum":290573969},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":78.183083,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":82029712,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-sync/hypergraphdb-4.json b/benchmarks/report/results/s1-sync/hypergraphdb-4.json new file mode 100644 index 0000000..3f2c736 --- /dev/null +++ b/benchmarks/report/results/s1-sync/hypergraphdb-4.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":1,"durability":"sync","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4200071168,"retainedHeap":124240384,"bytesWritten":4508841127,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1514.999292,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":931101296,"bytesWritten":53230968,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":4563.83675,"checksum":362757,"gcCount":3,"gcMillis":70,"allocated":2988679440,"bytesWritten":236233395,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":97.526666,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":10115448,"bytesRead":12640053,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":64.798583,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4947368,"bytesRead":25768880,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":709.099,"checksum":20287,"gcCount":1,"gcMillis":58,"allocated":161295696,"bytesRead":75142619,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":95.960709,"checksum":281832,"gcCount":0,"gcMillis":0,"allocated":18420304,"bytesRead":23968693,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":590.482916,"checksum":5852224,"gcCount":1,"gcMillis":11,"allocated":707353888},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":271.11675,"checksum":731528,"gcCount":0,"gcMillis":0,"allocated":521740272,"latency":{"p50":1.458,"p99":17.25,"p99.9":27.792,"max":144.917}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":3683.232208,"checksum":20000,"gcCount":4,"gcMillis":28,"allocated":4942358672,"bytesWritten":217514885,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":21806.690417,"checksum":5000,"gcCount":25,"gcMillis":220,"allocated":31094988664,"latency":{"p50":4016.916,"p99":9778.208,"p99.9":23644.625,"max":42342.334},"bytesWritten":1667756432,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":9089227,"millis":10000.28275,"checksum":0,"gcCount":16,"gcMillis":136,"allocated":20932176944,"latency":{"p50":3.958,"p99":73.75,"p99.9":511.459,"max":22901.542},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.28275,"checksum":0,"latency":{"p50":6574.084,"p99":43133.709,"p99.9":52943.584,"max":52943.584},"checked":false,"bytesWritten":555010636,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":1131.124875,"checksum":50000,"gcCount":1,"gcMillis":8,"allocated":1247143888,"bytesWritten":78987294,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":25.315334,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":456328,"bytesRead":5301433,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":121.969458,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":9898032,"bytesRead":26231059,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":1077.218209,"checksum":1,"gcCount":2,"gcMillis":11,"allocated":1855613792},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":1054.273791,"checksum":831528,"gcCount":1,"gcMillis":23,"allocated":584746192,"bytesRead":290957827,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":337342459,"millis":1000.0,"checksum":337342459},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":4719.779125,"checksum":20000,"gcCount":4,"gcMillis":125,"allocated":5545591992,"bytesWritten":237642523,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":1889.849417,"checksum":20000,"gcCount":3,"gcMillis":33,"allocated":3118268568,"bytesWritten":149596372,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":88.516917,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":9923760,"bytesRead":12305001,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":268941493,"millis":1000.0,"checksum":268941493},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9986.837167,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":772855192,"latency":{"p50":6661.208,"p99":13726.417,"p99.9":22484.583,"max":22484.583},"checked":false,"bytesWritten":37142132,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9987.568291,"checksum":0,"gcCount":3,"gcMillis":135,"allocated":3100629968,"latency":{"p50":6400.0,"p99":52300.583,"p99.9":100411.375,"max":100411.375},"checked":false,"bytesWritten":193367531,"logicalWritten":16000,"compactionMillis":14,"reclaimed":6594030},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":199632560,"millis":1000.0,"checksum":199632560},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":78.119625,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":80308136,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s1-sync/hypergraphdb-5.json b/benchmarks/report/results/s1-sync/hypergraphdb-5.json new file mode 100644 index 0000000..074a3b5 --- /dev/null +++ b/benchmarks/report/results/s1-sync/hypergraphdb-5.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":1,"durability":"sync","threads":8,"nodes":50000,"edges":100000,"incidences":362757,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4183818240,"retainedHeap":124251176,"bytesWritten":4275301887,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":50000,"millis":1618.216916,"checksum":50000,"gcCount":0,"gcMillis":0,"allocated":928801576,"bytesWritten":53260519,"logicalWritten":888890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":100000,"millis":7313.246834,"checksum":362757,"gcCount":3,"gcMillis":111,"allocated":2814194176,"bytesWritten":225843227,"logicalWritten":3702056},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":219.768625,"checksum":731528,"gcCount":1,"gcMillis":31,"allocated":10539320,"bytesRead":13273192,"logicalRead":5852224},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":87.75975,"checksum":363372,"gcCount":0,"gcMillis":0,"allocated":4947368,"bytesRead":5287587,"logicalRead":2906976},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":548.059083,"checksum":20287,"gcCount":1,"gcMillis":50,"allocated":162097120,"bytesRead":89759480,"logicalRead":162296},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":145.075291,"checksum":281832,"gcCount":1,"gcMillis":8,"allocated":18415272,"bytesRead":36865572,"logicalRead":2254656},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":512.3125,"checksum":5852224,"gcCount":0,"gcMillis":0,"allocated":736109528},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":364.543875,"checksum":731528,"gcCount":1,"gcMillis":9,"allocated":540918504,"latency":{"p50":1.625,"p99":24.625,"p99.9":150.5,"max":9637.833}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":5494.844708,"checksum":20000,"gcCount":5,"gcMillis":99,"allocated":5539507792,"bytesWritten":236955272,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":21881.645875,"checksum":5000,"gcCount":27,"gcMillis":228,"allocated":32914925640,"latency":{"p50":4018.375,"p99":10078.625,"p99.9":15869.083,"max":27534.083},"bytesWritten":1730196892,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":8851443,"millis":10000.232833,"checksum":0,"gcCount":16,"gcMillis":204,"allocated":21558216296,"latency":{"p50":3.917,"p99":76.834,"p99.9":476.875,"max":78756.208},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.232833,"checksum":0,"latency":{"p50":6231.083,"p99":122757.167,"p99.9":146349.375,"max":146349.375},"checked":false,"bytesWritten":607097729,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 50,000 members","operations":50000,"millis":798.846458,"checksum":50000,"gcCount":1,"gcMillis":13,"allocated":1310287728,"bytesWritten":84039384,"logicalWritten":400008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":20000000,"millis":29.709834,"checksum":20000000,"gcCount":0,"gcMillis":0,"allocated":462728,"bytesRead":10285876,"logicalRead":160000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":111.011958,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":9837640,"bytesRead":22198472,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":911.994958,"checksum":1,"gcCount":1,"gcMillis":6,"allocated":1642087664},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":962.867542,"checksum":831528,"gcCount":1,"gcMillis":27,"allocated":590126184,"bytesRead":316876571,"logicalRead":6652224},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":353471074,"millis":1000.0,"checksum":353471074},{"workload":"churn.delete","description":"delete 20,000 hyperedges (20%) in transactions of 1,000","operations":20000,"millis":3679.395334,"checksum":20000,"gcCount":5,"gcMillis":50,"allocated":5413251248,"bytesWritten":229210698,"logicalWritten":739496},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":1682.314666,"checksum":20000,"gcCount":2,"gcMillis":14,"allocated":2945369728,"bytesWritten":136555871,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":97.760708,"checksum":645087,"gcCount":0,"gcMillis":0,"allocated":9742000,"bytesRead":15068226,"logicalRead":5160696},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":289531017,"millis":1000.0,"checksum":289531017},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9986.970625,"checksum":0,"gcCount":1,"gcMillis":30,"allocated":616638424,"latency":{"p50":6603.667,"p99":12838.25,"p99.9":37809.625,"max":37809.625},"checked":false,"bytesWritten":23944169,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9984.95275,"checksum":0,"gcCount":1,"gcMillis":87,"allocated":1501120216,"latency":{"p50":6600.666,"p99":29746.291,"p99.9":93964.541,"max":93964.541},"checked":false,"bytesWritten":90756307,"logicalWritten":16000,"compactionMillis":21,"reclaimed":9207161},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":282340517,"millis":1000.0,"checksum":282340517},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":53.853166,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":83348424,"checked":false,"acknowledged":25000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s4-async/hstore-1.json b/benchmarks/report/results/s4-async/hstore-1.json new file mode 100644 index 0000000..d1e6c13 --- /dev/null +++ b/benchmarks/report/results/s4-async/hstore-1.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":4,"durability":"async","threads":8,"nodes":200000,"edges":400000,"incidences":1453427,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4145545216,"retainedHeap":450434464,"bytesWritten":2230887641,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":200000,"millis":4998.879333,"checksum":200000,"gcCount":5,"gcMillis":192,"allocated":5227238040,"bytesWritten":60160379,"logicalWritten":3688890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":400000,"millis":18928.860959,"checksum":1453427,"gcCount":40,"gcMillis":1253,"allocated":40921218088,"bytesWritten":392641408,"logicalWritten":14827416},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":14.153708,"checksum":719693,"gcCount":0,"gcMillis":0,"allocated":136008,"bytesRead":0,"logicalRead":5757544},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":21.939042,"checksum":361142,"gcCount":0,"gcMillis":0,"allocated":15760,"bytesRead":0,"logicalRead":2889136},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":65.265166,"checksum":20109,"gcCount":0,"gcMillis":0,"allocated":82510968,"bytesRead":0,"logicalRead":160872},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":44.068458,"checksum":280131,"gcCount":0,"gcMillis":0,"allocated":11826896,"bytesRead":2777116,"logicalRead":2241048},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":82.389,"checksum":5757544,"gcCount":0,"gcMillis":0,"allocated":105147832},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":39.483042,"checksum":719693,"gcCount":0,"gcMillis":0,"allocated":29322024,"latency":{"p50":0.375,"p99":0.625,"p99.9":1.5,"max":18.708}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":385.0815,"checksum":20000,"gcCount":1,"gcMillis":33,"allocated":1035827808,"bytesWritten":10155587,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":410.067833,"checksum":5000,"gcCount":1,"gcMillis":30,"allocated":697280968,"latency":{"p50":54.792,"p99":232.125,"p99.9":475.417,"max":30450.917},"bytesWritten":8872968,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":19294469,"millis":10000.121041,"checksum":0,"gcCount":2,"gcMillis":203,"allocated":2284548048,"latency":{"p50":1.833,"p99":13.542,"p99.9":68.458,"max":114355.875},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.121041,"checksum":0,"latency":{"p50":2636.583,"p99":65751.458,"p99.9":112247.125,"max":112247.125},"checked":false,"bytesWritten":56823499,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 200,000 members","operations":200000,"millis":408.784042,"checksum":200000,"gcCount":1,"gcMillis":26,"allocated":830358672,"bytesWritten":14573475,"logicalWritten":1600008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":80000000,"millis":21.988875,"checksum":80000000,"gcCount":0,"gcMillis":0,"allocated":176424,"bytesRead":0,"logicalRead":640000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":8.538333,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":6416528,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":206.307125,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":64680264},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":85.982333,"checksum":819693,"gcCount":0,"gcMillis":0,"allocated":59528632,"bytesRead":11309990,"logicalRead":6557544},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":173736544,"millis":1000.0,"checksum":173736544},{"workload":"churn.delete","description":"delete 80,000 hyperedges (20%) in transactions of 1,000","operations":80000,"millis":23612.875375,"checksum":80000,"gcCount":32,"gcMillis":693,"allocated":35567794840,"bytesWritten":1349057776,"logicalWritten":2970424},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":3465.6795,"checksum":20000,"gcCount":4,"gcMillis":162,"allocated":3987761888,"bytesWritten":297020440,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":17.613708,"checksum":665234,"gcCount":0,"gcMillis":0,"allocated":141760,"bytesRead":0,"logicalRead":5321872},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":1299803251,"millis":1000.0,"checksum":1299803251},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9982.790625,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":157883608,"latency":{"p50":3464.459,"p99":6182.167,"p99.9":9970.959,"max":9970.959},"checked":false,"bytesWritten":38352017,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9981.146792,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":139764344,"latency":{"p50":3379.084,"p99":6337.75,"p99.9":12463.875,"max":12463.875},"checked":false,"bytesWritten":3229400,"logicalWritten":16000,"compactionMillis":80,"reclaimed":-3302111},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":172512739,"millis":1000.0,"checksum":172512739},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":188.662458,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":175432480,"checked":false,"acknowledged":100000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s4-async/hstore-2.json b/benchmarks/report/results/s4-async/hstore-2.json new file mode 100644 index 0000000..3526df3 --- /dev/null +++ b/benchmarks/report/results/s4-async/hstore-2.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":4,"durability":"async","threads":8,"nodes":200000,"edges":400000,"incidences":1453427,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4128243712,"retainedHeap":456592896,"bytesWritten":2249237515,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":200000,"millis":3472.183917,"checksum":200000,"gcCount":5,"gcMillis":121,"allocated":5240988616,"bytesWritten":63063899,"logicalWritten":3688890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":400000,"millis":14646.238667,"checksum":1453427,"gcCount":41,"gcMillis":685,"allocated":41621087616,"bytesWritten":423052778,"logicalWritten":14827416},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":15.225708,"checksum":719693,"gcCount":0,"gcMillis":0,"allocated":144152,"bytesRead":0,"logicalRead":5757544},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":17.934416,"checksum":361142,"gcCount":0,"gcMillis":0,"allocated":16216,"bytesRead":0,"logicalRead":2889136},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":52.56125,"checksum":20109,"gcCount":0,"gcMillis":0,"allocated":82510944,"bytesRead":0,"logicalRead":160872},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":25.187958,"checksum":280131,"gcCount":0,"gcMillis":0,"allocated":9172032,"bytesRead":2037134,"logicalRead":2241048},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":53.877875,"checksum":5757544,"gcCount":1,"gcMillis":3,"allocated":111664280},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":42.401792,"checksum":719693,"gcCount":1,"gcMillis":10,"allocated":26908872,"latency":{"p50":0.292,"p99":0.584,"p99.9":0.958,"max":9545.917}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":295.91425,"checksum":20000,"gcCount":0,"gcMillis":0,"allocated":1047528656,"bytesWritten":10155587,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":298.2695,"checksum":5000,"gcCount":0,"gcMillis":0,"allocated":712618752,"latency":{"p50":40.708,"p99":204.75,"p99.9":280.542,"max":538.125},"bytesWritten":8872968,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":19847931,"millis":10000.111,"checksum":0,"gcCount":2,"gcMillis":62,"allocated":2269043088,"latency":{"p50":2.291,"p99":13.208,"p99.9":26.5,"max":42060.916},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.111,"checksum":0,"latency":{"p50":3068.041,"p99":9869.666,"p99.9":38905.375,"max":38905.375},"checked":false,"bytesWritten":55998670,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 200,000 members","operations":200000,"millis":377.108208,"checksum":200000,"gcCount":1,"gcMillis":9,"allocated":832478512,"bytesWritten":14573264,"logicalWritten":1600008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":80000000,"millis":21.458791,"checksum":80000000,"gcCount":0,"gcMillis":0,"allocated":177072,"bytesRead":0,"logicalRead":640000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":6.477208,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":16360,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":129.629167,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":66225992},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":49.846875,"checksum":819693,"gcCount":0,"gcMillis":0,"allocated":59532680,"bytesRead":11309826,"logicalRead":6557544},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":172907434,"millis":1000.0,"checksum":172907434},{"workload":"churn.delete","description":"delete 80,000 hyperedges (20%) in transactions of 1,000","operations":80000,"millis":18625.098375,"checksum":80000,"gcCount":34,"gcMillis":251,"allocated":36070217104,"bytesWritten":1334071335,"logicalWritten":2970424},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":3330.6515,"checksum":20000,"gcCount":4,"gcMillis":100,"allocated":4014354152,"bytesWritten":297847837,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":15.853,"checksum":665234,"gcCount":0,"gcMillis":0,"allocated":141736,"bytesRead":0,"logicalRead":5321872},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":1284740622,"millis":1000.0,"checksum":1284740622},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9983.586417,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":157723904,"latency":{"p50":5326.333,"p99":9800.792,"p99.9":10772.083,"max":10772.083},"checked":false,"bytesWritten":38358787,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9987.999208,"checksum":0,"gcCount":1,"gcMillis":15,"allocated":139723648,"latency":{"p50":5153.083,"p99":9946.166,"p99.9":21664.083,"max":21664.083},"checked":false,"bytesWritten":3241698,"logicalWritten":16000,"compactionMillis":86,"reclaimed":-3314463},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":172528768,"millis":1000.0,"checksum":172528768},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":218.654667,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":269927496,"checked":false,"acknowledged":100000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s4-async/hstore-3.json b/benchmarks/report/results/s4-async/hstore-3.json new file mode 100644 index 0000000..f78414d --- /dev/null +++ b/benchmarks/report/results/s4-async/hstore-3.json @@ -0,0 +1 @@ +{"store":"HStore","version":"0.1.0","cache":"node cache of 64 MiB and retains 64 generations of history","scale":4,"durability":"async","threads":8,"nodes":200000,"edges":400000,"incidences":1453427,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4131913728,"retainedHeap":429779056,"bytesWritten":2250060131,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":200000,"millis":5885.500417,"checksum":200000,"gcCount":5,"gcMillis":171,"allocated":5255854256,"bytesWritten":63571046,"logicalWritten":3688890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":400000,"millis":20013.312917,"checksum":1453427,"gcCount":42,"gcMillis":1001,"allocated":42609173200,"bytesWritten":409098707,"logicalWritten":14827416},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":15.696334,"checksum":719693,"gcCount":0,"gcMillis":0,"allocated":120176,"bytesRead":0,"logicalRead":5757544},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":16.858083,"checksum":361142,"gcCount":0,"gcMillis":0,"allocated":13216,"bytesRead":0,"logicalRead":2889136},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":55.878375,"checksum":20109,"gcCount":0,"gcMillis":0,"allocated":82510584,"bytesRead":0,"logicalRead":160872},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":32.124083,"checksum":280131,"gcCount":0,"gcMillis":0,"allocated":12634120,"bytesRead":2975456,"logicalRead":2241048},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":44.61675,"checksum":5757544,"gcCount":0,"gcMillis":0,"allocated":72663304},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":66.215084,"checksum":719693,"gcCount":0,"gcMillis":0,"allocated":29315376,"latency":{"p50":0.583,"p99":1.125,"p99.9":10.75,"max":61.834}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":911.148625,"checksum":20000,"gcCount":1,"gcMillis":37,"allocated":1043121608,"bytesWritten":10155587,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":596.46925,"checksum":5000,"gcCount":0,"gcMillis":0,"allocated":728025664,"latency":{"p50":103.75,"p99":285.208,"p99.9":696.417,"max":3354.125},"bytesWritten":8872968,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":21775522,"millis":10000.1335,"checksum":0,"gcCount":3,"gcMillis":79,"allocated":2870011880,"latency":{"p50":1.375,"p99":10.833,"p99.9":83.084,"max":50178.791},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.1335,"checksum":0,"latency":{"p50":3669.375,"p99":23526.459,"p99.9":58245.75,"max":58245.75},"checked":false,"bytesWritten":57545323,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 200,000 members","operations":200000,"millis":559.101417,"checksum":200000,"gcCount":1,"gcMillis":25,"allocated":832851552,"bytesWritten":14573475,"logicalWritten":1600008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":80000000,"millis":32.370834,"checksum":80000000,"gcCount":0,"gcMillis":0,"allocated":198560,"bytesRead":0,"logicalRead":640000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":14.675333,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":16432,"bytesRead":0,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":241.590875,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":64689896},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":101.567459,"checksum":819693,"gcCount":0,"gcMillis":0,"allocated":59549800,"bytesRead":11309990,"logicalRead":6557544},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":174461755,"millis":1000.0,"checksum":174461755},{"workload":"churn.delete","description":"delete 80,000 hyperedges (20%) in transactions of 1,000","operations":80000,"millis":35953.531375,"checksum":80000,"gcCount":30,"gcMillis":836,"allocated":33967982240,"bytesWritten":1348345415,"logicalWritten":2970424},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":5701.093167,"checksum":20000,"gcCount":4,"gcMillis":272,"allocated":3806376392,"bytesWritten":296308600,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":28.842916,"checksum":665234,"gcCount":0,"gcMillis":0,"allocated":139168,"bytesRead":0,"logicalRead":5321872},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":1299076699,"millis":1000.0,"checksum":1299076699},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9983.429416,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":153798120,"latency":{"p50":5132.666,"p99":10192.291,"p99.9":24199.0,"max":24199.0},"checked":false,"bytesWritten":38358918,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":500,"millis":9983.895166,"checksum":0,"gcCount":0,"gcMillis":0,"allocated":135812184,"latency":{"p50":4779.791,"p99":18456.708,"p99.9":39319.333,"max":39319.333},"checked":false,"bytesWritten":3229400,"logicalWritten":16000,"compactionMillis":79,"reclaimed":-3302111},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":172519023,"millis":1000.0,"checksum":172519023},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":257.284417,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":261124184,"checked":false,"acknowledged":100000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s4-async/hypergraphdb-1.json b/benchmarks/report/results/s4-async/hypergraphdb-1.json new file mode 100644 index 0000000..4fddad0 --- /dev/null +++ b/benchmarks/report/results/s4-async/hypergraphdb-1.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":4,"durability":"async","threads":8,"nodes":200000,"edges":400000,"incidences":1453427,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4221566976,"retainedHeap":316767568,"bytesWritten":106157943532,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":200000,"millis":6965.681916,"checksum":200000,"gcCount":5,"gcMillis":122,"allocated":5276596808,"bytesWritten":317722874,"logicalWritten":3688890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":400000,"millis":195087.434583,"checksum":1453427,"gcCount":276,"gcMillis":2088,"allocated":356263188104,"bytesWritten":11540492649,"logicalWritten":14827416},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":173.934375,"checksum":719693,"gcCount":0,"gcMillis":0,"allocated":10170160,"bytesRead":77434111,"logicalRead":5757544},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":57.747791,"checksum":361142,"gcCount":0,"gcMillis":0,"allocated":4958696,"bytesRead":36639461,"logicalRead":2889136},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":1103.276042,"checksum":20109,"gcCount":1,"gcMillis":17,"allocated":167650432,"bytesRead":483182793,"logicalRead":160872},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":139.707542,"checksum":280131,"gcCount":0,"gcMillis":0,"allocated":18445144,"bytesRead":61895311,"logicalRead":2241048},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":590.262958,"checksum":5757544,"gcCount":1,"gcMillis":13,"allocated":1272430704},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":285.169667,"checksum":719693,"gcCount":0,"gcMillis":0,"allocated":717408944,"latency":{"p50":1.334,"p99":21.583,"p99.9":35.041,"max":383.25}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":12745.708,"checksum":20000,"gcCount":20,"gcMillis":132,"allocated":25427847160,"bytesWritten":796687276,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":2805.004375,"checksum":5000,"gcCount":3,"gcMillis":16,"allocated":4197489992,"latency":{"p50":311.667,"p99":4169.959,"p99.9":8250.458,"max":18843.667},"bytesWritten":134046895,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":8429776,"millis":10035.263,"checksum":0,"gcCount":21,"gcMillis":194,"allocated":26267116576,"latency":{"p50":2.75,"p99":60.125,"p99.9":967.875,"max":42132.875},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":9980,"millis":10035.263,"checksum":0,"latency":{"p50":5622.125,"p99":74746.167,"p99.9":88035.125,"max":88035.125},"checked":false,"bytesWritten":498190094,"logicalWritten":319360},{"workload":"large.ingest","description":"create one hyperedge with 200,000 members","operations":200000,"millis":16561.517709,"checksum":200000,"gcCount":26,"gcMillis":179,"allocated":35079751176,"bytesWritten":1248364054,"logicalWritten":1600008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":80000000,"millis":529.778709,"checksum":80000000,"gcCount":0,"gcMillis":0,"allocated":456328,"bytesRead":111581793,"logicalRead":640000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":89.675375,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":9843168,"bytesRead":56214988,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":1157.123209,"checksum":1,"gcCount":2,"gcMillis":15,"allocated":2111114152},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":7644.840083,"checksum":819693,"gcCount":12,"gcMillis":117,"allocated":2443740896,"bytesRead":4243629361,"logicalRead":6557544},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":2482239070,"millis":1000.0,"checksum":2482239070},{"workload":"churn.delete","description":"delete 80,000 hyperedges (20%) in transactions of 1,000","operations":80000,"millis":54844.208167,"checksum":80000,"gcCount":86,"gcMillis":714,"allocated":113074291336,"bytesWritten":3356311725,"logicalWritten":2970424},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":7346.485958,"checksum":20000,"gcCount":11,"gcMillis":81,"allocated":14715324824,"bytesWritten":450490233,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":76.773875,"checksum":665234,"gcCount":0,"gcMillis":0,"allocated":9778504,"bytesRead":45166530,"logicalRead":5321872},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":1936326696,"millis":1000.0,"checksum":1936326696},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9984.558166,"checksum":0,"gcCount":16,"gcMillis":113,"allocated":20633429664,"latency":{"p50":3962.958,"p99":13119.208,"p99.9":25515.666,"max":25515.666},"checked":false,"bytesWritten":660196465,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":49106,"millis":982103.178917,"checksum":0,"gcCount":1575,"gcMillis":13087,"allocated":2081160235440,"latency":{"p50":4881.0,"p99":15368.375,"p99.9":83169.958,"max":283871.5},"checked":false,"bytesWritten":84952960325,"logicalWritten":1571392,"compactionMillis":981089,"reclaimed":849393714},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":1215413605,"millis":1000.0,"checksum":1215413605},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":454.755416,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":338847792,"checked":false,"acknowledged":100000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s4-async/hypergraphdb-2.json b/benchmarks/report/results/s4-async/hypergraphdb-2.json new file mode 100644 index 0000000..8a1b21e --- /dev/null +++ b/benchmarks/report/results/s4-async/hypergraphdb-2.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":4,"durability":"async","threads":8,"nodes":200000,"edges":400000,"incidences":1453427,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4171759616,"retainedHeap":313575576,"bytesWritten":101787400268,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":200000,"millis":5136.364208,"checksum":200000,"gcCount":5,"gcMillis":108,"allocated":5276666400,"bytesWritten":317087397,"logicalWritten":3688890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":400000,"millis":146224.391,"checksum":1453427,"gcCount":276,"gcMillis":1259,"allocated":356795714352,"bytesWritten":11506451261,"logicalWritten":14827416},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":77.491334,"checksum":719693,"gcCount":0,"gcMillis":0,"allocated":9830736,"bytesRead":46855525,"logicalRead":5757544},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":68.992125,"checksum":361142,"gcCount":0,"gcMillis":0,"allocated":4958696,"bytesRead":32332603,"logicalRead":2889136},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":371.469041,"checksum":20109,"gcCount":1,"gcMillis":23,"allocated":164469176,"bytesRead":208933349,"logicalRead":160872},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":103.382166,"checksum":280131,"gcCount":1,"gcMillis":4,"allocated":18443544,"bytesRead":61131629,"logicalRead":2241048},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":435.857917,"checksum":5757544,"gcCount":0,"gcMillis":0,"allocated":830831320},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":276.53875,"checksum":719693,"gcCount":1,"gcMillis":6,"allocated":736545848,"latency":{"p50":1.333,"p99":19.459,"p99.9":31.167,"max":6301.292}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":8765.17425,"checksum":20000,"gcCount":15,"gcMillis":91,"allocated":19275782704,"bytesWritten":603315874,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":1962.9915,"checksum":5000,"gcCount":3,"gcMillis":14,"allocated":4575113488,"latency":{"p50":272.625,"p99":1246.458,"p99.9":5384.333,"max":8377.375},"bytesWritten":146365552,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":10075117,"millis":10000.121292,"checksum":0,"gcCount":23,"gcMillis":176,"allocated":29638746696,"latency":{"p50":2.333,"p99":35.542,"p99.9":858.917,"max":34782.833},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.121292,"checksum":0,"latency":{"p50":6161.042,"p99":209658.958,"p99.9":237605.208,"max":237605.208},"checked":false,"bytesWritten":535203488,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 200,000 members","operations":200000,"millis":16221.591541,"checksum":200000,"gcCount":27,"gcMillis":176,"allocated":35662019424,"bytesWritten":1262840788,"logicalWritten":1600008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":80000000,"millis":176.634542,"checksum":80000000,"gcCount":0,"gcMillis":0,"allocated":462728,"bytesRead":102009152,"logicalRead":640000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":275.421709,"checksum":100000,"gcCount":1,"gcMillis":10,"allocated":10222312,"bytesRead":65287601,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":1028.163541,"checksum":1,"gcCount":1,"gcMillis":12,"allocated":1783529560},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":7511.055333,"checksum":819693,"gcCount":13,"gcMillis":106,"allocated":2491962672,"bytesRead":4431426387,"logicalRead":6557544},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":2406302427,"millis":1000.0,"checksum":2406302427},{"workload":"churn.delete","description":"delete 80,000 hyperedges (20%) in transactions of 1,000","operations":80000,"millis":56082.352625,"checksum":80000,"gcCount":85,"gcMillis":804,"allocated":111925493856,"bytesWritten":3339335231,"logicalWritten":2970424},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":10422.682667,"checksum":20000,"gcCount":12,"gcMillis":243,"allocated":15296077344,"bytesWritten":465636463,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":98.48175,"checksum":665234,"gcCount":0,"gcMillis":0,"allocated":9848632,"bytesRead":48705299,"logicalRead":5321872},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":2061649543,"millis":1000.0,"checksum":2061649543},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9983.557667,"checksum":0,"gcCount":15,"gcMillis":112,"allocated":19193614832,"latency":{"p50":3810.458,"p99":17737.375,"p99.9":41022.667,"max":41022.667},"checked":false,"bytesWritten":598897553,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":78533,"millis":1570647.540417,"checksum":0,"gcCount":1514,"gcMillis":61477,"allocated":2001405932808,"latency":{"p50":4079.0,"p99":118598.459,"p99.9":444591.292,"max":1643320.834},"checked":false,"bytesWritten":80773865951,"logicalWritten":2513056,"compactionMillis":1569623,"reclaimed":751137008},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":1234072654,"millis":1000.0,"checksum":1234072654},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":527.096375,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":340595816,"checked":false,"acknowledged":100000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/results/s4-async/hypergraphdb-3.json b/benchmarks/report/results/s4-async/hypergraphdb-3.json new file mode 100644 index 0000000..56748c3 --- /dev/null +++ b/benchmarks/report/results/s4-async/hypergraphdb-3.json @@ -0,0 +1 @@ +{"store":"HyperGraphDB","version":"1.4 @ 99485a1 (Berkeley DB JE 5.0.73)","cache":"Berkeley DB JE cache of 64 MiB plus the HyperGraphDB atom cache","scale":4,"durability":"async","threads":8,"nodes":200000,"edges":400000,"incidences":1453427,"java":"Java HotSpot(TM) 64-Bit Server VM 25.0.4+7-LTS-jvmci-b01","os":"Mac OS X 26.5.1 aarch64","processors":10,"maxHeap":4172808192,"retainedHeap":310427632,"bytesWritten":102654245945,"results":[{"workload":"ingest.nodes","description":"insert nodes in transactions of 1,000","operations":200000,"millis":7488.549875,"checksum":200000,"gcCount":5,"gcMillis":123,"allocated":5261005400,"bytesWritten":315998366,"logicalWritten":3688890},{"workload":"ingest.edges","description":"insert hyperedges (mean cardinality 3.6) in transactions of 1,000","operations":400000,"millis":231836.232667,"checksum":1453427,"gcCount":279,"gcMillis":3441,"allocated":360288635392,"bytesWritten":11762878841,"logicalWritten":14827416},{"workload":"read.incidence","description":"enumerate the incidence set of a node","operations":100000,"millis":182.057916,"checksum":719693,"gcCount":0,"gcMillis":0,"allocated":9938464,"bytesRead":56549561,"logicalRead":5757544},{"workload":"read.members","description":"enumerate the members of a hyperedge","operations":100000,"millis":54.522,"checksum":361142,"gcCount":0,"gcMillis":0,"allocated":4958696,"bytesRead":32897178,"logicalRead":2889136},{"workload":"read.comembership","description":"count hyperedges containing both atoms of a pair","operations":20000,"millis":1158.479166,"checksum":20109,"gcCount":2,"gcMillis":58,"allocated":167660432,"bytesRead":455648974,"logicalRead":160872},{"workload":"read.twohop","description":"count the distinct nodes that share a hyperedge with a node","operations":10000,"millis":213.6865,"checksum":280131,"gcCount":0,"gcMillis":0,"allocated":18449512,"bytesRead":67373708,"logicalRead":2241048},{"workload":"read.incidence.parallel","description":"incidence sets from 8 threads, 100,000 probes each","operations":800000,"millis":548.306958,"checksum":5757544,"gcCount":1,"gcMillis":17,"allocated":586078368},{"workload":"latency.read","description":"one incidence lookup per read transaction","operations":100000,"millis":487.627375,"checksum":719693,"gcCount":1,"gcMillis":15,"allocated":515349416,"latency":{"p50":1.5,"p99":26.708,"p99.9":603.583,"max":18205.375}},{"workload":"write.update","description":"replace a node value in transactions of 1,000","operations":20000,"millis":13943.861375,"checksum":20000,"gcCount":20,"gcMillis":174,"allocated":25900231680,"bytesWritten":820501441,"logicalWritten":640000},{"workload":"latency.commit","description":"one node update per write transaction","operations":5000,"millis":2211.539959,"checksum":5000,"gcCount":3,"gcMillis":24,"allocated":4168570376,"latency":{"p50":259.125,"p99":3178.041,"p99.9":10615.75,"max":14490.291},"bytesWritten":132749010,"logicalWritten":160000},{"workload":"mixed.read","description":"single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s","operations":8559360,"millis":10000.462666,"checksum":0,"gcCount":19,"gcMillis":174,"allocated":25290518992,"latency":{"p50":2.459,"p99":45.209,"p99.9":1073.666,"max":47706.375},"checked":false},{"workload":"mixed.write","description":"updates committed by the writer alongside the readers","operations":10000,"millis":10000.462666,"checksum":0,"latency":{"p50":6442.083,"p99":63126.916,"p99.9":99384.083,"max":99384.083},"checked":false,"bytesWritten":461204952,"logicalWritten":320000},{"workload":"large.ingest","description":"create one hyperedge with 200,000 members","operations":200000,"millis":16721.71525,"checksum":200000,"gcCount":27,"gcMillis":214,"allocated":35346139424,"bytesWritten":1254512032,"logicalWritten":1600008},{"workload":"large.scan","description":"enumerate all members of the large hyperedge (x400, after a warm-up)","operations":80000000,"millis":185.412666,"checksum":80000000,"gcCount":0,"gcMillis":0,"allocated":456328,"bytesRead":97425939,"logicalRead":640000000},{"workload":"large.probe","description":"test whether a node belongs to the large hyperedge","operations":100000,"millis":141.435584,"checksum":100000,"gcCount":0,"gcMillis":0,"allocated":10046384,"bytesRead":77361579,"logicalRead":800000},{"workload":"reopen","description":"close and reopen the database, including recovery","operations":1,"millis":1269.647333,"checksum":1,"gcCount":2,"gcMillis":13,"allocated":2744458344},{"workload":"read.incidence.cold","description":"incidence sets immediately after reopening","operations":100000,"millis":7983.663833,"checksum":819693,"gcCount":12,"gcMillis":138,"allocated":2445187336,"bytesRead":4315470754,"logicalRead":6557544},{"workload":"disk","description":"bytes on disk after a clean shutdown","operations":2319139933,"millis":1000.0,"checksum":2319139933},{"workload":"churn.delete","description":"delete 80,000 hyperedges (20%) in transactions of 1,000","operations":80000,"millis":61163.378917,"checksum":80000,"gcCount":89,"gcMillis":1060,"allocated":116239091360,"bytesWritten":3513481862,"logicalWritten":2970424},{"workload":"churn.remove","description":"remove one member from each of 20,000 other hyperedges, 1,000 per transaction","operations":20000,"millis":11924.960291,"checksum":20000,"gcCount":12,"gcMillis":345,"allocated":15298981224,"bytesWritten":472275606,"logicalWritten":320000},{"workload":"read.incidence.churned","description":"enumerate incidence sets after the deletes","operations":100000,"millis":188.185292,"checksum":665234,"gcCount":0,"gcMillis":0,"allocated":10012840,"bytesRead":88543854,"logicalRead":5321872},{"workload":"disk.churned","description":"bytes on disk after the deletes and a clean shutdown","operations":1970718945,"millis":1000.0,"checksum":1970718945},{"workload":"compaction.baseline","description":"single-update commits at 50/s with no compaction, for comparison","operations":500,"millis":9993.151167,"checksum":0,"gcCount":6,"gcMillis":419,"allocated":7169557776,"latency":{"p50":6690.125,"p99":126903.5,"p99.9":189999.542,"max":189999.542},"checked":false,"bytesWritten":234079729,"logicalWritten":16000},{"workload":"compaction","description":"single-update commits at 50/s while a full compaction runs","operations":54754,"millis":1095068.539791,"checksum":0,"gcCount":1519,"gcMillis":22624,"allocated":2007351654448,"latency":{"p50":5713.166,"p99":64288.041,"p99.9":181833.041,"max":393391.875},"checked":false,"bytesWritten":81434049393,"logicalWritten":1752128,"compactionMillis":1094052,"reclaimed":755082982},{"workload":"disk.compacted","description":"bytes on disk after a full compaction and a clean shutdown","operations":1173354262,"millis":1000.0,"checksum":1173354262},{"workload":"recover","description":"open the store after killing the ingest process with SIGKILL","operations":1,"millis":327.261334,"checksum":1,"gcCount":0,"gcMillis":0,"allocated":336987400,"checked":false,"acknowledged":100000,"lost":0}]} \ No newline at end of file diff --git a/benchmarks/report/sections/abstract.tex b/benchmarks/report/sections/abstract.tex new file mode 100644 index 0000000..39974e3 --- /dev/null +++ b/benchmarks/report/sections/abstract.tex @@ -0,0 +1 @@ +We compare HStore, a persistent hypergraph storage engine built on copy-on-write counted B+trees, with HyperGraphDB~1.4 on Berkeley DB Java Edition. Both systems run 26 storage-level workloads on the same skewed synthetic hypergraph, with up to 1.45 million incidences and equal 64\,MiB caches, in three configurations: two data sizes and two commit modes. Every read result is cross-checked between the systems. At scale 1 with asynchronous commits, HStore enumerates incidence sets \M{ratioAreadincidence} faster, counts shared hyperedges \M{ratioAreadcomembership} faster, applies batched updates \M{ratioAwriteupdate} faster, and needs \M{ratioAdisk} less disk. These leads come from storing incidence sets as small sorted trees, and from commits that log only the keys they changed while a background checkpoint writes pages. At scale 4, where both systems' files outgrow their caches, these leads hold or widen, and HStore enumerates hyperedge members \M{sumCreadmembers} faster, because its cached catalog pages decode only the records that are read. HyperGraphDB recovers from a crash faster at scale 1. With synchronous commits, both systems are bound by the cost of one forced write per commit. We also report latency percentiles, bytes written, heap use, space reclamation and crash recovery, the variation over runs, and a durability setting in HyperGraphDB that, left unchecked, makes its synchronous commits unsynced. diff --git a/benchmarks/report/sections/appendix.tex b/benchmarks/report/sections/appendix.tex new file mode 100644 index 0000000..e82e28e --- /dev/null +++ b/benchmarks/report/sections/appendix.tex @@ -0,0 +1,105 @@ +\section{Full results} +\label{app:full} + +Each cell is the median over runs. Throughput is in operations per second (k $=10^3$, M $=10^6$, G $=10^9$). Ratios above 1 favour HStore. ``Agree'' is \emph{yes} when both systems returned the same checksum in every run. + +\subsection{Scale 1, asynchronous commits} + +\begin{center}\footnotesize\input{gen/main-s1-async}\end{center} + +\begin{center}\footnotesize Latency (median of each percentile over runs)\\[2pt]\input{gen/latency-s1-async}\end{center} + +\begin{center}\footnotesize\input{gen/writes-s1-async}\end{center} + +\begin{center}\footnotesize\input{gen/memory-s1-async}\end{center} + +\subsection{Scale 1, synchronous commits} + +\begin{center}\footnotesize\input{gen/main-s1-sync}\end{center} + +\begin{center}\footnotesize Latency (median of each percentile over runs)\\[2pt]\input{gen/latency-s1-sync}\end{center} + +\begin{center}\footnotesize\input{gen/writes-s1-sync}\end{center} + +\begin{center}\footnotesize\input{gen/memory-s1-sync}\end{center} + +\subsection{Scale 4, asynchronous commits} + +\begin{center}\footnotesize\input{gen/main-s4-async}\end{center} + +\begin{center}\footnotesize Latency (median of each percentile over runs)\\[2pt]\input{gen/latency-s4-async}\end{center} + +\begin{center}\footnotesize\input{gen/writes-s4-async}\end{center} + +\begin{center}\footnotesize\input{gen/memory-s4-async}\end{center} + +\clearpage +\section{Run-to-run variability} +\label{app:variability} + +The tables below give the minimum and maximum over runs and the coefficient of variation (standard deviation over mean) of every throughput workload, followed by the value of every run. + +\subsection{Scale 1, asynchronous commits} +\begin{center}\footnotesize\input{gen/ranges-s1-async}\end{center} +{\footnotesize\input{gen/runs-s1-async}} + +\subsection{Scale 1, synchronous commits} +\begin{center}\footnotesize\input{gen/ranges-s1-sync}\end{center} +{\footnotesize\input{gen/runs-s1-sync}} + +\subsection{Scale 4, asynchronous commits} +\begin{center}\footnotesize\input{gen/ranges-s4-async}\end{center} +{\footnotesize\input{gen/runs-s4-async}} + +\clearpage +\section{Reproducing the results} +\label{app:repro} + +The harness is the \texttt{benchmarks} module of the HStore repository. HyperGraphDB and JE are built from source at the commits named in \cref{tab:config}. + +\begin{verbatim} +benchmarks/install-hypergraphdb.sh # builds HyperGraphDB 99485a1 and JE 5.0.73 +SCALE=1 DURABILITY=async CACHE_MB=64 RUNS=5 THREADS=8 benchmarks/run.sh +SCALE=1 DURABILITY=sync CACHE_MB=64 RUNS=5 THREADS=8 benchmarks/run.sh +SCALE=4 DURABILITY=async CACHE_MB=64 RUNS=3 THREADS=8 benchmarks/run.sh +\end{verbatim} + +\texttt{run.sh} builds the module, then runs each system \texttt{RUNS} times in alternation, one JVM per run, with these flags: + +\begin{verbatim} +java -Xms4g -Xmx4g -XX:+UseParallelGC \ + --add-opens java.base/java.lang=ALL-UNNAMED \ + --add-opens java.base/java.lang.ref=ALL-UNNAMED \ + --add-opens java.base/java.util=ALL-UNNAMED \ + io.hstore.bench.Comparison run --store \ + --scale --durability --threads 8 --history 64 --cache-mb 64 +\end{verbatim} + +Each run writes one JSON file with every measurement in this report. \texttt{Comparison report} turns a set of them into Markdown tables. The tables, figures and quoted numbers in this report were generated from the same files by \texttt{benchmarks/report/gen.py}, without manual transcription. The dataset statistics in \cref{tab:dataset} are printed by \texttt{io.hstore.bench.DatasetStats}, run with the same classpath. The raw files are in \texttt{benchmarks/report/results}, and the report is rebuilt with: + +\begin{verbatim} +cd benchmarks/report +python3 gen.py results gen +tectonic report.tex +\end{verbatim} + +\section{HStore settings} +\label{app:settings} + +\begin{center}\footnotesize +\begin{tabular}{@{}llr@{}} +\toprule +Setting & Meaning & Value \\ +\midrule +\texttt{page\_size} & tree node page size & 16\,KiB \\ +segment size & pages per segment file & 16,384 (256\,MiB) \\ +\texttt{node\_cache\_mb} & decoded node cache & 64\,MiB (set) \\ +\texttt{write\_buffer\_mb} & cap on unwritten tree nodes & 64\,MiB \\ +\texttt{checkpoint\_wal\_mb} & log or page bytes that trigger a checkpoint & 256\,MiB \\ +\texttt{compaction\_live\_ratio} & compact segments below this live fraction & 0.5 \\ +\texttt{history\_limit} & generations kept for time travel & 64 \\ +\texttt{wal\_mode} & log page references, not page images & references \\ +maintenance interval & background checkpoint and compaction pass & 500\,ms \\ +\bottomrule +\end{tabular} +\end{center} diff --git a/benchmarks/report/sections/conclusion.tex b/benchmarks/report/sections/conclusion.tex new file mode 100644 index 0000000..7773851 --- /dev/null +++ b/benchmarks/report/sections/conclusion.tex @@ -0,0 +1,5 @@ +\section{Conclusion} + +At the storage level, HStore is faster than HyperGraphDB on the operations that hypergraph workloads repeat most: finding the hyperedges that contain a node, intersecting those sets, and updating values. It writes and stores far fewer bytes doing so. Its design gives a clear reason for each of these results. Keeping cached pages compact matters as much as the layout on disk: decoding only the catalog records that are read is what lets HStore's catalog stay within a 64\,MiB cache at scale 4. HStore pays for its results with more retained heap, and at scale 1 it opens a crashed store more slowly than HyperGraphDB. Synchronous commits are bound by the device in both systems. Crash recovery lost no acknowledged commit in either system. + +The numbers come from one laptop under load, and \cref{sec:validity} lists what that limits. The harness, the adapters, the raw results and the script that produced every table in this report are published with HStore, so each result can be checked or repeated on other hardware. diff --git a/benchmarks/report/sections/discussion.tex b/benchmarks/report/sections/discussion.tex new file mode 100644 index 0000000..0de919e --- /dev/null +++ b/benchmarks/report/sections/discussion.tex @@ -0,0 +1,16 @@ +\section{Discussion} +\label{sec:discussion} + +\paragraph{Incidence and intersection.} An incidence lookup in HStore reads one entry of the reverse index. For most atoms the whole incidence set is stored inline in that entry, and for hubs it is the root of a small tree. The visitor API hands each hyperedge id to the caller without allocating, which is why \wl{read.incidence} allocates about one byte per lookup. HyperGraphDB opens a JE cursor on a duplicate set, decodes 16-byte handles, and builds a result object per lookup. With a 64\,MiB JE cache, part of each lookup also misses the cache and reads from the log files (\cref{app:full}). Co-membership adds a second effect. HStore intersects two sorted incidence trees and skips every subtree whose key range cannot match, so the cost follows the smaller set. HyperGraphDB plans \texttt{hg.and} as a general conjunctive query over two result sets. The two-hop workload combines both lookups, so it inherits both advantages. + +\paragraph{Member enumeration.} HyperGraphDB keeps a link's targets as a Java array in its atom cache, so enumerating the members of a cached link is an array loop with almost no allocation. HStore reads a hyperedge's members from its record in the catalog tree: from the leaf stored inline in the record for a small edge, or from the edge's own membership tree. What decides the result at scale 4 is how much heap a cached catalog leaf takes. A leaf holds about 80 records, each with its inline member list. Decoded in full, it takes roughly 11 times the bytes of its page image, and at scale 4 only about half of the catalog then fits in a 64\,MiB node cache. An earlier HStore version (commit \texttt{459b8be}) decoded leaves in full, and its member enumeration at scale 4 ran at 0.13 times HyperGraphDB's rate, with most of the time spent decoding records that the lookup did not need. The version measured here frames each record by its length and decodes a record only when it is read, so a cached leaf costs its page bytes plus the records actually used. The catalog leaves that the lookups reach then stay in the cache, and member lookups read nothing from the files. + +\paragraph{Tail latency.} HStore readers pin an immutable generation and never wait for a writer, a checkpoint or a compaction. Writers wait only for the commit lock, which a checkpoint takes just to swap stored references into the head. HyperGraphDB reads run inside JE transactions, which take record locks, and JE's cleaner and checkpointer compete for the same latches and log. In 16 of the 18 latency measurements (six workloads in three configurations), the gap between the two systems is wider at p99 and p99.9 than at the median. The two exceptions are the \wl{mixed} writer under synchronous commits and at scale 4. There the gap is widest at p99 and smaller at p99.9 than at the median, and since the writer makes 1,000 commits per run, its p99.9 is its slowest commit. + +\paragraph{Giant hyperedges.} HStore bulk-builds a large membership tree bottom-up in one pass, and a membership probe is one descent of that tree. HyperGraphDB stores the giant link as one record holding 50,000 handles (200,000 at scale 4). A membership probe goes through the node's incidence set instead. + +\paragraph{Footprint and reclamation.} Several layout choices keep HStore's files small: 8-byte ids stored as varint gaps instead of 16-byte handles, node images packed into 64-byte units, and small member trees and incidence sets stored inline in their parent record. HStore's log is also truncated at each checkpoint. A JE log keeps every superseded version until the cleaner copies the live entries out of a file and deletes it, and at scale 4 cleaning dominated HyperGraphDB's bytes written. HStore's weak point is the period right after a large delete. Pages replaced by the deletes stay on disk while any of the 64 retained generations can reach them, so the files grow first and shrink only after the history moves on, here at the restart that follows. An application that deletes heavily and needs its disk space back quickly would set a smaller \texttt{history\_limit}. + +\paragraph{Opening and recovery.} After a clean shutdown, HStore reads one catalog image and finds nothing in the log to replay. HyperGraphDB's open takes longer and allocates far more heap: \M{allocAhgdbreopen} per reopen at scale 1, against \M{allocAhstorereopen} for HStore. After a crash, HStore replays the logged key changes of every commit since its last checkpoint, and JE replays its log from its last checkpoint. HStore's replay cost grows with the number of commits since the last checkpoint, which the write buffer and the checkpoint thresholds bound. At scale 1 HyperGraphDB opens the crashed store faster, \M{gArecover} against \M{hArecover}. HStore's time there is close to its clean reopen of the full dataset (\M{hAreopen}), and it allocates \M{allocAhstorerecover} while opening, against \M{allocAhgdbrecover} for HyperGraphDB. At scale 4 the order reverses: \M{hCrecover} for HStore against \M{gCrecover}. + +\paragraph{What HStore spends.} HStore retains more heap after ingest than HyperGraphDB (\cref{tab:memory}). The retained heap includes nodes not yet written by a checkpoint, up to the 64\,MiB write buffer, and the node cache, whose 64\,MiB budget counts estimated rather than measured node sizes. At scale 1 it also allocates more per inserted node and hyperedge, opens a crashed store more slowly, and with asynchronous commits its median single-update commit is slightly slower than HyperGraphDB's. Its slowest single commit at scale 4 was slower than HyperGraphDB's, even though its p99 and p99.9 were lower. diff --git a/benchmarks/report/sections/findings.tex b/benchmarks/report/sections/findings.tex new file mode 100644 index 0000000..3e87a32 --- /dev/null +++ b/benchmarks/report/sections/findings.tex @@ -0,0 +1,8 @@ +\begin{itemize} +\item \textbf{Incidence-based reads} are faster in HStore in every configuration: \M{sumAreadincidence} to \M{sumCreadincidence} for incidence sets, \M{sumAreadcomembership} to \M{sumCreadcomembership} for co-membership counts, and \M{sumBreadincidencecold} to \M{sumCreadincidencecold} directly after a reopen. +\item \textbf{Writes} cost HStore far fewer bytes. Single-update commits write \M{bpoAhstorelatencycommit} against \M{bpoAhgdblatencycommit}, and batched updates run \M{sumAwriteupdate} to \M{sumCwriteupdate} faster. Bulk insertion of hyperedges is close at scale 1 (\M{sumBingestedges} to \M{sumAingestedges}) and \M{sumCingestedges} faster at scale 4. +\item \textbf{Member enumeration} is \M{sumCreadmembers} faster in HStore at scale 4, where HStore's catalog stays in its node cache because cached leaves decode only the records that are read. At scale 1 the ranges overlap. Two-hop neighbourhoods are \M{sumBreadtwohop} to \M{sumAreadtwohop} faster. +\item \textbf{Tail latency.} HStore's p99 and p99.9 latencies are lower than HyperGraphDB's in every latency workload and configuration, by 1.1 to 35 times. +\item \textbf{Disk.} HStore's files are \M{sumBdisk} to \M{sumCdisk} smaller after loading. After deletes they first grow while retained history still references the replaced pages. +\item \textbf{Recovery.} Neither system lost an acknowledged commit after SIGKILL. HyperGraphDB opens a crashed store faster at scale 1, and HStore at scale 4. +\end{itemize} diff --git a/benchmarks/report/sections/introduction.tex b/benchmarks/report/sections/introduction.tex new file mode 100644 index 0000000..718ed04 --- /dev/null +++ b/benchmarks/report/sections/introduction.tex @@ -0,0 +1,12 @@ +\section{Introduction} + +Many datasets record facts that involve more than two things at once. An insurance claim joins a patient, a provider, a drug and a pharmacy. A paper joins its authors, and a meeting joins its participants. A hypergraph stores each such fact as one hyperedge over all of its participants. A property graph has to break the same fact into a new node and one edge per participant, and a relational schema into a join table. Queries then ask hypergraph questions: which facts involve this entity, which facts involve both of these entities, and what lies two hops away. + +HyperGraphDB~\cite{hgdb} is the established open-source hypergraph database for the JVM. It stores atoms and links in Berkeley DB Java Edition (JE), a general-purpose log-structured key-value store. HStore is a newer engine built for this data model. Its incidence sets and hyperedge memberships are counted B+trees, and its storage, transaction and recovery design is described in \cref{sec:systems}. + +This report compares the two systems at the storage level, on the same machine and data, with the same cache budget. We measure what an embedded application sees: throughput of loads, updates, deletes and reads; per-operation latency, including tail latency while another thread writes; behaviour of giant hyperedges; disk footprint before and after deletes and space reclamation; time to recover after a crash; and the bytes written, heap allocated and GC time behind each result. Every read result is cross-checked between the two systems. + +\Cref{sec:method} describes the dataset, workloads and measurement method. \Cref{sec:results} presents the results, and \cref{sec:discussion} explains them in terms of each system's design. \Cref{sec:validity} lists the limits of this study. Full tables, per-run values and the steps to reproduce every number are in the appendices. + +\paragraph{Summary of findings.} +\input{sections/findings} diff --git a/benchmarks/report/sections/method.tex b/benchmarks/report/sections/method.tex new file mode 100644 index 0000000..8a08b62 --- /dev/null +++ b/benchmarks/report/sections/method.tex @@ -0,0 +1,100 @@ +\section{Benchmark design} +\label{sec:method} + +\subsection{Dataset} + +The dataset is generated from a fixed seed (20260904), so both systems load exactly the same graph. At scale $s$ it has $50{,}000\,s$ nodes and $100{,}000\,s$ hyperedges. The cardinality of a hyperedge follows a geometric tail: it starts at 2 and each further member is added with probability 0.62, up to 32, for a mean of 3.6. Members are drawn as $\lfloor n\,u^{2.2}\rfloor$ for uniform $u\in[0,1)$ over $n$ nodes. Low-numbered nodes therefore become hubs with large incidence sets, as in co-authorship, claims or citation data. Hyperedges never repeat a member. + +\begin{table}[t] +\centering\footnotesize +\caption{Dataset statistics, computed from the generator. An incidence set is the set of hyperedges that contain a node.} +\label{tab:dataset} +\begin{tabular}{@{}lrr@{}} +\toprule +& Scale 1 & Scale 4 \\ +\midrule +Nodes & 50{,}000 & 200{,}000 \\ +Hyperedges & 100{,}000 & 400{,}000 \\ +Incidences & 362{,}757 & 1{,}453{,}427 \\ +\midrule +Hyperedge cardinality, mean / p99 / max & 3.63 / 11 / 30 & 3.63 / 11 / 30 \\ +Incidence set size, mean & 7.26 & 7.27 \\ +Incidence set size, p50 / p90 / p99 & 5 / 12 / 41 & 5 / 13 / 42 \\ +Incidence set size, p99.9 / max & 145 / 2{,}645 & 142 / 5{,}714 \\ +Nodes in no hyperedge & 575 & 2{,}319 \\ +Incidence sets above 48 entries & 370 & 1{,}521 \\ +\bottomrule +\end{tabular} +\end{table} + +\Cref{tab:dataset} gives the sizes and the skew. The largest incidence set at scale 1 holds 2,645 hyperedges, while half of all nodes are in 5 or fewer. Only 370 incidence sets exceed the 48 entries that HStore stores inline. Probe sets drawn from the same seed hold 100,000 random nodes and hyperedges, 20,000 node pairs taken from a common hyperedge (so that co-membership counts are non-zero), and 20,000 node updates. A separate churn set, drawn after everything else, selects 20\% of the hyperedges for deletion and 20,000 other hyperedges, each with at least three members, that lose one member. + +\subsection{Workloads} + +\Cref{tab:workloads} defines the workloads. They run in the order listed, in one process per system and run, against one database directory. The table also names the API call each system uses. Each adapter uses the plain storage calls of its system and its own identifier type: \texttt{long} ids for HStore and persistent handles for HyperGraphDB. Higher-level traversal frameworks, such as HyperGraphDB's breadth-first traversal, are not used, so the numbers reflect the storage layer. + +\begin{table*}[t] +\centering\footnotesize +\caption{Workloads, in execution order. ``txn'' is a transaction. All read workloads except \wl{read.incidence.cold} run after a warm-up (\cref{sec:measurement}).} +\label{tab:workloads} +\begin{tabularx}{\textwidth}{@{}lXll@{}} +\toprule +Workload & Operation & \hstore{} call & \hgdb{} call \\ +\midrule +\wl{ingest.nodes} & insert all nodes, 1,000 per txn & \texttt{Writer.node} & \texttt{graph.add(String)} \\ +\wl{ingest.edges} & insert all hyperedges, 1,000 per txn & \texttt{Writer.edge}, \texttt{Writer.load} & \texttt{graph.add(HGPlainLink)} \\ +\wl{read.incidence} & enumerate a node's incidence set; 100,000 probes, 1,000 per read txn & \texttt{forEachIncidentEdge} & \texttt{getIncidenceSet} \\ +\wl{read.members} & enumerate a hyperedge's members; 100,000 probes & \texttt{forEachMemberId} & \texttt{getTargetAt} \\ +\wl{read.comembership} & count hyperedges containing both nodes of a pair; 20,000 pairs & leapfrog intersection & \texttt{hg.and(hg.incident, hg.incident)} \\ +\wl{read.twohop} & count distinct nodes sharing a hyperedge with a node; 10,000 probes & incidence, then members & incidence, then targets \\ +\wl{read.incidence.parallel} & \wl{read.incidence} from 8 threads, 100,000 probes each & as above & as above \\ +\wl{latency.read} & one incidence lookup per read txn, each timed; 100,000 txns & as above & as above \\ +\wl{write.update} & replace a node's value; 20,000 updates, 1,000 per txn & \texttt{Writer.set} & \texttt{graph.replace} \\ +\wl{latency.commit} & one update per write txn, each timed; 5,000 txns & \texttt{Writer.set} & \texttt{graph.replace} \\ +\wl{mixed.read} & single incidence lookups from 7 threads for 10\,s, beside \wl{mixed.write} & as \wl{latency.read} & as \wl{latency.read} \\ +\wl{mixed.write} & one thread committing 10 updates per txn on a fixed schedule of 1,000 updates/s & \texttt{Writer.set} & \texttt{graph.replace} \\ +\wl{large.ingest} & create one hyperedge containing every node & \texttt{Writer.load} & one \texttt{HGPlainLink} \\ +\wl{large.scan} & enumerate the large hyperedge 400 times & \texttt{forEachMemberId} & \texttt{getTargetAt} \\ +\wl{large.probe} & test whether a node belongs to the large hyperedge; 100,000 probes & membership-tree lookup & \texttt{getIncidenceSet(n).contains(e)} \\ +\wl{reopen} & close cleanly and open again & \texttt{HypergraphDatabase.open} & \texttt{HGEnvironment.get} \\ +\wl{read.incidence.cold} & \wl{read.incidence} immediately after the reopen, no warm-up & & \\ +\wl{disk} & bytes on disk after a clean shutdown & & \\ +\wl{churn.delete} & delete 20\% of the hyperedges, 1,000 per txn & \texttt{Writer.delete} & \texttt{graph.remove} \\ +\wl{churn.remove} & remove one member from each of 20,000 other hyperedges, 1,000 per txn & \texttt{Writer.remove} & \texttt{graph.replace} with a new link \\ +\wl{read.incidence.churned} & \wl{read.incidence} after the deletes & & \\ +\wl{disk.churned} & bytes on disk after the deletes and a clean shutdown & & \\ +\wl{compaction.baseline} & single-update commits at 50/s for 10\,s, each timed & \texttt{Writer.set} & \texttt{graph.replace} \\ +\wl{compaction} & the same commits while a full space reclamation runs, starting 1\,s in & \texttt{StorageEngine.compact} & \texttt{cleanLog} until idle, then checkpoint \\ +\wl{disk.compacted} & bytes on disk after the reclamation and a clean shutdown & & \\ +\wl{recover} & open a store whose ingest process was killed with SIGKILL & \texttt{HypergraphDatabase.open} & \texttt{HGEnvironment.get} \\ +\bottomrule +\end{tabularx} +\end{table*} + +HyperGraphDB links are immutable, so \wl{churn.remove} replaces each link with a new one that lacks the removed member. \wl{read.twohop} runs before \wl{large.ingest}, because once a hyperedge containing every node exists, every node is two hops from all others. \wl{recover} runs in a child JVM with the same flags. The child ingests nodes in transactions of 1,000 into a fresh directory and prints the count after each acknowledged commit. The parent kills it with SIGKILL once half the nodes are acknowledged, then times opening the directory and counts the surviving nodes. + +\subsection{Measurement} +\label{sec:measurement} + +\paragraph{Throughput.} Each workload is timed with \texttt{System.nanoTime()} around its whole operation set. Throughput is operations divided by elapsed time. Each write workload ends with a flush outside the timed section: an HStore checkpoint, or \texttt{Environment.sync()} for JE. Bytes still in buffers are therefore counted against the workload that produced them. + +\paragraph{Warm-up.} Before every timed read workload except \wl{read.incidence.cold}, the same workload runs untimed for at least 2\,s. Both systems get the same warm-up. The numbers describe a process whose JIT has compiled the read path. A short warm-up mostly measures how fast each system's code gets compiled: in our trials, a warm-up over 10\% of the probes left \wl{read.members} about three times below its steady rate. + +\paragraph{Latency.} \wl{latency.read} and \wl{latency.commit} time every transaction separately and report p50, p99, p99.9 and the maximum. In \wl{mixed} and \wl{compaction}, the writer runs on a fixed schedule (open loop), and each commit's latency is measured from the time it was \emph{scheduled} to start. A stall therefore shows up in the latency of every commit queued behind it, instead of silently slowing the writer down. The readers in \wl{mixed.read} run in a closed loop and keep a uniform sample of 200,000 latencies each, plus the exact maximum. + +\paragraph{Bytes written and read.} Both systems count their own I/O. For HStore this is the log bytes plus the page bytes written, including pages moved by compaction. For HyperGraphDB it is JE's sequential plus random write bytes, including the cleaner. Reads count the bytes each system reads from its files: HStore's segment reads, and JE's sequential plus random read bytes. A read served from the system's own cache counts as zero. A read that reaches a file counts even when the operating system's page cache serves it. Write amplification divides bytes written by an estimate of the logical bytes: 8 bytes per id plus the value, 24 bytes per updated value, $(c+1)\times 8$ bytes for a new or deleted hyperedge of cardinality $c$, and 16 bytes per removed member. + +\paragraph{Memory.} For every workload the harness records the bytes allocated, from \texttt{ThreadMXBean} (the whole process for write workloads, the reading thread alone for single-threaded reads), and the stop-the-world GC time from \texttt{GarbageCollectorMXBean}. After ingest it forces a full GC and records the heap in use, minus a baseline taken before the store opened. This is the \emph{retained heap}. + +\paragraph{Correctness cross-check.} Every read and delete workload returns a checksum: the number of ids visited, intersected or found. Both systems must return the same checksum in every run, or the result is flagged. All checksums agreed in every run reported here. Workloads whose result depends on timing (\wl{mixed}, \wl{compaction}, \wl{recover}) are not checked. + +\subsection{Environment and statistics} +\label{sec:environment} + +All runs used one machine: an Apple M5 with 4 performance and 6 efficiency cores, 24\,GiB of memory, and a 1\,TB Apple SSD formatted with APFS, running macOS~26.5.1. The JVM was Oracle GraalVM for JDK~25.0.4 with the HotSpot JIT, a fixed 4\,GiB heap (\texttt{-Xms4g -Xmx4g}) and the Parallel collector. Each system ran in its own JVM, and runs alternated between the two systems so that slow periods affect both. We ran three configurations: +\begin{itemize} +\item \textbf{S1 async}: scale 1, asynchronous commits, \M{runsA} runs per system; +\item \textbf{S1 sync}: scale 1, synchronous commits, \M{runsB} runs per system; +\item \textbf{S4 async}: scale 4, asynchronous commits, \M{runsC} runs per system. With 64\,MiB caches, the files of both systems are larger than their caches. +\end{itemize} +Each table reports the median over runs. Ratios are taken between medians and oriented so that values above 1 favour HStore: throughput divides HStore by HyperGraphDB, while times, latencies and sizes divide HyperGraphDB by HStore. Where the ranges of the two systems' runs overlap, the ratio is marked with $\dagger$, and the difference should not be read as significant. \Cref{app:variability} gives every run and the coefficient of variation of each workload. diff --git a/benchmarks/report/sections/results-configs.tex b/benchmarks/report/sections/results-configs.tex new file mode 100644 index 0000000..77d4dd5 --- /dev/null +++ b/benchmarks/report/sections/results-configs.tex @@ -0,0 +1,12 @@ +\subsection{Larger data: scale 4} +\label{sec:scale4} + +At scale 4 the dataset has \M{incidencesC} incidences. After loading, HStore's files take \M{hCdisk} and HyperGraphDB's \M{gCdisk}, so both are larger than their 64\,MiB caches, HyperGraphDB's by a much wider margin. HStore's leads hold or grow. Hyperedge insertion becomes \M{sumCingestedges} faster (\M{hCingestedges} against \M{gCingestedges} per second), batched updates \M{sumCwriteupdate} and single-update commits \M{sumClatencycommit}. Co-membership counts are \M{sumCreadcomembership} faster, incidence lookups \M{sumCreadincidence}, and cold lookups after a reopen \M{sumCreadincidencecold}. + +Member enumeration is \M{sumCreadmembers} faster: \M{hCreadmembers} hyperedges per second against HyperGraphDB's \M{gCreadmembers}. Two-hop neighbourhoods, which enumerate the members of every incident hyperedge, are \M{sumCreadtwohop} faster. During \wl{read.members}, HStore read nothing from its files and allocated \M{allocChstorereadmembers} per lookup, so every catalog leaf the probes reached was already in its node cache. A cached catalog leaf holds its page bytes and only the records that have been read (\cref{sec:systems}), which keeps the catalog within the 64\,MiB budget. HyperGraphDB serves the same lookups from its atom cache, which holds each link's targets as a Java array and is not limited by the 64\,MiB budget. It allocates \M{allocChgdbreadmembers} per member lookup. A two-hop query allocates \M{allocChstorereadtwohop} in HStore and \M{allocChgdbreadtwohop} in HyperGraphDB. + +After the deletes, HStore's files grow to \M{hCdiskchurned}. The deletes rewrite a large part of the incidence index, and the replaced pages remain reachable from the 64 retained generations until the next restart releases that history. The benchmark restarts the store after the deletes, and during the 10\,s \wl{compaction.baseline} window that follows, HStore's background maintenance released and compacted that space. When the measured compaction window began, the explicit compaction took \M{compmsChstore} and found almost nothing left to reclaim, and the final size was \M{hCdiskcompacted}. JE's cleaner ran for \M{compmsChgdb} in the same window and brought HyperGraphDB from \M{gCdiskchurned} to \M{gCdiskcompacted}. + +A clean reopen is \M{sumCreopen} faster in HStore, and opening after a crash takes \M{hCrecover} against \M{gCrecover}. Over the whole run, HStore wrote \M{totalwrittenChstore} and HyperGraphDB \M{totalwrittenChgdb}, most of it during cleaning. HStore's single-update commits are faster at every percentile: \M{latChstorelatencycommitptwo} against \M{latChgdblatencycommitptwo} at p99, and \M{latChstorelatencycommitpthree} against \M{latChgdblatencycommitpthree} at p99.9. The median of the per-run maxima is also lower in HStore, \M{latChstorelatencycommitmax} against \M{latChgdblatencycommitmax}, but the slowest single commit across all runs was HStore's: \M{latChstorelatencycommitworst}, against \M{latChgdblatencycommitworst} for HyperGraphDB. + +\input{sections/results-sync} diff --git a/benchmarks/report/sections/results-sync.tex b/benchmarks/report/sections/results-sync.tex new file mode 100644 index 0000000..fe23efd --- /dev/null +++ b/benchmarks/report/sections/results-sync.tex @@ -0,0 +1,10 @@ +\subsection{Synchronous commits} +\label{sec:sync} + +With synchronous commits, every acknowledged commit has been forced to stable storage. On this machine a forced write costs about 4\,ms, and it dominates single-update transactions in both systems. HStore commits \M{hBlatencycommit} of them per second and HyperGraphDB \M{gBlatencycommit} (\M{sumBlatencycommit}). The medians are \M{latBhstorelatencycommitmed} and \M{latBhgdblatencycommitmed}, one forced write each, and HStore's lead comes from the tail: \M{latBhstorelatencycommitpthree} against \M{latBhgdblatencycommitpthree} at p99.9. + +Batched work spreads the forced write over 1,000 operations. HStore inserts nodes \M{sumBingestnodes} as fast as HyperGraphDB and hyperedges \M{sumBingestedges} as fast (ranges overlapping), and updates values \M{sumBwriteupdate} as fast. The open-loop writer of \wl{mixed} commits 10 updates every 10\,ms. Both systems held that schedule in every run. Measured from their scheduled start, HStore's commits took \M{latBhstoremixedwritemed} at the median and \M{latBhstoremixedwriteptwo} at p99, and HyperGraphDB's \M{latBhgdbmixedwritemed} and \M{latBhgdbmixedwriteptwo}. + +Under synchronous commits, JE reports \M{bpoBhgdblatencycommit} written per single-update commit, against \M{bpoAhgdblatencycommit} with asynchronous commits. The extra bytes come with the log flush that the adapter adds (\cref{sec:durability}). We have not established what JE writes in that flush, so the bytes-written figures for synchronous single commits in \cref{app:full} should not be read as a comparison. + +Reads do not commit, and HStore leads every read workload in this configuration as well at the median, with overlapping ranges for member enumeration and giant-edge scans (\cref{tab:summary}). Recovery after SIGKILL takes \M{hBrecover} in HStore and \M{gBrecover} in HyperGraphDB at the median, and the ranges do not overlap. As in every other configuration, no run of either system lost an acknowledged commit. diff --git a/benchmarks/report/sections/results-text.tex b/benchmarks/report/sections/results-text.tex new file mode 100644 index 0000000..cdb8463 --- /dev/null +++ b/benchmarks/report/sections/results-text.tex @@ -0,0 +1,76 @@ +\subsection{Loading, updating and deleting} + +At scale 1 with asynchronous commits, HStore inserts nodes \M{ratioAingestnodes} and hyperedges \M{ratioAingestedges} as fast as HyperGraphDB. Updates differ more. Replacing node values 1,000 at a time runs \M{ratioAwriteupdate} faster in HStore (\M{hAwriteupdate} against \M{gAwriteupdate} updates per second). Deleting hyperedges is \M{ratioAchurndelete} faster and removing single members \M{ratioAchurnremove} faster. Building one hyperedge over all 50,000 nodes takes HStore a single bulk load, \M{ratioAlargeingest} faster than HyperGraphDB. + +Single-update transactions are the one write workload where HStore does not lead: \M{hAlatencycommit} against \M{gAlatencycommit} commits per second (\M{ratioAlatencycommit}, ranges overlapping). With asynchronous commits, neither system waits for the disk, and the cost of one commit is dominated by work in memory. \Cref{sec:latency} shows that the two medians are close and that the difference lies in the tail. + +\subsection{Reads} + +HStore's largest margins are on reads that go through incidence sets. It enumerates a node's incidence set \M{ratioAreadincidence} faster than HyperGraphDB, counts the hyperedges shared by two nodes \M{ratioAreadcomembership} faster, and computes two-hop neighbourhoods \M{ratioAreadtwohop} faster. With 8 threads, incidence lookups reach \M{hAreadincidenceparallel} per second in HStore against \M{gAreadincidenceparallel} (\M{ratioAreadincidenceparallel}). When every lookup is its own read transaction (\wl{latency.read}), the ratio is \M{ratioAlatencyread}. Enumerating the members of a hyperedge, the read where HyperGraphDB's atom cache helps most, is \M{ratioAreadmembers} faster in HStore at the median, with overlapping ranges: one of the five HStore runs was slower than HyperGraphDB's median. + +Two read workloads run under special conditions. Directly after a reopen, before any warm-up, HStore serves incidence lookups at \M{hAreadincidencecold} per second against \M{gAreadincidencecold} (\M{ratioAreadincidencecold}). HStore read \M{coldreadAhstore} per lookup from its files in that phase, against \M{coldreadAhgdb} for JE. After the deletes, incidence lookups are \M{ratioAreadincidencechurned} faster. + +\subsection{Giant hyperedges} + +On the hyperedge that contains all 50,000 nodes, HStore scans members \M{ratioAlargescan} faster (\M{hAlargescan} members per second against \M{gAlargescan}) and tests membership \M{ratioAlargeprobe} faster. A membership test in HStore is one descent of the edge's membership tree. + +\subsection{Latency} +\label{sec:latency} + +\begin{figure*}[t] +\centering +\input{gen/fig-latency} +\caption{Latency percentiles, median of each percentile over runs (log scale). From left: single-update commits with asynchronous and with synchronous commits, single-lookup read transactions, and commits of the open-loop writer while 7 threads read.} +\label{fig:latency} +\end{figure*} + +\begin{table*}[t] +\centering\footnotesize +\caption{S1 async: latency per operation, median of each percentile over runs.} +\label{tab:latency} +\input{gen/latency-s1-async} +\end{table*} + +\Cref{fig:latency} and \cref{tab:latency} give the latency distributions. For single-update commits with asynchronous commits, the medians are close, with HyperGraphDB slightly ahead: \M{latAhstorelatencycommitmed} for HStore and \M{latAhgdblatencycommitmed} for HyperGraphDB. The tails favour HStore. At p99, HStore needs \M{latAhstorelatencycommitptwo} and HyperGraphDB \M{latAhgdblatencycommitptwo}. At p99.9 the figures are \M{latAhstorelatencycommitpthree} and \M{latAhgdblatencycommitpthree}. Read transactions take \M{latAhstorelatencyreadmed} at the median in HStore and \M{latAhgdblatencyreadmed} in HyperGraphDB, and \M{latAhstorelatencyreadpthree} against \M{latAhgdblatencyreadpthree} at p99.9. + +\paragraph{Reads beside a writer.} In \wl{mixed}, the writer's median rate was 1,000 updates per second in both systems; its slowest run reached \M{mixminAhstore} updates per second in HStore and \M{mixminAhgdb} in HyperGraphDB. Meanwhile the 7 readers completed \M{hAmixedread} lookups per second in HStore and \M{gAmixedread} in HyperGraphDB (\M{ratioAmixedread}). Reader latency at p99.9 was \M{latAhstoremixedreadpthree} against \M{latAhgdbmixedreadpthree}. The writer's commits, measured from their scheduled start, took \M{latAhstoremixedwriteptwo} against \M{latAhgdbmixedwriteptwo} at p99. + +\paragraph{Commits during space reclamation.} In the \wl{compaction} window, the median commit latency was about the same with and without a reclamation running, for both systems. At p99, HStore's commits took \M{latAhstorecompactionptwo} during compaction against \M{latAhstorecompactionbaselineptwo} without; HyperGraphDB's took \M{latAhgdbcompactionptwo} during cleaning against \M{latAhgdbcompactionbaselineptwo} without. + +\subsection{Disk footprint, deletes and reclamation} + +\begin{figure}[t] +\centering +\input{gen/fig-disk} +\caption{Bytes on disk after load, after deleting 20\% of the hyperedges, and after a full space reclamation, median over runs.} +\label{fig:disk} +\end{figure} + +After loading at scale 1, HStore's files take \M{hAdisk} and HyperGraphDB's \M{gAdisk} (\cref{fig:disk}). After the deletes the figures are \M{hAdiskchurned} and \M{gAdiskchurned}. HStore's files grow at this point. The deletes write new pages, and the old ones stay on disk while any of the 64 retained generations can reach them, until a later checkpoint releases them and compaction removes them. After a full reclamation and a clean shutdown, HStore is back to \M{hAdiskcompacted}, and HyperGraphDB is at \M{gAdiskcompacted}. + +\subsection{Reopen and recovery} + +A clean close and reopen takes \M{hAreopen} in HStore and \M{gAreopen} in HyperGraphDB (\M{ratioAreopen}). After the ingest process is killed with SIGKILL, opening the store takes \M{hArecover} and \M{gArecover} respectively, and the ranges do not overlap. HyperGraphDB opens a crashed store faster at scale 1. In every run of both systems, every node acknowledged before the kill was present after recovery. + +\subsection{Resources} +\label{sec:resources} + +\begin{figure}[t] +\centering +\input{gen/fig-writes} +\caption{Bytes written per operation, S1 async, median over runs (log scale). Writes include each system's log, data files, checkpoints and cleaning.} +\label{fig:writes} +\end{figure} + +\begin{table*}[t] +\centering\footnotesize +\caption{Resources per configuration, median over runs. HS: HStore, HG: HyperGraphDB. Space reclaimed is the drop in disk size across the compaction window; it is negative when the writer added more than the reclamation freed.} +\label{tab:memory} +\input{gen/resources} +\end{table*} + +HStore writes fewer bytes per operation than HyperGraphDB in every write workload of every configuration (\cref{fig:writes} and \cref{app:full}). A single-update commit writes \M{bpoAhstorelatencycommit} in HStore and \M{bpoAhgdblatencycommit} in HyperGraphDB. A batched update writes \M{bpoAhstorewriteupdate} against \M{bpoAhgdbwriteupdate}. Over a whole run at scale 1, HStore wrote \M{totalwrittenAhstore} and HyperGraphDB \M{totalwrittenAhgdb}. + +\Cref{tab:memory} summarises memory and reclamation. HStore retained \M{retainedAhstore} of heap after ingest at scale 1, against \M{retainedAhgdb} for HyperGraphDB. Per-workload allocation and GC time are in \cref{app:full}. HStore allocates less per operation than HyperGraphDB in every read workload of every configuration. At scale 1 it allocates more per inserted node and hyperedge, and more while recovering after a crash. At scale 4 it allocates less in those workloads as well. + +\input{sections/results-configs} diff --git a/benchmarks/report/sections/results.tex b/benchmarks/report/sections/results.tex new file mode 100644 index 0000000..1f9064a --- /dev/null +++ b/benchmarks/report/sections/results.tex @@ -0,0 +1,27 @@ +\section{Results} +\label{sec:results} + +\begin{figure*}[t] +\centering +\input{gen/fig-ratios} +\caption{Ratio of HStore to HyperGraphDB for every workload and configuration (log scale). Bars above the dashed line favour HStore. Throughput workloads divide HStore by HyperGraphDB; \wl{reopen}, \wl{recover} and the disk sizes divide HyperGraphDB by HStore.} +\label{fig:ratios} +\end{figure*} + +\Cref{fig:ratios} shows the ratio for every workload in all three configurations. HStore leads in all but three of them: single-update commits in S1 async, where the ranges overlap, and opening the store after a crash at scale 1, in both commit modes. \Cref{tab:summary} gives the same ratios with the run-range test from \cref{sec:environment}. \Cref{tab:main} gives the absolute numbers for the S1 async configuration. The other configurations are tabulated in \cref{app:full}. + +\begin{table}[t] +\centering\footnotesize +\caption{Ratio of medians, HStore over HyperGraphDB, oriented so that values above 1 favour HStore (bold). $^\dagger$: the run ranges of the two systems overlap.} +\label{tab:summary} +\input{gen/summary} +\end{table} + +\begin{table*}[t] +\centering\footnotesize +\caption{S1 async: median throughput (operations per second), times and sizes over \M{runsA} runs per system.} +\label{tab:main} +\input{gen/main-s1-async} +\end{table*} + +\input{sections/results-text} diff --git a/benchmarks/report/sections/systems.tex b/benchmarks/report/sections/systems.tex new file mode 100644 index 0000000..2a923e1 --- /dev/null +++ b/benchmarks/report/sections/systems.tex @@ -0,0 +1,92 @@ +\section{Systems under test} +\label{sec:systems} + +Both systems are embedded Java libraries. Each runs in its own JVM process, and the benchmark harness calls its API directly. Neither is accessed over a network. + +\subsection{HStore} + +\paragraph{Data model.} Every stored object is an \emph{atom} with a 64-bit identifier. An atom is a node or a hyperedge. A hyperedge has any number of members, and a member can itself be a hyperedge. Hyperedges are either \emph{set} edges, keyed by member, or \emph{ordered} edges, keyed by position. Each membership carries a role set, a weight, a validity interval and a qualifier. The benchmark uses set edges and plain memberships. + +\paragraph{Layout.} All persistent state is held in copy-on-write counted B+trees. Every child reference stores a summary of its subtree: the entry count, the key range and an order-independent fingerprint. Three trees matter here: +\begin{itemize} +\item The \emph{catalog} maps an atom id to its record. A hyperedge record holds a reference to that edge's \emph{membership tree}, keyed by member id. If the membership tree is one leaf of at most 32 entries that fits in about 100 bytes, it is stored inside the record itself and needs no page of its own. +\item The \emph{reverse incidence index} maps an atom id to the set of hyperedges that contain it. Sets of up to 48 entries are stored inline in the index leaf. Larger sets are promoted to a tree of their own. +\item Secondary indexes on node keys and properties, which the benchmark touches only through node insertion and updates. +\end{itemize} +Enumerating an incidence set is a cursor walk over one inline list or one small tree. Counting the hyperedges that contain two given atoms is a leapfrog intersection~\cite{leapfrog} of their two incidence trees. The intersection skips whole subtrees whose key range cannot contain a match. + +\paragraph{Pages and files.} A tree node is written as a self-describing image: an 80-byte header with a CRC32C checksum, followed by the payload. Images are packed into append-only segment files of 256\,MiB, aligned to 64-byte units. A page directory maps each page number to the image's current address, so compaction can move an image without rewriting the trees that refer to it. Reads go through a memory map of each segment. Decoded nodes are kept in a node cache whose budget is set in bytes. In catalog and incidence index leaves, each record is preceded by its length. A leaf read from a page keeps the page bytes and the offset of every record, and decodes a record the first time it is read. A decoded record is then kept in the leaf and charged to the node cache, so a lookup that misses the cache decodes only the record it needs. + +\paragraph{Generations and readers.} A committed state is a \emph{generation}: an immutable vector of tree roots. A reader pins a generation and reads it without locks. Writers never modify a node that a published generation can reach, as in other copy-on-write B+tree stores~\cite{lmdb}. The last 64 generations are retained for time travel. + +\begin{figure*}[t] +\centering +\begin{tikzpicture}[ + font=\footnotesize, + box/.style={draw=rule,rounded corners=2pt,minimum height=9mm,minimum width=25mm,inner sep=3pt,align=center,fill=white}, + disk/.style={box,fill=hs!9}, + arr/.style={-{Stealth[length=2mm]},rule,semithick}, + lab/.style={font=\scriptsize,align=center,text=black!75}] +\node[box] (txn) at (0,0) {Transaction\\private workspace}; +\node[box] (commit) at (4.4,0) {Commit\\(holds commit lock)}; +\node[box,minimum width=36mm] (gen) at (9.8,0) {Published generation $g$\\changed nodes still in memory}; +\node[box] (read) at (15.1,0) {Readers\\pin $g$, no locks}; +\node[disk] (wal) at (4.4,-2.3) {Write-ahead log\\changed keys per tree}; +\node[box,minimum width=36mm] (ckpt) at (9.8,-2.3) {Checkpoint\\(background thread)}; +\node[disk] (seg) at (15.1,-2.3) {Segment files\\and page directory}; +\node[disk,minimum width=36mm] (cat) at (9.8,-4.3) {Catalog image\\and page reference counts}; +\draw[arr] (txn) -- (commit); +\draw[arr] (commit) -- node[lab,above]{(2) publish\\new roots} (gen); +\draw[arr] (read) -- (gen); +\draw[arr] (commit) -- node[lab,left]{(1) append, then\\one fsync per group} (wal); +\draw[arr] (gen) -- node[lab,left]{(3) nodes no\\checkpoint wrote} (ckpt); +\draw[arr] (ckpt) -- node[lab,below]{(4) write each\\node once, sync} (seg); +\draw[arr] (ckpt) -- node[lab,right]{(5) count, publish} (cat); +\draw[arr] (ckpt) -- node[lab,above]{(7) truncate} (wal); +\draw[arr,dashed] (ckpt.north east) to[out=40,in=-40] node[lab,right]{(6) swap in\\stored references} (gen.south east); +\end{tikzpicture} +\caption{HStore's write path. (1, 2) A commit logs only the keys it changed and publishes new roots whose changed nodes are still in memory. (3--7) A background checkpoint writes those nodes to pages, once for all the commits since the previous checkpoint, then swaps the stored references into the current generation and truncates the log. Only step 6 takes the commit lock.} +\label{fig:writepath} +\end{figure*} + +\paragraph{Commits and checkpoints.} \Cref{fig:writepath} shows the write path. A transaction collects its changes in a private workspace. At commit it applies them to new copies of the affected tree paths, appends one write-ahead log record listing the keys it changed in each tree, and publishes the new generation. The new tree nodes stay in memory, reachable from the published roots. A background checkpoint later writes every node that no earlier checkpoint wrote, syncs the files, updates page reference counts, and publishes a catalog image. It then swaps the stored references into the current generation and truncates the log. Only the swap holds the commit lock. Memory held by unwritten nodes is capped at 64\,MiB. A commit that finds the cap reached waits for a checkpoint. + +Two exceptions write pages before the checkpoint. A large bulk load spills finished subtrees to pages while it runs, and a tree that changed in more than 4,096 places is written to pages at commit and logged by reference. Under \emph{synchronous} durability, a commit returns after its log record is fsynced; concurrent commits share one fsync (group commit). Under \emph{asynchronous} durability the fsync is skipped, but commits are still published strictly in order. + +\paragraph{Recovery.} On open, HStore loads the newest catalog image and replays the logged key changes of every commit after it, in commit order. A commit's changes are applied only if its commit record reached the log, so recovery always yields a prefix of the commit order. + +\paragraph{Space reclamation.} Every page has a reference count, which changes only at checkpoints. A page no generation can reach is released once no pinned reader can see it. A segment whose live bytes fall below half of the bytes written to it is compacted: its live images are copied to the active segment, the page directory is updated, and the old file is deleted once no reader can still be using it. A background maintenance pass, run every 500\,ms, compacts one such segment at a time. An explicit compaction request compacts all of them. + +\subsection{HyperGraphDB} + +HyperGraphDB~\cite{hgdb} is a general hypergraph database for Java. Its atoms are identified by persistent handles (UUIDs). A link stores the ordered array of its target handles. The benchmark uses \texttt{HGPlainLink} for hyperedges and \texttt{String} atoms for nodes. The incidence set of an atom, the links pointing to it, is maintained by HyperGraphDB in a separate index. The atom cache keeps recently used atoms and incidence sets in memory as Java objects. + +We use the Berkeley DB Java Edition storage implementation (\texttt{bdb-je}), the default for HyperGraphDB~1.4. JE is a log-structured B+tree store~\cite{je,lfs}: every update appends a new version of the changed records and internal nodes to the log. A background cleaner copies the live entries out of underused log files and deletes them. A checkpointer bounds recovery time. Incidence sets are stored as sorted duplicate sets in a JE database. A lookup opens a cursor at the atom's handle. + +\subsection{Synchronous commits in HyperGraphDB} +\label{sec:durability} + +HyperGraphDB 1.4 begins every JE transaction with an explicit \texttt{WRITE\_NO\_SYNC} durability. That setting overrides the environment-wide \texttt{COMMIT\_SYNC}, so configuring the environment alone leaves commits unsynced: in a first run of the synchronous configuration, HyperGraphDB committed single updates in about 0.1\,ms, while one forced write on this machine takes about 3.8\,ms. For the synchronous configuration the HyperGraphDB adapter therefore calls \texttt{Environment.flushLog(true)} after every commit. This writes and forces JE's log, which is what \texttt{COMMIT\_SYNC} would do at commit time. The asynchronous configuration is unaffected. + +\subsection{Versions and configuration} + +\Cref{tab:config} lists the configuration of both systems. The two caches are given the same 64\,MiB budget. HStore's budget covers decoded nodes. JE's budget covers its B+tree nodes and records, and HyperGraphDB's atom cache comes on top of it. That cache is sized by HyperGraphDB itself and cannot be capped in bytes. The retained-heap figure in \cref{sec:resources} shows what each store actually kept. + +\begin{table*}[t] +\centering\footnotesize +\caption{System configuration. Both systems use their defaults except where stated.} +\label{tab:config} +\begin{tabularx}{0.82\textwidth}{@{}lXX@{}} +\toprule +& \hstore & \hgdb \\ +\midrule +Version & 0.1.0, commit \texttt{9291e18}, storage format 8 & 1.4, commit \texttt{99485a1}; JE 5.0.73 \\ +Cache & node cache, 64\,MiB & JE cache 64\,MiB, plus atom cache \\ +Async commits & no fsync, ordered publication & JE \texttt{WRITE\_NO\_SYNC} (default) \\ +Sync commits & fsync per commit group & JE log forced after each commit (\cref{sec:durability}) \\ +Page / node size & 16\,KiB pages & JE defaults \\ +Space reclamation & compact segments below 50\% live & cleaner, 50\% minimum utilisation \\ +History & 64 generations & none \\ +\bottomrule +\end{tabularx} +\end{table*} diff --git a/benchmarks/report/sections/validity.tex b/benchmarks/report/sections/validity.tex new file mode 100644 index 0000000..4b13114 --- /dev/null +++ b/benchmarks/report/sections/validity.tex @@ -0,0 +1,16 @@ +\section{Threats to validity} +\label{sec:validity} + +\paragraph{One machine, not idle.} All numbers come from one laptop that was also running other work. Sampled before each run, the one-minute load average was between \M{loadmin} and \M{loadmax} (median \M{loadmed}) on 10 cores. That figure includes the tail of the previous run, whose parallel workloads use 8 threads, so only part of it is outside work. Absolute throughput will differ on a server, and short workloads are sensitive to interference. We report medians, show every run in \cref{app:variability}, and mark ratios whose run ranges overlap. Conclusions in this report rest only on ratios that are large compared with that spread. + +\paragraph{Durability.} On this machine one \texttt{FileChannel.force} after a small write takes about 3.8\,ms at the median, the cost of a write that reaches stable storage. Both systems pay it on every synchronous commit: HStore through its group commit, HyperGraphDB through the explicit log flush described in \cref{sec:durability}. Without that flush, HyperGraphDB's synchronous configuration would not sync at all. The \wl{recover} workload kills a process, not the machine, so it measures recovery time and the system's own buffering, not power-failure durability. HStore's behaviour under torn writes and lost syncs is covered by its crash-injection tests, which are outside the scope of this report. + +\paragraph{Memory fits in RAM.} At both scales the files fit in the operating system's page cache. Reads that miss a system's own 64\,MiB cache are served from memory, not from the SSD. The cold-read numbers therefore measure decoding and cache admission, not device latency. + +\paragraph{Cache budgets.} HStore counts its 64\,MiB node cache against an estimate of the heap each cached node takes, not a measured size. JE's 64\,MiB budget is exact, but HyperGraphDB's atom and incidence caches are not capped in bytes. Neither side therefore holds exactly 64\,MiB. The retained heap in \cref{tab:memory} shows what each kept after ingest. + +\paragraph{Workload coverage.} The dataset is synthetic. It uses set hyperedges without roles, properties or ordered members, and node values of a few bytes. Queries are single storage operations, not HQL or HyperGraphDB queries, so query planning is not measured. Workloads run in a fixed order within one process, so each one starts from the state the previous one left: cache contents, garbage, and for HStore, unwritten nodes waiting for a checkpoint. + +\paragraph{Versions.} HyperGraphDB 1.4 is built against Berkeley DB JE 5.0.73, which is older than current JE releases. We did not try other JE versions or tuning beyond the cache size. Both systems otherwise use their default settings. + +\paragraph{Authorship.} The HStore authors wrote the harness and both adapters. To limit bias, both adapters call the most direct storage API available, every read result is cross-checked between the systems, and both receive the same warm-up and cache budget. The harness, the adapters and the raw results are published with HStore. diff --git a/benchmarks/src/main/java/io/hstore/bench/DatasetStats.java b/benchmarks/src/main/java/io/hstore/bench/DatasetStats.java new file mode 100644 index 0000000..bdefa1d --- /dev/null +++ b/benchmarks/src/main/java/io/hstore/bench/DatasetStats.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Metacog Labs +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 + +package io.hstore.bench; + +import java.util.Arrays; +import java.util.stream.IntStream; + +public final class DatasetStats { + + private static final long SEED = 20_260_904L; + private static final int INLINE_POSTINGS = 48; + + private DatasetStats() { + } + + public static void main(String[] args) { + IntStream.of(1, 4).forEach(DatasetStats::print); + } + + private static void print(int scale) { + Dataset dataset = Dataset.generate(scale, SEED); + int[] degree = new int[dataset.nodes()]; + int[] cardinality = Arrays.stream(dataset.edges()).mapToInt(edge -> edge.length).sorted().toArray(); + Arrays.stream(dataset.edges()).flatMapToInt(Arrays::stream).forEach(member -> degree[member]++); + long incidences = IntStream.of(cardinality).asLongStream().sum(); + int[] sorted = degree.clone(); + Arrays.sort(sorted); + IO.println("scale %d: %,d nodes, %,d hyperedges, %,d incidences".formatted(scale, dataset.nodes(), cardinality.length, incidences)); + IO.println(" cardinality mean %.2f, p99 %d, max %d".formatted(incidences / (double) cardinality.length, + at(cardinality, 0.99), cardinality[cardinality.length - 1])); + IO.println(" incidence set mean %.2f, p50 %d, p90 %d, p99 %d, p99.9 %d, max %d".formatted(incidences / (double) degree.length, + at(sorted, 0.5), at(sorted, 0.9), at(sorted, 0.99), at(sorted, 0.999), sorted[sorted.length - 1])); + IO.println(" nodes in no hyperedge %,d, incidence sets above %d entries %,d".formatted( + IntStream.of(degree).filter(d -> d == 0).count(), INLINE_POSTINGS, IntStream.of(degree).filter(d -> d > INLINE_POSTINGS).count())); + } + + private static int at(int[] sorted, double quantile) { + return sorted[(int) (sorted.length * quantile)]; + } +} diff --git a/benchmarks/src/main/java/io/hstore/bench/HyperGraphDbStore.java b/benchmarks/src/main/java/io/hstore/bench/HyperGraphDbStore.java index 7774608..edc88d9 100644 --- a/benchmarks/src/main/java/io/hstore/bench/HyperGraphDbStore.java +++ b/benchmarks/src/main/java/io/hstore/bench/HyperGraphDbStore.java @@ -39,10 +39,12 @@ final class HyperGraphDbStore implements Store { private long readBeforeReopen; private final long cacheBytes; + private final boolean sync; HyperGraphDbStore(Path directory, boolean sync, long cacheBytes) { this.directory = directory; this.cacheBytes = cacheBytes; + this.sync = sync; configuration.setTransactional(true); EnvironmentConfig environment = ((BJEStorageImplementation) configuration.getStoreImplementation()).getConfiguration().getEnvironmentConfig(); if (sync) { @@ -76,7 +78,11 @@ public Path directory() { } private T transact(Callable work) { - return graph.getTransactionManager().transact(work); + T result = graph.getTransactionManager().transact(work); + if (sync) { + environment().flushLog(true); + } + return result; } private T read(Callable work) { diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index b6e01a3..dd3793f 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -154,7 +154,7 @@ sequenceDiagram Notes: -* Nodes are decoded once and then shared by every reader through `NodeCache`. It has 16 shards, each an open-addressing table keyed by page number plus a CLOCK (second-chance) ring, with a total byte budget of `node_cache_mb`, each node counted at an estimate of its heap size (`Node.heapBytes`). Lookups are lock-free; only admission takes a per-shard lock. Decoded nodes are immutable (`owner == null`), so sharing them needs no copying. See [pages.md](../storage/pages.md#caching-and-io-accounting). +* Nodes are decoded once and then shared by every reader through `NodeCache`. Catalog and posting-list leaves are kept packed and decode each record the first time it is read. It has 16 shards, each an open-addressing table keyed by page number plus a CLOCK (second-chance) ring, with a total byte budget of `node_cache_mb`, each node counted at an estimate of its heap size (`Node.heapBytes`). Lookups are lock-free; only admission takes a per-shard lock. Decoded nodes are immutable (`owner == null`), so sharing them needs no copying. See [pages.md](../storage/pages.md#caching-and-io-accounting). * `IoTrace` is bound with `ScopedValue.where(...)` around a query. `recordRead` throws `HStoreException.limit` once page reads plus cache hits exceed the query's page budget (`query_page_budget`). * A tree changed since the last checkpoint starts with in-memory nodes (`Ref.Pending`). A read follows those directly and goes through `PagedNodeSource` only once it reaches a stored reference. * Historical reads use `TransactionManager.snapshotAt(generation, branch)`. It looks the generation up in the retained `history` map; see [catalog-and-generations.md](../storage/catalog-and-generations.md#history-retention-and-time-travel). @@ -248,7 +248,7 @@ semantic/state 53 B persisted HNSW index state (datab | Path | Owner | Format | |---|---|---| -| `FORMAT` | `StorageEngine.verifyFormat` | `Properties` with `format=7` (commits logged as key changes and tree pages written at checkpoints, page numbers resolved through a page directory, reference counts per page, small member trees inline in their edge record) and `page-size` (the maximum node size). Opening a directory written in an older format, or opening with a different page size, fails. | +| `FORMAT` | `StorageEngine.verifyFormat` | `Properties` with `format=8` (leaf records framed by length, commits logged as key changes and tree pages written at checkpoints, page numbers resolved through a page directory, reference counts per page, small member trees inline in their edge record) and `page-size` (the maximum node size). Opening a directory written in an older format, or opening with a different page size, fails. | | `LOCK` | `StorageEngine.open` | An exclusive OS file lock. A second process gets `database ... is opened by another process`. | | `catalog/generations/%016x.cat` | `CatalogStore.publish` | `int magic 0x54414348`, `int crc32c(payload)`, `long length`, then the `CatalogImage` payload. Written to `*.tmp`, fsynced, atomically renamed, and the directory fsynced. The three newest files are kept. | | `catalog/manifest` | `CatalogStore` | The file name of the latest `.cat`, written durably the same way. | diff --git a/docs/benchmark-report.pdf b/docs/benchmark-report.pdf new file mode 100644 index 0000000..5221467 Binary files /dev/null and b/docs/benchmark-report.pdf differ diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 441aff0..e95645a 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -23,11 +23,18 @@ flowchart LR | Data model used | node type `Entity` (indexed by key, property `name`); `SET_EDGE` type `Relation` | `String` atoms; `HGPlainLink` links | | Cache | node cache of half the heap, or `CACHE_MB` | JE cache at 30% of the heap (HyperGraphDB default), or `CACHE_MB`, plus the HyperGraphDB atom cache | | `async` durability | `durability = async` | HyperGraphDB default: JE `WRITE_NO_SYNC` | -| `sync` durability | `durability = sync`, `wal_mode = references`, group commit | JE `COMMIT_SYNC` | +| `sync` durability | `durability = sync`, `wal_mode = references`, group commit | JE `COMMIT_SYNC`, and `Environment.flushLog(true)` after every commit (see below) | | History | `history_limit = 64` generations retained for time travel (default) | none | -Each run used a 4 GiB heap and `-XX:+UseParallelGC` on JDK 25 (GraalVM, HotSpot JIT) on macOS 26.5.1 with an -Apple silicon `aarch64` CPU and 10 cores. +Each run used a 4 GiB heap and `-XX:+UseParallelGC` on Oracle GraalVM for JDK 25.0.4 (HotSpot JIT), on an Apple +M5 (4 performance and 6 efficiency cores) with 24 GiB of memory and an internal SSD, running macOS 26.5.1. + +**Synchronous commits in HyperGraphDB.** HyperGraphDB 1.4 begins every JE transaction with an explicit +`WRITE_NO_SYNC` durability, which overrides the environment's `COMMIT_SYNC`. Setting the environment alone +leaves its commits unsynced: single-update commits took about 0.1 ms, while one forced write on the machine +above takes about 3.8 ms. In `sync` mode the adapter therefore calls `Environment.flushLog(true)` after every +commit, which writes and forces JE's log as `COMMIT_SYNC` would. Results for `sync` published before this +change measured HyperGraphDB without any sync. **Dataset.** At scale *s*: 50,000·*s* nodes and 100,000·*s* hyperedges. Cardinality follows a geometric tail starting at 2 (continue with probability 0.62, capped at 32), for a mean of 3.6. Members are drawn as @@ -159,121 +166,219 @@ configured (see *Setup*). ## Results +These results come from HStore at commit `9291e18`, on the machine described in *Setup*, with both caches set to +64 MiB (`CACHE_MB=64`) and `THREADS=8`. Scale 1 ran 5 times per store in each durability mode, and scale 4 ran 3 +times. Each cell is the median, with the slowest and fastest run in brackets. The machine was running other +work: sampled before each run, the one-minute load average was between 3.4 and 18.3 on 10 cores. Where the +bracketed ranges of the two stores overlap, treat the ratio as a tendency rather than a measured difference. + +The [benchmark report](benchmark-report.pdf) has the full method, every table for all three configurations +(latency, bytes written, heap and GC), the value of every run, and the discussion. Its raw results, and the +script that builds its tables, are in [`benchmarks/report`](../benchmarks/report). + Ratios above 1 favour HStore. Throughput ratios divide HStore by HyperGraphDB; latency and size ratios divide -HyperGraphDB by HStore. Laptop runs vary by up to ±30% from one run to the next, so read the tables for -consistent multiples rather than single digits. +HyperGraphDB by HStore. ### Scale 1, `async` durability -Dataset: 50,000 nodes, 100,000 hyperedges, 362,757 incidences. +Dataset: 50,000 nodes, 100,000 hyperedges, 362,757 incidences; durability async; 8 threads. -| Workload | HStore | HyperGraphDB | Ratio | Results agree | +| Workload | HStore | HyperGraphDB | Ratio of medians | Results agree | |---|---:|---:|---:|:---:| -| `ingest.nodes` — insert nodes in transactions of 1,000 | 12,717 ops/s | 10,451 ops/s | **1.22×** | yes | -| `ingest.edges` — insert hyperedges (mean cardinality 3.6) in transactions of 1,000 | 9,610 ops/s | 16,684 ops/s | **0.58×** | yes | -| `read.incidence` — enumerate the incidence set of a node | 189,969 ops/s | 48,237 ops/s | **3.94×** | yes | -| `read.members` — enumerate the members of a hyperedge | 313,600 ops/s | 853,555 ops/s | **0.37×** | yes | -| `read.comembership` — count hyperedges containing both atoms of a pair | 72,541 ops/s | 9,134 ops/s | **7.94×** | yes | -| `read.incidence.parallel` — incidence sets from 10 threads, 100,000 probes each | 585,719 ops/s | 881,973 ops/s | **0.66×** | yes | -| `write.update` — replace a node value in transactions of 1,000 | 9,821 ops/s | 5,114 ops/s | **1.92×** | yes | -| `large.ingest` — create one hyperedge with 50,000 members | 93,884 ops/s | 40,387 ops/s | **2.32×** | yes | -| `large.scan` — enumerate all members of the large hyperedge (x20) | 2,392,915 ops/s | 81,701,289 ops/s | **0.03×** | yes | -| `large.probe` — test whether a node belongs to the large hyperedge | 594,904 ops/s | 200,944 ops/s | **2.96×** | yes | -| `reopen` — close and reopen the database, including recovery | 961 ms | 1,514 ms | **1.58×** | yes | -| `read.incidence.cold` — incidence sets immediately after reopening | 137,791 ops/s | 35,009 ops/s | **3.94×** | yes | -| `disk` — bytes on disk after a clean shutdown | 243.5 MiB | 252.9 MiB | **1.04×** | yes | +| `ingest.nodes`: insert nodes in transactions of 1,000 | 67,741 ops/s (51,067 to 72,960) | 37,664 ops/s (32,804 to 40,002) | **1.80×** | yes | +| `ingest.edges`: insert hyperedges (mean cardinality 3.6) in transactions of 1,000 | 50,042 ops/s (42,339 to 54,792) | 34,971 ops/s (33,281 to 39,454) | **1.43×** | yes | +| `read.incidence`: enumerate the incidence set of a node | 6,263,882 ops/s (3,838,956 to 8,193,140) | 1,489,214 ops/s (1,399,430 to 1,585,233) | **4.21×** | yes | +| `read.members`: enumerate the members of a hyperedge | 6,114,743 ops/s (2,146,691 to 6,408,134) | 2,663,119 ops/s (1,620,982 to 3,486,067) | **2.30×** | yes | +| `read.comembership`: count hyperedges containing both atoms of a pair | 610,291 ops/s (560,312 to 788,357) | 53,899 ops/s (2,236 to 76,650) | **11.32×** | yes | +| `read.twohop`: count the distinct nodes that share a hyperedge with a node | 543,317 ops/s (335,820 to 574,014) | 98,274 ops/s (71,007 to 148,759) | **5.53×** | yes | +| `read.incidence.parallel`: incidence sets from 8 threads, 100,000 probes each | 36,557,085 ops/s (20,950,132 to 40,593,168) | 2,246,677 ops/s (1,947,200 to 2,675,350) | **16.27×** | yes | +| `latency.read`: one incidence lookup per read transaction | 3,833,633 ops/s (2,414,108 to 4,720,358) | 456,998 ops/s (372,257 to 558,321) | **8.39×** | yes | +| `write.update`: replace a node value in transactions of 1,000 | 69,879 ops/s (50,256 to 108,002) | 12,986 ops/s (10,565 to 19,117) | **5.38×** | yes | +| `latency.commit`: one node update per write transaction | 12,387 ops/s (10,410 to 14,893) | 14,088 ops/s (7,229 to 24,612) | **0.88×** | yes | +| `mixed.read`: single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s | 2,457,466 ops/s (2,368,477 to 2,542,025) | 1,131,996 ops/s (1,059,891 to 1,199,589) | **2.17×** | n/a | +| `mixed.write`: updates committed by the writer alongside the readers | 1,000 ops/s (1,000 to 1,000) | 1,000 ops/s (999 to 1,000) | **1.00×** | n/a | +| `large.ingest`: create one hyperedge with 50,000 members | 610,120 ops/s (286,715 to 755,104) | 116,290 ops/s (83,926 to 136,066) | **5.25×** | yes | +| `large.scan`: enumerate all members of the large hyperedge (x400, after a warm-up) | 4,048,958,385 ops/s (3,507,028,260 to 4,912,948,690) | 1,122,938,397 ops/s (590,299,171 to 1,296,824,104) | **3.61×** | yes | +| `large.probe`: test whether a node belongs to the large hyperedge | 17,214,296 ops/s (9,625,064 to 19,639,451) | 1,824,713 ops/s (865,555 to 2,056,360) | **9.43×** | yes | +| `reopen`: close and reopen the database, including recovery | 111 ms (107 to 131) | 924 ms (674 to 1,361) | **8.29×** | yes | +| `read.incidence.cold`: incidence sets immediately after reopening | 4,044,844 ops/s (3,458,866 to 4,142,345) | 95,895 ops/s (89,040 to 136,966) | **42.18×** | yes | +| `disk`: bytes on disk after a clean shutdown | 92.4 MiB (90.4 to 92.6) | 346.4 MiB (289.6 to 371.8) | **3.75×** | yes | +| `churn.delete`: delete 20,000 hyperedges (20%) in transactions of 1,000 | 24,490 ops/s (20,169 to 29,917) | 7,904 ops/s (5,925 to 9,130) | **3.10×** | yes | +| `churn.remove`: remove one member from each of 20,000 other hyperedges, 1,000 per transaction | 43,155 ops/s (39,001 to 49,663) | 17,477 ops/s (16,078 to 19,754) | **2.47×** | yes | +| `read.incidence.churned`: enumerate incidence sets after the deletes | 8,398,538 ops/s (7,852,427 to 9,263,154) | 2,104,833 ops/s (932,051 to 2,516,987) | **3.99×** | yes | +| `disk.churned`: bytes on disk after the deletes and a clean shutdown | 117.1 MiB (114.1 to 117.9) | 219.8 MiB (203.2 to 302.1) | **1.88×** | yes | +| `disk.compacted`: bytes on disk after a full compaction and a clean shutdown | 91.2 MiB (91.2 to 91.2) | 198.1 MiB (188.1 to 265.7) | **2.17×** | yes | +| `recover`: open the store after killing the ingest process with SIGKILL | 128 ms (106 to 153) | 76 ms (61 to 88) | **0.59×** | n/a | + +Latency of single operations, median of each percentile across runs: + +| Workload | Percentile | HStore | HyperGraphDB | Ratio | +|---|---|---:|---:|---:| +| `latency.read` | p50 | 0.2 µs | 0.9 µs | **4.39×** | +| `latency.read` | p99 | 0.5 µs | 12.7 µs | **25.33×** | +| `latency.read` | p99.9 | 1.5 µs | 22.6 µs | **15.06×** | +| `latency.read` | max | 44.0 µs | 1.06 ms | **24.16×** | +| `latency.commit` | p50 | 52.0 µs | 45.5 µs | **0.87×** | +| `latency.commit` | p99 | 295.7 µs | 465.5 µs | **1.57×** | +| `latency.commit` | p99.9 | 441.0 µs | 936.1 µs | **2.12×** | +| `latency.commit` | max | 1.85 ms | 7.36 ms | **3.98×** | +| `mixed.read` | p50 | 1.8 µs | 3.3 µs | **1.86×** | +| `mixed.read` | p99 | 10.5 µs | 54.5 µs | **5.21×** | +| `mixed.read` | p99.9 | 25.7 µs | 305.5 µs | **11.90×** | +| `mixed.read` | max | 28.51 ms | 28.50 ms | **1.00×** | +| `mixed.write` | p50 | 2.33 ms | 2.90 ms | **1.25×** | +| `mixed.write` | p99 | 9.65 ms | 16.94 ms | **1.75×** | +| `mixed.write` | p99.9 | 22.02 ms | 34.41 ms | **1.56×** | +| `mixed.write` | max | 22.02 ms | 34.41 ms | **1.56×** | +| `compaction.baseline` | p50 | 3.31 ms | 3.36 ms | **1.02×** | +| `compaction.baseline` | p99 | 5.43 ms | 7.50 ms | **1.38×** | +| `compaction.baseline` | p99.9 | 8.80 ms | 10.52 ms | **1.20×** | +| `compaction.baseline` | max | 8.80 ms | 10.52 ms | **1.20×** | +| `compaction` | p50 | 3.36 ms | 3.24 ms | **0.97×** | +| `compaction` | p99 | 5.44 ms | 11.59 ms | **2.13×** | +| `compaction` | p99.9 | 6.17 ms | 84.26 ms | **13.66×** | +| `compaction` | max | 6.17 ms | 84.26 ms | **13.66×** | ### Scale 1, `sync` durability -| Workload | HStore | HyperGraphDB | Ratio | Results agree | +Dataset: 50,000 nodes, 100,000 hyperedges, 362,757 incidences; durability sync; 8 threads. + +| Workload | HStore | HyperGraphDB | Ratio of medians | Results agree | |---|---:|---:|---:|:---:| -| `ingest.nodes` — insert nodes in transactions of 1,000 | 15,748 ops/s | 11,211 ops/s | **1.40×** | yes | -| `ingest.edges` — insert hyperedges (mean cardinality 3.6) in transactions of 1,000 | 12,349 ops/s | 9,142 ops/s | **1.35×** | yes | -| `read.incidence` — enumerate the incidence set of a node | 339,666 ops/s | 31,321 ops/s | **10.84×** | yes | -| `read.members` — enumerate the members of a hyperedge | 557,841 ops/s | 803,557 ops/s | **0.69×** | yes | -| `read.comembership` — count hyperedges containing both atoms of a pair | 114,055 ops/s | 8,595 ops/s | **13.27×** | yes | -| `read.incidence.parallel` — incidence sets from 10 threads, 100,000 probes each | 912,412 ops/s | 682,443 ops/s | **1.34×** | yes | -| `write.update` — replace a node value in transactions of 1,000 | 12,867 ops/s | 4,844 ops/s | **2.66×** | yes | -| `large.ingest` — create one hyperedge with 50,000 members | 156,773 ops/s | 35,232 ops/s | **4.45×** | yes | -| `large.scan` — enumerate all members of the large hyperedge (x20) | 8,374,222 ops/s | 87,843,227 ops/s | **0.10×** | yes | -| `large.probe` — test whether a node belongs to the large hyperedge | 1,063,080 ops/s | 214,721 ops/s | **4.95×** | yes | -| `reopen` — close and reopen the database, including recovery | 445 ms | 1,472 ms | **3.31×** | yes | -| `read.incidence.cold` — incidence sets immediately after reopening | 289,371 ops/s | 38,394 ops/s | **7.54×** | yes | -| `disk` — bytes on disk after a clean shutdown | 243.5 MiB | 300.3 MiB | **1.23×** | yes | +| `ingest.nodes`: insert nodes in transactions of 1,000 | 55,831 ops/s (25,612 to 60,845) | 30,898 ops/s (25,247 to 33,003) | **1.81×** | yes | +| `ingest.edges`: insert hyperedges (mean cardinality 3.6) in transactions of 1,000 | 37,061 ops/s (16,880 to 43,465) | 29,575 ops/s (13,674 to 32,075) | **1.25×** | yes | +| `read.incidence`: enumerate the incidence set of a node | 6,914,354 ops/s (2,181,969 to 8,485,453) | 997,154 ops/s (455,024 to 1,403,695) | **6.93×** | yes | +| `read.members`: enumerate the members of a hyperedge | 5,423,092 ops/s (2,346,275 to 6,910,830) | 2,378,494 ops/s (1,139,475 to 3,352,302) | **2.28×** | yes | +| `read.comembership`: count hyperedges containing both atoms of a pair | 623,412 ops/s (225,960 to 750,270) | 40,468 ops/s (28,205 to 76,472) | **15.41×** | yes | +| `read.twohop`: count the distinct nodes that share a hyperedge with a node | 349,091 ops/s (212,259 to 575,284) | 104,597 ops/s (68,930 to 118,502) | **3.34×** | yes | +| `read.incidence.parallel`: incidence sets from 8 threads, 100,000 probes each | 26,820,024 ops/s (8,343,759 to 41,000,847) | 1,959,562 ops/s (1,354,823 to 2,040,085) | **13.69×** | yes | +| `latency.read`: one incidence lookup per read transaction | 3,592,293 ops/s (1,615,246 to 4,602,083) | 437,799 ops/s (274,315 to 695,056) | **8.21×** | yes | +| `write.update`: replace a node value in transactions of 1,000 | 64,088 ops/s (42,466 to 77,527) | 8,736 ops/s (3,640 to 10,551) | **7.34×** | yes | +| `latency.commit`: one node update per write transaction | 258 ops/s (244 to 260) | 236 ops/s (229 to 237) | **1.09×** | yes | +| `mixed.read`: single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s | 2,283,312 ops/s (1,936,304 to 2,346,786) | 1,048,371 ops/s (885,124 to 1,088,745) | **2.18×** | n/a | +| `mixed.write`: updates committed by the writer alongside the readers | 1,000 ops/s (1,000 to 1,000) | 1,000 ops/s (1,000 to 1,000) | **1.00×** | n/a | +| `large.ingest`: create one hyperedge with 50,000 members | 611,622 ops/s (330,705 to 701,290) | 68,501 ops/s (44,204 to 124,867) | **8.93×** | yes | +| `large.scan`: enumerate all members of the large hyperedge (x400, after a warm-up) | 3,856,784,479 ops/s (1,065,939,440 to 4,213,853,042) | 921,639,492 ops/s (673,177,777 to 1,080,788,976) | **4.18×** | yes | +| `large.probe`: test whether a node belongs to the large hyperedge | 11,861,283 ops/s (8,958,098 to 17,241,257) | 1,229,677 ops/s (819,877 to 1,814,296) | **9.65×** | yes | +| `reopen`: close and reopen the database, including recovery | 128 ms (119 to 148) | 1,077 ms (858 to 1,245) | **8.44×** | yes | +| `read.incidence.cold`: incidence sets immediately after reopening | 2,929,216 ops/s (2,168,586 to 4,181,768) | 104,708 ops/s (94,852 to 142,580) | **27.98×** | yes | +| `disk`: bytes on disk after a clean shutdown | 93.3 MiB (91.8 to 93.7) | 321.7 MiB (305.6 to 350.8) | **3.45×** | yes | +| `churn.delete`: delete 20,000 hyperedges (20%) in transactions of 1,000 | 26,928 ops/s (10,040 to 28,095) | 5,485 ops/s (4,237 to 8,294) | **4.91×** | yes | +| `churn.remove`: remove one member from each of 20,000 other hyperedges, 1,000 per transaction | 36,834 ops/s (21,182 to 40,925) | 11,888 ops/s (9,157 to 14,220) | **3.10×** | yes | +| `read.incidence.churned`: enumerate incidence sets after the deletes | 8,441,520 ops/s (6,577,667 to 9,027,409) | 1,129,728 ops/s (982,393 to 1,799,620) | **7.47×** | yes | +| `disk.churned`: bytes on disk after the deletes and a clean shutdown | 118.5 MiB (117.1 to 127.5) | 256.5 MiB (195.4 to 276.1) | **2.17×** | yes | +| `disk.compacted`: bytes on disk after a full compaction and a clean shutdown | 91.2 MiB (91.2 to 91.2) | 190.4 MiB (182.5 to 277.1) | **2.09×** | yes | +| `recover`: open the store after killing the ingest process with SIGKILL | 113 ms (107 to 184) | 78 ms (54 to 78) | **0.69×** | n/a | ### Scale 4, `async` durability -Dataset: 200,000 nodes, 400,000 hyperedges, 1,453,427 incidences. +Dataset: 200,000 nodes, 400,000 hyperedges, 1,453,427 incidences; durability async; 8 threads. -| Workload | HStore | HyperGraphDB | Ratio | Results agree | +| Workload | HStore | HyperGraphDB | Ratio of medians | Results agree | |---|---:|---:|---:|:---:| -| `ingest.nodes` — insert nodes in transactions of 1,000 | 11,013 ops/s | 13,093 ops/s | **0.84×** | yes | -| `ingest.edges` — insert hyperedges (mean cardinality 3.6) in transactions of 1,000 | 3,063 ops/s | 3,803 ops/s | **0.81×** | yes | -| `read.incidence` — enumerate the incidence set of a node | 180,086 ops/s | 8,166 ops/s | **22.05×** | yes | -| `read.members` — enumerate the members of a hyperedge | 134,192 ops/s | 888,461 ops/s | **0.15×** | yes | -| `read.comembership` — count hyperedges containing both atoms of a pair | 64,263 ops/s | 2,735 ops/s | **23.50×** | yes | -| `read.incidence.parallel` — incidence sets from 10 threads, 100,000 probes each | 339,512 ops/s | 736,535 ops/s | **0.46×** | yes | -| `write.update` — replace a node value in transactions of 1,000 | 7,015 ops/s | 4,010 ops/s | **1.75×** | yes | -| `large.ingest` — create one hyperedge with 200,000 members | 40,146 ops/s | 15,985 ops/s | **2.51×** | yes | -| `large.scan` — enumerate all members of the large hyperedge (x20) | 2,416,821 ops/s | 78,756,824 ops/s | **0.03×** | yes | -| `large.probe` — test whether a node belongs to the large hyperedge | 841,554 ops/s | 113,450 ops/s | **7.42×** | yes | -| `reopen` — close and reopen the database, including recovery | 2,035 ms | 9,304 ms | **4.57×** | yes | -| `read.incidence.cold` — incidence sets immediately after reopening | 63,822 ops/s | 8,299 ops/s | **7.69×** | yes | -| `disk` — bytes on disk after a clean shutdown | 2361.9 MiB | 1265.5 MiB | **0.54×** | yes | - -Rerunning scale 4 with `history_limit = 1` gives the following disk row, which shows that retained history is -not what drives HStore's footprint: - -| `disk` — bytes on disk after a clean shutdown | 2115.5 MiB | 1265.5 MiB | **0.60×** | yes | +| `ingest.nodes`: insert nodes in transactions of 1,000 | 40,009 ops/s (33,982 to 57,601) | 28,712 ops/s (26,707 to 38,938) | **1.39×** | yes | +| `ingest.edges`: insert hyperedges (mean cardinality 3.6) in transactions of 1,000 | 21,132 ops/s (19,987 to 27,311) | 2,050 ops/s (1,725 to 2,736) | **10.31×** | yes | +| `read.incidence`: enumerate the incidence set of a node | 6,567,839 ops/s (6,370,914 to 7,065,286) | 574,929 ops/s (549,276 to 1,290,467) | **11.42×** | yes | +| `read.members`: enumerate the members of a hyperedge | 5,575,872 ops/s (4,558,084 to 5,931,873) | 1,731,668 ops/s (1,449,441 to 1,834,122) | **3.22×** | yes | +| `read.comembership`: count hyperedges containing both atoms of a pair | 357,920 ops/s (306,442 to 380,508) | 18,128 ops/s (17,264 to 53,840) | **19.74×** | yes | +| `read.twohop`: count the distinct nodes that share a hyperedge with a node | 311,293 ops/s (226,920 to 397,015) | 71,578 ops/s (46,798 to 96,728) | **4.35×** | yes | +| `read.incidence.parallel`: incidence sets from 8 threads, 100,000 probes each | 14,848,396 ops/s (9,710,034 to 17,930,486) | 1,459,037 ops/s (1,355,328 to 1,835,461) | **10.18×** | yes | +| `latency.read`: one incidence lookup per read transaction | 2,358,391 ops/s (1,510,230 to 2,532,733) | 350,668 ops/s (205,075 to 361,613) | **6.73×** | yes | +| `write.update`: replace a node value in transactions of 1,000 | 51,937 ops/s (21,950 to 67,587) | 1,569 ops/s (1,434 to 2,282) | **33.10×** | yes | +| `latency.commit`: one node update per write transaction | 12,193 ops/s (8,383 to 16,763) | 2,261 ops/s (1,783 to 2,547) | **5.39×** | yes | +| `mixed.read`: single incidence lookups from 7 readers while one writer commits 10 updates at a time, targeting 1,000 updates/s | 1,984,771 ops/s (1,929,424 to 2,177,523) | 855,896 ops/s (840,015 to 1,007,499) | **2.32×** | n/a | +| `mixed.write`: updates committed by the writer alongside the readers | 1,000 ops/s (1,000 to 1,000) | 1,000 ops/s (994 to 1,000) | **1.00×** | n/a | +| `large.ingest`: create one hyperedge with 200,000 members | 489,256 ops/s (357,717 to 530,352) | 12,076 ops/s (11,960 to 12,329) | **40.51×** | yes | +| `large.scan`: enumerate all members of the large hyperedge (x400, after a warm-up) | 3,638,203,410 ops/s (2,471,360,485 to 3,728,075,827) | 431,469,984 ops/s (151,006,446 to 452,912,545) | **8.43×** | yes | +| `large.probe`: test whether a node belongs to the large hyperedge | 11,711,888 ops/s (6,814,155 to 15,438,751) | 707,036 ops/s (363,080 to 1,115,133) | **16.56×** | yes | +| `reopen`: close and reopen the database, including recovery | 206 ms (130 to 242) | 1,157 ms (1,028 to 1,270) | **5.61×** | yes | +| `read.incidence.cold`: incidence sets immediately after reopening | 1,163,030 ops/s (984,567 to 2,006,144) | 13,081 ops/s (12,526 to 13,314) | **88.91×** | yes | +| `disk`: bytes on disk after a clean shutdown | 165.7 MiB (164.9 to 166.4) | 2294.8 MiB (2211.7 to 2367.2) | **13.85×** | yes | +| `churn.delete`: delete 80,000 hyperedges (20%) in transactions of 1,000 | 3,388 ops/s (2,225 to 4,295) | 1,426 ops/s (1,308 to 1,459) | **2.38×** | yes | +| `churn.remove`: remove one member from each of 20,000 other hyperedges, 1,000 per transaction | 5,771 ops/s (3,508 to 6,005) | 1,919 ops/s (1,677 to 2,722) | **3.01×** | yes | +| `read.incidence.churned`: enumerate incidence sets after the deletes | 5,677,396 ops/s (3,467,056 to 6,307,954) | 1,015,417 ops/s (531,391 to 1,302,526) | **5.59×** | yes | +| `disk.churned`: bytes on disk after the deletes and a clean shutdown | 1238.9 MiB (1225.2 to 1239.6) | 1879.4 MiB (1846.6 to 1966.1) | **1.52×** | yes | +| `disk.compacted`: bytes on disk after a full compaction and a clean shutdown | 164.5 MiB (164.5 to 164.5) | 1159.1 MiB (1119.0 to 1176.9) | **7.05×** | yes | +| `recover`: open the store after killing the ingest process with SIGKILL | 219 ms (189 to 257) | 455 ms (327 to 527) | **2.08×** | n/a | ## Analysis **Where HStore leads, and why.** -* **Incidence enumeration (4–22×) and co-membership (8–24×).** An atom's incidence set is a counted B+tree keyed - by hyperedge id. Enumeration is therefore a sequential cursor walk over a few packed nodes, and +* **Incidence enumeration (4.2–11×), co-membership (11–20×) and cold reads after a reopen (28–89×).** An + atom's incidence set is one entry of the reverse index: a short list stored inline for most atoms, and a + counted B+tree for hubs. Enumeration hands each hyperedge id to the caller without allocating, and co-membership is a leapfrog intersection that skips whole subtrees by their summaries - ([persistent-tree.md](storage/persistent-tree.md)). HyperGraphDB stores incidence sets as BDB duplicate - sets: it opens a cursor for each lookup, and its `hg.and` query plans a general join. The gap widens with - scale because hubs grow and HStore's cost depends on the smaller side of the intersection. -* **Membership probes on giant hyperedges (3–7×).** A member lookup is a single descent of the hyperedge's - membership tree. HyperGraphDB answers it through the node's incidence set. -* **Updates (1.8–2.7×) and giant-edge ingest (2.3–4.5×).** A batch becomes one copy-on-write path per touched - tree and a single root swap at commit, and large edges are bulk-built bottom-up (`BulkBuilder`). -* **Reopen (1.6–4.6×) and cold reads (4–8×).** A clean shutdown leaves a checkpointed catalog, so opening a - database reads one catalog image and replays an empty WAL suffix. Cold reads benefit from packed 64-byte - extents: one 4 KiB read usually fetches a whole small node ([pages.md](storage/pages.md)). + ([persistent-tree.md](storage/persistent-tree.md)). HyperGraphDB opens a JE cursor on a duplicate set for + each lookup, and its `hg.and` query plans a general join. +* **Updates (5.4–33×) and bytes written.** A commit logs only the keys it changed, a few hundred bytes, and a + checkpoint writes each changed node once for all the commits since the previous one + ([wal-and-recovery.md](transactions/wal-and-recovery.md)). A JE commit appends a new version of every changed + record and of the B+tree nodes above it. With `async` durability, a single-update commit writes 659 B in + HStore against 6.9 KiB in HyperGraphDB. +* **Member enumeration at scale 4 (3.2×) and two-hop queries (3.3–5.5×).** A catalog leaf in the node cache + keeps its page bytes and decodes a record only when it is read + ([persistent-tree.md](storage/persistent-tree.md)), so the catalog stays within a 64 MiB cache: at scale 4 + a member lookup read nothing from the segment files and allocated nothing. At scale 1 HStore is also ahead + (2.3×), with overlapping ranges. +* **Giant hyperedges (ingest 5.3–41×, probes 9.4–17×).** Large edges are bulk-built bottom-up, and a membership + probe is one descent of the edge's membership tree. +* **Disk (3.5–14× smaller after load).** Ids are 8-byte varint gaps instead of 16-byte handles, node images are + packed into 64-byte units, small member trees and incidence sets live inside their parent record, and the WAL + is truncated at every checkpoint. +* **Tail latency.** HStore's p99 and p99.9 are lower in every latency workload, by 1.1–35×. Readers pin an + immutable generation and never wait for writers, checkpoints or compaction. **Where HyperGraphDB leads, and why.** -* **Member enumeration (`read.members`, `large.scan`).** `graph.get(link)` returns an `HGPlainLink` from the - atom cache, which holds a Java array of target handles, so a scan is an array loop. HStore walks a B+tree - cursor, decodes incidence columns, resolves role sets and builds a `Member` record for each member. That - record carries roles, weight, validity, position and qualifier. The cost buys per-member metadata and - snapshot-consistent reads, which a plain link cannot express. The benchmark now reads ids through - `Reader.forEachMemberId`, which builds neither `Member` records nor tree entries; the numbers below predate that. - Small hyperedges now keep their members inline in their catalog record, so the extra page per edge is gone - too (see *Inline member trees* below); the tables above predate that as well. -* **Edge ingest at small batch sizes (about 0.6–0.8× in async mode).** Each commit rewrites the incident-tree - paths of every touched member, which is copy-on-write write amplification. JE appends log records and - defers the work to its cleaner. In `sync` mode group commit amortises the fsync and HStore leads (1.35×). - Since storage format 7 a commit no longer writes those paths: it logs the keys it changed, and a checkpoint - writes each changed node once for all the commits since the previous one. These numbers predate that. -* **Parallel incidence reads at scale 4 (0.46×).** Every probe batch opens a snapshot, which pins a - generation in a shared skip-list map. At 10 threads this coordination, plus node-cache admission under - churn (the hot set exceeded the 1,048,576-node cache these runs used), costs more than JE's latch-free cache hits. At scale 1 - the results are mixed: 0.66× async and 1.34× sync. -* **Disk at scale 4 (0.54×).** Live data is comparable: about 1 GB of live node images in HStore against - 1.27 GB in JE. The difference is garbage that has not been reclaimed yet. Both engines are log-structured - with a 50% minimum-utilisation cleaner: HStore's `compaction_live_ratio = 0.5`, JE's - `je.cleaner.minUtilization = 50`. A sealed HStore segment is only compacted once fewer than half of its - images are live, and the background pass runs after every `checkpoint_wal_mb` of writes - ([maintenance.md](storage/maintenance.md)). Raising `compaction_live_ratio` trades write amplification for - space. -* **Crash recovery since storage format 7 (`recover`, not in the tables above).** Recovery now replays each - commit's changed keys onto the trees of the last checkpoint, which takes more CPU than pointing the roots at - pages that were already written. On a busy laptop at scale 1, the median went from 90 ms on format 6 to - 171 ms, against 123 ms for HyperGraphDB. That hasn't been rerun on an idle machine yet. +* **Opening a crashed store at scale 1 (0.59× with `async`, 0.69× with `sync`).** HyperGraphDB opened the + crashed store in 76 ms against HStore's 128 ms, and the ranges don't overlap. HStore's time is close to its + clean reopen of the full dataset (111 ms), and it allocates 121 MiB while opening, against 78 MiB for + HyperGraphDB. At scale 4 the order reverses: 219 ms for HStore against 455 ms. + +**Where the two are close.** + +* **Single-update commits at scale 1.** With `async` durability HyperGraphDB is slightly ahead at the median + (0.88×, 52 µs against 45 µs, ranges overlapping), and HStore is ahead at p99 and above. With `sync` + durability (1.09×) both stores pay about 4 ms for the forced write of each commit on this machine, and + HStore's p99.9 is 1.5× lower. JE reports 497 KiB written per commit in `sync` mode, against 6.9 KiB with + `async`; the extra bytes come with the log flush the adapter adds (see *Setup*), and we haven't established + what JE writes, so don't read the `sync` bytes-written figures for single commits as a comparison. +* **Bulk node and hyperedge ingest at scale 1 with `sync` (1.81× and 1.25×), and node ingest at scale 4 + (1.39×).** The ranges overlap in all three. +* **Crash safety.** Neither store lost an acknowledged commit in any run. + +**What HStore spends.** It retains more heap after ingest (190 MiB against 118 MiB at scale 1, 430 MiB against +299 MiB at scale 4), because the retained heap includes up to `write_buffer_mb` of nodes no checkpoint has +written yet. After a large delete, its files grow until the retained history stops referencing the replaced +pages: at scale 4 they reached 1.21 GiB before compaction brought them back to 164.5 MiB. At scale 4 its +single-update commits are faster at every percentile, but the slowest single commit across all runs was +HStore's: 30.5 ms against 18.8 ms. + +### Decoding leaf records on demand + +Storage format 8 writes each record of a catalog or incidence index leaf with its length in front, and a leaf +loaded from a page keeps the page bytes and decodes a record the first time it is read +([pages.md](storage/pages.md), [persistent-tree.md](storage/persistent-tree.md)). Before, a cache miss decoded +every record of the leaf, about 80 of them with their inline member lists, to return one. A fully decoded +catalog leaf took about 11 times the heap of its page image, and at scale 4 only about half the catalog fit in +the 64 MiB node cache. + +The table compares HStore at scale 4 (`async`, `CACHE_MB=64`, three runs each) at commit `459b8be`, which +decoded whole leaves, and at commit `9291e18`. The two sets of runs were made at different times with +different machine load, so treat the throughput columns as indicative. Allocation and bytes read per +operation are steady from run to run. + +| Workload | Before | After | Allocated per op | Read from storage per op | +|---|---:|---:|---:|---:| +| `read.members` | 218,369 ops/s | 5,575,872 ops/s | 9,038 B → 0 B | 778 B → 0 B | +| `read.twohop` | 15,008 ops/s | 311,293 ops/s | 133,848 B → 1,183 B | 12,464 B → 278 B | +| `read.incidence.cold` | 346,015 ops/s | 1,163,030 ops/s | 1,024 B → 595 B | 110 B → 113 B | + +The length in front of each record costs about one byte, so the files grow slightly: 165.1 MiB against +165.7 MiB at scale 4, and 91.3 MiB against 92.2 MiB at scale 1. At scale 1, three alternated runs of each +build moved no workload by more than the spread between runs, apart from incidence reads from 8 threads, +which were 1.28× faster. ### Inline member trees @@ -310,10 +415,9 @@ when it is read instead of being kept in the cached leaf, about 64 bytes per loo ``` benchmarks/install-hypergraphdb.sh # clone, compile and install HyperGraphDB 99485a1 and JE 5.0.73 -SCALE=1 DURABILITY=async benchmarks/run.sh -SCALE=1 DURABILITY=sync benchmarks/run.sh -SCALE=4 DURABILITY=async benchmarks/run.sh -SCALE=4 DURABILITY=async HISTORY=1 benchmarks/run.sh +SCALE=1 DURABILITY=async CACHE_MB=64 THREADS=8 RUNS=5 benchmarks/run.sh +SCALE=1 DURABILITY=sync CACHE_MB=64 THREADS=8 RUNS=5 benchmarks/run.sh +SCALE=4 DURABILITY=async CACHE_MB=64 THREADS=8 RUNS=3 benchmarks/run.sh ``` `run.sh` builds the `benchmarks` module (`-Pbenchmarks`) and runs each store in its own JVM, `RUNS` times diff --git a/docs/operations/docker.md b/docs/operations/docker.md index 29b5888..cfdd6cc 100644 --- a/docs/operations/docker.md +++ b/docs/operations/docker.md @@ -259,8 +259,8 @@ skipped because `FORMAT` exists. To verify a backup, run `docker run --rm -v restored:/var/lib/hstore/data hstore check /var/lib/hstore/data`. To upgrade, stop the container, back up the volume, and start the new image on the same volume. `FORMAT` -records the on-disk format version (`format=7`, commits logged as key changes) and the page size. Both are checked at +records the on-disk format version (`format=8`, leaf records framed by length) and the page size. Both are checked at open. A directory written in another format is refused with -`database … uses storage format N; this build reads format 7 (commits logged as key changes); export and reload it`, +`database … uses storage format N; this build reads format 8 (leaf records framed by length); export and reload it`, so the server never misreads it. Export the data with HQL against the old version and reload it into a fresh volume. diff --git a/docs/operations/logging.md b/docs/operations/logging.md index 1319577..cbeaf4f 100644 --- a/docs/operations/logging.md +++ b/docs/operations/logging.md @@ -115,7 +115,7 @@ and in Docker it is the last line of `docker logs`. The storage-integrity failur | Message | Meaning | |---|---| -| `database uses storage format N; this build reads format 7 (commits logged as key changes); export and reload it` | `FORMAT` was written by an incompatible build. | +| `database uses storage format N; this build reads format 8 (leaf records framed by length); export and reload it` | `FORMAT` was written by an incompatible build. | | `database uses N byte pages, options request M` | `page_size` differs from the value fixed at `init`. | | `write-ahead log segment is damaged () but later segments exist; replay would silently drop committed transactions` | `CORRUPT_LOG`: damage in a non-tail WAL segment. A torn record at the end of the *last* segment is normal after a crash. Replay simply ends there. Damage before later segments means storage corruption, so recovery refuses to guess. Restore from backup. | diff --git a/docs/storage/maintenance.md b/docs/storage/maintenance.md index 7b306d9..9b2ec34 100644 --- a/docs/storage/maintenance.md +++ b/docs/storage/maintenance.md @@ -268,6 +268,7 @@ History and pins are also what hold dead pages back. A page waits until every ge * magic, format, page identity and CRC-32C (`PageHeader.verify`); * the schema id against the tree being read (`NodeCodec.decode`); * strictly ascending keys in leaves; +* for a packed leaf, that the header's refers-to-pages flag matches its records once all of them are decoded. Loading a packed leaf skips that comparison, because it would decode every record; * separators that route each child; * uniform height (balance); * every stored child summary equal to its recomputed contents; diff --git a/docs/storage/pages.md b/docs/storage/pages.md index b239157..96322d3 100644 --- a/docs/storage/pages.md +++ b/docs/storage/pages.md @@ -76,6 +76,8 @@ zigzag timeMax i64le roleBits keys: zigzag keys[0], then varint (keys[i] − keys[i−1]) for i = 1..slotCount−1 LEAF: schema.codec().encode(out, keys, values) (value format owned by the schema) + or, for a codec with independentValues(): + (varint length | value) × slotCount (catalog, posting lists, row codecs) INTERNAL: Ref × slotCount Ref := u8 0 empty tree | u8 1 | i64le pageId | varint units | Summary stored tree @@ -88,6 +90,8 @@ For an internal node, "keys" are the separators. `separators[0]` is the first ch **Reference encoding** (`tree/Ref.java`). Every reference starts with a tag byte. `0` is the empty tree and nothing follows. `1` is a stored tree: the page number, the image size in 64-byte units and the subtree's summary. `2` is an inline tree: the entry count `n` (1 to 32), then the leaf exactly as a leaf page payload encodes it, keys as a zigzag first key and varint gaps followed by `schema.codec().encode(...)`. There is no summary, because the reader recomputes it from the entries. An inline tree can only be decoded where the reader knows its schema, through `Ref.read(in, schema)`. `AtomCodec` passes the member schema of the edge's kind for `members` and `order-index` for `order`. The tree changes in a WAL `Changes` record are read with their schema as well, so a nested tree change can carry an inline tree. Everywhere else (branch children, catalog images, posting and property directories) `Ref.read(in)` rejects tag `2`, since only nested references in codecs that opt in with `inlinesSmallTrees()` are ever inline. An inline reference is never larger than `Ref.maxEncodedSize()` (108 bytes); see [persistent-tree.md](persistent-tree.md#inline-trees) for why that keeps pages from overflowing. +**Framed values.** A codec whose values are written one after another, each without reference to the others, says so with `ValueCodec.independentValues()`: `AtomCodec` (the catalog), `PostingsCodec` (posting directories) and every `ValueCodec.rows` codec. In their leaves `NodeCodec` writes each value's length in front of it, so a reader can find one value without decoding the ones before it. The length is counted in `TreeSchema.entryBytes`, so the leaf budget still keeps every image within a page. Columnar codecs, such as the member and incidence codecs, keep the plain layout. When a framed leaf is read from a page, `NodeCodec.decode` walks the lengths to record where each value starts, rejects the image if a value runs past the payload or the values stop short of its end, and returns a packed leaf ([caching](#nodecache-treenodecachejava)). An inline tree of a framed codec uses the same layout and is decoded in full. + An edge record with three plain members `{1000, 1001, 1005}` holds this as its `members` reference. It takes 7 bytes. A stored reference to the same tree would take 65 bytes, most of them summary, and point at an image of at least 128: ```text @@ -346,10 +350,10 @@ and the reference journal. ### `NodeCache` (`tree/NodeCache.java`) -The cache stores **decoded, frozen** `Node` objects, not bytes. A hit therefore costs no decoding and no allocation. The cache has 16 shards; a node goes to shard `mix(pageId) >>> 60`. Each shard has: +The cache stores **decoded, frozen** `Node` objects, not bytes. A hit therefore costs no decoding and no allocation. The exception is a leaf of framed values, which stays **packed**: it holds the page image, the decoded keys and the offset of every value, and decodes a value the first time it is read (`Leaf.value`). The decoded value is kept in the leaf, published with a compare-and-set so another thread sees it whole, and two threads that decode the same value at once keep whichever got there first. A catalog lookup on a cold page therefore decodes the keys and one record instead of every record and every inline member list on the page. The cache has 16 shards; a node goes to shard `mix(pageId) >>> 60`. Each shard has: * an open-addressing table of `Slot(pageId, node, volatile referenced)`, probed linearly from `mix(pageId)`, so a lookup needs no boxed key. It is kept at most half full, counting removed slots, and is rebuilt into a new array when it would pass that; -* a byte budget of `max(64 KiB, node_cache_mb / 16)` and a circular, doubly linked ring of slots with a CLOCK hand. Each slot weighs `Node.heapBytes()`, an estimate of the decoded node's heap size: 144 bytes for the node and its summary, its key and value or child arrays, then 96 bytes plus the encoded value per leaf entry, or 128 bytes per child reference in a branch. A leaf whose values are implied by its keys or member ids counts only its `long` arrays, and a catalog leaf adds the estimate of every inline tree it holds. Those constants come from measuring retained heap against cache contents at two dataset sizes; +* a byte budget of `max(64 KiB, node_cache_mb / 16)` and a circular, doubly linked ring of slots with a CLOCK hand. Each slot weighs `Node.heapBytes()`, an estimate of the decoded node's heap size: 144 bytes for the node and its summary, its key and value or child arrays, then 96 bytes plus the encoded value per leaf entry, or 128 bytes per child reference in a branch. A leaf whose values are implied by its keys or member ids counts only its `long` arrays, and a catalog leaf adds the estimate of every inline tree it holds. A packed leaf starts at its image size plus its key and offset arrays. Each value it decodes adds 96 bytes plus the encoded value and any inline tree in it, and charges that to the leaf's slot (`Node.chargeTo`), which evicts under the shard lock until the shard is back within budget. A charge for a slot that was evicted, replaced or cleared is dropped. Those constants come from measuring retained heap against cache contents at two dataset sizes; * a `ReentrantLock` taken only for admission. **Reads are lock-free.** `get` probes the table, reading each slot with acquire ordering, plus setting `referenced = true` if it was clear. Writers change the table only under the shard lock, publishing each slot with release ordering, and a rebuild publishes the new array through a volatile field. A reader still probing the old array can find a node that was just evicted; that is harmless, because a page number's contents never change. The check avoids a redundant volatile write on hot entries. @@ -390,7 +394,7 @@ Because images are never overwritten in place, a torn write can only damage an i | Version | Where | Meaning | |---|---|---| -| `format=7` in `/FORMAT` | `StorageEngine.verifyFormat` | Commits logged as key changes (`Changes` WAL records), tree pages written at checkpoints, no history in catalog images. Also tagged references with small member and order trees inline in their edge record, reference counts in `pages.ref`, page numbers resolved through `pages.dir`, images packed in 64-byte units, image sizes in every stored reference. Directories written by older builds (`format=1` fixed page slots, `format=2` references without sizes, `format=3` references by address, `format=4` no reference counts, `format=5` untagged references with every nested tree on its own page, `format=6` commits that logged their pages) fail to open: `uses storage format 6; this build reads format 7 (commits logged as key changes); export and reload it`. | +| `format=8` in `/FORMAT` | `StorageEngine.verifyFormat` | Leaf values of codecs with independent values framed by their length. Also commits logged as key changes (`Changes` WAL records), tree pages written at checkpoints, no history in catalog images, tagged references with small member and order trees inline in their edge record, reference counts in `pages.ref`, page numbers resolved through `pages.dir`, images packed in 64-byte units, image sizes in every stored reference. Directories written by older builds (`format=1` fixed page slots, `format=2` references without sizes, `format=3` references by address, `format=4` no reference counts, `format=5` untagged references with every nested tree on its own page, `format=6` commits that logged their pages, `format=7` unframed leaf values) fail to open: `uses storage format 7; this build reads format 8 (leaf records framed by length); export and reload it`. | | `page-size=N` in `/FORMAT` | same | Fixed at creation. A mismatching `--page_size` is rejected. | | header byte 4 = `1` | `PageHeader.FORMAT` | Node image layout; unchanged by packing and by page numbers. | | `pages.dir` header | `PageDirectory` | `i32 magic 0x52494450`, `i32 format 1`. | diff --git a/docs/storage/persistent-tree.md b/docs/storage/persistent-tree.md index d29de88..a0e1d4d 100644 --- a/docs/storage/persistent-tree.md +++ b/docs/storage/persistent-tree.md @@ -204,7 +204,7 @@ An inline leaf is frozen (`owner == null`), so a writer copies it before changin **Equality.** `Ref.same` compares stored references by page id and pending ones by identity. A pending node stamped by a published epoch is also the same as the reference it was written as, and two such nodes are the same when they were written as the same reference. That matters to a transaction that began before a checkpoint: the checkpoint swaps the head generation's pending nodes for stored references, and without this rule every tree the transaction touched would look changed. Inline references have no identity worth comparing: two reads of the same catalog page decode two separate leaves. They are compared by content instead. Two inline references are the same when they belong to the same schema and have the same keys and equal values (`Leaf.sameEntries`). The same content test applies between a pending leaf and an inline reference, and between two pending leaves, when both are small enough that a checkpoint would store them inline (`Ref.fitsInline`). `Ref.Inline.equals` uses the content test, so `EdgeRecord.equals` compares inline member trees by content too. This is what the conflict checks in `Transaction` and `Op`, the shortcuts in `TreeAlgebra` and `TreeDiff`, and the read validation of serializable transactions rely on. A stored and an inline reference are never the same, even with equal entries. Every caller treats "not the same" as "look closer", so that only costs a slower path. -**Heap.** The node cache weighs a catalog leaf with `Leaf.heapBytes()`, which adds the heap estimate of every inline leaf it holds, so inline trees count against `node_cache_mb` like any other node. +**Heap.** The node cache weighs a catalog leaf with `Leaf.heapBytes()`, which adds the heap estimate of every inline leaf it holds, so inline trees count against `node_cache_mb` like any other node. A catalog leaf read from a page is packed: it decodes a record, and the inline trees in it, only when that record is read, and charges the cache as it does ([pages.md](pages.md#nodecache-treenodecachejava)). ## Reading diff --git a/docs/transactions/wal-and-recovery.md b/docs/transactions/wal-and-recovery.md index f117699..dd16a69 100644 --- a/docs/transactions/wal-and-recovery.md +++ b/docs/transactions/wal-and-recovery.md @@ -27,7 +27,7 @@ Related: [transactions](transactions.md), [change feed](../storage/change-feed.m ```text / - FORMAT properties: format=7, page-size= + FORMAT properties: format=8, page-size= LOCK exclusive FileLock held while open catalog/manifest name of the current catalog image file catalog/generations/%016x.cat checkpointed catalog images (3 newest retained) diff --git a/engine/src/main/java/io/hstore/engine/StorageEngine.java b/engine/src/main/java/io/hstore/engine/StorageEngine.java index 964fdcc..f44a56e 100644 --- a/engine/src/main/java/io/hstore/engine/StorageEngine.java +++ b/engine/src/main/java/io/hstore/engine/StorageEngine.java @@ -44,7 +44,7 @@ public final class StorageEngine implements AutoCloseable { private static final System.Logger LOG = System.getLogger("hstore.engine"); - private static final String FORMAT_VERSION = "7"; + private static final String FORMAT_VERSION = "8"; private static final int CHANGED_COUNTS_LIMIT = 1 << 20; private static final int MAX_ATTEMPTS = 8; @@ -135,7 +135,7 @@ private static void verifyFormat(Path directory, EngineOptions options) throws I } if (!format.getProperty("format", "1").equals(FORMAT_VERSION)) { throw HStoreException.invalid("database " + directory + " uses storage format " + format.getProperty("format", "1") - + "; this build reads format " + FORMAT_VERSION + " (commits logged as key changes); export and reload it"); + + "; this build reads format " + FORMAT_VERSION + " (leaf records framed by length); export and reload it"); } int stored = Integer.parseInt(format.getProperty("page-size")); if (stored != options.pageSize()) { diff --git a/engine/src/main/java/io/hstore/engine/catalog/AtomCodec.java b/engine/src/main/java/io/hstore/engine/catalog/AtomCodec.java index 71e7679..f403a0d 100644 --- a/engine/src/main/java/io/hstore/engine/catalog/AtomCodec.java +++ b/engine/src/main/java/io/hstore/engine/catalog/AtomCodec.java @@ -79,6 +79,11 @@ public boolean holdsRefs() { return true; } + @Override + public boolean independentValues() { + return true; + } + @Override public boolean inlinesSmallTrees() { return true; diff --git a/engine/src/main/java/io/hstore/engine/index/PostingsCodec.java b/engine/src/main/java/io/hstore/engine/index/PostingsCodec.java index f440431..0aab2e2 100644 --- a/engine/src/main/java/io/hstore/engine/index/PostingsCodec.java +++ b/engine/src/main/java/io/hstore/engine/index/PostingsCodec.java @@ -97,6 +97,11 @@ public boolean holdsRefs() { return true; } + @Override + public boolean independentValues() { + return true; + } + @Override public boolean refersToPages(Postings

value) { return value instanceof Postings.Promoted

(Ref root) && root instanceof Ref.Stored; diff --git a/engine/src/main/java/io/hstore/engine/tree/Leaf.java b/engine/src/main/java/io/hstore/engine/tree/Leaf.java index 1da4499..9c31d1d 100644 --- a/engine/src/main/java/io/hstore/engine/tree/Leaf.java +++ b/engine/src/main/java/io/hstore/engine/tree/Leaf.java @@ -5,30 +5,67 @@ import io.hstore.engine.page.ByteCursor; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; import java.util.AbstractList; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.function.IntConsumer; import java.util.function.LongConsumer; public final class Leaf extends Node { private static final int ENTRY_HEAP_BYTES = 96; + private static final Object ABSENT = new Object(); + private static final VarHandle UNPACKED_VALUES = MethodHandles.arrayElementVarHandle(Object[].class); + private static final VarHandle UNPACKED_BYTES; + + static { + try { + UNPACKED_BYTES = MethodHandles.lookup().findVarHandle(Leaf.class, "unpackedBytes", int.class); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } private long[] keys; private Object[] values; private final long[] words; + private final MemorySegment packed; + private final int[] offsets; + private final boolean flaggedAsReferring; private int size; private int keyBytes; private int valueBytes; + private volatile boolean measured; + private volatile int unpackedBytes; + private volatile IntConsumer charge; private Leaf(TreeSchema schema, Object owner, long[] keys, Object[] values, long[] words, int size, Summary summary) { super(schema, owner, summary); this.keys = keys; this.values = values; this.words = words; + this.packed = null; + this.offsets = null; + this.flaggedAsReferring = false; this.size = size; - remeasure(); + if (owner != null) { + remeasure(); + } + } + + private Leaf(TreeSchema schema, long[] keys, MemorySegment packed, int[] offsets, boolean flaggedAsReferring, Summary summary) { + super(schema, null, summary); + this.keys = keys; + this.values = new Object[keys.length]; + this.words = null; + this.packed = packed; + this.offsets = offsets; + this.flaggedAsReferring = flaggedAsReferring; + this.size = keys.length; } private Leaf(TreeSchema schema, Object owner, long[] keys, Object[] values, int size, Summary summary) { @@ -47,6 +84,14 @@ static Leaf implied(TreeSchema schema, long[] keys, long[] words, Summary sum return new Leaf(schema, null, keys, null, words, keys.length, summary); } + static Leaf packed(TreeSchema schema, long[] keys, MemorySegment page, int[] offsets, boolean flaggedAsReferring, Summary summary) { + return new Leaf(schema, keys, page, offsets, flaggedAsReferring, summary); + } + + boolean pageFlagMatches() { + return packed == null || flaggedAsReferring == NodeCodec.refersToPages(this); + } + static int keyStreamBytes(long[] keys, int size) { if (size == 0) { return 0; @@ -63,33 +108,58 @@ private static int gap(long low, long high) { } private void remeasure() { - keyBytes = keyStreamBytes(keys, size); - valueBytes = 0; + int valueTotal = 0; for (int i = 0; i < size; i++) { - valueBytes += values != null ? schema.entryBytes(values[i]) : schema.impliedBytes(words[i]); + valueTotal += values != null ? schema.entryBytes(peek(i)) : schema.impliedBytes(words[i]); + } + keyBytes = keyStreamBytes(keys, size); + valueBytes = valueTotal; + measured = true; + } + + private void measure() { + if (!measured) { + remeasure(); + } + } + + @Override + void chargeTo(IntConsumer charge) { + if (packed != null) { + this.charge = charge; } } @Override int heapBytes() { + if (packed != null) { + return NODE_HEAP_BYTES + 3 * ARRAY_HEAP_BYTES + Long.BYTES * keys.length + Integer.BYTES * (values.length + offsets.length) + + Math.toIntExact(packed.byteSize()) + unpackedBytes; + } if (values == null) { return NODE_HEAP_BYTES + 2 * ARRAY_HEAP_BYTES + Long.BYTES * keys.length + (words == keys ? 0 : Long.BYTES * words.length); } + measure(); int bytes = NODE_HEAP_BYTES + 2 * ARRAY_HEAP_BYTES + Long.BYTES * keys.length + Integer.BYTES * values.length + ENTRY_HEAP_BYTES * size + valueBytes; return schema.codec().holdsRefs() ? bytes + inlineHeapBytes(schema) : bytes; } private int inlineHeapBytes(TreeSchema typed) { + int total = 0; + for (int i = 0; i < size; i++) { + total += inlineHeapBytes(typed, value(i)); + } + return total; + } + + private static int inlineHeapBytes(TreeSchema typed, Object value) { int[] total = new int[1]; - ValueCodec.RefVisitor visitor = (ref, _) -> { + typed.codec().forEachRef(typed.cast(value), (ref, _) -> { if (ref instanceof Ref.Inline(Leaf inline)) { total[0] += inline.heapBytes(); } - }; - for (int i = 0; i < size; i++) { - typed.codec().forEachRef(typed.cast(value(i)), visitor); - } + }); return total[0]; } @@ -148,10 +218,55 @@ long[] impliedWords() { Object value(int index) { Object[] stored = values; - return stored != null ? stored[index] : schema.implied(words[index]); + if (stored == null) { + return schema.implied(words[index]); + } + if (packed == null) { + return stored[index]; + } + Object unpacked = UNPACKED_VALUES.getAcquire(stored, index); + if (unpacked == null) { + unpacked = unpack(stored, index); + } + return unpacked == ABSENT ? null : unpacked; + } + + private Object peek(int index) { + if (packed == null) { + return value(index); + } + Object unpacked = UNPACKED_VALUES.getAcquire(values, index); + if (unpacked == null) { + return decode(index); + } + return unpacked == ABSENT ? null : unpacked; + } + + private Object decode(int index) { + Object[] decoded = new Object[1]; + schema.codec().decode(ByteCursor.over(packed).position(offsets[index]), new long[]{keys[index]}, decoded); + return decoded[0]; + } + + private Object unpack(Object[] stored, int index) { + Object[] decoded = {decode(index)}; + Object value = decoded[0] == null ? ABSENT : decoded[0]; + Object raced = UNPACKED_VALUES.compareAndExchangeRelease(stored, index, null, value); + if (raced != null) { + return raced; + } + int grown = ENTRY_HEAP_BYTES + schema.entryBytes(decoded[0]) + + (schema.codec().holdsRefs() ? inlineHeapBytes(schema, decoded[0]) : 0); + UNPACKED_BYTES.getAndAdd(this, grown); + IntConsumer target = charge; + if (target != null) { + target.accept(grown); + } + return value; } int bytes() { + measure(); return keyBytes + valueBytes + schema.leafOverhead(size); } @@ -195,17 +310,18 @@ Leaf frozenWith(Object[] mappedValues) { } private Object[] valueArray(int capacity) { - if (values != null) { + if (values != null && packed == null) { return Arrays.copyOf(values, capacity); } Object[] copy = new Object[capacity]; for (int i = 0; i < size; i++) { - copy[i] = value(i); + copy[i] = peek(i); } return copy; } void insert(int position, long key, Object value) { + measure(); if (size == keys.length) { int capacity = Math.max(8, size + (size >> 1)); keys = Arrays.copyOf(keys, capacity); @@ -235,12 +351,14 @@ private int insertionDelta(int position, long key) { } void replace(int position, Object value) { + measure(); valueBytes += schema.entryBytes(value) - schema.entryBytes(values[position]); values[position] = value; invalidate(); } void remove(int position) { + measure(); long key = keys[position]; valueBytes -= schema.entryBytes(values[position]); System.arraycopy(keys, position + 1, keys, position, size - position - 1); diff --git a/engine/src/main/java/io/hstore/engine/tree/Node.java b/engine/src/main/java/io/hstore/engine/tree/Node.java index 0416ca3..7a66b9b 100644 --- a/engine/src/main/java/io/hstore/engine/tree/Node.java +++ b/engine/src/main/java/io/hstore/engine/tree/Node.java @@ -3,6 +3,8 @@ package io.hstore.engine.tree; +import java.util.function.IntConsumer; + public sealed abstract class Node permits Leaf, Branch { static final int NODE_HEAP_BYTES = 144; @@ -66,4 +68,7 @@ final boolean writableBy(WriteScope scope) { abstract int size(); abstract int heapBytes(); + + void chargeTo(IntConsumer charge) { + } } diff --git a/engine/src/main/java/io/hstore/engine/tree/NodeCache.java b/engine/src/main/java/io/hstore/engine/tree/NodeCache.java index b9997fb..85ed75e 100644 --- a/engine/src/main/java/io/hstore/engine/tree/NodeCache.java +++ b/engine/src/main/java/io/hstore/engine/tree/NodeCache.java @@ -69,21 +69,23 @@ private Shard shard(long pageId) { private static final class Slot { final long pageId; final Node node; - final int bytes; + final int generation; + int bytes; volatile boolean referenced; Slot previous; Slot next; - Slot(long pageId, Node node, int bytes) { + Slot(long pageId, Node node, int bytes, int generation) { this.pageId = pageId; this.node = node; this.bytes = bytes; + this.generation = generation; } } private static final class Shard { private static final int MIN_TABLE = 64; - private static final Slot GONE = new Slot(0, null, 0); + private static final Slot GONE = new Slot(0, null, 0, -1); private static final VarHandle SLOTS = MethodHandles.arrayElementVarHandle(Slot[].class); private final ReentrantLock admission = new ReentrantLock(); @@ -92,6 +94,7 @@ private static final class Shard { private volatile long bytes; private int live; private int gone; + private int generation; private Slot hand; Shard(long budget) { @@ -190,9 +193,26 @@ void put(long pageId, Node node, int weight) { while (bytes + weight > budget) { evict(); } - Slot slot = new Slot(pageId, node, weight); + Slot slot = new Slot(pageId, node, weight, generation); link(slot); insert(slot); + node.chargeTo(grown -> charge(slot, grown)); + } finally { + admission.unlock(); + } + } + + private void charge(Slot slot, int grown) { + admission.lock(); + try { + if (slot.next == null || slot.generation != generation) { + return; + } + slot.bytes += grown; + bytes += grown; + while (bytes > budget && hand != null) { + evict(); + } } finally { admission.unlock(); } @@ -243,6 +263,7 @@ void clear() { table = new Slot[MIN_TABLE]; live = 0; gone = 0; + generation++; hand = null; bytes = 0; } finally { diff --git a/engine/src/main/java/io/hstore/engine/tree/NodeCodec.java b/engine/src/main/java/io/hstore/engine/tree/NodeCodec.java index 373db8c..17a79ac 100644 --- a/engine/src/main/java/io/hstore/engine/tree/NodeCodec.java +++ b/engine/src/main/java/io/hstore/engine/tree/NodeCodec.java @@ -9,6 +9,8 @@ import io.hstore.engine.page.PageType; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.util.Collections; import java.util.List; public final class NodeCodec { @@ -72,10 +74,51 @@ private static Leaf decodeLeaf(ByteCursor in, TreeSchema schema, long[] keys, return Leaf.implied(schema, keys, words, summary); } Object[] values = new Object[keys.length]; - schema.codec().decode(in, keys, values); + if (schema.codec().independentValues()) { + decodeFramed(in, schema, keys, values); + } else { + schema.codec().decode(in, keys, values); + } return Leaf.decoded(schema, keys, values, summary); } + private static int[] offsets(ByteCursor in, int count, PageHeader header, long pageId) { + int[] offsets = new int[count]; + long end = PageHeader.SIZE + (long) header.payloadLength(); + for (int i = 0; i < count; i++) { + long length = in.getVarLong(); + offsets[i] = Math.toIntExact(in.position()); + if (length < 0 || in.position() + length > end) { + throw HStoreException.corrupt(pageId, "value " + i + " runs past the end of the page"); + } + in.skip(length); + } + if (in.position() != end) { + throw HStoreException.corrupt(pageId, "values end at byte " + in.position() + ", not at the end of the payload " + end); + } + return offsets; + } + + private static MemorySegment owned(MemorySegment page) { + return page.isNative() ? MemorySegment.ofArray(page.toArray(ValueLayout.JAVA_BYTE)) : page; + } + + private static void decodeFramed(ByteCursor in, TreeSchema schema, long[] keys, Object[] into) { + long[] key = new long[1]; + Object[] value = new Object[1]; + for (int i = 0; i < keys.length; i++) { + long length = in.getVarLong(); + long end = in.position() + length; + key[0] = keys[i]; + schema.codec().decode(in, key, value); + if (in.position() != end) { + throw new IllegalStateException("value " + i + " of a " + schema + " leaf is " + length + " bytes long but decoded from " + + (in.position() - end + length) + " bytes"); + } + into[i] = value[0]; + } + } + private static long[] decodeKeys(ByteCursor in, int count) { long[] keys = new long[count]; keys[0] = in.getSignedVarLong(); @@ -93,10 +136,22 @@ private static void encodeValues(ByteCursor out, TreeSchema schema, long[ } @SuppressWarnings("unchecked") List values = (List) leaf.valueList(); - schema.codec().encode(out, keys, values); + if (!schema.codec().independentValues()) { + schema.codec().encode(out, keys, values); + return; + } + ByteCursor value = ByteCursor.growable(256); + long[] key = new long[1]; + for (int i = 0; i < keys.length; i++) { + value.position(0); + key[0] = keys[i]; + schema.codec().encode(value, key, Collections.singletonList(values.get(i))); + MemorySegment written = value.written(); + out.putVarLong(written.byteSize()).putSegment(written); + } } - private static boolean refersToPages(Leaf leaf) { + static boolean refersToPages(Leaf leaf) { return leaf.schema.codec().holdsRefs() && refersToPages(leaf.schema, leaf.valueList()); } @@ -137,6 +192,9 @@ public static Node decode(MemorySegment page, PageHeader header, long pageId, Tr long[] keys = decodeKeys(in, count); return switch (header.type()) { case LEAF -> { + if (schema.codec().independentValues()) { + yield Leaf.packed(schema, keys, owned(page), offsets(in, count, header, pageId), header.refersToPages(), summary); + } Leaf leaf = decodeLeaf(in, schema, keys, summary); if (header.refersToPages() != refersToPages(leaf)) { throw HStoreException.corrupt(pageId, "page header and contents disagree on whether it refers to other pages"); diff --git a/engine/src/main/java/io/hstore/engine/tree/TreeSchema.java b/engine/src/main/java/io/hstore/engine/tree/TreeSchema.java index d5a2869..bf47ed3 100644 --- a/engine/src/main/java/io/hstore/engine/tree/TreeSchema.java +++ b/engine/src/main/java/io/hstore/engine/tree/TreeSchema.java @@ -3,6 +3,8 @@ package io.hstore.engine.tree; +import io.hstore.engine.page.ByteCursor; + import java.util.List; public record TreeSchema(int id, String name, FingerprintMode fingerprint, ValueCodec codec, EntryMeasure measure) { @@ -21,7 +23,8 @@ public V cast(Object value) { } int entryBytes(Object value) { - return codec.maxSize(cast(value)); + int size = codec.maxSize(cast(value)); + return codec.independentValues() ? size + ByteCursor.varLongSize(size) : size; } Object implied(long word) { diff --git a/engine/src/main/java/io/hstore/engine/tree/TreeVerifier.java b/engine/src/main/java/io/hstore/engine/tree/TreeVerifier.java index 2714d51..2a5b1d8 100644 --- a/engine/src/main/java/io/hstore/engine/tree/TreeVerifier.java +++ b/engine/src/main/java/io/hstore/engine/tree/TreeVerifier.java @@ -60,6 +60,9 @@ private Visit visit(Ref ref, TreeSchema schema, int expectedHeight) { report = report.plus(nested(schema, leaf.value(i))); } } + if (!leaf.pageFlagMatches()) { + throw fault(ref, "page header and contents disagree on whether it refers to other pages in " + schema); + } report = report.plus(new Report(0, leaf.size(), 0, 0)); } case Branch branch -> { diff --git a/engine/src/main/java/io/hstore/engine/tree/ValueCodec.java b/engine/src/main/java/io/hstore/engine/tree/ValueCodec.java index 77171bd..e54d08c 100644 --- a/engine/src/main/java/io/hstore/engine/tree/ValueCodec.java +++ b/engine/src/main/java/io/hstore/engine/tree/ValueCodec.java @@ -47,6 +47,10 @@ default boolean holdsRefs() { return false; } + default boolean independentValues() { + return false; + } + default V mapRefs(V value, RefMapper mapper) { return value; } @@ -86,6 +90,11 @@ public void decode(ByteCursor in, long[] keys, Object[] into) { into[i] = reader.apply(in); } } + + @Override + public boolean independentValues() { + return true; + } }; } diff --git a/engine/src/test/java/io/hstore/engine/CompactionTest.java b/engine/src/test/java/io/hstore/engine/CompactionTest.java index 5e37647..d5ea5d9 100644 --- a/engine/src/test/java/io/hstore/engine/CompactionTest.java +++ b/engine/src/test/java/io/hstore/engine/CompactionTest.java @@ -73,7 +73,7 @@ void compactionMovesPagesWithoutWritingAGeneration() { @Test void retainedHistoryStaysReadableOnceItsSegmentsAreDeleted() { - EngineOptions options = options().withHistoryLimit(20); + EngineOptions options = options().withHistoryLimit(20).withCompactionLiveRatio(0.75); List written = Stream.iterate(500_000L, i -> i + 1).limit(2_000).toList(); long large; long edge; diff --git a/engine/src/test/java/io/hstore/engine/EngineTest.java b/engine/src/test/java/io/hstore/engine/EngineTest.java index 507ab8c..e951cb6 100644 --- a/engine/src/test/java/io/hstore/engine/EngineTest.java +++ b/engine/src/test/java/io/hstore/engine/EngineTest.java @@ -614,10 +614,10 @@ void dictionaryInternsRolesAcrossRestarts() { } @Test - void directoriesFromTheInlineMemberFormatAreRefused() throws IOException { - Files.writeString(directory.resolve("FORMAT"), "format=6\npage-size=1024\n"); + void directoriesFromBeforeFramedLeafRecordsAreRefused() throws IOException { + Files.writeString(directory.resolve("FORMAT"), "format=7\npage-size=1024\n"); HStoreException refused = assertThrows(HStoreException.class, () -> StorageEngine.open(directory, small()).close()); - assertTrue(refused.getMessage().endsWith("uses storage format 6; this build reads format 7 (commits logged as key changes); export and reload it"), + assertTrue(refused.getMessage().endsWith("uses storage format 7; this build reads format 8 (leaf records framed by length); export and reload it"), refused.getMessage()); } } diff --git a/engine/src/test/java/io/hstore/engine/tree/MemoryPages.java b/engine/src/test/java/io/hstore/engine/tree/MemoryPages.java index 679ca5c..118b507 100644 --- a/engine/src/test/java/io/hstore/engine/tree/MemoryPages.java +++ b/engine/src/test/java/io/hstore/engine/tree/MemoryPages.java @@ -37,6 +37,10 @@ public void accept(long pageId, MemorySegment image, Node frozen) { return tree.withRoot(materializer.materialize(tree.root(), tree.schema())); } + MemorySegment image(long pageId) { + return pages.get(pageId); + } + @Override public Node load(long pageId, TreeSchema schema) { loads++; diff --git a/engine/src/test/java/io/hstore/engine/tree/PackedLeafTest.java b/engine/src/test/java/io/hstore/engine/tree/PackedLeafTest.java new file mode 100644 index 0000000..5eb195b --- /dev/null +++ b/engine/src/test/java/io/hstore/engine/tree/PackedLeafTest.java @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: Metacog Labs +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 + +package io.hstore.engine.tree; + +import io.hstore.engine.HStoreException; +import io.hstore.engine.catalog.AtomRecord; +import io.hstore.engine.catalog.EngineSlots; +import io.hstore.engine.page.ByteCursor; +import io.hstore.engine.page.PageHeader; +import io.hstore.engine.topology.EdgeKind; +import io.hstore.engine.topology.Incidence; +import io.hstore.engine.topology.TopologySchemas; + +import org.junit.jupiter.api.Test; + +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.random.RandomGenerator; +import java.util.random.RandomGeneratorFactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PackedLeafTest { + + private static final int PAGE = 16 * 1024; + private static final int RECORDS = 60; + + private final MemoryPages pages = new MemoryPages(PAGE); + private final List expected = new ArrayList<>(); + private final Ref root; + private final long rootPage; + + PackedLeafTest() { + WriteScope scope = new WriteScope(); + Tree catalog = Tree.empty(EngineSlots.ATOM_SCHEMA, pages); + for (int atom = 0; atom < RECORDS; atom++) { + AtomRecord record = atom % 3 == 0 ? AtomRecord.NodeRecord.of(1, "node-" + atom) : edge(scope, atom); + catalog = catalog.put(scope, atom, record); + } + Tree written = pages.persist(catalog); + root = written.root(); + rootPage = ((Ref.Stored) root).pageId(); + written.stream().forEach(entry -> expected.add(entry.value())); + } + + private AtomRecord edge(WriteScope scope, int atom) { + Tree members = Tree.empty(TopologySchemas.SET_MEMBERS, pages); + for (long member = 1; member <= 1 + atom % 6; member++) { + members = members.put(scope, 1_000L * atom + member, Incidence.of(1_000L * atom + member)); + } + return new AtomRecord.EdgeRecord(2, EdgeKind.SET, members.root(), Ref.EMPTY, atom, 0, 0, 0); + } + + private Leaf load() { + return (Leaf) NodeCodec.decode(pages.image(rootPage), rootPage, EngineSlots.ATOM_SCHEMA); + } + + @Test + void aPackedLeafDecodesEachRecordOnlyWhenItIsRead() { + Leaf leaf = load(); + assertEquals(RECORDS, leaf.size()); + int packedOnly = leaf.heapBytes(); + assertEquals(expected.get(17), leaf.value(17)); + int oneRecord = leaf.heapBytes(); + assertTrue(oneRecord > packedOnly); + assertEquals(expected.get(17), leaf.value(17)); + assertEquals(oneRecord, leaf.heapBytes(), "a record read twice is unpacked once"); + for (int i = 0; i < RECORDS; i++) { + assertEquals(expected.get(i), leaf.value(i)); + } + Leaf eager = Leaf.decoded(EngineSlots.ATOM_SCHEMA, leaf.keyArray(), expected.toArray(), null); + assertTrue(leaf.sameEntries(eager)); + assertEquals(eager.bytes(), leaf.bytes()); + } + + @Test + void aWriterCopiesAPackedLeafWithoutKeepingOrChargingItsRecords() { + Leaf leaf = load(); + int packedOnly = leaf.heapBytes(); + AtomicLong charged = new AtomicLong(); + leaf.chargeTo(charged::addAndGet); + Leaf copy = leaf.copyFor(new WriteScope()); + for (int i = 0; i < RECORDS; i++) { + assertEquals(expected.get(i), copy.value(i)); + } + assertEquals(leaf.bytes(), copy.bytes()); + assertEquals(packedOnly, leaf.heapBytes()); + assertEquals(0, charged.get()); + } + + @Test + void threadsReadingOneLeafSeeTheSameRecordsAndChargeEachOnce() throws Exception { + Leaf leaf = load(); + int packedOnly = leaf.heapBytes(); + AtomicLong charged = new AtomicLong(); + leaf.chargeTo(charged::addAndGet); + List> readers = new ArrayList<>(); + try (ExecutorService pool = Executors.newFixedThreadPool(8)) { + for (int thread = 0; thread < 8; thread++) { + RandomGenerator random = RandomGeneratorFactory.of("L64X128MixRandom").create(thread); + readers.add(pool.submit(() -> { + for (int round = 0; round < 2_000; round++) { + int i = random.nextInt(RECORDS); + assertEquals(expected.get(i), leaf.value(i)); + } + })); + } + for (Future reader : readers) { + reader.get(); + } + } + for (int i = 0; i < RECORDS; i++) { + leaf.value(i); + } + assertEquals(leaf.heapBytes() - packedOnly, charged.get()); + } + + @Test + void theCacheCountsRecordsAsTheyAreUnpackedAndStaysWithinBudget() { + Leaf leaf = load(); + NodeCache cache = new NodeCache(1 << 20, PAGE); + cache.put(rootPage, leaf, leaf.heapBytes()); + long before = cache.residentBytes(); + for (int i = 0; i < RECORDS; i++) { + leaf.value(i); + } + assertTrue(cache.residentBytes() > before); + assertEquals(leaf.heapBytes(), cache.residentBytes()); + + Leaf small = load(); + NodeCache tight = new NodeCache(small.heapBytes() + 2_000, PAGE); + tight.put(rootPage, small, small.heapBytes()); + for (int i = 0; i < RECORDS; i++) { + small.value(i); + assertTrue(tight.residentBytes() <= small.heapBytes() + 2_000, "resident " + tight.residentBytes()); + } + assertEquals(0, tight.residentBytes(), "the grown leaf no longer fits and is evicted"); + } + + @Test + void chargesForAnEvictedOrClearedLeafAreIgnored() { + NodeCache cache = new NodeCache(1 << 20, PAGE); + Leaf evicted = load(); + cache.put(rootPage, evicted, evicted.heapBytes()); + cache.put(rootPage, load(), 100); + long resident = cache.residentBytes(); + evicted.value(3); + assertEquals(resident, cache.residentBytes()); + + Leaf cleared = load(); + cache.put(rootPage + 1, cleared, cleared.heapBytes()); + cache.clear(); + cleared.value(4); + assertEquals(0, cache.residentBytes()); + } + + @Test + void checkingTheTreeFindsAPackedLeafWhoseHeaderClaimsPageReferencesItLacks() { + MemorySegment image = pages.image(rootPage); + PageHeader header = PageHeader.verify(image, rootPage); + assertTrue(!header.refersToPages()); + new TreeVerifier(pages).verify(root, EngineSlots.ATOM_SCHEMA); + image.set(ValueLayout.JAVA_BYTE, 7, (byte) (image.get(ValueLayout.JAVA_BYTE, 7) | 1)); + PageHeader.seal(image, header.payloadLength()); + RuntimeException fault = assertThrows(RuntimeException.class, () -> new TreeVerifier(pages).verify(root, EngineSlots.ATOM_SCHEMA)); + assertTrue(fault.getMessage().contains("disagree on whether it refers to other pages"), fault.getMessage()); + } + + @Test + void aLengthThatDoesNotMatchTheRecordsIsReportedAsCorruption() { + MemorySegment image = pages.image(rootPage); + PageHeader header = PageHeader.verify(image, rootPage); + ByteCursor in = ByteCursor.over(image).position(PageHeader.SIZE); + for (int field = 0; field < 5; field++) { + in.getSignedVarLong(); + } + in.getLong(); + in.getSignedVarLong(); + for (int key = 1; key < header.slotCount(); key++) { + in.getVarLong(); + } + long first = in.position(); + MemorySegment corrupt = MemorySegment.ofArray(image.toArray(ValueLayout.JAVA_BYTE)); + byte length = corrupt.get(ValueLayout.JAVA_BYTE, first); + assertNotEquals(0, length); + corrupt.set(ValueLayout.JAVA_BYTE, first, (byte) (length + 1)); + PageHeader.seal(corrupt, header.payloadLength()); + assertThrows(HStoreException.class, () -> NodeCodec.decode(corrupt, rootPage, EngineSlots.ATOM_SCHEMA)); + } +} diff --git a/engine/src/test/java/io/hstore/engine/tree/TreeTest.java b/engine/src/test/java/io/hstore/engine/tree/TreeTest.java index abc1695..52dbcfc 100644 --- a/engine/src/test/java/io/hstore/engine/tree/TreeTest.java +++ b/engine/src/test/java/io/hstore/engine/tree/TreeTest.java @@ -406,6 +406,10 @@ void materializingAnEdgeRecordInlinesSmallTreesWithTheSameSummary() { assertEquals(edge.contentHash(), inlined.contentHash()); assertEquals(small.stream().toList(), new Tree<>(TopologySchemas.SET_MEMBERS, pages, inlined.members()).stream().toList()); Leaf root = (Leaf) written.node(written.root()); + int packedOnly = root.heapBytes(); + assertEquals(inlined, root.value(0)); + assertEquals(paged, root.value(1)); + assertTrue(root.heapBytes() > packedOnly); Leaf bare = Leaf.decoded(EngineSlots.ATOM_SCHEMA, new long[]{1}, new Object[]{AtomRecord.EdgeRecord.of(2, EdgeKind.SET)}, null); assertTrue(root.heapBytes() >= bare.heapBytes() + ((Ref.Inline) inlined.members()).leaf().heapBytes()); }