Tüm Yazılar
KategoriPerformance
Okuma Süresi
22 dk
Yayın Tarihi
2025-06-20
Kelime Sayısı
2.380kelime

Kahveni hazırla - bu içerikli bir makale!

Instruments ile iOS Performance Profiling: Tam Rehber

Özet

Xcode Instruments ile CPU, memory, disk I/O ve network profiling. Time Profiler, Allocations, Leaks ve Energy Log detaylı kullanım rehberi.

  • Instruments Time Profiler, Allocations, Leaks, Network, Energy, Core Animation araclarini kapsar.
  • Profiling Release konfigurasyonunda yapilmali; Debug'da optimizer kapali, sonuc yaniltici olur.
  • Generation Analysis ile ekran gecislerinde bellek artisi (Growth) izlenerek leak tespit edilir.
  • Custom Instruments Package ve os_signpost ile production-level profiling pipeline'i kurulur.
Instruments ile iOS Performance Profiling: Tam Rehber

# Instruments ile iOS Performance Profiling: Tam Rehber

Kullanıcılar uygulamanızın yavaş olduğunu hissettiklerinde, App Store'dan silip gidiyorlar. Performance sadece teknik bir metrik değil — kullanıcı deneyiminin temelidir. Bu rehberde Xcode Instruments'ın tüm gücünü keşfedecek ve production-ready profiling becerilerini kazanacaksınız.


İçindekiler


1. Instruments'a Giriş

Instruments, Apple'ın en güçlü profiling aracıdır. Xcode → Product → Profile (⌘I) ile başlatılır.

Temel Kavramlar

Kavram
Açıklama
Ne Zaman Kullanılır
Time Profiler
CPU kullanım analizi
UI donması, yavaş hesaplama
Allocations
Bellek tahsis takibi
Yüksek bellek kullanımı
Leaks
Bellek sızıntısı tespiti
Retain cycle şüphesi
Network
HTTP trafik analizi
Yavaş API, fazla istek
Energy Log
Pil tüketimi
Background drain
System Trace
Thread ve dispatch analizi
Deadlock, priority inversion
Core Animation
FPS ve render analizi
Scroll kasması, animasyon

İlk Profiling Oturumu

swift
1// ÖNEMLİ: Release konfigürasyonunda profile edin!
2// Debug mode optimizer kapalıdır, yanıltıcı sonuçlar verir.
3 
4// Scheme ayarı:
5// Product → Scheme → Edit Scheme → Profile → Build Configuration: Release
6 
7// Signpost ile özel bölümleri işaretleyin
8import os
9 
10let logger = Logger(subsystem: "com.app.performance", category: "profiling")
11let signposter = OSSignposter(logger: logger)
12 
13func loadData() {
14 let signpostID = signposter.makeSignpostID()
15 let state = signposter.beginInterval("DataLoad", id: signpostID)
16 
17 // Veri yükleme işlemi
18 fetchFromNetwork { data in
19 signposter.endInterval("DataLoad", state)
20 }
21}

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?


2. Time Profiler Mastery

Time Profiler, CPU'nun nerede vakit geçirdiğini gösterir. Ağır hesaplamalar, gereksiz main thread işleri ve bottleneck'ler burada yakalanır.

Temel Kullanım

swift
1// KÖTÜ: Main thread'de ağır iş
2func processImages(_ images: [UIImage]) -> [UIImage] {
3 return images.map { image in
4 // Bu satır Time Profiler'da kırmızı yanar
5 applyFilter(to: image)
6 }
7}
8 
9// İYİ: Background thread + signpost
10func processImagesOptimized(_ images: [UIImage]) async -> [UIImage] {
11 let signposter = OSSignposter()
12 let state = signposter.beginInterval("ImageProcessing")
13 
14 let result = await withTaskGroup(of: (Int, UIImage).self) { group in
15 for (index, image) in images.enumerated() {
16 group.addTask {
17 let processed = await self.applyFilterAsync(to: image)
18 return (index, processed)
19 }
20 }
21 
22 var results = [(Int, UIImage)]()
23 for await result in group {
24 results.append(result)
25 }
26 return results.sorted { $0.0 < $1.0 }.map { $0.1 }
27 }
28 
29 signposter.endInterval("ImageProcessing", state)
30 return result
31}

Call Tree Okuma Stratejisi

Strateji
Ayar
Amaç
Separate by Thread
Thread bazında analiz
Invert Call Tree
En pahalı fonksiyonu üstte göster
Hide System Libraries
Sadece kendi kodunu gör
Flatten Recursion
Recursive call'ları birleştir
Top Functions
Self + children toplam

Hang Detection

