Tüm Yazılar
KategoriPerformance
Okuma Süresi
22 dk okuma
Yayın Tarihi
...
Kelime Sayısı
2.380kelime

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

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

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ı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu 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ş

Bunu da begenebilirsiniz