mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-09-12 19:50:57 +05:00
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019YHSjZM6TstSAMN2PhgfUg
203 lines
8.1 KiB
Python
Executable File
203 lines
8.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Benchmark ArchiveBox hot-path index queries at large row counts.
|
|
|
|
Run from inside an initialized (empty) collection directory, with the same
|
|
ARCHIVEBOX_DATABASE_* env vars the collection was initialized with:
|
|
|
|
cd /path/to/collection
|
|
uv run --project /path/to/ArchiveBox python /path/to/ArchiveBox/bin/benchmark_db_backends.py --rows 1000000
|
|
|
|
Seeds N snapshots (+1 archiveresult each) via bulk_create, runs ANALYZE, then
|
|
times the hot queries used by the admin UI, snapshot detail views, URL prefix
|
|
search, and the worker queue/claim paths. Works on both sqlite and postgres —
|
|
use it to compare backends or to catch performance regressions.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import statistics
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
|
|
def seed(rows: int, batch_size: int = 20_000) -> None:
|
|
from django.db import transaction
|
|
|
|
from archivebox.base_models.models import get_or_create_system_user_pk
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.crawls.models import Crawl
|
|
from archivebox.uuid_compat import uuid7
|
|
|
|
user_pk = get_or_create_system_user_pk()
|
|
crawl = Crawl.objects.create(urls="https://example.com/", created_by_id=user_pk, status="sealed", label="benchmark seed")
|
|
|
|
existing = Snapshot.objects.count()
|
|
if existing >= rows:
|
|
print(f"already seeded ({existing} snapshots)", flush=True)
|
|
return
|
|
|
|
base_time = datetime(2020, 1, 1, tzinfo=timezone.utc)
|
|
started = time.monotonic()
|
|
for start in range(existing, rows, batch_size):
|
|
end = min(start + batch_size, rows)
|
|
snapshots = []
|
|
for i in range(start, end):
|
|
created = base_time + timedelta(seconds=i)
|
|
snapshots.append(
|
|
Snapshot(
|
|
id=uuid7(),
|
|
url=f"https://site{i % 1000}.example.org/path/{i}" + ("#section" if i % 7 == 0 else ""),
|
|
timestamp=f"{1577836800 + i}.{i}",
|
|
title=f"Benchmark page {i}",
|
|
crawl=crawl,
|
|
bookmarked_at=created,
|
|
created_at=created,
|
|
modified_at=created,
|
|
downloaded_at=created if i % 10 else None,
|
|
status="sealed" if i % 20 else "queued",
|
|
retry_at=None if i % 20 else created,
|
|
fs_version="0.9.0",
|
|
config={},
|
|
depth=0,
|
|
),
|
|
)
|
|
with transaction.atomic():
|
|
Snapshot.objects.bulk_create(snapshots, batch_size=batch_size)
|
|
ArchiveResult.objects.bulk_create(
|
|
[
|
|
ArchiveResult(
|
|
id=uuid7(),
|
|
snapshot=snapshot,
|
|
plugin="wget",
|
|
hook_name="on_Snapshot__06_wget.py",
|
|
status="succeeded",
|
|
created_at=snapshot.created_at,
|
|
modified_at=snapshot.created_at,
|
|
start_ts=snapshot.created_at,
|
|
end_ts=snapshot.created_at,
|
|
output_str="benchmark",
|
|
output_files={},
|
|
)
|
|
for snapshot in snapshots
|
|
],
|
|
batch_size=batch_size,
|
|
)
|
|
if (end // batch_size) % 5 == 0 or end == rows:
|
|
rate = (end - existing) / max(time.monotonic() - started, 0.001)
|
|
print(f" seeded {end}/{rows} snapshots ({rate:,.0f} rows/s)", flush=True)
|
|
|
|
|
|
def analyze_tables() -> None:
|
|
from django.db import connection
|
|
|
|
with connection.cursor() as cursor:
|
|
for table in ("core_snapshot", "core_archiveresult"):
|
|
cursor.execute(f"ANALYZE {table}")
|
|
|
|
|
|
def timed(func, repeat: int = 5) -> tuple[float, object]:
|
|
times = []
|
|
result = None
|
|
for _ in range(repeat):
|
|
started = time.perf_counter()
|
|
result = func()
|
|
times.append((time.perf_counter() - started) * 1000)
|
|
return statistics.median(times), result
|
|
|
|
|
|
def run_benchmarks(rows: int) -> dict[str, float]:
|
|
from django.db import connection
|
|
from django.db.models import Count, Q
|
|
from django.utils import timezone as dj_timezone
|
|
|
|
from archivebox.core.models import ArchiveResult, Snapshot
|
|
from archivebox.misc.db import approximate_row_counts
|
|
from archivebox.search.views import iter_url_prefix_search_ids
|
|
|
|
now = dj_timezone.now()
|
|
results: dict[str, float] = {}
|
|
target_i = rows // 2
|
|
target_url = f"https://site{target_i % 1000}.example.org/path/{target_i}"
|
|
|
|
def fragmentless_q(url: str) -> Q:
|
|
if connection.vendor == "sqlite":
|
|
return Q(url=url) | (Q(url__gte=f"{url}#") & Q(url__lt=f"{url}#\U0010ffff"))
|
|
return Q(url=url) | Q(url__startswith=f"{url}#")
|
|
|
|
benchmarks = {
|
|
"exact_count": lambda: Snapshot.objects.count(),
|
|
"approximate_row_counts": lambda: approximate_row_counts(connection),
|
|
"admin_list_page": lambda: list(
|
|
Snapshot.objects.order_by("-bookmarked_at").values("id", "url", "title", "status", "bookmarked_at")[:40],
|
|
),
|
|
"admin_list_page_offset_10k": lambda: list(Snapshot.objects.order_by("-bookmarked_at").values("id", "url", "title")[10_000:10_040]),
|
|
"snapshot_detail_by_url": lambda: list(Snapshot.objects.filter(fragmentless_q(target_url))[:10]),
|
|
"snapshot_detail_archiveresults": lambda: list(
|
|
ArchiveResult.objects.filter(snapshot__url=target_url).order_by("start_ts").values("id", "plugin", "status")[:100],
|
|
),
|
|
"url_prefix_search": lambda: list(iter_url_prefix_search_ids("https://site500.example.org/", Snapshot.objects.all())),
|
|
"worker_queue_scan": lambda: list(
|
|
Snapshot.objects.filter(status="queued", retry_at__lte=now)
|
|
.order_by("retry_at", "created_at")
|
|
.values_list("id", flat=True)[:100],
|
|
),
|
|
"status_facet_counts": lambda: dict(Snapshot.objects.values_list("status").annotate(n=Count("id")).values_list("status", "n")),
|
|
"tag_join_filter": lambda: list(Snapshot.objects.filter(title__icontains="page 4242").values("id")[:20]),
|
|
}
|
|
|
|
for name, func in benchmarks.items():
|
|
median_ms, _ = timed(func)
|
|
results[name] = round(median_ms, 2)
|
|
print(f" {name:35s} {median_ms:10.2f} ms", flush=True)
|
|
|
|
def claim_one() -> int:
|
|
snapshot = Snapshot.objects.filter(status="queued").order_by("retry_at").first()
|
|
if snapshot is None:
|
|
return 0
|
|
return Snapshot.objects.filter(pk=snapshot.pk, retry_at=snapshot.retry_at).update(
|
|
retry_at=now + timedelta(seconds=60),
|
|
modified_at=now,
|
|
)
|
|
|
|
median_ms, _ = timed(claim_one)
|
|
results["worker_cas_claim"] = round(median_ms, 2)
|
|
print(f" {'worker_cas_claim':35s} {median_ms:10.2f} ms", flush=True)
|
|
return results
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--rows", type=int, default=1_000_000)
|
|
parser.add_argument("--seed-only", action="store_true")
|
|
parser.add_argument("--json", dest="json_path", default=None, help="also write results to this JSON file")
|
|
args = parser.parse_args()
|
|
|
|
from archivebox.config.django import setup_django
|
|
from archivebox.misc.db import database_exists
|
|
|
|
setup_django()
|
|
assert database_exists(), "run archivebox init in this directory first"
|
|
from django.db import connection
|
|
|
|
print(f"backend: {connection.vendor}, target rows: {args.rows}", flush=True)
|
|
print("seeding...", flush=True)
|
|
seed(args.rows)
|
|
if args.seed_only:
|
|
return
|
|
print("running ANALYZE...", flush=True)
|
|
analyze_tables()
|
|
print("benchmarks (median of 5):", flush=True)
|
|
results = run_benchmarks(args.rows)
|
|
results["_backend"] = connection.vendor
|
|
results["_rows"] = args.rows
|
|
if args.json_path:
|
|
with open(args.json_path, "w", encoding="utf-8") as f:
|
|
json.dump(results, f, indent=2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
main()
|