@swmansion/argent 0.32.1-next.13 → 0.32.1-next.14

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.
@@ -1709,6 +1709,8 @@ var init_failure_codes = __esm({
1709
1709
  REACT_PROFILER_START_FAILED: "REACT_PROFILER_START_FAILED",
1710
1710
  REACT_PROFILER_START_VERIFY_FAILED: "REACT_PROFILER_START_VERIFY_FAILED",
1711
1711
  REACT_PROFILER_SESSION_TORN_DOWN: "REACT_PROFILER_SESSION_TORN_DOWN",
1712
+ REACT_PROFILER_SESSION_TAKEN_OVER: "REACT_PROFILER_SESSION_TAKEN_OVER",
1713
+ REACT_PROFILER_STOP_IN_PROGRESS: "REACT_PROFILER_STOP_IN_PROGRESS",
1712
1714
  REACT_PROFILER_HOOK_ERROR: "REACT_PROFILER_HOOK_ERROR",
1713
1715
  REACT_PROFILER_ANALYZE_NO_DATA: "REACT_PROFILER_ANALYZE_NO_DATA",
1714
1716
  NATIVE_PROFILER_FACTORY_OPTIONS_MISSING: "NATIVE_PROFILER_FACTORY_OPTIONS_MISSING",
@@ -101186,7 +101188,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
101186
101188
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
101187
101189
  function readCliVersion() {
101188
101190
  if (true) {
101189
- return "0.32.1-next.13";
101191
+ return "0.32.1-next.14";
101190
101192
  }
101191
101193
  return "0.0.0";
101192
101194
  }
@@ -146304,23 +146306,31 @@ var BOOTSTRAP_DEVTOOLS_BACKEND_SCRIPT = `
146304
146306
  });
146305
146307
  })()
146306
146308
  `;
146307
- var HEARTBEAT_SCRIPT = `
146309
+ function buildHeartbeatScript(sessionId) {
146310
+ return `
146308
146311
  (function(){
146312
+ var o = globalThis.__ARGENT_PROFILER_OWNER__;
146313
+ if (!o || o.sessionId !== ${JSON.stringify(sessionId)}) return;
146309
146314
  if (typeof globalThis.__argent_profilerHeartbeat === 'function') {
146310
146315
  try { globalThis.__argent_profilerHeartbeat(); } catch (_e) {}
146311
146316
  }
146312
146317
  })()
146313
146318
  `;
146319
+ }
146314
146320
  var READ_STATE_SCRIPT = `
146315
146321
  (function __argent_readState() {
146316
146322
  var h = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
146317
146323
  if (!h) return JSON.stringify({ hookExists: false });
146318
146324
  var rendererInterfaceFound = false;
146319
146325
  var isRunning = false;
146326
+ var rendererStartedAtEpochMs = [];
146320
146327
  if (h.rendererInterfaces && typeof h.rendererInterfaces.forEach === 'function') {
146321
146328
  h.rendererInterfaces.forEach(function (ri) {
146322
146329
  rendererInterfaceFound = true;
146323
146330
  if (ri && ri.__argent_isProfiling__ === true) isRunning = true;
146331
+ if (ri && typeof ri.__argent_startedAtEpochMs__ === 'number') {
146332
+ rendererStartedAtEpochMs.push(ri.__argent_startedAtEpochMs__);
146333
+ }
146324
146334
  });
146325
146335
  }
146326
146336
  if (!rendererInterfaceFound) {
@@ -146333,6 +146343,7 @@ var READ_STATE_SCRIPT = `
146333
146343
  isRunning: isRunning,
146334
146344
  owner: globalThis.__ARGENT_PROFILER_OWNER__ || null,
146335
146345
  nowEpochMs: Date.now(),
146346
+ rendererStartedAtEpochMs: rendererStartedAtEpochMs,
146336
146347
  });
146337
146348
  })()
146338
146349
  `;
@@ -146618,6 +146629,53 @@ var RESOLVE_FIBER_META_SCRIPT = `
146618
146629
  })()
146619
146630
  `;
146620
146631
 
146632
+ // ../tool-server/src/utils/react-profiler/session-ownership.ts
146633
+ var DEFAULT_STALE_THRESHOLD_MS = 5 * 6e4;
146634
+ var STOP_IN_PROGRESS_NOTE = "A react-profiler-stop is collecting this run. Wait for its result before you call react-profiler-start or react-profiler-stop.";
146635
+ var STALE_OWN_RUN_NOTE = "This recording was started through this tool-server, but its heartbeat is stale, so it may be left over from another agent or an earlier conversation. If you started it, call react-profiler-stop to collect it. If not, ask the user before you discard it with react-profiler-start { force: true }.";
146636
+ function classifyStaleness({
146637
+ owner,
146638
+ nowEpochMs,
146639
+ staleThresholdMs = DEFAULT_STALE_THRESHOLD_MS
146640
+ }) {
146641
+ if (!owner) {
146642
+ return {
146643
+ stale: false,
146644
+ ageSeconds: null,
146645
+ heartbeatAgeSeconds: null,
146646
+ canReclaimWithoutForce: true
146647
+ };
146648
+ }
146649
+ const ageMs = nowEpochMs - owner.startedAtEpochMs;
146650
+ const heartbeatAgeMs = nowEpochMs - owner.lastHeartbeatEpochMs;
146651
+ const stale = heartbeatAgeMs > staleThresholdMs;
146652
+ return {
146653
+ stale,
146654
+ ageSeconds: Math.max(0, ageMs) / 1e3,
146655
+ heartbeatAgeSeconds: Math.max(0, Math.round(heartbeatAgeMs / 1e3)),
146656
+ canReclaimWithoutForce: stale
146657
+ };
146658
+ }
146659
+ function takeoverOf(state3, run2) {
146660
+ if (run2.sessionId === null) return void 0;
146661
+ if (state3.isRunning) {
146662
+ if (state3.owner?.sessionId === run2.sessionId) return void 0;
146663
+ return { by: state3.owner?.sessionId ?? null, stillRecording: true };
146664
+ }
146665
+ const starts = state3.rendererStartedAtEpochMs ?? [];
146666
+ if (starts.length === 0 || run2.profileStartWallMs === null) return void 0;
146667
+ if (starts.includes(run2.profileStartWallMs)) return void 0;
146668
+ return { by: null, stillRecording: false };
146669
+ }
146670
+ async function readTakeover(cdp, run2) {
146671
+ try {
146672
+ const raw = await cdp.evaluate(READ_STATE_SCRIPT);
146673
+ return raw ? takeoverOf(JSON.parse(raw), run2) : void 0;
146674
+ } catch {
146675
+ return void 0;
146676
+ }
146677
+ }
146678
+
146621
146679
  // ../tool-server/src/blueprints/react-profiler-session.ts
146622
146680
  var REACT_PROFILER_SESSION_NAMESPACE = "ReactProfilerSession";
146623
146681
  var reactProfilerSessionBlueprint = {
@@ -146674,6 +146732,7 @@ var reactProfilerSessionBlueprint = {
146674
146732
  detectedArchitecture: null,
146675
146733
  sessionPaths: null,
146676
146734
  profilingActive: false,
146735
+ stopping: false,
146677
146736
  scriptSources: /* @__PURE__ */ new Map(),
146678
146737
  anyCompilerOptimized: null,
146679
146738
  hotCommitIndices: null,
@@ -146744,13 +146803,16 @@ var reactProfilerSessionBlueprint = {
146744
146803
  );
146745
146804
  };
146746
146805
  cdp.events.on("disconnected", onDisconnected);
146806
+ let runtimeReloaded = false;
146747
146807
  const onCdpEvent = (method) => {
146748
146808
  if (method !== "Runtime.executionContextDestroyed" && method !== "Runtime.executionContextsCleared") {
146749
146809
  return;
146750
146810
  }
146751
- if (state3.profilingActive) {
146811
+ runtimeReloaded = true;
146812
+ if (state3.profilingActive || state3.stopping) {
146752
146813
  state3.profilingActive = false;
146753
146814
  clearCachedProfilerPaths(state3.port, state3.deviceId);
146815
+ recordLostProfilerRun(state3, "js_reload");
146754
146816
  stopSamplerInNextRuntime(cdp);
146755
146817
  }
146756
146818
  events.emit("terminated");
@@ -146763,13 +146825,23 @@ var reactProfilerSessionBlueprint = {
146763
146825
  cdp.events.off("scriptParsed", onScriptParsed);
146764
146826
  cdp.events.off("disconnected", onDisconnected);
146765
146827
  cdp.events.off("event", onCdpEvent);
146766
- if (state3.profilingActive) {
146828
+ if (!runtimeReloaded && (state3.profilingActive || state3.sessionId !== null)) {
146767
146829
  state3.profilingActive = false;
146768
- await cdp.send("Runtime.evaluate", {
146769
- expression: STOP_FOR_TAKEOVER_SCRIPT,
146770
- returnByValue: true
146771
- }).catch(warnOnError("STOP_FOR_TAKEOVER_SCRIPT"));
146772
- await cdp.send("Profiler.stop").catch(ignore);
146830
+ const takeover = cdp.isConnected() ? await readTakeover(cdp, state3) : void 0;
146831
+ if (takeover) {
146832
+ recordLostProfilerRun(state3, "taken_over", takeover.by);
146833
+ if (takeover.by === null) await cdp.send("Profiler.stop").catch(ignore);
146834
+ } else {
146835
+ recordLostProfilerRun(
146836
+ state3,
146837
+ cdp.isConnected() ? "session_torn_down" : "cdp_disconnected"
146838
+ );
146839
+ await cdp.send("Runtime.evaluate", {
146840
+ expression: STOP_FOR_TAKEOVER_SCRIPT,
146841
+ returnByValue: true
146842
+ }).catch(warnOnError("STOP_FOR_TAKEOVER_SCRIPT"));
146843
+ await cdp.send("Profiler.stop").catch(ignore);
146844
+ }
146773
146845
  }
146774
146846
  await cdp.send("Profiler.disable").catch(ignore);
146775
146847
  },
@@ -146815,6 +146887,36 @@ function clearCachedProfilerPaths(port, deviceId) {
146815
146887
  profilerPathsCache.delete(cacheKey(port, id));
146816
146888
  }
146817
146889
  }
146890
+ var lostRuns = /* @__PURE__ */ new Map();
146891
+ function findLostRun(port, deviceId) {
146892
+ for (const id of deviceIdsOf(deviceId)) {
146893
+ const entry = lostRuns.get(cacheKey(port, id));
146894
+ if (entry) return entry;
146895
+ }
146896
+ return void 0;
146897
+ }
146898
+ function recordLostProfilerRun(run2, cause, takenOverBy) {
146899
+ const prior = findLostRun(run2.port, run2.deviceId)?.run;
146900
+ if (prior && prior.sessionId !== null && prior.sessionId === run2.sessionId) return;
146901
+ clearLostProfilerRun(run2.port, run2.deviceId);
146902
+ const entry = {
146903
+ run: {
146904
+ cause,
146905
+ sessionId: run2.sessionId,
146906
+ startedAtEpochMs: run2.profileStartWallMs,
146907
+ lostAtEpochMs: Date.now(),
146908
+ ...takenOverBy !== void 0 ? { takenOverBy } : {}
146909
+ },
146910
+ keys: deviceIdsOf(run2.deviceId).map((id) => cacheKey(run2.port, id))
146911
+ };
146912
+ for (const key2 of entry.keys) lostRuns.set(key2, entry);
146913
+ }
146914
+ function getLostProfilerRun(port, deviceId) {
146915
+ return findLostRun(port, deviceId)?.run;
146916
+ }
146917
+ function clearLostProfilerRun(port, deviceId) {
146918
+ for (const key2 of findLostRun(port, deviceId)?.keys ?? []) lostRuns.delete(key2);
146919
+ }
146818
146920
 
146819
146921
  // ../tool-server/src/tools/devices/boot-device.ts
146820
146922
  var import_node_child_process32 = require("node:child_process");
@@ -158758,30 +158860,6 @@ var crypto6 = __toESM(require("node:crypto"));
158758
158860
  init_zod();
158759
158861
  init_src2();
158760
158862
 
158761
- // ../tool-server/src/utils/react-profiler/session-ownership.ts
158762
- var DEFAULT_STALE_THRESHOLD_MS = 5 * 6e4;
158763
- function classifyStaleness({
158764
- owner,
158765
- nowEpochMs,
158766
- staleThresholdMs = DEFAULT_STALE_THRESHOLD_MS
158767
- }) {
158768
- if (!owner) {
158769
- return {
158770
- stale: false,
158771
- ageSeconds: null,
158772
- canReclaimWithoutForce: true
158773
- };
158774
- }
158775
- const ageMs = nowEpochMs - owner.startedAtEpochMs;
158776
- const heartbeatAgeMs = nowEpochMs - owner.lastHeartbeatEpochMs;
158777
- const stale = heartbeatAgeMs > staleThresholdMs;
158778
- return {
158779
- stale,
158780
- ageSeconds: Math.max(0, ageMs) / 1e3,
158781
- canReclaimWithoutForce: stale
158782
- };
158783
- }
158784
-
158785
158863
  // ../tool-server/src/utils/debugger/runtime-device.ts
158786
158864
  var import_node_crypto13 = require("node:crypto");
158787
158865
  var import_node_child_process43 = require("node:child_process");
@@ -158920,6 +158998,46 @@ function logicalDeviceWarning(deviceId, runtimeApp) {
158920
158998
  var NO_DEVTOOLS_HOOK_ERROR = "React DevTools hook (__REACT_DEVTOOLS_GLOBAL_HOOK__) is not present in this app's JavaScript runtime. React profiling requires a development build with React DevTools enabled. Likely causes: (1) the app is a release/production build \u2014 DevTools is stripped to reduce bundle size; (2) you connected to the wrong JS runtime; (3) this isn't a React (Native) app. Fix: rebuild in debug/dev mode (e.g. `npx react-native run-ios` without --configuration Release; for Expo, run a dev client). Once the app is running with DevTools attached, call react-profiler-start again.";
158921
158999
  var NO_RENDERERS_ATTACHED_ERROR = "React DevTools hook is present but no React renderer has registered yet. The hook is loaded but no fiber renderer has attached \u2014 typically because the app has not committed its first render, or the DevTools backend has not been bootstrapped on a bridgeless React Native dev build. Fix: ensure the app has rendered (interact with it once, then retry); if it stays empty, call react-profiler-start first \u2014 it will attempt to attach the DevTools backend automatically.";
158922
159000
  var UNKNOWN_DEVICE_NOTE = "Two or more devices share this Metro, and Argent could not tell which of them runs this app. So taps and swipes on that device still return a screenshot and an element tree, and the capture loads the app while the profiler records. To avoid this, run the app on its own Metro port.";
159001
+ function stopInProgress() {
159002
+ return { already_running: true, mine: true, note: STOP_IN_PROGRESS_NOTE };
159003
+ }
159004
+ function describeTakeover(input2) {
159005
+ const { owner, mine, ownRunWasActive, stale, heartbeatAgeSeconds } = input2;
159006
+ if (!owner) {
159007
+ return {
159008
+ session_id: null,
159009
+ reason: "unattributed",
159010
+ note: "Stopped a running profiling session that had no owner record (likely a DevTools client outside argent) and started this one."
159011
+ };
159012
+ }
159013
+ const id = owner.sessionId;
159014
+ if (mine) {
159015
+ return ownRunWasActive ? {
159016
+ session_id: id,
159017
+ reason: "forced",
159018
+ note: `Discarded this tool-server's own running recording (session ${id}) because force: true was passed.`
159019
+ } : {
159020
+ session_id: id,
159021
+ reason: "own_unsaved",
159022
+ note: `Stopped this tool-server's earlier run (session ${id}), which the app kept recording after it was lost; it was not saved.`
159023
+ };
159024
+ }
159025
+ return stale ? {
159026
+ session_id: id,
159027
+ reason: "stale",
159028
+ heartbeat_age_seconds: heartbeatAgeSeconds,
159029
+ note: `Took over session ${id} without force: its last heartbeat was ${heartbeatAgeSeconds} s ago (stale after ${DEFAULT_STALE_THRESHOLD_MS / 1e3} s), so its owner was presumed gone. Its recorded data was discarded.`
159030
+ } : {
159031
+ session_id: id,
159032
+ reason: "forced",
159033
+ heartbeat_age_seconds: heartbeatAgeSeconds,
159034
+ note: `Took over session ${id} because force: true was passed. Its recorded data was discarded.`
159035
+ };
159036
+ }
159037
+ function failedTakeoverText({ session_id, reason: reason2 }) {
159038
+ const run2 = session_id ? `session ${session_id}` : "a recording with no owner record";
159039
+ return `Before it failed, this start stopped ${run2} (reason: ${reason2}), and nothing is recording now.`;
159040
+ }
158923
159041
  function safeGetState(registry2, urn) {
158924
159042
  try {
158925
159043
  return registry2.getServiceState(urn);
@@ -158944,7 +159062,7 @@ function createReactProfilerStartTool(registry2) {
158944
159062
  Use when you need to profile a user interaction.
158945
159063
  On an iOS simulator or Android device, also call native-profiler-start in the same message with the same device_id, unless the user has said no to native profiling. profiler-combined-report then correlates the two.
158946
159064
  After starting, ask the user to perform the interaction to profile, then call react-profiler-stop.
158947
- Returns { started_at, startedAtEpochMs, hermes_version, detected_architecture }. On Android, a warning names the apps with native network capture on (started by native-network-logs): their timings in the profile can differ. If another tool-server already owns the session, returns { already_running: true, owner, stale, how_to_reclaim } instead, without clobbering their data. BEFORE OVERTAKING with { force: true }, ask the user for approval first, as how_to_reclaim explains.
159065
+ Returns { started_at, startedAtEpochMs, hermes_version, detected_architecture }. On Android, a warning names the apps with native network capture on (started by native-network-logs): their timings in the profile can differ. If a session is already running, returns { already_running: true, mine, owner, \u2026 } instead, without clobbering its data, unless it is another tool-server's stale run, has no owner record, or is a lost run of this tool-server: start replaces those. mine: true means this tool-server started the run: if it is yours, call react-profiler-stop. With stale: true it may be another agent's: follow the note. Otherwise, BEFORE OVERTAKING with { force: true }, ask the user for approval first, as how_to_reclaim explains. A start that replaces a run reports it in took_over.
158948
159066
  Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot be established.`,
158949
159067
  zodSchema: zodSchema45,
158950
159068
  interaction: {
@@ -158985,6 +159103,7 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
158985
159103
  await disposeAndWait();
158986
159104
  }
158987
159105
  let api = await registry2.resolveService(psUrn);
159106
+ if (api.stopping) return stopInProgress();
158988
159107
  if (!api.cdp.isConnected()) {
158989
159108
  await disposeAndWait();
158990
159109
  api = await registry2.resolveService(psUrn);
@@ -159071,6 +159190,13 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159071
159190
  );
159072
159191
  }
159073
159192
  }
159193
+ let tookOver;
159194
+ const withTakeover = (message) => tookOver ? `${message} ${failedTakeoverText(tookOver)}` : message;
159195
+ const rethrowWithTakeover = (err) => {
159196
+ if (err instanceof Error) err.message = withTakeover(err.message);
159197
+ throw err;
159198
+ };
159199
+ if (api.stopping) return stopInProgress();
159074
159200
  if (state3.isRunning) {
159075
159201
  const owner = state3.owner;
159076
159202
  const staleness = classifyStaleness({
@@ -159078,10 +159204,24 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159078
159204
  nowEpochMs: state3.nowEpochMs,
159079
159205
  staleThresholdMs: DEFAULT_STALE_THRESHOLD_MS
159080
159206
  });
159081
- const canTakeOverSilently = staleness.canReclaimWithoutForce || params.force === true;
159207
+ const lostRun = getLostProfilerRun(api.port, api.deviceId);
159208
+ const mine = owner !== null && (api.sessionId !== null && owner.sessionId === api.sessionId || lostRun !== void 0 && lostRun.sessionId !== null && owner.sessionId === lostRun.sessionId);
159209
+ if (mine && api.profilingActive && params.force !== true) {
159210
+ return {
159211
+ already_running: true,
159212
+ mine: true,
159213
+ owner,
159214
+ age_seconds: staleness.ageSeconds,
159215
+ stale: staleness.stale,
159216
+ heartbeat_age_seconds: staleness.heartbeatAgeSeconds,
159217
+ note: staleness.stale ? STALE_OWN_RUN_NOTE : "A recording started through this tool-server is still running: yours, unless another agent shares this tool-server. Call react-profiler-stop to collect it. To discard it and start over, call react-profiler-start again with { force: true }."
159218
+ };
159219
+ }
159220
+ const canTakeOverSilently = mine || staleness.canReclaimWithoutForce || params.force === true;
159082
159221
  if (!canTakeOverSilently) {
159083
159222
  return {
159084
159223
  already_running: true,
159224
+ mine: false,
159085
159225
  owner,
159086
159226
  age_seconds: staleness.ageSeconds,
159087
159227
  stale: staleness.stale,
@@ -159089,11 +159229,16 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159089
159229
  };
159090
159230
  }
159091
159231
  await cdp.evaluate(STOP_FOR_TAKEOVER_SCRIPT).catch(ignore);
159232
+ tookOver = describeTakeover({
159233
+ owner,
159234
+ mine,
159235
+ ownRunWasActive: mine && api.profilingActive,
159236
+ stale: staleness.stale,
159237
+ heartbeatAgeSeconds: staleness.heartbeatAgeSeconds
159238
+ });
159092
159239
  }
159093
- if (api.profilingActive) {
159094
- await cdp.send("Profiler.stop").catch(ignore);
159095
- api.profilingActive = false;
159096
- }
159240
+ await cdp.send("Profiler.stop").catch(ignore);
159241
+ api.profilingActive = false;
159097
159242
  const runtimeDevice = isLogicalKeyedDevice(api.deviceId) ? await findRuntimeDevice(cdp) : void 0;
159098
159243
  const sessionId = crypto6.randomUUID();
159099
159244
  const ownerPayload = {
@@ -159104,21 +159249,30 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159104
159249
  lastHeartbeatEpochMs: 0
159105
159250
  };
159106
159251
  await cdp.send("Profiler.enable").catch(ignore);
159107
- await cdp.send("Profiler.start", { interval: params.sample_interval_us });
159108
- const startJson = await cdp.evaluate(buildStartScript(JSON.stringify(ownerPayload)));
159252
+ await cdp.send("Profiler.start", { interval: params.sample_interval_us }).catch(rethrowWithTakeover);
159253
+ const startJson = await cdp.evaluate(buildStartScript(JSON.stringify(ownerPayload))).catch(async (err) => {
159254
+ await cdp.send("Profiler.stop").catch(ignore);
159255
+ return rethrowWithTakeover(err);
159256
+ });
159109
159257
  if (!startJson) {
159110
- throw new FailureError("Failed to start React profiler (no value returned from runtime).", {
159111
- error_code: FAILURE_CODES.REACT_PROFILER_NO_RUNTIME_DATA,
159112
- failure_stage: "react_profiler_start_runtime_start",
159113
- failure_area: "tool_server",
159114
- error_kind: "subprocess"
159115
- });
159258
+ await cdp.send("Profiler.stop").catch(ignore);
159259
+ throw new FailureError(
159260
+ withTakeover("Failed to start React profiler (no value returned from runtime)."),
159261
+ {
159262
+ error_code: FAILURE_CODES.REACT_PROFILER_NO_RUNTIME_DATA,
159263
+ failure_stage: "react_profiler_start_runtime_start",
159264
+ failure_area: "tool_server",
159265
+ error_kind: "subprocess"
159266
+ }
159267
+ );
159116
159268
  }
159117
159269
  const startResult = JSON.parse(startJson);
159118
159270
  if (!startResult.ok) {
159119
159271
  await cdp.send("Profiler.stop").catch(ignore);
159120
159272
  throw new FailureError(
159121
- `React profiler failed to start (${startResult.reason ?? "unknown"}${startResult.message ? `: ${startResult.message}` : ""})`,
159273
+ withTakeover(
159274
+ `React profiler failed to start (${startResult.reason ?? "unknown"}${startResult.message ? `: ${startResult.message}` : ""})`
159275
+ ),
159122
159276
  {
159123
159277
  error_code: FAILURE_CODES.REACT_PROFILER_START_FAILED,
159124
159278
  failure_stage: "react_profiler_start_runtime_start",
@@ -159130,7 +159284,9 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159130
159284
  if (startResult.isProfilingFlagSet !== true || startResult.ownerInstalled !== true) {
159131
159285
  await cdp.send("Profiler.stop").catch(ignore);
159132
159286
  throw new FailureError(
159133
- `React profiler failed to start (post-start verification failed: isProfilingFlagSet=${startResult.isProfilingFlagSet === true}, ownerInstalled=${startResult.ownerInstalled === true})`,
159287
+ withTakeover(
159288
+ `React profiler failed to start (post-start verification failed: isProfilingFlagSet=${startResult.isProfilingFlagSet === true}, ownerInstalled=${startResult.ownerInstalled === true})`
159289
+ ),
159134
159290
  {
159135
159291
  error_code: FAILURE_CODES.REACT_PROFILER_START_VERIFY_FAILED,
159136
159292
  failure_stage: "react_profiler_start_verify",
@@ -159143,7 +159299,9 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159143
159299
  await cdp.evaluate(STOP_FOR_TAKEOVER_SCRIPT).catch(ignore);
159144
159300
  await cdp.send("Profiler.stop").catch(ignore);
159145
159301
  throw new FailureError(
159146
- `The React profiler session for ${api.deviceId} was torn down while the profiler was starting, so nothing is recorded. One tool-server serves every agent using this argent install, so another agent may have run stop-all-simulator-servers. Call react-profiler-start again.`,
159302
+ withTakeover(
159303
+ `The React profiler session for ${api.deviceId} was torn down while the profiler was starting, so nothing is recorded. One tool-server serves every agent using this argent install, so another agent may have run stop-all-simulator-servers. Call react-profiler-start again.`
159304
+ ),
159147
159305
  {
159148
159306
  error_code: FAILURE_CODES.REACT_PROFILER_SESSION_TORN_DOWN,
159149
159307
  failure_stage: "react_profiler_start_session_torn_down",
@@ -159154,6 +159312,7 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159154
159312
  }
159155
159313
  const startedAtEpochMs = startResult.startedAtEpochMs ?? Date.now();
159156
159314
  clearCachedProfilerPaths(api.port, api.deviceId);
159315
+ clearLostProfilerRun(api.port, api.deviceId);
159157
159316
  api.sessionPaths = null;
159158
159317
  api.profilingActive = true;
159159
159318
  api.anyCompilerOptimized = null;
@@ -159170,6 +159329,7 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159170
159329
  startedAtEpochMs,
159171
159330
  hermes_version: api.hermesVersion,
159172
159331
  detected_architecture: api.detectedArchitecture,
159332
+ ...tookOver ? { took_over: tookOver } : {},
159173
159333
  ...warning ? { warning } : {},
159174
159334
  ...isLogicalKeyedDevice(api.deviceId) && !runtimeDevice ? { [RESULT_NOTE_KEY]: UNKNOWN_DEVICE_NOTE } : {}
159175
159335
  };
@@ -159240,6 +159400,55 @@ async function readCommitTree(path59) {
159240
159400
  return JSON.parse(json2);
159241
159401
  }
159242
159402
 
159403
+ // ../tool-server/src/utils/react-profiler/last-run.ts
159404
+ function savedSessionInfo(paths) {
159405
+ return {
159406
+ session_id: paths.sessionId,
159407
+ commits_path: paths.commitsPath,
159408
+ cpu_profile_path: paths.cpuProfilePath,
159409
+ ...paths.loadedFromDisk ? { loaded_from_disk: true } : {}
159410
+ };
159411
+ }
159412
+ function lostRunInfo(run2) {
159413
+ return {
159414
+ session_id: run2.sessionId,
159415
+ cause: run2.cause,
159416
+ lost_at: new Date(run2.lostAtEpochMs).toISOString(),
159417
+ ...run2.takenOverBy !== void 0 ? { taken_over_by: run2.takenOverBy } : {}
159418
+ };
159419
+ }
159420
+ function lostRunCauseText(cause, takenOverBy) {
159421
+ switch (cause) {
159422
+ case "js_reload":
159423
+ return "a JS reload (Metro's `r`, a red-box Reload, debugger-reload-metro) ended it";
159424
+ case "cdp_disconnected":
159425
+ return "the debugger connection dropped while it was recording: the app crashed or was killed, Metro restarted, or another debugger took the connection";
159426
+ case "session_torn_down":
159427
+ return "its session was torn down mid-run, typically by stop-all-simulator-servers: the run rides on the device's JS-runtime debugger, which that teardown reaps, and one tool-server serves every agent using this argent install";
159428
+ case "taken_over":
159429
+ return takenOverBy ? `session ${takenOverBy} took it over, which discarded its data` : "another session took it over, which discarded its data";
159430
+ case "stop_failed":
159431
+ return "a react-profiler-stop, or a react-profiler-start that replaced it, failed partway (see the error it returned)";
159432
+ case "ended_in_app":
159433
+ return "it stopped recording in the app without a react-profiler-stop, most likely from outside argent";
159434
+ }
159435
+ }
159436
+ function savedSessionText(paths) {
159437
+ if (paths.loadedFromDisk) {
159438
+ return `Session ${paths.sessionId} is loaded from disk: react-profiler-analyze and the profiler query tools read it.`;
159439
+ }
159440
+ return `Your last run was stopped and saved as session ${paths.sessionId}: react-profiler-analyze and the profiler query tools read it. After a tool-server restart, load it again with profiler-load { mode: "load_react", session_id: "${paths.sessionId}", device_id: <this run's device_id> }. Pass port too if this run passed one.`;
159441
+ }
159442
+ function lostRunText(lost) {
159443
+ const which = lost.sessionId ? `Your last run (session ${lost.sessionId})` : "Your last run";
159444
+ return `${which} was not saved: ${lostRunCauseText(lost.cause, lost.takenOverBy)}.`;
159445
+ }
159446
+ function lastRunText(saved, lost) {
159447
+ if (lost) return lostRunText(lost);
159448
+ if (saved) return savedSessionText(saved);
159449
+ return null;
159450
+ }
159451
+
159243
159452
  // ../tool-server/src/tools/profiler/react/react-profiler-stop.ts
159244
159453
  function normalizeChangeDescription(raw) {
159245
159454
  if (!raw || typeof raw !== "object") return null;
@@ -159309,6 +159518,15 @@ function flattenProfilingData(merged, displayNameById, fiberMeta) {
159309
159518
  }
159310
159519
  return { commits, totalCommits: flatCommitIndex, unattributedByCommit };
159311
159520
  }
159521
+ var COLLECT_TIMEOUT_MS = 6e4;
159522
+ function stopInProgressError() {
159523
+ return new FailureError(STOP_IN_PROGRESS_NOTE, {
159524
+ error_code: FAILURE_CODES.REACT_PROFILER_STOP_IN_PROGRESS,
159525
+ failure_stage: "react_profiler_stop_in_progress",
159526
+ failure_area: "tool_server",
159527
+ error_kind: "not_found"
159528
+ });
159529
+ }
159312
159530
  var zodSchema46 = external_exports.object({
159313
159531
  port: metroPortField,
159314
159532
  device_id: external_exports.string().describe(
@@ -159323,8 +159541,11 @@ Stores the results for react-profiler-analyze or react-profiler-cpu-summary.
159323
159541
  Call react-profiler-start first, then exercise the app, then call this.
159324
159542
  Returns { duration_ms, sample_count, fiber_renders_captured, total_react_commits, hot_commit_indices }.
159325
159543
  If some fibers' display names could not be resolved (typically modals, tooltips or animations that unmounted before stop), it also returns { unattributed_ms, unattributed_fiber_count, unattributed_commit_count }: the work missing from the per-component breakdown. Per-commit durations stay correct.
159326
- Fails if no active profiling session exists or the CDP connection was lost during recording.`,
159544
+ Fails if no active profiling session exists, another stop is running, the CDP connection was lost during recording, or another session took the run over.`,
159327
159545
  zodSchema: zodSchema46,
159546
+ // Collection can outlast the MCP client's 30 s abort. The client would then
159547
+ // resend the call, and the resend fails on the stop still collecting.
159548
+ longRunning: true,
159328
159549
  interaction: {
159329
159550
  startedMsg: () => "Stopping React profiler",
159330
159551
  completedMsg: () => "Stopped React profiler",
@@ -159333,12 +159554,18 @@ Fails if no active profiling session exists or the CDP connection was lost durin
159333
159554
  capability: RN_ONLY_TOOL_CAPABILITY,
159334
159555
  services: () => ({}),
159335
159556
  async execute(_services, params) {
159336
- const psUrn = `${REACT_PROFILER_SESSION_NAMESPACE}:${metroPort(params)}:${canonicalDeviceId(params.device_id)}`;
159557
+ const port = metroPort(params);
159558
+ const deviceId = canonicalDeviceId(params.device_id) ?? params.device_id;
159559
+ const psUrn = `${REACT_PROFILER_SESSION_NAMESPACE}:${port}:${deviceId}`;
159337
159560
  const snapshot = registry2.getSnapshot();
159338
159561
  const entry = snapshot.services.get(psUrn);
159562
+ const lastRun = lastRunText(
159563
+ getCachedProfilerPaths(port, deviceId),
159564
+ getLostProfilerRun(port, deviceId)
159565
+ );
159339
159566
  if (!entry || entry.state !== "RUNNING" /* RUNNING */) {
159340
159567
  throw new FailureError(
159341
- "No active profiling session. The session may have been lost to a Metro reload, or torn down by a stop-all-simulator-servers \u2014 this session rides on the device's JS-runtime debugger, which that teardown reaps, and one tool-server serves every agent using this argent install. Call react-profiler-start to begin a new session.",
159568
+ lastRun ? `No active profiling session. ${lastRun} Call react-profiler-start to begin a new session.` : "No active profiling session. The session may have been lost to a Metro reload, or torn down by a stop-all-simulator-servers \u2014 this session rides on the device's JS-runtime debugger, which that teardown reaps, and one tool-server serves every agent using this argent install. Call react-profiler-start to begin a new session.",
159342
159569
  {
159343
159570
  error_code: FAILURE_CODES.REACT_PROFILER_NO_ACTIVE_SESSION,
159344
159571
  failure_stage: "react_profiler_stop_session_lookup",
@@ -159349,6 +159576,7 @@ Fails if no active profiling session exists or the CDP connection was lost durin
159349
159576
  }
159350
159577
  const api = await registry2.resolveService(psUrn);
159351
159578
  const cdp = api.cdp;
159579
+ if (api.stopping) throw stopInProgressError();
159352
159580
  if (!api.cdp.isConnected()) {
159353
159581
  api.profilingActive = false;
159354
159582
  throw new FailureError(
@@ -159362,6 +159590,17 @@ Fails if no active profiling session exists or the CDP connection was lost durin
159362
159590
  );
159363
159591
  }
159364
159592
  if (!api.profilingActive) {
159593
+ if (lastRun) {
159594
+ throw new FailureError(
159595
+ `No active profiling run to stop. ${lastRun} Call react-profiler-start to record a new one.`,
159596
+ {
159597
+ error_code: FAILURE_CODES.REACT_PROFILER_NO_ACTIVE_SESSION,
159598
+ failure_stage: "react_profiler_stop_inactive",
159599
+ failure_area: "tool_server",
159600
+ error_kind: "not_found"
159601
+ }
159602
+ );
159603
+ }
159365
159604
  throw new FailureError(
159366
159605
  "No active profiling run to stop. Either the run ended without its data (a JS reload ends it: Metro's `r`, a red-box Reload, debugger-reload-metro), or Hermes CPU sampling was never started \u2014 typically because react-profiler-start failed before reaching the sampler (often on release builds without React DevTools). After a reload, call react-profiler-start again. Otherwise check the error react-profiler-start returned, address the underlying cause (rebuild in dev mode, reconnect the debugger, etc.), then call react-profiler-start again.",
159367
159606
  {
@@ -159374,180 +159613,210 @@ Fails if no active profiling session exists or the CDP connection was lost durin
159374
159613
  }
159375
159614
  );
159376
159615
  }
159377
- api.profilingActive = false;
159378
- const cpuResult = await cdp.send("Profiler.stop");
159379
- if (!cpuResult?.profile) {
159616
+ const takeover = await readTakeover(cdp, api);
159617
+ if (takeover) {
159618
+ recordLostProfilerRun(api, "taken_over", takeover.by);
159619
+ if (takeover.by === null) await cdp.send("Profiler.stop").catch(() => {
159620
+ });
159621
+ api.profilingActive = false;
159622
+ api.sessionId = null;
159623
+ api.ownerToolServerPid = null;
159624
+ const howTakeoversHappen = "A takeover happens when another agent or tool-server calls react-profiler-start with force, or once a run's heartbeat is 5 minutes old.";
159625
+ const replacedBy = takeover.by ? `session ${takeover.by}` : "a recording with no owner record (a DevTools client outside argent)";
159380
159626
  throw new FailureError(
159381
- "Hermes Profiler.stop returned no profile data. The CPU sampler may have been reset between start and stop (Metro reload, debugger disconnect). Call react-profiler-start to begin a fresh session.",
159627
+ takeover.stillRecording ? `This profiling run was taken over by ${replacedBy}, which is still recording. Its data was discarded at the takeover, so there is nothing to collect, and the other recording was left running. ${howTakeoversHappen} Ask the user before taking over with react-profiler-start { force: true }.` : `This profiling run was taken over by another session, which has since stopped. Its data was discarded at the takeover, so there is nothing to collect. ${howTakeoversHappen} Nothing is recording now: call react-profiler-start to record again.`,
159382
159628
  {
159383
- error_code: FAILURE_CODES.REACT_PROFILER_NO_CPU_PROFILE,
159384
- failure_stage: "react_profiler_stop_cpu_profile",
159629
+ error_code: FAILURE_CODES.REACT_PROFILER_SESSION_TAKEN_OVER,
159630
+ failure_stage: "react_profiler_stop_taken_over",
159385
159631
  failure_area: "tool_server",
159386
- error_kind: "unknown"
159632
+ error_kind: "not_found"
159387
159633
  }
159388
159634
  );
159389
159635
  }
159390
- const profile = cpuResult.profile;
159391
- if (!Array.isArray(profile.samples) || !Array.isArray(profile.nodes) || !Array.isArray(profile.timeDeltas)) {
159392
- throw new FailureError(
159393
- "Hermes Profiler.stop returned a malformed profile (missing samples/nodes/timeDeltas). This usually means CPU sampling was never actually started for this session \u2014 most often a release build without React DevTools, or a Metro reload between start and stop. Call react-profiler-start on a dev build and retry.",
159394
- {
159395
- error_code: FAILURE_CODES.REACT_PROFILER_NO_CPU_PROFILE,
159396
- failure_stage: "react_profiler_stop_malformed_profile",
159636
+ if (api.stopping) throw stopInProgressError();
159637
+ api.profilingActive = false;
159638
+ api.stopping = true;
159639
+ try {
159640
+ const cpuResult = await cdp.send("Profiler.stop", void 0, COLLECT_TIMEOUT_MS);
159641
+ if (!cpuResult?.profile) {
159642
+ throw new FailureError(
159643
+ "Hermes Profiler.stop returned no profile data. The CPU sampler may have been reset between start and stop (Metro reload, debugger disconnect). Call react-profiler-start to begin a fresh session.",
159644
+ {
159645
+ error_code: FAILURE_CODES.REACT_PROFILER_NO_CPU_PROFILE,
159646
+ failure_stage: "react_profiler_stop_cpu_profile",
159647
+ failure_area: "tool_server",
159648
+ error_kind: "unknown"
159649
+ }
159650
+ );
159651
+ }
159652
+ const profile = cpuResult.profile;
159653
+ if (!Array.isArray(profile.samples) || !Array.isArray(profile.nodes) || !Array.isArray(profile.timeDeltas)) {
159654
+ throw new FailureError(
159655
+ "Hermes Profiler.stop returned a malformed profile (missing samples/nodes/timeDeltas). This usually means CPU sampling was never actually started for this session \u2014 most often a release build without React DevTools, or a Metro reload between start and stop. Call react-profiler-start on a dev build and retry.",
159656
+ {
159657
+ error_code: FAILURE_CODES.REACT_PROFILER_NO_CPU_PROFILE,
159658
+ failure_stage: "react_profiler_stop_malformed_profile",
159659
+ failure_area: "tool_server",
159660
+ error_kind: "unknown"
159661
+ }
159662
+ );
159663
+ }
159664
+ const stopReadStr = await cdp.send(
159665
+ "Runtime.evaluate",
159666
+ { expression: STOP_AND_READ_SCRIPT, returnByValue: true },
159667
+ COLLECT_TIMEOUT_MS
159668
+ );
159669
+ if (stopReadStr.exceptionDetails) {
159670
+ throw new FailureError(
159671
+ `Runtime exception while reading profiling data: ${stopReadStr.exceptionDetails.text ?? "unknown"}`,
159672
+ {
159673
+ error_code: FAILURE_CODES.REACT_PROFILER_RUNTIME_EXCEPTION,
159674
+ failure_stage: "react_profiler_stop_runtime_read",
159675
+ failure_area: "tool_server",
159676
+ error_kind: "unknown"
159677
+ }
159678
+ );
159679
+ }
159680
+ const stopReadRaw = stopReadStr.result?.value;
159681
+ if (!stopReadRaw) {
159682
+ throw new FailureError("No profiling data returned from runtime.", {
159683
+ error_code: FAILURE_CODES.REACT_PROFILER_NO_RUNTIME_DATA,
159684
+ failure_stage: "react_profiler_stop_runtime_read",
159397
159685
  failure_area: "tool_server",
159398
159686
  error_kind: "unknown"
159687
+ });
159688
+ }
159689
+ const stopRead = JSON.parse(stopReadRaw);
159690
+ const merged = stopRead.live ?? { dataForRoots: [] };
159691
+ const backendCommitCount = merged.dataForRoots.reduce(
159692
+ (a, r) => a + (r.commitData?.length ?? 0),
159693
+ 0
159694
+ );
159695
+ let fiberMeta = {};
159696
+ if (backendCommitCount > 0) {
159697
+ try {
159698
+ const metaStr = await cdp.send(
159699
+ "Runtime.evaluate",
159700
+ { expression: RESOLVE_FIBER_META_SCRIPT, returnByValue: true },
159701
+ 15e3
159702
+ );
159703
+ if (metaStr.result?.value) {
159704
+ fiberMeta = JSON.parse(metaStr.result.value);
159705
+ }
159706
+ } catch {
159399
159707
  }
159708
+ }
159709
+ const {
159710
+ commits: allCommits,
159711
+ totalCommits,
159712
+ unattributedByCommit
159713
+ } = flattenProfilingData(merged, stopRead.displayNameById, fiberMeta);
159714
+ const commitHeat = /* @__PURE__ */ new Map();
159715
+ let anyCompilerOptimized = false;
159716
+ for (const c of allCommits) {
159717
+ const prev = commitHeat.get(c.commitIndex) ?? 0;
159718
+ if (c.commitDuration > prev) commitHeat.set(c.commitIndex, c.commitDuration);
159719
+ if (c.isCompilerOptimized) anyCompilerOptimized = true;
159720
+ }
159721
+ const ABSOLUTE_FLOOR_MS = 16;
159722
+ const interestingKeys = [...commitHeat.keys()].filter(
159723
+ (k) => (commitHeat.get(k) ?? 0) >= ABSOLUTE_FLOOR_MS
159400
159724
  );
159401
- }
159402
- const stopReadStr = await cdp.send("Runtime.evaluate", {
159403
- expression: STOP_AND_READ_SCRIPT,
159404
- returnByValue: true,
159405
- timeout: 6e4
159406
- });
159407
- if (stopReadStr.exceptionDetails) {
159408
- throw new FailureError(
159409
- `Runtime exception while reading profiling data: ${stopReadStr.exceptionDetails.text ?? "unknown"}`,
159725
+ const hotSet = new Set(interestingKeys);
159726
+ const marginSet = /* @__PURE__ */ new Set();
159727
+ for (const ci of interestingKeys) {
159728
+ if (commitHeat.has(ci - 1) && !hotSet.has(ci - 1)) marginSet.add(ci - 1);
159729
+ if (commitHeat.has(ci + 1) && !hotSet.has(ci + 1)) marginSet.add(ci + 1);
159730
+ }
159731
+ const keepSet = /* @__PURE__ */ new Set([...hotSet, ...marginSet]);
159732
+ const filteredCommits = interestingKeys.length === 0 ? [] : allCommits.filter((c) => keepSet.has(c.commitIndex));
159733
+ api.anyCompilerOptimized = anyCompilerOptimized;
159734
+ api.totalReactCommits = totalCommits;
159735
+ api.hotCommitIndices = interestingKeys;
159736
+ const commitTree = { commits: filteredCommits, hookNames: /* @__PURE__ */ new Map() };
159737
+ const sessionTs = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, (m) => m === "T" ? "-" : "").slice(0, 15);
159738
+ const debugDir = await getDebugDir();
159739
+ const cpuProfilePath = await writeDumpCompact(
159740
+ debugDir,
159741
+ `react-profiler-${sessionTs}_cpu.json`,
159742
+ profile
159743
+ );
159744
+ const commitsPath = await writeDumpCompact(
159745
+ debugDir,
159746
+ `react-profiler-${sessionTs}_commits.json`,
159410
159747
  {
159411
- error_code: FAILURE_CODES.REACT_PROFILER_RUNTIME_EXCEPTION,
159412
- failure_stage: "react_profiler_stop_runtime_read",
159413
- failure_area: "tool_server",
159414
- error_kind: "unknown"
159748
+ commits: commitTree.commits,
159749
+ meta: {
159750
+ detectedArchitecture: api.detectedArchitecture,
159751
+ anyCompilerOptimized: api.anyCompilerOptimized,
159752
+ hotCommitIndices: api.hotCommitIndices,
159753
+ totalReactCommits: api.totalReactCommits,
159754
+ profileStartWallMs: api.profileStartWallMs,
159755
+ projectRoot: api.projectRoot,
159756
+ deviceId: api.deviceId,
159757
+ port: api.port,
159758
+ appName: api.appName,
159759
+ deviceName: api.deviceName,
159760
+ unattributedByCommit: unattributedByCommit.length > 0 ? unattributedByCommit : null
159761
+ }
159415
159762
  }
159416
159763
  );
159417
- }
159418
- const stopReadRaw = stopReadStr.result?.value;
159419
- if (!stopReadRaw) {
159420
- throw new FailureError("No profiling data returned from runtime.", {
159421
- error_code: FAILURE_CODES.REACT_PROFILER_NO_RUNTIME_DATA,
159422
- failure_stage: "react_profiler_stop_runtime_read",
159423
- failure_area: "tool_server",
159424
- error_kind: "unknown"
159425
- });
159426
- }
159427
- const stopRead = JSON.parse(stopReadRaw);
159428
- const merged = stopRead.live ?? { dataForRoots: [] };
159429
- const backendCommitCount = merged.dataForRoots.reduce(
159430
- (a, r) => a + (r.commitData?.length ?? 0),
159431
- 0
159432
- );
159433
- let fiberMeta = {};
159434
- if (backendCommitCount > 0) {
159435
- try {
159436
- const metaStr = await cdp.send("Runtime.evaluate", {
159437
- expression: RESOLVE_FIBER_META_SCRIPT,
159438
- returnByValue: true,
159439
- timeout: 15e3
159440
- });
159441
- if (metaStr.result?.value) {
159442
- fiberMeta = JSON.parse(metaStr.result.value);
159764
+ const sessionPaths = {
159765
+ sessionId: sessionTs,
159766
+ debugDir,
159767
+ cpuProfilePath,
159768
+ commitsPath,
159769
+ cpuSampleIndexPath: null,
159770
+ detectedArchitecture: api.detectedArchitecture,
159771
+ anyCompilerOptimized: api.anyCompilerOptimized,
159772
+ hotCommitIndices: api.hotCommitIndices,
159773
+ totalReactCommits: api.totalReactCommits
159774
+ };
159775
+ cacheProfilerPaths(api.port, sessionPaths, api.deviceId);
159776
+ clearLostProfilerRun(api.port, api.deviceId);
159777
+ api.sessionPaths = sessionPaths;
159778
+ api.sessionId = null;
159779
+ api.ownerToolServerPid = null;
159780
+ api.disposeSession();
159781
+ const duration_ms = (profile.endTime - profile.startTime) / 1e3;
159782
+ const response = {
159783
+ duration_ms,
159784
+ sample_count: profile.samples.length,
159785
+ fiber_renders_captured: allCommits.length
159786
+ };
159787
+ if (totalCommits > 0) {
159788
+ response["total_react_commits"] = totalCommits;
159789
+ response["hot_commit_indices"] = api.hotCommitIndices ?? [];
159790
+ response["any_compiler_optimized"] = api.anyCompilerOptimized ?? false;
159791
+ response["fiber_renders_analyzed"] = filteredCommits.length;
159792
+ const hotCount = api.hotCommitIndices?.length ?? 0;
159793
+ if (hotCount === 0) {
159794
+ response["selection_note"] = "All commits below 16ms \u2014 app appears smooth (all-clear)";
159795
+ } else if (hotCount < totalCommits) {
159796
+ response["selection_note"] = `${hotCount} of ${totalCommits} commits at \u226516ms absolute floor`;
159797
+ }
159798
+ if (unattributedByCommit.length > 0) {
159799
+ let totalMs = 0;
159800
+ let totalFibers = 0;
159801
+ for (const [, count2, ms] of unattributedByCommit) {
159802
+ totalFibers += count2;
159803
+ totalMs += ms;
159804
+ }
159805
+ response["unattributed_ms"] = Math.round(totalMs * 100) / 100;
159806
+ response["unattributed_fiber_count"] = totalFibers;
159807
+ response["unattributed_commit_count"] = unattributedByCommit.length;
159808
+ if (stopRead.displayNameApiAvailable === false) {
159809
+ response["unattributed_reason"] = "react-devtools backend in this app does not expose a recognized display-name accessor (neither getDisplayNameForElementID nor getDisplayNameForFiberID). The component breakdown is unavailable for this profiling session; commit-level timing data remains correct.";
159810
+ }
159443
159811
  }
159444
- } catch {
159445
159812
  }
159813
+ return response;
159814
+ } catch (err) {
159815
+ recordLostProfilerRun(api, "stop_failed");
159816
+ throw err;
159817
+ } finally {
159818
+ api.stopping = false;
159446
159819
  }
159447
- const {
159448
- commits: allCommits,
159449
- totalCommits,
159450
- unattributedByCommit
159451
- } = flattenProfilingData(merged, stopRead.displayNameById, fiberMeta);
159452
- const commitHeat = /* @__PURE__ */ new Map();
159453
- let anyCompilerOptimized = false;
159454
- for (const c of allCommits) {
159455
- const prev = commitHeat.get(c.commitIndex) ?? 0;
159456
- if (c.commitDuration > prev) commitHeat.set(c.commitIndex, c.commitDuration);
159457
- if (c.isCompilerOptimized) anyCompilerOptimized = true;
159458
- }
159459
- const ABSOLUTE_FLOOR_MS = 16;
159460
- const interestingKeys = [...commitHeat.keys()].filter(
159461
- (k) => (commitHeat.get(k) ?? 0) >= ABSOLUTE_FLOOR_MS
159462
- );
159463
- const hotSet = new Set(interestingKeys);
159464
- const marginSet = /* @__PURE__ */ new Set();
159465
- for (const ci of interestingKeys) {
159466
- if (commitHeat.has(ci - 1) && !hotSet.has(ci - 1)) marginSet.add(ci - 1);
159467
- if (commitHeat.has(ci + 1) && !hotSet.has(ci + 1)) marginSet.add(ci + 1);
159468
- }
159469
- const keepSet = /* @__PURE__ */ new Set([...hotSet, ...marginSet]);
159470
- const filteredCommits = interestingKeys.length === 0 ? [] : allCommits.filter((c) => keepSet.has(c.commitIndex));
159471
- api.anyCompilerOptimized = anyCompilerOptimized;
159472
- api.totalReactCommits = totalCommits;
159473
- api.hotCommitIndices = interestingKeys;
159474
- const commitTree = { commits: filteredCommits, hookNames: /* @__PURE__ */ new Map() };
159475
- const sessionTs = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, (m) => m === "T" ? "-" : "").slice(0, 15);
159476
- const debugDir = await getDebugDir();
159477
- const cpuProfilePath = await writeDumpCompact(
159478
- debugDir,
159479
- `react-profiler-${sessionTs}_cpu.json`,
159480
- profile
159481
- );
159482
- const commitsPath = await writeDumpCompact(
159483
- debugDir,
159484
- `react-profiler-${sessionTs}_commits.json`,
159485
- {
159486
- commits: commitTree.commits,
159487
- meta: {
159488
- detectedArchitecture: api.detectedArchitecture,
159489
- anyCompilerOptimized: api.anyCompilerOptimized,
159490
- hotCommitIndices: api.hotCommitIndices,
159491
- totalReactCommits: api.totalReactCommits,
159492
- profileStartWallMs: api.profileStartWallMs,
159493
- projectRoot: api.projectRoot,
159494
- deviceId: api.deviceId,
159495
- port: api.port,
159496
- appName: api.appName,
159497
- deviceName: api.deviceName,
159498
- unattributedByCommit: unattributedByCommit.length > 0 ? unattributedByCommit : null
159499
- }
159500
- }
159501
- );
159502
- const sessionPaths = {
159503
- sessionId: sessionTs,
159504
- debugDir,
159505
- cpuProfilePath,
159506
- commitsPath,
159507
- cpuSampleIndexPath: null,
159508
- detectedArchitecture: api.detectedArchitecture,
159509
- anyCompilerOptimized: api.anyCompilerOptimized,
159510
- hotCommitIndices: api.hotCommitIndices,
159511
- totalReactCommits: api.totalReactCommits
159512
- };
159513
- cacheProfilerPaths(api.port, sessionPaths, api.deviceId);
159514
- api.sessionPaths = sessionPaths;
159515
- api.sessionId = null;
159516
- api.ownerToolServerPid = null;
159517
- api.disposeSession();
159518
- const duration_ms = (profile.endTime - profile.startTime) / 1e3;
159519
- const response = {
159520
- duration_ms,
159521
- sample_count: profile.samples.length,
159522
- fiber_renders_captured: allCommits.length
159523
- };
159524
- if (totalCommits > 0) {
159525
- response["total_react_commits"] = totalCommits;
159526
- response["hot_commit_indices"] = api.hotCommitIndices ?? [];
159527
- response["any_compiler_optimized"] = api.anyCompilerOptimized ?? false;
159528
- response["fiber_renders_analyzed"] = filteredCommits.length;
159529
- const hotCount = api.hotCommitIndices?.length ?? 0;
159530
- if (hotCount === 0) {
159531
- response["selection_note"] = "All commits below 16ms \u2014 app appears smooth (all-clear)";
159532
- } else if (hotCount < totalCommits) {
159533
- response["selection_note"] = `${hotCount} of ${totalCommits} commits at \u226516ms absolute floor`;
159534
- }
159535
- if (unattributedByCommit.length > 0) {
159536
- let totalMs = 0;
159537
- let totalFibers = 0;
159538
- for (const [, count2, ms] of unattributedByCommit) {
159539
- totalFibers += count2;
159540
- totalMs += ms;
159541
- }
159542
- response["unattributed_ms"] = Math.round(totalMs * 100) / 100;
159543
- response["unattributed_fiber_count"] = totalFibers;
159544
- response["unattributed_commit_count"] = unattributedByCommit.length;
159545
- if (stopRead.displayNameApiAvailable === false) {
159546
- response["unattributed_reason"] = "react-devtools backend in this app does not expose a recognized display-name accessor (neither getDisplayNameForElementID nor getDisplayNameForFiberID). The component breakdown is unavailable for this profiling session; commit-level timing data remains correct.";
159547
- }
159548
- }
159549
- }
159550
- return response;
159551
159820
  }
159552
159821
  };
159553
159822
  }
@@ -159563,7 +159832,7 @@ var zodSchema47 = external_exports.object({
159563
159832
  function createReactProfilerStatusTool(registry2) {
159564
159833
  return {
159565
159834
  id: "react-profiler-status",
159566
- description: `Check the state of the React profiler session without side effects. Use when resuming after an interruption (debugger disconnect, unexpected error, agent pause) to decide whether to continue with react-profiler-stop, start a new session, or reconnect the debugger. Returns { session_status, is_running, current_owner, note, \u2026 }: session_status is active, stopped, taken_over (e.g. this tool-server restarted after react-profiler-start) or no_react_runtime, and note says what to do next.`,
159835
+ description: `Check the state of the React profiler session. Use when resuming after an interruption (debugger disconnect, unexpected error, agent pause) to decide whether to continue with react-profiler-stop, start a new session, or reconnect the debugger. Returns { session_status, is_running, current_owner, note, \u2026 }: session_status is active, saved, lost, taken_over, not_started or no_react_runtime, and note says what to do next.`,
159567
159836
  zodSchema: zodSchema47,
159568
159837
  readOnly: true,
159569
159838
  interaction: {
@@ -159575,7 +159844,19 @@ function createReactProfilerStatusTool(registry2) {
159575
159844
  capability: RN_ONLY_TOOL_CAPABILITY,
159576
159845
  services: () => ({}),
159577
159846
  async execute(_services, params) {
159578
- const psUrn = `${REACT_PROFILER_SESSION_NAMESPACE}:${metroPort(params)}:${params.device_id}`;
159847
+ const port = metroPort(params);
159848
+ const deviceId = canonicalDeviceId(params.device_id) ?? params.device_id;
159849
+ const psUrn = `${REACT_PROFILER_SESSION_NAMESPACE}:${port}:${deviceId}`;
159850
+ const saved = getCachedProfilerPaths(port, deviceId);
159851
+ const lost = getLostProfilerRun(port, deviceId);
159852
+ const lastRun = {
159853
+ ...saved ? { saved_session: savedSessionInfo(saved) } : {},
159854
+ ...lost ? { lost_run: lostRunInfo(lost) } : {}
159855
+ };
159856
+ const withLastRun = (note) => {
159857
+ const text = lastRunText(saved, lost);
159858
+ return text ? `${note} ${text}` : note;
159859
+ };
159579
159860
  const noRuntime = (note) => ({
159580
159861
  hook_exists: false,
159581
159862
  renderer_interface_found: false,
@@ -159583,7 +159864,8 @@ function createReactProfilerStatusTool(registry2) {
159583
159864
  current_session_id: null,
159584
159865
  current_owner: null,
159585
159866
  session_status: "no_react_runtime",
159586
- note
159867
+ note: withLastRun(note),
159868
+ ...lastRun
159587
159869
  });
159588
159870
  let api;
159589
159871
  try {
@@ -159593,6 +159875,16 @@ function createReactProfilerStatusTool(registry2) {
159593
159875
  "Unable to resolve a React profiler session \u2014 the Hermes runtime may not be reachable. Reconnect the debugger and retry."
159594
159876
  );
159595
159877
  }
159878
+ const stopInProgress2 = () => ({
159879
+ hook_exists: true,
159880
+ renderer_interface_found: true,
159881
+ is_running: true,
159882
+ current_session_id: api.sessionId,
159883
+ current_owner: null,
159884
+ session_status: "active",
159885
+ note: STOP_IN_PROGRESS_NOTE
159886
+ });
159887
+ if (api.stopping) return stopInProgress2();
159596
159888
  const cdp = api.cdp;
159597
159889
  if (!cdp.isConnected()) {
159598
159890
  return noRuntime(
@@ -159617,61 +159909,119 @@ function createReactProfilerStatusTool(registry2) {
159617
159909
  );
159618
159910
  }
159619
159911
  const state3 = JSON.parse(stateJson);
159912
+ if (api.stopping) return stopInProgress2();
159620
159913
  if (!state3.hookExists) {
159621
- return {
159622
- hook_exists: false,
159623
- renderer_interface_found: false,
159624
- is_running: false,
159625
- current_session_id: null,
159626
- current_owner: null,
159627
- session_status: "no_react_runtime",
159628
- note: "React DevTools hook not attached \u2014 debugger may be disconnected or the app isn't a dev build."
159629
- };
159914
+ return noRuntime(
159915
+ "React DevTools hook not attached \u2014 debugger may be disconnected or the app isn't a dev build."
159916
+ );
159630
159917
  }
159631
159918
  if (!("rendererInterfaceFound" in state3) || !state3.rendererInterfaceFound) {
159632
159919
  return {
159633
- hook_exists: true,
159634
- renderer_interface_found: false,
159635
- is_running: false,
159636
- current_session_id: null,
159637
- current_owner: null,
159638
- session_status: "no_react_runtime",
159639
- note: "No React renderer attached yet \u2014 the app hasn't rendered its first commit."
159920
+ ...noRuntime(
159921
+ "No React renderer attached yet \u2014 the app hasn't rendered its first commit."
159922
+ ),
159923
+ hook_exists: true
159640
159924
  };
159641
159925
  }
159642
159926
  const owner = state3.owner;
159643
- const currentSessionId = owner?.sessionId ?? null;
159644
- if (!state3.isRunning) {
159927
+ const inApp = {
159928
+ hook_exists: true,
159929
+ renderer_interface_found: true,
159930
+ is_running: state3.isRunning,
159931
+ current_session_id: owner?.sessionId ?? null,
159932
+ current_owner: owner
159933
+ };
159934
+ const ownRunId = api.sessionId;
159935
+ const recordingHere = api.profilingActive && ownRunId !== null;
159936
+ const lostResponse = (sessionId, cause, stillRecording) => ({
159937
+ ...inApp,
159938
+ session_status: "lost",
159939
+ note: `Your run${sessionId ? ` (session ${sessionId})` : ""} was not saved: ${lostRunCauseText(cause)}. ` + (stillRecording ? "The app is still recording it, but react-profiler-stop can no longer collect it. Call react-profiler-start: it stops that recording and starts a new run, no force needed." : cause === "ended_in_app" ? "Call react-profiler-stop to collect it. A react-profiler-start would discard it." : "Call react-profiler-start to record again."),
159940
+ ...lastRun,
159941
+ lost_run: { session_id: sessionId, cause }
159942
+ });
159943
+ if (state3.isRunning) {
159944
+ if (owner && ownRunId !== null && owner.sessionId === ownRunId) {
159945
+ if (recordingHere) {
159946
+ const { stale, heartbeatAgeSeconds } = classifyStaleness({
159947
+ owner,
159948
+ nowEpochMs: state3.nowEpochMs
159949
+ });
159950
+ await cdp.evaluate(buildHeartbeatScript(ownRunId)).catch(() => {
159951
+ });
159952
+ return {
159953
+ ...inApp,
159954
+ session_status: "active",
159955
+ stale,
159956
+ heartbeat_age_seconds: heartbeatAgeSeconds,
159957
+ note: stale ? STALE_OWN_RUN_NOTE : "A profiling session started through this tool-server is still running: yours, unless another agent shares this tool-server. Call react-profiler-stop to collect the data, or continue profiling."
159958
+ };
159959
+ }
159960
+ return lostResponse(ownRunId, "stop_failed", true);
159961
+ }
159962
+ if (owner && lost && lost.sessionId === owner.sessionId) {
159963
+ return {
159964
+ ...inApp,
159965
+ session_status: "lost",
159966
+ note: `Your run (session ${owner.sessionId}) was not saved: ${lostRunCauseText(lost.cause, lost.takenOverBy)}. The app is still recording it, but react-profiler-stop can no longer collect it. Call react-profiler-start: it stops that recording and starts a new run, no force needed.`,
159967
+ ...lastRun
159968
+ };
159969
+ }
159970
+ const yoursReplaced = recordingHere || lost?.cause === "taken_over";
159971
+ const yourRunId = ownRunId ?? lost?.sessionId ?? null;
159972
+ if (!owner) {
159973
+ return {
159974
+ ...inApp,
159975
+ session_status: "taken_over",
159976
+ note: withLastRun(
159977
+ (yoursReplaced ? "Your run was replaced: a profiling session is running in the app, but its ownership is unattributable" : "A profiling session is running but its ownership is unattributable") + " \u2014 likely started by a foreign DevTools client without our setup script, or cleared in a narrow window. Treat as lost and restart."
159978
+ ),
159979
+ ...lastRun
159980
+ };
159981
+ }
159645
159982
  return {
159646
- hook_exists: true,
159647
- renderer_interface_found: true,
159648
- is_running: false,
159649
- current_session_id: currentSessionId,
159650
- current_owner: owner,
159651
- session_status: "stopped",
159652
- note: "No profiling session is currently running. If you expected to be profiling, the session ended \u2014 call react-profiler-start to begin a new one; prior data is not recoverable."
159983
+ ...inApp,
159984
+ session_status: "taken_over",
159985
+ note: yoursReplaced ? `Your run${yourRunId ? ` (session ${yourRunId})` : ""} was taken over by session ${owner.sessionId}, which is still recording. Your data was discarded at the takeover. Do not stop their run; ask the user before taking over with react-profiler-start { force: true }.` : withLastRun(
159986
+ `Another profiling session (${owner.sessionId}) is running: another tool-server instance took over, or this tool-server restarted after react-profiler-start. Data from a run it replaced is lost at the takeover moment. Use react-profiler-start { force: true } to reclaim, after asking the user if the run may be someone else's.`
159987
+ ),
159988
+ ...lastRun
159653
159989
  };
159654
159990
  }
159655
- if (!owner) {
159991
+ if (recordingHere) {
159992
+ if (takeoverOf(state3, api)) {
159993
+ return {
159994
+ ...inApp,
159995
+ session_status: "taken_over",
159996
+ note: `Your run (session ${ownRunId}) was not saved: ${lostRunCauseText("taken_over")}. That session has since stopped too. Nothing is recording now; call react-profiler-start to record again.`,
159997
+ ...lastRun,
159998
+ lost_run: { session_id: ownRunId, cause: "taken_over" }
159999
+ };
160000
+ }
160001
+ return lostResponse(ownRunId, "ended_in_app", false);
160002
+ }
160003
+ if (ownRunId !== null) return lostResponse(ownRunId, "stop_failed", false);
160004
+ if (lost) {
160005
+ const takenOver = lost.cause === "taken_over";
159656
160006
  return {
159657
- hook_exists: true,
159658
- renderer_interface_found: true,
159659
- is_running: true,
159660
- current_session_id: null,
159661
- current_owner: null,
159662
- session_status: "taken_over",
159663
- note: "A profiling session is running but its ownership is unattributable \u2014 likely started by a foreign DevTools client without our setup script, or cleared in a narrow window. Treat as lost and restart."
160007
+ ...inApp,
160008
+ session_status: takenOver ? "taken_over" : "lost",
160009
+ note: lostRunText(lost) + (takenOver ? " That session has since stopped too. " : " ") + "Nothing is recording now; call react-profiler-start to record again.",
160010
+ ...lastRun
160011
+ };
160012
+ }
160013
+ if (saved) {
160014
+ return {
160015
+ ...inApp,
160016
+ session_status: "saved",
160017
+ note: `No profiling session is running. ${savedSessionText(saved)} A new react-profiler-start replaces it.`,
160018
+ ...lastRun
159664
160019
  };
159665
160020
  }
159666
- const isMine = api.sessionId !== null && owner.sessionId === api.sessionId;
159667
160021
  return {
159668
- hook_exists: true,
159669
- renderer_interface_found: true,
159670
- is_running: true,
159671
- current_session_id: owner.sessionId,
159672
- current_owner: owner,
159673
- session_status: isMine ? "active" : "taken_over",
159674
- note: isMine ? "Your profiling session is still running. Call react-profiler-stop to collect the data, or continue profiling." : "A different profiling session is running (another tool-server instance took over, this process restarted after start, or a stop-all-simulator-servers reaped this device's JS-runtime debugger and took this session down with it, leaving the in-app owner behind). Data from the prior session is lost at the takeover moment. Use react-profiler-start { force: true } to reclaim."
160022
+ ...inApp,
160023
+ session_status: "not_started",
160024
+ note: "No profiling session is running, and this tool-server has no run on record for this device: none was started since it launched. Call react-profiler-start to begin one."
159675
160025
  };
159676
160026
  }
159677
160027
  };
@@ -159718,13 +160068,6 @@ async function detectSessionContext(input2) {
159718
160068
  reactCompilerEnabled = true;
159719
160069
  }
159720
160070
  }
159721
- let strictModeEnabled = false;
159722
- const indexJs = await readFileSafe((0, import_path3.join)(projectRoot, "index.js"));
159723
- const indexTs = await readFileSafe((0, import_path3.join)(projectRoot, "index.ts"));
159724
- const indexSource = (indexJs ?? "") + (indexTs ?? "");
159725
- if (indexSource.includes("<StrictMode>") || indexSource.includes("StrictMode")) {
159726
- strictModeEnabled = true;
159727
- }
159728
160071
  let buildMode = "dev";
159729
160072
  if (input2.flamegraph) {
159730
160073
  for (const node2 of input2.flamegraph.nodes) {
@@ -159764,7 +160107,6 @@ async function detectSessionContext(input2) {
159764
160107
  }
159765
160108
  return {
159766
160109
  reactCompilerEnabled,
159767
- strictModeEnabled,
159768
160110
  buildMode,
159769
160111
  rnArchitecture,
159770
160112
  projectRoot,
@@ -160315,14 +160657,12 @@ function bestRootCause(votes, hookTypeNames) {
160315
160657
  }
160316
160658
  function enrich(input2) {
160317
160659
  const { sessionContext } = input2;
160318
- const strictMode = sessionContext.strictModeEnabled;
160319
160660
  const components = /* @__PURE__ */ new Map();
160320
160661
  for (const [name, acc] of input2.components) {
160321
160662
  const n = acc.n;
160322
160663
  if (n === 0) continue;
160323
160664
  const mean = acc.sum / n;
160324
- const totalRenderMs = n * mean;
160325
- const normalizedRenderCount = strictMode ? Math.ceil(n / 2) : n;
160665
+ const totalRenderMs = acc.sum;
160326
160666
  const dr = dominantReason(acc.reasonHistogram);
160327
160667
  const dp = dr === "parent" ? topParent(acc.parentFreq) : void 0;
160328
160668
  const isCompilerOptimized = acc.compilerOptimizedCount > n / 2;
@@ -160330,7 +160670,6 @@ function enrich(input2) {
160330
160670
  const enriched = {
160331
160671
  name,
160332
160672
  n,
160333
- normalizedRenderCount,
160334
160673
  mean,
160335
160674
  min: acc.min === Infinity ? 0 : acc.min,
160336
160675
  max: acc.max === -Infinity ? 0 : acc.max,
@@ -160398,7 +160737,7 @@ var ACTIONABLE_REASONS = /* @__PURE__ */ new Set([
160398
160737
  ]);
160399
160738
  var MAX_FINDINGS = 20;
160400
160739
  var PARETO_THRESHOLD_PCT = 5e-3;
160401
- var MIN_NORMALIZED_RENDERS = 3;
160740
+ var MIN_RENDERS = 3;
160402
160741
  var MIN_MAX_DURATION_MS = 30;
160403
160742
  function rank(input2) {
160404
160743
  const { sessionContext, recordingMs } = input2;
@@ -160407,7 +160746,7 @@ function rank(input2) {
160407
160746
  if (comp.isAnimated) continue;
160408
160747
  if (comp.isRecyclerChild) continue;
160409
160748
  if (!ACTIONABLE_REASONS.has(comp.dominantReason)) continue;
160410
- const passesRenderCount = comp.normalizedRenderCount >= MIN_NORMALIZED_RENDERS;
160749
+ const passesRenderCount = comp.n >= MIN_RENDERS;
160411
160750
  const passesMaxDuration = comp.max >= MIN_MAX_DURATION_MS;
160412
160751
  if (!passesRenderCount && !passesMaxDuration) continue;
160413
160752
  candidates.push({ name, comp });
@@ -160420,7 +160759,7 @@ function rank(input2) {
160420
160759
  const topChangedHookNames = comp.hookTypeNames !== void 0 ? comp.topChangedHooks.map((idx) => comp.hookTypeNames[idx] ?? `hook[${idx}]`) : [];
160421
160760
  const finding = {
160422
160761
  component: name,
160423
- renders: comp.normalizedRenderCount,
160762
+ renders: comp.n,
160424
160763
  totalMs: Math.round(comp.totalRenderMs * 10) / 10,
160425
160764
  avgMs: Math.round(comp.mean * 100) / 100,
160426
160765
  maxMs: Math.round(comp.max * 100) / 100,
@@ -160429,7 +160768,7 @@ function rank(input2) {
160429
160768
  topChangedHookNames
160430
160769
  };
160431
160770
  if (comp.isCompilerOptimized) finding.isCompilerOptimized = true;
160432
- if (sessionContext.reactCompilerEnabled && !comp.isCompilerOptimized && comp.normalizedRenderCount > 5) {
160771
+ if (sessionContext.reactCompilerEnabled && !comp.isCompilerOptimized && comp.n > 5) {
160433
160772
  finding.compilerBailoutSuspected = true;
160434
160773
  }
160435
160774
  if (comp.parentTrigger !== void 0) finding.parentTrigger = comp.parentTrigger;
@@ -161624,13 +161963,14 @@ Fails if the React DevTools hook is not present in the runtime or the app is not
161624
161963
  // RN-only: queries the React DevTools backend hook on the live runtime.
161625
161964
  capability: RN_ONLY_TOOL_CAPABILITY,
161626
161965
  services: (params) => ({
161627
- profilerSession: `${REACT_PROFILER_SESSION_NAMESPACE}:${metroPort(params)}:${params.device_id}`
161966
+ // Same key as react-profiler-start, so a forwarded logicalDeviceId reads its run.
161967
+ profilerSession: `${REACT_PROFILER_SESSION_NAMESPACE}:${metroPort(params)}:${canonicalDeviceId(params.device_id)}`
161628
161968
  }),
161629
161969
  async execute(services, params) {
161630
161970
  const api = services.profilerSession;
161631
161971
  const cdp = api.cdp;
161632
- if (api.profilingActive && api.ownerToolServerPid === process.pid) {
161633
- await cdp.evaluate(HEARTBEAT_SCRIPT).catch(() => {
161972
+ if (api.profilingActive && api.sessionId && api.ownerToolServerPid === process.pid) {
161973
+ await cdp.evaluate(buildHeartbeatScript(api.sessionId)).catch(() => {
161634
161974
  });
161635
161975
  }
161636
161976
  async function evalRenders() {
@@ -161801,13 +162141,14 @@ Fails if the React DevTools hook is not present or no fiber roots have been comm
161801
162141
  // RN-only: walks the fiber tree via the React DevTools backend hook.
161802
162142
  capability: RN_ONLY_TOOL_CAPABILITY,
161803
162143
  services: (params) => ({
161804
- profilerSession: `${REACT_PROFILER_SESSION_NAMESPACE}:${metroPort(params)}:${params.device_id}`
162144
+ // Same key as react-profiler-start, so a forwarded logicalDeviceId reads its run.
162145
+ profilerSession: `${REACT_PROFILER_SESSION_NAMESPACE}:${metroPort(params)}:${canonicalDeviceId(params.device_id)}`
161805
162146
  }),
161806
162147
  async execute(services, params) {
161807
162148
  const api = services.profilerSession;
161808
162149
  const cdp = api.cdp;
161809
- if (api.profilingActive && api.ownerToolServerPid === process.pid) {
161810
- await cdp.evaluate(HEARTBEAT_SCRIPT).catch(() => {
162150
+ if (api.profilingActive && api.sessionId && api.ownerToolServerPid === process.pid) {
162151
+ await cdp.evaluate(buildHeartbeatScript(api.sessionId)).catch(() => {
161811
162152
  });
161812
162153
  }
161813
162154
  const script = buildFiberTreeScript(params.max_depth, params.filter ?? "");
@@ -169041,7 +169382,8 @@ async function loadReactSession(debugDir, sessionId, port, deviceId) {
169041
169382
  detectedArchitecture,
169042
169383
  anyCompilerOptimized,
169043
169384
  hotCommitIndices,
169044
- totalReactCommits
169385
+ totalReactCommits,
169386
+ loadedFromDisk: true
169045
169387
  };
169046
169388
  cacheProfilerPaths(port, sessionPaths, deviceId);
169047
169389
  const lines = [