Analyze potential hangs from a .trace bundle
analyzeHangsAnalyze .trace bundles to detect potential hangs, return aggregated stats (counts, durations), and list the top longest hangs sorted by duration. Filter with minDurationMs for user-visible hangs.
Instructions
[mg.trace] Run xcrun xctrace export against a .trace bundle for the potential-hangs schema and return aggregated stats (Hang vs Microhang counts, longest, average, total duration) plus the top N longest hangs sorted by duration. Use minDurationMs: 250 to filter to user-visible hangs only.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| tracePath | Yes | Absolute path to a `.trace` bundle (output of `xctrace record` with the Time Profiler or Hangs template). | |
| topN | No | Return the top N longest hangs in the response (default 10). | |
| minDurationMs | No | Filter out hangs shorter than this duration in milliseconds (default 0 — include all). Use 250 to focus on "real" hangs only. |
Implementation Reference
- src/tools/analyzeHangs.ts:159-189 (handler)The main async handler function for the analyzeHangs tool. Accepts input with tracePath, topN, and minDurationMs. Validates the trace bundle exists, runs `xcrun xctrace export` to extract potential-hangs data, then delegates to analyzeHangsFromXml for parsing and analysis.
export async function analyzeHangs( input: AnalyzeHangsInput, ): Promise<AnalyzeHangsResult> { const tracePath = resolvePath(input.tracePath); if (!existsSync(tracePath)) { throw new Error(`Trace bundle not found: ${tracePath}`); } const result = await runCommand( "xcrun", [ "xctrace", "export", "--input", tracePath, "--xpath", '/trace-toc/run/data/table[@schema="potential-hangs"]', ], { timeoutMs: 5 * 60_000 }, ); if (result.code !== 0) { throw new Error( `xctrace export failed (code ${result.code}): ${result.stderr || result.stdout}`, ); } return analyzeHangsFromXml( result.stdout, tracePath, input.topN ?? 10, input.minDurationMs ?? 0, ); } - src/tools/analyzeHangs.ts:11-31 (schema)Zod schema defining the input parameters for the analyzeHangs tool: tracePath (required string), topN (positive int, default 10), and minDurationMs (nonnegative number, default 0).
export const analyzeHangsSchema = z.object({ tracePath: z .string() .min(1) .describe( "Absolute path to a `.trace` bundle (output of `xctrace record` with the Time Profiler or Hangs template).", ), topN: z .number() .int() .positive() .default(10) .describe("Return the top N longest hangs in the response (default 10)."), minDurationMs: z .number() .nonnegative() .default(0) .describe( "Filter out hangs shorter than this duration in milliseconds (default 0 — include all). Use 250 to focus on \"real\" hangs only.", ), }); - src/tools/analyzeHangs.ts:61-136 (handler)Pure function analyzeHangsFromXml that parses the xctrace XML output, filters hangs by minDurationMs, classifies into Hang/Microhang, computes stats (counts, longest, average, total duration), sorts and slices to topN, and builds a diagnosis string.
export function analyzeHangsFromXml( xml: string, tracePath: string, topN = 10, minDurationMs = 0, ): AnalyzeHangsResult { const tables = parseXctraceXml(xml); const hangsTable = tables.find((t) => t.schema === "potential-hangs"); if (!hangsTable) { return { ok: true, tracePath, totals: { rows: 0, hangs: 0, microhangs: 0, longestMs: 0, averageMs: 0, totalDurationMs: 0, }, top: [], diagnosis: "No potential-hangs table found in the trace.", }; } const allEntries: HangEntry[] = []; for (const row of hangsTable.rows) { const startNs = asNumber(row.start) ?? 0; const durationNs = asNumber(row.duration) ?? 0; allEntries.push({ startNs, startFmt: asFormatted(row.start) ?? "", durationNs, durationMs: durationNs / 1_000_000, durationFmt: asFormatted(row.duration) ?? "", hangType: asFormatted(row["hang-type"]) ?? "", }); } const filtered = allEntries.filter((e) => e.durationMs >= minDurationMs); const hangs = filtered.filter((e) => e.hangType === "Hang"); const microhangs = filtered.filter((e) => e.hangType === "Microhang"); const totalDurationMs = filtered.reduce((sum, e) => sum + e.durationMs, 0); const longestMs = filtered.reduce( (max, e) => Math.max(max, e.durationMs), 0, ); const averageMs = filtered.length > 0 ? totalDurationMs / filtered.length : 0; const top = [...filtered] .sort((a, b) => b.durationMs - a.durationMs) .slice(0, topN); const diagnosis = buildHangDiagnosis( filtered.length, hangs.length, microhangs.length, longestMs, averageMs, ); return { ok: true, tracePath, totals: { rows: filtered.length, hangs: hangs.length, microhangs: microhangs.length, longestMs, averageMs, totalDurationMs, }, top, diagnosis, }; } - src/index.ts:217-230 (registration)Registration of the analyzeHangs tool with the MCP server, including title, description, inputSchema, and the callback that invokes analyzeHangs and returns JSON-stringified results.
server.registerTool( "analyzeHangs", { title: "Analyze potential hangs from a .trace bundle", description: "[mg.trace] Run `xcrun xctrace export` against a `.trace` bundle for the `potential-hangs` schema and return aggregated stats (Hang vs Microhang counts, longest, average, total duration) plus the top N longest hangs sorted by duration. Use `minDurationMs: 250` to filter to user-visible hangs only.", inputSchema: analyzeHangsSchema.shape, }, async (input) => { const result = await analyzeHangs(input); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; }, - src/tools/analyzeHangs.ts:138-157 (helper)Helper function buildHangDiagnosis that generates a human-readable diagnosis string summarizing hang counts, longest/average durations, and severity indicators.
function buildHangDiagnosis( rows: number, hangs: number, microhangs: number, longestMs: number, averageMs: number, ): string { if (rows === 0) { return "No hangs detected (or all were filtered out by minDurationMs)."; } const parts: string[] = []; parts.push(`${rows} hangs total (${hangs} Hang, ${microhangs} Microhang).`); parts.push(`Longest: ${longestMs.toFixed(0)}ms, average: ${averageMs.toFixed(0)}ms.`); if (hangs >= 10) { parts.push("Severe hang load — investigate main-thread work on the slow path."); } else if (hangs > 0 && longestMs > 1000) { parts.push("At least one hang over 1s — likely user-visible freeze."); } return parts.join(" "); }