mobile-debug-mcp 0.32.0 → 0.32.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +1 -0
  2. package/dist/observe/android-tree-bridge.js +202 -0
  3. package/dist/observe/android.js +49 -15
  4. package/dist/observe/ios.js +45 -29
  5. package/dist/observe/performance-metrics.js +124 -0
  6. package/dist/server/tool-handlers.js +31 -9
  7. package/dist/server-core.js +1 -1
  8. package/dist/utils/exec.js +15 -4
  9. package/docs/CHANGELOG.md +6 -0
  10. package/docs/ROADMAP.md +9 -0
  11. package/docs/benchmarks/ui-tree-performance/README.md +58 -0
  12. package/docs/benchmarks/ui-tree-performance/baseline.json +59546 -0
  13. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +29 -0
  14. package/docs/benchmarks/ui-tree-performance/candidate.json +51691 -0
  15. package/docs/benchmarks/ui-tree-performance/comparison-attempt-1.json +110679 -0
  16. package/docs/benchmarks/ui-tree-performance/comparison.json +111230 -0
  17. package/docs/benchmarks/ui-tree-performance/comparison.md +41 -0
  18. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +59560 -0
  19. package/docs/specs/019-ui-tree-observation-performance/spec.md +148 -0
  20. package/docs/tools/observe.md +2 -0
  21. package/package.json +1 -1
  22. package/skills/mobile-debug-tools-usage/SKILL.md +74 -0
  23. package/src/observe/android-tree-bridge.ts +178 -0
  24. package/src/observe/android.ts +44 -17
  25. package/src/observe/ios.ts +45 -36
  26. package/src/observe/performance-metrics.ts +125 -0
  27. package/src/server/tool-handlers.ts +34 -10
  28. package/src/server-core.ts +1 -1
  29. package/src/utils/exec.ts +10 -4
  30. package/test/device/automated/ui-tree-performance/benchmark.ts +401 -0
  31. package/test/device/automated/ui-tree-performance/bridge-probe.ts +197 -0
  32. package/test/fixtures/latency-journey-app/AndroidManifest.xml +2 -1
  33. package/test/fixtures/latency-journey-app/README.md +19 -1
  34. package/test/fixtures/latency-journey-app/src/MainActivity.java +88 -1
  35. package/test/fixtures/ui-tree-bridge/AndroidManifest.xml +6 -0
  36. package/test/fixtures/ui-tree-bridge/build.ts +34 -0
  37. package/test/fixtures/ui-tree-bridge/src/TreeInstrumentation.java +126 -0
  38. package/test/unit/observe/android_tree_acquisition.test.ts +70 -0
  39. package/test/unit/observe/android_tree_bridge_cleanup.test.ts +95 -0
  40. package/test/unit/observe/ios_tree_acquisition.test.ts +77 -0
  41. package/test/unit/observe/performance_metrics.test.ts +40 -0
  42. package/test/unit/server/operation_budget.test.ts +13 -0
  43. package/test/unit/server/run_journey.test.ts +30 -0
  44. /package/docs/{specs-deltas/001-state-verification.md → specs/001-state-verification/spec.md} +0 -0
  45. /package/docs/{specs-deltas/002-richer-element-identity → specs/002-richer-element-identity/spec.md} +0 -0
  46. /package/docs/{specs-deltas/003-wait-and-synchronization-reliability.md → specs/003-wait-and-synchronization-reliability/spec.md} +0 -0
  47. /package/docs/{specs-deltas/004-action-verification-routing.md → specs/004-action-verification-routing/spec.md} +0 -0
  48. /package/docs/{specs-deltas/005-unified-action-execution-and-verification-model.md → specs/005-unified-action-execution-and-verification-model/spec.md} +0 -0
  49. /package/docs/{specs-deltas/006-runtime-action-instrumentation-and-binding-layer.md → specs/006-runtime-action-instrumentation-and-binding-layer/spec.md} +0 -0
  50. /package/docs/{specs-deltas/007-actionability-resolution-and-executable-target-selection.md → specs/007-actionability-resolution-and-executable-target-selection/spec.md} +0 -0
  51. /package/docs/{specs-deltas/008-adjustable-control-support-and-semantic-value-manipulation.md → specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md} +0 -0
  52. /package/docs/{specs-deltas/009-semantic-control-modeling-for-custom-and-composite-controls.md → specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md} +0 -0
  53. /package/docs/{specs-deltas/010-verification-stabilization-and-temporal-convergence.md → specs/010-verification-stabilization-and-temporal-convergence/spec.md} +0 -0
  54. /package/docs/{specs-deltas/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md → specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md} +0 -0
  55. /package/docs/{specs-deltas/011.1-recovery-contract-types-and-runtime-wiring-spec.md → specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md} +0 -0
  56. /package/docs/{specs-deltas/012-action-trace-and-xecution-observability.md → specs/012-action-trace-and-xecution-observability/spec.md} +0 -0
  57. /package/docs/{specs-deltas/013-wait-and-synchronization-reliability.md → specs/013-wait-and-synchronization-reliability/spec.md} +0 -0
  58. /package/docs/{specs-deltas/014-actionability-resolution.md → specs/014-actionability-resolution/spec.md} +0 -0
  59. /package/docs/{specs-deltas/015-adjustable-control-support.md → specs/015-adjustable-control-support/spec.md} +0 -0
  60. /package/docs/{specs-deltas/016-environment-tools.md → specs/016-environment-tools/spec.md} +0 -0
  61. /package/docs/{specs-deltas/018-interaction-latency-and-batched-journeys.md → specs/018-interaction-latency-and-batched-journeys/spec.md} +0 -0
