Skip to content

Speed up plugin startup port checks#285

Merged
dsarno merged 1 commit into
mainfrom
fix/plugin-startup-fast-path-clean
May 2, 2026
Merged

Speed up plugin startup port checks#285
dsarno merged 1 commit into
mainfrom
fix/plugin-startup-fast-path-clean

Conversation

@dsarno

@dsarno dsarno commented May 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add opt-in startup timing traces via GODOT_AI_STARTUP_TRACE or the godot_ai/log_startup_timing editor setting
  • avoid Windows PowerShell/netstat/lsof on the free-port startup path by trying a loopback bind first
  • make Windows PID lookup netstat-first, with PowerShell only as a fallback, and briefly cache netsh excluded-port output

Verification

  • git diff --check
  • Godot_v4.6.2-stable_win64_console.exe --headless --path test_project --import (exited 0; sandbox could not save user-level Godot editor settings)
  • live Windows check on the earlier branch showed startup down to ~166ms on the spawned path; remaining pause was the client-status refresh, which is already async on current main

Copilot AI review requested due to automatic review settings May 2, 2026 02:50
@codecov

codecov Bot commented May 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Speeds up Godot AI plugin startup by reducing expensive external port checks and adding opt-in startup timing traces for diagnosing remaining startup latency.

Changes:

  • Add opt-in startup timing tracing (env GODOT_AI_STARTUP_TRACE or EditorSetting godot_ai/log_startup_timing) with per-tool counters/phases.
  • Avoid netstat/lsof on the “port is free” path by attempting a loopback bind first, and prefer netstat over PowerShell for Windows PID lookup.
  • Cache Windows netsh interface ipv4 show excludedportrange output briefly (TTL) and add tests for the cache.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
test_project/tests/test_windows_port_reservation.gd Adds unit tests covering the new netsh excluded-port output cache behavior (TTL hit/expire/clear).
plugin/addons/godot_ai/utils/windows_port_reservation.gd Implements a short-lived cache for netsh output and tracks netsh query count for startup tracing.
plugin/addons/godot_ai/plugin.gd Adds startup trace plumbing, switches free-port detection to “try bind first”, and makes Windows PID lookup netstat-first with PowerShell fallback.
plugin/addons/godot_ai/client_configurator.gd Registers a new EditorSetting and env var handling for enabling startup timing traces.
Comments suppressed due to low confidence (1)

plugin/addons/godot_ai/plugin.gd:154

  • _startup_trace_begin() is invoked before the headless-disable early return, so when the plugin is disabled for headless launches a trace can print “begin” without ever printing a matching “done”. Consider moving _startup_trace_begin() to after the _mcp_disabled_for_headless_launch() guard, or calling _startup_trace_finish("headless_disabled") just before returning.
func _enter_tree() -> void:
	_startup_trace_begin()

	## `_process` is only used by the adoption-confirmation watcher; keep
	## it off until `_watch_for_adoption_confirmation` arms it, so the
	## plugin has zero per-frame cost in the common case.
	set_process(false)

	if _mcp_disabled_for_headless_launch():
		_headless_disabled = true
		print("MCP | plugin disabled in headless mode")
		return

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

dsarno commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

