Section 15/161 menit

15. Real Use Cases

15. Real Use Cases

Use Case 1: Menemukan Memory Leak di UIKit

Skenario: Aplikasi mengonsumsi RAM terus bertambah setelah navigasi bolak-balik ke halaman detail. Instruments menunjukkan leak tapi tidak jelas dari mana.

swift
// Kode yang diduga mengalami leak
final class DetailViewController: UIViewController {
    var viewModel: DetailViewModel?
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Bug: closure capture strong self tanpa [weak self]
        viewModel?.dataPublisher
            .sink { data in
                self.updateUI(with: data)  // ← strong reference cycle!
            }
            .store(in: &cancellables)
    }
}
swift
# Langkah 1: Buka Instruments Leaks → konfirmasi DetailViewController leak

# Langkah 2: Pasang breakpoint di deinit
(lldb) breakpoint set --func-regex "DetailViewController.*deinit"

# Langkah 3: Navigasi ke detail, kembali ke list
# Jika breakpoint TIDAK dipicu → confirm object tidak di-deallocate (leak)

# Langkah 4: Cek retain count
(lldb) expr import ObjectiveC
(lldb) expr CFGetRetainCount(myViewController)
# Output: retain count tinggi → ada strong reference yang tidak dilepas

# Langkah 5: Cari siapa yang retain object
(lldb) watchpoint set variable myViewController --watch write
# Setiap kali retain count berubah, LLDB berhenti dan tunjukkan stack trace

# Langkah 6: Identifikasi leaking subscriber
(lldb) po myViewController.cancellables
# Lihat subscriber — apakah ada yang tidak di-cancel

# Langkah 7: Fix di kode
swift
// Setelah menemukan bug via LLDB, fix dengan [weak self]
viewModel?.dataPublisher
    .sink { [weak self] data in
        self?.updateUI(with: data)
    }
    .store(in: &cancellables)

// Verifikasi fix: breakpoint di deinit sekarang dipicu saat pop ViewController

Use Case 2: Debugging Race Condition di Swift Concurrency

Skenario: App kadang-kadang crash dengan "Fatal error: Unexpectedly found nil" di baris yang seharusnya aman. Crash tidak konsisten dan sulit direproduksi.

swift
// Kode yang diduga race condition
final class CacheManager {
    private var cache: [String: Data] = [:]  // ⚠️ tidak thread-safe

    func store(_ data: Data, for key: String) {
        cache[key] = data  // bisa race dengan read
    }

    func retrieve(for key: String) -> Data? {
        return cache[key]  // bisa race dengan write
    }
}

// Dipanggil dari multiple async contexts
Task {
    await networkManager.fetchImage(url: url1) { data in
        cacheManager.store(data, for: "img1")  // thread A
    }
}
Task {
    await networkManager.fetchImage(url: url2) { data in
        cacheManager.store(data, for: "img2")  // thread B — race!
    }
}
swift
# Aktifkan Thread Sanitizer di Edit Scheme → Run → Diagnostics

# Jalankan app — TSan akan mendeteksi race:
# WARNING: ThreadSanitizer: Swift access race on address 0x...
# Write of size 8 at 0x... by thread T3:
#   #0 Dictionary._rawIndex(forKey:) CacheManager.swift:8
#
# Previous read of size 8 at 0x... by thread T2:
#   #0 Dictionary.subscript.getter CacheManager.swift:13

# Di LLDB saat TSan pause:
(lldb) bt all          → lihat stack trace kedua threads yang race
(lldb) f 0             → pergi ke frame crash
(lldb) fr v            → lihat state variabel
(lldb) po cacheManager.cache    → inspect cache state

# Dari info LLDB, konfirmasi: dua thread mengakses cache bersamaan

# Fix: gunakan actor untuk thread safety
swift
// Fix menggunakan actor
actor CacheManager {
    private var cache: [String: Data] = [:]

    func store(_ data: Data, for key: String) {
        cache[key] = data  // actor isolation: hanya satu akses dalam satu waktu
    }

    func retrieve(for key: String) -> Data? {
        return cache[key]
    }
}

