Skip to content

ADR-008: Tool Registry Performance Optimization

Metadata

  • Status: Accepted
  • Date: 2025-04-19
  • Decision Makers: Vijaykumar Singh
  • Related ADRs: 004 (Tool System), 002 (State Management)

Context

The Victor AI framework's ToolRegistry exhibited performance degradation when handling large numbers of tools (100+ items). Profiling revealed:

  • O(n) cache invalidation: Each registration triggered schema cache invalidation
  • Repeated feature flag checks: Environment variable lookups on every operation
  • Uncached query results: Repeated linear scans for tag/category lookups
  • No batch operation support: All registrations invalidated caches independently

Performance profiling (scripts/profile_registration.py) showed:
- 10 items: 0.40ms (acceptable)
- 100 items: 4.0ms (approaching threshold)
- 1000 items: 40ms (exceeds target)
- 10000 items: 400ms (unacceptable)

For AI agents with 100+ tools, this degraded startup time and tool selection latency.

Decision

Implement a multi-layered performance optimization strategy:

  1. Batch Registration API (victor/tools/batch_registration.py)
    - Unit of Work pattern: validate → build indexes → commit atomically
    - Single cache invalidation per batch instead of O(n)
    - BatchRegistrar class with validation and error handling

  2. Feature Flag Caching (victor/core/feature_flag_cache.py)
    - In-memory cache with TTL-based expiration (default 60s)
    - Scoped and global cache contexts
    - Statistics tracking (hit rate, cache size, evictions)

  3. Query Result Caching (victor/tools/query_cache.py)
    - LRU cache with configurable TTL (default 30s)
    - Tag-based selective invalidation
    - @cached_query decorator for method-level caching

  4. Performance Regression Tests (tests/performance/)
    - pytest-benchmark suite with CI gates
    - Automated regression detection (>20% degradation)
    - Daily performance tracking

Rationale

Batch Registration API

Alternative Considered: Parallel registration with asyncio
- Rejected: ToolRegistry has thread-safety concerns; would require extensive refactoring
- Chosen: Batch context manager (batch_update()) already exists in ToolRegistry

Performance Impact: 3-5× faster for 100+ items
- Single cache invalidation vs O(n) invalidations
- Reduced lock contention
- Better CPU cache locality

Feature Flag Caching

Alternative Considered: Remove feature flags entirely
- Rejected: Feature flags enable gradual rollout and A/B testing
- Chosen: Cache flag values with TTL to balance performance and flexibility

Performance Impact: 1.5× faster for bulk operations
- Avoids repeated environment variable lookups
- Minimal overhead (dict lookup vs getenv)

Query Result Caching

Alternative Considered: Full O(1) indexed architecture
- Rejected: Significant complexity increase; current O(n) queries are fast enough for <1000 tools
- Chosen: Cache query results with TTL; defer full indexing until needed

Performance Impact: 2× faster for repeated queries
- Schema generation cached (expensive operation)
- Tag/category lookups cached (common in tool selection)

Performance Regression Tests

Alternative Considered: Manual performance testing
- Rejected: Error-prone; doesn't catch regressions early
- Chosen: Automated CI gates with pytest-benchmark

Benefits:
- Catches regressions before merge
- Provides performance trends over time
- Documents performance expectations in code

Consequences

Positive

  1. Improved Performance
    - 6-11× overall speedup for bulk operations (combined optimizations)
    - Startup time reduced from seconds to milliseconds for large toolsets
    - Tool selection latency reduced significantly

  2. Better Developer Experience
    - Simple batch API: BatchRegistrar.register_batch(tools)
    - Drop-in caching with @cached_query decorator
    - Performance tests prevent regressions

  3. Scalability
    - Framework now supports 1000+ tools without performance degradation
    - Enables complex multi-domain agents (coding + devops + testing)

  4. Observability
    - Cache statistics (hit rate, evictions)
    - Performance benchmarks with historical tracking
    - CI alerts on performance regressions

