Files
CoreRasurae 054b432197
CI / lint (pull_request) Successful in 56s
CI / typecheck (pull_request) Successful in 56s
CI / security (pull_request) Successful in 1m2s
CI / quality (pull_request) Successful in 41s
CI / integration_tests (pull_request) Successful in 1m11s
CI / build (pull_request) Successful in 45s
CI / unit_tests (pull_request) Successful in 3m29s
CI / coverage (pull_request) Successful in 3m14s
CI / status-check (pull_request) Successful in 4s
CI / lint (push) Successful in 48s
CI / typecheck (push) Successful in 58s
CI / security (push) Successful in 1m1s
CI / quality (push) Successful in 39s
CI / integration_tests (push) Successful in 1m15s
CI / build (push) Successful in 41s
CI / unit_tests (push) Successful in 3m31s
CI / coverage (push) Successful in 3m27s
CI / status-check (push) Successful in 3s
feat(registry): implement RegistryCache with LRU eviction and TTL
- Add RegistryCache with LRU eviction, TTL expiry, SHA-1 content validation
 - Add CacheFactory for centralised cache configuration (Factory Method pattern)
 - Add CacheStats dataclass for hit/miss/eviction observability
 - Implement singleflight coalescing for concurrent-miss protection
 - Use tuple keys for resolve store to prevent separator collision
 - Fix cancellation-safe singleflight via plain Future + asyncio.shield
 - Independent max_size budgets per store (content + resolve)
 - Concurrent test assertions strengthened
 - All test imports moved to module level per CONTRIBUTING.md
 - Unused dead code removed from tests

 ISSUES CLOSED: #28
2026-06-11 19:26:04 +01:00

256 lines
7.9 KiB
Python

