It was two in the morning when a tester sent over evidence of an imminent crash: after backing in and out of a certain screen a dozen or so times, memory usage crept from 80MB all the way up to 600MB before the system finally killed the app. This kind of "slow-growth" leak is nearly impossible to spot by reading code — you need Instruments. The trouble was that our local Macs were permanently tied up running simulators and build jobs, so firing up yet another Instruments session sent the fans into overdrive and left us with noisy, unreliable data. Eventually we moved the whole workflow onto a cloud Mac mini instead. A dedicated physical machine with no neighbors competing for resources produced noticeably cleaner traces, so here's a walkthrough of exactly how we did it.
Why debug on a dedicated physical machine
Instruments itself carries a fair amount of CPU and memory overhead — the Leaks template periodically walks the heap, and Allocations records a call stack for every single allocation. If the host is a shared environment being time-sliced across multiple jobs, your timeline gets polluted with scheduling jitter that has nothing to do with your app, and the resulting curve is messy enough to produce false "growth points." The advantage of a dedicated physical machine is simple: nearly every memory jump you see maps directly to something your own code did, so you don't have to first rule out "is some other job stealing resources right now."
The scariest thing when hunting a memory issue isn't failing to find any clue — it's finding a pile of false clues. The noisier the environment, the more false clues you get.
Setup: screen sharing and version alignment
Once the cloud Mac mini boots, connect to it over the built-in screen sharing (VNC) first. Set the resolution to at least 1920x1080 — at lower resolutions, the Instruments timeline gets crushed into a tiny strip and you simply can't make out the detail.
# From your local terminal, confirm the instance is reachable via SSH first
ssh dev@<your-instance-address> "sw_vers -productVersion; xcodebuild -version"
# Connect with a VNC client using the address assigned to your instance (private/public)
open "vnc://dev@<your-instance-address>"
Once you're in, the first thing to do is check that the Xcode version matches your local dev machine — differences in the Instruments template library in particular can cause newer features (such as tracing related to Swift Concurrency) to go missing. Run xcode-select -p to confirm the command-line tools path is correct, then open Xcode → Open Developer Tool → Instruments.
Use the Leaks template to locate the leak first
Create a new profile, pick a device (either a real device connected through the cloud Mac mini, or a simulator to reproduce the issue first), and choose the Leaks template. Leaks periodically scans the heap in the background and flags in red any object that's still alive after its retain count has hit zero — an orphaned object.
Steps:
- Start recording, then repeatedly trigger the suspect action in the app (e.g. entering and leaving a screen) 15–20 times;
- Watch the Leaks track on the left — note which operation each red marker corresponds to in time;
- Click into the corresponding leak record; the Extended Detail panel on the right shows the allocation call stack, which takes you straight to the offending class and method.
Leaks' limitation is that it only reports "confirmed leaks." Cases like an NSTimer holding a strong reference, or a closure capturing self, where the reference chain is technically intact but logically should have been released, often slip right past Leaks. That's when you switch to Allocations.
Read the growth curve with Allocations
Create a new profile, pick the Allocations template, and check "Record Reference Counts." Repeat the same sequence of operations, clicking Mark Generation manually after each round — this drops a marker on the timeline, making it easy to compare "after each round, did memory settle back to its starting point or not."
The key thing to read: if the memory curve climbs in a stair-step pattern across multiple rounds, with each step roughly the same height, you can be fairly confident that some fixed object or array is accumulating something with every operation. Switch to the "All Heap Allocations" view, sort by Growth, and compare across generations — you'll be able to see directly which class's instance count keeps steadily rising.
| Observation | Leaks | Allocations |
|---|---|---|
| Detects closure/timer retain cycles | Weak | Strong |
| Speed of localization | Fast, flagged directly in red | Requires comparing multiple rounds |
| Best suited for | Initial triage | Precisely pinpointing the growth source |
Cross-check with Memory Graph
Once Instruments has flagged a suspect class, go back to Xcode and run Debug Memory Graph (the memory icon in Xcode's debug bar), triggering it after the same sequence of operations. Memory Graph draws out the object reference graph, and a purple exclamation mark flags a likely retain cycle. This step is a second confirmation of what Instruments found, and it's especially good at clearly showing "who's holding a strong reference to whom" — often pinpointing the exact line, something like a closure capture pattern such as self.completion = { self.doSomething() }, more intuitively than an Instruments call stack.
After fixing the issue, don't declare victory immediately — go back to Allocations and run the exact same sequence of operations again, confirming that the curve between Mark Generation points is now flat rather than stair-stepping upward. That's the point at which the fix is actually verified.
Common pitfalls and a checklist
- Always do a "warm-up" pass before recording (launch, login, and any other one-time allocations) — otherwise normal cold-start allocations get mistaken for a leak;
- Memory behavior in Release builds isn't identical to Debug builds — always double-check your final conclusion under a Release configuration;
- If the screen sharing session to the cloud Mac mini drops and reconnects, the Instruments session may get interrupted — before any long recording, run
caffeinate -ito keep the system from sleeping mid-capture; - After every fix, verify with the Allocations Generation-comparison method rather than concluding from a code review alone.
Frequently asked questions
Is a cloud Mac mini fast enough for Instruments profiling?
Yes. Instruments adds CPU and memory overhead itself; a dedicated physical machine with no neighbors gives cleaner samples than a shared VM, which matters when reading fine-grained timelines.
Does low screen sharing resolution hurt the workflow?
It reduces timeline readability. Set the VNC session to at least 1920x1080 and zoom into the specific time range in Instruments before capturing evidence.
Should I cancel the rental right after finding the leak?
Not yet. Apply the fix on the same instance first and confirm the allocation curve flattens before deciding whether to end the current billing period.
portal://order
Need one right now?
A dedicated physical Mac mini, billed by the day, ready to use in about 4 minutes from payment.