Files
cleveragents-core/robot/helper_scaling_performance.py
T
brent.edwards b88bc0ec1b
CI / build (push) Successful in 19s
CI / lint (push) Successful in 3m19s
CI / quality (push) Successful in 3m43s
CI / typecheck (push) Successful in 3m55s
CI / security (push) Successful in 4m2s
CI / unit_tests (push) Successful in 6m39s
CI / integration_tests (push) Successful in 6m48s
CI / docker (push) Successful in 1m7s
CI / e2e_tests (push) Successful in 9m7s
CI / benchmark-regression (push) Has been skipped
CI / coverage (push) Failing after 16m36s
CI / benchmark-publish (push) Successful in 25m51s
CI / status-check (push) Failing after 4s
feat(perf): large project scaling tests (#984)
## Summary

Add large project scaling benchmarks and tests at production scale (10K–100K files).

### New ASV Benchmarks

**IndexingScalingSuite** (`large_project_scaling_bench.py`):
- `time_walk_and_index` at 1K/10K/50K/100K files
- `time_incremental_refresh` (1% modified files)
- `track_indexed_file_count`, `track_tokens_per_second`

**ContextAssemblyScalingSuite** (`context_assembly_scaling_bench.py`):
- `time_full_pipeline` at 100/1K/5K/10K fragments
- `time_tiered_strategy`, `time_recency_strategy`
- `track_assembled_tokens`, `track_fragments_per_second`

**ExecutionThroughputSuite** (`execution_throughput_bench.py`):
- `time_sequential_plans` at 10/50/100 plans
- `time_executor_construction`, `time_decision_tree_scaling`

### Scale Fixture Updates

- Added `xlarge` (50K files) and `xxlarge` (100K files) profiles to `scale_metadata.json`
- Added 50K/100K thresholds to `baseline_thresholds.json`
- Added `context_assembly` and `execution_throughput` threshold sections

### Tests & Documentation

- 15 Behave scenarios validating profiles, thresholds, monotonicity, memory budgets
- 6 Robot integration tests including live 1K-file indexing throughput check
- `docs/reference/scaling_baselines.md` documenting all baseline metrics

### Quality Gates

| Session | Result |
|---|---|
| `nox -s lint` | PASS |
| `nox -s typecheck` | PASS (0 errors) |
| `nox -s unit_tests` | PASS (10,910 scenarios) |
| `nox -s integration_tests` | PASS (1,526 tests) |
| `nox -s coverage_report` | 97% (>= 97%) |

Closes #859

Reviewed-on: #984
Co-authored-by: Brent E. Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent E. Edwards <brent.edwards@cleverthis.com>
2026-03-21 04:46:45 +00:00

208 lines
7.7 KiB
Python

"""Robot Framework helper for large-project scaling performance tests.
Provides a CLI-style interface for Robot to invoke extended fixture
validation, threshold sub-linearity checks, and a small-scale indexing
throughput test.
Usage:
python robot/helper_scaling_performance.py check-extended-profiles
python robot/helper_scaling_performance.py check-extended-thresholds
python robot/helper_scaling_performance.py check-context-assembly
python robot/helper_scaling_performance.py check-execution-throughput
python robot/helper_scaling_performance.py check-sublinear
python robot/helper_scaling_performance.py run-small-index
"""
from __future__ import annotations
import json
import math
import os
import shutil
import sys
import tempfile
import time
from pathlib import Path
from typing import Any
# Ensure the src directory is on the import path.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "features" / "fixtures" / "scale"
def _load_json(filename: str) -> dict[str, Any]:
"""Load a JSON fixture file."""
path = _FIXTURES_DIR / filename
if not path.exists():
print(f"ERROR: fixture not found: {path}")
sys.exit(1)
with open(path) as fh:
result: dict[str, Any] = json.load(fh)
return result
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
def _cmd_check_extended_profiles() -> int:
"""Verify xlarge and xxlarge profiles are present with valid mixes."""
data = _load_json("scale_metadata.json")
names = {p["name"] for p in data["scale_profiles"]}
assert "xlarge" in names, "Missing xlarge profile"
assert "xxlarge" in names, "Missing xxlarge profile"
for profile in data["scale_profiles"]:
if profile["name"] in ("xlarge", "xxlarge"):
total = sum(profile["language_mix"].values())
assert math.isclose(total, 1.0, rel_tol=1e-6), (
f"Language mix for {profile['name']} sums to {total}"
)
assert profile["file_count"] >= 50000, (
f"File count for {profile['name']} too low: {profile['file_count']}"
)
print("scaling-profiles-ok: xlarge and xxlarge profiles validated")
return 0
def _cmd_check_extended_thresholds() -> int:
"""Verify 50K and 100K indexing and decomposition thresholds."""
data = _load_json("baseline_thresholds.json")
for section in ("indexing", "decomposition"):
for key in ("50000_files", "100000_files"):
assert key in data[section], f"Missing {key} in {section}"
t = data[section][key]
for pct in ("p50_ms", "p95_ms", "p99_ms"):
assert pct in t, f"Missing {pct} in {section}[{key}]"
assert t["p50_ms"] < t["p95_ms"] < t["p99_ms"], (
f"Non-monotonic in {section}[{key}]"
)
print("scaling-thresholds-ok: 50K and 100K thresholds validated")
return 0
def _cmd_check_context_assembly() -> int:
"""Verify context assembly threshold section exists."""
data = _load_json("baseline_thresholds.json")
assert "context_assembly" in data, "Missing context_assembly section"
for key in ("5000_fragments", "10000_fragments"):
assert key in data["context_assembly"], f"Missing {key} in context_assembly"
print("scaling-ctx-assembly-ok: context assembly thresholds validated")
return 0
def _cmd_check_execution_throughput() -> int:
"""Verify execution throughput threshold section exists."""
data = _load_json("baseline_thresholds.json")
assert "execution_throughput" in data, "Missing execution_throughput section"
for key in ("10_plans", "50_plans", "100_plans"):
assert key in data["execution_throughput"], (
f"Missing {key} in execution_throughput"
)
print("scaling-exec-throughput-ok: execution throughput thresholds validated")
return 0
def _cmd_check_sublinear() -> int:
"""Verify sub-linear scaling of extended thresholds."""
data = _load_json("baseline_thresholds.json")
idx = data["indexing"]
p50_10k = idx["10000_files"]["p50_ms"]
p50_50k = idx["50000_files"]["p50_ms"]
p50_100k = idx["100000_files"]["p50_ms"]
# 50K should be less than 5x the 10K value (sub-linear)
assert p50_50k < 5 * p50_10k, f"50K p50 ({p50_50k}) >= 5x 10K p50 ({5 * p50_10k})"
# 100K should be less than 2.5x the 50K value (sub-linear)
limit = int(2.5 * p50_50k)
assert p50_100k < limit, f"100K p50 ({p50_100k}) >= 2.5x 50K p50 ({limit})"
print("scaling-sublinear-ok: sub-linear scaling verified")
return 0
def _cmd_run_small_index() -> int:
"""Create a 1K-file fixture, index it, verify throughput."""
from cleveragents.application.services.repo_indexing_utils import walk_and_index
tmpdir = tempfile.mkdtemp(prefix="robot-scale-idx-")
try:
# Create 1000 files
for i in range(1000):
subdir = os.path.join(tmpdir, f"pkg{i % 10}")
os.makedirs(subdir, exist_ok=True)
fpath = os.path.join(subdir, f"file_{i:04d}.py")
with open(fpath, "w") as fh:
fh.write(f"# module {i}\ndef func_{i}() -> int:\n return {i}\n")
t0 = time.perf_counter()
records = walk_and_index(
root=Path(tmpdir),
include_globs=(),
exclude_globs=("*.pyc",),
max_file_size=None,
max_total_size=None,
)
elapsed = time.perf_counter() - t0
file_count = len(records)
total_tokens = sum(r.token_count for r in records)
throughput = total_tokens / elapsed if elapsed > 0 else 0.0
print(f" files_indexed: {file_count}")
print(f" total_tokens: {total_tokens}")
print(f" elapsed_s: {elapsed:.3f}")
print(f" tokens_per_second: {throughput:.0f}")
# Minimum threshold: at least 500 files should be indexed
assert file_count >= 500, f"Only {file_count} files indexed, expected >= 500"
# Index should complete within the 1K threshold (12 seconds max)
assert elapsed < 12.0, f"Indexing took {elapsed:.1f}s, exceeds 12s threshold"
print("scaling-index-ok: small-scale indexing passed")
return 0
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
# ---------------------------------------------------------------------------
# Main dispatch
# ---------------------------------------------------------------------------
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 2:
print(
"Usage: helper_scaling_performance.py "
"<check-extended-profiles|check-extended-thresholds|"
"check-context-assembly|check-execution-throughput|"
"check-sublinear|run-small-index>"
)
return 1
command = sys.argv[1]
commands: dict[str, Any] = {
"check-extended-profiles": _cmd_check_extended_profiles,
"check-extended-thresholds": _cmd_check_extended_thresholds,
"check-context-assembly": _cmd_check_context_assembly,
"check-execution-throughput": _cmd_check_execution_throughput,
"check-sublinear": _cmd_check_sublinear,
"run-small-index": _cmd_run_small_index,
}
handler = commands.get(command)
if handler is None:
print(f"Unknown command: {command}")
return 1
try:
result: int = handler()
return result
except (AssertionError, KeyError, json.JSONDecodeError) as exc:
print(f"FAIL: {exc}")
return 1
if __name__ == "__main__":
sys.exit(main())