@swmansion/argent 0.32.1-next.12 → 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.12";
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");
@@ -157339,10 +157441,11 @@ var VEGA_CONTENT_ROLES = /* @__PURE__ */ new Set([...CONTENT_ROLES, "button", "t
157339
157441
  function clampFinite(n) {
157340
157442
  return Number.isFinite(n) ? n : 0;
157341
157443
  }
157444
+ function fmtCoord(n) {
157445
+ return clampFinite(n).toFixed(3);
157446
+ }
157342
157447
  function fmtFrame(f) {
157343
- return `(${clampFinite(f.x).toFixed(3)}, ${clampFinite(f.y).toFixed(3)}, ${clampFinite(
157344
- f.width
157345
- ).toFixed(3)}, ${clampFinite(f.height).toFixed(3)})`;
157448
+ return `(${fmtCoord(f.x)},${fmtCoord(f.y)},${fmtCoord(f.width)},${fmtCoord(f.height)})`;
157346
157449
  }
157347
157450
  function escapeForLine(s) {
157348
157451
  return s.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
@@ -158757,30 +158860,6 @@ var crypto6 = __toESM(require("node:crypto"));
158757
158860
  init_zod();
158758
158861
  init_src2();
158759
158862
 
158760
- // ../tool-server/src/utils/react-profiler/session-ownership.ts
158761
- var DEFAULT_STALE_THRESHOLD_MS = 5 * 6e4;
158762
- function classifyStaleness({
158763
- owner,
158764
- nowEpochMs,
158765
- staleThresholdMs = DEFAULT_STALE_THRESHOLD_MS
158766
- }) {
158767
- if (!owner) {
158768
- return {
158769
- stale: false,
158770
- ageSeconds: null,
158771
- canReclaimWithoutForce: true
158772
- };
158773
- }
158774
- const ageMs = nowEpochMs - owner.startedAtEpochMs;
158775
- const heartbeatAgeMs = nowEpochMs - owner.lastHeartbeatEpochMs;
158776
- const stale = heartbeatAgeMs > staleThresholdMs;
158777
- return {
158778
- stale,
158779
- ageSeconds: Math.max(0, ageMs) / 1e3,
158780
- canReclaimWithoutForce: stale
158781
- };
158782
- }
158783
-
158784
158863
  // ../tool-server/src/utils/debugger/runtime-device.ts
158785
158864
  var import_node_crypto13 = require("node:crypto");
158786
158865
  var import_node_child_process43 = require("node:child_process");
@@ -158919,6 +158998,46 @@ function logicalDeviceWarning(deviceId, runtimeApp) {
158919
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.";
158920
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.";
158921
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
+ }
158922
159041
  function safeGetState(registry2, urn) {
158923
159042
  try {
158924
159043
  return registry2.getServiceState(urn);
@@ -158943,7 +159062,7 @@ function createReactProfilerStartTool(registry2) {
158943
159062
  Use when you need to profile a user interaction.
158944
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.
158945
159064
  After starting, ask the user to perform the interaction to profile, then call react-profiler-stop.
158946
- 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.
158947
159066
  Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot be established.`,
158948
159067
  zodSchema: zodSchema45,
158949
159068
  interaction: {
@@ -158984,6 +159103,7 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
158984
159103
  await disposeAndWait();
158985
159104
  }
158986
159105
  let api = await registry2.resolveService(psUrn);
159106
+ if (api.stopping) return stopInProgress();
158987
159107
  if (!api.cdp.isConnected()) {
158988
159108
  await disposeAndWait();
158989
159109
  api = await registry2.resolveService(psUrn);
@@ -159070,6 +159190,13 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159070
159190
  );
159071
159191
  }
159072
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();
159073
159200
  if (state3.isRunning) {
159074
159201
  const owner = state3.owner;
159075
159202
  const staleness = classifyStaleness({
@@ -159077,10 +159204,24 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159077
159204
  nowEpochMs: state3.nowEpochMs,
159078
159205
  staleThresholdMs: DEFAULT_STALE_THRESHOLD_MS
159079
159206
  });
159080
- 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;
159081
159221
  if (!canTakeOverSilently) {
159082
159222
  return {
159083
159223
  already_running: true,
159224
+ mine: false,
159084
159225
  owner,
159085
159226
  age_seconds: staleness.ageSeconds,
159086
159227
  stale: staleness.stale,
@@ -159088,11 +159229,16 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159088
159229
  };
159089
159230
  }
159090
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
+ });
159091
159239
  }
159092
- if (api.profilingActive) {
159093
- await cdp.send("Profiler.stop").catch(ignore);
159094
- api.profilingActive = false;
159095
- }
159240
+ await cdp.send("Profiler.stop").catch(ignore);
159241
+ api.profilingActive = false;
159096
159242
  const runtimeDevice = isLogicalKeyedDevice(api.deviceId) ? await findRuntimeDevice(cdp) : void 0;
159097
159243
  const sessionId = crypto6.randomUUID();
159098
159244
  const ownerPayload = {
@@ -159103,21 +159249,30 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159103
159249
  lastHeartbeatEpochMs: 0
159104
159250
  };
159105
159251
  await cdp.send("Profiler.enable").catch(ignore);
159106
- await cdp.send("Profiler.start", { interval: params.sample_interval_us });
159107
- 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
+ });
159108
159257
  if (!startJson) {
159109
- throw new FailureError("Failed to start React profiler (no value returned from runtime).", {
159110
- error_code: FAILURE_CODES.REACT_PROFILER_NO_RUNTIME_DATA,
159111
- failure_stage: "react_profiler_start_runtime_start",
159112
- failure_area: "tool_server",
159113
- error_kind: "subprocess"
159114
- });
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
+ );
159115
159268
  }
159116
159269
  const startResult = JSON.parse(startJson);
159117
159270
  if (!startResult.ok) {
159118
159271
  await cdp.send("Profiler.stop").catch(ignore);
159119
159272
  throw new FailureError(
159120
- `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
+ ),
159121
159276
  {
159122
159277
  error_code: FAILURE_CODES.REACT_PROFILER_START_FAILED,
159123
159278
  failure_stage: "react_profiler_start_runtime_start",
@@ -159129,7 +159284,9 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159129
159284
  if (startResult.isProfilingFlagSet !== true || startResult.ownerInstalled !== true) {
159130
159285
  await cdp.send("Profiler.stop").catch(ignore);
159131
159286
  throw new FailureError(
159132
- `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
+ ),
159133
159290
  {
159134
159291
  error_code: FAILURE_CODES.REACT_PROFILER_START_VERIFY_FAILED,
159135
159292
  failure_stage: "react_profiler_start_verify",
@@ -159142,7 +159299,9 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159142
159299
  await cdp.evaluate(STOP_FOR_TAKEOVER_SCRIPT).catch(ignore);
159143
159300
  await cdp.send("Profiler.stop").catch(ignore);
159144
159301
  throw new FailureError(
159145
- `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
+ ),
159146
159305
  {
159147
159306
  error_code: FAILURE_CODES.REACT_PROFILER_SESSION_TORN_DOWN,
159148
159307
  failure_stage: "react_profiler_start_session_torn_down",
@@ -159153,6 +159312,7 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159153
159312
  }
159154
159313
  const startedAtEpochMs = startResult.startedAtEpochMs ?? Date.now();
159155
159314
  clearCachedProfilerPaths(api.port, api.deviceId);
159315
+ clearLostProfilerRun(api.port, api.deviceId);
159156
159316
  api.sessionPaths = null;
159157
159317
  api.profilingActive = true;
159158
159318
  api.anyCompilerOptimized = null;
@@ -159169,6 +159329,7 @@ Fails if the Hermes runtime is not reachable or the Metro CDP connection cannot
159169
159329
  startedAtEpochMs,
159170
159330
  hermes_version: api.hermesVersion,
159171
159331
  detected_architecture: api.detectedArchitecture,
159332
+ ...tookOver ? { took_over: tookOver } : {},
159172
159333
  ...warning ? { warning } : {},
159173
159334
  ...isLogicalKeyedDevice(api.deviceId) && !runtimeDevice ? { [RESULT_NOTE_KEY]: UNKNOWN_DEVICE_NOTE } : {}
159174
159335
  };
@@ -159239,6 +159400,55 @@ async function readCommitTree(path59) {
159239
159400
  return JSON.parse(json2);
159240
159401
  }
159241
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
+
159242
159452
  // ../tool-server/src/tools/profiler/react/react-profiler-stop.ts
159243
159453
  function normalizeChangeDescription(raw) {
159244
159454
  if (!raw || typeof raw !== "object") return null;
@@ -159308,6 +159518,15 @@ function flattenProfilingData(merged, displayNameById, fiberMeta) {
159308
159518
  }
159309
159519
  return { commits, totalCommits: flatCommitIndex, unattributedByCommit };
159310
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
+ }
159311
159530
  var zodSchema46 = external_exports.object({
159312
159531
  port: metroPortField,
159313
159532
  device_id: external_exports.string().describe(
@@ -159322,8 +159541,11 @@ Stores the results for react-profiler-analyze or react-profiler-cpu-summary.
159322
159541
  Call react-profiler-start first, then exercise the app, then call this.
159323
159542
  Returns { duration_ms, sample_count, fiber_renders_captured, total_react_commits, hot_commit_indices }.
159324
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.
159325
- 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.`,
159326
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,
159327
159549
  interaction: {
159328
159550
  startedMsg: () => "Stopping React profiler",
159329
159551
  completedMsg: () => "Stopped React profiler",
@@ -159332,12 +159554,18 @@ Fails if no active profiling session exists or the CDP connection was lost durin
159332
159554
  capability: RN_ONLY_TOOL_CAPABILITY,
159333
159555
  services: () => ({}),
159334
159556
  async execute(_services, params) {
159335
- 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}`;
159336
159560
  const snapshot = registry2.getSnapshot();
159337
159561
  const entry = snapshot.services.get(psUrn);
159562
+ const lastRun = lastRunText(
159563
+ getCachedProfilerPaths(port, deviceId),
159564
+ getLostProfilerRun(port, deviceId)
159565
+ );
159338
159566
  if (!entry || entry.state !== "RUNNING" /* RUNNING */) {
159339
159567
  throw new FailureError(
159340
- "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.",
159341
159569
  {
159342
159570
  error_code: FAILURE_CODES.REACT_PROFILER_NO_ACTIVE_SESSION,
159343
159571
  failure_stage: "react_profiler_stop_session_lookup",
@@ -159348,6 +159576,7 @@ Fails if no active profiling session exists or the CDP connection was lost durin
159348
159576
  }
159349
159577
  const api = await registry2.resolveService(psUrn);
159350
159578
  const cdp = api.cdp;
159579
+ if (api.stopping) throw stopInProgressError();
159351
159580
  if (!api.cdp.isConnected()) {
159352
159581
  api.profilingActive = false;
159353
159582
  throw new FailureError(
@@ -159361,6 +159590,17 @@ Fails if no active profiling session exists or the CDP connection was lost durin
159361
159590
  );
159362
159591
  }
159363
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
+ }
159364
159604
  throw new FailureError(
159365
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.",
159366
159606
  {
@@ -159373,180 +159613,210 @@ Fails if no active profiling session exists or the CDP connection was lost durin
159373
159613
  }
159374
159614
  );
159375
159615
  }
159376
- api.profilingActive = false;
159377
- const cpuResult = await cdp.send("Profiler.stop");
159378
- 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)";
159379
159626
  throw new FailureError(
159380
- "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.`,
159381
159628
  {
159382
- error_code: FAILURE_CODES.REACT_PROFILER_NO_CPU_PROFILE,
159383
- 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",
159384
159631
  failure_area: "tool_server",
159385
- error_kind: "unknown"
159632
+ error_kind: "not_found"
159386
159633
  }
159387
159634
  );
159388
159635
  }
159389
- const profile = cpuResult.profile;
159390
- if (!Array.isArray(profile.samples) || !Array.isArray(profile.nodes) || !Array.isArray(profile.timeDeltas)) {
159391
- throw new FailureError(
159392
- "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.",
159393
- {
159394
- error_code: FAILURE_CODES.REACT_PROFILER_NO_CPU_PROFILE,
159395
- 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",
159396
159685
  failure_area: "tool_server",
159397
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 {
159398
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
159399
159724
  );
159400
- }
159401
- const stopReadStr = await cdp.send("Runtime.evaluate", {
159402
- expression: STOP_AND_READ_SCRIPT,
159403
- returnByValue: true,
159404
- timeout: 6e4
159405
- });
159406
- if (stopReadStr.exceptionDetails) {
159407
- throw new FailureError(
159408
- `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`,
159409
159747
  {
159410
- error_code: FAILURE_CODES.REACT_PROFILER_RUNTIME_EXCEPTION,
159411
- failure_stage: "react_profiler_stop_runtime_read",
159412
- failure_area: "tool_server",
159413
- 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
+ }
159414
159762
  }
159415
159763
  );
159416
- }
159417
- const stopReadRaw = stopReadStr.result?.value;
159418
- if (!stopReadRaw) {
159419
- throw new FailureError("No profiling data returned from runtime.", {
159420
- error_code: FAILURE_CODES.REACT_PROFILER_NO_RUNTIME_DATA,
159421
- failure_stage: "react_profiler_stop_runtime_read",
159422
- failure_area: "tool_server",
159423
- error_kind: "unknown"
159424
- });
159425
- }
159426
- const stopRead = JSON.parse(stopReadRaw);
159427
- const merged = stopRead.live ?? { dataForRoots: [] };
159428
- const backendCommitCount = merged.dataForRoots.reduce(
159429
- (a, r) => a + (r.commitData?.length ?? 0),
159430
- 0
159431
- );
159432
- let fiberMeta = {};
159433
- if (backendCommitCount > 0) {
159434
- try {
159435
- const metaStr = await cdp.send("Runtime.evaluate", {
159436
- expression: RESOLVE_FIBER_META_SCRIPT,
159437
- returnByValue: true,
159438
- timeout: 15e3
159439
- });
159440
- if (metaStr.result?.value) {
159441
- 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
+ }
159442
159811
  }
159443
- } catch {
159444
159812
  }
159813
+ return response;
159814
+ } catch (err) {
159815
+ recordLostProfilerRun(api, "stop_failed");
159816
+ throw err;
159817
+ } finally {
159818
+ api.stopping = false;
159445
159819
  }
159446
- const {
159447
- commits: allCommits,
159448
- totalCommits,
159449
- unattributedByCommit
159450
- } = flattenProfilingData(merged, stopRead.displayNameById, fiberMeta);
159451
- const commitHeat = /* @__PURE__ */ new Map();
159452
- let anyCompilerOptimized = false;
159453
- for (const c of allCommits) {
159454
- const prev = commitHeat.get(c.commitIndex) ?? 0;
159455
- if (c.commitDuration > prev) commitHeat.set(c.commitIndex, c.commitDuration);
159456
- if (c.isCompilerOptimized) anyCompilerOptimized = true;
159457
- }
159458
- const ABSOLUTE_FLOOR_MS = 16;
159459
- const interestingKeys = [...commitHeat.keys()].filter(
159460
- (k) => (commitHeat.get(k) ?? 0) >= ABSOLUTE_FLOOR_MS
159461
- );
159462
- const hotSet = new Set(interestingKeys);
159463
- const marginSet = /* @__PURE__ */ new Set();
159464
- for (const ci of interestingKeys) {
159465
- if (commitHeat.has(ci - 1) && !hotSet.has(ci - 1)) marginSet.add(ci - 1);
159466
- if (commitHeat.has(ci + 1) && !hotSet.has(ci + 1)) marginSet.add(ci + 1);
159467
- }
159468
- const keepSet = /* @__PURE__ */ new Set([...hotSet, ...marginSet]);
159469
- const filteredCommits = interestingKeys.length === 0 ? [] : allCommits.filter((c) => keepSet.has(c.commitIndex));
159470
- api.anyCompilerOptimized = anyCompilerOptimized;
159471
- api.totalReactCommits = totalCommits;
159472
- api.hotCommitIndices = interestingKeys;
159473
- const commitTree = { commits: filteredCommits, hookNames: /* @__PURE__ */ new Map() };
159474
- const sessionTs = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, (m) => m === "T" ? "-" : "").slice(0, 15);
159475
- const debugDir = await getDebugDir();
159476
- const cpuProfilePath = await writeDumpCompact(
159477
- debugDir,
159478
- `react-profiler-${sessionTs}_cpu.json`,
159479
- profile
159480
- );
159481
- const commitsPath = await writeDumpCompact(
159482
- debugDir,
159483
- `react-profiler-${sessionTs}_commits.json`,
159484
- {
159485
- commits: commitTree.commits,
159486
- meta: {
159487
- detectedArchitecture: api.detectedArchitecture,
159488
- anyCompilerOptimized: api.anyCompilerOptimized,
159489
- hotCommitIndices: api.hotCommitIndices,
159490
- totalReactCommits: api.totalReactCommits,
159491
- profileStartWallMs: api.profileStartWallMs,
159492
- projectRoot: api.projectRoot,
159493
- deviceId: api.deviceId,
159494
- port: api.port,
159495
- appName: api.appName,
159496
- deviceName: api.deviceName,
159497
- unattributedByCommit: unattributedByCommit.length > 0 ? unattributedByCommit : null
159498
- }
159499
- }
159500
- );
159501
- const sessionPaths = {
159502
- sessionId: sessionTs,
159503
- debugDir,
159504
- cpuProfilePath,
159505
- commitsPath,
159506
- cpuSampleIndexPath: null,
159507
- detectedArchitecture: api.detectedArchitecture,
159508
- anyCompilerOptimized: api.anyCompilerOptimized,
159509
- hotCommitIndices: api.hotCommitIndices,
159510
- totalReactCommits: api.totalReactCommits
159511
- };
159512
- cacheProfilerPaths(api.port, sessionPaths, api.deviceId);
159513
- api.sessionPaths = sessionPaths;
159514
- api.sessionId = null;
159515
- api.ownerToolServerPid = null;
159516
- api.disposeSession();
159517
- const duration_ms = (profile.endTime - profile.startTime) / 1e3;
159518
- const response = {
159519
- duration_ms,
159520
- sample_count: profile.samples.length,
159521
- fiber_renders_captured: allCommits.length
159522
- };
159523
- if (totalCommits > 0) {
159524
- response["total_react_commits"] = totalCommits;
159525
- response["hot_commit_indices"] = api.hotCommitIndices ?? [];
159526
- response["any_compiler_optimized"] = api.anyCompilerOptimized ?? false;
159527
- response["fiber_renders_analyzed"] = filteredCommits.length;
159528
- const hotCount = api.hotCommitIndices?.length ?? 0;
159529
- if (hotCount === 0) {
159530
- response["selection_note"] = "All commits below 16ms \u2014 app appears smooth (all-clear)";
159531
- } else if (hotCount < totalCommits) {
159532
- response["selection_note"] = `${hotCount} of ${totalCommits} commits at \u226516ms absolute floor`;
159533
- }
159534
- if (unattributedByCommit.length > 0) {
159535
- let totalMs = 0;
159536
- let totalFibers = 0;
159537
- for (const [, count2, ms] of unattributedByCommit) {
159538
- totalFibers += count2;
159539
- totalMs += ms;
159540
- }
159541
- response["unattributed_ms"] = Math.round(totalMs * 100) / 100;
159542
- response["unattributed_fiber_count"] = totalFibers;
159543
- response["unattributed_commit_count"] = unattributedByCommit.length;
159544
- if (stopRead.displayNameApiAvailable === false) {
159545
- 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.";
159546
- }
159547
- }
159548
- }
159549
- return response;
159550
159820
  }
159551
159821
  };
159552
159822
  }
@@ -159562,7 +159832,7 @@ var zodSchema47 = external_exports.object({
159562
159832
  function createReactProfilerStatusTool(registry2) {
159563
159833
  return {
159564
159834
  id: "react-profiler-status",
159565
- 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.`,
159566
159836
  zodSchema: zodSchema47,
159567
159837
  readOnly: true,
159568
159838
  interaction: {
@@ -159574,7 +159844,19 @@ function createReactProfilerStatusTool(registry2) {
159574
159844
  capability: RN_ONLY_TOOL_CAPABILITY,
159575
159845
  services: () => ({}),
159576
159846
  async execute(_services, params) {
159577
- 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
+ };
159578
159860
  const noRuntime = (note) => ({
159579
159861
  hook_exists: false,
159580
159862
  renderer_interface_found: false,
@@ -159582,7 +159864,8 @@ function createReactProfilerStatusTool(registry2) {
159582
159864
  current_session_id: null,
159583
159865
  current_owner: null,
159584
159866
  session_status: "no_react_runtime",
159585
- note
159867
+ note: withLastRun(note),
159868
+ ...lastRun
159586
159869
  });
159587
159870
  let api;
159588
159871
  try {
@@ -159592,6 +159875,16 @@ function createReactProfilerStatusTool(registry2) {
159592
159875
  "Unable to resolve a React profiler session \u2014 the Hermes runtime may not be reachable. Reconnect the debugger and retry."
159593
159876
  );
159594
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();
159595
159888
  const cdp = api.cdp;
159596
159889
  if (!cdp.isConnected()) {
159597
159890
  return noRuntime(
@@ -159616,61 +159909,119 @@ function createReactProfilerStatusTool(registry2) {
159616
159909
  );
159617
159910
  }
159618
159911
  const state3 = JSON.parse(stateJson);
159912
+ if (api.stopping) return stopInProgress2();
159619
159913
  if (!state3.hookExists) {
159620
- return {
159621
- hook_exists: false,
159622
- renderer_interface_found: false,
159623
- is_running: false,
159624
- current_session_id: null,
159625
- current_owner: null,
159626
- session_status: "no_react_runtime",
159627
- note: "React DevTools hook not attached \u2014 debugger may be disconnected or the app isn't a dev build."
159628
- };
159914
+ return noRuntime(
159915
+ "React DevTools hook not attached \u2014 debugger may be disconnected or the app isn't a dev build."
159916
+ );
159629
159917
  }
159630
159918
  if (!("rendererInterfaceFound" in state3) || !state3.rendererInterfaceFound) {
159631
159919
  return {
159632
- hook_exists: true,
159633
- renderer_interface_found: false,
159634
- is_running: false,
159635
- current_session_id: null,
159636
- current_owner: null,
159637
- session_status: "no_react_runtime",
159638
- 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
159639
159924
  };
159640
159925
  }
159641
159926
  const owner = state3.owner;
159642
- const currentSessionId = owner?.sessionId ?? null;
159643
- 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
+ }
159644
159982
  return {
159645
- hook_exists: true,
159646
- renderer_interface_found: true,
159647
- is_running: false,
159648
- current_session_id: currentSessionId,
159649
- current_owner: owner,
159650
- session_status: "stopped",
159651
- 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
159652
159989
  };
159653
159990
  }
159654
- 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";
159655
160006
  return {
159656
- hook_exists: true,
159657
- renderer_interface_found: true,
159658
- is_running: true,
159659
- current_session_id: null,
159660
- current_owner: null,
159661
- session_status: "taken_over",
159662
- 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
159663
160019
  };
159664
160020
  }
159665
- const isMine = api.sessionId !== null && owner.sessionId === api.sessionId;
159666
160021
  return {
159667
- hook_exists: true,
159668
- renderer_interface_found: true,
159669
- is_running: true,
159670
- current_session_id: owner.sessionId,
159671
- current_owner: owner,
159672
- session_status: isMine ? "active" : "taken_over",
159673
- 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."
159674
160025
  };
159675
160026
  }
159676
160027
  };
@@ -159717,13 +160068,6 @@ async function detectSessionContext(input2) {
159717
160068
  reactCompilerEnabled = true;
159718
160069
  }
159719
160070
  }
159720
- let strictModeEnabled = false;
159721
- const indexJs = await readFileSafe((0, import_path3.join)(projectRoot, "index.js"));
159722
- const indexTs = await readFileSafe((0, import_path3.join)(projectRoot, "index.ts"));
159723
- const indexSource = (indexJs ?? "") + (indexTs ?? "");
159724
- if (indexSource.includes("<StrictMode>") || indexSource.includes("StrictMode")) {
159725
- strictModeEnabled = true;
159726
- }
159727
160071
  let buildMode = "dev";
159728
160072
  if (input2.flamegraph) {
159729
160073
  for (const node2 of input2.flamegraph.nodes) {
@@ -159763,7 +160107,6 @@ async function detectSessionContext(input2) {
159763
160107
  }
159764
160108
  return {
159765
160109
  reactCompilerEnabled,
159766
- strictModeEnabled,
159767
160110
  buildMode,
159768
160111
  rnArchitecture,
159769
160112
  projectRoot,
@@ -160314,14 +160657,12 @@ function bestRootCause(votes, hookTypeNames) {
160314
160657
  }
160315
160658
  function enrich(input2) {
160316
160659
  const { sessionContext } = input2;
160317
- const strictMode = sessionContext.strictModeEnabled;
160318
160660
  const components = /* @__PURE__ */ new Map();
160319
160661
  for (const [name, acc] of input2.components) {
160320
160662
  const n = acc.n;
160321
160663
  if (n === 0) continue;
160322
160664
  const mean = acc.sum / n;
160323
- const totalRenderMs = n * mean;
160324
- const normalizedRenderCount = strictMode ? Math.ceil(n / 2) : n;
160665
+ const totalRenderMs = acc.sum;
160325
160666
  const dr = dominantReason(acc.reasonHistogram);
160326
160667
  const dp = dr === "parent" ? topParent(acc.parentFreq) : void 0;
160327
160668
  const isCompilerOptimized = acc.compilerOptimizedCount > n / 2;
@@ -160329,7 +160670,6 @@ function enrich(input2) {
160329
160670
  const enriched = {
160330
160671
  name,
160331
160672
  n,
160332
- normalizedRenderCount,
160333
160673
  mean,
160334
160674
  min: acc.min === Infinity ? 0 : acc.min,
160335
160675
  max: acc.max === -Infinity ? 0 : acc.max,
@@ -160397,7 +160737,7 @@ var ACTIONABLE_REASONS = /* @__PURE__ */ new Set([
160397
160737
  ]);
160398
160738
  var MAX_FINDINGS = 20;
160399
160739
  var PARETO_THRESHOLD_PCT = 5e-3;
160400
- var MIN_NORMALIZED_RENDERS = 3;
160740
+ var MIN_RENDERS = 3;
160401
160741
  var MIN_MAX_DURATION_MS = 30;
160402
160742
  function rank(input2) {
160403
160743
  const { sessionContext, recordingMs } = input2;
@@ -160406,7 +160746,7 @@ function rank(input2) {
160406
160746
  if (comp.isAnimated) continue;
160407
160747
  if (comp.isRecyclerChild) continue;
160408
160748
  if (!ACTIONABLE_REASONS.has(comp.dominantReason)) continue;
160409
- const passesRenderCount = comp.normalizedRenderCount >= MIN_NORMALIZED_RENDERS;
160749
+ const passesRenderCount = comp.n >= MIN_RENDERS;
160410
160750
  const passesMaxDuration = comp.max >= MIN_MAX_DURATION_MS;
160411
160751
  if (!passesRenderCount && !passesMaxDuration) continue;
160412
160752
  candidates.push({ name, comp });
@@ -160419,7 +160759,7 @@ function rank(input2) {
160419
160759
  const topChangedHookNames = comp.hookTypeNames !== void 0 ? comp.topChangedHooks.map((idx) => comp.hookTypeNames[idx] ?? `hook[${idx}]`) : [];
160420
160760
  const finding = {
160421
160761
  component: name,
160422
- renders: comp.normalizedRenderCount,
160762
+ renders: comp.n,
160423
160763
  totalMs: Math.round(comp.totalRenderMs * 10) / 10,
160424
160764
  avgMs: Math.round(comp.mean * 100) / 100,
160425
160765
  maxMs: Math.round(comp.max * 100) / 100,
@@ -160428,7 +160768,7 @@ function rank(input2) {
160428
160768
  topChangedHookNames
160429
160769
  };
160430
160770
  if (comp.isCompilerOptimized) finding.isCompilerOptimized = true;
160431
- if (sessionContext.reactCompilerEnabled && !comp.isCompilerOptimized && comp.normalizedRenderCount > 5) {
160771
+ if (sessionContext.reactCompilerEnabled && !comp.isCompilerOptimized && comp.n > 5) {
160432
160772
  finding.compilerBailoutSuspected = true;
160433
160773
  }
160434
160774
  if (comp.parentTrigger !== void 0) finding.parentTrigger = comp.parentTrigger;
@@ -161623,13 +161963,14 @@ Fails if the React DevTools hook is not present in the runtime or the app is not
161623
161963
  // RN-only: queries the React DevTools backend hook on the live runtime.
161624
161964
  capability: RN_ONLY_TOOL_CAPABILITY,
161625
161965
  services: (params) => ({
161626
- 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)}`
161627
161968
  }),
161628
161969
  async execute(services, params) {
161629
161970
  const api = services.profilerSession;
161630
161971
  const cdp = api.cdp;
161631
- if (api.profilingActive && api.ownerToolServerPid === process.pid) {
161632
- await cdp.evaluate(HEARTBEAT_SCRIPT).catch(() => {
161972
+ if (api.profilingActive && api.sessionId && api.ownerToolServerPid === process.pid) {
161973
+ await cdp.evaluate(buildHeartbeatScript(api.sessionId)).catch(() => {
161633
161974
  });
161634
161975
  }
161635
161976
  async function evalRenders() {
@@ -161800,13 +162141,14 @@ Fails if the React DevTools hook is not present or no fiber roots have been comm
161800
162141
  // RN-only: walks the fiber tree via the React DevTools backend hook.
161801
162142
  capability: RN_ONLY_TOOL_CAPABILITY,
161802
162143
  services: (params) => ({
161803
- 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)}`
161804
162146
  }),
161805
162147
  async execute(services, params) {
161806
162148
  const api = services.profilerSession;
161807
162149
  const cdp = api.cdp;
161808
- if (api.profilingActive && api.ownerToolServerPid === process.pid) {
161809
- await cdp.evaluate(HEARTBEAT_SCRIPT).catch(() => {
162150
+ if (api.profilingActive && api.sessionId && api.ownerToolServerPid === process.pid) {
162151
+ await cdp.evaluate(buildHeartbeatScript(api.sessionId)).catch(() => {
161810
162152
  });
161811
162153
  }
161812
162154
  const script = buildFiberTreeScript(params.max_depth, params.filter ?? "");
@@ -169040,7 +169382,8 @@ async function loadReactSession(debugDir, sessionId, port, deviceId) {
169040
169382
  detectedArchitecture,
169041
169383
  anyCompilerOptimized,
169042
169384
  hotCommitIndices,
169043
- totalReactCommits
169385
+ totalReactCommits,
169386
+ loadedFromDisk: true
169044
169387
  };
169045
169388
  cacheProfilerPaths(port, sessionPaths, deviceId);
169046
169389
  const lines = [