Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
745517b
Add Datahike benchmark parity and avet predicate range fast path
cursoragent Aug 27, 2026
da87d76
Add query planner ADR, fix q2 same-entity joins, expand parity tests
cursoragent Aug 27, 2026
e82959c
Fix AVET value-range scans to seek and stop on encoded bounds
cursoragent Aug 27, 2026
ac1a172
Optimize AVET range scans and label benchmark storage backends
cursoragent Aug 27, 2026
f80411a
Align benchmark storage with in-memory nosync LMDB profile
cursoragent Aug 27, 2026
12fbd67
Speed up same-entity join queries and fast benchmark defaults
cursoragent Aug 27, 2026
73f07b3
Add single-query filter to Datahike compare benchmarks
cursoragent Aug 27, 2026
4fc6266
Fix same-entity join fast path for mixed constant/value queries
cursoragent Aug 27, 2026
e8c96a4
Add fast paths for q5, q-or-join, and q-not-join benchmarks
cursoragent Aug 27, 2026
5d291d9
Fix attr-only index slice and fold_slice prefix bounds
cursoragent Aug 27, 2026
70b3504
Add fast path for non-recursive follow rule query (q-rule)
cursoragent Aug 27, 2026
5dc1a05
Document Datahike vs OCaml query gaps; fast-path bind_var equality
cursoragent Aug 27, 2026
c12e8e7
Simplify query paths to align with Datahike structure
cursoragent Aug 27, 2026
49c3356
Use per-entity attr lookup for single-value same-entity queries
cursoragent Aug 27, 2026
5819867
Align multi-value same-entity queries with Datahike entity-group pattern
cursoragent Aug 27, 2026
79fd8c7
Speed up same-entity value lookups with direct Aevt array search
cursoragent Aug 27, 2026
df8d464
Fix LMDB read txn, AVET seek/range, and unify index codec with SQLite
cursoragent Aug 27, 2026
a60563b
Remove legacy PSS Logseq SQLite examples and fix unique checks mid-tx
cursoragent Aug 27, 2026
bf64449
Disable benchmark/perf gates and lein-dependent cross-runtime tests
cursoragent Aug 27, 2026
f5d3a42
Add Datahike category parity tests and fix as_of query visibility
cursoragent Aug 27, 2026
f1a2a2d
Strengthen Datahike result identity and speed qpred2 range scans
cursoragent Aug 27, 2026
1d07749
Speed same-entity and not/not-join query fast paths
cursoragent Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ to use a different checkout or compiled JS bundle.
- `type/`: shared public type definitions
- `impl/`: implementation modules
- `test/`: unit, integration, js_of_ocaml, and cross-runtime tests
- `examples/`: small executable examples
- `bench/`: benchmark entry points
- `script/`: parity and benchmark helper scripts

Expand Down
188 changes: 188 additions & 0 deletions bench/compare_ocaml_datahike.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat <<'EOF'
Usage: compare_ocaml_datahike.sh [SIZE] [QUERY]

Run OCaml vs Datahike shared query benchmarks.

SIZE entity count (default: 2000)
QUERY optional single query name, e.g. q3, qpred1, q-rule

Environment:
BENCH_QUERY same as QUERY positional arg
BENCH_WARMUP_MS warmup duration per benchmark (default: 200, 2000 when FULL=1)
BENCH_SAMPLE_MS sample duration per benchmark (default: 200, 2000 when FULL=1)
BENCH_REPEATS median sample count (default: 2)
BENCH_JIT_WARMUP JIT iterations per query before timing (default: 100)
FULL=1 use publication timing (2000ms warmup/sample)

Examples:
./compare_ocaml_datahike.sh 2000 q3
BENCH_QUERY=qpred1 ./compare_ocaml_datahike.sh
dune exec --release bench/datahike_compare.exe -- --size 2000 --query q3 --list-queries
EOF
}

SIZE="${1:-2000}"
QUERY="${BENCH_QUERY:-}"

if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi

if [[ -n "${2:-}" ]]; then
QUERY="$2"
fi

if [[ "$SIZE" == "--help" || "$SIZE" == "-h" ]]; then
usage
exit 0
fi

if [[ "${FULL:-0}" == "1" ]]; then
WARMUP_MS="${WARMUP_MS:-2000}"
SAMPLE_MS="${SAMPLE_MS:-2000}"
REPEATS="${REPEATS:-2}"
JIT_WARMUP="${JIT_WARMUP:-100}"
else
WARMUP_MS="${WARMUP_MS:-200}"
SAMPLE_MS="${SAMPLE_MS:-200}"
REPEATS="${REPEATS:-2}"
JIT_WARMUP="${JIT_WARMUP:-100}"
fi

