Analyze a Time Profiler trace
analyzeTimeProfileExports time-profile schema from a .trace bundle and returns the top hottest stacks by sample count. Provides workarounds when symbolication fails.
Instructions
[mg.trace] Export the time-profile schema from a .trace bundle and return top symbols by sample count. Note: heavy/unsymbolicated traces may crash xctrace export — when that happens, the tool returns a notice field with workarounds (open in Instruments first to symbolicate, or re-record shorter).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| tracePath | Yes | Absolute path to a `.trace` bundle. | |
| topN | No | Return the top N hottest stacks by sample count (default 20). |
Implementation Reference
- src/tools/analyzeTimeProfile.ts:119-157 (handler)Main handler: validates trace path, runs 'xcrun xctrace export' to extract time-profile XML data, then delegates to analyzeTimeProfileFromXml for parsing. Handles SIGSEGV crash with a user-friendly notice.
export async function analyzeTimeProfile( input: AnalyzeTimeProfileInput, ): Promise<AnalyzeTimeProfileResult> { 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="time-profile"]', ], { timeoutMs: 5 * 60_000 }, ); if (result.code !== 0) { // SIGSEGV typically reports as 139 (128 + 11). Surface a useful message. if (result.code === 139 || /Segmentation/i.test(result.stderr)) { return { ok: false, tracePath, totalSamples: 0, topSymbols: [], topRows: [], notice: SIGSEGV_NOTICE, diagnosis: "Could not export time-profile schema (xctrace crashed). See `notice` for workarounds.", }; } throw new Error( `xctrace export failed (code ${result.code}): ${result.stderr || result.stdout}`, ); } return analyzeTimeProfileFromXml(result.stdout, tracePath, input.topN ?? 20); } - src/tools/analyzeTimeProfile.ts:53-110 (handler)Pure function that parses xctrace XML output, aggregates sample counts per symbol, and returns sorted top symbols and raw rows. Handles edge cases like missing time-profile table or empty samples.
export function analyzeTimeProfileFromXml( xml: string, tracePath: string, topN = 20, ): AnalyzeTimeProfileResult { const tables = parseXctraceXml(xml); const tp = tables.find((t) => t.schema === "time-profile"); if (!tp) { return { ok: true, tracePath, totalSamples: 0, topSymbols: [], topRows: [], diagnosis: "No time-profile table found in the export.", }; } const rows: SampleEntry[] = []; const symbolCounts = new Map<string, number>(); for (const row of tp.rows) { const weight = asNumber(row.weight); const weightFmt = asFormatted(row.weight); // Symbol may live under 'backtrace' or 'symbol' or as a nested cell. const symbol = asFormatted(row.symbol) ?? asFormatted(row["weight"]) ?? row.backtrace?.fmt ?? row.backtrace?.raw ?? undefined; const threadName = row.thread?.fmt ?? undefined; rows.push({ weight, weightFmt, symbol, threadName }); if (symbol) { symbolCounts.set(symbol, (symbolCounts.get(symbol) ?? 0) + 1); } } const topSymbols = Array.from(symbolCounts.entries()) .map(([symbol, samples]) => ({ symbol, samples })) .sort((a, b) => b.samples - a.samples) .slice(0, topN); const topRows = [...rows] .sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0)) .slice(0, topN); return { ok: true, tracePath, totalSamples: rows.length, topSymbols, topRows, diagnosis: rows.length === 0 ? "No samples found in the time-profile table." : `${rows.length} samples; top symbol: ${topSymbols[0]?.symbol ?? "unknown"} (${topSymbols[0]?.samples ?? 0} samples).`, }; } - Zod schema for input validation: tracePath (required string) and topN (optional int, default 20).
export const analyzeTimeProfileSchema = z.object({ tracePath: z .string() .min(1) .describe("Absolute path to a `.trace` bundle."), topN: z .number() .int() .positive() .default(20) .describe("Return the top N hottest stacks by sample count (default 20)."), }); - Result type definition: ok, tracePath, totalSamples, topSymbols, topRows, optional notice, and diagnosis string.
export interface AnalyzeTimeProfileResult { ok: boolean; tracePath: string; totalSamples: number; /** Per-symbol aggregation, sorted by sample count descending. */ topSymbols: Array<{ symbol: string; samples: number }>; /** Top N rows after the aggregation step (raw view). */ topRows: SampleEntry[]; /** * Optional notice explaining a known limitation * (e.g. xctrace crashed exporting the time-profile schema). */ notice?: string; diagnosis: string; } - src/index.ts:233-247 (registration)Tool registration with MCP server: registers 'analyzeTimeProfile' with title, description, inputSchema, and async handler that calls analyzeTimeProfile and returns JSON response.
server.registerTool( "analyzeTimeProfile", { title: "Analyze a Time Profiler trace", description: "[mg.trace] Export the `time-profile` schema from a `.trace` bundle and return top symbols by sample count. Note: heavy/unsymbolicated traces may crash xctrace export — when that happens, the tool returns a `notice` field with workarounds (open in Instruments first to symbolicate, or re-record shorter).", inputSchema: analyzeTimeProfileSchema.shape, }, async (input) => { const result = await analyzeTimeProfile(input); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; }, );