@@ -9,6 +9,7 @@ import { ToolsObserve } from '../observe/index.js';
9
9
  import { classifyActionOutcome } from '../interact/classify.js';
10
10
  import { ToolsNetwork } from '../network/index.js';
11
11
  import { getSystemStatus } from '../system/index.js';
12
+ import { recordObservationConsumer, recordObservationEvent, withObservationContext } from '../observe/performance-metrics.js';
12
13
  import { buildActionExecutionResult, captureActionFingerprint, getArrayArg, getBooleanArg, getNumberArg, getObjectArg, getStringArg, inferGenericFailure, inferScrollFailure, requireBooleanArg, requireNumberArg, requireObjectArg, requireStringArg, wrapResponse, wrapToolError } from './common.js';
13
14
  function interactionControls(args) {
14
15
  const verificationMode = getStringArg(args, 'verificationMode') ?? 'full';
@@ -79,11 +80,25 @@ function actionHandler(type, execute) {
79
80
  result.retryable = !dispatchStarted;
80
81
  result.action_id ??= randomUUID();
81
82
  result.action_type ??= type;
83
+ recordObservationEvent({
84
+ step_id: args._stepId,
85
+ action_type: type,
86
+ dispatch_ms: timing.dispatch_ms,
87
+ verification_mode: controls.verificationMode,
88
+ dispatch_started: dispatchStarted
89
+ });
82
90
  let delta;
83
91
  let observationAvailable = false;
84
92
  if (result.success && controls.verificationMode === 'light') {
85
- const tree = await measure('post_observation_ms', () => runWithBudget(controls.verificationTimeoutMs, () => ToolsObserve.getUITreeHandler({ platform: args.platform ?? result.device?.platform, deviceId: args.deviceId ?? result.device?.id }))).catch(() => null);
93
+ const verificationStarted = performance.now();
94
+ const tree = await withObservationContext({ stepId: args._stepId, purpose: 'verification' }, () => measure('post_observation_ms', () => runWithBudget(controls.verificationTimeoutMs, () => ToolsObserve.getUITreeHandler({ platform: args.platform ?? result.device?.platform, deviceId: args.deviceId ?? result.device?.id })))).catch(() => null);
86
95
  observationAvailable = !!tree && !tree.error;
96
+ recordObservationEvent({
97
+ verification_budget_ms: controls.verificationTimeoutMs,
98
+ verification_elapsed_ms: performance.now() - verificationStarted,
99
+ verification_outcome: observationAvailable ? 'observed' : 'unavailable',
100
+ verification_budget_expired: !observationAvailable && performance.now() - verificationStarted >= controls.verificationTimeoutMs
101
+ });
87
102
  if (observationAvailable && tree.device)
88
103
  delta = ToolsObserve.getLatestStateDelta(tree.device.platform, tree.device.id) ?? undefined;
89
104
  }
@@ -286,7 +301,8 @@ async function handleGetUITree(args) {
286
301
  const deviceId = getStringArg(args, 'deviceId');
287
302
  const responseMode = getStringArg(args, 'responseMode') ?? 'debug';
288
303
  const sinceSnapshotRevision = getNumberArg(args, 'sinceSnapshotRevision');
289
- const res = await ToolsObserve.getUITreeHandler({ platform, deviceId, responseMode, sinceSnapshotRevision });
304
+ const metricsStepId = process.env.MOBILE_DEBUG_MCP_UI_TREE_METRICS === '1' ? getStringArg(args, '_metricsStepId') : undefined;
305
+ const res = await withObservationContext({ stepId: metricsStepId, purpose: metricsStepId?.includes('post-response') ? 'assertion' : 'debug' }, () => ToolsObserve.getUITreeHandler({ platform, deviceId, responseMode, sinceSnapshotRevision }));
290
306
  return wrapResponse(res);
291
307
  }
292
308
  async function handleGetCurrentScreen(args) {
@@ -396,7 +412,9 @@ async function handleFindElement(args) {
396
412
  const timeoutMs = getNumberArg(args, 'timeoutMs') ?? 3000;
397
413
  const platform = getStringArg(args, 'platform');
398
414
  const deviceId = getStringArg(args, 'deviceId');
399
- const res = await ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId });
415
+ const targetStarted = performance.now();
416
+ const res = await withObservationContext({ purpose: 'target' }, () => ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId }));
417
+ recordObservationEvent({ target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: res.found ? 'found' : 'not_found', operation: 'find_element' });
400
418
  return wrapResponse(res);