"""ASV benchmarks for RegistryCache, CacheFactory, and cached resolution.
Measures performance of:
- get_package cache hit and cache miss+fetch
- resolve_package cache hit and cache miss+fetch
- singleflight coalescing (concurrent-miss deduplication)
- manual put (content warming), invalidate, and clear
- CacheFactory.create() construction
- End-to-end PackageContentResolver resolution with cache
"""
from __future__ import annotations
import asyncio
from typing import Any
from unittest.mock import AsyncMock, MagicMock
_STANDARD_CONTENT: dict[str, Any] = {
"name": "bench-pkg",
"type": "actor",
"version": "v1.0.0",
"config": {
"agent_type": "llm",
"model": "gpt-4",
"temperature": 0.7,
"system_prompt": "You are a benchmarking assistant.",
},
}
_RESOLVE_CONTENT: dict[str, Any] = {
"package_id": "pkg_act_0123456789abcdef0123456789abcdef01234567",
"type": "actor",
"name": "bench-pkg",
"version": "v1.0.0",
"config": {
"agent_type": "llm",
"model": "gpt-4",
"temperature": 0.7,
},
}
_STANDARD_PKG_ID: str = "pkg_act_0123456789abcdef0123456789abcdef01234567"
def _create_mock_client() -> MagicMock:
from cleveractors.registry.client import RegistryClient
mock = MagicMock(spec=RegistryClient)
mock.get_package = AsyncMock(return_value=dict(_STANDARD_CONTENT))
mock.resolve_package = AsyncMock(return_value=dict(_RESOLVE_CONTENT))
mock.close = AsyncMock()
return mock
class CacheGetPackageHitBenchmark:
"""Benchmark get_package() when content is already cached."""
def setup(self) -> None:
from cleveractors.registry.cache import RegistryCache
self.cache = RegistryCache(_create_mock_client(), validate_content=False)
self.pkg_id = _STANDARD_PKG_ID
asyncio.run(self.cache.get_package(self.pkg_id))
def time_get_package_hit(self) -> None:
asyncio.run(self.cache.get_package(self.pkg_id))
def peakmem_get_package_hit(self) -> None:
asyncio.run(self.cache.get_package(self.pkg_id))
class CacheGetPackageMissBenchmark:
"""Benchmark get_package() on cold cache (miss + upstream fetch)."""
def setup(self) -> None:
from cleveractors.registry.cache import RegistryCache
self.mock_client = _create_mock_client()
self.pkg_id = _STANDARD_PKG_ID
def time_get_package_miss(self) -> None:
from cleveractors.registry.cache import RegistryCache
cache = RegistryCache(self.mock_client, validate_content=False)
asyncio.run(cache.get_package(self.pkg_id))
class CacheResolvePackageHitBenchmark:
"""Benchmark resolve_package() when the resolution is cached."""
params: list[str] = ["actor", "template", "skill"]
def setup(self, pkg_type: str) -> None:
from cleveractors.registry.cache import RegistryCache
self.cache = RegistryCache(_create_mock_client(), validate_content=False)
asyncio.run(
self.cache.resolve_package(
package_type=pkg_type,
namespace="bench-ns",
name="bench-pkg",
version="v1.0.0",
)
)
def time_resolve_package_hit(self, pkg_type: str) -> None:
asyncio.run(
self.cache.resolve_package(
package_type=pkg_type,
namespace="bench-ns",
name="bench-pkg",
version="v1.0.0",
)
)
class CacheResolvePackageMissBenchmark:
"""Benchmark resolve_package() on cold cache."""
params: list[str] = ["actor", "template", "skill"]
def setup(self, pkg_type: str) -> None:
from cleveractors.registry.cache import RegistryCache
self.mock_client = _create_mock_client()
def time_resolve_package_miss(self, pkg_type: str) -> None:
from cleveractors.registry.cache import RegistryCache
cache = RegistryCache(self.mock_client, validate_content=False)
asyncio.run(
cache.resolve_package(
package_type=pkg_type,
namespace="bench-ns",
name="bench-pkg",
version="v1.0.0",
)
)
class CacheSingleflightBenchmark:
"""Benchmark the singleflight coalescing under concurrent access.
When multiple coroutines request the same cold key concurrently,
only one upstream fetch is performed — the rest join the in-flight
future. This benchmark measures the throughput of N concurrent
get_package calls for the same cold key.
"""
params: list[int] = [10, 50]
def setup(self, concurrency: int) -> None:
from cleveractors.registry.cache import RegistryCache
self.cache = RegistryCache(_create_mock_client(), validate_content=False)
self.pkg_id = _STANDARD_PKG_ID
self.concurrency = concurrency
def time_get_package_concurrent(self, concurrency: int) -> None:
async def _run():
tasks = [
asyncio.ensure_future(self.cache.get_package(self.pkg_id))
for _ in range(self.concurrency)
]
await asyncio.gather(*tasks)
asyncio.run(_run())
class CacheInvalidateBenchmark:
"""Benchmark per-entry invalidation."""
def setup(self) -> None:
from cleveractors.registry.cache import RegistryCache
self.cache = RegistryCache(_create_mock_client(), validate_content=False)
self.pkg_id = _STANDARD_PKG_ID
asyncio.run(self.cache.get_package(self.pkg_id))
def time_invalidate(self) -> None:
asyncio.run(self.cache.invalidate(self.pkg_id))
class CacheClearBenchmark:
"""Benchmark full cache clear with N pre-loaded entries."""
params: list[int] = [10, 100]
def setup(self, num_entries: int) -> None:
from cleveractors.registry.cache import RegistryCache
self.cache = RegistryCache(
_create_mock_client(), max_size=max(num_entries, 10), validate_content=False
)
self.entries = [f"pkg_act_{i:040x}" for i in range(num_entries)]
async def _warm():
for pid in self.entries:
await self.cache.put(pid, dict(_STANDARD_CONTENT))
asyncio.run(_warm())
def time_clear(self, num_entries: int) -> None:
asyncio.run(self.cache.clear())
class CachePutBenchmark:
"""Benchmark manual cache warming via put()."""
def setup(self) -> None:
from cleveractors.registry.cache import RegistryCache
self.cache = RegistryCache(_create_mock_client(), validate_content=False)
self.pkg_id = _STANDARD_PKG_ID
self.content = dict(_STANDARD_CONTENT)
def time_put(self) -> None:
asyncio.run(self.cache.put(self.pkg_id, self.content))
class CacheFactoryBenchmark:
"""Benchmark CacheFactory.create() construction."""
def setup(self) -> None:
from cleveractors.registry.cache import CacheFactory
self.factory = CacheFactory(max_size=256, ttl=300.0, validate_content=False)
self.mock_client = _create_mock_client()
def time_create(self) -> None:
self.factory.create(self.mock_client)
class CacheContentValidationBenchmark:
"""Benchmark SHA-1 content validation overhead.
Compares get_package throughput with and without validation
enabled to measure the cost of canonicalize + SHA-1 recompute.
When validation fails (content hash ≠ stored ID) the entry
is evicted and re-fetched — the computational cost of the
failed validation is the same but the re-fetch allocates
additional memory.
"""
params: list[bool] = [True, False]
def setup(self, validate: bool) -> None:
from cleveractors.registry.cache import RegistryCache
self.cache = RegistryCache(_create_mock_client(), validate_content=validate)
self.pkg_id = _STANDARD_PKG_ID
asyncio.run(self.cache.get_package(self.pkg_id))
def time_get_package(self, validate: bool) -> None:
asyncio.run(self.cache.get_package(self.pkg_id))