agentleFS
fal3fal3/claude-skills-collection/skills/memory-leak-diagnosis-skill/SKILL.md

Memory Leak Diagnosis Skill

Skill18 starsChanged 59 days agoMITView on GitHub

Diagnose, explain, and fix Swift and Apple-platform memory leaks, unexpected object retention, deinit failures, heap growth, retain cycles, runaway caches, task and AsyncSequence lifetimes, timer/delegate/closure ownership, jetsam, and ARC issues using Xcode Memory Graph and Instruments Allocations, Leaks, and VM tools. Use when objects do not deallocate, memory rises across repeated workflows, or weak/unowned and capture-list choices are unclear. Do not enable Zombies for leak measurement, call every temporary retention a leak, or use unowned without a proven lifetime invariant.

The file

Show its rules and text

We split this skill into the rules it gives an agent

14 rules. About 0 are common, 8 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈8

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Add temporary deinit probes to expected owners and dependencies.

    line 35
  2. Distinctive:

    Use Allocations generation marks before and after each repetition.

    line 38
  3. Distinctive:

    Run Leaks for unreachable allocations and inspect their allocation backtraces.

    line 39
  4. Distinctive:

    Use Zombies only to diagnose messaging/use-after-free.

    line 44
  5. Distinctive:

    Read examples/example_instruments_leaks.md for a repeatable capture and verification procedure.

    line 46
  6. Very specific:

    Use weak when the referenced object may deallocate first.

    line 51
  7. Very specific:

    Use unowned only when the referenced object is guaranteed to outlive every access.

    line 52
  8. Very specific:

    If the car does not own its person, make Car.owner weak.

    line 83
  9. Very specific:

    Read examples/example_retain_cycle.swift for compiling stored-closure, delegate, bidirectional, value-container, and task-lifetime examples.

    line 85
  10. Very specific:

    Do not add [weak self] mechanically.

    line 89
  11. Distinctive:

    keep the owner alive until a finite operation completes.

    line 91
  12. Distinctive:

    stop when the owner disappears.

    line 92
  13. Distinctive:

    Search for the concrete type and compare instance count with the expected count.

    line 109
  14. Distinctive:

    Do not depend on a particular arrow color, UI icon, or exact reference count.

    line 114

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Swift Memory Diagnosis

Prove the lifetime bug before changing capture lists. “Memory grew,” “deinit has not run yet,” “an object is retained,” and “an allocation is leaked” describe different evidence.

Compatibility baseline

ARC, weak references, and Xcode’s core memory tools apply across currently supported Swift/Apple-platform projects. State version requirements for the specific APIs in the inspected code:

  • Structured concurrency and Task require Swift 5.5 and the corresponding platform concurrency runtime.
  • The task example in this skill uses duration-based sleep, available with Swift 5.7-era toolchains and iOS 16 / macOS 13.
  • Observation, SwiftData, Combine, and framework-specific lifetime rules depend on the app’s declared deployment targets.
  • Xcode and Instruments UI labels change. Describe goals and inspected relationships, not assumed arrow colors or displayed reference-count numbers.

Classify the symptom

ObservationLikely categoryBest first tool
A dismissed screen remains reachableUnwanted retention / ownership bugMemory Graph
Heap grows after repeating one workflowPersistent allocations, cache, or leakAllocations generations
Leaks instrument reports unreachable blocksHeap leakLeaks + allocation backtrace
Memory falls after pressure or cache purgeCache / transient high-water markAllocations + signposts
Process is killed under memory pressureExcess footprint / jetsamOrganizer report, VM Tracker, Allocations
Crash after replacing weak with unownedLifetime invariant violationCrash backtrace + ownership graph

The Leaks instrument does not find every strongly retained object. A retain cycle can remain reachable from framework or application roots and appear only in Memory Graph or repeated Allocations generations.

Evidence-first workflow

  1. Reproduce one bounded lifecycle: present/use/dismiss, start/stop, subscribe/unsubscribe, or load/purge.
  2. Add temporary deinit probes to expected owners and dependencies.
  3. Repeat the lifecycle several times. Record settled footprint after idle, not only peak memory.
  4. Capture Memory Graph after the object should be gone. Inspect incoming strong paths back to roots.
  5. Use Allocations generation marks before and after each repetition. Look for instance counts that monotonically persist.
  6. Run Leaks for unreachable allocations and inspect their allocation backtraces.
  7. Inspect tasks, streams, timers, observers, sessions, display links, delegates, and caches that cross the lifecycle boundary.
  8. Fix the ownership or cancellation contract, not merely the nearest closure.
  9. Repeat the identical workflow and verify deinitialization, stable generation counts, and acceptable footprint.

Use Zombies only to diagnose messaging/use-after-free. Zombies intentionally preserve deallocated objects and invalidate leak and footprint measurements.

Read examples/example_instruments_leaks.md for a repeatable capture and verification procedure.

