<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Carloshperc</title>
    <description>The latest articles on DEV Community by Carloshperc (@carlosperc).</description>
    <link>https://dev.to/carlosperc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F671471%2Fbd59aff3-b2dd-45ea-8562-53067045479f.jpeg</url>
      <title>DEV Community: Carloshperc</title>
      <link>https://dev.to/carlosperc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/carlosperc"/>
    <language>en</language>
    <item>
      <title>An afternoon of iOS perf work, with Claude in the loop</title>
      <dc:creator>Carloshperc</dc:creator>
      <pubDate>Fri, 08 May 2026 17:02:29 +0000</pubDate>
      <link>https://dev.to/carlosperc/an-afternoon-of-ios-perf-work-with-claude-in-the-loop-42eg</link>
      <guid>https://dev.to/carlosperc/an-afternoon-of-ios-perf-work-with-claude-in-the-loop-42eg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Subtitle: An iOS performance investigation I actually did, end-to-end through Claude Code, with MCP-driven simulator control, &lt;code&gt;xctrace&lt;/code&gt; Time Profiler, and the Memory Graph CLI. What the AI handled, what it didn't, and how my workflow changed.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hey folks, let me walk you through an afternoon I had debugging an iOS perf ticket I'd been postponing for three weeks. Motivations, obstacles, dead ends, and wins included.&lt;/p&gt;

&lt;p&gt;Quick setup. Working on an app that uses SwiftUI quite a bit. The SavedItems tab was getting slow after about 15 location detail screens. Probably a memory leak. Probably something related to SwiftUI navigation. Probably "easy" after I stopped to analyze it. Spoiler: None of those hypotheses were correct.&lt;/p&gt;

&lt;p&gt;The investigation took an entire afternoon and resulted in three independent corrections and three pull requests. And most of my time at the keyboard was spent reviewing what Claude had just done, not typing.&lt;/p&gt;

&lt;p&gt;This text is not an account of how AI changed someone's life (please don't). It's a description of a workflow that I believe many mobile application engineers haven't yet experienced, with its friction points and parts that didn't work. If you work with iOS at a company, as an indie developer, or as a freelancer, this could be a valuable investment of a few hours (or minutes) in tools that will yield benefits when something on the device malfunctions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow, drawn out
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fml45ezfnz2dc3muzfbkl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fml45ezfnz2dc3muzfbkl.png" alt=" " width="800" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The unusual feature of this diagram is that &lt;strong&gt;everything except the Developer node and the artifacts is accessible from a single chat session&lt;/strong&gt;. Claude reads &lt;code&gt;.memgraph&lt;/code&gt; files via &lt;code&gt;leaks&lt;/code&gt;, controls the simulator through XcodeBuildMCP, and opens PRs via &lt;code&gt;gh&lt;/code&gt;. My job is to (a) capture artifacts that require physical access to a device, (b) review proposed changes, and (c) ensure the integrity of the LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Three tools, all pre-configured:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code in the terminal&lt;/strong&gt;, pointing to the iOS repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XcodeBuildMCP&lt;/strong&gt;, a Sentry Model Context Protocol server that exposes approximately 60 Apple development tools to LLM (build workspace, run in simulator, touch UI elements, capture screenshots, attach LLDB, stream logs). Installed via npm, with a single line of MCP client configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;xctrace&lt;/code&gt;&lt;/strong&gt;, the CLI for Instruments. Logs Time Profiler traces on a physical device and exports them to XML, which LLM can parse.
Plus the things macOS gives you for free: &lt;code&gt;leaks&lt;/code&gt;, &lt;code&gt;heap&lt;/code&gt;, &lt;code&gt;vmmap&lt;/code&gt;, &lt;code&gt;atos&lt;/code&gt;, &lt;code&gt;xcrun simctl&lt;/code&gt;. The CLI tooling for iOS performance is a lot better than people think.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How it actually went
&lt;/h2&gt;

&lt;p&gt;I'm going to describe what happened more or less in the order in which it occurred. Not a polished retrospective. The genuine story, including the dead end where I got lost for two hours before getting back on track.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1. Reframing the problem (5 minutes)
&lt;/h3&gt;

&lt;p&gt;I started the conversation with a short sentence: "Opening the SaveItems location details is slow after about 15 cycles." Claude consulted existing memory entries from previous sessions (project-specific information about the source code, file layout, and naming conventions) and immediately questioned the explanation: "Slow how? Memory leak, instance buildup? Or, in real time, does each opening seem to take longer?"&lt;/p&gt;

