perf: QwenPaw's application startup#3386
perf: QwenPaw's application startup#3386rayrayraykk wants to merge 3 commits intoagentscope-ai:mainfrom
Conversation
|
Hi @rayrayraykk, this is your 114th Pull Request. 🙌 Join Developer CommunityThanks so much for your contribution! We'd love to invite you to join the official QwenPaw developer group! You can find the Discord and DingTalk group links under the "Developer Community" section on our docs page: We truly appreciate your enthusiasm—and look forward to your future contributions! 😊 We'll review your PR soon. |
There was a problem hiding this comment.
Pull request overview
This PR refactors QwenPaw’s backend startup sequence to reduce time-to-serve by splitting initialization into a fast “server-ready” phase and a background “heavy init” phase, and by enabling more parallel startup work (notably agent workspace initialization).
Changes:
- Refactors FastAPI lifespan into a two-phase startup: minimal synchronous setup first, heavy initialization in a background task.
- Refactors
MultiAgentManager.get_agent()to deduplicate concurrent workspace creation and reduce lock hold time to enable parallel agent startup. - Reduces startup log noise by demoting many INFO logs to DEBUG and adds timing/debug instrumentation for service startup.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/qwenpaw/app/_app.py | Introduces two-phase lifespan and background initialization task; reorganizes plugin/agent/service startup order. |
| src/qwenpaw/app/multi_agent_manager.py | Adds per-agent pending-start coordination to avoid duplicate initialization and allow parallel startup. |
| src/qwenpaw/app/workspace/service_manager.py | Adds yielding/timing logs; offloads constructors/start methods to thread pool to avoid blocking the event loop. |
| src/qwenpaw/app/workspace/workspace.py | Demotes workspace lifecycle logs from INFO to DEBUG. |
| src/qwenpaw/app/workspace/service_factories.py | Demotes ChatManager reuse/creation logs from INFO to DEBUG. |
| src/qwenpaw/app/runner/manager.py | Demotes ChatManager constructor log from INFO to DEBUG. |
| src/qwenpaw/app/runner/control_commands/init.py | Demotes command registration logs from INFO to DEBUG. |
| src/qwenpaw/app/mcp/stateful_client.py | Demotes MCP “connected” logs from INFO to DEBUG. |
| src/qwenpaw/app/channels/unified_queue_manager.py | Demotes queue manager initialization/cleanup loop logs from INFO to DEBUG. |
| src/qwenpaw/app/channels/manager.py | Demotes workspace injection log from INFO to DEBUG. |
| src/qwenpaw/app/channels/console/channel.py | Demotes console channel started log from INFO to DEBUG. |
| src/qwenpaw/app/channels/command_registry.py | Demotes command registry logs from INFO to DEBUG. |
| src/qwenpaw/app/agent_config_watcher.py | Demotes watcher started log from INFO to DEBUG. |
| src/qwenpaw/agents/memory/reme_light_memory_manager.py | Demotes memory manager init/config logs from INFO to DEBUG. |
| src/qwenpaw/providers/provider_manager.py | Demotes “background local model restore completed” log from INFO to DEBUG. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Offload synchronous constructor to thread pool to avoid blocking | ||
| # the event loop during background startup. | ||
| service = await asyncio.to_thread( | ||
| partial(service_cls, **init_kwargs), | ||
| ) |
There was a problem hiding this comment.
Service constructors are now always executed via asyncio.to_thread(). This will break services whose init creates asyncio primitives or expects a running event loop (e.g., MCPClientManager.init creates asyncio.Lock at src/qwenpaw/app/mcp/manager.py:34-38; CronManager.init creates AsyncIOScheduler and asyncio.Lock at src/qwenpaw/app/crons/manager.py:52-58). In a worker thread, these typically raise RuntimeError("no running event loop") or bind to the wrong loop. Consider making thread-offloading opt-in per ServiceDescriptor (default off), or refactor affected services so event-loop-bound objects are created in an async start() that runs on the main loop (and keep init lightweight).
| self._pending_starts.pop(agent_id, None) | ||
|
|
||
| elapsed = time.perf_counter() - t0 | ||
| event.set() | ||
| logger.debug( | ||
| f"Workspace created and started: {agent_id} " | ||
| f"({elapsed:.3f}s)", | ||
| ) | ||
| return instance | ||
| except Exception as e: | ||
| async with self._lock: | ||
| self._pending_starts.pop(agent_id, None) | ||
| event.set() | ||
| logger.error(f"Failed to start workspace {agent_id}: {e}") | ||
| raise |
There was a problem hiding this comment.
get_agent() cleans up _pending_starts and signals waiters only in the success path and in except Exception. On Python 3.11+ asyncio.CancelledError inherits from BaseException, so a cancellation during await instance.start() will skip this handler, leaving _pending_starts populated and the Event never set—subsequent callers will await forever. Ensure cleanup + event.set() happens in a finally (or handle CancelledError explicitly) and then re-raise the cancellation.
| self._pending_starts.pop(agent_id, None) | |
| elapsed = time.perf_counter() - t0 | |
| event.set() | |
| logger.debug( | |
| f"Workspace created and started: {agent_id} " | |
| f"({elapsed:.3f}s)", | |
| ) | |
| return instance | |
| except Exception as e: | |
| async with self._lock: | |
| self._pending_starts.pop(agent_id, None) | |
| event.set() | |
| logger.error(f"Failed to start workspace {agent_id}: {e}") | |
| raise | |
| elapsed = time.perf_counter() - t0 | |
| logger.debug( | |
| f"Workspace created and started: {agent_id} " | |
| f"({elapsed:.3f}s)", | |
| ) | |
| return instance | |
| except Exception as e: | |
| logger.error(f"Failed to start workspace {agent_id}: {e}") | |
| raise | |
| finally: | |
| async with self._lock: | |
| self._pending_starts.pop(agent_id, None) | |
| event.set() |

