- CI warm: ccache dep, continue-on-error, 60min timeout, YAMS_DISABLE_FAISS - Faiss: YAMS_HAS_FAISS guard, Apple Clang disable, OpenMP detection - Metadata: retry constraint failures, deduplicate batch UPSERT entries - Docker: wget for Zig, gfortran, YAMS_DISABLE_FAISS=1 - CI config: free-disk-space v1.3.1, libomp-18-dev, experimental trigger - Weekly benchmarks: faiss disable, ccache, diagnostics capture - Simeon: bump to 7763a97, EntropyRouter, concept_count - CorpusStats: extraction readiness fields - Remove committed artifacts (.wrangler, .artifacts, dispatcher_external) - Fix .vscode/settings.json hardcoded path Signed-off-by: trvon <git@trevon.dev>
357 lines
10 KiB
Bash
Executable file
357 lines
10 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: scripts/dev/benchmark_taskflow_ab.sh [options]
|
|
|
|
Benchmarks current HEAD against a baseline commit on the same machine using
|
|
temporary git worktrees, separate build directories, and a fixed dataset.
|
|
|
|
By default:
|
|
- current ref: HEAD
|
|
- baseline ref: HEAD^
|
|
- dataset: <repo>/src
|
|
- build dir: build/release
|
|
- api iters: 5
|
|
- throughput repeats: 3
|
|
|
|
Important:
|
|
- This script avoids your dirty main worktree by creating detached worktrees.
|
|
- It overlays the current full-pipeline benchmark source into both worktrees so
|
|
the throughput harness is identical across commits.
|
|
|
|
Options:
|
|
--baseline-ref <ref> Baseline git ref to compare against (default: HEAD^)
|
|
--current-ref <ref> Current git ref to benchmark (default: HEAD)
|
|
--dataset <path> Fixed dataset path to ingest (default: <repo>/src)
|
|
--work-root <path> Temporary worktree/results root
|
|
--build-jobs <n> Meson compile parallelism (default: 4)
|
|
--api-iters <n> Iterations for yams_api_benchmarks (default: 5)
|
|
--repeat <n> Repeats for ingestion_throughput_bench (default: 3)
|
|
--keep-worktrees Keep temporary worktrees/results after completion
|
|
--no-overlay-bench Do not copy current throughput benchmark source into both worktrees
|
|
-h, --help Show this help
|
|
|
|
Examples:
|
|
scripts/dev/benchmark_taskflow_ab.sh
|
|
scripts/dev/benchmark_taskflow_ab.sh --baseline-ref a5d5ddc9 --dataset "$PWD/src"
|
|
EOF
|
|
}
|
|
|
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
baseline_ref="HEAD^"
|
|
current_ref="HEAD"
|
|
dataset_path="${repo_root}/src"
|
|
work_root="${TMPDIR:-/tmp}/yams-taskflow-ab"
|
|
build_jobs=4
|
|
api_iters=5
|
|
repeat_count=3
|
|
keep_worktrees=0
|
|
overlay_bench=1
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--baseline-ref)
|
|
baseline_ref="$2"
|
|
shift 2
|
|
;;
|
|
--current-ref)
|
|
current_ref="$2"
|
|
shift 2
|
|
;;
|
|
--dataset)
|
|
dataset_path="$2"
|
|
shift 2
|
|
;;
|
|
--work-root)
|
|
work_root="$2"
|
|
shift 2
|
|
;;
|
|
--build-jobs)
|
|
build_jobs="$2"
|
|
shift 2
|
|
;;
|
|
--api-iters)
|
|
api_iters="$2"
|
|
shift 2
|
|
;;
|
|
--repeat)
|
|
repeat_count="$2"
|
|
shift 2
|
|
;;
|
|
--keep-worktrees)
|
|
keep_worktrees=1
|
|
shift
|
|
;;
|
|
--no-overlay-bench)
|
|
overlay_bench=0
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
printf 'Unknown option: %s\n' "$1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ ! -d "$dataset_path" ]]; then
|
|
printf 'Dataset path does not exist: %s\n' "$dataset_path" >&2
|
|
exit 2
|
|
fi
|
|
|
|
baseline_sha="$(git rev-parse --verify "$baseline_ref")"
|
|
current_sha="$(git rev-parse --verify "$current_ref")"
|
|
baseline_short="$(git rev-parse --short "$baseline_sha")"
|
|
current_short="$(git rev-parse --short "$current_sha")"
|
|
|
|
work_root="${work_root%/}-${baseline_short}-vs-${current_short}"
|
|
baseline_dir="${work_root}/baseline"
|
|
current_dir="${work_root}/current"
|
|
results_dir="${work_root}/results"
|
|
|
|
mkdir -p "$results_dir"
|
|
|
|
cleanup() {
|
|
if [[ "$keep_worktrees" -eq 1 ]]; then
|
|
return
|
|
fi
|
|
git worktree remove --force "$baseline_dir" >/dev/null 2>&1 || true
|
|
git worktree remove --force "$current_dir" >/dev/null 2>&1 || true
|
|
rm -rf "$work_root"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
git worktree remove --force "$baseline_dir" >/dev/null 2>&1 || true
|
|
git worktree remove --force "$current_dir" >/dev/null 2>&1 || true
|
|
rm -rf "$baseline_dir" "$current_dir"
|
|
|
|
git worktree add --detach "$baseline_dir" "$baseline_sha" >/dev/null
|
|
git worktree add --detach "$current_dir" "$current_sha" >/dev/null
|
|
|
|
overlay_files=()
|
|
if [[ "$overlay_bench" -eq 1 ]]; then
|
|
overlay_files+=("tests/benchmarks/ingestion_throughput_bench.cpp")
|
|
fi
|
|
|
|
overlay_into_worktree() {
|
|
local worktree="$1"
|
|
local rel
|
|
for rel in "${overlay_files[@]}"; do
|
|
if [[ -f "${repo_root}/${rel}" && -f "${worktree}/${rel}" ]]; then
|
|
cp "${repo_root}/${rel}" "${worktree}/${rel}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
find_native_file() {
|
|
local worktree="$1"
|
|
local candidates=(
|
|
"${worktree}/build/release/build-release/conan/conan_meson_native.ini"
|
|
"${worktree}/build/release/conan/conan_meson_native.ini"
|
|
"${worktree}/build/release/conan_meson_native.ini"
|
|
)
|
|
local candidate
|
|
for candidate in "${candidates[@]}"; do
|
|
if [[ -f "$candidate" ]]; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
printf 'Unable to locate Conan Meson native file under %s/build/release\n' "$worktree" >&2
|
|
return 1
|
|
}
|
|
|
|
generate_throughput_config() {
|
|
local out_json="$1"
|
|
local out_jsonl="$2"
|
|
DATASET_PATH="$dataset_path" OUT_JSON="$out_json" OUT_JSONL="$out_jsonl" \
|
|
REPEAT_COUNT="$repeat_count" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
cfg = {
|
|
"dataset_path": os.environ["DATASET_PATH"],
|
|
"fixture_manifest": "unused",
|
|
"output_metrics": os.environ["OUT_JSONL"],
|
|
"post_run_cleanup": True,
|
|
"runs": [
|
|
{
|
|
"label": "taskflow-ab-1w",
|
|
"workers": 1,
|
|
"repeat": int(os.environ["REPEAT_COUNT"]),
|
|
"args": [
|
|
"--recursive",
|
|
"--include=*.cpp,*.h,*.hpp",
|
|
"--exclude=**/.git/**,**/node_modules/**,**/build/**",
|
|
],
|
|
}
|
|
],
|
|
}
|
|
|
|
Path(os.environ["OUT_JSON"]).write_text(json.dumps(cfg, indent=2) + "\n")
|
|
PY
|
|
}
|
|
|
|
build_worktree() {
|
|
local worktree="$1"
|
|
overlay_into_worktree "$worktree"
|
|
|
|
git -C "$worktree" submodule update --init --recursive >/dev/null
|
|
|
|
conan install "$worktree" -of "$worktree/build/release" \
|
|
-s build_type=Release \
|
|
-s compiler.cppstd=20 \
|
|
-b missing >/dev/null
|
|
|
|
local native_file
|
|
native_file="$(find_native_file "$worktree")"
|
|
|
|
if [[ -d "$worktree/build/release" && -f "$worktree/build/release/build.ninja" ]]; then
|
|
meson setup "$worktree/build/release" \
|
|
"$worktree" \
|
|
--reconfigure \
|
|
--native-file "$native_file" \
|
|
--buildtype=release \
|
|
-Dbuild-tests=true \
|
|
-Dbuild-benchmarks=true \
|
|
>/dev/null
|
|
else
|
|
meson setup "$worktree/build/release" \
|
|
"$worktree" \
|
|
--native-file "$native_file" \
|
|
--buildtype=release \
|
|
-Dbuild-tests=true \
|
|
-Dbuild-benchmarks=true \
|
|
>/dev/null
|
|
fi
|
|
|
|
meson compile -C "$worktree/build/release" -j"$build_jobs" yams_api_benchmarks ingestion_throughput_bench >/dev/null
|
|
}
|
|
|
|
run_worktree() {
|
|
local label="$1"
|
|
local worktree="$2"
|
|
local sha="$3"
|
|
local short_sha
|
|
short_sha="$(git -C "$worktree" rev-parse --short HEAD)"
|
|
|
|
local api_jsonl="${results_dir}/${label}-${short_sha}-api.jsonl"
|
|
local throughput_json="${results_dir}/${label}-${short_sha}-throughput.json"
|
|
local throughput_jsonl="${results_dir}/${label}-${short_sha}-throughput.jsonl"
|
|
|
|
generate_throughput_config "$throughput_json" "$throughput_jsonl"
|
|
|
|
(
|
|
cd "$worktree/build/release"
|
|
./tests/benchmarks/yams_api_benchmarks --iterations "$api_iters" --quiet --no-archive --output "$api_jsonl" >/dev/null
|
|
./tests/benchmarks/ingestion_throughput_bench --config "$throughput_json" >/dev/null
|
|
)
|
|
|
|
LABEL="$label" SHA="$sha" API_JSONL="$api_jsonl" THROUGHPUT_JSONL="$throughput_jsonl" \
|
|
SUMMARY_JSON="${results_dir}/${label}-${short_sha}-summary.json" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
label = os.environ["LABEL"]
|
|
sha = os.environ["SHA"]
|
|
api_path = Path(os.environ["API_JSONL"])
|
|
throughput_path = Path(os.environ["THROUGHPUT_JSONL"])
|
|
summary_path = Path(os.environ["SUMMARY_JSON"])
|
|
|
|
summary = {
|
|
"label": label,
|
|
"sha": sha,
|
|
"api": {},
|
|
"throughput": {},
|
|
}
|
|
|
|
if api_path.exists():
|
|
for line in api_path.read_text().splitlines():
|
|
if not line.strip():
|
|
continue
|
|
row = json.loads(line)
|
|
summary["api"][row["name"]] = {
|
|
"ops_per_sec": row.get("ops_per_sec"),
|
|
"duration_ms": row.get("duration_ms"),
|
|
}
|
|
|
|
if throughput_path.exists():
|
|
rows = [json.loads(line) for line in throughput_path.read_text().splitlines() if line.strip()]
|
|
if rows:
|
|
summary["throughput"] = {
|
|
"avg_docs_per_sec": sum(r["throughput_files_per_second"] for r in rows) / len(rows),
|
|
"avg_duration_seconds": sum(r["duration_seconds"] for r in rows) / len(rows),
|
|
"avg_documents_total": sum(r.get("documents_total", 0) for r in rows) / len(rows),
|
|
"all_drained": all(bool(r.get("drained")) for r in rows),
|
|
"samples": len(rows),
|
|
}
|
|
|
|
summary_path.write_text(json.dumps(summary, indent=2) + "\n")
|
|
PY
|
|
}
|
|
|
|
printf 'Taskflow A/B benchmark\n'
|
|
printf ' baseline: %s (%s)\n' "$baseline_ref" "$baseline_short"
|
|
printf ' current : %s (%s)\n' "$current_ref" "$current_short"
|
|
printf ' dataset : %s\n' "$dataset_path"
|
|
printf ' workroot: %s\n' "$work_root"
|
|
|
|
build_worktree "$baseline_dir"
|
|
run_worktree baseline "$baseline_dir" "$baseline_sha"
|
|
|
|
build_worktree "$current_dir"
|
|
run_worktree current "$current_dir" "$current_sha"
|
|
|
|
BASELINE_SUMMARY="${results_dir}/baseline-${baseline_short}-summary.json" \
|
|
CURRENT_SUMMARY="${results_dir}/current-${current_short}-summary.json" \
|
|
RESULTS_DIR="$results_dir" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
results_dir = Path(os.environ["RESULTS_DIR"])
|
|
baseline = json.loads(Path(os.environ["BASELINE_SUMMARY"]).read_text())
|
|
current = json.loads(Path(os.environ["CURRENT_SUMMARY"]).read_text())
|
|
|
|
def delta(cur, base):
|
|
if base in (None, 0):
|
|
return None
|
|
return ((cur - base) / base) * 100.0
|
|
|
|
print("\nAPI throughput delta (same-machine)")
|
|
for name in sorted(set(baseline["api"]) | set(current["api"])):
|
|
b = baseline["api"].get(name, {}).get("ops_per_sec")
|
|
c = current["api"].get(name, {}).get("ops_per_sec")
|
|
d = delta(c, b) if b is not None and c is not None else None
|
|
if b is None or c is None:
|
|
print(f" {name}: unavailable")
|
|
else:
|
|
print(f" {name}: {b:.2f} -> {c:.2f} ({d:+.1f}%)")
|
|
|
|
print("\nFull pipeline throughput delta (same-machine)")
|
|
b = baseline.get("throughput", {}).get("avg_docs_per_sec")
|
|
c = current.get("throughput", {}).get("avg_docs_per_sec")
|
|
d = delta(c, b) if b is not None and c is not None else None
|
|
if b is None or c is None:
|
|
print(" unavailable")
|
|
else:
|
|
print(f" docs/sec: {b:.2f} -> {c:.2f} ({d:+.1f}%)")
|
|
print(f" baseline drained: {baseline['throughput'].get('all_drained')}")
|
|
print(f" current drained: {current['throughput'].get('all_drained')}")
|
|
|
|
print("\nArtifacts")
|
|
print(f" {results_dir}")
|
|
PY
|
|
|
|
if [[ "$keep_worktrees" -eq 1 ]]; then
|
|
printf '\nKept worktrees and results under %s\n' "$work_root"
|
|
fi
|