ios-memgraph-analysis

$npx mdskill add dpearson2699/swift-ios-skills/ios-memgraph-analysis

Analyze iOS memory graphs to diagnose leaks and growth.

  • Captures and analyzes .memgraph files for memory leaks or growth.
  • Depends on Apple CLI tools: leaks, heap, vmmap, malloc_history.
  • Distinguishes unreachable leaks from reachable growth using ownership paths.
  • Preserves raw tool output and verifies fixes with same-flow comparison.

SKILL.md

.github/skills/ios-memgraph-analysisView on GitHub ↗
---
name: ios-memgraph-analysis
description: "Use when capturing or analyzing an iOS .memgraph, especially when the task mentions a memory leak, heap growth, persistent memory increase, ownership path, or matched-capture comparison with Apple CLI tools. Covers unambiguous Simulator capture, leaks/heap/vmmap/malloc_history evidence, raw artifact preservation, and same-flow verification. Use debugging-instruments for interactive Xcode Memory Graph, Instruments, generic retain-cycle inspection, or LLDB work."
---

# iOS Memgraph Analysis

Use memory graphs to prove why memory survives a defined lifetime boundary.
Separate unreachable leaks from reachable growth, preserve raw tool output, and
verify the same app-owned type and ownership path after a fix.

## Contents