// Penggunaan — sekarang memerlukan await
let data = await cacheManager.retrieve(for: "img1")

Use Case 3: Debug Parsing Bug dengan Conditional Breakpoint

Skenario: JSON parser menghasilkan data salah untuk beberapa user, tapi tidak semua. Log sudah ada tapi terlalu banyak dan susah di-filter.

swift
// Parser yang dicurigai bermasalah
struct UserParser {
    func parse(from json: [String: Any]) throws -> User {
        guard let id = json["id"] as? String else {
            throw ParserError.missingField("id")
        }
        guard let name = json["name"] as? String else {
            throw ParserError.missingField("name")
        }

        // Bug diduga di sini: logic priority yang salah
        let rawPriority = json["priority"] as? Int ?? 0
        let priority = Priority(rawValue: rawPriority) ?? .medium

        return User(id: id, name: name, priority: priority)
    }
}
swift
# Skenario: ada 10.000 users di database, hanya beberapa yang salah priority

# TANPA conditional breakpoint: breakpoint di parse() → berhenti 10.000 kali
# DENGAN conditional breakpoint:

# Berhenti hanya jika rawPriority memiliki nilai tidak terduga
(lldb) breakpoint set --file UserParser.swift --line 12 \
       --condition "rawPriority != 0 && rawPriority != 1 && rawPriority != 2"

# Ketika berhenti, inspect:
(lldb) po json
# Output: {"id": "usr_999", "name": "Budi", "priority": 99}
# Terlihat: ada data dengan priority = 99 yang tidak valid di enum

# Modifikasi runtime untuk test fix hipotesis:
(lldb) expr let $safePriority = min(rawPriority, 2)
(lldb) po Priority(rawValue: $safePriority)
# Output: Optional(Priority.high) ← ini yang benar

# Lanjutkan execution dengan nilai yang diperbaiki:
(lldb) expr rawPriority = 2
(lldb) continue

# Setelah konfirmasi hipotesis, fix di kode:
swift
// Fix: clamp nilai priority ke range yang valid
let rawPriority = (json["priority"] as? Int ?? 0).clamped(to: 0...2)
let priority = Priority(rawValue: rawPriority) ?? .medium

Use Case 4: Debugging Crash di Production dengan dSYM

Skenario: Crash report dari Crashlytics/Firebase menunjukkan crash di framework code tanpa nama fungsi yang jelas.

swift
Thread 1 Crashed:
0  MyApp                   0x0000000104a3c210 0x104a00000 + 246288
1  MyApp                   0x0000000104a3b8f4 0x104a00000 + 243956
2  MyApp                   0x0000000104a12300 0x104a00000 + 75552
swift
# Langkah 1: Symbolicate dengan atos (command line)
xcrun atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp \
           -arch arm64 \
           -l 0x104a00000 \
           0x0000000104a3c210

# Output:
# NetworkManager.fetchUser(id:) (in MyApp) (NetworkManager.swift:87)

# Langkah 2: Reproduce dengan LLDB untuk analisis lebih dalam
# Buka crash report di Xcode: Window → Devices → lihat crash log
# Xcode otomatis symbolicate jika dSYM tersedia

# Langkah 3: Reproduce scenario di debug build
(lldb) breakpoint set --file NetworkManager.swift --line 87
# Pasang conditional breakpoint untuk reproduce kondisi crash

# Langkah 4: Di breakpoint, inspect state
(lldb) po self
(lldb) fr v
(lldb) po urlSession
# Konfirmasi: urlSession = nil (deallocated sebelum callback)

# Root cause: URLSession di-deallocate sebelum task selesai
swift
// Fix: pastikan URLSession retained selama task berjalan
final class NetworkManager {
    private let session: URLSession  // disimpan sebagai stored property

    init() {
        self.session = URLSession(configuration: .default)  // tidak ephemeral
    }

    func fetchUser(id: String) async throws -> User {
        let (data, _) = try await session.data(from: userURL(id: id))
        return try JSONDecoder().decode(User.self, from: data)
    }
}