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
## 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>
217 lines
8.1 KiB
Python
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}"
|
|
)
|