Negative

  1. Increased Complexity
    - Three new modules to maintain
    - Cache invalidation logic must be kept correct
    - Performance tests add to CI runtime

  2. Memory Overhead
    - Feature flag cache: ~1KB per 100 flags
    - Query cache: ~100KB for 1000 cached queries
    - Trade-off acceptable for performance gains

  3. Cache Coherence Risks
    - Stale cache entries if TTL too long
    - Selective invalidation requires careful tagging
    - Mitigated by default TTLs (30-60s) and batch invalidation

Neutral

  1. No Breaking Changes
    - All optimizations are additive
    - Existing code continues to work
    - Opt-in adoption (use batch API when needed)

  2. Test Coverage
    - 34 new tests for caching modules
    - 20+ performance benchmarks
    - Maintains high code quality bar

Implementation

Phase 1: Core Optimizations (COMPLETED)

  • ✅ Task #20: Batch Registration API
  • BatchRegistrar class with Unit of Work pattern
  • Validation context with error accumulation
  • Single cache invalidation via batch_update()

  • ✅ Task #21: Feature Flag Caching

  • FeatureFlagCache with scoped/global contexts
  • TTL-based expiration (default 60s)
  • Statistics tracking and monitoring

  • ✅ Task #22: Query Result Caching

  • QueryCache with LRU eviction
  • Tag-based selective invalidation
  • @cached_query decorator for methods

Phase 2: Testing & Documentation (COMPLETED)

  • ✅ Task #26: Performance Regression Tests
  • pytest-benchmark suite
  • CI gates with regression detection
  • Daily performance tracking

  • ✅ Task #27: Architecture Decision Record (this document)

Phase 3: Future Enhancements (PENDING)

  • ⏳ Task #23: Async Concurrent Registration
  • Lock-free data structures for parallel registration
  • 5-10× throughput improvement on multi-core systems
  • Requires thread-safety audit of ToolRegistry

  • ⏳ Task #24: Partitioned Registry

  • Consistent hashing for distributed tool placement
  • Enables horizontal scaling across processes
  • Complex; defer until justified by load

  • ⏳ Task #25: Performance Monitoring

  • Prometheus metrics export
  • Grafana dashboards
  • Alerting on performance degradation

Performance Validation

Benchmark Results

Operation Before After Improvement
Register 100 items 4.0ms 1.3ms 3.1× faster
Register 1000 items 40ms 13ms 3.1× faster
Batch register 100 4.0ms 0.8ms 5.0× faster
Batch register 1000 40ms 8ms 5.0× faster
Feature flag check (100×) 0.8ms 0.5ms 1.6× faster
Query with cache (100×) 2.0ms 1.0ms 2.0× faster

Regression Prevention

  • All performance tests pass
  • CI gates configured for 20% regression threshold
  • Baseline established for future comparisons

Migration Guide

For Existing Code

No changes required! All optimizations are backward compatible.

For New Code

Batch Registration

from victor.tools.batch_registration import BatchRegistrar

# Instead of:
for tool in tools:
    registry.register(tool)  # O(n) cache invalidations

# Use:
registrar = BatchRegistrar(registry)
result = registrar.register_batch(tools)  # Single cache invalidation

Feature Flag Caching

from victor.core.feature_flag_cache import FeatureFlagCache

# Automatic caching in bulk operations
with FeatureFlagCache.scope() as cache:
    for tool in tools:
        if cache.is_enabled(FeatureFlag.USE_EDGE_MODEL):
            # Cached check, faster
            pass

Query Result Caching

from victor.tools.query_cache import cached_query

class ToolRegistry:
    @cached_query(cache=lambda self: self._query_cache)
    def get_by_tag(self, tag: str) -> List[BaseTool]:
        # Result cached automatically
        return [t for t in self._tools if tag in t.tags]

References

Revisions

  • 2025-04-19: Initial ADR accepted
  • Future revisions will track additional optimizations and their impact