| |
| """ |
| Vortexa Indexer Benchmark β VTX Codebase |
| ========================================= |
| Benchmarks TWO embedding models from the vortexa library on the VTX codebase: |
| |
| Model A: VTXAI/vtx-embed-1M (LF4Embedder β 4-bit static nano, ~1 MB, fastest) |
| Model B: VTXAI/vtx-embed-7M (LF4Embedder β 4-bit static mini, ~7 MB, default) |
| |
| Metrics reported per model |
| - Indexing time (ms) |
| - Peak RAM usage during index (MB) |
| - Number of files indexed |
| - Number of chunks produced |
| - Chunk config (size / overlap) |
| - Model size / embedding dim |
| - Per-query latency (mean / p50 / p95 / p99) |
| - R@1 β query-level: was the ground-truth file ranked #1? |
| - R@5 β query-level: was the ground-truth file in top-5? |
| - R@10 β query-level: was the ground-truth file in top-10? |
| |
| 30-query test set is hand-crafted from real VTX source symbols and concepts. |
| |
| Usage: |
| uv run python benchmark.py |
| uv run python benchmark.py --root /path/to/other/codebase |
| uv run python benchmark.py --alpha 0.7 # override hybrid weight |
| uv run python benchmark.py --no-m2v # skip Model2Vec (faster) |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import gc |
| import json |
| import shutil |
| import sys |
| import tempfile |
| import time |
| from dataclasses import asdict, dataclass, field |
| from pathlib import Path |
| from typing import Any |
|
|
| |
| try: |
| import resource as _resource |
|
|
| def _peak_rss_mb() -> float: |
| """Peak resident-set-size in MB (Linux/macOS).""" |
| return _resource.getrusage(_resource.RUSAGE_SELF).ru_maxrss / 1024 |
|
|
| except ImportError: |
|
|
| def _peak_rss_mb() -> float: |
| try: |
| import psutil |
|
|
| return psutil.Process().memory_info().rss / 1024 / 1024 |
| except Exception: |
| return 0.0 |
|
|
|
|
| |
| try: |
| from vortexa.core.embedding import LF4Embedder, Model2VecEmbedder |
| from vortexa.core.indexer import CodebaseIndexer |
| from vortexa.core.types import ChunkConfig, IndexStats |
| except ModuleNotFoundError as exc: |
| sys.exit( |
| f"\n[ERROR] Could not import vortexa: {exc}\n\n" |
| "vortexa must be available in the SAME Python environment used to run this\n" |
| "script. The simplest ways:\n\n" |
| " # Recommended β uv injects vortexa into a fresh env at runtime:\n" |
| " uv run --with vortexa python benchmark.py\n\n" |
| " # Or add it to the project venv permanently:\n" |
| " uv add --dev vortexa\n" |
| " uv run python benchmark.py\n\n" |
| "Do NOT run with .venv/bin/python directly; that venv lacks vortexa.\n" |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| QUERIES: list[tuple[str, str | None, str]] = [ |
| |
| ("agent runner main loop execution", "agent_runner", "Agent runner entry point"), |
| ("how does the agent run tasks asynchronously", "agent_runner", "Async task execution"), |
| |
| ("command line interface argument parsing", "cli", "CLI argument parsing"), |
| ("vtx CLI entry point argparse", "cli", "CLI argparse setup"), |
| |
| ("configuration loading and defaults", "config", "Config loading"), |
| ("user config file path resolution", "config", "Config path resolution"), |
| |
| ("LLM API call streaming response", "llm", "LLM streaming"), |
| ("model provider selection gemini openai", "llm", "Model provider"), |
| ("token counting and context window management", "llm", "Token counting"), |
| |
| ("context window governance truncation", "context_governance", "Context governance"), |
| ("context item priority ranking", "context", "Context priority"), |
| |
| ("tool dispatcher routing function calls", "dispatcher", "Tool dispatcher"), |
| ("dispatch tool call with arguments", "dispatcher", "Tool call dispatch"), |
| |
| ("event system publish subscribe hooks", "events", "Event system"), |
| ("streaming event output to terminal", "events", "Event streaming"), |
| |
| ("git branch creation and switching", "git_branch", "Git branch"), |
| ("github CLI integration shell command", "gh_cli", "GitHub CLI"), |
| |
| ("diff display unified patch format", "diff_display", "Diff display"), |
| ("show file changes colored diff output", "diff_display", "Colored diff"), |
| |
| ("plugin extension registration loading", "extensions", "Extension loading"), |
| |
| ("builtin skill definition and registration", "builtin_skill", "Builtin skills"), |
| ("skill slash command frontmatter parsing", "builtin_skill", "Skill frontmatter"), |
| |
| ("subagent invocation and communication", "agents", "Subagent invocation"), |
| ("agent message passing protocol", "agents", "Agent messaging"), |
| |
| ("pre-commit hook integration", "hooks", "Pre-commit hooks"), |
| |
| ("async utility helpers gather timeout", "async_utils", "Async utilities"), |
| |
| ("headless non-interactive execution mode", "headless", "Headless mode"), |
| |
| ("error handling exception retry logic", None, "Generic: error handling (open)"), |
| ("logging setup structured log output", None, "Generic: logging (open)"), |
| ("test fixtures mock patching pytest", "tests", "Test utilities"), |
| ] |
|
|
| assert len(QUERIES) == 30, f"Expected 30 queries, got {len(QUERIES)}" |
|
|
|
|
| |
| |
| |
| @dataclass |
| class QueryResult: |
| query: str |
| description: str |
| expected_file: str | None |
| latency_ms: float |
| top1_file: str | None |
| top5_files: list[str] |
| top10_files: list[str] |
| hit_at_1: bool | None |
| hit_at_5: bool | None |
| hit_at_10: bool | None |
| top1_score: float |
|
|
|
|
| @dataclass |
| class ModelBenchResult: |
| model_name: str |
| model_label: str |
| embedding_dim: int |
| model_size_mb: float |
| index_dir: str |
|
|
| |
| indexed_files: int = 0 |
| total_chunks: int = 0 |
| chunk_size: int = 0 |
| chunk_overlap: int = 0 |
| languages: dict[str, int] = field(default_factory=dict) |
| index_time_ms: float = 0.0 |
| peak_ram_mb: float = 0.0 |
| memo_hits: int = 0 |
| memo_misses: int = 0 |
|
|
| |
| query_results: list[QueryResult] = field(default_factory=list) |
|
|
| |
| recall_at_1: float = 0.0 |
| recall_at_5: float = 0.0 |
| recall_at_10: float = 0.0 |
| mean_latency_ms: float = 0.0 |
| p50_latency_ms: float = 0.0 |
| p95_latency_ms: float = 0.0 |
| p99_latency_ms: float = 0.0 |
| evaluated_queries: int = 0 |
| open_queries: int = 0 |
|
|
|
|
| |
| |
| |
| def _check_hit(result_files: list[str], expected: str) -> bool: |
| return any(expected.lower() in f.lower() for f in result_files) |
|
|
|
|
| def run_benchmark( |
| root: Path, |
| model_label: str, |
| embedder: Any, |
| alpha: float | None, |
| top_k: int = 10, |
| verbose: bool = True, |
| ) -> ModelBenchResult: |
| model_name = getattr(embedder, "_model_id", None) or getattr( |
| embedder, "_model_name", "unknown" |
| ) |
| if verbose: |
| print(f"\n{'β' * 60}") |
| print(f" Benchmarking: {model_label}") |
| print(f" Model ID : {model_name}") |
| print(f"{'β' * 60}") |
|
|
| |
| tmp_index = tempfile.mkdtemp(prefix=f"vtx_bench_{model_label.replace(' ', '_')}_") |
|
|
| chunk_cfg = ChunkConfig(chunk_size=1500, chunk_overlap=200) |
| indexer = CodebaseIndexer( |
| root=root, model=embedder, index_dir=tmp_index, chunk_config=chunk_cfg |
| ) |
|
|
| |
| gc.collect() |
| ram_before = _peak_rss_mb() |
|
|
| if verbose: |
| print(" [1/3] Indexing codebase β¦", end="", flush=True) |
|
|
| t0 = time.perf_counter() |
| stats: IndexStats = indexer.index(force=True, include_text_files=False) |
| index_elapsed = (time.perf_counter() - t0) * 1000 |
|
|
| ram_after = _peak_rss_mb() |
| peak_ram = max(ram_after - ram_before, 0.0) |
|
|
| if verbose: |
| print(f" done in {index_elapsed:.0f} ms ({stats.total_chunks} chunks)") |
|
|
| |
| emb_dim = embedder.dim |
| model_size_mb = 0.0 |
| |
| if hasattr(embedder, "_model") and embedder._model is not None: |
| raw = embedder._model |
| if hasattr(raw, "model_size_mb"): |
| model_size_mb = raw.model_size_mb |
| elif hasattr(raw, "dim"): |
| model_size_mb = 0.0 |
|
|
| result = ModelBenchResult( |
| model_name=model_name, |
| model_label=model_label, |
| embedding_dim=emb_dim, |
| model_size_mb=model_size_mb, |
| index_dir=tmp_index, |
| indexed_files=stats.indexed_files, |
| total_chunks=stats.total_chunks, |
| chunk_size=chunk_cfg.chunk_size, |
| chunk_overlap=chunk_cfg.chunk_overlap, |
| languages=dict(stats.languages), |
| index_time_ms=round(index_elapsed, 1), |
| peak_ram_mb=round(peak_ram, 1), |
| memo_hits=stats.memo_hits, |
| memo_misses=stats.memo_misses, |
| ) |
|
|
| |
| if verbose: |
| print(f" [2/3] Running {len(QUERIES)} queries β¦") |
|
|
| latencies: list[float] = [] |
| hits1: list[bool] = [] |
| hits5: list[bool] = [] |
| hits10: list[bool] = [] |
| open_count = 0 |
|
|
| for i, (query, expected_file, description) in enumerate(QUERIES): |
| t_q = time.perf_counter() |
| search_results = indexer.search(query, top_k=top_k, alpha=alpha) |
| q_elapsed = (time.perf_counter() - t_q) * 1000 |
| latencies.append(q_elapsed) |
|
|
| top_files_all = [r.chunk.file_path for r in search_results] |
| top1_file = top_files_all[0] if top_files_all else None |
| top5_files = top_files_all[:5] |
| top10_files = top_files_all[:10] |
| top1_score = search_results[0].score if search_results else 0.0 |
|
|
| if expected_file is None: |
| hit1 = hit5 = hit10 = None |
| open_count += 1 |
| else: |
| hit1 = _check_hit(top_files_all[:1], expected_file) |
| hit5 = _check_hit(top5_files, expected_file) |
| hit10 = _check_hit(top10_files, expected_file) |
| hits1.append(hit1) |
| hits5.append(hit5) |
| hits10.append(hit10) |
|
|
| qr = QueryResult( |
| query=query, |
| description=description, |
| expected_file=expected_file, |
| latency_ms=round(q_elapsed, 2), |
| top1_file=top1_file, |
| top5_files=top5_files, |
| top10_files=top10_files, |
| hit_at_1=hit1, |
| hit_at_5=hit5, |
| hit_at_10=hit10, |
| top1_score=round(top1_score, 4), |
| ) |
| result.query_results.append(qr) |
|
|
| if verbose: |
| status = ("β" if hit1 else "~" if hit5 else "β") if expected_file else "β" |
| print( |
| f" [{i + 1:02d}/{len(QUERIES)}] {status} {description[:42]:<42} " |
| f"{q_elapsed:6.1f}ms score={top1_score:.3f}" |
| ) |
|
|
| |
| import statistics |
|
|
| n_eval = len(hits1) |
| result.evaluated_queries = n_eval |
| result.open_queries = open_count |
| result.recall_at_1 = round(sum(hits1) / n_eval, 4) if n_eval else 0.0 |
| result.recall_at_5 = round(sum(hits5) / n_eval, 4) if n_eval else 0.0 |
| result.recall_at_10 = round(sum(hits10) / n_eval, 4) if n_eval else 0.0 |
| result.mean_latency_ms = round(statistics.mean(latencies), 2) |
| result.p50_latency_ms = round(statistics.median(latencies), 2) |
| sorted_lat = sorted(latencies) |
| result.p95_latency_ms = round(sorted_lat[int(0.95 * len(sorted_lat)) - 1], 2) |
| result.p99_latency_ms = round(sorted_lat[int(0.99 * len(sorted_lat)) - 1], 2) |
|
|
| if verbose: |
| print(" [3/3] Cleanup temporary index β¦") |
|
|
| shutil.rmtree(tmp_index, ignore_errors=True) |
|
|
| return result |
|
|
|
|
| |
| |
| |
| _RESET = "\033[0m" |
| _BOLD = "\033[1m" |
| _GREEN = "\033[92m" |
| _RED = "\033[91m" |
| _YELLOW = "\033[93m" |
| _CYAN = "\033[96m" |
| _MAGENTA = "\033[95m" |
| _DIM = "\033[2m" |
|
|
|
|
| def _pct(v: float) -> str: |
| colour = _GREEN if v >= 0.8 else (_YELLOW if v >= 0.5 else _RED) |
| return f"{colour}{v * 100:.1f}%{_RESET}" |
|
|
|
|
| def _ms(v: float) -> str: |
| colour = _GREEN if v < 50 else (_YELLOW if v < 200 else _RED) |
| return f"{colour}{v:.1f}ms{_RESET}" |
|
|
|
|
| def print_report(results: list[ModelBenchResult], alpha: float | None) -> None: |
| print(f"\n{_BOLD}{'β' * 70}{_RESET}") |
| print(f"{_BOLD}{' VORTEXA BENCHMARK REPORT':^70}{_RESET}") |
| print(f"{_BOLD}{'β' * 70}{_RESET}") |
| print(" Codebase : VTX (vtx-coding-agent)") |
| n_eval_q = sum(1 for q in QUERIES if q[1] is not None) |
| n_open_q = sum(1 for q in QUERIES if q[1] is None) |
| print(f" Queries : {len(QUERIES)} total ({n_eval_q} evaluated, {n_open_q} open)") |
| print(f" alpha : {'adaptive (auto)' if alpha is None else alpha}") |
| print() |
|
|
| |
| col = 26 |
| hdr = f"{'Metric':<{col}}" |
| for r in results: |
| hdr += f" {_BOLD}{r.model_label[:22]:<22}{_RESET}" |
| print(hdr) |
| print(f"{'β' * (col + len(results) * 24)}") |
|
|
| def row(label: str, vals: list[str]) -> None: |
| line = f"{label:<{col}}" |
| for v in vals: |
| line += f" {v:<22}" |
| print(line) |
|
|
| |
| row("Model ID", [f"{_DIM}{r.model_name[:20]}{_RESET}" for r in results]) |
| row("Embedding dim", [str(r.embedding_dim) for r in results]) |
| row( |
| "Model size (MB)", |
| [f"{r.model_size_mb:.1f}" if r.model_size_mb else "n/a" for r in results], |
| ) |
| row("Chunk size / overlap", [f"{r.chunk_size} / {r.chunk_overlap}" for r in results]) |
| row("Files indexed", [str(r.indexed_files) for r in results]) |
| row("Total chunks", [str(r.total_chunks) for r in results]) |
| row("Memo hits / misses", [f"{r.memo_hits} / {r.memo_misses}" for r in results]) |
| row("Index time", [_ms(r.index_time_ms) for r in results]) |
| row("Peak RAM delta (MB)", [f"{r.peak_ram_mb:.1f}" for r in results]) |
| print(f"{'β' * (col + len(results) * 24)}") |
|
|
| |
| row("R@1 (evaluated)", [_pct(r.recall_at_1) for r in results]) |
| row("R@5 (evaluated)", [_pct(r.recall_at_5) for r in results]) |
| row("R@10 (evaluated)", [_pct(r.recall_at_10) for r in results]) |
| print(f"{'β' * (col + len(results) * 24)}") |
|
|
| |
| row("Latency mean", [_ms(r.mean_latency_ms) for r in results]) |
| row("Latency p50", [_ms(r.p50_latency_ms) for r in results]) |
| row("Latency p95", [_ms(r.p95_latency_ms) for r in results]) |
| row("Latency p99", [_ms(r.p99_latency_ms) for r in results]) |
| print(f"{'β' * (col + len(results) * 24)}") |
|
|
| |
| for r in results: |
| langs_str = ", ".join( |
| f"{k}:{v}" for k, v in sorted(r.languages.items(), key=lambda x: -x[1])[:5] |
| ) |
| row("Top languages", [langs_str if rr is r else "" for rr in results]) |
| break |
|
|
| print() |
|
|
| |
| print(f"{_BOLD}Per-Query Breakdown{_RESET}") |
| print(f"{'β' * 100}") |
| header = f"{'#':<3} {'Description':<44} {'Expected':<18}" |
| for _r in results: |
| header += f" {'@1 @5 @10':^12} {'ms':>6}" |
| print(header) |
| print(f"{'β' * 100}") |
|
|
| for i, (_query, expected, desc) in enumerate(QUERIES): |
| line = f"{i + 1:<3} {desc[:43]:<44} {(expected or '(open)')[:17]:<18}" |
| for r in results: |
| qr = r.query_results[i] |
| if qr.hit_at_1 is None: |
| hits = f"{'β':^4} {'β':^4} {'β':^4}" |
| else: |
| h1 = f"{_GREEN}β{_RESET}" if qr.hit_at_1 else f"{_RED}β{_RESET}" |
| h5 = f"{_GREEN}β{_RESET}" if qr.hit_at_5 else f"{_RED}β{_RESET}" |
| h10 = f"{_GREEN}β{_RESET}" if qr.hit_at_10 else f"{_RED}β{_RESET}" |
| hits = f"{h1:<4} {h5:<4} {h10:<4}" |
| line += f" {hits} {qr.latency_ms:6.1f}" |
| print(line) |
|
|
| print(f"{'β' * 100}") |
| print() |
|
|
|
|
| |
| |
| |
| def save_json_report(results: list[ModelBenchResult], output_path: Path) -> None: |
| report = [] |
| for r in results: |
| d = asdict(r) |
| report.append(d) |
| output_path.write_text(json.dumps(report, indent=2)) |
| print(f" JSON report saved β {output_path}") |
|
|
|
|
| |
| |
| |
| def main() -> None: |
| parser = argparse.ArgumentParser( |
| description="Benchmark vortexa indexers on the VTX codebase.", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| ) |
| parser.add_argument( |
| "--root", |
| type=Path, |
| default=Path(__file__).parent, |
| help="Codebase root to index (default: this file's directory)", |
| ) |
| parser.add_argument( |
| "--alpha", |
| type=float, |
| default=None, |
| help="Hybrid search alpha (0=BM25, 1=semantic, None=adaptive)", |
| ) |
| parser.add_argument( |
| "--top-k", type=int, default=10, help="Max results per query (default: 10)" |
| ) |
| parser.add_argument( |
| "--no-m2v", action="store_true", help="Skip Model2Vec benchmark (faster, fewer deps)" |
| ) |
| parser.add_argument("--no-lf4", action="store_true", help="Skip LF4 (vtx-embed-7M) benchmark") |
| parser.add_argument( |
| "--output", |
| type=Path, |
| default=Path("benchmark_results.json"), |
| help="Path for JSON report output", |
| ) |
| parser.add_argument("--quiet", action="store_true", help="Suppress per-query progress output") |
| args = parser.parse_args() |
|
|
| root = args.root.resolve() |
| if not root.exists(): |
| sys.exit(f"[ERROR] Root not found: {root}") |
|
|
| verbose = not args.quiet |
|
|
| print(f"\n{_BOLD}{_CYAN}Vortexa Indexer Benchmark{_RESET}") |
| print(f"{_DIM}Codebase root: {root}{_RESET}") |
| import importlib.util |
|
|
| _vsite = importlib.util.find_spec("vortexa") |
| _vloc = Path(_vsite.origin).parent if _vsite else "unknown" |
| print(f"{_DIM}vortexa site : {_vloc}{_RESET}\n") |
|
|
| models_to_run: list[tuple[str, Any]] = [] |
|
|
| if not args.no_lf4: |
| models_to_run.append(("LF4 vtx-embed-1M (nano)", LF4Embedder("VTXAI/vtx-embed-1M"))) |
| models_to_run.append(("LF4 vtx-embed-7M (mini)", LF4Embedder("VTXAI/vtx-embed-7M"))) |
|
|
| if not args.no_m2v: |
| models_to_run.append( |
| ("Model2Vec JARVIS-tool-search", Model2VecEmbedder("AI4free/JARVIS-tool-search-v1")) |
| ) |
|
|
| if not models_to_run: |
| sys.exit("[ERROR] All models disabled β enable at least one.") |
|
|
| |
| print(f"{_BOLD}Pre-warming models (downloading weights if needed)β¦{_RESET}") |
| for label, emb in models_to_run: |
| print(f" loading {label} β¦", end="", flush=True) |
| t_load = time.perf_counter() |
| _ = emb.dim |
| print(f" ready in {(time.perf_counter() - t_load) * 1000:.0f}ms") |
|
|
| results: list[ModelBenchResult] = [] |
|
|
| for label, emb in models_to_run: |
| r = run_benchmark( |
| root=root, |
| model_label=label, |
| embedder=emb, |
| alpha=args.alpha, |
| top_k=args.top_k, |
| verbose=verbose, |
| ) |
| results.append(r) |
|
|
| print_report(results, args.alpha) |
| save_json_report(results, args.output) |
|
|
| |
| if len(results) > 1: |
| best_r1 = max(results, key=lambda r: r.recall_at_1) |
| best_speed = min(results, key=lambda r: r.mean_latency_ms) |
| r1_pct = f"{best_r1.recall_at_1 * 100:.1f}%" |
| spd_ms = f"{best_speed.mean_latency_ms:.1f}ms mean" |
| print(f"{_BOLD}Winner R@1 :{_RESET} {_GREEN}{best_r1.model_label}{_RESET} ({r1_pct})") |
| print( |
| f"{_BOLD}Winner Speed :{_RESET} {_GREEN}{best_speed.model_label}{_RESET} ({spd_ms})" |
| ) |
| print() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|