Files
planet/docs/technical/en/ops-planet-sh-startup.md
2026-05-10 22:06:01 +08:00

10 KiB

planet.sh Startup Performance Optimization

Background

planet.sh manages start, stop, restart, health checks, and logs for all local services. The previous implementation had several startup issues:

  1. AI Provider rebuilt every time, even when code had not changed.
  2. Port cleanup could wait up to 45 seconds.
  3. Port bind detection used a Python subprocess, adding about 300 ms per call.
  4. Plain restart and restart -b behaved differently.

Issue 1: AI Provider Rebuilt Every Time

Root Cause

The build stamp file lived under /tmp/. After WSL or Linux restart, /tmp is cleared, so the stamp_non_empty condition failed and the script decided to rebuild:

# All three conditions had to be true to skip rebuild
image_exists AND stamp_non_empty AND fingerprint_match

Fix

The stamp file moved to a persistent cache path:

AI_PROVIDER_BUILD_STAMP_FILE="$HOME/.cache/planet/aiprovider_build.sha256"

Writing the stamp creates the directory first:

write_ai_provider_build_stamp() {
    mkdir -p "$(dirname "$AI_PROVIDER_BUILD_STAMP_FILE")"
    compute_ai_provider_build_fingerprint > "$AI_PROVIDER_BUILD_STAMP_FILE"
}

Faster Fingerprint

The previous implementation tarred the whole aiprovider/ directory before hashing, which could take seconds in large trees. The new version uses find + stat and reads only file metadata:

compute_ai_provider_build_fingerprint() {
    find aiprovider \
        -type f \
        ! -path '*/__pycache__/*' \
        ! -name '.env' \
        ! -name '.env.*' \
        ! -name '*.pyc' \
        ! -name '*.pyo' \
        | LC_ALL=C sort \
        | xargs -r stat --format="%Y %s %n" 2>/dev/null
    sha256sum docker-compose.yml docker-compose.simple.yml 2>/dev/null
    python3 "$SCRIPT_DIR/scripts/compute_aiprovider_dependency_fingerprint.py" 2>/dev/null
}

This is roughly 10 times faster for many-small-file workloads while preserving the same practical rebuild signal. .env and .env.* are excluded because runtime model, key, and Base URL changes should not force an image rebuild.

Docker Build Context

AI Provider only needs root pyproject.toml, uv.lock, and aiprovider/ source code. Sending the entire repository as Docker build context wastes time on frontend assets, PDFs, historical data, and Unreal files.

The root .dockerignore now narrows the context:

**