ARC rules

  • Strong is the default for ownership.
  • Use weak when the referenced object may deallocate first. A weak property must be optional and becomes nil automatically.
  • Use unowned only when the referenced object is guaranteed to outlive every access. A violated invariant traps; “avoids optional handling” is not a valid reason.
  • Delegates are commonly weak because the delegate often owns the delegating object, but verify the actual graph.
  • A closure forms a cycle only when an owner stores the closure and that closure strongly captures the owner (directly or through another path).
  • A queue or task may retain a captured object only until work finishes. That can be harmful delayed release, but it is not automatically a permanent cycle.

Correct delegate graphs

protocol DataDelegate: AnyObject {}

final class StrongDelegateManager {
    var delegate: (any DataDelegate)?
}

final class ScreenOwner: DataDelegate {
    let manager = StrongDelegateManager()
    init() { manager.delegate = self }
}

This graph cycles: owner → manager → delegate → owner. Making delegate weak breaks it. If delegate is already weak, the delegate relationship is not the cycle; inspect stored callbacks, tasks, and external owners instead.

Correct bidirectional graph

Both links below are strong, so the graph cycles:

final class Person { var car: Car? }
final class Car { var owner: Person? }

If the car does not own its person, make Car.owner weak. Do not then describe that weak link as incrementing a retain count.

Read examples/example_retain_cycle.swift for compiling stored-closure, delegate, bidirectional, value-container, and task-lifetime examples.

Closure and task lifetimes

Do not add [weak self] mechanically. Decide whether work should:

  • keep the owner alive until a finite operation completes;
  • stop when the owner disappears; or
  • be owned by a longer-lived service independent of the UI.

For indefinite work owned by a screen or model, store a task/token, cancel it in an explicit stop, and make the task avoid a strong capture across suspension points. A weak capture followed by guard let self outside an infinite loop still promotes self for the task’s entire remaining lifetime.

For AsyncStream, define who finishes the continuation and set onTermination when producer cleanup is required. For Combine, retain cancellables only for the intended owner lifetime. For NotificationCenter block observers, retain and remove the returned token. Invalidate Timer and CADisplayLink; invalidate URL sessions whose delegate lifetime should end.

Deliver UI-facing delegate callbacks and state mutations on MainActor rather than calling UIKit/AppKit from a background queue.

Value types are not a blanket escape hatch

Structs and enums do not have identity under ARC, but they can contain reference-typed storage or escaping closures. A class can strongly own a struct that owns a closure that strongly captures the class. Standard-library collections also use internal reference storage. Analyze the complete graph rather than declaring value types incapable of participating in cycles.

Memory Graph

  1. Reproduce and pause after the owner should be released.
  2. Open Debug Memory Graph.
  3. Search for the concrete type and compare instance count with the expected count.
  4. Select a stale instance and follow incoming strong references toward a root.
  5. Separate framework roots that are expected to persist from application ownership that should have ended.
  6. Capture the path and lifecycle that created it before editing code.

Do not depend on a particular arrow color, UI icon, or exact reference count; those presentations vary by Xcode release and compiler optimization.

Instruments

  • Allocations: mark generations around repeated workflows; compare persistent instance counts, allocation sites, and backtraces.
  • Leaks: inspect reported unreachable allocations; absence of a report does not prove a dismissed controller was released.
  • VM Tracker: investigate image buffers, mapped files, graphics surfaces, and other virtual-memory categories outside ordinary object counts.
  • Points of Interest / signposts: align allocation changes with user actions.

Distinguish bounded caches from leaks by documenting their limit, eviction trigger, response to memory pressure, and observed steady state. An unbounded cache is still a memory defect even if every object remains intentionally reachable.

Common ownership audit

  • Stored closures and completion handlers
  • Task, task groups, actors, and continuations
  • AsyncStream producers and consumers
  • Combine subscriptions and notification tokens
  • Timers, display links, animation callbacks, and run-loop sources
  • Delegates and data sources
  • URLSession delegates and outstanding requests
  • KVO/context observations
  • Image/data caches and autorelease-heavy loops
  • SwiftUI state models, environment values, hosting controllers, and presentation closures
  • Core Data / SwiftData contexts and fetched object graphs

Review checklist

  • The reported stale type has a documented expected lifetime.
  • There is a reproducible workflow and a baseline instance count.
  • Incoming strong paths identify a root; no conclusion relies only on memory rising once.
  • Weak and unowned choices state the actual lifetime invariant.
  • Finite asynchronous work is distinguished from indefinite retained work.
  • Cleanup has an explicit owner and can run on cancellation and error paths.
  • Caches have bounds and memory-pressure behavior.
  • The same workflow was repeated after the fix and reached a stable steady state.

Supporting material

  • README.md summarizes the diagnostic model.
  • examples/example_retain_cycle.swift contains strict-concurrency-safe ownership examples.
  • examples/example_instruments_leaks.md provides an Instruments and Memory Graph workflow.
  • examples/prompts.md contains representative activation prompts.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.