Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2024 SwiftDeveloper Tools

WWDC24 · 33 min · Swift / Developer Tools

Analyze heap memory

Dive into the basis for your app’s dynamic memory: the heap! Explore how to use Instruments and Xcode to measure, analyze, and fix common heap issues. We’ll also cover some techniques and best practices for diagnosing transient growth, persistent growth, and leaks in your app.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

  • 0:00 — Introduction
  • 1:05 — Heap memory overview
  • 3:45 — Tools for inspecting heap memory issues
  • 7:40 — Transient memory growth overview
  • 10:34 — Managing autorelease pool growth in Swift
  • 13:57 — Persistent memory growth overview
  • 16:00 — How the Xcode memory graph debugger works
  • 20:15 — Reachability and ensuring memory is deallocated appropriately
  • 21:54 — Resolving leaks of Swift closure contexts
  • 24:13 — Leaks FAQ
  • 26:51 — Comparing performance of weak and unowned
  • 30:44 — Reducing reference counting overhead
  • 32:06 — Cost of measurement
  • 32:30 — Wrap up

Code shown on screen · 23 snippets

ThumbnailLoader.makeThumbnail(from:) implementation swift · at 10:01 ↗
func makeThumbnail(from photoURL: URL) -> PhotoThumbnail {
  validate(url: photoURL)
  var coreImage = CIImage(contentsOf: photoURL)!

  let sepiaTone = CIFilter.sepiaTone()
  sepiaTone.inputImage = coreImage
  sepiaTone.intensity = 0.4
  coreImage = sepiaTone.outputImage!

  let squareSize = min(coreImage.extent.width, coreImage.extent.height)
  coreImage = coreImage.cropped(to: CGRect(x: 0, y: 0, width: squareSize, height: squareSize))

  let targetSize = CGSize(width:64, height:64)
  let scalingFilter = CIFilter.lanczosScaleTransform()

  scalingFilter.inputImage = coreImage
  scalingFilter.scale = Float(targetSize.height / coreImage.extent.height)
  scalingFilter.aspectRatio = Float(Double(coreImage.extent.width) / Double(coreImage.extent.height))
  coreImage = scalingFilter.outputImage!

  let imageData = context.generateImageData(of: coreImage)

  return PhotoThumbnail(size: targetSize, data: imageData, url: photoURL)
}
ThumbnailLoader.loadThumbnails(with:), with autorelease pool growth issues swift · at 10:23 ↗
func loadThumbnails(with renderer: ThumbnailRenderer) {
  for photoURL in urls {
    renderer.faultThumbnail(from: photoURL)
  }
}
Simple autorelease example swift · at 10:33 ↗
print("Now is \(Date.now)") // Produces autoreleased .description String
Autorelease pool growth in loop swift · at 11:08 ↗
autoreleasepool {
  // ...
  
  for _ in 1...1000 {
    // Autoreleases into single pool, causing growth as loop runs
    print("Now is \(Date.now)")
  }
  
  // ...
}
Autorelease pool growth in loop, managed by nested pool swift · at 11:50 ↗
autoreleasepool {
  // ...
  
  for _ in 1...1000 {
    autoreleasepool {
      // Autoreleases into nested pool, preventing outer pool from bloating
      print("Now is \(Date.now)")
    }
  }
  
  // ...
}
ThumbnailLoader.loadThumbnails(with:), with nested autorelease pool growth issues fixed swift · at 12:16 ↗
func loadThumbnails(with renderer: ThumbnailRenderer) {
    for photoURL in urls {
        autoreleasepool {
            renderer.faultThumbnail(from: photoURL)
        }
    }
}
C++ class with virtual method cpp · at 17:27 ↗
class Coconut {
  Swallow *swallow;
  virtual void virtualMethod() {}
};
C++ class without virtual method cpp · at 17:40 ↗
class Coconut {
  Swallow *swallow;
};
ThumbnailRenderer.faultThumbnail(from:), caching thumbnails incorrectly swift · at 18:41 ↗
func faultThumbnail(from photoURL: URL) {
  // Cache the thumbnail based on url + creationDate
  let timestamp = UInt64(Date.now.timeIntervalSince1970) // Bad - caching with wrong timestamp
  let cacheKey = CacheKey(url: photoURL, timestamp: timestamp)

  let thumbnail = cacheProvider.thumbnail(for: cacheKey) {
    return makeThumbnail(from: photoURL)
  }
  images.append(thumbnail.image)
}
ThumbnailRenderer.faultThumbnail(from:), caching thumbnails correctly swift · at 19:28 ↗
func faultThumbnail(from photoURL: URL) {
  // Cache the thumbnail based on url + creationDate
  let timestamp = cacheKeyTimestamp(for: photoURL) // Fixed - caching with correct timestamp
  let cacheKey = CacheKey(url: photoURL, timestamp: timestamp)

  let thumbnail = cacheProvider.thumbnail(for: cacheKey) {
    return makeThumbnail(from: photoURL)
  }
  images.append(thumbnail.image)
}
Code creating reference cycle with closure context swift · at 22:19 ↗
let swallow = Swallow()
swallow.completion = {
  print("\(swallow) finished carrying a coconut")
}
PhotosView image loading code, with leak swift · at 23:11 ↗
// ...
let renderer = ThumbnailRenderer(style: .vibrant)
let loader = ThumbnailLoader(bundle: .main, completionQueue: .main)
loader.completionHandler = {
  self.thumbnails = renderer.images // implicit strong capture of renderer causes strong reference cycle
}
loader.beginLoading(with: renderer)
// ...
PhotosView image loading code, with leak fixed swift · at 23:40 ↗
// ...
let renderer = ThumbnailRenderer(style: .vibrant)
let loader = ThumbnailLoader(bundle: .main, completionQueue: .main)
loader.completionHandler = { [weak renderer] in
	guard let renderer else { return }

  self.thumbnails = renderer.images
}
loader.beginLoading(with: renderer)
// ...
Intentional leak of manually-managed allocation swift · at 24:24 ↗
let oops = UnsafeMutablePointer<Int>.allocate(capacity: 16)
// intentional mistake: missing `oops.deallocate()`
Loop over intentional leak of manually-managed allocations swift · at 25:12 ↗
for _ in 0..<100 {
  let oops = UnsafeMutablePointer<Int>.allocate(capacity: 16)
  // intentional mistake: missing `oops.deallocate()`
}
Nonreturning function which can see leaks of allocations owned by local variables swift · at 26:11 ↗
func beginServer() {
  let singleton = Server(delegate: self)
  dispatchMain() // __attribute__((noreturn))
}
Fix for reported leak in nonreturning function swift · at 26:22 ↗
static var singleton: Server?