swift
1// iOS 16+ otomatik hang detection
2// Instruments → App Hang template
3 
4// Manual hang detection
5final class HangDetector {
6 private let watchdogQueue = DispatchQueue(label: "hangDetector")
7 private var lastPing = Date()
8 private let threshold: TimeInterval = 0.5 // 500ms
9 
10 func start() {
11 schedulePing()
12 scheduleCheck()
13 }
14 
15 private func schedulePing() {
16 DispatchQueue.main.async { [weak self] in
17 self?.lastPing = Date()
18 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
19 self?.schedulePing()
20 }
21 }
22 }
23 
24 private func scheduleCheck() {
25 watchdogQueue.asyncAfter(deadline: .now() + 0.2) { [weak self] in
26 guard let self else { return }
27 let interval = Date().timeIntervalSince(self.lastPing)
28 if interval > self.threshold {
29 self.reportHang(duration: interval)
30 }
31 self.scheduleCheck()
32 }
33 }
34 
35 private func reportHang(duration: TimeInterval) {
36 os_log(.fault, "Main thread hang: %.2f seconds", duration)
37 }
38}

3. Allocations Deep Dive

Memory allocation pattern'leri anlamak, OOM crash'lerini önlemenin anahtarıdır.

Generation Analysis

swift
1// Allocations → Mark Generation ile bellek artışını izleyin
2 
3// Her ekran geçişinde generation mark koyun:
4// 1. Ana ekran → Mark Generation A
5// 2. Detay ekrana git → Mark Generation B
6// 3. Geri dön → Mark Generation C
7// Generation C'de Growth varsa → bellek sızıntısı!
8 
9// Yaygın sızıntı: Closure capture
10class DetailViewController: UIViewController {
11 var onDismiss: (() -> Void)?
12 private var timer: Timer?
13 
14 override func viewDidLoad() {
15 super.viewDidLoad()
16 
17 // KÖTÜ: self'i strong capture ediyor
18 // timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
19 // self.updateUI()
20 // }
21 
22 // İYİ: weak capture
23 timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
24 self?.updateUI()
25 }
26 }
27 
28 deinit {
29 timer?.invalidate()
30 print("DetailVC deallocated ✅")
31 }
32 
33 private func updateUI() {
34 // UI güncelle
35 }
36}

Transient vs Persistent Allocations

Tip
Açıklama
Sorun Belirtisi
Persistent
Hâlâ bellekte
Growth > 0 = potansiyel leak
Transient
Oluşturulup serbest bırakılmış
Çok fazla = allocation churn
Dirty Memory
Değiştirilmiş sayfa
Sistem bunu sıkıştıramaz
Compressed
Sıkıştırılmış bellek
Erişimde decompress gerekir

Autorelease Pool Optimizasyonu

swift
1// Döngüde çok nesne oluşturuyorsanız
2func processLargeDataset(_ items: [Data]) -> [ProcessedItem] {
3 var results: [ProcessedItem] = []
4 results.reserveCapacity(items.count) // Önceden kapasite ayır
5 
6 for chunk in items.chunked(into: 100) {
7 autoreleasepool {
8 let processed = chunk.map { ProcessedItem(data: $0) }
9 results.append(contentsOf: processed)
10 }
11 // Her 100 öğeden sonra autorelease pool temizlenir
12 }
13 return results
14}
15 
16extension Array {
17 func chunked(into size: Int) -> [[Element]] {
18 stride(from: 0, to: count, by: size).map {
19 Array(self[$0..<Swift.min($0 + size, count)])
20 }
21 }
22}

4. Leaks Instrument

Retain cycle'lar en yaygın bellek sızıntısı nedenidir.

Retain Cycle Tespit Kalıpları

swift
1// Pattern 1: Delegate retain cycle
2protocol DataManagerDelegate: AnyObject { // AnyObject zorunlu!
3 func didUpdateData()
4}
5 
6class DataManager {
7 weak var delegate: DataManagerDelegate? // WEAK zorunlu!
8}
9 
10// Pattern 2: Closure retain cycle
11class NetworkService {
12 var completionHandler: ((Result<Data, Error>) -> Void)?
13 
14 func fetch(url: URL) {
15 URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
16 guard let self else { return }
17 if let data {
18 self.completionHandler?(.success(data))
19 }
20 // İşlem bitti, closure'ı temizle
21 self.completionHandler = nil
22 }.resume()
23 }
24}
25 
26// Pattern 3: NotificationCenter (iOS 9+ otomatik temizler ama explicit daha güvenli)
27class ObserverVC: UIViewController {
28 private var observers: [NSObjectProtocol] = []
29 
30 override func viewDidLoad() {
31 super.viewDidLoad()
32 let obs = NotificationCenter.default.addObserver(
33 forName: .dataDidChange, object: nil, queue: .main
34 ) { [weak self] notification in
35 self?.handleDataChange(notification)
36 }
37 observers.append(obs)
38 }
39 
40 deinit {
41 observers.forEach { NotificationCenter.default.removeObserver($0) }
42 }
43}

5. Network Profiling

swift
1// URLSession metrikleri toplama
2class NetworkMetricsCollector: NSObject, URLSessionTaskDelegate {
3 func urlSession(
4 _ session: URLSession,
5 task: URLSessionTask,
6 didFinishCollecting metrics: URLSessionTaskMetrics
7 ) {
8 for metric in metrics.transactionMetrics {
9 let dns = metric.domainLookupEndDate?.timeIntervalSince(
10 metric.domainLookupStartDate ?? Date()
11 ) ?? 0
12 let connect = metric.connectEndDate?.timeIntervalSince(
13 metric.connectStartDate ?? Date()
14 ) ?? 0
15 let ttfb = metric.responseStartDate?.timeIntervalSince(
16 metric.requestStartDate ?? Date()
17 ) ?? 0
18 
19 os_log(
20 "Network: DNS=%.3fs Connect=%.3fs TTFB=%.3fs Total=%.3fs",
21 dns, connect, ttfb,
22 metric.responseEndDate?.timeIntervalSince(
23 metric.fetchStartDate ?? Date()
24 ) ?? 0
25 )
26 }
27 }
28}
29 
30// Optimized URLSession configuration
31let config = URLSessionConfiguration.default
32config.httpMaximumConnectionsPerHost = 6
33config.timeoutIntervalForRequest = 15
34config.waitsForConnectivity = true
35config.urlCache = URLCache(
36 memoryCapacity: 20 * 1024 * 1024, // 20MB memory
37 diskCapacity: 100 * 1024 * 1024 // 100MB disk
38)

6. Energy Log & Pil Optimizasyonu

Bileşen
Yüksek Tüketim
Optimizasyon
CPU
>20% sürekli
Background task'ları QoS.utility ile
Network
Sık küçük istekler
Batch request, URLSession background
GPS
Continuous significant
significantLocationChange kullan
Display
120Hz sürekli
CADisplayLink adaptive
Bluetooth
Continuous scan
İhtiyaç olunca scan, timeout koy
swift
1// Adaptive frame rate
2class AdaptiveDisplayLink {
3 private var displayLink: CADisplayLink?
4 private var isAnimating = false
5 
6 func start(preferredFPS: Int = 60) {
7 displayLink = CADisplayLink(target: self, selector: #selector(update))
8 if #available(iOS 15.0, *) {
9 displayLink?.preferredFrameRateRange = CAFrameRateRange(
10 minimum: 30, maximum: Float(preferredFPS), preferred: Float(preferredFPS)
11 )
12 }
13 displayLink?.add(to: .main, forMode: .common)
14 }
15 
16 @objc private func update(_ link: CADisplayLink) {
17 // Render update
18 }
19 
20 func stop() {
21 displayLink?.invalidate()
22 displayLink = nil
23 }
24}

7. Core Animation Profiling

swift
1// Scroll performance optimizasyonu
2class OptimizedCell: UICollectionViewCell {
3 // 1. Opaque views kullan
4 override func awakeFromNib() {
5 super.awakeFromNib()
6 layer.shouldRasterize = true
7 layer.rasterizationScale = UIScreen.main.scale
8 contentView.isOpaque = true
9 backgroundColor = .systemBackground
10 }
11 
12 // 2. Image'ları background'da hazırla
13 func configure(with imageURL: URL) {
14 Task.detached(priority: .userInitiated) {
15 let (data, _) = try await URLSession.shared.data(from: imageURL)
16 guard let image = UIImage(data: data) else { return }
17 
18 // Downscale to cell size
19 let renderer = UIGraphicsImageRenderer(size: CGSize(width: 100, height: 100))
20 let resized = renderer.image { _ in
21 image.draw(in: CGRect(origin: .zero, size: CGSize(width: 100, height: 100)))
22 }
23 
24 await MainActor.run {
25 self.imageView.image = resized
26 }
27 }
28 }
29 
30 private let imageView = UIImageView()
31}

FPS Hedefleri

Senaryo
Hedef FPS
Kabul Edilebilir
Scroll
60 (120 ProMotion)
>50
Animasyon
60
>45
Geçiş
60
>30
İdle
Düşük
<10

8. System Trace & Thread Analizi

swift
1// Priority inversion önleme
2actor DataProcessor {
3 private let processingQueue = DispatchQueue(
4 label: "processing",
5 qos: .userInitiated, // Doğru QoS seviyesi
6 attributes: .concurrent
7 )
8 
9 func process(_ items: [DataItem]) async -> [Result] {
10 await withCheckedContinuation { continuation in
11 processingQueue.async {
12 let results = items.map { self.transform($0) }
13 continuation.resume(returning: results)
14 }
15 }
16 }
17 
18 private func transform(_ item: DataItem) -> Result {
19 // CPU-intensive dönüşüm
20 Result(item: item)
21 }
22}

9. Custom Instruments

swift
1// os_signpost ile custom trace noktaları
2import os
3 
4extension OSSignposter {
5 static let appFlow = OSSignposter(
6 subsystem: "com.app.flow",
7 category: "UserFlow"
8 )
9}
10 
11// Kullanım
12func userTappedPurchase() {
13 let id = OSSignposter.appFlow.makeSignpostID()
14 let state = OSSignposter.appFlow.beginInterval("Purchase", id: id)
15 
16 Task {
17 do {
18 let product = try await fetchProduct()
19 OSSignposter.appFlow.emitEvent("ProductFetched", id: id, "SKU: \(product.sku)")
20 
21 let result = try await purchase(product)
22 OSSignposter.appFlow.endInterval("Purchase", state, "Success: \(result.transactionId)")
23 } catch {
24 OSSignposter.appFlow.endInterval("Purchase", state, "Error: \(error.localizedDescription)")
25 }
26 }
27}

10. CI/CD Performance Regression

swift
1// XCTest performance tests
2final class PerformanceTests: XCTestCase {
3 func testScrollPerformance() throws {
4 let app = XCUIApplication()
5 app.launch()
6 
7 let options = XCTMeasureOptions()
8 options.iterationCount = 5
9 
10 measure(metrics: [
11 XCTCPUMetric(application: app),
12 XCTMemoryMetric(application: app),
13 XCTClockMetric()
14 ], options: options) {
15 let list = app.collectionViews.firstMatch
16 list.swipeUp(velocity: .fast)
17 list.swipeDown(velocity: .fast)
18 }
19 }
20 
21 func testLaunchPerformance() throws {
22 measure(metrics: [XCTApplicationLaunchMetric()]) {
23 XCUIApplication().launch()
24 }
25 }
26}

11. Production Monitoring

swift
1// MetricKit ile production metrikleri
2import MetricKit
3 
4class MetricSubscriber: NSObject, MXMetricManagerSubscriber {
5 func didReceive(_ payloads: [MXMetricPayload]) {
6 for payload in payloads {
7 // Launch time
8 if let launch = payload.applicationLaunchMetrics {
9 let resumeTime = launch.histogrammedApplicationResumeTime
10 logMetric("app_resume", histogram: resumeTime)
11 }
12 
13 // Memory
14 if let memory = payload.memoryMetrics {
15 let peak = memory.peakMemoryUsage
16 logMetric("peak_memory", measurement: peak)
17 }
18 
19 // Disk
20 if let disk = payload.diskIOMetrics {
21 logMetric("disk_writes", measurement: disk.cumulativeLogicalWrites)
22 }
23 }
24 }
25 
26 func didReceive(_ payloads: [MXDiagnosticPayload]) {
27 for payload in payloads {
28 if let hangs = payload.hangDiagnostics {
29 for hang in hangs {
30 logDiagnostic("hang", callStack: hang.callStackTree)
31 }
32 }
33 }
34 }
35 
36 private func logMetric(_ name: String, histogram: MXHistogram<some Unit>? = nil,
37 measurement: Measurement<some Unit>? = nil) {
38 // Analytics'e gönder
39 }
40 
41 private func logDiagnostic(_ type: String, callStack: MXCallStackTree) {
42 // Crash reporting'e gönder
43 }
44}

12. Performance Checklist

Launch Time

  • Cold launch < 400ms
  • Warm launch < 200ms
  • Lazy initialization kullan
  • +initialize ve +load'dan kaçın

Memory

  • Peak memory < 150MB (iPhone)
  • Leak-free navigation flow
  • Image downsampling aktif
  • Cache eviction politikası var

Rendering

  • 60 FPS scroll (ProMotion: 120)
  • Off-screen render yok
  • Blending minimize
  • shouldRasterize uygun yerde

Network

  • HTTP/2 aktif
  • Response caching var
  • Batch requests kullanılıyor
  • Background download/upload

Energy

  • Background'da CPU < 5%
  • Location accuracy uygun seviyede
  • Timer coalescing aktif
  • Idle state düşük tüketim

ALTIN İPUCU

Bu yazının en değerli bilgisi

Bu ipucu, yazının en önemli çıkarımını içeriyor.

Okuyucu Ödülü

Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:

Etiketler

#Instruments#Performance#Xcode#Profiling#Optimization#iOS
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

12+ yıllık deneyime sahip Lead Mobile Engineer. Swift, SwiftUI, Kotlin ve Flutter ile iOS, Android ve cross-platform mimarilerde uzman. Performanslı ve kullanıcı dostu mobil uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

İlgili İçerik