Description
This PR significantly improves QwenPaw's application startup performance through lazy loading and parallel initialization, reducing server ready time from ~4.5 seconds to ~0.05 seconds while maintaining full functionality.
Key Improvements
Two-Phase Startup Architecture
True Parallel Agent Initialization
MultiAgentManager.get_agent()with fine-grained locking_pending_startscoordination withasyncio.Eventto prevent duplicate initializationNon-Blocking Service Initialization
asyncio.to_thread()to prevent event loop blockingReMeLightMemoryManagerand MCP clients no longer block the main event loop during initializationCleaner Startup Logs
✨ QwenPaw ready!message with server URL after all background tasks completePerformance Results
Before:
After:
Related Issue: N/A (Performance optimization)
Security Considerations: None. Changes only affect initialization order and concurrency control, not security boundaries.
Type of Change
Component(s) Affected
Checklist
pre-commit run --all-fileslocally and it passespytestor as relevant) and they passTesting
Manual Testing Steps
Startup Performance Verification
Functional Verification
Parallel Initialization Verification
UI Responsiveness
Modified Files
Core Changes:
src/qwenpaw/app/_app.py: Two-phase lifespan with background initializationsrc/qwenpaw/app/multi_agent_manager.py: Fine-grained locking and parallel agent startupsrc/qwenpaw/app/workspace/service_manager.py:asyncio.to_thread()for sync constructorsLog Level Adjustments (INFO → DEBUG):
src/qwenpaw/app/channels/command_registry.pysrc/qwenpaw/app/channels/unified_queue_manager.pysrc/qwenpaw/app/channels/console/channel.pysrc/qwenpaw/app/channels/manager.pysrc/qwenpaw/app/runner/manager.pysrc/qwenpaw/app/runner/control_commands/__init__.pysrc/qwenpaw/app/agent_config_watcher.pysrc/qwenpaw/app/mcp/stateful_client.pysrc/qwenpaw/app/workspace/service_factories.pysrc/qwenpaw/app/workspace/workspace.pysrc/qwenpaw/agents/memory/reme_light_memory_manager.pysrc/qwenpaw/providers/provider_manager.pyLocal Verification Evidence
Additional Notes
Technical Details
Concurrency Strategy:
MultiAgentManager.get_agent()Workspace.start()runs outside the lock, allowing parallel executionasyncio.Eventcoordination prevents duplicate initialization attemptsEvent Loop Protection:
asyncio.to_thread()Backward Compatibility:
Future Enhancements
Potential follow-ups (not included in this PR):
Known Limitations