Further startup-speedup targets (post-#285 review)

With this PR landed, the free-port path is fast and the netstat/PowerShell scrapes are gated, so the remaining ~80%-loaded ditch is most likely main-thread work in the dock's first paint, not the port checks. Ranked by likely impact:

  1. Sync JSON status reads in _perform_initial_client_status_refresh (mcp_dock.gd:1898) — for ~17 non-CLI clients we synchronously FileAccess.open + JSON.parse_string on each config file on the main thread, before the dock paints. The comment justifies it as a bytecode pre-warm against hot-reload SIGABRT, but the warm only needs a single dereference per strategy script — the remaining 16 calls and all the disk/parse work could be moved into the existing worker thread.

  2. Defer the initial dock refresh entirely_perform_initial_client_status_refresh() is called inside _build_ui() synchronously (mcp_dock.gd:633). Wrapping it in call_deferred (or gating on the first NOTIFICATION_VISIBILITY_CHANGED) lets the editor finish its loading bar before any client config disk walk starts. Cheap and low-risk.

  3. _probe_live_server_status_for_port is a sync polling HTTP probe with OS.delay_msec(10) up to 800 ms (plugin.gd:742). On the adopt path (port already in use) this stalls _enter_tree. Could move to HTTPRequest async with a deferred adopt continuation, or shrink the timeout/probe budget for the startup call specifically (separate ceiling for "is this our server?" vs the runtime case).

  4. _refresh_setup_status shells out to uvx --version on main in user mode (mcp_dock.gd:1185OS.execute). Even though the call is call_deferred, it still runs in the same loading window. Cache the result for the session, or run it on the same client-status worker.

  5. Dock _build_ui builds 18 client rows + theme overrides synchronously (mcp_dock.gd:582). If 1–4 don't fully close the gap, lazy-building rows on first reveal of the Clients window would trim the rest.

Recommendation: turn on GODOT_AI_STARTUP_TRACE=1 (added in this PR) and capture a trace once on the current branch — it will tell us whether the ditch is dock-build, client refresh, or HTTP probe, and we can target the actual offender instead of guessing. If you want to skip that step, #1+#2 together is the safest swing — biggest expected win, no protocol changes, and reversible.


Generated by Claude Code

dsarno commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

Startup trace follow-up

Ran the requested startup experiments on this PR branch (49f6ddc) with Godot 4.6.2 on Windows.

1. Default editor settings / default ports

This did not exercise the free-port path. Port 8000 was occupied by an existing Python listener (python.exe, PID 56340) that returned HTTP 404 on the status endpoint, so the plugin took the incompatible-server path:

MCP startup trace | begin platform=Windows http_port=8000 ws_port=9500
MCP startup trace | phase=settings_registered delta_ms=0 total_ms=0
MCP | managed server v1.5.0 does not match plugin v2.3.1, restarting
MCP | proof: (none)
MCP startup trace | phase=server_start delta_ms=2646 total_ms=2646
MCP startup trace | phase=core_objects delta_ms=5 total_ms=2651
MCP startup trace | phase=handlers_registered delta_ms=1 total_ms=2652
MCP startup trace | phase=dock_attached delta_ms=192 total_ms=2844
MCP startup trace | done path=incompatible total_ms=2844 counters={ "powershell": 2, "netstat": 3, "netsh": 1, "lsof": 0, "http_status_probe": 5, "server_command_discovery": 0 }

Takeaway: the biggest measured stall here is review item #3. The synchronous occupied-port/status-probe path can still cost seconds when the port is held by an unverified server.

2. Isolated fresh editor settings / alternate free ports

I ran with isolated APPDATA/LOCALAPPDATA and configured godot_ai/http_port = 18000, godot_ai/ws_port = 19500 to avoid the existing 8000 listener. This exercised the clean spawned path:

MCP startup trace | begin platform=Windows http_port=18000 ws_port=19500
MCP startup trace | phase=settings_registered delta_ms=0 total_ms=0
MCP | using uvx (godot-ai==2.3.1)
MCP | started server (PID 58884, v2.3.1): ... --port 18000 --ws-port 19500 ...
MCP startup trace | phase=server_start delta_ms=60 total_ms=60
MCP startup trace | phase=core_objects delta_ms=6 total_ms=66
MCP startup trace | phase=handlers_registered delta_ms=1 total_ms=67
MCP startup trace | phase=dock_attached delta_ms=187 total_ms=254
MCP startup trace | done path=spawned total_ms=254 counters={ "powershell": 0, "netstat": 0, "netsh": 1, "lsof": 0, "http_status_probe": 0, "server_command_discovery": 1 }

Takeaway: for the normal free-port spawned path, the PR is doing what we want: no PowerShell/netstat/lsof/status-probe work, and plugin-side startup is ~250ms. The largest remaining plugin phase in this trace is dock attach/build (~187ms), not port discovery.

3. uvx --version cost

Measured uvx --version directly three times because _refresh_setup_status() runs that on the main thread in user mode:

83.4 ms
85.4 ms
77.2 ms

Takeaway: item #4 is real but smaller than the occupied-port probe path and roughly half the observed dock attach/build phase.

Notes

The headless Godot runs exited 0 and emitted complete startup traces. They also logged sandbox/editor-cache save warnings from the isolated temp settings directory; those did not affect the trace or plugin startup path.

Recommendation from these measurements: keep #285 as-is for the free-port win, then prioritize reducing or asyncing the incompatible/occupied-port status probe path. After that, dock attach/build and the synchronous initial client status refresh are the next reasonable places to trim, but the trace does not show them as multi-second on the clean spawned path.

@dsarno dsarno merged commit c5e7321 into main May 2, 2026
15 checks passed
@dsarno dsarno deleted the fix/plugin-startup-fast-path-clean branch May 7, 2026 14:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants