Count instances reachable from a specific cycle root
reachableFromCycleCount reachable objects from a chosen retain cycle root to distinguish the cycle culprit from its dependencies. Returns per-class occurrence counts and total reachable nodes.
Instructions
[mg.memory] Cycle-scoped reachability + class counting. Answers questions like "how many NSURLSessionConfiguration instances are reachable from the cycle rooted at DetailViewModel?" — distinguishing the actual culprit (the cycle root) from its retained dependencies. Pick a cycle by zero-based cycleIndex or by rootClassName substring. Returns per-class counts ranked by occurrence, plus the total reachable node count.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Absolute path to a `.memgraph` file. | |
| cycleIndex | No | Zero-based index of the ROOT CYCLE to scope to. Mutually exclusive with `rootClassName`. When neither is given, defaults to cycle index 0. | |
| rootClassName | No | Substring of the root cycle's class name (e.g. "DetailViewModel"). Picks the first ROOT CYCLE whose root matches. Mutually exclusive with `cycleIndex`. | |
| className | No | Optional filter — only count nodes whose className contains this substring. When omitted, returns the full per-class breakdown. | |
| topN | No | Cap on per-class entries returned (default 20). | |
| verbosity | No | Class-name verbosity for the response. See analyzeMemgraph for the same flag. | compact |
Implementation Reference
- src/tools/reachableFromCycle.ts:188-198 (handler)Main async handler that runs leaks + parse, then delegates to the pure reachableFromReport function.
export async function reachableFromCycle( input: ReachableFromCycleInput, ): Promise<ReachableFromCycleResult> { if (input.cycleIndex !== undefined && input.rootClassName !== undefined) { throw new Error( "Provide either `cycleIndex` or `rootClassName`, not both.", ); } const { report, resolvedPath } = await runLeaksAndParse(input.path); return reachableFromReport(report, resolvedPath, input); } - src/tools/reachableFromCycle.ts:130-186 (handler)Pure function that computes the reachability result from a parsed LeaksReport (exposed for testing).
export function reachableFromReport( report: LeaksReport, path: string, input: ReachableFromCycleInput, ): ReachableFromCycleResult { const picked = pickCycle(report, input); if (!picked) { throw new Error( input.rootClassName ? `No ROOT CYCLE found whose root class contains "${input.rootClassName}".` : `No ROOT CYCLE at index ${input.cycleIndex ?? 0}. Available roots: ${rootCyclesOnly(report.cycles).length}.`, ); } const verbosity = input.verbosity ?? "compact"; const { byClass, total } = countReachableFromNode(picked.node, verbosity); let entries: ClassCount[] = Array.from(byClass.entries()).map(([n, c]) => ({ className: n, count: c, })); if (input.className) { entries = entries.filter((e) => e.className.includes(input.className!), ); } entries.sort((a, b) => b.count - a.count); entries = entries.slice(0, input.topN ?? 20); // Find a class that looks "app-level" (not stdlib / SwiftUI internal) to // suggest as the followup target. The cycle root is the canonical answer // when it itself is app-level; otherwise we walk the counts. const suggestedNextCalls: NextCallSuggestion[] = []; const rootShort = shortenForVerbosity(picked.node.className, verbosity); const candidate = pickPrimaryAppClass([rootShort]) ?? pickPrimaryAppClass(entries.map((e) => e.className)); if (candidate) { suggestedNextCalls.push(suggestionGetDefinition({ symbolName: candidate })); suggestedNextCalls.push(suggestionFindReferences({ symbolName: candidate })); } return { ok: true, path, cycle: { index: picked.index, rootClass: rootShort, rootAddress: picked.node.address, totalReachable: total, }, counts: entries, uniqueClasses: byClass.size, ...(suggestedNextCalls.length > 0 ? { suggestedNextCalls } : {}), }; } - Zod schema defining the input parameters: path, cycleIndex, rootClassName, className, topN, verbosity.
export const reachableFromCycleSchema = z.object({ path: z.string().min(1).describe("Absolute path to a `.memgraph` file."), cycleIndex: z .number() .int() .nonnegative() .optional() .describe( "Zero-based index of the ROOT CYCLE to scope to. Mutually exclusive with `rootClassName`. When neither is given, defaults to cycle index 0.", ), rootClassName: z .string() .optional() .describe( "Substring of the root cycle's class name (e.g. \"DetailViewModel\"). Picks the first ROOT CYCLE whose root matches. Mutually exclusive with `cycleIndex`.", ), className: z .string() .optional() .describe( "Optional filter — only count nodes whose className contains this substring. When omitted, returns the full per-class breakdown.", ), topN: z .number() .int() .positive() .max(100) .default(20) .describe("Cap on per-class entries returned (default 20)."), verbosity: z .enum(["compact", "normal", "full"]) .default("compact") .describe( "Class-name verbosity for the response. See analyzeMemgraph for the same flag.", ), }); - src/index.ts:411-425 (registration)Registration of the 'reachableFromCycle' tool with MCP server, including title, description, inputSchema, and handler.
server.registerTool( "reachableFromCycle", { title: "Count instances reachable from a specific cycle root", description: "[mg.memory] Cycle-scoped reachability + class counting. Answers questions like \"how many `NSURLSessionConfiguration` instances are reachable from the cycle rooted at `DetailViewModel`?\" — distinguishing the actual culprit (the cycle root) from its retained dependencies. Pick a cycle by zero-based `cycleIndex` or by `rootClassName` substring. Returns per-class counts ranked by occurrence, plus the total reachable node count.", inputSchema: reachableFromCycleSchema.shape, }, async (input) => { const result = await reachableFromCycle(input); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; }, ); - src/runtime/suggestions.ts:176-189 (helper)Helper function that builds a NextCallSuggestion to chain reachableFromCycle after analyzeMemgraph.
/** Build suggestion to call reachableFromCycle (used after analyzeMemgraph). */ export function suggestionReachableFromCycle(opts: { path: string; cycleIndex?: number; }): NextCallSuggestion { return { tool: "reachableFromCycle", args: { path: opts.path, cycleIndex: opts.cycleIndex ?? 0, }, why: "Confirm which app-level class is the actual culprit (the cycle root) versus collateral retained dependencies.", }; }