&lt;p&gt;This is a useful first step. "Slow" is a symptom; the problem lies in retention (objects accumulating) or computation (task taking too long). There are different tools for each case. I described the symptom in observable terms: "after 15 openings, the next one takes about 6 seconds, and the Memory Graph shows 12 active instances of &lt;code&gt;DetailViewModel&lt;/code&gt; after 12 cycles". And the path forked: investigate the leak first, as it is the simplest problem, and we have the ground truth (the Memory Graph has already shown us that the instances are not being released).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2. Reproducing on the simulator, hands-off (10 minutes)
&lt;/h3&gt;

&lt;p&gt;This is where MCP earns its keep. Claude built and ran the app on the iOS Simulator via a single tool called:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;mcp__xcodebuildmcp__build_run_sim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I used the app: I tapped the Saved Items tab using the accessibility-adapted touch commands, tapped a location card, tapped back, tapped another card, and repeated the process. I captured screenshots between steps so the app could check (and so I could see in the chat history) what state it was in. There were about 12 opening/closing cycles without my interference.&lt;/p&gt;

&lt;p&gt;With MCP running, I monitored the Xcode console and copied the relevant output to the chat. It's a bit worse than being fully automated, but it works.&lt;br&gt;
By minute 30, we had a confirmed reproduction and ROOT CYCLE candidates from a Memory Graph file I'd exported and dropped on the Desktop.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3. &lt;code&gt;leaks ~/Desktop/x.memgraph&lt;/code&gt; (10 minutes that ended the leak)
&lt;/h3&gt;

&lt;p&gt;Here's the part that ended a &lt;em&gt;lot&lt;/em&gt; of speculation in two minutes. I exported a &lt;code&gt;.memgraph&lt;/code&gt; from Xcode (Debug ➜ View Debugging ➜ Capture View Hierarchy ➜ Memory Graph ➜ File ➜ Export Memory Graph). Saved it to &lt;code&gt;~/Desktop/example-leaks.memgraph&lt;/code&gt;. Sent the path to Claude.&lt;/p&gt;

&lt;p&gt;It ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;leaks ~/Desktop/example-leaks.memgraph 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"ROOT CYCLE|DetailViewModel"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-40&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And produced the chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ROOT CYCLE: SwiftUI._DictionaryStorage&amp;lt;AnyHashable, WeakBox&amp;lt;AnyLocationBase&amp;gt;&amp;gt;
  → TagIndexProjection&amp;lt;Int&amp;gt;
    → ForEachState&amp;lt;MediaGalleryItem...&amp;gt;
      → Closure context (.onImageSliderTap)
        → ._viewModel.wrappedValue → DetailViewModel
        → ._coordinator → DetailsCoordinator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a SwiftUI internal observation graph that holds a closure capturing &lt;code&gt;self&lt;/code&gt; from inside a photo carousel &lt;code&gt;ForEach&lt;/code&gt; with a &lt;code&gt;.tag(Int)&lt;/code&gt; modifier on items. The closure was &lt;code&gt;onImageSliderTap&lt;/code&gt;, passed into &lt;code&gt;MediaCarouselHeaderView&lt;/code&gt;. It captured &lt;code&gt;self&lt;/code&gt; strongly, which retained the entire view's &lt;code&gt;@ObservedObject viewModel&lt;/code&gt; and &lt;code&gt;@State coordinator&lt;/code&gt; backings forever.&lt;/p&gt;

&lt;p&gt;I'd never seen &lt;code&gt;TagIndexProjection&amp;lt;Int&amp;gt;&lt;/code&gt; before. Wouldn't have guessed &lt;code&gt;.tag()&lt;/code&gt; caused this. The CLI told me directly. Without &lt;code&gt;leaks&lt;/code&gt;, I would have spent another four hours auditing closures.&lt;/p&gt;

&lt;p&gt;The fix was 15 lines. I hoisted &lt;code&gt;handlePhotoTap&lt;/code&gt; to &lt;code&gt;static&lt;/code&gt;, captured &lt;code&gt;[weak viewModel, weak coord = self.coordinator]&lt;/code&gt; instead of relying on implicit &lt;code&gt;self&lt;/code&gt;. Re-captured a fresh &lt;code&gt;.memgraph&lt;/code&gt;. Zero ROOT CYCLEs containing my classes. &lt;strong&gt;Done.&lt;/strong&gt; 🎉&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4. The dead end I'd have walked into anyway (90 minutes. Pause here.)
&lt;/h3&gt;

