Research – Codebase Bottleneck Analysis
Date: 2026-01-31 Phase: Research
Executive Summary
This document maps the critical performance bottlenecks and architectural chokepoints in the tunacode codebase. The analysis reveals 10 high-priority bottleneck locations spanning synchronous I/O in async contexts, shared mutable state without synchronization, O(n²) algorithms, and threading/async incompatibility issues.
Structure Overview
src/tunacode/
├── ui/ # 197 Python files total
│ ├── app.py # TextualReplApp - main event loop
│ ├── widgets/ # 10 UI components
│ └── renderers/ # 15+ output renderers
├── core/
│ ├── agents/
│ │ ├── main.py # RequestOrchestrator
│ │ ├── agent_components/
│ │ │ ├── streaming.py # Streaming handler
│ │ │ ├── tool_executor.py # Parallel execution
│ │ │ ├── orchestrator.py # Node processing
│ │ │ └── response_state.py # State with RLock
│ │ └── resume/
│ │ ├── sanitize.py # Message cleanup
│ │ └── prune.py # Token pruning
│ ├── state.py # StateManager singleton
│ └── types/tool_registry.py # Mutable registry
└── tools/ # Tool implementations
Critical Bottlenecks (Prioritized)
1. Synchronous File I/O in Async Context
Location: src/tunacode/core/state.py:234-237
# save_session() - blocking call in async flow
with open(session_file, "w") as f:
json.dump(session_data, f, indent=2)
Impact: Event loop stall during session persistence
Pattern: Called from ui/app.py:250 in _process_request() finally block
Related:
load_session()atstate.py:257-302- similar blocking I/Oagent_config.py:174-175- blocking file reads at startup
2. Threading Lock in Async Context (RLock)
Location: src/tunacode/core/agents/agent_components/response_state.py:26
_lock: threading.RLock = field(default_factory=threading.RLock)
Impact: Blocks event loop when acquired from async contexts Usage: All property accessors acquire lock (lines 52-59)
Related:
state_transition.py:49- threading.RLock in StateMachinecore/logging/manager.py:22-35- LogManager threading locks
3. Unbounded String Accumulation (O(n²) Memory)
Location: src/tunacode/core/agents/agent_components/streaming.py:349-350
# Per-node debug accumulators - unbounded growth
state_manager.session._debug_raw_stream_accum += delta_text
Impact: Quadratic memory growth during large streaming responses Pattern: String concatenation in loop (line 349-350, 153-154)
4. Shared Mutable State Without Synchronization
Location: src/tunacode/core/types/tool_registry.py:33-40
@dataclass(slots=True)
class ToolCallRegistry:
_calls: dict[ToolCallId, CanonicalToolCall] = field(default_factory=dict)
_order: list[ToolCallId] = field(default_factory=list)
Impact: Race conditions during parallel tool execution Accessed by: Main loop, tool executor, cleanup operations concurrently
5. In-Place List Mutation During Iteration
Location: src/tunacode/core/agents/resume/sanitize.py:315
messages[:] = remaining_messages # In-place mutation
Impact: Race conditions if agent holds references during cleanup
Called from: main.py:461 during abort handling
Related:
run_cleanup_loop()atsanitize.py:458-502- up to 10 iterations over messages- O(n²) worst case from repeated list mutations
6. Non-Atomic Counter Increment (Race Condition)
Location: src/tunacode/core/agents/agent_components/orchestrator/tool_dispatcher.py:298-299
batch_id = runtime.batch_counter + 1 # Read
runtime.batch_counter = batch_id # Write (non-atomic)
Impact: ID collisions between concurrent tool batches TOCTOU Race: State could change between read and write
7. Deep Attribute Access Chains (Violation of 2-Dot Rule)
Location: src/tunacode/core/agents/main.py:96-100
session = self.state_manager.session
conversation = session.conversation
runtime = session.runtime
tool_registry = runtime.tool_registry # 4 levels of nesting
Impact: High coupling, difficult refactoring, brittle code Pattern: Violates "Law of Demeter" - reaches through 4+ object layers
Related:
ui/app.py:376-387- UI layer directly accessessession.conversation.total_tokens
8. Sequential Batch Processing (Under-Utilized Parallelism)
Location: src/tunacode/core/agents/agent_components/tool_executor.py:119-129
# Batches execute sequentially - batch 2 waits for batch 1
for i in range(0, len(tool_calls), max_parallel):
batch = tool_calls[i:i + max_parallel]
batch_tasks = [execute_with_retry(part, node) for part, node in batch]
batch_results = await asyncio.gather(*batch_tasks)
Impact: If batch 1 has 4 slow tools, batch 2 waits despite available slots
9. Synchronous Token Counting (CPU Bottleneck)
Location: src/tunacode/utils/messaging/token_counter.py
Called from: state.py:62-75 - update_token_count()
for msg in messages:
content = get_content(msg)
token_count = estimate_tokens(content, model_name) # Synchronous, O(n)
Impact: O(n) scan on every message, no caching per message
10. Debug Logging Overhead in Hot Path
Location: src/tunacode/core/agents/agent_components/streaming.py:211-267
async for event in request_stream:
debug_event_count += 1
if debug_event_count <= DEBUG_STREAM_EVENT_LOG_LIMIT:
# Complex reflection and logging executes even when debug_mode=False
Impact: Debug logic executes unconditionally (check is inside loop)
Dependency Flow Violations
UI → Core Boundary Violations
Location: src/tunacode/ui/app.py:376-387
def _update_resource_bar(self) -> None:
session = self.state_manager.session
conversation = session.conversation
context_tokens = conversation.total_tokens # Deep reach into core state
Issue: UI layer reaches 3 levels deep into core state structures Correct pattern: UI should use protocols, not direct state access
Critical Request Flow (With Bottlenecks Marked)
User Input → Editor.action_submit() → widgets/editor.py:115-127
└── EditorSubmitRequested message → ui/app.py:268-274
└── request_queue.put() → ui/app.py:274
└── _request_worker() → ui/app.py:177-186
└── _process_request() → ui/app.py:188-251
└── process_request() → core/agents/main.py:553
└── RequestOrchestrator.run() → main.py:221-239
└── _run_impl() → main.py:241-482
└── agent.iter() → main.py:355
└── stream_model_request_node() → streaming.py:122-384
└── [BOTTLENECK: Unbounded string accumulation]
└── process_node() → orchestrator.py:178-292
└── dispatch_tools() → tool_dispatcher.py:219-352
└── [BOTTLENECK: Non-atomic counter]
└── execute_tools_parallel() → tool_executor.py:53-140
└── [BOTTLENECK: Sequential batch processing]
└── [BOTTLENECK: Shared registry access]
└── save_session() → state.py:234-237
└── [BOTTLENECK: Synchronous file I/O]
Symbol Index – Key Functions/Classes
| Symbol | Location | Purpose | Risk |
|---|---|---|---|
RequestOrchestrator |
main.py:141 |
Main request coordination | Single point of failure |
process_node() |
orchestrator.py:178 |
Per-node processing | 8 parameters, high coupling |
execute_tools_parallel() |
tool_executor.py:53 |
Parallel tool execution | Sequential batches |
ToolCallRegistry |
tool_registry.py:33 |
Tool call tracking | No synchronization |
ResponseState |
response_state.py:26 |
State management | RLock in async |
save_session() |
state.py:234 |
Session persistence | Blocking I/O |
run_cleanup_loop() |
sanitize.py:458 |
Message cleanup | O(n²) iterations |
prune_old_tool_outputs() |
prune.py:160 |
Token pruning | O(n) on large histories |
update_token_count() |
state.py:62 |
Token counting | No caching |
TextualReplApp |
ui/app.py:79 |
Main UI | Queue serializes requests |
Patterns Found
Anti-Pattern 1: Threading Locks in Async Code
Locations:
response_state.py:26- threading.RLockstate_transition.py:49- threading.RLocklogging/manager.py:22-35- threading.RLock
Fix: Use asyncio.Lock or asyncio.Semaphore for async contexts
Anti-Pattern 2: In-Place Mutation of Shared State
Locations:
sanitize.py:315-messages[:] = remaining_messagesmain.py:267-269-session_messagesmutated in-place
Fix: Create new lists, swap references atomically
Anti-Pattern 3: Synchronous I/O in Async Functions
Locations:
state.py:234-237- File write insave_session()state.py:257-302- File read inload_session()agent_config.py:174-175- File reads at startup
Fix: Wrap in asyncio.to_thread() or use aiofiles
Anti-Pattern 4: Unbounded Accumulators
Locations:
streaming.py:349-350-_debug_raw_stream_accumstreaming.py:153-154-_debug_events
Fix: Limit size, use StringIO for accumulation, clear periodically
Anti-Pattern 5: Deep Attribute Chains (4+ dots)
Locations:
main.py:96-100- 4 levels to reachtool_registryui/app.py:376-387- 3 levels to reachtotal_tokens
Fix: Extract needed values, pass as parameters, use protocols
Contention Points
- StateManager.session - Accessed by UI, core, and tools without coordination
- ToolCallRegistry - Mutated during parallel tool execution without locks
- conversation.messages - Mutated during cleanup while agent may hold references
- runtime.batch_counter - Non-atomic increment under concurrent access
- LogManager singleton - Threading locks don't yield to asyncio event loop
Appendix: File Statistics
- Total Python files: 197
- Core agent files: ~25
- UI files: ~35
- Tool files: ~40
- Test files: Located in
tests/directory
Research Sources
- codebase-locator agent (0ab75708)
- codebase-analyzer agent (ade05d3b)
- context-synthesis agent (03730480)
- structure-map.sh output
- ast-scan.sh functions output