export BENCH_SIZE="$SIZE"
export BENCH_WARMUP_MS="$WARMUP_MS"
export BENCH_SAMPLE_MS="$SAMPLE_MS"
export BENCH_REPEATS="$REPEATS"
export BENCH_JIT_WARMUP="$JIT_WARMUP"
if [[ -n "$QUERY" ]]; then
export BENCH_QUERY="$QUERY"
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DATAHIKE_REPO="${DATAHIKE_REPO:-/tmp/bench-datahike}"
OCAML_BENCH="${REPO_ROOT}/_build/default/bench/datahike_compare.exe"

ensure_datahike_java() {
if [[ ! -e "$DATAHIKE_REPO/deps.edn" ]]; then
git clone --depth 1 https://github.com/replikativ/datahike.git "$DATAHIKE_REPO"
fi
(
cd "$DATAHIKE_REPO"
mkdir -p target/classes
local cp
cp="$(clojure -Spath -M:bench)"
if [[ ! -f target/classes/datahike/java/QueryResult.class ]]; then
javac -cp "$cp:target/classes" -d target/classes \
java/src/datahike/java/IEntity.java \
java/src/datahike/java/Util.java \
java/src/datahike/java/QueryResult.java
fi
)
}

run_datahike() {
(
cd "$DATAHIKE_REPO"
DATAHIKE_QUERY_PLANNER=true clojure -M:bench -e \
"(load-file \"${REPO_ROOT}/bench/datahike_shared_bench.clj\")" \
2>/dev/null
)
}

run_ocaml() {
local ocaml_args=(
--size "$SIZE"
--warmup-ms "$WARMUP_MS"
--sample-ms "$SAMPLE_MS"
--repeats "$REPEATS"
--jit-warmup "$JIT_WARMUP"
)
if [[ -n "$QUERY" ]]; then
ocaml_args+=(--query "$QUERY")
fi
(
cd "$REPO_ROOT"
dune build --profile release bench/datahike_compare.exe >/dev/null
BENCH_RUNTIME_LABEL=ocaml "$OCAML_BENCH" "${ocaml_args[@]}" 2>/dev/null
)
}

parse_dh_row() {
local name="$1"
awk -v n="$name" '$1 == n { print $2; exit }'
}

parse_ocaml_row() {
local name="$1"
awk -F'\t' -v n="$name" '$1 == n { print $2; exit }'
}

ratio_cell() {
awk -v o="$1" -v d="$2" 'BEGIN {
if (o + 0 == 0 || d + 0 == 0) print "?";
else printf "%.2fx", o / d
}'
}

ensure_datahike_java

if [[ -n "$QUERY" ]]; then
echo "=== OCaml vs Datahike query benchmark (${SIZE} entities, query=${QUERY}) ==="
else
echo "=== OCaml vs Datahike query benchmark (${SIZE} entities) ==="
fi
if [[ "${FULL:-0}" == "1" ]]; then
echo "Protocol: FULL warmup=${WARMUP_MS}ms sample=${SAMPLE_MS}ms repeats=${REPEATS} jit=${JIT_WARMUP} (set FULL=1)"
else
echo "Protocol: fast warmup=${WARMUP_MS}ms sample=${SAMPLE_MS}ms repeats=${REPEATS} jit=${JIT_WARMUP} (use FULL=1 for publication timing)"
fi
echo "Storage: datahike=memory+persistent-set ocaml=memory LMDB index (nosync, see storage row in raw output)"
echo

START=$(date +%s)
echo "Running Datahike (JVM cold start may take ~30-60s)..."
DH_OUT="$(run_datahike)"
DH_SEC=$(( $(date +%s) - START ))
echo "Running OCaml (${DH_SEC}s for Datahike side)..."
OCAML_START=$(date +%s)
OCAML_OUT="$(run_ocaml)"
OCAML_SEC=$(( $(date +%s) - OCAML_START ))
TOTAL_SEC=$(( $(date +%s) - START ))

QUERY_ORDER=(
q1 q2 q2-switch q3 q4 q5 qpred1 qpred2
q-or q-not q-or-join q-not-join q-pred-range q-5-merge q-rule
)

if [[ -n "$QUERY" ]]; then
QUERY_ORDER=("$QUERY")
fi

printf "%-14s %12s %12s %12s\n" "benchmark" "datahike(ms)" "ocaml(ms)" "ocaml/dh"
echo "------------------------------------------------------------"

