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>
200 lines
7.1 KiB
Python
200 lines
7.1 KiB
Python
"""Robot Framework helper for scale test fixture validation.
|
|
|
|
Provides a CLI-style interface for Robot to invoke fixture loading,
|
|
metadata validation, threshold checks, and profile simulation.
|
|
Exit code 0 = success, 1 = failure.
|
|
|
|
Usage:
|
|
python robot/helper_scale_test.py load-metadata
|
|
python robot/helper_scale_test.py validate-profiles
|
|
python robot/helper_scale_test.py validate-thresholds
|
|
python robot/helper_scale_test.py check-monotonicity
|
|
python robot/helper_scale_test.py simulate-distribution
|
|
python robot/helper_scale_test.py check-generator-docs
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# 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:
|
|
"""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:
|
|
return json.load(fh)
|
|
|
|
|
|
def _cmd_load_metadata() -> int:
|
|
"""Load scale_metadata.json and verify basic structure."""
|
|
data = _load_json("scale_metadata.json")
|
|
assert "schema_version" in data, "Missing schema_version"
|
|
assert "scale_profiles" in data, "Missing scale_profiles"
|
|
assert len(data["scale_profiles"]) > 0, "No profiles defined"
|
|
print(f"scale-metadata-ok: {len(data['scale_profiles'])} profiles loaded")
|
|
return 0
|
|
|
|
|
|
def _cmd_validate_profiles() -> int:
|
|
"""Validate all scale profiles have required fields."""
|
|
data = _load_json("scale_metadata.json")
|
|
required_fields = [
|
|
"name",
|
|
"file_count",
|
|
"total_size_mb",
|
|
"language_mix",
|
|
"expected_index_time_s",
|
|
"expected_decomposition_time_s",
|
|
]
|
|
expected_names = {"small", "medium", "large", "xlarge", "xxlarge"}
|
|
actual_names = {p["name"] for p in data["scale_profiles"]}
|
|
assert actual_names == expected_names, (
|
|
f"Expected profiles {expected_names}, got {actual_names}"
|
|
)
|
|
for profile in data["scale_profiles"]:
|
|
for field in required_fields:
|
|
assert field in profile, f"Missing {field} in profile {profile.get('name')}"
|
|
# Verify language mix sums to 1.0
|
|
mix_total = sum(profile["language_mix"].values())
|
|
assert math.isclose(mix_total, 1.0, rel_tol=1e-6), (
|
|
f"Language mix for {profile['name']} sums to {mix_total}"
|
|
)
|
|
print(f"scale-profiles-ok: {len(data['scale_profiles'])} profiles validated")
|
|
return 0
|
|
|
|
|
|
def _cmd_validate_thresholds() -> int:
|
|
"""Load and validate baseline thresholds structure."""
|
|
data = _load_json("baseline_thresholds.json")
|
|
assert "schema_version" in data, "Missing schema_version"
|
|
for section in ("indexing", "decomposition"):
|
|
assert section in data, f"Missing section: {section}"
|
|
for file_count in (
|
|
"1000_files",
|
|
"5000_files",
|
|
"10000_files",
|
|
"50000_files",
|
|
"100000_files",
|
|
):
|
|
assert file_count in data[section], f"Missing {file_count} in {section}"
|
|
for pct in ("p50_ms", "p95_ms", "p99_ms"):
|
|
assert pct in data[section][file_count], (
|
|
f"Missing {pct} in {section}[{file_count}]"
|
|
)
|
|
print("scale-thresholds-ok: all threshold sections validated")
|
|
return 0
|
|
|
|
|
|
def _cmd_check_monotonicity() -> int:
|
|
"""Verify p50 < p95 < p99 for all threshold entries."""
|
|
data = _load_json("baseline_thresholds.json")
|
|
for section in ("indexing", "decomposition"):
|
|
for file_count, thresholds in data[section].items():
|
|
p50 = thresholds["p50_ms"]
|
|
p95 = thresholds["p95_ms"]
|
|
p99 = thresholds["p99_ms"]
|
|
assert p50 < p95 < p99, (
|
|
f"Non-monotonic in {section}[{file_count}]: "
|
|
f"p50={p50}, p95={p95}, p99={p99}"
|
|
)
|
|
print("scale-monotonicity-ok: all thresholds are monotonically increasing")
|
|
return 0
|
|
|
|
|
|
def _cmd_simulate_distribution() -> int:
|
|
"""Generate file distributions for all profiles and verify totals."""
|
|
data = _load_json("scale_metadata.json")
|
|
for profile in data["scale_profiles"]:
|
|
total = profile["file_count"]
|
|
mix = profile["language_mix"]
|
|
distribution: dict[str, int] = {}
|
|
allocated = 0
|
|
items = sorted(mix.items(), key=lambda x: x[1], reverse=True)
|
|
for i, (lang, ratio) in enumerate(items):
|
|
if i == len(items) - 1:
|
|
distribution[lang] = total - allocated
|
|
else:
|
|
count = round(total * ratio)
|
|
distribution[lang] = count
|
|
allocated += count
|
|
|
|
actual_total = sum(distribution.values())
|
|
assert actual_total == total, (
|
|
f"Profile {profile['name']}: expected {total} files, got {actual_total}"
|
|
)
|
|
# Verify distribution roughly matches mix
|
|
for lang, expected_ratio in mix.items():
|
|
actual_ratio = distribution[lang] / total
|
|
assert abs(actual_ratio - expected_ratio) < 0.05, (
|
|
f"Profile {profile['name']}, lang {lang}: "
|
|
f"expected ~{expected_ratio}, got {actual_ratio}"
|
|
)
|
|
print(f" {profile['name']}: {distribution}")
|
|
|
|
print("scale-distribution-ok: all profile distributions validated")
|
|
return 0
|
|
|
|
|
|
def _cmd_check_generator_docs() -> int:
|
|
"""Verify the generator instructions document exists and has content."""
|
|
path = _FIXTURES_DIR / "generator_instructions.md"
|
|
assert path.exists(), f"Generator instructions not found: {path}"
|
|
content = path.read_text()
|
|
assert len(content) > 100, "Generator instructions file is too short"
|
|
assert "## Overview" in content, "Missing Overview section"
|
|
assert "## Generating" in content or "## Prerequisites" in content, (
|
|
"Missing generation instructions"
|
|
)
|
|
print("scale-generator-docs-ok: generator instructions validated")
|
|
return 0
|
|
|
|
|
|
def main() -> int:
|
|
"""Entry point called by Robot Framework ``Run Process``."""
|
|
if len(sys.argv) < 2:
|
|
print(
|
|
"Usage: helper_scale_test.py "
|
|
"<load-metadata|validate-profiles|validate-thresholds|"
|
|
"check-monotonicity|simulate-distribution|check-generator-docs>"
|
|
)
|
|
return 1
|
|
|
|
command = sys.argv[1]
|
|
|
|
commands = {
|
|
"load-metadata": _cmd_load_metadata,
|
|
"validate-profiles": _cmd_validate_profiles,
|
|
"validate-thresholds": _cmd_validate_thresholds,
|
|
"check-monotonicity": _cmd_check_monotonicity,
|
|
"simulate-distribution": _cmd_simulate_distribution,
|
|
"check-generator-docs": _cmd_check_generator_docs,
|
|
}
|
|
|
|
handler = commands.get(command)
|
|
if handler is None:
|
|
print(f"Unknown command: {command}")
|
|
return 1
|
|
|
|
try:
|
|
return handler()
|
|
except (AssertionError, KeyError, json.JSONDecodeError) as exc:
|
|
print(f"FAIL: {exc}")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|