-
Notifications
You must be signed in to change notification settings - Fork 436
Description
Summary
The plugin framework in mcpgateway/plugins/framework/manager.py performs deep copies of GlobalContext.state and GlobalContext.metadata dictionaries for each plugin in the execution chain. For plugins that store large objects in state/metadata and chains with many plugins, this creates memory overhead.
Important: The payload itself is NOT deep copied - only the context's state and metadata dicts are copied.
Impact
- Memory: Each plugin execution duplicates the
stateandmetadatadicts - GC Pressure: Many short-lived dict copies increase garbage collection overhead
- Latency: Deep copy overhead scales with state/metadata size × number of plugins
- Scalability: With 10 plugins and 100KB state = 1MB temporary allocations per request
Memory Analysis
| State/Metadata Size | Plugins | Current Memory Overhead | Optimized Overhead |
|---|---|---|---|
| 1KB | 5 | 5KB | ~0KB (if read-only) |
| 10KB | 10 | 100KB | ~0KB (if read-only) |
| 100KB | 10 | 1MB | ~0KB (if read-only) |
| 100KB | 20 | 2MB | ~0KB (if read-only) |
Affected Code
File: mcpgateway/plugins/framework/manager.py
The deepcopy calls are inside the for hook_ref in hook_refs: loop (lines 148-165), meaning they execute for EVERY plugin:
tmp_global_context = GlobalContext(
request_id=global_context.request_id,
user=global_context.user,
tenant_id=global_context.tenant_id,
server_id=global_context.server_id,
state={} if not global_context.state else deepcopy(global_context.state), # Deep copy per plugin!
metadata={} if not global_context.metadata else deepcopy(global_context.metadata), # Deep copy per plugin!
)Root Cause
The deep copy approach provides:
- Plugin Isolation: Plugins shouldn't see each other's state modifications until after execution
- Rollback Safety: If a plugin fails, the original state is preserved
- State Aggregation: After plugin execution, state changes are merged back
However, most plugins only read the state/metadata or make small modifications.
Proposed Solution
Implement a Copy-on-Write (COW) Dict Wrapper that only materializes a copy when modification is detected:
class CopyOnWriteDict:
"""Dict wrapper that defers deep copy until modification."""
def __init__(self, original: Dict[str, Any]):
self._original = original
self._local_changes: Dict[str, Any] = {}
self._deleted_keys: set = set()
self._modified = False
def __getitem__(self, key: str) -> Any:
if key in self._deleted_keys:
raise KeyError(key)
if key in self._local_changes:
return self._local_changes[key]
return self._original[key]
def __setitem__(self, key: str, value: Any) -> None:
self._local_changes[key] = value
self._modified = True
# ... additional dict methods ...
def to_dict(self) -> Dict[str, Any]:
if not self._modified:
return self._original # No copy needed!
result = dict(self._original)
for key in self._deleted_keys:
result.pop(key, None)
result.update(self._local_changes)
return resultImplementation Tasks
- Profile current memory usage with plugins that have large state
- Identify which built-in plugins actually modify state/metadata
- Create
CopyOnWriteDictclass inmcpgateway/plugins/framework/cow_dict.py - Update
PluginExecutor.execute()to use COW wrappers (lines 158-165) - Update state merge logic (lines 227-229) to use
to_dict() - Add unit tests for COW dict behavior
- Add memory profiling tests
- Verify plugin isolation still works
- Benchmark before/after
Acceptance Criteria
- Memory usage reduced for multi-plugin chains with read-only plugins
- No regression in plugin isolation
- State/metadata modifications still work correctly
- All existing plugin tests pass
- New tests verify COW behavior
- Passes
make verify
References
- Detailed analysis:
todo/performance/plugin-framework-memory-optimization.md - Python Copy-on-Write Patterns
- collections.ChainMap (alternative approach)