Skip to content

perf(startup): scope orphan reapers to server commands, share one /proc walk, allow opt-out #399

Description

@debugmcpdev

Problem

The two startup orphan reapers (reapOrphanJvms, reapOrphanProxies) are awaited at the very top of main(), before commander even parses argv:

  • src/index.ts:108-111await Promise.allSettled([reapOrphanJvms(...), reapOrphanProxies(...)])
  • createCLI() / program.parseAsync() run strictly after

Consequences:

  1. They run for every invocation, including --version, --help, and bad-arg errors — not just when a server is actually starting. A --version call on a busy Linux host pays the full double /proc walk.
  2. They block startup by their full duration (~250 ms post-perf(reaper): bound /proc scan concurrency at startup #395 on a 1600-process host; more on darwin/win32 where they shell out to ps/PowerShell).
  3. The two reapers walk /proc independently and concurrently — the same readdir('/proc') + per-pid cmdline read happens twice at the same time (2 × PROC_SCAN_CONCURRENCY reads in flight post-perf(reaper): bound /proc scan concurrency at startup #395), for two different marker greps over identical data.

There is also no env opt-out for containerized/ephemeral environments where orphans are impossible (fresh PID namespace) and the scan is pure waste.

Proposal

  • Move the reaper invocation into the server-starting command actions (stdio/http/sse) so --version/--help never pay it.
  • Share one /proc walk: a single scan produces (pid, argv) pairs; each reaper contributes a matcher over that list. Halves the I/O and the transient allocations.
  • Consider firing the reapers after the transport is up (fire-and-forget with logging) instead of blocking startup — orphan reaping is not a precondition for serving requests. Needs care re: process-lifetime races, so measure first.
  • Optional MCP_SKIP_ORPHAN_REAPERS=1 escape hatch for PID-namespaced containers.
  • Further slimming candidate, measured by the perf(reaper): bound /proc scan concurrency at startup #395 author but deferred to keep that PR's I/O model unchanged: a synchronous openSync/readSync scan with one recycled buffer (their numbers: 57 MB / 25 ms vs 69 MB / 99 ms for the bounded-async version). Requires rewriting the fs/promises mocks in the reaper tests.

Measuring

scripts/mem-bench.mjs (busy-proc scenario) synthesizes an N-process /proc inside the docker image and reports RSS + peak (VmHWM) medians across trials — use it to validate any change here. Baseline numbers are in the PR that added the harness.

Follow-up to #395.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions