Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ import { loadAndroidCombinedData } from "../../../utils/android-profiler/pipelin
import { buildHotCommitSummaries } from "../../../utils/react-profiler/pipeline/00-hot-commits";
import { preprocess } from "../../../utils/react-profiler/pipeline/00-preprocess";
import { readCpuProfile, readCommitTree } from "../../../utils/react-profiler/debug/dump";
import { metroDeviceIdParam } from "../../../utils/debugger/device-id-param";

const zodSchema = z.object({
port: z.coerce.number().default(8081).describe("Metro server port"),
device_id: z.string().describe("iOS Simulator/device UDID or Android serial"),
device_id: metroDeviceIdParam("iOS Simulator/device UDID or Android serial"),
});

interface HangCommitCorrelation {
Expand Down Expand Up @@ -102,6 +103,29 @@ Fails if either react-profiler-analyze or native-profiler-analyze has not been c
// For iOS, the analyze step cached uiHangs + memoryLeaks in parsedData.
// For Android, drill-down re-queries the .pftrace, so we load the same
// shape on demand here.
// A session with no capture state at all was minted by THIS call: the
// device_id matched no existing session, so nothing is known about the
// device. Say so without naming a platform — classification is shape-based
// and falls back to "android" for any opaque id (utils/device-info.ts:52),
// so an id this tool cannot place would otherwise be reported as an Android
// device (#618). That happens routinely: a forwarded Metro logicalDeviceId
// resolves only while a debugger connection is live, and the alias is
// dropped when it disposes.
if (!nativeApi.traceFile && !nativeApi.exportedFiles && !nativeApi.parsedData) {
throw new FailureError(
`No native profiler capture is loaded for device \`${params.device_id}\`. Run ` +
"native-profiler-start → native-profiler-stop → native-profiler-analyze on this device " +
"first. (If that id came from debugger-connect, pass the id from list-devices instead — " +
"the simulator UDID or adb serial — since profiler sessions are keyed by that one.)",
{
error_code: FAILURE_CODES.PROFILER_DATA_NOT_LOADED,
failure_stage: "profiler_combined_report_load_native_data",
failure_area: "tool_server",
error_kind: "not_found",
}
);
}

let uiHangs: UiHang[];
let memoryLeaks: MemoryLeak[];
if (nativeApi.platform === "android") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import type { NativeProfilerAnalyzeResult } from "../../../utils/ios-profiler/ty
import { analyzeNativeProfilerIos } from "./platforms/ios";
import { analyzeNativeProfilerAndroid } from "./platforms/android";
import { requireArtifacts, type ArtifactHandle } from "../../../artifacts";
import { metroDeviceIdParam } from "../../../utils/debugger/device-id-param";

const zodSchema = z.object({
device_id: z
.string()
.describe("Target device id from `list-devices` (iOS UDID or Android serial)."),
device_id: metroDeviceIdParam(
"Target device id from `list-devices` (iOS UDID or Android serial)."
),
});

const capability = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import { assertSupported } from "../../../utils/capability";
import { ensureDeps } from "../../../utils/check-deps";
import { startNativeProfilerIos } from "./platforms/ios";
import { startNativeProfilerAndroid } from "./platforms/android";
import { metroDeviceIdParam } from "../../../utils/debugger/device-id-param";

const zodSchema = z.object({
device_id: z
.string()
.describe("Target device id from `list-devices` (iOS UDID or Android serial)."),
device_id: metroDeviceIdParam(
"Target device id from `list-devices` (iOS UDID or Android serial)."
),
app_process: z
.string()
.optional()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import { stopNativeProfilerAndroid, type AndroidStopResult } from "./platforms/a
import type { ExportDiagnostics } from "../../../utils/ios-profiler/export";
import { requireArtifacts, type ArtifactHandle } from "../../../artifacts";
import type { ArtifactStore } from "@argent/registry";
import { metroDeviceIdParam } from "../../../utils/debugger/device-id-param";

const zodSchema = z.object({
device_id: z
.string()
.describe("Target device id from `list-devices` (iOS UDID or Android serial)."),
device_id: metroDeviceIdParam(
"Target device id from `list-devices` (iOS UDID or Android serial)."
),
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
renderComponentNameMiss,
describeResolution,
} from "../../../utils/react-profiler/component-names";
import { metroDeviceIdParam } from "../../../utils/debugger/device-id-param";

const timeRangeSchema = z.object({
start: z.coerce.number().describe("Start of range in ms (performance.now clock)"),
Expand All @@ -21,11 +22,9 @@ const timeRangeSchema = z.object({

const zodSchema = z.object({
port: z.coerce.number().default(8081).describe("Metro server port"),
device_id: z
.string()
.describe(
"Device logicalDeviceId from debugger-connect (iOS simulator UDID or Android logicalDeviceId)."
),
device_id: metroDeviceIdParam(
"Device logicalDeviceId from debugger-connect (iOS simulator UDID or Android logicalDeviceId)."
),
mode: z
.enum(["by_component", "by_time_range", "by_index", "cascade_tree"])
.describe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
describeResolution,
} from "../../../utils/react-profiler/component-names";
import { promises as fs } from "fs";
import { metroDeviceIdParam } from "../../../utils/debugger/device-id-param";

const timeWindowSchema = z.object({
start: z.coerce.number().describe("Start of window in ms (performance.now clock)"),
Expand All @@ -28,11 +29,9 @@ const timeWindowSchema = z.object({

const zodSchema = z.object({
port: z.coerce.number().default(8081).describe("Metro server port"),
device_id: z
.string()
.describe(
"Device logicalDeviceId from debugger-connect (iOS simulator UDID or Android logicalDeviceId)."
),
device_id: metroDeviceIdParam(
"Device logicalDeviceId from debugger-connect (iOS simulator UDID or Android logicalDeviceId)."
),
mode: z
.enum(["top_functions", "time_window", "call_tree", "component_cpu"])
.describe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
isCaptureInFlight,
inFlightGuardMessage,
} from "../../../utils/profiler-shared/capture-guard";
import { metroDeviceIdParam } from "../../../utils/debugger/device-id-param";

// session_id is interpolated into on-disk file paths
// (`react-profiler-${id}_cpu.json`, `native-profiler-${id}_raw_cpu.xml`, …).
Expand Down Expand Up @@ -68,11 +69,9 @@ const zodSchema = z.object({
.describe(
"Metro port — the loaded React data is cached under this port for query tools (default 8081)"
),
device_id: z
.string()
.describe(
"Target device id from `list-devices`. Used to cache the loaded React session under the correct port+device key, and required to resolve the native profiler session for load_native."
),
device_id: metroDeviceIdParam(
"Target device id from `list-devices`. Used to cache the loaded React session under the correct port+device key, and required to resolve the native profiler session for load_native."
),
app_process: z
.string()
.optional()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import {
import { normalizeThreadName } from "../../../utils/profiler-shared/thread";
import { formatBytes, escapeMarkdownTableCell } from "../../../utils/profiler-shared/format";
import { demangleSymbol } from "../../../utils/profiler-shared/demangle";
import { metroDeviceIdParam } from "../../../utils/debugger/device-id-param";

const zodSchema = z.object({
device_id: z.string().describe("iOS Simulator UDID or Android serial."),
device_id: metroDeviceIdParam("iOS Simulator UDID or Android serial."),
mode: z
.enum(["hang_stacks", "function_callers", "thread_breakdown", "leak_stacks"])
.describe(
Expand Down Expand Up @@ -455,6 +456,29 @@ Fails if native-profiler-analyze has not been run or no parsed trace data is in
}),
async execute(services, params) {
const api = services.session as NativeProfilerSessionApi;
// A session with no capture state at all was minted by THIS call: the
// device_id matched no existing session, so nothing is known about the
// device. Say so without naming a platform — classification is shape-based
// and falls back to "android" for any opaque id (utils/device-info.ts:52),
// so an id this tool cannot place would otherwise be reported as an Android
// device (#618). That happens routinely: a forwarded Metro logicalDeviceId
// resolves only while a debugger connection is live, and the alias is
// dropped when it disposes.
if (!api.traceFile && !api.exportedFiles && !api.parsedData) {
throw new FailureError(
`No native profiler capture is loaded for device \`${params.device_id}\`. Run ` +
"native-profiler-start → native-profiler-stop → native-profiler-analyze on this device " +
"first. (If that id came from debugger-connect, pass the id from list-devices instead — " +
"the simulator UDID or adb serial — since profiler sessions are keyed by that one.)",
{
error_code: FAILURE_CODES.PROFILER_DATA_NOT_LOADED,
failure_stage: "profiler_stack_query_load_native_data",
failure_area: "tool_server",
error_kind: "not_found",
}
);
}

if (api.platform === "android") {
return executeAndroid(api, params);
}
Expand Down
40 changes: 40 additions & 0 deletions packages/tool-server/src/utils/debugger/device-id-param.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { z } from "zod";
import { canonicalDeviceId } from "./device-alias";

/**
* A `device_id` parameter that accepts either id namespace the Metro-family
* tools deal in.
*
* `debugger-connect` returns a Metro `logicalDeviceId`, and when several devices
* share one Metro the debugger tools instruct the user to pass it. But profiler
* sessions are keyed by the list-devices id (the simulator UDID or adb serial),
* and `classifyDevice` decides platform purely from an id's shape — an opaque
* logicalDeviceId matches no known shape, so it falls through to "android"
* (utils/device-info.ts:52). Following the debugger tools' own advice therefore
* built an Android session for an iOS device: the wrong service, not merely the
* wrong word in a message (#618).
*
* Canonicalizing here rather than inside each tool is what makes it stick. The
* registry parses params exactly once and hands the SAME object to `services()`
* and to `execute()` (packages/registry/src/registry.ts:125-153), so one
* transform covers URN construction, the platform branch, and every id the tool
* forwards to a platform impl — including the ones passed straight to `adb` and
* `simctl` as a serial. Canonicalizing in `services()` alone would leave those
* disagreeing with each other.
*
* Deliberately NOT done inside `resolveDevice`: that is the lowest-level
* identity primitive, used by ~60 call sites that can only ever receive a
* list-devices id, and making it consult a mutable alias map would make the same
* input resolve differently depending on whether a debugger had connected.
*
* The transform is invisible on the wire — `zodObjectToJsonSchema` derives the
* published schema with `io: "input"`, which keeps the `string`, the
* `minLength` and the description and drops the transform.
*/
export function metroDeviceIdParam(description: string) {
return z
.string()
.min(1, "device_id must not be empty")
.describe(description)
.transform((id: string) => canonicalDeviceId(id) ?? id);
}
Loading