&lt;p&gt;After the leak fix, the screen still felt slow. The next obvious hypothesis: &lt;code&gt;fullScreenCover&lt;/code&gt; tears down the SavedItems' SwiftUI tree on dismiss, the parent re-renders a 50-cell grid with &lt;code&gt;AsyncImage&lt;/code&gt;s, that's why the next open is laggy.&lt;/p&gt;

&lt;p&gt;Easy test: swap &lt;code&gt;.fullScreenCover(item:)&lt;/code&gt; for &lt;code&gt;.sheet(item:)&lt;/code&gt;. One-line change. Ran it on the simulator, captured a fresh Memory Graph. Same slowness. Comparable counts. Hypothesis rejected.&lt;/p&gt;

&lt;p&gt;This is where my workflow provided me with something I want to highlight specifically: &lt;strong&gt;my hypothesis was wrong, and the test cost 10 minutes instead of half a day.&lt;/strong&gt; I changed one line, Claude recompiled, ran the simulator, captured the artifact, performed the analysis, and provided me with the results.&lt;/p&gt;

&lt;p&gt;Reverting the change was easy (Git is wonderful). Total cost of the error: less than fifteen minutes. With a manual workflow, this experiment would have required an hour of work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5. Time Profiler, comparison-first (45 minutes)
&lt;/h3&gt;

&lt;p&gt;The pivot: if presentation isn't the bottleneck, the cost has to be in the &lt;em&gt;work&lt;/em&gt; each open does. I needed CPU samples.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;xctrace&lt;/code&gt; Time Profiler against my iPhone, attached to the running app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcrun xctrace record &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template&lt;/span&gt; &lt;span class="s1"&gt;'Time Profiler'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--device&lt;/span&gt; &amp;lt;UDID&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attach&lt;/span&gt; DemoApp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--time-limit&lt;/span&gt; 90s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; ~/Desktop/saveditems-tti-device.trace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I drove the device manually for 90 seconds. Same flow, six places opened and closed, plus scrolling. Then a second pass on Browse (the fluid baseline) for comparison.&lt;/p&gt;

&lt;p&gt;I exported the &lt;code&gt;time-profile&lt;/code&gt; schema of each &lt;code&gt;.trace&lt;/code&gt; (this part of &lt;code&gt;xctrace&lt;/code&gt; works via &lt;code&gt;--xpath&lt;/code&gt;, unlike the Leaks data) to Claude, and it wrote a small analyzer in Python to count the frames that include the main thread and generated this side-by-side comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frame in &lt;code&gt;DemoApp&lt;/code&gt; binary&lt;/th&gt;
&lt;th&gt;Browse&lt;/th&gt;
&lt;th&gt;SavedItems&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GraphQLClient.init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7.7%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;NetworkConnectivityChecker.init&lt;/code&gt; ➜ &lt;code&gt;CTTelephonyNetworkInfo.init&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18.5%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*Grid.body.getter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9.9%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ActionsFactory.SavedItemsContext.make&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;According to the &lt;code&gt;potential-freezes&lt;/code&gt; schema, SavedItems experienced 35 freezes lasting more than 250 ms in 90 seconds, totaling 21.97 seconds of freezing. Browse (another part of the application that uses the same components and some of the same structures; I used it for comparison) experienced 6 freezes, totaling 2.87 seconds. The main thread of SavedItems was hangs for 24% of the time during normal use.&lt;/p&gt;

&lt;p&gt;The stack told a clean story. Every &lt;code&gt;GridItemView&lt;/code&gt; body recompute was building fresh &lt;code&gt;ItemActionsViewModel&lt;/code&gt; instances per cell, each one allocating a fresh &lt;code&gt;GraphQLClient&lt;/code&gt;, each one allocating a fresh &lt;code&gt;CTTelephonyNetworkInfo&lt;/code&gt; (a &lt;code&gt;CoreTelephony&lt;/code&gt; class with a documented 30 to 100ms allocation cost on iOS). Multiply by N visible cells × every recompute. Main thread freezes everywhere.&lt;/p&gt;

