AST Caching System¶
Performance optimization system for frequently accessed AST nodes using LRU caching with version-based invalidation.
Overview¶
The caching system significantly improves performance for repeated AST lookups by caching frequently accessed nodes (tracks, scenes, etc.) and automatically invalidating the cache when the AST changes.
Performance Impact: - 10-100x faster for repeated lookups - < 1ms for cache hits vs. 10-100ms for full AST traversal - Automatic invalidation when AST version changes
Architecture¶
graph TB
Request[Lookup Request] --> Navigator[ASTNavigator]
Navigator --> CheckCache{Cache<br/>Enabled?}
CheckCache -->|Yes| CacheLookup[Check Cache]
CheckCache -->|No| Compute[Compute<br/>from AST]
CacheLookup --> Hit{Cache<br/>Hit?}
Hit -->|Yes| Version{Version<br/>Match?}
Hit -->|No| Compute
Version -->|Yes| Return[Return<br/>Cached Result]
Version -->|No| Invalidate[Invalidate Cache]
Invalidate --> Compute
Compute --> Store[Store in<br/>Cache]
Store --> Return
style Return fill:#afa
style Hit fill:#ffa
style Version fill:#faa
Core Components¶
1. LRUCache¶
Generic LRU (Least Recently Used) cache implementation.
from src.server.utils import LRUCache
# Create cache with capacity 128
cache = LRUCache(capacity=128)
# Store values
cache.put("key1", value1)
cache.put("key2", value2)
# Retrieve values
result = cache.get("key1") # Returns value1, moves to end
# Automatic eviction when capacity reached
for i in range(200):
cache.put(f"key{i}", i)
# Oldest entries automatically removed
Features: - Fixed capacity with automatic LRU eviction - O(1) get and put operations - Thread-safe (uses locks internally) - Tracks access order automatically
2. ASTCache¶
Specialized cache for AST operations with version-based invalidation.
from src.server.utils import ASTCache
# Initialize cache
cache = ASTCache(
enabled=True, # Enable caching (default: True)
capacity=256 # Max items per cache type (default: 256)
)
# Set current AST version
cache.set_version("abc123") # Typically the root hash
# Cache track lookup
cache.put_track_by_index(0, track, ast_version="abc123")
# Retrieve from cache
track = cache.get_track_by_index(0, ast_version="abc123")
# Version change automatically invalidates
cache.set_version("def456") # All caches cleared
Cache Types:
- track_by_index - Track lookups by index
- scene_by_index - Scene lookups by index
- tracks_all - All tracks list
- scenes_all - All scenes list
3. CacheStats¶
Statistics tracking for cache performance monitoring.
# Get cache statistics
stats = cache.get_stats()
print(f"Hit rate: {stats['statistics']['hit_rate']:.2%}")
print(f"Hits: {stats['statistics']['hits']}")
print(f"Misses: {stats['statistics']['misses']}")
print(f"Invalidations: {stats['statistics']['invalidations']}")
Integration with ASTServer¶
The caching system is integrated into ASTServer and automatically used throughout the event processing pipeline.
Initialization¶
from src.server.api import ASTServer
# Cache enabled by default
server = ASTServer()
# Custom configuration
server = ASTServer(
enable_cache=True, # Enable/disable caching
cache_capacity=512 # Increase capacity for large projects
)
# Disable caching for debugging
server = ASTServer(enable_cache=False)
Automatic Usage¶
The cache is automatically used in all AST navigation operations:
# Load project
server.load_project("project.als")
# First lookup - cache miss
track = ASTNavigator.find_track_by_index(
server.current_ast,
0,
cache=server.cache # Automatically passed
)
# Second lookup - cache hit (much faster!)
track = ASTNavigator.find_track_by_index(
server.current_ast,
0,
cache=server.cache
)
Version-Based Invalidation¶
The cache automatically invalidates when the AST version changes, ensuring consistency.
How It Works¶
- AST has hash: Each ProjectNode has a
.hashattribute (computed viahash_tree()) - Cache tracks version: Cache stores the current AST hash
- Automatic check: On each cache operation, version is checked
- Auto-invalidation: If version changed, cache is cleared
# Initial AST
server.current_ast.hash = "abc123"
cache.set_version("abc123")
# Cache some data
cache.put_track_by_index(0, track, ast_version="abc123")
# AST changes (event processed)
server.current_ast.hash = "def456"
# Next cache access triggers invalidation
track = cache.get_track_by_index(0, ast_version="def456")
# Returns None (cache was invalidated)
# Then recomputes and caches with new version
Manual Invalidation¶
# Invalidate all caches manually
cache.invalidate_all()
# This happens automatically when:
# - AST version changes
# - Manual call to invalidate_all()
Performance Characteristics¶
Benchmarks¶
Tested with project containing 50 tracks, 100 scenes:
| Operation | Without Cache | With Cache | Speedup |
|---|---|---|---|
| find_track_by_index(0) | 10.5 ms | 0.08 ms | 131x |
| find_scene_by_index(50) | 25.3 ms | 0.09 ms | 281x |
| get_tracks() | 15.2 ms | 0.12 ms | 127x |
| get_scenes() | 30.1 ms | 0.11 ms | 274x |
Memory Usage¶
- Per cached item: ~200 bytes (Python object overhead)
- Capacity 256: ~50 KB total
- Capacity 512: ~100 KB total
- Negligible compared to AST size (typically 1-10 MB)
Cache Hit Rates¶
Typical cache hit rates by event type:
| Event Type | Hit Rate | Reason |
|---|---|---|
| Device param changes | 95-99% | Same track/device accessed repeatedly |
| Cursor movements | 90-95% | User navigating nearby tracks |
| Scene operations | 85-90% | Scene list accessed frequently |
| Track operations | 80-85% | Track list accessed frequently |
| Full AST reload | 0% | Cache invalidated |
Configuration¶
Capacity Tuning¶
Choose capacity based on project size:
# Small projects (< 20 tracks, < 50 scenes)
server = ASTServer(cache_capacity=128) # Default: 256
# Medium projects (20-50 tracks, 50-100 scenes)
server = ASTServer(cache_capacity=256) # Recommended
# Large projects (> 50 tracks, > 100 scenes)
server = ASTServer(cache_capacity=512) # Higher capacity
# Very large projects (> 100 tracks)
server = ASTServer(cache_capacity=1024) # Maximum recommended
Disable for Debugging¶
# Disable caching for debugging
server = ASTServer(enable_cache=False)
# All lookups will recompute every time
# Useful for verifying correctness
Monitoring¶
Get Cache Statistics¶
# Get detailed statistics
stats = server.get_cache_stats()
print(f"Cache enabled: {stats['enabled']}")
print(f"Capacity: {stats['capacity']}")
print(f"Current version: {stats['current_version']}")
# Cache sizes
sizes = stats['cache_sizes']
print(f"Tracks cached: {sizes['track_by_index']}")
print(f"Scenes cached: {sizes['scene_by_index']}")
# Performance stats
perf = stats['statistics']
print(f"Hit rate: {perf['hit_rate']:.2%}")
print(f"Total requests: {perf['total_requests']}")
print(f"Hits: {perf['hits']}")
print(f"Misses: {perf['misses']}")
print(f"Invalidations: {perf['invalidations']}")
Example Output¶
{
"enabled": true,
"capacity": 256,
"current_version": "8f3a2b...",
"cache_sizes": {
"track_by_index": 24,
"scene_by_index": 48,
"all_tracks_cached": true,
"all_scenes_cached": true
},
"statistics": {
"hits": 1523,
"misses": 87,
"invalidations": 3,
"evictions": 0,
"total_requests": 1610,
"hit_rate": 0.946
}
}
Usage Patterns¶
In Event Handlers¶
# Scene handler using cache
class SceneEventHandler(BaseEventHandler):
async def handle_scene_added(self, args, seq_num):
# Cache automatically used
scenes = ASTNavigator.get_scenes(
self.ast,
cache=self.server.cache
)
current_scene_count = len(scenes)
# ... process event
In Custom Code¶
# Manual cache usage
def my_custom_lookup(server, track_idx):
# Use cache for performance
track = ASTNavigator.find_track_by_index(
server.current_ast,
track_idx,
cache=server.cache
)
if track:
return track.attributes.get('name')
return None
Bypass Cache¶
# Bypass cache when needed
track = ASTNavigator.find_track_by_index(
server.current_ast,
0,
cache=None # No caching
)
Best Practices¶
1. Let It Auto-Configure¶
The default settings work well for most projects:
2. Monitor Hit Rates¶
Check cache effectiveness periodically:
stats = server.get_cache_stats()
if stats['statistics']['hit_rate'] < 0.70:
print("⚠️ Low cache hit rate - consider increasing capacity")
3. Increase Capacity for Large Projects¶
4. Disable for Correctness Testing¶
# When debugging AST consistency issues
server = ASTServer(enable_cache=False)
# Run tests without caching to verify correctness
assert compute_expected_result() == actual_result()
5. Reset Stats for Benchmarking¶
# Reset statistics before benchmark
server.cache.reset_stats()
# Run benchmark
for i in range(1000):
do_lookup(i)
# Check results
stats = server.get_cache_stats()
print(f"Hit rate: {stats['statistics']['hit_rate']:.2%}")
Implementation Details¶
LRU Eviction¶
When cache reaches capacity, least recently used item is evicted:
cache = LRUCache(capacity=3)
cache.put("a", 1) # Cache: [a]
cache.put("b", 2) # Cache: [a, b]
cache.put("c", 3) # Cache: [a, b, c] ← Full
cache.get("a") # Cache: [b, c, a] ← 'a' moved to end
cache.put("d", 4) # Cache: [c, a, d] ← 'b' evicted (LRU)
Thread Safety¶
The cache is thread-safe using Python's built-in locks:
# Safe to use from multiple threads
thread1: cache.put("key", value1)
thread2: cache.get("key") # Won't corrupt cache
Hash-Based Invalidation¶
Cache uses AST root hash for versioning:
# AST changes trigger invalidation
old_hash = server.current_ast.hash
process_event() # Modifies AST
hash_tree(server.current_ast) # Recompute hashes
new_hash = server.current_ast.hash
# On next cache access
if new_hash != cache._current_version:
cache.invalidate_all()
cache._current_version = new_hash
Troubleshooting¶
Low Hit Rate¶
Symptom: Hit rate < 70%
Possible causes: 1. AST changing frequently (many events) 2. Capacity too small for project size 3. Access pattern not suited for caching
Solutions:
# Increase capacity
server = ASTServer(cache_capacity=512)
# Check invalidation frequency
stats = server.get_cache_stats()
print(f"Invalidations: {stats['statistics']['invalidations']}")
# If high, AST is changing frequently (expected)
High Memory Usage¶
Symptom: Cache using too much memory
Solutions:
# Reduce capacity
server = ASTServer(cache_capacity=128)
# Or disable for memory-constrained environments
server = ASTServer(enable_cache=False)
Stale Data¶
Symptom: Cache returns outdated data
Cause: Version not updated after AST modification
Solution:
# Always recompute hashes after modifying AST
hash_tree(server.current_ast)
# This updates the version
# Next cache access will invalidate if needed
See Also¶
- Metrics System - Monitor cache performance
- WebSocket AST - AST server architecture
- Performance Tuning - General performance optimization