Cache Architecture
Beacon uses a multi-layer caching system to minimize redundant analysis while maintaining correctness.
Cache Layers
The system provides four specialized cache layers, each optimized for different granularities:
TypeCache (Node-Level)
Caches inferred types for specific AST nodes. Each entry maps (uri, node_id, version) to a Type.
Capacity: 100 entries (default)
Eviction: LRU
Use case: Hover requests, completion suggestions, and other features that need type information for a specific node.
ScopeCache (Scope-Level)
Provides granular incremental re-analysis at scope level rather than document level. When only a single function changes in a large file, unchanged scopes retain their cached analysis results.
Cache key: (uri, scope_id, content_hash)
Content hashing: Uses DefaultHasher to compute a deterministic hash of the scope's source text. Different content produces different hashes, enabling precise change detection.
Cached data:
type_map: inferred types for nodes within the scopeposition_map: mapping from source positions to node IDsdependencies: scopes this scope depends on (parent, referenced scopes)
Capacity: 200 entries (default)
Eviction: LRU
Statistics: Tracks hits/misses for performance monitoring.
Use case: Type checking, diagnostics, and semantic analysis that can reuse results from unchanged scopes.
AnalysisCache (Document-Level)
Caches complete analysis artifacts per document version. Each entry maps (uri, version) to full analysis results including type maps, position maps, type errors, and static analysis findings.
Cached data:
- Complete type maps
- Position maps
- Type errors
- Static analysis results
Capacity: 50 entries (default)
Eviction: LRU
Version-based invalidation: New document versions automatically create new cache entries rather than invalidating existing ones.
Use case: Publishing diagnostics, workspace-wide queries, and features that need complete document analysis.
IntrospectionCache (Persistent)
Caches Python introspection results for external modules and the standard library. Persists to disk in .beacon-cache/introspection.json to survive server restarts.
Cached data:
- Function signatures
- Docstrings
- Module metadata
Capacity: 1000 entries
Eviction: LRU (in-memory), write-through to disk
Use case: Hover information for stdlib and third-party modules, completion for imported symbols.
Content Hashing Validation
ScopeCache uses content hashing to detect changes with high precision:
Hash computation:
#![allow(unused)] fn main() { let mut hasher = DefaultHasher::new(); source_content.hash(&mut hasher); let content_hash = hasher.finish(); }
Properties:
- Deterministic: same content always produces the same hash
- Whitespace-sensitive:
x = 1andx=1produce different hashes - Collision-resistant: sufficient for cache validation
Validation: Cache lookups compare the computed content hash against the cached key. Mismatches result in cache misses, forcing re-analysis of the modified scope.
Invalidation Strategies
Version-Based Invalidation
TypeCache checks document version on every access. If the document version differs from the cached entry's version, the entry is treated as stale.
AnalysisCache embeds version in the cache key, so new versions naturally create new entries without explicit invalidation.
Content-Based Invalidation
ScopeCache compares content hashes. When a scope's source changes:
- Compute new content hash from updated source
- Look up cache with new key
- Cache miss if hash differs
- Re-analyze and insert with new hash
Explicit Invalidation
CacheManager provides methods to invalidate specific scopes or entire documents:
invalidate_document: Removes all cache entries for a URI across all layers.
invalidate_scope: Removes entries for a specific scope from ScopeCache.
invalidate_selective: Invalidates specific scopes and returns the set of affected URIs for cascade invalidation.
Cascade Invalidation
When a scope changes, dependent scopes may also need invalidation. ImportDependencyTracker maintains a dependency graph to determine which scopes reference the changed scope, enabling selective cascade invalidation without over-invalidating.
Cache Coordination
CacheManager unifies all cache layers and coordinates invalidation:
On document change:
- Identify changed scopes by comparing content hashes
- Invalidate changed scopes in ScopeCache
- Clear document-level entries in AnalysisCache for the affected URI
- Query dependency tracker to find dependent scopes
- Invalidate dependents selectively
- TypeCache entries naturally become stale via version mismatch
On document close:
- Remove all cache entries for the URI
- Persist IntrospectionCache to disk
Performance Characteristics
Cache hit rates directly impact analysis latency:
Cold cache (first analysis): Full analysis required for all scopes.
Warm cache, no changes: All scopes hit, near-instant response.
Warm cache, localized change: Only changed scopes and dependents miss, dramatic speedup for large files.
ScopeCache statistics provide hit rate monitoring:
#![allow(unused)] fn main() { let stats = cache_manager.scope_cache_stats(); println!("Hit rate: {:.2}%", stats.hit_rate); }
Formatter Cache
The formatter uses a separate two-level cache optimized for formatting requests:
Short-Circuit Cache
Maps (source_hash, config_hash) to unit. Detects already-formatted code in O(1) time, avoiding redundant formatting operations.
Result Cache
Maps (source_hash, config_hash, start_line, end_line) to formatted output. Reuses formatting results for identical source and configuration.
Capacity: 100 entries (default) per layer
Eviction: LRU
Use case: Format-on-save, range formatting, and editor-initiated format requests.