Skip to content

perf: start the CLI faster; load device-mcp on first mobile use - #73

Draft
cryptotavares wants to merge 3 commits into
mainfrom
cryptotavares/mmcli-p1-25-cold-start
Draft

cryptotavares wants to merge 3 commits into
mainfrom
cryptotavares/mmcli-p1-25-cold-start

Conversation

@cryptotavares

Copy link
Copy Markdown
Collaborator

Summary: Every mm call now costs about 0.1 s of CLI time instead of 0.23 s, and a browser daemon no longer loads @metamask/device-mcp. The README tells agents to call node_modules/.bin/mm, because yarn mm adds another 0.35-0.45 s per call.

Stack context: mm CLI v2, Phase 1, P1-25 (first Tier 1 ticket after the stack merge). Base: main.

Problem

Measured on main 227674a, daemon running, p50 of 10:

Per call ms
node -e 0 65
node_modules/.bin/mm status 227
yarn mm status 574 (Extension) / 702 (Mobile)
  • resolveWorktreeRoot (src/cli/mm.ts:270) runs git rev-parse --show-toplevel on every call: about 90 ms.
  • The CLI imports observeLevelSchema from validation/schemas.ts only to read three strings. That loads zod: 79 of the CLI's 95 modules, about 25 ms.
  • MobilePlatformDriver imports device-mcp at module load, and src/index.ts re-exports it, so every browser daemon loads 54 device-mcp modules at start.
  • The README says to use yarn mm. On a 400-call release run, yarn alone is about 2.5-3 minutes.

Solution

Change Where
Walk up from the real cwd to the nearest .git entry (directory or file). Fall back to git rev-parse when there is none, or when GIT_DIR / GIT_WORK_TREE is set findGitWorktreeRoot in src/cli/mm.ts
Move the observe levels to a zod-free module; schemas.ts builds its enum from it src/validation/observe-level.ts
Load device-mcp on first use: require in the CJS build (shares the consumer's copy), import() in the ESM build. The Hermes session is created on first use src/platform/device-mcp-runtime.ts, mobile-platform-driver.ts
Recommend node_modules/.bin/mm for agents, with the measured cost README.md

Outcome

Before = main, after = this branch, same host, back to back. No device is touched by the bench.

Metric App Before p50 ms After p50 ms
node_modules/.bin/mm status Extension 227 99
node_modules/.bin/mm status Mobile 227 98
yarn mm status Extension 574 439
yarn mm status Mobile 702 564
mm --help both 93-95 61-62
Daemon start, 8 interleaved rounds Extension 3943 3704
Daemon start, 8 interleaved rounds Mobile 1013 878
CLI modules loaded 95 15
device-mcp modules in a browser daemon Extension 54 0

Decisions

Decision Why Rejected
require in CJS, import() in ESM Native import() in the CJS build loads device-mcp's ESM build as a second copy next to the consumer's CJS copy. Module state (for example getHermesSession()) would split import() everywhere; createRequire (works, but adds a shim for no gain)
Keep the git rev-parse fallback Covers GIT_DIR / GIT_WORK_TREE setups and unusual layouts at no cost to the normal path Removing git entirely
.git file counts as a root Linked worktrees (our own setup) and submodules have a .git file; git rev-parse --show-toplevel returns that directory too Directory-only check

Review guide

  1. src/platform/device-mcp-runtime.ts and its test: the test fails on main ("expected true to be false") and passes here.
  2. src/platform/mobile-platform-driver.ts: the four call sites that now load device-mcp (#hermesCdp, hermesTargets, generateLocators, #getHermesSession). Types stay import type.
  3. findGitWorktreeRoot in src/cli/mm.ts and its tests.
  4. The Hermes tests now mock ./device-mcp-runtime.js, because vi.mock('@metamask/device-mcp') does not intercept a CJS require.

Breaking changes

None. MobilePlatformDriver's public API is unchanged.

Consumer impact

Consumer Change needed Done on branch
Extension test/e2e/playwright/llm-workflow/ None n/a
Mobile tests/llm-workflow/ Docs only: mention node_modules/.bin/mm cryptotavares/mm-cli-v2-mobile 66a504511f0
Testing details

Yalc build of this branch (0.12.0+e92d761d):

  • Extension e2e context (fake wallet, headless): launch, unlock, describe-screen, get-state, list-testids, cdp, screenshot. All exit 0.
  • iOS simulator (dev client), read-only: launch, unlock, list-testids (wallet-screen), describe-screen, cdp Runtime.evaluate 1+1 = 2, screenshot. All exit 0. cdp goes through the lazily loaded device-mcp.
  • Real Node: require('dist/index.cjs') loads 0 device-mcp modules. The first generateLocators() loads 54, the same copy as the consumer's require('@metamask/device-mcp'). import('dist/index.mjs') works through import().
 Test Files  54 passed (54)
      Tests  1844 passed (1844)
All files          |   97.55 |    94.94 |   96.75 |   97.69 |

yarn lint passes. attw --pack is green for node10, node16 CJS, node16 ESM and bundler. Coverage thresholds are unchanged.

- Find the project root by walking up to the nearest `.git` entry instead
  of running `git rev-parse` (about 90 ms per call). Fall back to git when
  no `.git` entry is found or GIT_DIR / GIT_WORK_TREE is set.
- Do not load zod in the CLI: the `--observe` levels move to a zod-free
  module that `schemas.ts` also uses (CLI modules 95 -> 15).
- `MobilePlatformDriver` loads `@metamask/device-mcp` on first use, so a
  browser-only daemon never loads it. The CJS build uses `require` to share
  the consumer's copy; the ESM build uses `import()`.
- README: recommend `node_modules/.bin/mm` over `yarn mm` / `npx mm` for
  agents.
@cryptotavares
cryptotavares added this pull request to stack #75 October 29, 2026 23:41

This branch has not been deployed

No deployments
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.

1 participant