for name in "${QUERY_ORDER[@]}"; do
dh_ms="$(printf '%s\n' "$DH_OUT" | parse_dh_row "$name")"
ocaml_ms="$(printf '%s\n' "$OCAML_OUT" | parse_ocaml_row "$name")"
if [[ -z "$dh_ms" || -z "$ocaml_ms" ]]; then
printf "%-14s %12s %12s %12s\n" "$name" "${dh_ms:-?}" "${ocaml_ms:-?}" "?"
continue
fi
ratio="$(ratio_cell "$ocaml_ms" "$dh_ms")"
printf "%-14s %12s %12s %12s\n" "$name" "$dh_ms" "$ocaml_ms" "$ratio"
done

echo
echo "Timing: datahike=${DH_SEC}s ocaml=${OCAML_SEC}s total=${TOTAL_SEC}s"
echo
echo "=== raw: datahike ==="
printf '%s\n' "$DH_OUT" | awk '/^(q|runtime|size|warmup|sample|repeats|jit|Setting|Query planner|Done)/ || /^[[:space:]]*q/ || /^Benchmark/ || /^---/ { print }'
echo
echo "=== raw: ocaml ==="
printf '%s\n' "$OCAML_OUT"
96 changes: 96 additions & 0 deletions bench/count_avet.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
open Datascript

let indexed =
{
cardinality = One
; unique = None
; indexed = true
; is_component = false
; no_history = false
; doc = None
; value_type = None
; tuple_attrs = None
; tuple_types = None
}

let many = { indexed with cardinality = Many; indexed = false }

let rng = ref 1

let next_int bound =
rng := (!rng * 1_664_525 + 1_013_904_223) land 0x7fffffff;
!rng mod bound

let names = [| "Ivan"; "Petr"; "Sergey"; "Oleg"; "Yuri"; "Dmitry"; "Fedor"; "Denis" |]
let last_names = [| "Ivanov"; "Petrov"; "Sidorov"; "Kovalev"; "Kuznetsov"; "Voronoi" |]
let aliases = [| "A. C. Q. W."; "A. J. Finn"; "A.A. Fair"; "Aapeli"; "Aaron Wolfe" |]

let random_man i =
let name = names.(i mod Array.length names) in
let last_name = last_names.(i mod Array.length last_names) in
let alias_count = 1 + next_int 10 in
let alias_values = List.init alias_count (fun _ -> String aliases.(next_int (Array.length aliases))) in
Entity
{
db_id = Some (Temp_id (string_of_int (i + 1)))
; attrs =
[ "name", One_value (String name)
; "last-name", One_value (String last_name)
; "full-name", One_value (String (name ^ " " ^ last_name))
; "alias", Many_values alias_values
; "sex", One_value (Keyword (if next_int 2 = 0 then "male" else "female"))
; "age", One_value (Int (next_int 100))
; "salary", One_value (Int (next_int 100_000))
]
}

let minimal_schema = [ "salary", indexed ]

let full_schema =
[ "name", indexed; "last-name", indexed; "age", indexed; "salary", indexed; "alias", many ]

let build_db schema size =
let entities =
match schema with
| "minimal" ->
List.init size (fun index ->
Entity
{
db_id = Some (Temp_id (string_of_int (index + 1)))
; attrs = [ "salary", One_value (Int (next_int 100_000)) ]
})
| _ -> List.init size random_man
in
let schema = if schema = "minimal" then minimal_schema else full_schema in
let storage = benchmark_memory_storage () in
let db = db_with entities (empty_db ~schema ~storage ()) in
refresh_db_indexes db

let time_ms iterations f =
let start = Sys.time () in
for _ = 1 to iterations do
ignore (f ())
done;
(Sys.time () -. start) *. 1000. /. float iterations

let seq_len seq =
Seq.fold_left (fun count _ -> count + 1) 0 seq

let bench label db =
let q () =
q_string db "[:find ?e ?s :where [?e :salary ?s] [(> ?s 50000)]]" |> List.length
in
let seq () = seq_len (index_range db "salary" ~start:(Int 50001) ()) in
let seq_one () = seq_len (index_range db "salary" ~start:(Int 1) ~stop:(Int 1) ()) in
Printf.printf "%s count=%d one=%d ms_q=%.4f ms_seq=%.4f ms_one=%.4f max_e=%d\n" label (q ())
(seq_one ())
(time_ms 200 q)
(time_ms 200 seq)
(time_ms 200 seq_one)
db.max_datom_e

let () =
rng := 1;
bench "minimal" (build_db "minimal" 2000);
rng := 1;
bench "full" (build_db "full" 2000)
Loading