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
- 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
256 lines
7.9 KiB
Python
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))
|