&lt;p&gt;Claude grepped the relevant factory file and found the smoking gun: four out of five &lt;code&gt;*Context.make&lt;/code&gt; enums in &lt;code&gt;ActionsFactory.swift&lt;/code&gt; use &lt;code&gt;ViewModelCache.shared.getOrCreateViewModel(...)&lt;/code&gt;. The fifth, &lt;code&gt;SavedItemsContext.make&lt;/code&gt;, bypasses the cache and creates new VMs unconditionally.&lt;/p&gt;

&lt;p&gt;Fix: A 50 lines mirroring the existing cache pattern in &lt;code&gt;BrowseContext.make&lt;/code&gt;. This allowed me to recapture a trace using Time Profiler.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hangs &amp;gt;250ms&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total hang time&lt;/td&gt;
&lt;td&gt;21.97s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GraphQLClient.init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;23.7%&lt;/td&gt;
&lt;td&gt;7.9% (parity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CTTelephonyNetworkInfo.init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;18.5%&lt;/td&gt;
&lt;td&gt;6.1% (parity)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;🚀&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6. Stacked PRs and app-wide cleanup (30 minutes)
&lt;/h3&gt;

&lt;p&gt;By this point, three independent fixes had emerged: the leak (a PR), the cache parity (a second PR added to the first), and a third that encompassed the entire application. Even after parity, both SavedItems and Browse were consuming about 6% of the main thread in &lt;code&gt;CTTelephonyNetworkInfo.init&lt;/code&gt;, because the convenience init of &lt;code&gt;GraphQLClient&lt;/code&gt; created a new &lt;code&gt;NetworkConnectivityChecker&lt;/code&gt; each time. This &lt;code&gt;NetworkConnectivityChecker&lt;/code&gt; should be a singleton across the entire application, not just in SavedItems.&lt;/p&gt;

&lt;p&gt;Claude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Used &lt;code&gt;gh pr create --base feature/leak-fix-001&lt;/code&gt; to stack PR #4406 on top of PR #4405.&lt;/li&gt;
&lt;li&gt;Opened PR #4407 against &lt;code&gt;dev&lt;/code&gt; directly (independent change).&lt;/li&gt;
&lt;li&gt;Wrote each PR description with the before/after tables embedded. When the Time Profiler validation showed the cache fix alone was sufficient (the originally planned 5-step migration was no longer needed), it dropped the unnecessary steps from the PR scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I reviewed each PR and each commit message before it went out. The total typing I did on those was about 200 characters of confirmation. Everything was draft-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for me
&lt;/h2&gt;

&lt;p&gt;A few things stand out, and they're not all positive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The not-so-good first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The CLI for iOS perf investigation is great, but it's &lt;em&gt;brittle&lt;/em&gt;. &lt;code&gt;xctrace --template Leaks --attach&lt;/code&gt; silently produces empty data due to a &lt;code&gt;libmalloc not initialized&lt;/code&gt; error that you only see if you dig into a SQLite file inside the trace bundle. Some custom logging SDKs don't show in &lt;code&gt;simctl log stream&lt;/code&gt;. SourceKit gets confused after &lt;code&gt;tuist generate&lt;/code&gt; and reports false-positive errors. I had to know about all of these. Claude doesn't always. And the time savings depend on my catching the wrong path before going deep.&lt;/p&gt;

&lt;p&gt;On the other hand, a lot of intuition in mobile engineering is also wrong, and the LLM is faster than I am at testing wrong intuitions. My hypothesis of &lt;code&gt;.fullScreenCover ➜ .sheet&lt;/code&gt; was a wrong path that I would have followed even further without the cheap experimentation cycle. The retention cycle hypothesis I started with ("audit each closure looking for &lt;code&gt;[weak self]&lt;/code&gt;") was also wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Treating each artifact (&lt;code&gt;.memgraph&lt;/code&gt;, &lt;code&gt;.trace&lt;/code&gt;, screenshot) as a &lt;em&gt;programmable input&lt;/em&gt; changes the loop. Memory Graph isn't "open Xcode and stare at the sidebar". It's &lt;code&gt;leaks ~/path.memgraph 2&amp;gt;&amp;amp;1 | grep ROOT CYCLE&lt;/code&gt; piped through Python that an LLM can write inline. Time Profiler isn't "scrub through the timeline in Instruments GUI". It's &lt;code&gt;xctrace export --xpath '/trace-toc/run/data/table[@schema="time-profile"]'&lt;/code&gt; and a 30-line parser. Once the artifacts are in CLI form, the LLM is genuinely useful.&lt;/p&gt;

&lt;p&gt;The LLM is &lt;em&gt;especially&lt;/em&gt; good at the boring parts. Writing a Python parser to fold thousands of stack frames into a top-20 inclusive table is exactly the kind of task it's fast at. Producing a side-by-side comparison table for a PR description with consistent formatting? Same. The stuff that's not &lt;em&gt;intellectually&lt;/em&gt; hard but is &lt;em&gt;attention-tax&lt;/em&gt; hard.&lt;/p&gt;

&lt;p&gt;I keep memory entries in &lt;code&gt;~/.claude/projects/&amp;lt;repo&amp;gt;/memory/&lt;/code&gt; for project-specific facts: the &lt;code&gt;ViewModelCache&lt;/code&gt; pattern, the &lt;code&gt;CTTelephonyNetworkInfo&lt;/code&gt; allocation-cost trap, and the SwiftUI &lt;code&gt;TagIndexProjection&lt;/code&gt; pitfall. Next time someone (me or a colleague who picks up the workflow) starts a similar investigation, the LLM begins with that context instead of rediscovering it.&lt;/p&gt;

&lt;p&gt;I also wrote a slash command, &lt;code&gt;/perf-investigate&lt;/code&gt;, that captures the workflow as a checklist and rejects the natural temptations: don't propose architectural changes before a &lt;code&gt;.memgraph&lt;/code&gt; or &lt;code&gt;.trace&lt;/code&gt; exists, don't use &lt;code&gt;xctrace --template Leaks --attach&lt;/code&gt; because it doesn't work, weak-capture only the closure proven by the memgraph to be the cycle root (not all of them). The slash command is the discipline that keeps me out of dead ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capture the "fluid" baseline first.&lt;/strong&gt; When the symptom is "X feels slow", capture Time Profiler on X &lt;em&gt;and&lt;/em&gt; on a sibling feature that's known to be fluid. The comparison is ten times more informative than the absolute numbers. I almost skipped the Browse baseline. That comparison was what made the cache-miss diagnosis irrefutable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resist sizing the ticket to the size of the original plan.&lt;/strong&gt; I scoped the migration as a five-step refactor up front. The Time Profiler showed that step 1 alone closed the gap, and steps 2 to 5 were dropped. If your plan is "do A, then B, then C, then validate", validate after A and re-plan. Don't let the size of the original plan anchor the actual scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory Graph CLI is underused even by people who use the GUI version daily.&lt;/strong&gt; &lt;br&gt;
The Memory Graph debugger in Xcode is well known, but most devs never realize there are leaks, heap, and vmmap CLI tools that operate on .memgraph files and are fully scriptable. Combine that with an LLM in the loop, and you get a feedback cycle that most teams haven't tried.&lt;/p&gt;
&lt;h2&gt;
  
  
  I Packaged the Workflow
&lt;/h2&gt;

&lt;p&gt;After this investigation, I sat down and transformed the manual parts into an MCP server, &lt;code&gt;memorydetective&lt;/code&gt;. The first cut used 12 tools to cover the workflow above. By v1.8, it had grown to &lt;strong&gt;31 tools, 34 catalog resources, and 5 Investigation Prompts&lt;/strong&gt; covering the Instruments ecosystem.&lt;/p&gt;

&lt;p&gt;v1.8 in particular was born from a real regression. On macOS 26.x, &lt;code&gt;leaks --outputGraph&lt;/code&gt; aborts with &lt;code&gt;Failed to get DYLD info for task&lt;/code&gt; whenever the target was not launched with &lt;code&gt;MallocStackLogging=1&lt;/code&gt;. The new &lt;code&gt;bootAndLaunchForLeakInvestigation&lt;/code&gt; absorbs build + boot + install + launch with the pre-propagated env var for capture to work out of the box, and &lt;code&gt;captureMemgraph&lt;/code&gt; now returns a structured &lt;code&gt;workaroundNotice&lt;/code&gt; pointing to the &lt;code&gt;recordTimeProfile&lt;/code&gt; (Allocations) fallback when the regression hits anyway. The agent decides; The tools just stop lying about the failure mode.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;memgraph analysis: &lt;code&gt;analyzeMemgraph&lt;/code&gt;, &lt;code&gt;findCycles&lt;/code&gt;, &lt;code&gt;classifyCycle&lt;/code&gt; (which would have gotten the &lt;code&gt;TagIndexProjection&lt;/code&gt; cycle in 30 seconds, with fix hint), &lt;code&gt;findRetainers&lt;/code&gt;, &lt;code&gt;diffMemgraphs&lt;/code&gt;, &lt;code&gt;countAlive&lt;/code&gt;, &lt;code&gt;reachableFromCycle&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;cycle-semantic CI gating: &lt;code&gt;verifyFix&lt;/code&gt;, &lt;code&gt;compareTracesByPattern&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;xctrace coverage: &lt;code&gt;analyzeHangs&lt;/code&gt;, &lt;code&gt;analyzeAnimationHitches&lt;/code&gt;, &lt;code&gt;analyzeAllocations&lt;/code&gt;, &lt;code&gt;analyzeAppLaunch&lt;/code&gt;, &lt;code&gt;analyzeTimeProfile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;macOS unified logging: &lt;code&gt;logShow&lt;/code&gt;, &lt;code&gt;logStream&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;capture + boot/launch: &lt;code&gt;recordTimeProfile&lt;/code&gt;, &lt;code&gt;captureMemgraph&lt;/code&gt;, &lt;code&gt;bootAndLaunchForLeakInvestigation&lt;/code&gt; (single-call build + boot + launch with &lt;code&gt;MallocStackLogging=1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;verify-fix loop: &lt;code&gt;replayScenario&lt;/code&gt; (drives the simulator via tap/swipe/wait/type with a &lt;code&gt;repeat&lt;/code&gt; count for leaks that only appear after N iterations), &lt;code&gt;captureScenarioState&lt;/code&gt; (composite before/after snapshot: memgraph + screenshot + accessibility tree)&lt;/li&gt;
&lt;li&gt;discovery: &lt;code&gt;getInvestigationPlaybook&lt;/code&gt;, &lt;code&gt;listTraceDevices&lt;/code&gt;, &lt;code&gt;listTraceTemplates&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;retain-cycle visualization (Mermaid/DOT): &lt;code&gt;renderCycleGraph&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;leak detection in XCUITest to run in CI: &lt;code&gt;detectLeaksInXCUITest&lt;/code&gt; (experimental)&lt;/li&gt;
&lt;li&gt;bridge with Swift source via SourceKit-LSP: &lt;code&gt;swiftGetSymbolDefinition&lt;/code&gt;, &lt;code&gt;swiftFindSymbolReferences&lt;/code&gt;, &lt;code&gt;swiftSearchPattern&lt;/code&gt;, &lt;code&gt;swiftGetSymbolsOverview&lt;/code&gt;, &lt;code&gt;swiftGetHoverInfo&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cycle catalog covers SwiftUI (including Swift 6/&lt;code&gt;@Observable&lt;/code&gt;/ SwiftData/NavigationStack), Combine, Swift Concurrency (including AsyncSequence-on-self), UIKit, Core Animation, Core Data, the Coordinator pattern, RxSwift, and Realm. Every classification carries a &lt;code&gt;staticAnalysisHint&lt;/code&gt; pointing to the SwiftLint rule that would catch it in the parsing, or an explicit gap warning when there is no static rule. And a &lt;code&gt;fixTemplate&lt;/code&gt; with a Swift before/after snippet that can be directly adapted.&lt;/p&gt;

&lt;p&gt;It's Apache 2.0, it's on npm (&lt;code&gt;memorydetective@1.8.0&lt;/code&gt;), and it works with Claude Code, Claude Desktop, Cursor, Cline, Kiro, and (experimentally) GitHub Copilot Agent mode.&lt;/p&gt;

&lt;p&gt;Two ways to install. The classic MCP path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; memorydetective
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ~/.claude/settings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"memorydetective"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"memorydetective"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you're on Claude Code, the same workflow ships as a one-command plugin install (no JSON edit, no global npm):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add carloshpdoc/memorydetective-plugin
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;memorydetective@memorydetective-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This plugin wraps the same MCP server &lt;code&gt;memorydetective@^1.8&lt;/code&gt; (pulled via &lt;code&gt;npx&lt;/code&gt; under the hood) and also includes a slash command &lt;code&gt;/perf-investigate&lt;/code&gt; with the built-in discipline checklist (don't propose architectural changes before memgraph or trace exists, don't trust &lt;code&gt;xctrace --template Leaks --attach&lt;/code&gt;, weak-capture only the closure proven as the root of the cycle, etc.). Same workflow, less typing.&lt;/p&gt;

&lt;p&gt;Then you ask the LLM something like &lt;em&gt;"run leaks on &lt;code&gt;~/Desktop/myapp.memgraph&lt;/code&gt; and tell me what's leaking"&lt;/em&gt;. The agent calls &lt;code&gt;analyzeMemgraph&lt;/code&gt; ➜ &lt;code&gt;classifyCycle&lt;/code&gt; and you receive a structured diagnosis with a fix hint. Or you can use it via the shell: &lt;code&gt;memorydetective analyze ~/Desktop/myapp.memgraph&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What honestly &lt;em&gt;isn't&lt;/em&gt; solid yet in v1.8.0: sample-level Time Profile analysis is still fragile (&lt;code&gt;xctrace export&lt;/code&gt; of the &lt;code&gt;time-profile&lt;/code&gt; schema crashes on heavy, non-symbolized traces; the tool surfaces a workaround). Hang, and animation-hitches analysis are rock-solid. The &lt;code&gt;leaks --outputGraph&lt;/code&gt; regression on macOS 26.x is mitigated via &lt;code&gt;bootAndLaunchForLeakInvestigation&lt;/code&gt;, but not 100% resolved (&lt;code&gt;task_for_pid&lt;/code&gt; down there needs a fix from Apple); when the workaround fails, &lt;code&gt;captureMemgraph&lt;/code&gt; surfaces a structured fallback for Allocations.&lt;/p&gt;

&lt;p&gt;Memory Graph capture works for Mac apps + iOS simulator but not on physical devices (limitation of &lt;code&gt;leaks(1)&lt;/code&gt;, I can't fix it). &lt;code&gt;replayScenario&lt;/code&gt; and the &lt;code&gt;captureScenarioState&lt;/code&gt; UI tree sub-capture have a soft dependency on &lt;a href="https://github.com/cameroncooke/AXe" rel="noopener noreferrer"&gt;axe&lt;/a&gt; (&lt;code&gt;brew install cameroncooke/axe/axe&lt;/code&gt;); the rest of the plugin works without it. &lt;code&gt;detectLeaksInXCUITest&lt;/code&gt; is shipped but marked as experimental until real production runs validate the orchestration. The CHANGELOG is honest about all of this. See &lt;a href="https://github.com/carloshpdoc/memorydetective/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;.&lt;br&gt;
GitHub: &lt;a href="https://github.com/carloshpdoc/memorydetective" rel="noopener noreferrer"&gt;github.com/carloshpdoc/memorydetective&lt;/a&gt;. PRs welcome, especially new cycle patterns from real production leaks you've found.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try this
&lt;/h2&gt;

&lt;p&gt;A short afternoon of setup gets you the whole workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt; either via npm + MCP config (&lt;code&gt;npm install -g memorydetective&lt;/code&gt; plus 1 line of JSON in your client) or, on Claude Code, via one-line plugin install: &lt;code&gt;/plugin marketplace add carloshpdoc/memorydetective-plugin&lt;/code&gt; then &lt;code&gt;/plugin install memorydetective@memorydetective-plugin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Install Claude Code in the terminal if you haven't. Point it at one of your iOS projects.&lt;/li&gt;
&lt;li&gt;(Optional, but recommended) Install &lt;a href="https://github.com/getsentry/XcodeBuildMCP" rel="noopener noreferrer"&gt;XcodeBuildMCP&lt;/a&gt; for the simulator-driving parts. Pairs nicely.&lt;/li&gt;
&lt;li&gt;Spend 20 minutes learning Memory Graph + &lt;code&gt;leaks&lt;/code&gt; on a &lt;code&gt;.memgraph&lt;/code&gt; you generate yourself. Pick a known retain cycle in your codebase, or build a tiny &lt;code&gt;class A { var b: B }&lt;/code&gt; cycle in a playground and confirm &lt;code&gt;leaks&lt;/code&gt; finds it. Then run &lt;code&gt;memorydetective analyze&lt;/code&gt; on it and watch the cycle classified.&lt;/li&gt;
&lt;li&gt;Pick a real perf ticket. Don't reach for the Xcode UI first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The setup is small. The workflow is genuinely faster. The unfair advantage is that most of the iOS engineers I know haven't even tried this loop yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Looking back, the afternoon broke down like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wins&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three independent perf fixes shipped in one afternoon (one leak, one cache parity, one app-wide singleton).&lt;/li&gt;
&lt;li&gt;Cost of being wrong dropped from "half a day per hypothesis" to "10 minutes per hypothesis". That single shift mattered more than any individual fix.&lt;/li&gt;
&lt;li&gt;Memory Graph + &lt;code&gt;leaks&lt;/code&gt; CLI gave me a precise retain chain in seconds, instead of four hours of closure-auditing.&lt;/li&gt;
&lt;li&gt;PR housekeeping (descriptions, before/after tables, stack management) was off my plate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CLI tooling around iOS perf is brittle. Some templates silently produce empty data (&lt;code&gt;xctrace --template Leaks --attach&lt;/code&gt; is the worst offender). You have to know the workarounds, and the LLM doesn't always.&lt;/li&gt;
&lt;li&gt;LLM accepts proposing wholesale refactors of &lt;code&gt;[weak self]&lt;/code&gt; that don't fix the leak (the real cycle is usually in only one closure, not all of them) and even introduce bugs that need fixing: closures that become silent no-ops, lost asynchronous work, races. In worse cases, re-strongification via guard let self recreates the cycle in a different way. Discipline (the slash command /perf-investigate) is what keeps you out of this hole: weak-capture only the closure proven by Memgraph as the root of the cycle, not all of them.&lt;/li&gt;
&lt;li&gt;Some custom logging SDKs route around &lt;code&gt;os_log&lt;/code&gt;, which means MCP-driven log capture won't see them. You fall back to pasting from Xcode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I'd repeat&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Capture a "fluid" sibling feature as a baseline before reading any absolute numbers.&lt;/li&gt;
&lt;li&gt;Validate after step 1 of any plan, then re-plan. Don't let the original size of a ticket anchor the real scope.&lt;/li&gt;
&lt;li&gt;Treat artifacts (&lt;code&gt;.memgraph&lt;/code&gt;, &lt;code&gt;.trace&lt;/code&gt;) as programmable inputs, not GUI-only files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And you, what does your iOS perf investigation flow look like today? Are you using the Memory Graph CLI, or staring at the Xcode sidebar? I'd love to hear what tooling actually moves the needle for you.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and until next time. 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Memory Graph CLI (&lt;code&gt;leaks&lt;/code&gt;, &lt;code&gt;heap&lt;/code&gt;, &lt;code&gt;vmmap&lt;/code&gt;, &lt;code&gt;malloc_history&lt;/code&gt;, and &lt;code&gt;.memgraph&lt;/code&gt; files)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/xcode/gathering-information-about-memory-use" rel="noopener noreferrer"&gt;Apple Developer: Gathering information about memory use&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2021/10180/" rel="noopener noreferrer"&gt;WWDC21: Detect and diagnose memory issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/forums/thread/125133" rel="noopener noreferrer"&gt;Apple Developer Forums: Generating Memgraph with leaks tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.manpagez.com/man/1/leaks/osx-10.12.6.php" rel="noopener noreferrer"&gt;man &lt;code&gt;leaks(1)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blogs.halodoc.io/memgraph-detection-of-memory-issues-on-ios/" rel="noopener noreferrer"&gt;Halodoc: Memgraph, detection of memory issues on iOS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;[weak self]&lt;/code&gt; (mechanics, perf overhead, and re-strongification pitfalls)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.donnywals.com/when-to-use-weak-self-and-why/" rel="noopener noreferrer"&gt;Donny Wals: when to use &lt;code&gt;[weak self]&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.avanderlee.com/swift/weak-self/" rel="noopener noreferrer"&gt;SwiftLee: weak vs unowned&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.jacobstechtavern.com/p/swift-reference-counting" rel="noopener noreferrer"&gt;Jacob Bartlett: bits &amp;amp; side tables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;A deep technical companion piece on the actual leak (a SwiftUI &lt;code&gt;TagIndexProjection&amp;lt;Int&amp;gt;&lt;/code&gt; cycle through &lt;code&gt;_DictionaryStorage&amp;lt;AnyHashable, WeakBox&amp;lt;AnyLocationBase&amp;gt;&amp;gt;&lt;/code&gt;, with the full retain chain and the wrong wholesale-&lt;code&gt;[weak self]&lt;/code&gt; refactor I tried first) is coming next week. Different audience: pure technical, no AI workflow.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>ai</category>
      <category>productivity</category>
      <category>swift</category>
    </item>
  </channel>
</rss>