- [Boundary](#boundary)
- [Evidence Model](#evidence-model)
- [Workflow](#workflow)
- [Ownership Decisions](#ownership-decisions)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Boundary

This skill owns `.memgraph` capture and command-line ownership/growth analysis.
Use the Memory Graph Debugger or Instruments when their interactive graph and
allocation timeline are the primary task. Use source review for a suspected
closure capture only after runtime evidence identifies the lifetime or path.

## Evidence Model

Do not collapse these conditions:

- **Unreachable leak:** allocated memory no longer has a path from a live root.
  An isolated strong cycle can be unreachable and still consume memory.
- **Reachable but abandoned state:** a live root still retains objects the user
  flow no longer needs. `leaks` may correctly report zero.
- **Expected cache or pool:** memory survives intentionally and must be judged by
  its bound, eviction behavior, and pressure response.
- **Heap regression or fragmentation:** footprint grows because more/larger
  allocations persist or dirty pages are poorly utilized, without a leak.

Apple's leak scanner uses conservative pointer discovery and incomplete type
metadata. Counts can fluctuate, and a zero result does not prove the absence of
an ownership bug. Strong evidence identifies the expected lifetime, an
app-owned type or allocation, and a credible path or isolated reproduction.

## Workflow

### 1. Define the lifetime before capturing

Name the object that should disappear and the event that ends its useful life.
For example: `EditorViewModel` should deinitialize after dismissing the editor
and completing pending save work.

Record one deterministic sequence:

1. launch or restore a known state;
2. take an optional baseline graph;
3. perform the feature flow;
4. cross the expected release boundary;
5. wait for legitimate asynchronous cleanup;
6. take the post-flow graph.

Keep build, simulator/device, data, Malloc Stack Logging setting, and repetition
count stable. Malloc Stack Logging adds valuable allocation backtraces but also
overhead; compare only runs with the same setting.

### 2. Capture a graph without guessing the process

Xcode can export a graph from the Memory Graph Debugger. For a running Simulator
app, use the helper from this skill:

```bash
mkdir -p /tmp/myapp-memory
mkdir /tmp/myapp-memory/run-01
python3 scripts/capture_sim_memgraph.py \
  --bundle-id com.example.MyApp \
  --output-dir /tmp/myapp-memory/run-01 \
  --pretty > /tmp/myapp-memory/run-01/capture.json
```

The per-run `mkdir` must fail if the capture directory already exists. Use a
new run name rather than mixing stale evidence with a retry.

Pass `--udid` when more than one Simulator is booted. The helper accepts only
one exact launchd label and PID; zero or multiple matches are errors. It runs the
host `leaks --outputGraph` command, retains stdout/stderr, and writes a manifest.
Do not replace this with `pgrep | head -1` or a substring match.

Capturing suspends the process. Do not use capture latency as performance data.

### 3. Preserve raw output and build a bounded summary

```bash
MEMGRAPH=$(jq -er \
  'select(.status == "captured") | .memgraph | select(type == "string" and length > 0)' \
  /tmp/myapp-memory/run-01/capture.json)
test -s "$MEMGRAPH"
python3 scripts/summarize_memgraph.py \
  "$MEMGRAPH" \
  --artifact-dir /tmp/myapp-memory/run-01/analysis-raw \
  --app-image 'MyApp|MyFeatureKit' \
  --trace-limit 3 --group-by-type --pretty \
  > /tmp/myapp-memory/run-01/analysis.json
```

Read the exact graph path from the preserved capture report; do not guess a
timestamped filename. The helper creates a dedicated raw-artifact directory,
refuses to reuse it, runs `leaks --list`, and parses only a conservative subset
of its text. `--app-image` marks candidate rows; it does not prove ownership.
`--trace-limit` runs bounded
`leaks --traceTree=<address>` queries. Add `--reference-tree` when aggregate
root paths are more useful than individual leaked addresses. With
`--group-by-type`, that reference-tree query is grouped in the same invocation.
Exit statuses 0 and 1 from `leaks` remain analyzable; a primary status above 1
fails the summary, while optional-query failures are preserved and warned as
unusable without discarding a valid primary summary.

Apple does not publish these text formats as stable machine schemas. Treat
parse warnings as a reason to inspect the raw artifacts, not to loosen the
parser until it emits a desired answer.

### 4. Find the first actionable app-owned edge

Start with an app-owned leaked type or allocation stack. Inspect:

- the leak's object graph and Malloc Stack Logging backtrace, when present;
- a bounded `--traceTree=<address>` for objects that reference one address;
- `--groupByType` to compress repeated types and reveal a retained payload;
- `--referenceTree` for a top-down view when the responsible address is unclear;
- source code for the first strong edge controlled by the app.

An unreachable self-cycle may have no live root in `traceTree`. Use the grouped
leak graph plus source verification or reduce the behavior to an isolated
reproduction. Never invent a root path that the graph does not contain.

### 5. Investigate growth when `leaks` is empty

Use matching baseline and post-flow graphs, locate the growing region, compare
object types, then trace a suspicious address back to an app-owned edge. The
evidence goal is persistent reachable growth across the same lifetime—not a
lower RSS value or a single large snapshot. Load
[reachable-growth.md](references/reachable-growth.md) only for this empty-leak
branch; it contains the ordered `vmmap`, `heap`, `leaks`, and `malloc_history`
queries and their logging-dependent alternatives.

### 6. Fix and verify the same lifetime

Prefer the narrowest ownership correction: break the unintended strong edge,
cancel work that owns the object, remove an observer, bound/evict a cache, or
release a large buffer after its last use. Use `weak` when the reference may
legitimately become `nil`; use `unowned` only with a proven lifetime guarantee.

Repeat the identical flow. A fix is supported when the same app-owned type/path
disappears or the pre/post growth attributable to it is removed across repeated
runs. Lower RSS, a smaller graph file, or a lower aggregate leak count alone is
not proof.

## Ownership Decisions

| Evidence | Next action |
|---|---|
| App type in a root cycle | Inspect both strong edges and allocation stack. |
| No root for a leaked address | Inspect grouped cycle evidence and isolate the flow. |
| Live root retains dismissed feature state | Follow the path to the first app-owned edge. |
| Zero leaks but repeated malloc growth | Diff baseline/post heap objects. |
| Framework object dominates | Find the app-created owner, input, or call frequency. |
| Growth stabilizes at a documented bound | Test eviction/pressure behavior before changing it. |

## Common Mistakes

- Declaring the app leak-free because `leaks` returned zero once.
- Selecting the first PID or Simulator from an ambiguous list.
- Enabling Malloc Stack Logging in only one side of a comparison.
- Treating a parser's best-effort type column as an API guarantee.
- Pasting enormous reference trees into a report without finding an app edge.
- Fixing every closure with `[weak self]` without reasoning about lifetime.
- Claiming success from graph size, RSS, or total-count changes without proving the target lifetime and ownership path.

## Review Checklist

- [ ] The object and expected release boundary are explicit.
- [ ] Baseline and post-flow graphs use the same build, runtime target, data
      state, deterministic flow, cleanup wait, repetitions, and Malloc Stack
      Logging setting.
- [ ] Simulator, bundle identifier, process label, and PID are unambiguous.
- [ ] Original graph and raw command outputs are preserved.
- [ ] Current installed-tool help confirms version-sensitive command shapes.
- [ ] Leak, reachable growth, expected cache, and fragmentation are separated.
- [ ] The finding names an app-owned type/allocation and credible path.
- [ ] Missing type metadata or conservative-scanner limits are disclosed.
- [ ] The fix changes one ownership/lifetime cause.
- [ ] Verification repeats the same flow and evidence query.

## References

- [Reachable growth when `leaks` is empty](references/reachable-growth.md) —
  matched-graph comparison and address-to-owner workflow
- [Gathering information about memory use](https://sosumi.ai/documentation/xcode/gathering-information-about-memory-use)
- [Detect and diagnose memory issues — WWDC21](https://sosumi.ai/videos/play/wwdc2021/10180)
- [Analyze heap memory — WWDC24](https://sosumi.ai/videos/play/wwdc2024/10173)

More from dpearson2699/swift-ios-skills

SkillDescription
accessorysetupkitDiscover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.
activitykitImplement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.
adattributionkitMeasure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.
alarmkitImplement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, StandBy, and paired Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, system Stop and AlarmButton secondary actions, authorization, state observation, countdown widget-extension handoff, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.
app-clipsBuild iOS App Clips with invocation URLs, App Clip Codes, NFC, QR codes, Safari banners, Maps, Messages, target setup, App Store Connect experiences, size/capability constraints, NSUserActivity routing, SKOverlay promotion, App Group/keychain handoff, ephemeral notifications, location confirmation, and full-app migration. Use when creating App Clips or wiring App Clip invocation, experience configuration, or full-app handoff.
app-intentsImplement App Intents for Siri, Shortcuts, Spotlight, widgets, Control Center, and Apple Intelligence on iOS. Covers AppIntent actions, AppEntity and EntityQuery models, AppShortcutsProvider phrases, IndexedEntity Spotlight indexing, WidgetConfigurationIntent, SnippetIntent, and assistant schemas. Use when exposing app actions or entities to system surfaces.
app-store-reviewAudits App Store submission readiness and rejection risk across current review guidelines, PrivacyInfo.xcprivacy and required-reason APIs, privacy labels, ATT, StoreKit payments, metadata, entitlements, widgets, and Live Activities. Use when preparing a submission, responding to rejection, reconciling privacy evidence, or separating upload blockers from cleanup.
apple-on-device-aiBuild private, on-device AI features on iPhone, iPad, and Mac with Foundation Models, Core ML, MLX Swift, or llama.cpp. Use when choosing an Apple-local model runtime, building an Apple Intelligence chatbot or tool-calling feature, running an LLM on Apple Silicon, converting or compressing a Python model for Core ML, or comparing on-device inference backends. For Swift Core ML loading and prediction code, use the coreml skill.
appmigrationkitTransfer app data to or from other platforms using AppMigrationKit. Use when implementing system-orchestrated one-time migration between iOS and Android or another platform, building an AppMigrationExtension, packaging transportable resources with ResourcesArchiver, importing resources on the destination device, reporting import progress, handling migration errors and app group cleanup, checking MigrationStatus, or testing migration code with AppMigrationTester.
audioaccessorykitSupport automatic audio switching for paired third-party Bluetooth headphones or earbuds with AudioAccessoryKit. Use when a companion app registers an audio accessory, an app extension reports worn/removed placement or connected source-device changes, or AccessoryControlDevice capabilities and errors need handling. Do not use for general AVAudioSession routing, Bluetooth transport, or initial accessory pairing.