Files
cleveragents-core/features/steps/scaling_performance_steps.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

217 lines
8.1 KiB
Python

"""Step definitions for large-project scaling performance validation."""
from __future__ import annotations
import json
import math
from pathlib import Path
from typing import Any
from behave import given, then, when
from behave.runner import Context
_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "scale"
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a scaling performance test environment is ready")
def step_scaling_perf_env_ready(context: Context) -> None:
"""Verify scale test fixtures directory exists."""
assert _FIXTURES_DIR.is_dir(), f"Fixtures dir missing: {_FIXTURES_DIR}"
context.sp_metadata = None
context.sp_thresholds = None
# ---------------------------------------------------------------------------
# Fixture loading
# ---------------------------------------------------------------------------
@when("I scaling perf load the scale metadata fixture")
def step_scaling_perf_load_metadata(context: Context) -> None:
path = _FIXTURES_DIR / "scale_metadata.json"
assert path.exists(), f"Missing fixture: {path}"
with open(path) as fh:
context.sp_metadata = json.load(fh)
@when("I scaling perf load the baseline thresholds fixture")
def step_scaling_perf_load_thresholds(context: Context) -> None:
path = _FIXTURES_DIR / "baseline_thresholds.json"
assert path.exists(), f"Missing fixture: {path}"
with open(path) as fh:
context.sp_thresholds = json.load(fh)
# ---------------------------------------------------------------------------
# Profile validation
# ---------------------------------------------------------------------------
def _get_profile(context: Context, name: str) -> dict[str, Any]:
"""Return the profile dict with the given name, or fail."""
assert context.sp_metadata is not None, "Metadata not loaded"
for p in context.sp_metadata["scale_profiles"]:
if p["name"] == name:
return p
raise AssertionError(f"Profile {name!r} not found in metadata")
@then('the scaling perf metadata should contain profile "{name}"')
def step_scaling_perf_has_profile(context: Context, name: str) -> None:
_get_profile(context, name)
@then('the scaling perf profile "{name}" should have file count {count:d}')
def step_scaling_perf_profile_file_count(
context: Context, name: str, count: int
) -> None:
profile = _get_profile(context, name)
actual = profile["file_count"]
assert actual == count, f"Profile {name}: expected {count}, got {actual}"
@then('the scaling perf language mix should sum to 1.0 for profile "{name}"')
def step_scaling_perf_lang_mix_sums(context: Context, name: str) -> None:
profile = _get_profile(context, name)
total = sum(profile["language_mix"].values())
assert math.isclose(total, 1.0, rel_tol=1e-6), (
f"Language mix for {name} sums to {total}, expected 1.0"
)
# ---------------------------------------------------------------------------
# Threshold coverage
# ---------------------------------------------------------------------------
@then('the scaling perf indexing thresholds should cover "{file_count}"')
def step_scaling_perf_indexing_covers(context: Context, file_count: str) -> None:
assert context.sp_thresholds is not None
assert file_count in context.sp_thresholds["indexing"], (
f"Missing indexing threshold for {file_count}"
)
@then('the scaling perf decomposition thresholds should cover "{file_count}"')
def step_scaling_perf_decomp_covers(context: Context, file_count: str) -> None:
assert context.sp_thresholds is not None
assert file_count in context.sp_thresholds["decomposition"], (
f"Missing decomposition threshold for {file_count}"
)
# ---------------------------------------------------------------------------
# Monotonicity
# ---------------------------------------------------------------------------
@then('scaling perf p50 should be less than p95 for "{key}" indexing')
def step_scaling_perf_p50_lt_p95(context: Context, key: str) -> None:
assert context.sp_thresholds is not None
t = context.sp_thresholds["indexing"][key]
assert t["p50_ms"] < t["p95_ms"], (
f"p50 ({t['p50_ms']}) >= p95 ({t['p95_ms']}) for {key}"
)
@then('scaling perf p95 should be less than p99 for "{key}" indexing')
def step_scaling_perf_p95_lt_p99(context: Context, key: str) -> None:
assert context.sp_thresholds is not None
t = context.sp_thresholds["indexing"][key]
assert t["p95_ms"] < t["p99_ms"], (
f"p95 ({t['p95_ms']}) >= p99 ({t['p99_ms']}) for {key}"
)
# ---------------------------------------------------------------------------
# Sub-linear scaling checks
# ---------------------------------------------------------------------------
@then("scaling perf 50K indexing p50 should be less than 5x the 10K p50")
def step_scaling_perf_50k_sublinear(context: Context) -> None:
assert context.sp_thresholds is not None
idx = context.sp_thresholds["indexing"]
p50_10k = idx["10000_files"]["p50_ms"]
p50_50k = idx["50000_files"]["p50_ms"]
assert p50_50k < 5 * p50_10k, (
f"50K p50 ({p50_50k}) should be < 5x 10K p50 ({5 * p50_10k})"
)
@then("scaling perf 100K indexing p50 should be less than 2.5x the 50K p50")
def step_scaling_perf_100k_sublinear(context: Context) -> None:
assert context.sp_thresholds is not None
idx = context.sp_thresholds["indexing"]
p50_50k = idx["50000_files"]["p50_ms"]
p50_100k = idx["100000_files"]["p50_ms"]
limit = int(2.5 * p50_50k)
assert p50_100k < limit, f"100K p50 ({p50_100k}) should be < 2.5x 50K p50 ({limit})"
# ---------------------------------------------------------------------------
# Context assembly
# ---------------------------------------------------------------------------
@then("the scaling perf thresholds should have context assembly section")
def step_scaling_perf_has_ctx_assembly(context: Context) -> None:
assert context.sp_thresholds is not None
assert "context_assembly" in context.sp_thresholds
@then('the scaling perf context assembly thresholds should cover "{fragment_count}"')
def step_scaling_perf_ctx_assembly_covers(
context: Context, fragment_count: str
) -> None:
assert context.sp_thresholds is not None
assert fragment_count in context.sp_thresholds["context_assembly"], (
f"Missing context_assembly threshold for {fragment_count}"
)
# ---------------------------------------------------------------------------
# Execution throughput
# ---------------------------------------------------------------------------
@then("the scaling perf thresholds should have execution throughput section")
def step_scaling_perf_has_exec_throughput(context: Context) -> None:
assert context.sp_thresholds is not None
assert "execution_throughput" in context.sp_thresholds
@then('the scaling perf execution thresholds should cover "{plan_count}"')
def step_scaling_perf_exec_covers(context: Context, plan_count: str) -> None:
assert context.sp_thresholds is not None
assert plan_count in context.sp_thresholds["execution_throughput"], (
f"Missing execution_throughput threshold for {plan_count}"
)
# ---------------------------------------------------------------------------
# Memory thresholds
# ---------------------------------------------------------------------------
@then('the scaling perf memory thresholds should cover "{file_count}"')
def step_scaling_perf_memory_covers(context: Context, file_count: str) -> None:
assert context.sp_thresholds is not None
assert file_count in context.sp_thresholds["memory_usage_mb"], (
f"Missing memory_usage_mb threshold for {file_count}"
)
@then('scaling perf memory peak should exceed steady state for "{file_count}"')
def step_scaling_perf_memory_peak_gt_steady(context: Context, file_count: str) -> None:
assert context.sp_thresholds is not None
mem = context.sp_thresholds["memory_usage_mb"][file_count]
assert mem["peak_mb"] > mem["steady_state_mb"], (
f"Peak ({mem['peak_mb']}) <= steady ({mem['steady_state_mb']}) for {file_count}"
)