401
419
  }
402
420
  async function executeTap(args) {
@@ -431,13 +449,17 @@ async function executeTapElement(args) {
431
449
  let freshTree;
432
450
  if (selector) {
433
451
  const waitFor = getObjectArg(args, 'waitFor');
434
- const resolution = await measure('target_resolution_ms', () => runWithBudget(waitFor?.timeoutMs ?? 1500, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: waitFor?.timeoutMs ?? 1500, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') })));
452
+ const targetStarted = performance.now();
453
+ const resolution = await withObservationContext({ stepId: args._stepId, purpose: 'target' }, () => measure('target_resolution_ms', () => runWithBudget(waitFor?.timeoutMs ?? 1500, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: waitFor?.timeoutMs ?? 1500, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }))));
454
+ recordObservationEvent({ step_id: args._stepId, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status });
435
455
  if (resolution.status !== 'success' || !resolution.element?.elementId)
436
456
  return wrapResponse({ success: false, action_type: 'tap_element', failure_code: resolution.error?.code ?? 'ELEMENT_NOT_FOUND', retryable: true });
437
457
  resolvedId = resolution.element.elementId;
438
458
  freshTree = resolution._tree;
439
459
  }
440
460
  ToolsNetwork.notifyActionStart();
461
+ if (freshTree)
462
+ recordObservationConsumer(freshTree, 'target', args._stepId);
441
463
  const res = await measure('dispatch_ms', () => ToolsInteract.tapElementHandler({ elementId: resolvedId, freshTree, verificationMode, timeoutMs: actionTimeoutMs, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }));
442
464
  return wrapResponse(res);
443
465
  }
@@ -505,14 +527,14 @@ async function handleRunJourney(args) {
505
527
  }
506
528
  try {
507
529
  if (step.type === 'tap') {
508
- result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }));
530
+ result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }));
509
531
  }
