gns3-server/docs/features/project-open-performance.md
YueGuobin 641d10177a
refactor: move MCP service from api/routes to agent package
MCP is an optional AI feature that already depends on
agent.gns3_copilot (Gns3Connector, nornir/netmiko tools) and whose
MCP_AVAILABLE feature flag lives in gns3server/agent. Moving it there
collocates all AI features under one tree and removes AI code from the
core REST routes.

- git mv gns3server/api/routes/mcp -> gns3server/agent/mcp (no content changes)
- api/server.py, core/tasks.py: update import paths
- tests: tests/api/routes/mcp -> tests/agent/mcp, rewrite patch BASE and
  handler imports; fix MCP_DIR depth in test_tool_params.py
- agent/__init__.py: probe the SDK via importlib.import_module so the
  top-level name "mcp" is not bound in the agent namespace (it would
  shadow the new gns3server.agent.mcp subpackage and break
  'from gns3server.agent import mcp')
- docs: update source file paths

Verified: full suite 1567 passed; 82 MCP tools registered, SSE mounted
at /v3/mcp/transport.
2026-08-22 19:32:13 +08:00

149 lines
6.0 KiB
Markdown

# Project Open Performance
## Overview
Optimizations to accelerate project opening (`POST /projects/{id}/open`) and node creation for topologies with many nodes and links. The main bottlenecks were sequential link creation, redundant subprocess calls, and SQLite write contention.
## Before vs After
| Scenario | Before | After |
|----------|--------|-------|
| 20 IOU nodes + 20 links (project open) | ~2s | ~1s |
| 40 QEMU nodes creation (MCP batch) | ~40s | ~1-2s |
## Optimizations
### 1. Parallel Link Creation
**File:** `gns3server/controller/project.py`
Links were created sequentially during project loading, each requiring up to 5 HTTP round-trips to the compute. Now uses `Pool(concurrency=100)` for parallel creation.
```python
# Before: sequential loop
for link_data in topology.get("links", []):
link = await self.add_link(...)
await link.add_node(...)
# After: parallel Pool
pool = Pool(concurrency=100)
for link_data in topology.get("links", []):
pool.append(self._create_link_from_topology_data, link_data)
await pool.join()
```
### 2. Batch UDP Port Allocation
**Files:** `gns3server/api/routes/compute/compute.py`, `gns3server/controller/project.py`, `gns3server/controller/udp_link.py`
During project loading, all required UDP ports are pre-allocated per compute in a single batch call before link creation begins. `UDPLink.create()` checks the pre-allocated pool first, falling back to individual allocation if unavailable.
```python
# New batch endpoint
POST /projects/{id}/ports/udp/batch {"count": N} {"udp_ports": [...]}
```
### 3. IOU Image Subprocess Cache
**File:** `gns3server/compute/iou/iou_vm.py`
Each IOU VM creation spawned `ld-linux --verify` and `iou-image -h` subprocesses. With 20 nodes using the same image, this ran 40 redundant subprocesses. Now results are cached per image path at the class level.
```python
# Class-level caches shared across all instances
IOUVM._loader_cache = {} # image path → loader command
IOUVM._default_values_cache = {} # image path → (ram, nvram)
```
Only the first node with a given image runs the subprocesses; subsequent nodes reuse cached values.
### 4. SQLite WAL Mode
**File:** `gns3server/db/tasks.py`
Write-Ahead Logging allows concurrent reads without blocking on writes. The PRAGMA is registered on `engine.sync_engine` instead of the `Engine` class to correctly fire for async engine connections.
```python
@event.listens_for(engine.sync_engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=WAL")
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
```
Without WAL mode, concurrent API requests caused `sqlite3.OperationalError: database is locked`.
### 5. API Key Authentication O(1) Lookup
**Files:** `gns3server/api/routes/controller/api_keys.py`, `gns3server/api/routes/controller/dependencies/authentication.py`
**Old format:** `gns3_<random>` — required scanning ALL keys and running bcrypt on each (O(n)).
**New format:** `gns3_<api_key_id>_<random_secret>` — extract UUID from token, single DB query (O(1)), single bcrypt.
```python
# New auth flow
parts = token.split("_", 2)
key_id = UUID(parts[1])
secret = parts[2]
db_key = await api_keys_repo.get_api_key(key_id) # O(1) lookup
if await asyncio.to_thread(bcrypt.checkpw, secret.encode(), db_key.key_hash.encode()):
# Authenticated — 1 query + 1 bcrypt regardless of total key count
```
### 6. bcrypt in Thread Pool
**File:** `gns3server/api/routes/controller/dependencies/authentication.py`
`bcrypt.checkpw()` is CPU-bound (~1.3s per call) and was blocking the async event loop. With 5 API keys and 10 concurrent requests, this caused ~13s delay before any handler could start.
```python
# Before: blocking the event loop
if bcrypt.checkpw(token.encode(), db_key.key_hash.encode()):
...
# After: offloaded to thread pool
if await asyncio.to_thread(bcrypt.checkpw, secret.encode(), db_key.key_hash.encode()):
...
```
### 7. Concurrency Settings
| Setting | Before | After | File |
|---------|--------|-------|------|
| Node creation Pool | 5 | 100 | `controller/project.py` |
| Link creation Pool | 5 | 100 | `controller/project.py` |
| MCP BATCH_MAX_WORKERS | 10 | 100 | `agent/mcp/nodes.py` |
| MCP HTTP timeout | 10s | 30s | `agent/gns3_copilot/gns3_client/custom_gns3fy.py` |
| HTTP connection pool | 10 (default) | 500/1000 | `agent/gns3_copilot/gns3_client/custom_gns3fy.py` |
| Start nodes Pool | 3 | 3 (unchanged) | `controller/project.py` |
### 8. MCP Auth Returns JWT
**File:** `gns3server/agent/mcp/__init__.py`
When an MCP client connects with an API key, the `_resolve_token` function validates the key then returns a fresh short-lived JWT instead of the raw API key. The JWT is stored in a `ContextVar` and reused for all subsequent tool calls within the same SSE session — zero extra bcrypt.
```python
if user:
fresh_token = auth_service.create_access_token(user.username)
return fresh_token # JWT for subsequent REST API calls
```
## Related Files
| File | Changes |
|------|---------|
| `gns3server/controller/project.py` | Parallel link creation, batch UDP, Pool(100) |
| `gns3server/compute/iou/iou_vm.py` | Image subprocess cache |
| `gns3server/db/tasks.py` | WAL mode + sync_engine event listener |
| `gns3server/api/routes/compute/compute.py` | Batch UDP endpoint |
| `gns3server/controller/udp_link.py` | Pre-allocated port consumption |
| `gns3server/api/routes/controller/api_keys.py` | O(1) key format |
| `gns3server/api/routes/controller/dependencies/authentication.py` | O(1) auth + thread pool bcrypt |
| `gns3server/agent/mcp/__init__.py` | Auth returns JWT, tool enhancements |
| `gns3server/agent/mcp/nodes.py` | fields filter, inherited template_id, name passthrough |
| `gns3server/agent/mcp/links.py` | fields filter, compact array format |
| `gns3server/agent/gns3_copilot/gns3_client/custom_gns3fy.py` | Timeout 30s, connection pool 500/1000 |
| `gns3server/utils/images.py` | md5sum cache error → warning |