!pyproject.toml
!uv.lock
!aiprovider/
!aiprovider/**

aiprovider/.env
aiprovider/.env.*
!aiprovider/.env.example

The Dockerfile copies only AI Provider inputs:

COPY pyproject.toml uv.lock /app/
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev

COPY aiprovider /app/aiprovider

uv sync uses a BuildKit cache mount. The first build may still depend on network speed, but later builds reuse /root/.cache/uv.

Runtime Configuration

Before starting AI Provider, planet.sh generates a temporary env-file and passes it to Compose or the manual docker run fallback. Configuration priority:

  1. aiprovider/.env
  2. simple export AI_...=... or AI_...=... lines from ~/.zshrc

The default parser is static and only covers AI Provider, image, and proxy variables. It avoids executing interactive shell initialization. Complex shell expansion can be enabled explicitly:

PLANET_LOAD_ZSHRC_ENV=source ./planet.sh start -a

To ignore personal shell config during debugging:

PLANET_LOAD_ZSHRC_ENV=0 ./planet.sh start -a

Skip-Rebuild Behavior

When the fingerprint matches, the script skips docker compose build and starts the existing container:

docker start planet_aiprovider

docker stop stops the container without deleting the image. cleanup_exit_containers removes exited containers but not images, so the next docker start can reuse the existing image.

Issue 2: Slow Port Cleanup

Cause

wait_for_port_release could wait up to 45 seconds by default: 15 attempts times 3 seconds.

Fix

Background process cleanup now uses a 3-second timeout: TERM, 1.5 seconds, KILL, 1.5 seconds.

PORT_RELEASE_ATTEMPTS=15
PORT_RELEASE_INTERVAL=0.2

wait_for_port_release "$port" 15 0.2

wait_for_port_release accepts optional parameters so different situations can choose different timeouts.

Issue 3: Port Detection Used Python

Cause

can_bind_port used python3 -c "import socket..."; each call cost about 300 ms.

Fix

Prefer system tools and keep Python as a fallback:

can_bind_port() {
    local port="$1"
    if command -v ss >/dev/null 2>&1; then
        ! ss -tlnH 2>/dev/null | awk '{print $4}' | grep -qE ":${port}$"
        return
    fi
    if command -v lsof >/dev/null 2>&1; then
        [ -z "$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null)" ]
        return
    fi
    python3 - "$port" <<'PY'
import sys, socket
p = int(sys.argv[1])
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(("", p)); s.close(); sys.exit(0)
except OSError:
    sys.exit(1)
PY
}

Frontend startup now has an additional pre-start cleanup retry layer:

  • PORT_PRESTART_RETRIES: defaults to 3 attempts.
  • PORT_PRESTART_RETRY_INTERVAL: defaults to 2 seconds.

kill_port_if_requested() first cleans listener PIDs visible in the current environment. It only checks for Windows-side listeners when the script detects WSL, no local listener PID is visible, and the port still cannot bind. In that WSL-only path it tries to stop the owning Windows service or force-stop the owning process through PowerShell. If permissions are missing, or a system service such as iphlpsvc refuses to stop, the script prints the Windows listener details and stops startup immediately instead of launching the service into the same port error. Non-WSL environments do not run the Windows cleanup path. At that point, use Administrator PowerShell to clear the portproxy/service ownership, or choose another port.

Issue 4: restart Behavior

Before the stamp path fix:

  • restart -b: stop all services, check fingerprint, rebuild only when needed, then start.
  • plain restart: stop all services, then often rebuild AI Provider because /tmp lost the stamp.

After moving the stamp file, plain restart uses the same stop + start behavior and the same fingerprint check as restart -b.

Optional Motion Agent Startup

planet.sh can now manage the local Motion Capture Agent. It is disabled by default so ordinary development machines do not fail startup when cameras, OpenCV, or MediaPipe are unavailable.

Start it with:

./planet.sh start --motion-agent

Common options:

  • --motion-agent / -m: start or restart the Motion Agent for this command.
  • --motion-agent-port <port>: override the default WebSocket port 8765.
  • --motion-agent-camera-indexes <indexes>: override auto-detected camera indexes, for example 0 or 0,1. The same can be provided through MOTION_AGENT_CAMERA_INDEXES=0,1.
  • --motion-agent-camera-urls <urls>: use RTSP/HTTP camera streams, useful for WSL, phone cameras, or network cameras. The same can be provided through MOTION_AGENT_CAMERA_URLS=....
  • --motion-agent-dry-run: start only the protocol service without opening cameras or loading CV dependencies; useful for Web client debugging.

Non-dry-run live mode checks mediapipe and opencv-python before startup. If the current .venv is missing them, the script automatically runs:

uv add mediapipe opencv-python

To disable startup-time auto-install:

PLANET_MOTION_AGENT_AUTO_INSTALL=0 ./planet.sh start --motion-agent

Live mode auto-detects /dev/video* and passes the first two indexes to the Motion Agent. In WSL, Windows cameras usually do not appear as /dev/video* automatically. Check available devices first:

ls /dev/video*

To override auto-detection, pass indexes explicitly:

./planet.sh start --motion-agent --motion-agent-camera-indexes 1,2

In WSL, the more general path is to connect a phone or network camera through an RTSP/HTTP stream:

./planet.sh start --motion-agent --motion-agent-camera-urls http://192.168.1.20:8080/video

If WSL has no /dev/video* and no --motion-agent-camera-urls is provided, live startup stops and prints guidance instead of silently falling back to dry-run. Choose one of:

./planet.sh start --motion-agent --motion-agent-camera-urls http://<phone-ip>:8080/video
./planet.sh start --motion-agent --motion-agent-dry-run

Automatic dry-run fallback only happens when PLANET_MOTION_AGENT_WSL_ALLOW_DRY_RUN_FALLBACK=1 is explicitly set.

Environment-variable startup is also supported:

PLANET_START_MOTION_AGENT=1 ./planet.sh start
MOTION_AGENT_DRY_RUN=1 PLANET_START_MOTION_AGENT=1 ./planet.sh start

Logs:

./planet.sh log -m

To expose it together with the frontend on the LAN:

./planet.sh start --allow-lan --motion-agent

In this mode the Motion Agent binds 0.0.0.0, and startup output prints both the local WebSocket URL and the recommended LAN WebSocket URL. When opening Earth from another LAN browser, point motionAgent at the display machine:

http://<LAN_IP>:3000/earth?motion=1&motionAgent=ws://<LAN_IP>:8765/ws/gestures

./planet.sh stop also stops a script-managed Motion Agent. ./planet.sh health reports its online/offline status. The Earth page still requires ?motion=1 or browser local storage to enable the Web client connection explicitly.

For ordinary web, WSL, or no-install demo scenarios, you can skip Motion Agent entirely: choose the Browser Camera input source in Earth settings and enable Motion Debug Mode. This route uses browser getUserMedia, so the page must run on HTTPS or localhost and the user must grant camera permission.

Other Cleanup

Two redundant sleep 3 waits were removed because health checks already cover the same readiness:

  • start_backend_service: post-database-health-check sleep.
  • restart_database_service: post-restart sleep.