510
532
  else if (step.type === 'wait') {
511
- result = await ToolsInteract.waitForUIHandler({ selector: step.selector, condition: step.condition ?? 'exists', timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId });
533
+ result = await withObservationContext({ stepId: step.id, purpose: 'target' }, () => ToolsInteract.waitForUIHandler({ selector: step.selector, condition: step.condition ?? 'exists', timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId }));
512
534
  result = { success: result.status === 'success', ...result };
513
535
  }
514
536
  else if (step.type === 'assert') {
515
- result = await runWithBudget(step.timeoutMs ?? 5000, async () => {
537
+ result = await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, async () => {
516
538
  const assertion = step.assertion;
517
539
  if (assertion?.kind === 'element_visible')
518
540
  result = await ToolsInteract.expectElementVisibleHandler({ selector: assertion.selector, timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId });
@@ -525,10 +547,10 @@ async function handleRunJourney(args) {
525
547
  else
526
548
  result = { success: false, failure_code: 'INVALID_ASSERTION' };
527
549
  return result;
528
- });
550
+ }));
529
551
  }
530
552
  else if (step.type === 'start_app') {
531
- result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
553
+ result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
532
554
  }
533
555
  else {
534
556
  result = { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' };
@@ -6,7 +6,7 @@ import { handleToolCall } from './server/tool-handlers.js';
6
6
  export { wrapResponse, toolDefinitions, handleToolCall };
7
7
  export const serverInfo = {
8
8
  name: 'mobile-debug-mcp',
9
- version: '0.32.0'
9
+ version: '0.32.2'
10
10
  };
11
11
  export function createServer() {
12
12
  const server = new Server(serverInfo, {
@@ -12,24 +12,35 @@ export async function execCmd(cmd, args, opts = {}) {
12
12
  if (child.stderr)
13
13
  child.stderr.on('data', (d) => { stderr += d.toString(); });
14
14
  let timedOut = false;
15
+ let forceKillTimer = null;
15
16
  const timer = timeout && timeout > 0 ? setTimeout(() => {
16
17
  timedOut = true;
17
18
  try {
18
19
  child.kill();
19
20
  }
20
21
  catch { }
21
- resolve({ exitCode: null, stdout: stdout.trim(), stderr: 'ACTION_TIMEOUT' });
22
+ forceKillTimer = setTimeout(() => {
23
+ try {
24
+ child.kill('SIGKILL');
25
+ }
26
+ catch { }
27
+ }, 1000);
28
+ forceKillTimer.unref();
22
29
  }, timeout) : null;
23
30
  child.on('close', (code) => {
24
31
  if (timer)
25
32
  clearTimeout(timer);
26
- if (timedOut)
27
- return;
28
- resolve({ exitCode: code, stdout: stdout.trim(), stderr: stderr.trim() });
33
+ if (forceKillTimer)
34
+ clearTimeout(forceKillTimer);
35
+ resolve(timedOut
36
+ ? { exitCode: null, stdout: stdout.trim(), stderr: 'ACTION_TIMEOUT' }
37
+ : { exitCode: code, stdout: stdout.trim(), stderr: stderr.trim() });
29
38
  });
30
39
  child.on('error', (err) => {
31
40
  if (timer)
32
41
  clearTimeout(timer);
42
+ if (forceKillTimer)
43
+ clearTimeout(forceKillTimer);
33
44
  reject(err);
34
45
  });
35
46
  });
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
+ ## [0.32.2]
6
+ - bug fix
7
+
8
+ ## [0.32.1]
9
+ - Further performance enhancements
10
+
5
11
  ## [0.32.0]
6
12
  - Added bounded interaction verification modes, compact responses, selector-based taps, and batched journeys to reduce MCP interaction latency.
7
13
 
package/docs/ROADMAP.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - Actionability Resolution
12
12
  - Environment Auto-Configuration and Toolchain Discovery
13
13
  - Adjustable Control Support
14
+ - UI-Tree Observation Performance (Android implemented; iOS latency unmeasured)
14
15
 
15
16
  ## Todo
16
17
 
@@ -21,6 +22,14 @@
21
22
  - Pinch to Zoom
22
23
  - Advanced Trace Correlation and Analysis
23
24
 
25
+ # UI-Tree Observation Performance
26
+
27
+ **Status:** Android implemented and benchmarked; iOS latency remains unmeasured — [Specification 019](specs/019-ui-tree-observation-performance/spec.md)
28
+
29
+ Measure physical hierarchy reads and their cost, then reduce the measured hot path while preserving fresh post-action and asynchronous UI evidence. Validate the change against the existing static journey and a controlled network-backed screen before claiming a speedup.
30
+
31
+ Exit criteria and performance gates are defined in Specification 019.
32
+
24
33
  # Stronger State Verification
25
34
 
26
35
  ## Rationale
@@ -0,0 +1,58 @@
1
+ # UI tree observation performance benchmark
2
+
3
+ This paired Android benchmark compares a clean, built instrumentation baseline
4
+ with a clean, built candidate. It runs the Spec 018 static journey and a local
5
+ network-backed loading-to-ready flow against the same fixture APK and emulator.
6
+ The network flow asserts a run-unique value from a physical tree read that
7
+ started after the local server sent its response.
8
+
9
+ Capture the instrumented baseline first from a clean, built baseline checkout:
10
+
11
+ ```sh
12
+ UI_TREE_BASELINE_ROOT=/path/to/instrumented-baseline \
13
+ LATENCY_DEVICE_ID=emulator-5554 \
14
+ npx tsx test/device/automated/ui-tree-performance/benchmark.ts --baseline-only
15
+ ```
16
+
17
+ This writes `baseline.json`. After measuring and implementing an optimization,
18
+ run the paired comparison from the clean, built candidate checkout:
19
+
20
+ ```sh
21
+ UI_TREE_BASELINE_ROOT=/path/to/instrumented-baseline \
22
+ UI_TREE_CANDIDATE_ROOT=/path/to/optimized-candidate \
23
+ LATENCY_DEVICE_ID=emulator-5554 \
24
+ npx tsx test/device/automated/ui-tree-performance/benchmark.ts
25
+ ```
26
+
27
+ Add `--smoke --baseline-only` for one attempt per flow while checking the
28
+ fixture and emulator connection. Smoke mode writes no performance report and
29
+ does not satisfy the sample-count or acceptance gates.
30
+
31
+ Both input checkouts must contain `dist/server.js` and have clean Git working
32
+ trees. The harness checks both before installing or resetting the fixture. It
33
+ builds one APK from the candidate fixture and uses that exact APK in both modes.
34
+ Each flow and mode receives three warm-ups and ten measured attempts; baseline
35
+ and candidate order alternates by pair.
36
+
37
+ The paired run writes `baseline.json`, `candidate.json`, `comparison.json`, and
38
+ `comparison.md` here. Timing events are enabled only in the spawned benchmark
39
+ servers through `MOBILE_DEBUG_MCP_UI_TREE_METRICS=1`; normal MCP responses do
40
+ not gain timing fields. Events contain timings, platform, generated journey step
41
+ IDs, and opaque acquisition IDs. They omit UI contents, app identifiers,
42
+ screenshots, logs, payloads, and device serials. iOS latency remains unmeasured.
43
+
44
+ The checked-in `initial-baseline.json` is the standalone capture taken before
45
+ the optimization. The checked-in `baseline.json` is the baseline side of the
46
+ paired run, so it shares the fixture APK hash with `candidate.json`.
47
+ `comparison-attempt-1.json` retains an earlier paired run with two visible
48
+ network failures; see `comparison.md` for the outcome and limitation.
49
+ The report commit SHAs are available locally as `codex/spec-019-baseline` and
50
+ `codex/spec-019-candidate`; create clean, built checkouts of those refs to
51
+ reproduce the paired measurement.
52
+
53
+ For the opt-in Android bridge, run
54
+ `LATENCY_DEVICE_ID=emulator-5554 node --import tsx test/device/automated/ui-tree-performance/bridge-probe.ts`.
55
+ Its `bridge-probe.json` compares ten bridge and legacy reads, checks a network
56
+ update, and checks a timer that changes visually without emitting an
57
+ accessibility event. The timer check catches a stale accessibility cache even
58
+ when every bridge request calls `getRootInActiveWindow()`.