func beginServer() {
  Self.singleton = Server(delegate: self)
  dispatchMain()
}
Weak reference example swift · at 27:21 ↗
weak var holder: Swallow?
Unowned reference example swift · at 27:43 ↗
unowned let holder: Swallow
Implicit use of self by method causes reference cycle swift · at 29:07 ↗
class ByteProducer {
  let data: Data
  private var generator: ((Data) -> UInt8)? = nil

  init(data: Data) {
    self.data = data
    generator = defaultAction // Implicitly uses `self`
  }

  func defaultAction(_ data: Data) -> UInt8 {
    // ...
  }
}
Break reference cycle cause day implicit use of self by method, using weak swift · at 29:25 ↗
class ByteProducer {
  let data: Data
  private var generator: ((Data) -> UInt8)? = nil

  init(data: Data) {
    self.data = data
    generator = { [weak self] data in
      return self?.defaultAction(data)
    }
  }

  func defaultAction(_ data: Data) -> UInt8 {
    // ...
  }
}
Break reference cycle cause day implicit use of self by method, using unowned swift · at 29:41 ↗
class ByteProducer {
  let data: Data
  private var generator: ((Data) -> UInt8)? = nil

  init(data: Data) {
    self.data = data
    generator = { [unowned self] data in
      return self.defaultAction(data)
    }
  }

  func defaultAction(_ data: Data) -> UInt8 {
    // ...
  }
}
Struct with non-trivial init/copy/deinit swift · at 31:14 ↗
struct Nontrivial {
  var number: Int64
  var simple: CGPoint?
  var complex: String // Copy-on-write, requires non-trivial struct init/copy/destroy
}

Resources