---
title: 'Test, Başarım ve Dağıtım'
source: 'https://academia.sh/tr/kurslar/android-uretim'
language: tr
updated: '2026-10-21T05:02:42+00:00'
license: 'CC BY-SA 4.0'
lessons: 19
outcomes:
    - 'Birim ve aygıt testlerini katmanlara göre kurar'
    - 'Başlangıç süresi ve akıcılık sorunlarını ölçerek giderir'
    - 'Uygulama paketini mağaza kanallarına yayımlar'
    - 'Bir ölçümün bütçesini hiçbir örnekte mi, örneklerin azında mı, çoğunda mı yoksa hepsinde mi aştığını ayırt eder'
---

# Test, Başarım ve Dağıtım

Bir uygulamayı üretime götüren her adım bir ölçümü bir bütçeyle karşılaştırır: birim testi bir işlevin attığı adımları, başlangıç ölçümü ilk ekrana kadar yapılan işi, çizim ölçümü bir karede yapılan işi, aşamalı yayın her aşamadaki çökmeleri. Tek ölçüm yanıltır; aynı ölçüm birden çok örnekte alınır. Kurs her düzeneği tek bir soruyla ölçer — bu ölçüm bütçesini kaç örnekte aşıyor? Birim, aygıt üstü ve arayüz testleri, test kararlılığı, statik çözümleme, başlangıç süresi, çizim akıcılığı, bellek, ağ ve pil profili, karşılaştırmalı ölçüm, kod küçültme, uygulama paketleri, sürüm yönetimi, sürekli tümleştirme, test kanalları, aşamalı yayın, mağaza incelemesi ve yayın sonrası izleme aynı çerçeveden geçirilir.

## Test

Saf Kotlin katmanlarının birim testleri, bağımlılıkların sahtelenmesi, aygıt üstü testler, bileşen tabanlı ve klasik arayüz testleri, bekleme ve eşzamanlılık kaynaklı kırılganlık, statik çözümleme.

### Birim Testleri

Source: https://academia.sh/tr/kurslar/android-uretim/birim-testleri

Birim testi saf Kotlin katmanındaki bir işlevin ürettiği sayıyı bir girdi kümesi için sabitler. Ders bir ölçümün bütçesini kaç örnekte aştığını dört yanıtla sınıflar: içinde, azında, çoğunda, hepsinde.

Önceki kurs bir olayın alıcıya kaç kopya ulaştığını saydı. O kursun ölçü ekseni burada sürmez. Bu kursta uygulama artık üretime götürülüyor ve üretime giden her adım bir ölçümü bir sınırla karşılaştırıyor: birim testi bir işlevin attığı adımları, aygıt üstü test bir bekleme döngüsünün tur sayısını, başlangıç ölçümü ilk ekrana kadar yapılan işi, aşamalı yayın her aşamadaki çökmeleri. Tek bir ölçüm yanıltır, çünkü tek bir girdide her şey yolunda görünebilir. Bu yüzden aynı ölçüm birden çok girdide alınır ve her girdi bir örnek olur.

Kursun tek sorusu bu dersten başlar: **bu ölçüm bütçesini kaç örnekte aşıyor?** Bütçe, bir ölçümün aşmaması beklenen sınırdır. Yanıtın dört biçimi var ve kurs boyunca adları değişmez. Hiçbir örnek bütçeyi aşmıyorsa ölçüm **içinde**dir. Aşan örnekler var ama örneklerin yarısından azıysa **azında**dır. Örneklerin en az yarısı aşıyor ama hepsi aşmıyorsa **çoğunda**dır. Her örnek aşıyorsa **hepsinde**dir. Dört yanıtın üst adı aşımdır; bir ölçümün aşımı denince bu dört yanıttan biri anlaşılır.

#### Ölçünün Kuralı

Aşımı etiket değil kural verir. Aşağıdaki çekirdek bir bütçeyi (`Budget`), bir ölçümün etiketini, bütçesini ve örnek listesini (`Overrun`) tanımlar. Örnek sayısını ve sınırı kesin olarak aşan örneklerin sayısını alır, sonra soruyu sırayla sorar. Sınıra eşit bir örnek aşım sayılmaz. Tam yarı da çoğunda sayılır: sekiz örnekten dördü aşıyorsa ölçüm azında değil çoğundadır.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`countOverruns` dört aşımı sabit sırayla sayar ve sıfır olan aşımı da basar. Kural iki şeyi görmez. Birincisi büyüklüktür: bütçeyi bir adımla aşan örnek ile on adımla aşan örnek aynı sayılır. İkincisi nedendir: yavaş bir algoritma, fazla bir çağrı ve fazla sıkı konmuş bir bütçe aynı aşımı verebilir. Büyüklüğü ve nedeni derste düzyazı söyler, kural söylemez.

Bir örnek listesini elle yazmak kuralı gösterir ama bir şey ölçmez. Bu kursta aşımı taşıyan her ölçüm, örneklerini bir model işlevinin dönüşünden alır: işlev bir girdiden bir iş birimi üretir ve örnek listesi bu işlevin her girdi için döndürdüğü sayılardır. İş birimi dersin işine göre adım, tur, istek ya da çökme olur; süre değildir. Bütçe de modelin girdisidir: derste bir bütçe koyarsak gerekçesini söyleriz ve onu bir platform eşiği gibi sunmayız.

**TA1.** Model, test çerçevesinin kendisini koşturmaz; bu ortamda çerçeve yoktur. Modelde bir testin ölçtüğü şey, bir işlevin bir girdi için attığı adım sayısıdır. Test sınıfı ise platform parçasıdır ve çıktı vermez.

#### Birim Testi Neyi Sabitler

Birim testi (unit test), uygulamanın saf Kotlin katmanındaki bir işlevi çalıştırır ve sonucunu bir beklentiyle karşılaştırır. Çalıştırılan işleve test hedefi (system under test), beklentiyi dile getiren çağrıya iddia (assertion) denir. Saf Kotlin katmanı ekranı, dosyayı, ağı ve saati bilmez; işlev aynı girdiyle her zaman aynı sonucu verir. Bu yüzden test aygıt ya da öykünücü olmadan, geliştirme makinesinde, çok kısa sürede koşar.

Bir iddia tek bir girdi için tek bir sayıyı sabitler. İşlevin davranışı ise girdiye göre değişir: bir girdide iki adım atan işlev başka bir girdide on adım atabilir. Bir girdi kümesi seçildiğinde birim testi, o kümedeki her girdi için sayıyı sabitleyen iddialar topluluğuna dönüşür. Platformda bu şöyle görünür.

```kotlin
// Platform kodu: ValidatorTest
import org.junit.Assert.assertEquals
import org.junit.Test

class ValidatorTest {
    @Test
    fun acceptsShortCode() {
        assertEquals(4, validateSteps("AB12"))
    }

    @Test
    fun stopsAtFirstBrokenCharacter() {
        assertEquals(2, validateSteps("A-1234"))
    }
}
```

Her `@Test` yöntemi bir girdiyi sabitler. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde iddia yerine kurala bakarız: örneklerin kaçı bütçeyi aşıyor.

Bu sorunun önceki kurslardaki iki soruyla karışması kolaydır. M30/K06'da bir test ile bir sürüm çiftini karşılaştırıp bu test doğru sürümü kusurlu sürümden ayırıyor mu diye sormuştuk. Orada ölçülen şey testin doğruluğuydu ve her ölçüm bir test ile bir sürümden oluşan bir çifte dayanıyordu. Burada çift yoktur: tek bir ölçüm, tek bir bütçe ve bir örnek listesi vardır. Soru testin doğruluğu değildir, bütçenin kaç kez aşıldığıdır.

#### Sekiz Örnek, Dört Arama

Bir uygulamanın saf Kotlin katmanında ürün kodu araması olsun. Kodlar 1'den 16'ya sıralı durur ve sekiz kod aranır. Dört arama yöntemi aynı bütçeyle ölçülür: bir arama en fazla dört adım atabilir. Dört adımı bir platform eşiği gibi sunmuyoruz; ürün ekranının bir aramaya ayırdığı payın modeldeki karşılığı olarak koyuyoruz.

`scanSteps` baştan tarar ve aranan kodun sırası kadar adım atar. `halveSteps` aralığı her adımda yarıya indirir. `filterSteps` aranan kodu bulsa da bütün listeyi dolaşır. `bucketSteps` kodu sekize bölümden kalanına göre bir kovaya ayırır ve yalnız o kovada arar. Her işlev aranan kodu bulmak için attığı adımı döndürür.

```kotlin
// Main.kt
fun scanSteps(items: List<Int>, target: Int): Int {
    var steps = 0
    for (item in items) {
        steps++
        if (item == target) return steps
    }
    return steps
}

fun halveSteps(items: List<Int>, target: Int): Int {
    var low = 0
    var high = items.size - 1
    var steps = 0
    while (low <= high) {
        steps++
        val mid = (low + high) / 2
        when {
            items[mid] == target -> return steps
            items[mid] < target -> low = mid + 1
            else -> high = mid - 1
        }
    }
    return steps
}

fun filterSteps(items: List<Int>, target: Int): Int {
    var steps = 0
    items.filter { steps++; it == target }
    return steps
}

fun bucketSteps(items: List<Int>, target: Int): Int =
    items.filter { it % 8 == target % 8 }.indexOf(target) + 1

fun main() {
    val codes = (1..16).toList()
    val targets = listOf(2, 5, 7, 9, 12, 14, 16, 3)
    println("aranan kodlar: $targets")

    val step = Budget("adim", 4)
    val overruns = listOf(
        Overrun("dogrusal-arama", step, targets.map { scanSteps(codes, it) }),
        Overrun("ikili-arama", step, targets.map { halveSteps(codes, it) }),
        Overrun("tam-suzme", step, targets.map { filterSteps(codes, it) }),
        Overrun("kovali-arama", step, targets.map { bucketSteps(codes, it) }),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
aranan kodlar: [2, 5, 7, 9, 12, 14, 16, 3]
dogrusal-arama: butce=adim<=4 | asan=6/8 -> cogunda
ikili-arama: butce=adim<=4 | asan=1/8 -> azinda
tam-suzme: butce=adim<=4 | asan=8/8 -> hepsinde
kovali-arama: butce=adim<=4 | asan=0/8 -> icinde
icinde: 1
azinda: 1
cogunda: 1
hepsinde: 1
```

Dört ölçümün her biri farklı bir aşım verdi: içinde 1, azında 1, çoğunda 1, hepsinde 1. Bu dağılım kursun bağlayıcı sayısıdır; örnek sayısı 8, bütçe 4 adımdır. Her ölçümün örnekleri, ilgili işlevin sekiz girdi için döndürdüğü listedir.

`dogrusal-arama` çoğundadır. 5, 7, 9, 12, 14 ve 16 kodlarının sırası dörtten büyüktür, 2 ve 3 ise sınırın altında kalır. Altı örnek sekizin yarısından fazladır. `ikili-arama` azındadır: on altı kodun en fazla beş adım gerektirdiği tek yer son koddur, yani yalnız 16 aşar. `tam-suzme` her örnekte 16 adım atar ve hepsindedir; aranan kod ilk sıralarda olsa da filtre listeyi sonuna kadar dolaşır. `bucketSteps` her kovaya iki kod koyar, bu yüzden hiçbir örnek iki adımı geçmez ve ölçüm içindedir.

Çoğunda ile hepsinde arasındaki fark sayıda değil, çıkarımdadır. Çoğundaysa bütçe yalnız bazı girdiler için tutmuyor; girdi seçimi önemli. Hepsindeyse hiçbir girdi bütçeyi tutturamıyor; işlevin kendisi bütçenin dışındadır.

#### Girdi Doğrulama: En Kötü Duruma Göre Bütçe

Birim testi yalnız arama için değil, her saf işlev için kurulur. Bir kampanya kodunu doğrulayan işlevi ele alalım. İşlev kodun karakterlerini sırayla gezer ve geçersiz bir karakter görür görmez durur. Bir kod en fazla altı karakterlidir; yani işlevin en kötü durumu altı adımdır. Bütçeyi bu en kötü duruma koymak, işlevin hiçbir girdide kendi sınırını aşmadığını sabitlemenin yoludur. İşlev bu derste yardımcı bir dosyada durur ve sonraki ölçümler onu çağırır.

```kotlin
// Validation.kt — kampanya kodunun karakterlerini sirayla gezer, gecersiz karakterde durur
fun validateSteps(code: String): Int {
    var steps = 0
    for (ch in code) {
        steps++
        if (!ch.isLetterOrDigit()) return steps
    }
    return steps
}
```

Sekiz kod seçtik. Kodlardan biri kısa, ikisi hemen başında bozuk, üçü altı karakterin tamamını gezecek kadar uzun. Kodların listesi aynı kalacak; yalnız bütçe değişecek.

```kotlin
// Main.kt — girdi dogrulamanin butcesi en kotu duruma gore
fun main() {
    val codes = listOf("AB12", "A-1234", "YAZ24", "123456", "K 9", "TR2026", "ZZ", "P7Q8R9")
    val steps = codes.map { validateSteps(it) }
    println("denenen kodlar: $codes")
    println("adimlar: $steps")

    val worst = Budget("adim", 6)
    val overrun = Overrun("kod-dogrulama", worst, steps)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
denenen kodlar: [AB12, A-1234, YAZ24, 123456, K 9, TR2026, ZZ, P7Q8R9]
adimlar: [4, 2, 5, 6, 2, 6, 2, 6]
kod-dogrulama: butce=adim<=6 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 0
```

Sekiz kodun adımları 4, 2, 5, 6, 2, 6, 2 ve 6 çıktı. Hiçbiri altıyı geçmedi, ölçüm içindedir. Altı adım atan üç kod bütçenin tam sınırındadır ve sınıra eşit örnek aşım sayılmaz. Bu ölçümün değeri bir sayı değil, bir sözleşmedir: işlev gelecekte değişir ve bir kod yedi adım atarsa, bu ölçüm o ana kadar içinde diyen bir testin kırılmasına yol açar. Birim testi bu yüzden bir kez yazılıp unutulmaz; her değişiklikte aynı sekiz girdi aynı bütçeyle yeniden ölçülür.

**TA2.** Bütçe olan 6 adım, bu işlev için en uzun kodun karakter sayısından türer. Platformda ürün ekibi başka bir en kötü durum seçebilir; model bu seçimi bir bütçe olarak koyar ve onu bir eşik iddiasına çevirmez.

#### Yaygın Yanılgılar

Birim testi etrafında üç yanılgı sürekli tekrar eder ve ölçüm her birine somut bir karşılık verir.

Birincisi, "test yeşilse işlev doğrudur" düşüncesidir. Yeşil test yalnız seçilen girdiler için sabitlenen iddiaların tuttuğunu söyler. Girdi kümesi dar seçilmişse en kötü durum kümenin dışında kalabilir. Bu derste bütçeyi en uzun kodun adım sayısına göre koymamızın nedeni budur: bütçe kümenin en zorlu üyesine dayanmazsa, `içinde` yanıtı bir şey söylemez.

İkincisi, "bütçe ne kadar sıkıysa o kadar iyi" düşüncesidir. Sıkı bir bütçe aşımı artırır, ama aşım sayısı yükseldi diye işlev kötüleşmez; yalnız sınır değişmiştir. Aynı işlev 6 adımlık bütçede içinde, 5 adımlık bütçede azında çıktı. İşlev aynıydı. Ölçülen şey işlevin kendisi değil, işlevle sınırın ilişkisiydi. Bu yüzden bir bütçe değeri her zaman bir gerekçeyle birlikte yazılır ve gerekçe değişince bütçe de gözden geçirilir.

Üçüncüsü, "birim testi yalnız hata arar" düşüncesidir. Burada test bir hatayı değil bir sayıyı sabitliyor. Sayı bir sonraki değişiklikte kımıldarsa test kırılır ve ekip bu değişimi fark eder. Hatasız bir işlevde bile adım sayısının sessizce artması, bütçeye yaklaşıldığının ilk işaretidir; test bu işareti yakalamak için vardır.

Üç yanılgının ortak yanı tek bir sonuçtan fazlasını okumaktır. Kural yalnız şunu söyler: sınır kesin olarak kaç örnekte aşıldı. Bunun ötesi, yani aşımın neden olduğu, ekibin işidir.

#### Sınırlayıcı Ölçüm

Bütçeyi sıkılaştıralım. İşlev değişmedi, kod listesi değişmedi; yalnız bütçe 6 adımdan 5 adıma indi. Aynı sekiz örnek yeni sınırla karşılaştırılacak.

```kotlin
// Main.kt — ayni islev, ayni kodlar, daha siki butce
fun main() {
    val codes = listOf("AB12", "A-1234", "YAZ24", "123456", "K 9", "TR2026", "ZZ", "P7Q8R9")
    val steps = codes.map { validateSteps(it) }

    val tight = Budget("adim", 5)
    val overrun = Overrun("kod-dogrulama-siki", tight, steps)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
kod-dogrulama-siki: butce=adim<=5 | asan=3/8 -> azinda
icinde: 0
azinda: 1
cogunda: 0
hepsinde: 0
```

Aynı işlev, aynı sekiz girdi ve aynı adımlar: 4, 2, 5, 6, 2, 6, 2, 6. Bütçe 5'e inince altı adım atan üç kod aşar. Sekiz örnekten üçü aşıyor ve üç, yarının altında kaldığı için ölçüm azındadır. Sayı bir önceki ölçümde sıfırdı. İşlevde tek bir satır değişmedi.

Bu bir çelişki değil, ölçünün kendisidir. Aşım yalnız işlevin değil, işlev ile bütçenin birlikte bir özelliğidir. Aynı işlev bir bütçede içinde, başka bir bütçede azında, daha da sıkı bir bütçede çoğunda ya da hepsinde olabilir. Bu yüzden bir ölçümün aşımı söylenirken bütçe de söylenir: "kod doğrulama içinde" tek başına bir bilgi değildir, "kod doğrulama, 6 adımlık bütçeyle içinde" bir bilgidir.

Kuralın görmediği iki şey burada da geçerlidir. Büyüklüğü görmez: beş adımlık bütçeyi bir adımla aşan üç kod, kural gözünde on adımla aşan üç kodla aynıdır. Nedeni de görmez: bu ölçümde aşımın nedeni işlev değildir, çünkü işlev aynı kalmıştır; neden bütçenin daha sıkı konmasıdır. Birim testi yazan biri bir ölçümün aşımını okurken önce işleve değil, bütçenin neye göre konduğuna bakmalıdır.

Dört aramadan iki ders çıkar. Birincisi, aynı bütçe dört işlevde dört aşım verebilir: bütçenin sabit olması işlevleri ayırır. İkincisi, aynı işlev iki bütçede iki aşım verebilir: işlevin sabit olması bütçeleri ayırır. Birim testi bu iki yönün ikisinde de bir şey sabitler. Girdi kümesini sabitleyerek işlevi, bütçeyi sabitleyerek beklentiyi kilitler.

#### Özet

- Bu dersin aşımı: içinde. Altı adım bütçeli girdi doğrulama sekiz kodun hiçbirinde bütçeyi aşmadı.
- Dersin ölçütü, bir ölçümün bütçesini kaç örnekte aştığı: dört aramada içinde 1, azında 1, çoğunda 1, hepsinde 1 çıktı; örnek sayısı 8, bütçe 4 adımdı.
- Birim testi bir girdi için değil bir girdi kümesi için sayı sabitler; saf Kotlin katmanındaki işlev aynı girdiyle aynı sonucu verir.
- Aynı işlev ve aynı sekiz kod, bütçe 6 adımdan 5 adıma inince içinden azına geçti: aşım işlevin değil, işlev ile bütçenin özelliğidir.
- Kural aşımın sayısını görür, büyüklüğünü ve nedenini görmez; ikisini düzyazı söyler.

#### Sonraki Adım

Bu derste ölçülen işlevler hiçbir dış kaynağa dokunmadı. Gerçek bir depo ise ağ kaynağını çağırır ve birim testi o çağrıyı istemez. Sıradaki ders, `Sahteleme`, bir bağımlılığı sahtesiyle değiştirmenin çağrı sayısını bütçenin kaç örneğinde aştığını ölçer.

### Sahteleme

Source: https://academia.sh/tr/kurslar/android-uretim/sahteleme

Sahteleme bir testte gerçek bağımlılığın yerine sahtesini koyar. Ders bir deponun her işlemde ağ kaynağına kaç kez gittiğini sayar ve sahte bağımlılığın neyi ölçtürüp neyi ölçtürmediğini gösterir.

Önceki derste ölçülen işlevler hiçbir dış kaynağa dokunmuyordu: girdi gelir, adım sayısı çıkardı. Gerçek bir depo ise bir ağ kaynağına gider ve birim testi o yolculuğu istemez. Test ağa çıkarsa yavaşlar, ağın durumuna bağlı kalır ve aynı girdiyle iki kez farklı sonuç verebilir. Çözüm bağımlılığı sahtesiyle değiştirmektir. Bu derste kursun sorusu aynı kalıyor: **bu ölçüm bütçesini kaç örnekte aşıyor?** Dört yanıt da aynı: içinde, azında, çoğunda, hepsinde. Ölçülen iş birimi bu derste bir ağ kaynağına giden çağrı sayısıdır.

Çekirdek önceki dersle aynıdır. `Budget` bir bütçenin adını ve sınırını taşır, `Overrun` bir ölçümün etiketini, bütçesini ve örnek listesini alır; sınırı kesin olarak aşan örnekleri sayıp aşımı seçer.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

#### Sahte Bağımlılığın Dört Türü

Test edilen sınıfın dışarıya bağlandığı her yere bağımlılık denir. Testte bu bağımlılığın yerine konan nesneye sahte bağımlılık (test double) deriz. Sahte bağımlılığın görevine göre dört türü vardır ve her biri ayrı bir adla anılır.

Kukla nesne (dummy) yalnız bir parametreyi doldurur; çağrılmaz, hiçbir yanıt vermez. Güdük (stub) önceden belirlenmiş bir yanıt verir: kime ne sorulursa sorulsun hazır yanıtı döner. Casus nesne (spy) çağrıları kaydeder ve sayar; gerçek davranışı sürdürebilir ya da hazır yanıt verebilir. Sahte nesne (mock) ise beklenen çağrıları baştan bilir ve beklenen çağrı gelmezse ya da fazla gelirse testi kırar. Bu dersin modelinde casus nesneyi ve güdüğü kuracağız; sahte nesneyi ise platform parçasında göreceğiz.

Kavramlar iç içe geçtiği için sınır koymak yararlıdır: güdük yanıtı belirler, casus nesne çağrıyı sayar. Güdük bir soruya "ne cevap verildi" yanıtını, casus nesne "kaç kez soruldu" yanıtını taşır. Bu derste ölçülen sayı ikincisidir: bir deponun ağ kaynağına kaç kez gittiği.

#### İki Çağrı, Bir Bütçe

Bir sipariş deposu düşünelim. Depo bir siparişi okurken ağ kaynağına önce bir tazelik denetimi için, sonra asıl veriyi almak için olmak üzere iki kez gider. Bu davranışın bir hata olup olmadığını ürün kararı verir; model davranışı olduğu gibi alır ve sayar. Ağ kaynağı bir arayüzdür. Üretimde ağa giden bir sınıf onu uygular; testte ise çağrıları sayan bir casus nesne onun yerine geçer.

```kotlin
// Source.kt — ag kaynagi sozlesmesi, cagrilari sayan casus nesne ve hazir yanit veren gudukleyen onbellek
interface RemoteSource {
    fun fetch(key: String): String
}

class SpySource(private val replies: Map<String, String>) : RemoteSource {
    var requestCount: Int = 0
        private set

    override fun fetch(key: String): String {
        requestCount++
        return replies[key] ?: "yok"
    }
}

class CacheStub(private val entries: Map<String, String>) {
    fun lookup(key: String): String? = entries[key]
}
```

`SpySource` her `fetch` çağrısında `requestCount` sayacını bir artırır; sayaç dışarıdan okunur ama yalnız sınıfın içinde yazılır. `CacheStub` ise yalnız hazır yanıtlarını döndürür ve hiçbir çağrıyı saymaz. Şimdi depoyu yazalım.

```kotlin
// OrderRepository.kt — her islemde ag kaynagini iki kez cagirir: biri tazelik denetimi
class OrderRepository(
    private val source: RemoteSource,
    private val cache: CacheStub = CacheStub(emptyMap()),
) {
    fun load(key: String): String {
        val cached = cache.lookup(key)
        if (cached != null) return cached
        source.fetch(key)
        return source.fetch(key)
    }
}
```

Depo önce önbelleğe bakar. Önbellek bir yanıt verirse ağa gitmez; vermezse iki kez gider. Varsayılan önbellek boştur, yani hiç yanıt vermez. Ölçüm işlevi de bir yardımcı dosyaya konur: her işlem için ayrı bir casus nesne kurar, depoyu çalıştırır ve o işlemde sayılan çağrıyı döndürür.

```kotlin
// Probe.kt — her islem icin ag kaynagina giden cagri sayisini olcer
fun callsPerOperation(keys: List<String>, stub: CacheStub = CacheStub(emptyMap())): List<Int> =
    keys.map { key ->
        val spy = SpySource(mapOf(key to "agdan"))
        OrderRepository(spy, stub).load(key)
        spy.requestCount
    }
```

Bu işlev bir girdi listesinden bir sayı listesi üretir; örnekleri `Overrun`'a verecek olan dönüş budur. Sekiz sipariş okunacak ve bütçe işlem başına bir çağrıdır. Bu bütçe, ekranın bir siparişi açarken ağa bir kez gitmesini isteyen bir ürün kararının modeldeki karşılığıdır; platform ya da ağ sağlayıcısı böyle bir sınır koymaz.

**TA1.** Model, sahteleme kitaplığını koşturmaz; bu ortamda yoktur. Casus nesne ve güdük elle yazılmış sınıflardır, kitaplığın ürettiği nesneler değildir. Bütçe olan işlem başına 1 çağrı modelin girdisidir.

```kotlin
// Main.kt
fun main() {
    val keys = listOf(
        "siparis-1", "siparis-2", "siparis-3", "siparis-4",
        "siparis-5", "siparis-6", "siparis-7", "siparis-8",
    )
    val calls = callsPerOperation(keys)
    println("cagri sayilari: $calls")

    val overrun = Overrun("depo-cagrisi", Budget("cagri", 1), calls)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
cagri sayilari: [2, 2, 2, 2, 2, 2, 2, 2]
depo-cagrisi: butce=cagri<=1 | asan=8/8 -> hepsinde
icinde: 0
azinda: 0
cogunda: 0
hepsinde: 1
```

Sekiz işlemin sekizi de iki çağrı yaptı ve her biri bütçeyi bir çağrıyla aştı. Ölçüm hepsindedir. Hepsinde, işlevin bütçenin dışında olduğunu söyler: girdiyi değiştirmek sonucu değiştirmez, çünkü aşımın kaynağı bir girdinin özelliği değil deponun kendi düzenidir. Kural büyüklüğü görmez, burada fark bir çağrıdır; düzyazıda söyleriz ki bu bir çağrı bir ağ yolculuğuna eşittir.

#### Çağrı Doğrulaması

Aynı sayıyı bir birim testi de sabitleyebilir. Casus nesne çağrıyı sayar ve test, sayacın beklenen değerde olduğunu doğrular. Sahte nesne kuran bir kitaplıkla bu şöyle yazılır.

```kotlin
// Platform kodu: OrderRepositoryTest
class OrderRepositoryTest {
    @Test
    fun loadAsksSourceOnce() {
        val source = mock<RemoteSource>()
        whenever(source.fetch("siparis-1")).thenReturn("tamam")

        OrderRepository(source).load("siparis-1")

        verify(source, times(1)).fetch("siparis-1")
    }
}
```

Parça iki iş yapar. `whenever` çağrısı bir güdük kurar: `fetch("siparis-1")` her sorulduğunda `"tamam"` döner. `verify` çağrısı bir çağrı doğrulamasıdır: kaynak tam bir kez sorulmuş olmalı. Depo kaynağı iki kez sorduğu için bu test kırılır. Kitaplığın ürettiği sahte nesne, modeldeki `SpySource` ve `requestCount`'ın platform karşılığıdır, ama aynı şey değildir. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Testin kırılması bir aşımdır: beklenen sayı bir, gerçek sayı iki.

Kuralı bir iddiayla karşılaştırırken bir fark var. İddia tek bir işlem için kırılır ya da kırılmaz; kural ise sekiz işlemin kaçında kırıldığını söyler. İddia hata verir, kural yaygınlığı verir. Bir testte hata görüp onu ölçmek istemeyen biri aynı testi sekiz girdide koşar ve aşımı okur.

#### Kitaplığın Satırları Ne Yapar

Platform parçasındaki beş satırın her biri ayrı bir iş yapar ve hangisinin hangi sahte bağımlılık türüne karşılık geldiğini bilmek, testin neyi sınadığını anlamanın yoludur.

`mock<RemoteSource>()` arayüzün çalışma anında üretilmiş sahte bir uygulamasını kurar. Bu nesnede hiçbir yöntem gerçek iş yapmaz; her çağrı karşılıksız döner ve kaydedilir. Modelde bunun yerini elle yazdığımız `SpySource` tutar, ama onun gibi sayaç değil, bir kayıt defteri taşır.

`whenever(source.fetch("siparis-1")).thenReturn("tamam")` satırı güdük kurar. İlk yarısı hangi çağrıdan söz edildiğini belirtir, ikinci yarısı o çağrıya hangi yanıtın verileceğini. Güdük bir kez kurulunca aynı çağrı kaç kez gelirse gelsin aynı yanıtı döner.

`OrderRepository(source).load("siparis-1")` test edilen sınıfın kendisidir. Sahte nesne kurucuya bağımlılık olarak verilir; depo bunun sahte olduğunu bilmez. Bağımlılığın dışarıdan verilmesi, yani kurucudan enjekte edilmesi, sahteleme için gerekli koşuldur. Depo kaynağı kendi içinde kursaydı testin araya girecek yeri olmazdı.

`verify(source, times(1)).fetch("siparis-1")` doğrulama satırıdır. Kitaplık kayıt defterine bakar ve `fetch("siparis-1")` çağrısının tam bir kez yapılıp yapılmadığını denetler. `times(1)` yerine başka bir sayı yazılsaydı beklenti de değişirdi. Bu satırın başarısızlığı, depodaki iki çağrının doğrudan sonucudur.

#### Sahte Nesnenin Sınırı

Çağrı doğrulaması güçlü bir araçtır ve tam bu yüzden dikkat ister. Test, deponun kaynağı hangi sırayla ve kaç kez çağırdığını sabitlediğinde davranışı değil uygulamayı sabitlemiş olur. Depo bir gün tazelik denetimini bırakır ve çağrı sayısını bire indirirse, davranış değişmemiştir ama iki çağrı bekleyen bir test kırılır. Test ekibi bu kırılmayı bir hata sanıp testi onarmak zorunda kalır.

Bu derste bütçeyi bire koymamızın nedeni tersine bir değişikliğe dayanır: ürün kararı bir çağrıyı istiyor ve test o kararı sabitliyor. Fark kararın kimden geldiğindedir. Bir bütçe ürün kararına dayanıyorsa aşımı bir bulgudur; yalnızca mevcut uygulamayı kopyalıyorsa aşımı bir gürültüdür. Ölçüm ikisini birbirinden ayıramaz, ayıran ekiptir.

#### Hangi Bağımlılık Hangi Türle Sahtelenir

Sahte bağımlılığın türünü seçerken sorulacak soru, testin o bağımlılıktan ne beklediğidir. Test yalnız bir yanıta ihtiyaç duyuyorsa güdük yeter: önbellek bir değer döndürsün, depo da o değeri iletsin. Test bir çağrının yapılıp yapılmadığını sınamak istiyorsa casus nesne ya da sahte nesne gerekir: ağa gidildi mi, kaç kez gidildi. Bağımlılık yalnız kurucunun imzasını doldurmak için varsa kukla nesne yeter ve hiçbir davranış yazılmaz.

Seçim yanlış yapılırsa test ya gereksiz yere karmaşıklaşır ya da yetersiz kalır. Çağrı sayısını sınamak isteyen bir test güdük kullanırsa sayıyı göremez; yalnız yanıt arayan bir test sahte nesne kurarsa, ilgisiz çağrıların doğrulamasıyla kırılır. Bu derste iki tür kullandık: ağ kaynağı çağrıyı saymak için casus nesneyle, önbellek yanıt vermek için güdükle sahtelendi. Her bağımlılık kendi görevine uygun türle sahtelendi.

#### Yaygın Yanılgılar

İlk yanılgı, "her bağımlılık sahtelenmelidir" düşüncesidir. Sahteleme bağımlılığı gerçek davranıştan koparır. Bağımlılık hızlı, deterministik ve yan etkisizse, örneğin saf bir hesap işlevi, onu sahtelemek testi yalnız uzatır ve gerçek davranışı sınamaktan alıkoyar.

İkinci yanılgı, "sahte nesne gerçek nesneden hızlıdır, o hâlde daima iyidir" düşüncesidir. Sahte nesne hızlıdır, ama sınadığı şey sahte nesnenin davranışıdır. Güdüğün eski bir yanıt verdiği ölçümde bunu gördük: çağrı sayısı sıfırdı, yanıt yanlıştı.

Üçüncü yanılgı, "çağrı sayısı başarımın ölçüsüdür" düşüncesidir. Çağrı sayısı bir ağ yolculuğunun sayısıdır; her yolculuğun süresi aynı değildir. İki çağrı bir bütçeyi aşabilir ve toplam süre yine de kısa kalabilir. Sayı bu derste bir iş birimi olarak kullanılıyor, süre olarak değil.

#### Sınırlayıcı Ölçüm

Bu ölçümün sınırlayıcısı güdüklü önbellektir. Gerekçesi şudur: ilk ölçümün hepsinde çıkması aşımın girdiden değil düzenden geldiğini gösterdi. Bunu doğrulamanın yolu düzeni değiştirip aynı girdiyi yeniden ölçmektir. Girdi, bütçe ve örnek sayısı sabit kalır; yalnız önbellek değişir. Böylece sonuçtaki fark başka bir etkene yüklenemez.

Şimdi aynı sekiz işlemi bir güdükle okuyalım. Güdük önbellek her anahtar için hazır bir yanıt verir. Depo önbellekte yanıt bulunca ağa gitmez.

```kotlin
// Main.kt — gudukleyen onbellekle ag cagrisi sifira iner
fun main() {
    val keys = listOf(
        "siparis-1", "siparis-2", "siparis-3", "siparis-4",
        "siparis-5", "siparis-6", "siparis-7", "siparis-8",
    )
    val stub = CacheStub(keys.associateWith { "onbellekten" })
    val calls = callsPerOperation(keys, stub)
    println("cagri sayilari: $calls")

    val overrun = Overrun("gudukle-depo", Budget("cagri", 1), calls)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
cagri sayilari: [0, 0, 0, 0, 0, 0, 0, 0]
gudukle-depo: butce=cagri<=1 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 0
```

Aynı depo, aynı sekiz anahtar, aynı bütçe. Ağ çağrısı sekizde de sıfıra indi ve ölçüm hepsindeden içinde yönüne geçti. Burada deponun davranışı değişmedi: depo hâlâ önbellek yanıt vermezse iki kez gider. Değişen, testin deponun o yoluna hiç girmemesidir.

Bu, sahteleme ölçümünün sınırıdır. Güdük yanıtı belirler, yani test gerçek ağın değil güdüğün yanıtını ölçer. Sıfır çağrı ağın hızlı olduğunu ya da deponun iyi çalıştığını söylemez; yalnız güdüğün her soruya cevap verdiğini söyler. Bunu bir ölçümle görelim: güdüğe eski bir değer koyalım, ağın ise yeni bir değer verdiğini bilelim.

```kotlin
// Main.kt — gudukle ag arasindaki fark kuralin gormedigi seydir
fun main() {
    val keys = listOf("siparis-1", "siparis-2", "siparis-3", "siparis-4")
    val stale = CacheStub(keys.associateWith { "eski" })
    val fresh = keys.associateWith { "yeni" }

    val fromStub = keys.map { OrderRepository(SpySource(fresh), stale).load(it) }
    val fromNetwork = keys.map { OrderRepository(SpySource(fresh)).load(it) }
    println("gudukten gelen yanitlar: $fromStub")
    println("ag yanitlari: $fromNetwork")

    val calls = callsPerOperation(keys, stale)
    println("cagri sayilari: $calls")
    val overrun = Overrun("gudukle-bayat", Budget("cagri", 1), calls)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
gudukten gelen yanitlar: [eski, eski, eski, eski]
ag yanitlari: [yeni, yeni, yeni, yeni]
cagri sayilari: [0, 0, 0, 0]
gudukle-bayat: butce=cagri<=1 | asan=0/4 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 0
```

Güdük dört işlemde de `eski` döndürdü; ağ aynı anahtarlarda `yeni` söylüyordu. Çağrı sayısı yine sıfır, ölçüm yine içinde. Kural çağrıyı sayar, yanıtın doğru olup olmadığına bakmaz. Bu bir hata değil, kuralın amacıdır; ama güdükle yazılmış bir testin yeşil olması, gerçek ağın aynı yanıtı vereceği anlamına gelmez. Bunu kapatan şey başka bir test türüdür: gerçek bağımlılığa yaklaşan bir sonraki derste göreceğimiz aygıt üstü testler.

**TA2.** Model güdüğü bir eşleme olarak kurar; gerçek bir sahteleme kitaplığı güdüğe çağrı sırasına göre farklı yanıtlar da verebilir, modelde bu yok.

#### Özet

- Bu dersin aşımı: hepsinde. Depo her işlemde ağ kaynağına iki kez gitti ve sekiz işlemin sekizi bir çağrılık bütçeyi aştı.
- Dersin ölçütü, bir işlemde ağ kaynağına giden çağrı sayısı: casus nesne sayar, güdük yanıtı belirler, kukla nesne yalnız parametreyi doldurur, sahte nesne beklenen çağrıyı doğrular.
- Güdüklenen önbellekle aynı sekiz işlemde çağrı sıfıra indi ve ölçüm hepsindeden içinde yönüne geçti; depo değişmedi, testin girdiği yol değişti.
- Güdük kendi yanıtını ölçtürür: ağ yeni değeri söylerken güdük eskiyi döndürdü ve çağrı sayısı yine sıfırdı.
- Çağrı doğrulaması bir işlem için kırılır ya da kırılmaz; kural aşımın kaç işlemde olduğunu söyler.

#### Sonraki Adım

Bu derste ağ kaynağının yerine geçen nesneler elle yazılmış sınıflardı ve hiçbiri bir aygıtta koşmadı. Aygıt üstü testler bunu tersine çevirir: sahte bağımlılık yerine gerçek bir ekran ve gerçek bir sistem kullanır, ama her test o ekranı baştan kurar. Sıradaki ders, `Aygıt Üstü Testler`, bu kurulumun bütçeyi kaç testte aştığını ölçer.

### Aygıt Üstü Testler

Source: https://academia.sh/tr/kurslar/android-uretim/aygit-ustu-testler

Aygıt üstü test gerçek bir ekranı bir aygıtta ya da öykünücüde çalıştırır ve her testin kurulumunu yeniden öder. Ders kurulum işinin bütçeyi kaç testte aştığını ve kurulumu paylaşmanın bedelini ölçer.

Önceki derste sahte bağımlılıklar ağın yerini aldı ve testin hiçbir parçası bir aygıtta koşmadı. Güdük kendi yanıtını ölçtürüyordu; gerçek bir ekranın gerçek bir sistemle nasıl konuştuğunu ölçtürmüyordu. Bunu ancak ekranı gerçekten çalıştıran bir test söyler. Bu dersin sorusu kursun sorusudur: **bu ölçüm bütçesini kaç örnekte aşıyor?** Dört yanıt aynıdır: içinde, azında, çoğunda, hepsinde. Ölçülen iş birimi, bir testin başlamadan önce ve çalışırken yaptığı iştir.

Çekirdek önceki derslerle aynıdır: `Budget` bir sınırı, `Overrun` bir ölçümün örneklerini ve bunların kaçının sınırı kesin olarak aştığını taşır.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

#### Aygıt Üstü Test Neyi Çalıştırır

Aygıt üstü test (instrumented test), uygulamayı ve test kodunu bir Android aygıtına yükler ve oradaki sistemle birlikte çalıştırır. Birim testi geliştirme makinesinde, saf Kotlin katmanında koşar; aygıt üstü test ise gerçek bir ekranı, gerçek bir yaşam döngüsünü ve sistemin kendi hizmetlerini kullanır. Bu, birim testinin yakalayamadığı kusurları yakalar: bir ekranın döndürüldüğünde durumunu kaybetmesi, bir izin iletişim kutusunun açılmaması, bir veri tabanının sistemde gerçekten oluşmaması.

Testlerin koştuğu yer iki türlüdür. Gerçek aygıt (physical device) bir kullanıcının eline geçecek donanımdır; sensör, kamera ve üreticiye özgü sistem davranışı yalnız orada doğrulanır. Öykünücü (emulator) bir aygıtın yazılımla taklit edilmiş hâlidir; kurması ve silmesi kolaydır, ama donanım davranışını yalnız taklit eder. Her ikisinde de bir test çalıştırıcısı (test runner) uygulamayı yükler, testleri sırayla koşar ve sonucu toplar.

Bu çalışma düzeninin bir bedeli var: her test, sınadığı ekranı kendi başına ayağa kaldırmak zorundadır. Test bir ekranı başlatır, gerekli veriyi yükler, kullanıcıyı oturum açmış duruma getirir ve ancak sonra asıl iddiasına geçer. Bu hazırlığa test düzeneği (fixture), hazırlığın yaptığı işe ise `fixtureWork` diyoruz. Platformda bir ekranı sıfırdan başlatan test şöyle yazılır.

```kotlin
// Platform kodu: CartScreenTest
@RunWith(AndroidJUnit4::class)
class CartScreenTest {
    @Test
    fun showsEmptyCart() {
        ActivityScenario.launch(CartActivity::class.java).use { scenario ->
            scenario.onActivity { activity ->
                assertEquals(0, activity.itemCount())
            }
        }
    }
}
```

`launch` çağrısı her testte ekranı yeniden başlatır; `use` bloğu test biterken onu kapatır. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde başlatma işi sayılır, bir ekran gerçekten başlatılmaz.

**TA1.** Model, aygıt üstü testi koşturmaz; bu ortamda aygıt ya da öykünücü yoktur. Kurulum işi adımların toplamıdır: ekranı başlatmak 6, tohum veriyi yüklemek 4, oturum açmak 2. Bu sayılar modelin girdisidir, bir aygıtın ölçümü değildir.

#### Her Testte Ödenen Kurulum

Model iki işlev kurar. İlki her testte ekranı sıfırdan başlatır: testin işi, kurulum işi ile testin kendi gövdesinin işinin toplamıdır. İkincisi kurulumu test sınıfı başına bir kez yapar: yalnız ilk test kurulumu öder, sonrakiler ekranı hazır bulur.

```kotlin
// Fixture.kt — ekrani sifirdan baslatan test duzeni: kurulum isi adimlarla sayilir
class Fixture(val steps: List<Int>) {
    val fixtureWork: Int = steps.sum()
}

fun freshPerTest(fixture: Fixture, bodies: List<Int>): List<Int> =
    bodies.map { fixture.fixtureWork + it }

fun sharedPerClass(fixture: Fixture, bodies: List<Int>): List<Int> =
    bodies.mapIndexed { index, body -> (if (index == 0) fixture.fixtureWork else 0) + body }
```

Her iki işlev de bir test gövdesi listesinden bir iş listesi üretir; `Overrun`'a verilecek örnekler bu listelerdir. Sekiz test düşünelim ve gövdelerinin işi 1, 2, 1, 3, 2, 1, 2 ve 3 olsun. Bütçe, bir testin yapabileceği işi 10 birimle sınırlar. Bu sayı bir platform sınırı değildir; ekibin bir testin kısa kalması için koyduğu hedefin modeldeki karşılığıdır.

```kotlin
// Main.kt
fun main() {
    val fixture = Fixture(listOf(6, 4, 2))
    val bodies = listOf(1, 2, 1, 3, 2, 1, 2, 3)
    println("kurulum isi: ${fixture.fixtureWork}")

    val work = freshPerTest(fixture, bodies)
    println("test basina is: $work")

    val overrun = Overrun("her-testte-kurulum", Budget("is", 10), work)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
kurulum isi: 12
test basina is: [13, 14, 13, 15, 14, 13, 14, 15]
her-testte-kurulum: butce=is<=10 | asan=8/8 -> hepsinde
icinde: 0
azinda: 0
cogunda: 0
hepsinde: 1
```

Kurulum işi tek başına 12 birimdir ve 10 birimlik bütçenin üstündedir. Gövdeler ne kadar küçük olursa olsun sekiz testin sekizi bütçenin üstüne çıktı: ölçüm hepsindedir. Hepsinde burada bir ipucu verir: aşımın nedeni hiçbir testin gövdesi değildir. En küçük gövde 1 birimdir ve yine 13'e ulaşır. Neden kurulumdur ve her testte yeniden ödenir.

Bütçeyi gevşetmek aşımı kapatırdı: 16 birimlik bir bütçe sekiz testi de içinde bırakırdı. Ama bu, kurulumun pahalı olduğu gerçeğini değiştirmez; yalnız onu ölçmenin dışına iter. Kural aşımın büyüklüğünü görmez ve kurulumun neden pahalı olduğunu da görmez. Bunu düzyazı söylemelidir.

#### Kurulumun Parçaları

Modeldeki 12 birimlik kurulum üç parçadan oluşur ve her parça gerçek bir testte ayrı bir iş karşılığıdır. Ekranı başlatmak, sistemden bir ekranın istenmesi, onun yaşam döngüsünün ilk durumlara getirilmesi ve çizilmesidir; bu yüzden üç parçanın en pahalısı olan 6 birimi alır. Tohum veriyi yüklemek, ekranın göstereceği kayıtların veri tabanına yazılmasıdır ve 4 birimdir. Oturum açmak, kullanıcıyı ekranın beklediği duruma getirir ve 2 birimdir.

Bu üç parçanın hiçbiri testin asıl iddiasına ait değildir. Test "sepet boşken boş mesajı görünür" diyorsa, iddia tek bir satırdır. Önceki üç iş yalnız iddiayı mümkün kılar. Bu yüzden kurulum işi test gövdesinden ayrı bir sütunda sayılır.

Kurulumu küçültmenin iki yolu vardır. Birincisi parçaları ucuzlatmaktır: tohum veriyi daha az kayıtla yüklemek ya da oturumu sahte bir kimlik doğrulayıcıyla vermek. İkincisi parçaları paylaşmaktır; sınıf başına bir kez yapmak bunun bir biçimidir. Birincisi testin gerçekçiliğinden fedakârlık eder, ikincisi testlerin bağımsızlığından.

#### Kaynak Parçaların Satır Satır Okunuşu

`CartScreenTest` parçasında `@RunWith(AndroidJUnit4::class)` satırı, testin aygıt üstü bir çalıştırıcıyla koşacağını bildirir. `ActivityScenario.launch(CartActivity::class.java)` ekranı, yani activity'yi sistemin gerçekten başlatmasını ister ve bir senaryo nesnesi döndürür. `.use { scenario -> ... }` bloğu, senaryo ne olursa olsun blok bittiğinde onu kapatır; kapatılmayan bir ekran sonraki testi etkiler. `scenario.onActivity { activity -> ... }` bloğu, activity'ye ekranın kendi iş parçacığında erişim verir. `assertEquals(0, activity.itemCount())` iddianın kendisidir: sepette sıfır ürün var.

Sınırlayıcı ölçümde göreceğimiz `SeedOnce` parçasında `companion object` Kotlin'de sınıfa bağlı üyeleri tutar. `@BeforeClass` ek açıklaması, yöntemin sınıftaki ilk testten önce bir kez çalışmasını ister. `@JvmStatic`, yöntemin çalıştırıcının beklediği biçimde, gerçek bir statik yöntem olarak üretilmesini sağlar. `seedDatabase()` ise kurulumun pahalı parçasıdır ve modelde 4 birimlik tohum yüklemeye karşılık gelir.

#### Gerçek Aygıt mı, Öykünücü mü

Bütçeyi kuran taraf testlerin koştuğu yeri de seçmek zorundadır. Öykünücü hızlı kurulur ve tek komutla sıfırlanabilir; sürekli tümleştirme hattında bu, her koşumun temiz bir durumdan başlaması demektir. Gerçek aygıt ise donanıma bağlı kusurları yakalar, ama her koşumda temiz bir durum garanti etmez. Önceki testin bıraktığı bir izin ya da bir hesap aygıtta kalmış olabilir.

Bu derste ölçülen kurulum işi bu seçimden bağımsızdır. İkisinde de ekran başlatılır, veri yüklenir ve oturum açılır. Değişen, işin birim başına maliyetidir, ve model maliyeti süreyle değil adımla sayar.

#### Test Piramidi ve Bütçe

Aygıt üstü testlerin pahalılığı, test paketinin biçimini belirler. Birim testi hızlıdır ve çok sayıda koşar; aygıt üstü test yavaştır ve az sayıda koşar. Bu dağılıma test piramidi denir: tabanda çok sayıda birim testi, ortada bileşenleri bir araya getiren testler, tepede az sayıda uçtan uca aygıt üstü test bulunur. Piramidi ters çeviren, yani her şeyi aygıt üstünde sınayan bir paket, bu derste ölçtüğümüz kurulum maliyetini binlerce testle çarpar.

Ekip bütçeyi her test için ayrı koyar. Birim testinin bütçesi birkaç adım olabilir; aygıt üstü testin bütçesi kuruluma göre belirlenir. Aynı 10 birim, iki farklı test türünde çok farklı bir beklentidir. Bu yüzden kurs boyunca her ölçümün bütçesi yanında bir gerekçeyle yazılır.

#### Paylaşılan Kurulumun Koşulları

Kurulumu sınıf başına paylaşmak, bazı koşullara bağlıdır. Testlerin sırası sonucu etkilememelidir; bir test diğerinin bıraktığı durumdan yararlanıyorsa, sırası değiştiğinde ya da tek başına koştuğunda kırılır. Paylaşılan durumu değiştiren testler, işlerini bitirdiklerinde durumu eski hâline getirmelidir. Platformda bu, her testten sonra çalışan bir temizlik yöntemiyle yapılır. Son olarak, paylaşılan kurulum salt okunur olmalıdır: tohum veri bir kez yüklenir ve hiçbir test onu değiştirmez.

Bu üç koşuldan biri sağlanamıyorsa, kurulumu paylaşmak hızı artırır ama kararlılığı düşürür. Sepet ölçümü bu dengeyi gösterdi: paylaşılan sepette yedi test başlangıçta bir şey buldu. Temizliğin de bir bedeli vardır ve her testten sonra yapılan temizlik, kurulumun yarattığı kazancın bir kısmını geri alır.

#### Yaygın Yanılgılar

İlk yanılgı, "aygıt üstü test birim testinin yerini alır" düşüncesidir. Aygıt üstü test gerçek sistemi kullanır ve bu yüzden daha pahalıdır. Bir işlevin sınır durumlarını aygıt üstünde sınamak, kurulumu her sınır için yeniden ödemek demektir. Sınır durumları birim testinde, ekran akışları aygıt üstünde sınanır.

İkinci yanılgı, "kurulumu paylaşmak her zaman iyidir" düşüncesidir. Paylaşılan kurulum ilk testin yükünü azaltmaz, yalnız bir kez ödetir; ama paylaşılan durum testleri birbirine bağlar. Sepet ölçümünde yedi testin başlangıçta bir şey bulması bunun somut göstergesiydi.

Üçüncü yanılgı, "bütçeyi aşan testi bütçeyi büyüterek kapatmak yeterlidir" düşüncesidir. Bütçeyi 16'ya çıkarmak ölçümü hepsindeden içine taşırdı, ama kurulumun maliyeti hiç değişmezdi. Bütçe ürün kararına dayanır; aşımı kapatmak için değiştirilen bir bütçe ölçümün anlamını yok eder.

#### Sınırlayıcı Ölçüm

Bu ölçümün sınırlayıcısı sınıf başına kuruluma geçmektir. Gerekçesi şudur: ilk ölçümde aşımın nedeni kurulumdu ve bunu doğrulamak için tek yapılacak şey kurulumun ödendiği yeri değiştirmektir. Gövdeler, bütçe ve örnek sayısı aynı kalır. Aşım hepsindeden azına geçerse, neden gerçekten kurulumdu.

Kurulumu test sınıfı başına bir kez yapalım. Testlerin gövdesi, bütçe ve kurulum aynı kalıyor; yalnız kurulumun ne zaman ödendiği değişiyor. Platformda bu, sınıf başına bir kez çalışan bir hazırlık yöntemiyle yapılır.

```kotlin
// Platform kodu: SeedOnce
class CartScreenTest {
    companion object {
        @BeforeClass
        @JvmStatic
        fun seedOnce() {
            seedDatabase()
        }
    }
}
```

Hazırlık yöntemi sınıftaki ilk testten önce bir kez çalışır. Modelde bu, kurulum işinin yalnız ilk testte ödenmesidir.

```kotlin
// Main.kt — kurulum sinif basina bir kez odenir
fun main() {
    val fixture = Fixture(listOf(6, 4, 2))
    val bodies = listOf(1, 2, 1, 3, 2, 1, 2, 3)

    val work = sharedPerClass(fixture, bodies)
    println("test basina is: $work")

    val overrun = Overrun("sinif-basina-kurulum", Budget("is", 10), work)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
test basina is: [13, 2, 1, 3, 2, 1, 2, 3]
sinif-basina-kurulum: butce=is<=10 | asan=1/8 -> azinda
icinde: 0
azinda: 1
cogunda: 0
hepsinde: 0
```

Aynı sekiz test, aynı bütçe. Yedi test 1 ile 3 birim arasında kaldı, yalnız ilk test 13'e çıktı. Ölçüm hepsindeden azına geçti. Aşım tamamen kapanmadı: kurulum yok olmadı, yalnız tek bir testin üstüne yığıldı. Kural bunu "bir test aşıyor" diye okur; ilk testin kurulumun yükünü taşıdığını yine düzyazı söylemelidir.

Bu kazanç bedelsiz değildir. Kurulum paylaşılınca testler aynı ekranı, aynı veriyi ve aynı sistem durumunu paylaşır. Bir test sepete bir ürün koyarsa, sonraki test sepeti dolu bulur. Bunu bir ölçümle görelim: her test sepete belirli sayıda ürün ekliyor ve sonraki test başlarken sepette ne kadar ürünün kaldığı sayılıyor. Bütçe sıfırdır, çünkü her test boş bir sepetle başlamalıdır.

```kotlin
// Main.kt — paylasilan durum sonraki testin baslangicini bozar
fun leftoverBefore(adds: List<Int>, cleanup: Boolean): List<Int> {
    val cart = mutableListOf<String>()
    return adds.map { count ->
        val leftover = cart.size
        repeat(count) { cart.add("urun") }
        if (cleanup) cart.clear()
        leftover
    }
}

fun main() {
    val adds = listOf(1, 0, 2, 1, 0, 1, 1, 0)
    val shared = leftoverBefore(adds, false)
    val cleaned = leftoverBefore(adds, true)
    println("temizlik yok: $shared")
    println("temizlik var: $cleaned")

    val clean = Budget("kalan", 0)
    val overruns = listOf(
        Overrun("paylasilan-sepet", clean, shared),
        Overrun("temizlenen-sepet", clean, cleaned),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
temizlik yok: [0, 1, 1, 3, 4, 4, 5, 6]
temizlik var: [0, 0, 0, 0, 0, 0, 0, 0]
paylasilan-sepet: butce=kalan<=0 | asan=7/8 -> cogunda
temizlenen-sepet: butce=kalan<=0 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 1
hepsinde: 0
```

Paylaşılan sepet çoğundadır: ilk test dışında yedi test başlangıçta bir şey bulur. Her testten sonra temizleme yapılırsa ölçüm içindedir, ama temizleme de iştir. Kurulumu paylaşmak testi hızlandırır; paylaşılan durumu temizlemek bu kazancın bir kısmını geri alır. Hangi yöntemin seçileceği, ekibin testlerin bağımsızlığına mı yoksa süresine mi öncelik verdiğine bağlıdır. Kural bu kararı vermez, yalnız kararın sonucunu sayar.

**TA2.** Model sepeti bir liste olarak kurar ve temizlemeyi tek adımda yapar. Gerçek bir veri tabanında temizleme, tablo sayısı ve yabancı anahtarlara göre değişen bir işlemdir; modelde bu fark yok.

#### Özet

- Bu dersin aşımı: hepsinde. Her test ekranı sıfırdan başlattığı için 12 birimlik kurulum sekiz testin sekizinde 10 birimlik bütçeyi aştırdı.
- Dersin ölçütü, bir testin işinin bütçeyi kaç testte aştığı: her testte kurulumla hepsinde, sınıf başına kurulumla azında çıktı.
- Aşımın nedeni test gövdesi değildi; en küçük gövde 1 birim olduğu hâlde test 13'e çıktı. Neden kurulumdu ve her testte yeniden ödeniyordu.
- Kurulumu paylaşmak ilk teste yükü yığdı ve aşım hepsindeden azına geçti, ama kurulum yok olmadı.
- Paylaşılan durum sonraki testlerin başlangıcını bozdu: temizlik yokken ölçüm çoğunda, her testten sonra temizlikle içinde çıktı.

#### Sonraki Adım

Bu derste ekran sıfırdan başlatıldı ve test içinde ekranın hangi parçasının kontrol edileceği hiç sorulmadı. Gerçek bir arayüz testi ekranda bir düğümü bulmak zorundadır ve bulmanın bir yolu vardır. Sıradaki ders, `Arayüz Testleri`, bir düğümü metniyle aramanın ve test etiketiyle aramanın bütçeyi kaç örnekte aştığını ölçer.

### Arayüz Testleri

Source: https://academia.sh/tr/kurslar/android-uretim/arayuz-testleri

Arayüz testi bir ekranda düğümü bulur, onunla etkileşir ve sonucu doğrular. Ders düğümü metniyle aramanın anlam ağacını baştan dolaştığını, test etiketiyle aramanın tek adımda bittiğini ölçer.

Önceki derste her test ekranı sıfırdan başlatıyordu ve ölçüm kurulumun maliyetini saydı. Ekran ayağa kalktıktan sonra test onunla konuşmak zorundadır: bir düğümü bulur, ona dokunur, sonucu okur. Bu konuşmanın da bir maliyeti vardır ve bu dersin konusu odur. Kursun sorusu aynı: **bu ölçüm bütçesini kaç örnekte aşıyor?** Dört yanıt değişmez: içinde, azında, çoğunda, hepsinde. Ölçülen iş birimi, bir düğümü bulmak için atılan adımdır.

Çekirdek önceki derslerle aynıdır: bir bütçe, bir ölçümün etiketi ve örnek listesi, sınırı kesin olarak aşan örneklerin sayısı.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

#### Ekranın Test İçin Görünüşü

Bir arayüz testi ekrandaki pikselleri değil, ekranın anlamını okur. Bileşen tabanlı arayüzde ekranın bu anlamlı hâline anlam ağacı (semantics tree) denir. Ağaçtaki her düğüm bir metin, bir rol ya da bir eylem taşır: başlık, düğme, bir ürünün adı. Erişilebilirlik hizmetleri de aynı ağacı temel alır; düğümleri birleştirme kurallarını ise kendileri uygular. Testin işi bu ağaçta bir düğümü bulmak, ona bir eylem uygulamak ve düğümün sonraki durumunu doğrulamaktır.

Düğümü bulmanın iki yolu var. Birincisi metniyle aramaktır: kullanıcının gördüğü yazıyla düğümü bulursun. Bu doğal bir yoldur, çünkü test kullanıcının baktığı şeye bakar. İkincisi test etiketiyle aramaktır: geliştirici düğüme bir etiket (test tag) verir ve test o etiketi sorar. Etiket kullanıcıya görünmez; yalnız teste hizmet eder. Platformda ikisi şöyle görünür.

```kotlin
// Platform kodu: CartComposeTest
class CartComposeTest {
    @get:Rule
    val composeRule = createComposeRule()

    @Test
    fun deleteButtonIsShown() {
        composeRule.setContent { CartScreen() }
        composeRule.onNodeWithTag("sil-defter").assertIsDisplayed()
        composeRule.onNodeWithText("Defteri sil").assertIsDisplayed()
    }
}
```

`setContent` ekranı kurar, `onNodeWithTag` etiketle, `onNodeWithText` metinle düğümü bulur. Klasik görünüm sisteminde aynı iş başka bir araçla yapılır.

```kotlin
// Platform kodu: CartViewTest
class CartViewTest {
    @Test
    fun deleteButtonIsShownInViews() {
        onView(withId(R.id.deleteNotebook)).check(matches(isDisplayed()))
    }
}
```

`onView` görünüm ağacında kimlikle ya da metinle bir görünüm bulur. İki parça da platform koduna aittir. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde ağaç bir düğüm listesidir ve bir arama bu listede atılan adımlarla sayılır.

**TA1.** Model, arayüz testi kitaplığını koşturmaz; bu ortamda ekran ve ağaç yoktur. Anlam ağacı düz bir listeyle temsil edilir ve arama listeyi baştan dolaşır. Gerçek araç ağacı derinlik sırasıyla dolaşır; model bu sıralamayı listenin sırasına indirger.

#### Düğümü Metniyle Aramak

Bir alışveriş sepeti ekranının ağacında on iki düğüm var: başlık, geri düğmesi, iki ürün ve her ürün için adet ve silme düğmesi, toplam, kupon kodu, kuponu uygulama düğmesi ve ödemeye geçme düğmesi. Her düğümün bir metni ve benzersiz bir etiketi vardır. Yardımcı dosya ağacı kurar, metinle aramayı ve etiketle aramayı tanımlar.

```kotlin
// Semantics.kt — sepet ekraninin anlam agaci, metinle ve etiketle arama
class Node(val text: String, val tag: String)

fun cartTree(extra: Int = 0): List<Node> {
    val head = listOf(
        Node("Sepetim", "baslik"),
        Node("Geri", "geri"),
        Node("Defter", "urun-defter"),
        Node("Defter adedi", "adet-defter"),
        Node("Defteri sil", "sil-defter"),
        Node("Kalem", "urun-kalem"),
        Node("Kalem adedi", "adet-kalem"),
        Node("Kalemi sil", "sil-kalem"),
    )
    val offers = (1..extra).map { Node("Oneri $it", "oneri-$it") }
    val tail = listOf(
        Node("Toplam", "toplam"),
        Node("Kupon kodu", "kupon"),
        Node("Kuponu uygula", "kupon-uygula"),
        Node("Odemeye gec", "odeme"),
    )
    return head + offers + tail
}

fun pickByTags(tree: List<Node>, tags: List<String>): List<Node> =
    tags.map { tag -> tree.first { it.tag == tag } }

fun findSteps(tree: List<Node>, text: String): Int {
    var steps = 0
    for (node in tree) {
        steps++
        if (node.text == text) return steps
    }
    return steps
}

fun tagSteps(byTag: Map<String, Node>, tag: String): Int =
    if (byTag.containsKey(tag)) 1 else 0
```

`findSteps` ağacı baştan dolaşır ve metin eşleşince durur. Atılan adım, düğümün ağaçtaki sırasıdır: başlık 1 adımda, ödemeye geçme düğmesi 12 adımda bulunur. `cartTree` ağaca isteğe bağlı olarak öneri düğümleri ekler; şimdilik eklemiyoruz. Sekiz test sekiz düğümü arayacak: geri, defteri silme, kalem, kalem adedi, toplam, kuponu uygulama, ödemeye geçme ve başlık. Bütçe 5 adımdır: bu, bir düğümün ekranın başından beş düğüm içinde bulunmasını isteyen bir ekip kararının modeldeki karşılığıdır, bir platform eşiği değildir.

```kotlin
// Main.kt
fun main() {
    val tree = cartTree()
    val picks = pickByTags(
        tree,
        listOf("geri", "sil-defter", "urun-kalem", "adet-kalem", "toplam", "kupon-uygula", "odeme", "baslik"),
    )
    val steps = picks.map { findSteps(tree, it.text) }
    println("adimlar: $steps")

    val overrun = Overrun("metinle-arama", Budget("adim", 5), steps)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
adimlar: [2, 5, 6, 7, 9, 11, 12, 1]
metinle-arama: butce=adim<=5 | asan=5/8 -> cogunda
icinde: 0
azinda: 0
cogunda: 1
hepsinde: 0
```

Sekiz aramanın beşi bütçeyi aştı: kalem, kalem adedi, toplam, kuponu uygulama ve ödemeye geçme düğmeleri ekranın ilk beş düğümünün ötesinde duruyor. Ölçüm çoğundadır. Başlık, geri düğmesi ve defteri silme düğmesi ağacın başında olduğu için bütçenin içinde kaldı. Aşım düğümün metninden değil, konumundan gelir: aynı metni taşıyan bir düğüm ağacın başında olsaydı arama kısa sürerdi.

Kural bu ölçümde de iki şeyi görmez. Büyüklüğü görmez: ödemeye geçme düğmesi bütçeyi yedi adımla, kalem bir adımla aşar ve ikisi de bir sayılır. Nedeni görmez: aşımın nedeni ağacın dolaşım düzenidir ve düğümlerin kullanıcıya göre dizilişidir.

#### Anlam Ağacının Düğümleri

Anlam ağacındaki her düğüm, ekrandaki bir öğenin teste ve erişilebilirlik hizmetlerine görünen özetidir. Bir düğüm metin taşıyabilir, bir rol bildirebilir, bir eylem sunabilir ve bir test etiketi alabilir. Ekranda iç içe duran birkaç öğe, örneğin bir kartın içindeki başlık ve açıklama, bileşen tabanlı arayüzde varsayılan olarak tek bir düğümde birleşebilir. Test bu birleşik düğümü arar; birleşmeden önceki ağaç ayrıca istenebilir. Bu ayrıntı aramanın kaç adım süreceğini değiştirir, çünkü birleşik ağaç daha az düğüm taşır.

Modelimizde on iki düğüm var ve her biri ayrı bir öğedir. Gerçek bir ekranda düğüm sayısı ekranın yapısına göre çok daha az ya da çok daha fazla olabilir. Ölçümün öğrettiği şey sayı değil, ilişkidir: arama düğümün sırasına bağlıysa, ekran büyüdükçe aşım artar.

#### Platform Parçalarının Satır Satır Okunuşu

`CartComposeTest` parçasında `@get:Rule` ek açıklaması, bileşen tabanlı arayüzün test kuralını her testten önce kurar ve sonra kapatır. `createComposeRule()` bu kuralı üretir. `composeRule.setContent { CartScreen() }` satırı test edilecek ekranı kurala yükler; ekran boş bir activity içinde kurulur, önceki dersteki gibi uygulamanın kendi ekranını başlatmak gerekmez. `onNodeWithTag("sil-defter")` etiketi `sil-defter` olan düğümü bulur. `assertIsDisplayed()` düğümün ekranda göründüğünü doğrular. `onNodeWithText("Defteri sil")` ise aynı düğümü metniyle bulur.

`CartViewTest` parçasında `onView(withId(R.id.deleteNotebook))` klasik görünüm sisteminde kimliği verilen görünümü bulur. `check(matches(isDisplayed()))` görünümün görünür olduğunu doğrular. Kimlik, metnin yerini tutan bir etikettir: kaynak dosyada bir kez tanımlanır ve çeviriden bağımsızdır.

Bir etkileşim eklemek istersek aynı düğüm arama zincirine `performClick()` gibi bir eylem eklenir. Zincirin ilk halkası her zaman düğüm aramasıdır; eylem ve doğrulama bulunan düğüm üzerinde çalışır. Bu yüzden aramanın maliyeti, testteki her etkileşimin maliyetine eklenir.

#### Metinle Aramanın Kırılganlığı

Metinle arama yalnız yavaş değildir; metin değişince kırılır. Düğmenin yazısı "Defteri sil"den "Sil"e dönerse, metinle arayan her test kırılır. Uygulama birden çok dilde yayımlanıyorsa, test yalnız tek bir dilde geçer ve diğer dillerde düğümü bulamaz. Etiket bu iki sorunu da yaşamaz, çünkü etiket kullanıcının dilinden bağımsızdır.

Öte yandan metin, kullanıcının gerçekten gördüğü şeydir. Metinle arayan test, düğmenin kullanıcıya doğru yazıyla göründüğünü örtük olarak sınar. Etiketle arayan test bunu sınamaz ve bu fark bir seçimdir: hızlı aramayı mı, yoksa görünen yazının doğruluğunu mu istediğini ekip belirler.

#### Etiketlerin Adlandırılması

Etiketin değeri, kararlı kalmasında yatar. Model etiketlerin biçimini `eylem-öğe` olarak seçti: `sil-defter`, `adet-kalem`, `kupon-uygula`. Bu ad ekranın düzenine değil, öğenin işlevine bağlıdır. Düğme ekranın başka bir yerine taşınsa da etiket aynı kalır. Düzeni anlatan bir ad, örneğin `ikinci-satir-sag-dugme`, ekran değişince yanıltıcı olur.

Etiketin benzersiz olması da gerekir. Listedeki her satır aynı etiketi taşırsa, tek düğüm bekleyen arama birden çok eşleşme bulur ve test hata verir. Bu yüzden listelerde etikete satırı ayırt eden bir değer, örneğin ürün kodu, eklenir.

#### Erişilebilirlikle İlişki

Anlam ağacı yalnız testlerin değil, ekran okuyucuların da kullandığı bir yapıdır. Bu yüzden metinle arayan bir test, ekran okuyucunun okuyacağı metne yakın bir metni arar; metni doğru olmayan bir düğümü bulamayan test, erişilebilirlik sorununu da dolaylı olarak yakalar. Etiket ise yalnız testlere hizmet eder ve ekran okuyucuya görünmeyebilir. İki aramayı birlikte kullanmanın gerekçesi budur: etiket hızlı ve kararlı bulur, metin kullanıcının duyduğu şeyi doğrular.

#### Yaygın Yanılgılar

İlk yanılgı, "her düğüme etiket koymak testi sağlamlaştırır" düşüncesidir. Etiketler üretim koduna test amaçlı bir yük ekler ve unutulan bir etiket testi kırar. Etiket, yalnız metnin güvenilir olmadığı düğümlere konur: yinelenen metinler, dinamik metinler ve simgeler.

İkinci yanılgı, "metinle arama her zaman yavaştır" düşüncesidir. Ölçümde yavaşlık düğümün sırasından geldi. Aranan düğüm ağacın başındaysa metinle arama da kısa sürer: başlık bir adımda, geri düğmesi iki adımda bulundu. Aşım konuma bağlıdır, aramanın biçimine değil.

Üçüncü yanılgı, "ekran büyüyünce testi yeniden yazmak gerekir" düşüncesidir. Etiketle arayan testlerde büyüme aramanın maliyetini değiştirmedi. Büyüyen ekranda yeniden yazılması gereken şey, etiketsiz kalan düğümlerin aramasıdır.

#### Sınırlayıcı Ölçüm

Bu ölçümün sınırlayıcısı etiketle aramadır. Gerekçesi şudur: ilk ölçümde aşım, aramanın ağaçtaki konuma bağlı olmasından geliyordu. Düğümü eşlemden bulan bir arama konumu ortadan kaldırır. Düğümler, bütçe ve örnek sayısı aynı kalırsa, sonuçtaki farkı yalnız arama biçimi açıklar.

Aynı ekranı bu kez test etiketiyle arayalım. Etiket benzersizdir, bu yüzden bir eşlemde tek bakışla bulunur. Eşlemin kendisi bir kez kurulmalıdır: ağaç bir kez dolaşılır ve her düğüm etiketiyle bir eşleme konur. Bu bir kez ödenen bir iştir ve ekran başına bir defa yapılır.

```kotlin
// Main.kt — ayni ekran, etiketle arama
fun main() {
    val tree = cartTree()
    val picks = pickByTags(
        tree,
        listOf("geri", "sil-defter", "urun-kalem", "adet-kalem", "toplam", "kupon-uygula", "odeme", "baslik"),
    )
    val byTag = tree.associateBy { it.tag }
    println("esleme kurulumu: ${tree.size} adim, ekran basina bir kez")

    val steps = picks.map { tagSteps(byTag, it.tag) }
    println("adimlar: $steps")

    val overrun = Overrun("etiketle-arama", Budget("adim", 5), steps)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
esleme kurulumu: 12 adim, ekran basina bir kez
adimlar: [1, 1, 1, 1, 1, 1, 1, 1]
etiketle-arama: butce=adim<=5 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 0
```

Aynı ekran, aynı sekiz düğüm, aynı bütçe. Her arama tek adımda bitti ve ölçüm çoğundadan içinde yönüne geçti. Değişen test edilen ekran değil, düğüme ulaşma yoludur.

Etiketle arama bir şeyi de feda eder: etiket kullanıcıya görünmez. Bir düğmenin üzerindeki yazı yanlışsa metinle arayan test hatayı yakalar, etiketle arayan test yakalamaz. Platformda iki yol birlikte kullanılır: etiketle bulunur, metin ayrıca doğrulanır.

Bir üçüncü ölçüm, ekran büyüyünce ne olduğunu gösterir. Toplamın önüne sekiz öneri düğümü ekleyelim. Bütçeyi bu kez eski ağacın uzunluğuna, yani 12 adıma koyalım: metinle arama eski ekranda her düğümü bu sınırın içinde bulur.

```kotlin
// Main.kt — ekran buyuyunce metinle arama pahalanir, etiketle arama ayni kalir
fun main() {
    val tags = listOf("geri", "sil-defter", "urun-kalem", "adet-kalem", "toplam", "kupon-uygula", "odeme", "baslik")
    val small = cartTree()
    val large = cartTree(8)
    val smallPicks = pickByTags(small, tags)
    val largePicks = pickByTags(large, tags)
    val byTag = large.associateBy { it.tag }

    val smallText = smallPicks.map { findSteps(small, it.text) }
    val largeText = largePicks.map { findSteps(large, it.text) }
    val largeTag = largePicks.map { tagSteps(byTag, it.tag) }
    println("eski ekran, metin: $smallText")
    println("buyuk ekran, metin: $largeText")
    println("buyuk ekran, etiket: $largeTag")

    val limit = Budget("adim", 12)
    val overruns = listOf(
        Overrun("metinle-eski-ekran", limit, smallText),
        Overrun("metinle-buyuk-ekran", limit, largeText),
        Overrun("etiketle-buyuk-ekran", limit, largeTag),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
eski ekran, metin: [2, 5, 6, 7, 9, 11, 12, 1]
buyuk ekran, metin: [2, 5, 6, 7, 17, 19, 20, 1]
buyuk ekran, etiket: [1, 1, 1, 1, 1, 1, 1, 1]
metinle-eski-ekran: butce=adim<=12 | asan=0/8 -> icinde
metinle-buyuk-ekran: butce=adim<=12 | asan=3/8 -> azinda
etiketle-buyuk-ekran: butce=adim<=12 | asan=0/8 -> icinde
icinde: 2
azinda: 1
cogunda: 0
hepsinde: 0
```

Eski ekranda metinle arama 12 adımlık bütçenin içindeydi. Ekran sekiz düğüm büyüyünce toplam, kuponu uygulama ve ödemeye geçme düğmeleri sınırın ötesine geçti ve ölçüm azına düştü. Etiketle arama büyüyen ekranda hâlâ tek adımdır. Ekran büyüdükçe metinle aramanın aşımı artar; etiketle aramanın aşımı konumdan bağımsızdır.

**TA2.** Model öneri düğümlerini toplamın önüne koyar; gerçek bir ekranda yeni düğümlerin ağaçtaki yeri ekran tasarımına bağlıdır. Eşlemin her düğümü kapsadığı da varsayılır: iki düğüm aynı etiketi taşırsa gerçek araç hata verir, model vermez.

#### Özet

- Bu dersin aşımı: çoğunda. Düğümü metniyle arayan test, sekiz aramanın beşinde ağacın ilk beş adımını geçti.
- Dersin ölçütü, bir düğümü bulmak için atılan adımın bütçeyi kaç aramada aştığı: aşımı üreten düğümün metni değil, ağaçtaki konumudur.
- Test etiketi ve eşlem üzerinden arama her örnekte tek adımdır; aynı ekranda çoğundadan içinde yönüne geçildi. Eşlem ekran başına bir kez kurulur.
- Ekran sekiz düğüm büyüyünce metinle arama 12 adımlık bütçede içindeden azındaya geçti, etiketle arama içinde kaldı.
- Kural adımı sayar, düğümün kullanıcıya doğru görünüp görünmediğine bakmaz; etiketle arayan test yanlış yazılmış bir düğme metnini yakalamaz.

#### Sonraki Adım

Bu derste ekran duruyordu ve arama yalnız adım saydı. Gerçek bir ekran ise hazır olmadan test onu aramaya başlayabilir. Sıradaki ders, `Test Kararlılığı`, hazır olma turunu bekleyen bir testin bekleme bütçesini kaç koşumda aştığını ölçer.

### Test Kararlılığı

Source: https://academia.sh/tr/kurslar/android-uretim/test-kararliligi

Aynı kodda bazen geçen bazen kalan test kırılgandır. Ders sabit tur bekleyen bir testin hazır olma turuyla bütçeyi kaç koşumda aştığını, koşula bağlı yoklamanın bunu nasıl kapattığını ölçer.

Önceki derste test ekranı bulabiliyordu. Ama ekran hazır olmadan bulmaya kalkarsa, aynı test bir koşumda geçer, ötekinde kalır. Bu derste ölçülen iş birimi bir bekleme turudur: testin ekranın hazır olmasını beklerken harcadığı tur. Kursun sorusu değişmez: **bu ölçüm bütçesini kaç örnekte aşıyor?** Yanıtlar yine içinde, azında, çoğunda ve hepsinde.

Çekirdek önceki derslerle aynıdır.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

#### Kırılgan Testin Anatomisi

Kırılgan test (flaky test), kod değişmediği hâlde bazen geçen bazen kalan testtir. Nedeni çoğunlukla testte değil, testin sıraya koyduğu şeylerdedir: ekranın çizilmesi, ağ yanıtı, bir eş yordamın bitişi. Test bunların ne zaman olacağını bilmez ve bir tahmin yapar: "iki saniye sonra hazırdır". Tahmin çoğu koşumda tutar, bir koşumda tutmaz.

Bu sorun üç ayrı soruyla karışır. M30/K05'te bir sonucun yinelemeyle ya da sırayla değişip değişmediğini sormuştuk. M13/K04'te bir sonucu neyin belirlediğini sorduk. Burada ikisi de yok: kırılganlığı bir aşım kategorisi olarak adlandırmıyoruz ve kaynağını da aramıyoruz. Ölçüm yalnızca aşımın kaç koşuma yayıldığını sayar. Kırılganlık bir kategori değildir; aşımı üreten düzenektir, yani bekleme biçimidir.

Platformda bu soruna karşı kullanılan araç, eşitleme (synchronization) sağlayan boşta kaynağıdır (idling resource). Kaynak, uygulamanın meşgul olup olmadığını teste bildirir ve test, kaynak boşa çıkana kadar işlemini erteler.

```kotlin
// Platform kodu: BusyCounterResource
class BusyCounterResource : IdlingResource {
    private val busy = AtomicInteger(0)
    private var callback: IdlingResource.ResourceCallback? = null

    override fun getName(): String = "busy-counter"
    override fun isIdleNow(): Boolean = busy.get() == 0
    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
        this.callback = callback
    }

    fun begin() { busy.incrementAndGet() }
    fun end() {
        if (busy.decrementAndGet() == 0) callback?.onTransitionToIdle()
    }
}
```

`begin` iş başlarken, `end` iş biterken çağrılır. `isIdleNow` sıfırsa test ilerler. Test süre tahmin etmez; uygulamanın kendisine sorar. Model bu araçları koşturmaz; bunun yerine ekranın hazır olduğu turu bir diziden okur ve testin beklemeyi nasıl yaptığına göre harcanan turu sayar.

**TA1.** Modelde hazır olma turu sabit bir dizidir; gerçek bir koşumda bu değer ağın, işlemcinin ve sıranın o anki durumuna bağlıdır. Dizi, sekiz koşumun hazır olma turunu temsil eder ve rastgele üretilmez.

#### Sabit Tur Bekleyen Test

Yardımcı dosya hazır olma turlarını ve iki bekleme biçimini tanımlar.

```kotlin
// Stability.kt — hazir olma turlari ve iki bekleme bicimi
val readyTable: List<Int> = listOf(3, 4, 3, 8, 4, 3, 9, 4)

fun pollRounds(run: Int): Int = readyTable[run]

fun fixedWaitRounds(run: Int, wait: Int): Int = wait

fun passesAfterFixedWait(run: Int, wait: Int): Boolean = pollRounds(run) <= wait
```

`pollRounds(run)` o koşumda ekranın kaçıncı turda hazır olduğunu verir. `fixedWaitRounds` testin sabit tahminini: ne olursa olsun `wait` tur bekler. `passesAfterFixedWait` testin geçip geçmediğini: ekran `wait` turdan önce hazırsa geçer.

Test, ekranın beş turda hazır olacağını varsayıyor. Sekiz koşumun hazır olma turları ölçülür; bütçe, testin beklediği 5 turdur.

```kotlin
// Main.kt
fun main() {
    val runs = (0 until 8).toList()
    val ready = runs.map { pollRounds(it) }
    println("hazir olma turlari: $ready")

    val failed = runs.count { !passesAfterFixedWait(it, 5) }
    println("bekleme 5 tur, kalan kosum: $failed")

    val overrun = Overrun("sabit-bekleme", Budget("tur", 5), ready)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
hazir olma turlari: [3, 4, 3, 8, 4, 3, 9, 4]
bekleme 5 tur, kalan kosum: 2
sabit-bekleme: butce=tur<=5 | asan=2/8 -> azinda
icinde: 0
azinda: 1
cogunda: 0
hepsinde: 0
```

Sekiz koşumun ikisinde ekran beş turdan sonra hazır oldu: dördüncü koşum 8, yedinci koşum 9 turda. Test bu iki koşumda kalır, öteki altısında geçer. Ölçüm azındadır. Sekizde altı geçen bir test güvenilir sayılmaz: kalan iki koşum ekip için gürültüdür. Kural aşımın sayısını bilir, ama o gürültünün bir regresyon mu yoksa hazır olma turu mu olduğunu ayırt etmez.

#### Beklemeyi Uzatmak

İlk akla gelen çare beklemeyi uzatmaktır. Test dokuz tur beklerse ekran her koşumda hazırdır ve test hiç kalmaz. Ama bunun bedeli vardır: ekran üç turda hazır olduğunda da test dokuz tur bekler. Bütçeyi bu kez bir koşumun harcayabileceği tur sayısına, yani 5'e koyalım.

```kotlin
// Main.kt — beklemeyi dokuza cikarmak
fun main() {
    val runs = (0 until 8).toList()
    val failed = runs.count { !passesAfterFixedWait(it, 9) }
    println("bekleme 9 tur, kalan kosum: $failed")

    val spent = runs.map { fixedWaitRounds(it, 9) }
    println("harcanan turlar: $spent")

    val overrun = Overrun("uzun-bekleme", Budget("tur", 5), spent)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
bekleme 9 tur, kalan kosum: 0
harcanan turlar: [9, 9, 9, 9, 9, 9, 9, 9]
uzun-bekleme: butce=tur<=5 | asan=8/8 -> hepsinde
icinde: 0
azinda: 0
cogunda: 0
hepsinde: 1
```

Test kararlı oldu ama her koşumda bütçeyi aştı: hepsinde. Kırılganlık gitti, yerine sürekli bir maliyet geldi. Sabit bekleme iki hata türünden birini seçtirir: ya bazı koşumlarda kalan test ya da her koşumda yavaş test.

**TA2.** Bekleme turu modelde ortak bir birimdir. Gerçek bir testte bir tur, tek bir sabit süre değil, değişken uzunlukta bir bekleme adımıdır; model onu bir iş birimine indirger.

#### Kırılganlığın Kaynakları

Bir testin kırılgan olması için kodun yanlış olması gerekmez; test ile uygulama arasındaki zamanlama ilişkisinin yanlış kurulması yeter. Zamanlama kaynaklı kırılganlığın üç tipik sahnesi vardır.

Birincisi arayüz iş parçacığıdır. Uygulama arayüzü kendi iş parçacığında çizer; test ise başka bir iş parçacığında koşar ve bir düğmeye dokunduktan hemen sonra sonucu okumaya kalkabilir. Çizim henüz bitmemişse okuma eski durumu görür.

İkincisi arka plan işidir. Ağdan gelen bir yanıt, bir veri tabanı yazması ya da bir eş yordamın bitişi testin kontrolünde değildir. Test bunun ne zaman biteceğini bilmediği için bir tahmin yürütür ve tahmin tutmayabilir.

Üçüncüsü animasyon ve geçiştir. Bir ekran açılırken düğümler yerini bulana kadar geçici durumlarda kalır. Test geçişin ortasında düğüme dokunursa, düğüm henüz tıklanabilir olmayabilir.

Bu üç sahnenin ortak noktası, testin bir olayın bittiğini varsaymasıdır. Doğru çare olayın bittiğini sormaktır, varsaymak değildir. Modelde ekranın hazır olduğu tur bu olayın karşılığıdır ve test onu ya tahmin eder ya da sorar.

#### Eşitleme Seçenekleri

Testle uygulamayı eşitlemenin dört yolu vardır ve her biri farklı bir bedel taşır.

Sabit bekleme en basitidir ve bu derste ilk iki ölçümde kullanıldı. Test belirli bir süre ya da tur sayısı kadar durur. Sorunu şudur: bekleme kısaysa test kırılır, uzunsa her koşumda boşuna bekler.

Yoklama (polling), testin kısa aralıklarla bir koşulu denetlemesidir. Koşul sağlanınca test ilerler; belirli bir üst sınıra varılırsa test başarısız olur. Son ölçümde kullandığımız koşula bağlı bekleme budur. Yoklamanın bedeli, her denetimin kendi işini yapmasıdır.

Boşta kaynağı, uygulamanın kendisinin "meşgulüm" ya da "boştayım" demesidir. Yoklamadan farkı, koşulu testin değil uygulamanın bilmesidir. Uygulama bir iş başlatırken sayacı artırır, bitirirken azaltır; test sayaç sıfırlanana kadar bekler. Kaynak platform parçasında gösterilen sınıftır.

Eş yordamlar için test zamanlayıcısı, sanal bir saat kullanır ve beklemeleri gerçek zaman geçmeden ilerletir. Bu sanal saat, testin zamanı kontrol edebildiği için en kararlı seçenektir, ama yalnız eş yordam ve zamanlayıcıyla sınırlı işlerde kullanılabilir; ağ ya da çizim bu saati bilmez. Bu derste kitaplığın test paketi kurulu olmadığı için sanal saat ölçülmez, yalnız adı geçer.

#### Boşta Kaynağının Satır Satır Okunuşu

`BusyCounterResource`, test çerçevesinin tanıdığı kaynak arayüzünü uygular. `busy` bir `AtomicInteger`dır: sayaç birden çok iş parçacığından güvenle artırılıp azaltılabilir. `getName()` kaynağın hata iletilerinde görünecek adını verir. `isIdleNow()` test çerçevesinin sorduğu sorudur: sayaç sıfırsa uygulama boştadır.

`registerIdleTransitionCallback` çerçevenin bir geri çağırım kaydetmesini sağlar; kaynak boşa çıktığında çerçeveye haber vermek için saklanır. `begin()` uygulama bir işe başlarken çağrılır ve sayacı artırır. `end()` iş bittiğinde sayacı azaltır; sayaç sıfıra inerse `onTransitionToIdle()` çağrılır ve test çerçevesi beklemeyi bırakır.

En sık yapılan hata `begin` çağrısını yapıp `end` çağrısını bir hata yolunda unutmaktır. O zaman sayaç hiç sıfırlanmaz ve test sonsuza kadar bekler. Çare, `end` çağrısını bir `finally` bloğuna koymaktır.

#### Bütçenin Nasıl Konduğu

Bekleme bütçesi iki farklı soruyu yanıtlar ve ikisini karıştırmak yaygın bir hatadır. Birinci soru "test en çok kaç tur beklemeye razı" sorusudur. İkinci soru "uygulama en çok kaç turda hazır oluyor" sorusudur. Sabit beklemede bu iki soru tek sayıyla yanıtlanır: test, uygulamanın en çok kaç turda hazır olacağını tahmin eder ve o kadar bekler. Tahmin gözlenen değerlere dayanıyorsa yeterli görünür, ama gözlenen değerler yeni koşumları kapsamaz.

Koşula bağlı beklemede ikisi ayrılır. Test hazır olunca ilerler; üst sınır yalnız uygulamanın hiç hazır olmadığı durumda devreye girer. Bu derste üst sınırı gözlenen en kötü tur olarak koyduk: 9. Sekiz koşum kısa bir süreyi temsil eder, bu yüzden gerçek bir ekip üst sınırı gözlenen en kötü turun üstünde, bir güvenlik payıyla koyar. Bu pay bir ürün kararıdır.

#### Kararlılığı Ölçmek

Kırılgan bir testi onarmadan önce ne kadar kırılgan olduğunu bilmek gerekir. Testi tek bir koşumda koşturmak yetmez; aynı testi yinelemeyle çok kez koşturup kaç kez kaldığını saymak gerekir. Bu derste sekiz koşum ve hazır olma turlarının dizisi bu yinelemenin modelidir. Sekiz koşumun ikisinde kalma, ekip için bir kırılganlık işaretidir.

Ölçüm, onarımın etkisini de gösterir. Onarımdan sonra aynı sayıda koşum yinelenir ve kalan koşum sayısı sıfırsa onarım tutmuştur. Ama sıfır kalan koşum, test kararlıdır anlamına gelmez; yalnız bu koşumlarda kırılmadı anlamına gelir. Koşum sayısı arttıkça kanıt güçlenir. Bu yüzden onarım öncesi ve sonrası aynı sayıda koşumla karşılaştırılır.

#### Kırılgan Testle Ne Yapılır

Kırılgan bir test bulunduğunda üç yol vardır. İlki testi karantinaya almaktır: sonuç raporlanır ama hattı durdurmaz. Karantina geçici bir önlemdir ve bir son tarihle birlikte konur. İkincisi kök nedeni bulup onarmaktır: zamanlamayı koşula bağlamak, paylaşılan durumu temizlemek, bağımlılığı sahtelemek. Üçüncüsü testi silmektir; test kimsenin güvenmediği bir sonuç veriyorsa, hiç sonuç vermemesi daha dürüsttür.

Karantina sürekli bir çözüm olursa test paketi yavaş yavaş gürültüyle dolar ve ekip kırmızı sonuçlara alışır; alışılmış bir kırmızı ise gerçek bir hatayı da gizler. Ekip karantinadaki test sayısını da bir bütçeyle izleyebilir; bu kursun ölçüsü tam bunu sorar.

#### Yaygın Yanılgılar

İlk yanılgı, "kırılgan testi yeniden denemek sorunu çözer" düşüncesidir. Yeniden deneme testi geçirir, ama aşımı gizler: bir koşumda iki kez deneyen test, iki koşumun maliyetini öder ve kırılganlığı raporlardan siler. Bu derste ölçülen şey tam olarak bu gizlenen aşımdır.

İkinci yanılgı, "kırılgan testin sorumlusu testtir" düşüncesidir. Zamanlamayı belirsiz bırakan uygulama kodu da kırılganlığın nedeni olabilir. Uygulama hazır olduğunu bildirmiyorsa, test ne kadar iyi yazılırsa yazılsın tahmin yürütmek zorunda kalır. Boşta kaynağı bu iki tarafı birbirine bağlar.

Üçüncü yanılgı, "bekleme süresini bir kez ayarlayıp unutmak yeterlidir" düşüncesidir. Dokuz turluk bekleme bugün tüm koşumları geçirebilir; yarın donanım ya da uygulama değişir ve onuncu turda hazır olan bir koşum çıkar. Sabit değerler eskir, koşula bağlı bekleme eskimez.

#### Sınırlayıcı Ölçüm

Bu ölçümün sınırlayıcısı koşula bağlı yoklamadır. Gerekçesi şudur: sabit beklemede iki kötü seçenek vardı ve ikisi de tahminden doğuyordu. Tahmini koşulla değiştirmek, hazır olma turlarını ve bütçeyi hiç değiştirmeden aynı koşumları yeniden ölçmeyi sağlar. Bütçe bu kez en kötü gözlenen tura konur; çünkü koşula bağlı beklemede amaç aşımı tahmin etmek değil, gözlenen en kötü durumun içinde kalmaktır.

Üçüncü çare, beklemeyi tahmine değil koşula bağlamaktır. Test her turda ekranın hazır olup olmadığına bakar ve hazır olunca ilerler; boşta kaynağı bunu platformda yapar. Bu durumda harcanan tur, o koşumun hazır olma turuna eşittir. Bütçe de başarısızlığa göre değil, gözlenen en kötü hazırlık turuna göre konur: 9 tur.

```kotlin
// Main.kt — kosula bagli yoklama
fun main() {
    val runs = (0 until 8).toList()
    val spent = runs.map { pollRounds(it) }
    println("harcanan turlar: $spent")

    val failed = runs.count { !passesAfterFixedWait(it, pollRounds(it)) }
    println("kalan kosum: $failed")

    val overrun = Overrun("kosula-bagli-yoklama", Budget("tur", 9), spent)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
harcanan turlar: [3, 4, 3, 8, 4, 3, 9, 4]
kalan kosum: 0
kosula-bagli-yoklama: butce=tur<=9 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 0
```

Her koşum kendi hazır olma turunda bitti, hiçbir koşum kalmadı ve ölçüm bütçenin içinde kaldı. Önceki iki çarenin ödediği bedeli bu çare ödemedi: hızlı koşumlar kısa bekledi, yavaş koşumlar yavaş ekranı bekledi.

Bu sonucun sınırı bütçededir. 9 turluk bütçe gözlenen en kötü turdan geliyor. Onuncu turda hazır olan bir koşum ortaya çıkarsa ölçüm yeniden yapılmalıdır. Koşula bağlı yoklama kırılganlığı yok etmez; kırılganlığı üreten düzeneği, yani tahmini kaldırır.

#### Özet

- Bu dersin aşımı: azında. Beş tur bekleyen test, sekiz koşumun ikisinde hazır olma turuna yetişemedi.
- Kırılganlık bir aşım kategorisi değil, aşımı üreten düzenektir; ölçüm yalnız aşımın kaç koşuma yayıldığını sayar.
- Beklemeyi dokuza çıkarmak kalan koşumu sıfırladı ama harcanan turu her koşumda bütçenin üstüne taşıdı: hepsinde.
- Koşula bağlı yoklama her koşumu kendi hazır olma turunda bitirdi ve 9 turluk bütçenin içinde kaldı.
- Boşta kaynağı, uygulamanın meşgul olup olmadığını teste bildirir; test süreyi tahmin etmez, uygulamaya sorar.

#### Sonraki Adım

Test yazıldı, kararlı hâle getirildi ve koşuyor. Ama kodun kendisi hakkında test çalıştırmadan da bir şeyler söylenebilir. Sıradaki ders, `Statik Çözümleme`, bir otomatik denetleyicinin modül başına bulduğu kural ihlallerinin bütçeyi kaç modülde aştığını ölçer.

### Statik Çözümleme

Source: https://academia.sh/tr/kurslar/android-uretim/statik-cozumleme

Statik çözümleme kodu çalıştırmadan okur ve kural ihlallerini listeler. Ders modül başına ihlal sayısının bütçeyi kaç modülde aştığını, temel değer dosyasının sayımı nasıl değiştirdiğini ölçer.

Önceki ders testi kararlı kıldı. Test kodu çalıştırarak doğrular; kodun bir kısmı ise hiç çalıştırılmadan da yanlış olduğunu söyler. Kullanılmayan bir kaynak, kapatılmamış bir akış, yanlış sürümde çağrılan bir API bunlardandır. Bu işi otomatik denetleyici yapar. Bu derste ölçülen iş birimi bir kural ihlalidir ve ölçüm modül başına yapılır. Kursun sorusu aynı: **bu ölçüm bütçesini kaç örnekte aşıyor?** Yanıtlar içinde, azında, çoğunda ve hepsinde; örnek bu kez bir modüldür.

Çekirdek önceki derslerle aynıdır.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

#### Kodu Çalıştırmadan Okumak

Statik çözümleme (static analysis), kaynak kodu çalıştırmadan okuyup bir kural listesine karşı denetler. Bulduğu her şeye ihlal denir. İhlal bir hata olabilir, bir uyarı olabilir ya da denetleyicinin yanlış alarmı (false positive) olabilir: kod doğrudur ama kural onu yanlış okur. Bu üçünü ayırmak denetleyicinin değil, ekibin işidir.

Denetleyicinin davranışı bir yapılandırma dosyasından gelir: hangi kuralın açık olduğu ve hangisinin hata sayıldığı orada yazılır. Temel değer olarak kabul edilen ihlaller ise ayrı bir dosyada tutulur.

```xml
<!-- Platform kodu: lint.xml -->
<lint>
    <issue id="UnusedResources" severity="error" />
    <issue id="HardcodedText" severity="warning" />
    <issue id="OldTargetApi" severity="error">
        <ignore path="legacy/**" />
    </issue>
</lint>
```

Bu parça platforma aittir: hangi kural hata sayılır, hangisi uyarı kalır, hangi yol bir kuraldan muaf tutulur. Model yapılandırmayı okumaz; modül başına ihlal sayısını sabit bir tablodan alır ve bütçeyle karşılaştırır.

**TA1.** Modelde sekiz modülün ihlal sayıları sabit bir tablodur; gerçek bir projede sayılar denetleyicinin sürümüne ve kural kümesine göre değişir. Tablo, aynı kural kümesiyle yapılmış tek bir denetimi temsil eder.

#### Modül Başına İhlal

Yardımcı dosya modülleri ve ihlal sayısını tanımlar. Her modülün iki sayısı vardır: temel değerden önce var olan ihlaller ve yeni eklenen ihlaller. Ayrıca her modülde denetleyicinin yanlış alarm verdiği ihlaller ayrı sayılır.

```kotlin
// Lint.kt — modullerin ihlal tablosu
class Module(val name: String, val existing: Int, val added: Int, val falseAlarms: Int)

val modules: List<Module> = listOf(
    Module("app", 6, 2, 1),
    Module("core", 3, 0, 0),
    Module("data", 7, 1, 2),
    Module("ui", 9, 3, 1),
    Module("network", 2, 0, 0),
    Module("auth", 5, 1, 0),
    Module("feature-cart", 8, 0, 2),
    Module("feature-search", 4, 2, 1),
)

fun lintFindings(m: Module): Int = m.existing + m.added

fun cleanedFindings(m: Module): Int = lintFindings(m) - m.falseAlarms

fun newFindings(m: Module): Int = m.added
```

`lintFindings` modülün ham ihlal sayısıdır. `cleanedFindings` yanlış alarmlar ayıklandıktan sonraki sayıdır. `newFindings` temel değerden sonra kalan, yani yeni eklenen ihlaldir. Ekip, her modülün en çok beş ihlal taşımasına izin veriyor.

```kotlin
// Main.kt
fun main() {
    val samples = modules.map { lintFindings(it) }
    println("ihlaller: $samples")

    val overrun = Overrun("ham-ihlal", Budget("ihlal", 5), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
ihlaller: [8, 3, 8, 12, 2, 6, 8, 6]
ham-ihlal: butce=ihlal<=5 | asan=6/8 -> cogunda
icinde: 0
azinda: 0
cogunda: 1
hepsinde: 0
```

Sekiz modülün altısı beş ihlalin üstünde. Yalnız `core` ve `network` bütçenin içinde. Ölçüm çoğundadır. Kural bu kez de büyüklüğü görmez: `ui` on iki, `auth` altı ihlalle aynı biçimde aşmış sayılır. Nedeni de görmez: bir modül ihlalleri yıllar içinde biriktirmiş olabilir, bir başkası bir hafta içinde yazılmış olabilir.

#### Yanlış Alarmları Ayıklamak

İlk tepki ihlallerin bir kısmının yanlış alarm olduğunu söylemektir. Doğrudur: sekiz modülde toplam yedi yanlış alarm var. Ayıklanırsa sayım değişir. Bunun bütçeyi kaç modülde kurtardığına bakalım.

```kotlin
// Main.kt — yanlis alarmlar ayiklandiktan sonra
fun main() {
    val samples = modules.map { cleanedFindings(it) }
    println("ayiklanmis ihlaller: $samples")

    val overrun = Overrun("ayiklanmis-ihlal", Budget("ihlal", 5), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
ayiklanmis ihlaller: [7, 3, 6, 11, 2, 6, 6, 5]
ayiklanmis-ihlal: butce=ihlal<=5 | asan=5/8 -> cogunda
icinde: 0
azinda: 0
cogunda: 1
hepsinde: 0
```

Beş modül hâlâ bütçenin üstünde ve ölçüm çoğunda kaldı. `feature-search` beş ihlale inerek bütçenin sınırında kaldı; sınırı kesin aşan sayıldığı için artık aşmış değil. Yanlış alarmı ayıklamak sayıyı düşürdü ama sınıfı değiştirmedi. Yanlış alarm, aşımın ana nedeni değildi.

#### İhlalin Türleri

Bir otomatik denetleyici bulduğu her şeye aynı ağırlığı vermez. Her kuralın bir ciddiyeti vardır ve yapılandırma dosyası bunu belirler. Hata düzeyindeki bir ihlal sürekli tümleştirme hattını durdurur; uyarı düzeyindeki ihlal raporda görünür ama hattı durdurmaz; bilgi düzeyindeki ihlal ise yalnız bir öneridir. Aynı ihlal sayısı, ciddiyet dağılımına göre çok farklı anlamlar taşır. Bu derste ölçüm ciddiyeti ayırmaz: bütün ihlaller bir birim sayılır. Bu bir sadeleştirmedir ve gerçek bir projede ekip hata düzeyindekileri ayrı bir bütçeyle izleyebilir.

İhlalin iki ayrı kökeni de vardır. Bazı kurallar gerçekten yanlış kodu yakalar: kapatılmayan bir akış, bildirim dosyasında eksik bir izin, kullanılmayan bir kaynak. Bazıları ise bir stil tercihini zorunlu kılar: sabit yazılmış metin ya da adlandırma. İlk grup hataya, ikinci grup uyuma yöneliktir. Ekip hangisini bütçeye saydığını bilmelidir; yoksa bütçe aşımı ne bir hata ne de bir stil sorunu olarak okunabilir.

#### Yapılandırma Parçasının Satır Satır Okunuşu

`lint.xml` dosyasının kök öğesi `<lint>` yapılandırmanın kapsamını tanımlar. Her `<issue>` öğesi bir kuralı adlandırır ve `id` alanı kuralın kimliğidir. Birinci öğe `UnusedResources` kuralını `error` düzeyine çıkarır: kullanılmayan bir kaynak artık hattı durdurur. İkinci öğe `HardcodedText` kuralını `warning` düzeyinde bırakır: sabit yazılmış bir metin raporda görünür, ama hattı durdurmaz.

Üçüncü öğe `OldTargetApi` kuralını `error` yapar ve içinde bir `<ignore path="legacy/**" />` satırı taşır. Bu satır, `legacy` dizini altındaki dosyaların bu kural için denetlenmeyeceğini söyler. Bu bir temel değer değildir; kuralın belli bir yola uygulanmamasıdır. İkisi arasındaki fark önemlidir. Muafiyet bir karardır ve yapılandırmada görünür kalır. Temel değer ise bir geçmiş kaydıdır ve dosya olarak ayrı tutulur.

#### Yanlış Alarmın Nedenleri

Yanlış alarm, denetleyicinin kodu yanlış anlaması demektir. Kural kodun yalnız yapısına bakar, çalışma zamanını bilmez. Bir kaynağın yalnız yansıma ile kullanılması, kuralın o kaynağı kullanılmamış sanmasına yol açar. Bir işlev başka bir modülden dinamik olarak çağrılıyorsa, kural onu ölü kod sayabilir. Bu durumlarda doğru çare ihlali kodda düzeltmek değil, satırı ya da yolu bilinçli olarak muaf tutmaktır.

Yanlış alarm ayıklamanın iki riski vardır. Birincisi, gerçek ihlallerin yanlış alarm diye ayıklanmasıdır; ekip sayıyı düşürmek için sınırı gevşetir. İkincisi, ayıklamanın belgelenmemesidir; bir yıl sonra kimse neden ayıklandığını bilmez. Bu derste yanlış alarm sayısını tabloda açıkça ayrı tuttuk, çünkü ayıklamayı görünür kılmak bütçenin dürüst kalmasını sağlar.

#### Temel Değerin Riskleri

Temel değer dosyası bir borç defteridir ve her borç defteri gibi büyüyebilir. Üç risk öne çıkar.

Birincisi, görünmez borçtur. Gizlenen 44 ihlal ekip toplantılarında konuşulmaz; çünkü raporda yoktur. Yalnız yeni ihlalleri saymak, birikmiş borcun varlığını unutturur. Bu yüzden gizlenen sayı, görünen sayıyla birlikte raporlanır.

İkincisi, eşleşme kırılganlığıdır. Temel değer ihlali bir dosyaya ve bir kural kimliğine bağlar. Dosya yeniden düzenlenir ya da kod taşınırsa, aynı ihlal temel değerle eşleşmez ve yeni ihlal olarak yeniden görünür. Bu bir yanlış alarm gibi görünür, ama nedeni kodun değişmesidir.

Üçüncüsü, temel değerin yenilenmesidir. Ekip bir gün temel değeri baştan üretirse, o güne kadar eklenen bütün ihlaller eski borca dönüşür. Bu işlem bir karar olarak kaydedilmeli ve gerekçesi yazılmalıdır; yoksa temel değer, bütçeyi sıfırlamanın sessiz bir yoluna dönüşür.

#### Denetimin Hatta Yeri

Statik çözümleme aynı kuralları üç yerde çalıştırılabilir ve her birinin ödünleşimi farklıdır. Geliştiricinin yerel makinesinde, kod yazılırken ya da kaydedilirken çalışırsa geri bildirim en hızlıdır ama ekibin herkesi aynı sürüm ve ayarla çalıştırdığının garantisi yoktur. Bir değişiklik birleştirilmeden önce çekme isteğinde çalışırsa, herkesin aynı ayarla denetlendiği bilinir ve yeni ihlal birleşmeden yakalanır. Gece koşan bir tam denetimde ise bütün modüllerin ham ihlal sayısı toplanır.

Bu derste ölçülen iki sayı iki farklı yere karşılık gelir. Yeni ihlal bütçesi çekme isteği denetimine uygundur: değişiklik en çok bir yeni ihlal getirebilir. Modül başına beş ihlallik ham bütçe gece koşan tam denetime uygundur: modülün toplam borcu izlenir.

#### Bütçeyi Zamanla Sıkılaştırmak

Birikmiş ihlaller varken bütçeyi bir anda sıfıra çekmek mümkün değildir. Ekipler genellikle bütçeyi kademeli düşürür: bugün modül başına beş ihlal, üç ay sonra dört. Her düşüşte aşan modül sayısı artabilir, çünkü sınır sıkılaşmıştır. Bu derste 5'ten 4'e geçişin etkisi görülebilir: ayıklanmış sayımda `feature-search` beş ihlalle sınırda kalıyordu ve bütçe dörde inerse aşan modül sayısı altıya çıkar.

Sıkılaştırma ile iyileştirme aynı şey değildir. Aşan modül sayısı artınca ekip kodun kötüleştiğini sanabilir; oysa sınır değişmiştir. Bu yüzden bütçe değişikliği, ölçüm raporunda ayrı bir not olarak belirtilir. İyileştirme, aşan modül sayısının aynı bütçeyle azalmasıdır.

#### Kural Seçimi

Kural kümesi büyüdükçe ihlal sayısı büyür, ama değer aynı oranda büyümez. Ekip bütün kuralları açarsa her modül bütçeyi aşar ve ölçüm hepsinde çıkar; hepsinde artık ayrım yapmaz. Bütün kurallar kapatılırsa hiçbir modül aşmaz ve ölçüm bir şey söylemez. Yararlı kural kümesi, ekibin gerçekten düzeltmeye razı olduğu kurallardan oluşur. Düzeltilmeyecek bir kuralı açık tutmak, yalnız gürültü üretir.

Kural eklemenin bir yolu da deneme süresidir. Yeni bir kural önce uyarı düzeyinde açılır, birkaç hafta boyunca ihlal sayısı izlenir ve yanlış alarm oranı gözlenir. Kural yararlı ihlaller buluyorsa hata düzeyine çıkarılır; gürültü üretiyorsa kapatılır ya da bir yola özgü muafiyetle daraltılır. Bu sıra, kuralın ekibe ani bir engel olarak çıkmasını önler ve bütçenin anlamını korur: bütçeye yalnız ekibin benimsediği kurallar sayılır.

Benimsenmemiş bir kuralın ihlali, bütçeyi şişirir ve gerçek sorunların görünmesini zorlaştırır. Bu yüzden kural kümesini gözden geçirmek, bütçeyi gözden geçirmek kadar düzenli bir iştir; ekip her çeyrekte hangi kuralların işe yaradığını sorar ve yaramayanı kümeden çıkarır.

#### Yaygın Yanılgılar

İlk yanılgı, "ihlal sayısı sıfırsa kod doğrudur" düşüncesidir. Otomatik denetleyici yalnız tanımlı kuralları bilir. Mantık hatası, yanlış bir hesap ya da güvenlik açığı çoğu zaman kural kümesinin dışında kalır. Sıfır ihlal, kuralların bulabildiği ihlal olmadığı anlamına gelir.

İkinci yanılgı, "temel değer ihlalleri düzeltir" düşüncesidir. Ölçümde bunu gördük: 44 ihlal temel değerde durdu, sayım değişti, kod değişmedi. Temel değer bir ölçüm aracıdır, bir onarım aracı değildir.

Üçüncü yanılgı, "yanlış alarm çok olduğu için denetleyici işe yaramaz" düşüncesidir. Yedi yanlış alarm, kırk dört ihlalin küçük bir kısmıdır. Ölçümde yanlış alarmı ayıklamak aşan modül sayısını altıdan beşe indirdi; yani yanlış alarm gerçekti ama aşımın ana nedeni değildi. Denetleyiciden vazgeçmek, gerçek ihlalleri de bırakmak demektir.

Dördüncü yanılgı, "bütçe bütün modüllerde aynı olmalıdır" düşüncesidir. Bir çekirdek modül ile bir deneme modülü aynı beklentiyle ölçülmeyebilir. Ama bütçeyi modül başına ayırmak ölçümü karıştırır: her modülün kendi sınırı varsa, aşan modül sayısı artık aynı soruyu yanıtlamaz. Bu derste tek bir bütçe koyduk ve bunun bir sadeleştirme olduğunu açıkça söylüyoruz.

#### Sınırlayıcı Ölçüm

Bu ölçümün sınırlayıcısı temel değer dosyasıdır. Gerekçesi şudur: ham sayım borcu ve yeni ihlali birbirine karıştırır. Eski ihlalleri sayımdan düşürmek, modüllerin aynı tablosunu başka bir soruya yanıt verecek biçimde yeniden ölçer. Bütçe de bu soruya uyar: yeni ihlal bütçesi 1'dir, çünkü her değişiklik en çok bir yeni ihlal getirebilmelidir.

Birikmiş ihlalleri bir gecede düzeltmek mümkün değildir. Yaygın çare temel değer (baseline) dosyasıdır: denetleyici o günkü ihlalleri kaydeder ve sonraki denetimlerde yalnız yeni ihlalleri bildirir. Eski ihlaller sayımdan düşer. Bütçe de yeni ihlale göre konur: modül başına en çok bir.

```kotlin
// Main.kt — temel deger dosyasiyla yalniz yeni ihlaller
fun main() {
    val samples = modules.map { newFindings(it) }
    println("yeni ihlaller: $samples")

    val hidden = modules.sumOf { it.existing }
    val visible = modules.sumOf { newFindings(it) }
    println("temel degerde gizlenen: $hidden")
    println("gorunen: $visible")

    val overrun = Overrun("temel-deger", Budget("yeni-ihlal", 1), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
yeni ihlaller: [2, 0, 1, 3, 0, 1, 0, 2]
temel degerde gizlenen: 44
gorunen: 9
temel-deger: butce=yeni-ihlal<=1 | asan=3/8 -> azinda
icinde: 0
azinda: 1
cogunda: 0
hepsinde: 0
```

Ölçüm çoğundadan azına geldi: yalnız `app`, `ui` ve `feature-search` yeni ihlal bütçesini aşıyor. Ama bunun nedeni ihlallerin düzelmesi değil, sayımın değişmesidir. Gizlenen 44 ihlal durduğu yerde duruyor. Temel değer dosyası ihlali düzeltmez; denetleyicinin neye bakacağını değiştirir. Bu iki ölçüm farklı sorulara cevap verir: ilki "bu modülde ne kadar borç var" diye, ikincisi "bu modüle yeni ne eklendi" diye sorar. İkisi birbirinin yerini tutmaz.

**TA2.** Temel değer modelde tek sayıdır: önceki ihlal sayısı. Gerçek bir temel değer dosyası ihlalleri tek tek kaydeder; kod değiştiğinde bir ihlal kaydıyla eşleşmeyebilir ve yeni ihlal olarak yeniden görünebilir.

#### Özet

- Bu dersin aşımı: çoğunda. Modül başına beş ihlallik bütçeyi sekiz modülün altısı aştı.
- Yanlış alarmları ayıklamak aşan modül sayısını altıdan beşe indirdi ama ölçümü çoğunda bıraktı; ana neden yanlış alarm değildi.
- Temel değer dosyası ihlalleri düzeltmez, sayımı değiştirir: 44 ihlal gizlendi, 9 yeni ihlal göründü ve ölçüm azına geldi.
- Ham sayım "ne kadar borç var", temel değerli sayım "ne eklendi" sorusunu yanıtlar; ikisi birlikte okunur.
- Kural ihlalin büyüklüğünü ve nedenini görmez; yalnız bütçenin kaç modülde aşıldığını sayar.

#### Sonraki Adım

Test ve denetim kodun doğruluğunu ve düzenini sınadı. Uygulamanın kullanıcıya ilk görünüşü ise ayrı bir ölçümdür. Sıradaki ders, `Başlangıç Süresi`, uygulama açılırken yapılan işin başlangıç bütçesini kaç örnekte aştığını ölçer.

## Başarım ve Hata Ayıklama

Soğuk, ılık ve sıcak başlangıç, kare atlama nedenleri, sızıntı tespiti ve bellek profili, ağ ve pil tüketiminin ölçümü, başarım gerilemesini yakalayan karşılaştırmalı ölçüm, kod küçültme ve karartma.

### Başlangıç Süresi

Source: https://academia.sh/tr/kurslar/android-uretim/baslangic-suresi

Soğuk, ılık ve sıcak başlatma ilk kareye kadar farklı miktarda iş yapar. Ders sekiz açılışın ilk kare bütçesini kaç örnekte aştığını ölçer; aynı işin iki bütçede iki aşım verdiğini ve ertelenen işin yok olmadığını gösterir.

Önceki derste statik çözümleme koda çalıştırmadan baktı; temel değer dosyası eski ihlalleri sayımdan düşürdü ve yalnız yeni ihlal taşıyan modüller aştı. Bu derste koda çalışırken bakıyoruz. Uygulama kullanıcının aygıtında açılır ve ilk kareyi çizene kadar belli bir miktar iş yapar. Soru, bu işin bir bütçeye sığıp sığmadığıdır.

Kursun ölçüsü değişmez: bir ölçümün bütçesi kaç örnekte aşılıyor. Dört yanıt vardır. Hiçbir örnek bütçeyi aşmıyorsa **içinde**, örneklerin yarısından azı aşıyorsa **azında**, yarısı ya da fazlası aşıyor ama hepsi aşmıyorsa **çoğunda**, hepsi aşıyorsa **hepsinde**. Sınıra eşit örnek aşım sayılmaz. Çekirdekte sekiz örnek her aşımdan birer kez verdi; bu derste aynı sınıflandırma, sekiz açılışa uygulanıyor.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Budget` bir ölçümün adını ve sınırını taşır. `Overrun` örnekleri sınırla karşılaştırır ve aşan örnekleri sayar. `countOverruns` dört aşımı sabit sırayla sayar. Bu dosya kursun her dersinde aynıdır ve bir daha değişmez.

#### Üç Başlatma

Uygulama üç durumdan birinde açılabilir. **Soğuk başlatma** (cold start) sürecin henüz var olmadığı durumdur: platform süreci kurar, uygulamanın başlangıç kodu çalışır, ilk ekran oluşturulur ve ilk kare çizilir. **Ilık başlatma** (warm start) soğuk başlatmadaki işlerin bir alt kümesidir: çoğu durumda süreç hâlâ bellektedir ve yalnız ekranın yeniden oluşturulması gerekir; platform süreci bellekten attıysa süreç de yeniden kurulur, ama ekran kaydedilmiş durumundan yararlanır. **Sıcak başlatma** (hot start) ekranın bellekte durduğu ve yalnızca öne getirildiği durumdur; nesne ve arayüz kurulumu çoğunlukla tekrarlanmaz, yalnız ekran yeniden çizilir.

İlk kare, kullanıcının uygulamadan gördüğü ilk çizimdir. Başlatma süresi, açılma isteğiyle bu kare arasındaki sürenin adıdır. Bu derste süre ölçmüyoruz. Ölçeceğimiz şey ilk kareye kadar yapılan işin adım sayısıdır ve bu sayı modelin içinden gelir.

```kotlin
// Startup.kt — baslatma turune gore ilk kareye kadar yapilan isi adim olarak hesaplar
class Phase(val name: String, val steps: Int)

fun startupPhases(kind: String): List<Phase> = when (kind) {
    "soguk" -> listOf(
        Phase("surec", 2),
        Phase("uygulama", 1),
        Phase("bagimlilik", 4),
        Phase("ekran", 2),
        Phase("cizim", 1),
    )
    "ilik" -> listOf(Phase("ekran", 2), Phase("cizim", 1))
    else -> listOf(Phase("cizim", 1))
}

fun startupWork(kind: String): Int = startupPhases(kind).sumOf { it.steps }
```

`Phase` ilk kareye giden yolda bir aşamayı adlandırır. `startupPhases` başlatma türüne göre aşamaları verir; `startupWork` aşamaların adımlarını toplar. Üç türün neye benzediğine bakalım.

```kotlin
// Main.kt — uc baslatma turunun ilk kareye kadar yaptigi isi doker
fun main() {
    for (kind in listOf("soguk", "ilik", "sicak")) {
        val parts = startupPhases(kind).joinToString(" ") { "${it.name}=${it.steps}" }
        println("$kind: $parts | toplam=${startupWork(kind)}")
    }
}
```

```text
soguk: surec=2 uygulama=1 bagimlilik=4 ekran=2 cizim=1 | toplam=10
ilik: ekran=2 cizim=1 | toplam=3
sicak: cizim=1 | toplam=1
```

**BA1.** Modelde adım, bir aşamanın ağırlığını gösteren sayıdır; saniye ya da kare değildir. Aşamaların adımları ders kararıdır: bağımlılık başlatması soğuk başlatmada en ağır aşama diye kurulmuştur. Gerçek bir uygulamada hangi aşamanın ağır olduğunu bir ölçüm söyler; model yalnız kuralın nasıl çalıştığını gösterir.

Soğuk başlatmanın toplamı ılığınkinin üç katından fazladır ve bunun nedeni görünür: süreç, uygulama ve bağımlılık aşamaları ılık başlatmada hiç yoktur. Bu yüzden başlangıç işinin kaynağını arayan biri önce soğuk başlatmaya bakar. Platformda bu aşamaları izlemek ve ilk etkileşimin bittiğini bildirmek için aşağıdaki parçalar kullanılır.

```kotlin
// Platform kodu: baslatma isini izleme ve ilk etkilesimin bitisini bildirme
import android.app.Application
import android.os.Bundle
import android.os.Trace
import androidx.appcompat.app.AppCompatActivity

class SampleApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Trace.beginSection("uygulama-baslatma")
        try {
            prepareCore()
        } finally {
            Trace.endSection()
        }
    }
}

class HomeActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.home)
    }

    // ilk veri yuklemesi bitince cagrilir
    private fun showFirstContent(items: List<String>) {
        render(items)
        reportFullyDrawn()
    }
}
```

Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. `Trace` çağrıları başlangıç kodunun bir bölümünü geliştirme ortamının profil aracında adlı bir dilim olarak gösterir; `reportFullyDrawn` ise uygulamanın kullanıma hazır olduğu anı platforma bildirir. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Ayrıca soğuk başlatmanın bir kısmı, yani derlenmiş kodun hazır olması, temel profil denen bir dosyayla öne alınabilir. Temel profilin kazancı uygulamaya ve aygıta bağlıdır ve burada bir sayıyla anılmaz.

#### Sekiz Açılış

Şimdi bir bütçe koyalım. İlk kare bütçesi altı adım olsun. Sekiz açılışın türleri sabit bir diziden gelir: iki soğuk, iki ılık, dört sıcak. Her açılışın işi `startupWork` ile hesaplanır, yani örnekler bir model işlevinin dönüşüdür.

```kotlin
// Main.kt — sekiz acilis ilk kare butcesine gore olculur
fun main() {
    val launches = listOf("soguk", "sicak", "ilik", "sicak", "sicak", "ilik", "soguk", "sicak")
    val samples = launches.map { startupWork(it) }
    println("is: $samples")

    val overrun = Overrun("acilis", Budget("ilk-kare", 6), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
is: [10, 1, 3, 1, 1, 3, 10, 1]
acilis: butce=ilk-kare<=6 | asan=2/8 -> azinda
icinde: 0
azinda: 1
cogunda: 0
hepsinde: 0
```

Sekiz açılışın ikisi, soğuk olanlar, altı adımı aştı: 2/8, yani **azında**. Altı ılık ve sıcak açılış bütçenin çok altında kaldı. Bu sonuç ortalama bir açılışın iyi olduğunu söylemez; ortalama burada hesaplanmıyor. Söylediği şey, bütçeyi aşan örneklerin azınlıkta olduğudur. Bir uygulamanın açılışlarının çoğu ılık ya da sıcaksa bu sayı küçük kalır ve yine de kullanıcının ilk karşılaşması, yani soğuk başlatma, bütçeyi hep aşıyor olabilir.

**BA2.** Dizinin iki soğuk, iki ılık ve dört sıcak olması modelin varsayımıdır. Gerçek kullanımda türlerin oranı kullanıcıya, aygıtın bellek baskısına ve uygulamanın ne sıklıkla kapatıldığına göre değişir ve bu oran burada ölçülmüş bir veri olarak sunulmuyor. Kural türlerin oranını bilmez; yalnız elindeki örnekleri sayar.

Kuralın görmediği iki şey var. Birincisi, aşımın büyüklüğü: soğuk başlatma bütçeyi bir adım da aşsa dört adım da aşsa, `asan` aynı kalır. İkincisi, aşımın nedeni: soğuk başlatmanın toplamı on, ama kural bunun bağımlılık aşamasından mı, süreç kurulumundan mı geldiğini bilmez. Neden `startupPhases` çıktısındadır, kuraldaki satırda değil. Kural bedelin nerede ödendiğini değil, bütçenin kaç örnekte aşıldığını sayar.

#### Aynı İş, İki Bütçe

Aynı örnekleri üç farklı bütçeye vuralım. Bütçe işlevin bir özelliği değil, ona yöneltilen bir sorudur; sorunun sıkılığı değişince aynı işlev başka bir aşım verir.

```kotlin
// Main.kt — ayni sekiz acilis uc butceyle olculur
fun main() {
    val launches = listOf("soguk", "sicak", "ilik", "sicak", "sicak", "ilik", "soguk", "sicak")
    val samples = launches.map { startupWork(it) }

    val overruns = listOf(
        Overrun("genis", Budget("ilk-kare", 10), samples),
        Overrun("olculu", Budget("ilk-kare", 6), samples),
        Overrun("siki", Budget("ilk-kare", 2), samples),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
genis: butce=ilk-kare<=10 | asan=0/8 -> icinde
olculu: butce=ilk-kare<=6 | asan=2/8 -> azinda
siki: butce=ilk-kare<=2 | asan=4/8 -> cogunda
icinde: 1
azinda: 1
cogunda: 1
hepsinde: 0
```

Onluk bütçe hiçbir örneği aşmadı, çünkü en ağır açılış tam on adımdı ve sınıra eşit örnek aşım sayılmaz. Altılık bütçe iki, ikilik bütçe dört örneği aştı. İkilik bütçede ılık açılışlar da aşan oldu: üç adımlık ılık başlatma iki adımlık sınırı geçti. Dört aşan örnek sekizin tam yarısıdır ve yarı **çoğunda** sayılır; bir örnek eksik olsaydı sonuç azında olurdu.

**BA3.** Üç bütçenin hangisinin doğru olduğunu ders söylemez. Bütçeyi koyan bir ürün kararıdır ve kural yalnız o karara göre sayar. Burada önemli olan şudur: açılış kodu hiç değişmedi, yalnız soru değişti ve aşım içinden çoğuna geçti. Bu yüzden bir aşım tek başına iyi ya da kötü bir uygulamayı göstermez; hangi bütçeyle, hangi örneklerle sayıldığı birlikte okunur.

#### Bütçeyi Nasıl Seçmeli

Bu derste üç bütçe denedik ve hiçbirini doğru ilan etmedik. Bunun nedeni bütçenin bir ölçüm sonucu değil, bir ürün kararı olmasıdır. Bütçeyi koyan kişi, kullanıcının uygulamayı açtığında en çok ne kadar beklemeye razı olduğunu varsayar; kural bu varsayımı örneklere uygular. Bütçe çok genişse her açılış içinde görünür ve gerileme fark edilmez. Bütçe çok darsa her açılış aşar ve rapor bir şey söylemez hâle gelir, çünkü her şey kırmızıdır.

İyi bir bütçe iki yerden beslenir. Birincisi, uygulamanın kendi geçmişidir: yayımlanmış sürümün ölçümü, sonraki sürümler için bir tavandır. İkincisi, başlatma türüdür. Soğuk, ılık ve sıcak başlatma aynı işi yapmadığı için tek bir bütçeyle hepsini yargılamak yanıltır. Türe göre ayrı bütçe koymak, ya da ölçümü tek bir türe sabitlemek, aşımın neyden geldiğini daha okunur kılar.

Bütçe ayrıca ölçülen anla ilgilidir. Bu derste ilk kare bütçesi ile ilk etkileşim bütçesi ayrı sorulardır: biri kullanıcının ekranı gördüğü ana, öteki ekranın dokunuşa yanıt verebildiği ana bakar. İkisini tek bir sayıda birleştirmek, ertelenen işin nerede ödendiğini gizler.

Başlatma ölçümünde bir tuzak da ölçümün başladığı noktadır. Kullanıcı simgeye dokunduğu anda saati başlatır; uygulamanın kendi kodu ise süreç kurulduktan sonra çalışır. Uygulama içinden alınan bir ölçüm bu yüzden platformun süreç kurma payını görmez. Bu derste modelin `surec` aşaması o payı ayrı bir satır olarak tutar; gerçek bir ölçümde hangi noktadan başlandığı yazılmadan sayı karşılaştırılmaz.

Başlatmayı iyileştiren biri çoğu zaman soğuk başlatmaya odaklanır, çünkü en çok iş orada yapılır. Ama ılık başlatma, kullanıcının uygulamayı gün içinde en sık yaşadığı durum olabilir. İki türü ayrı bütçelerle izlemek, birinin iyileşirken ötekinin geriye gittiğini fark etmeyi sağlar.

Aynı nedenle bir başlatma bütçesi, hangi türün hedeflendiğini adında taşır. "İlk kare bütçesi" belirsiz bir addır; "soğuk başlatma, ilk kare, altı adım" ise kimin neyi neye karşı saydığını söyler. Adı belirli olmayan bir bütçe, yıllar sonra kimsenin yorumlayamadığı bir sayıya dönüşür.

#### Yaygın Yanılgılar

**"Sıcak başlatma ölçülürse başlangıç ölçülmüş olur."** Sıcak başlatmada süreç ve uygulama nesnesi zaten bellektedir; yapılan iş esas olarak ekranı öne getirmektir. Bu türü ölçmek başlatma işinin büyük kısmını dışarıda bırakır. Modelde de sıcak açılış tek adımdır ve hiçbir bütçeyi aşmadı; bu, uygulamanın başlatma kodunun iyi olduğunu göstermez.

**"İşi ilk kareden sonraya ertelemek işi kaldırır."** Ertelenen iş hâlâ yapılır; yalnız kullanıcının ilk kareyi gördüğü ana kadar beklemesi gerekmez. İlk etkileşim bütçesi bu yüzden ayrı bir ölçümdür. Erteleme, işi bir bütçeden öbürüne taşır; iki bütçeyi birlikte okumadan kazanç ilan edilemez.

**"Tek bir ölçüm yeterlidir."** Aynı açılış farklı koşullarda farklı iş yapabilir. Bu yüzden ders sekiz açılışlık bir dizi kurdu ve kuralı örnek sayısı üzerinden işletti. Tek bir örnekte aşım ancak içinde ya da hepsinde olabilir; ara kategoriler ancak birden çok örnekle ortaya çıkar.

**"Aşım sayısı uygulamanın kalitesini söyler."** Sayı, yalnız seçilen bütçe ve seçilen örneklerle birlikte anlam taşır. Aynı uygulama üç bütçede üç farklı aşım verdi; uygulama hiç değişmedi.

#### Sınırlayıcı Ölçüm

Soğuk başlatmanın en ağır aşaması bağımlılık başlatmasıydı. Yaygın bir çözüm, bu işi ilk kareden sonraya ertelemektir: ilk kare çizilir, kullanıcı ekranı görür, bağımlılık arkada hazırlanır. Modelde bunu iki işlevle kurarız. Biri ilk kareye kadar kalan işi, öteki ertelenen işi verir.

```kotlin
// Main.kt — agir bagimlilik ilk kareden sonraya ertelenir; is ilk etkilesime tasinir
fun beforeFirstFrame(kind: String): Int =
    startupPhases(kind).filter { it.name != "bagimlilik" }.sumOf { it.steps }

fun afterFirstFrame(kind: String): Int =
    startupPhases(kind).filter { it.name == "bagimlilik" }.sumOf { it.steps }

fun main() {
    val launches = listOf("soguk", "sicak", "ilik", "sicak", "sicak", "ilik", "soguk", "sicak")
    val first = launches.map { beforeFirstFrame(it) }
    val later = launches.map { afterFirstFrame(it) }
    println("ilk kare: $first")
    println("ilk etkilesim: $later")

    val overruns = listOf(
        Overrun("kare-isi", Budget("ilk-kare", 6), first),
        Overrun("etkilesim-isi", Budget("ilk-etkilesim", 3), later),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
ilk kare: [6, 1, 3, 1, 1, 3, 6, 1]
ilk etkilesim: [4, 0, 0, 0, 0, 0, 4, 0]
kare-isi: butce=ilk-kare<=6 | asan=0/8 -> icinde
etkilesim-isi: butce=ilk-etkilesim<=3 | asan=2/8 -> azinda
icinde: 1
azinda: 1
cogunda: 0
hepsinde: 0
```

İlk kare bütçesi artık **içinde**: soğuk başlatma altı adıma indi ve altı, sınıra eşit olduğu için aşım sayılmadı. Ama iş yok olmadı. Dört adımlık bağımlılık başlatması ilk etkileşime kaldı ve o ayrı bütçe, üç adımlık sınırıyla, yine iki örnekte aşıldı: **azında**. Erteleme bedeli ilk kare satırından alıp etkileşim satırına yazdı.

**BA4.** İlk etkileşim bütçesinin üç adım olması ders kararıdır. Kullanıcının ilk dokunuşunun ne kadar işi kaldırabileceği, uygulamanın ne yaptığına göre değişir. Ertelemenin her zaman kötü olduğu da söylenmiyor: kullanıcı ilk etkileşimden önce çok daha uzun süre ekrana bakabilir ve ertelenen iş bu sürede biter. Model bu paralelliği kurmaz. Kurduğu tek şey, ertelenen işin başka bir bütçede yeniden sayıldığıdır.

Kural burada da aşımın sayısını görür, büyüklüğünü ve nedenini görmez. İlk kare satırındaki iyileşme gerçektir, ama bütün resmi yalnız o satır vermez. Başlangıç işini azaltmak için işi başka bir yere taşımak ile işi gerçekten kaldırmak ayrı şeylerdir; kural bunları ancak iki bütçe yan yana konursa ayırır.

#### Özet

- Bu dersin aşımı: azında. Sekiz açılışın ikisi, soğuk olanlar, altı adımlık ilk kare bütçesini aştı.
- Soğuk başlatma süreç, uygulama, bağımlılık, ekran ve çizim aşamalarını taşır; ılık başlatma ekranı ve çizimi, sıcak başlatma yalnız çizimi yapar.
- Aynı sekiz örnek on adımlık bütçede içinde, altı adımlıkta azında, iki adımlıkta çoğunda çıktı. Aşım işlevin değil, işlevle bütçenin ortak özelliğidir.
- Sınıra eşit örnek aşım sayılmaz: altı adımlık ilk kare işi altı adımlık bütçeyi aşmaz.
- Bağımlılık başlatmasını ilk kareden sonraya ertelemek ilk kareyi içinde yaptı; iş yok olmadı, ayrı bir ilk etkileşim bütçesinde azında olarak yeniden göründü.

#### Sonraki Adım

İlk kare çizildi, ama uygulamanın işi bitmedi. Kullanıcı bir listeyi kaydırdığında her kare yeniden çizilir ve her kare kendi bütçesine sahiptir. Sıradaki ders, `Çizim Akıcılığı`, kaydırılan bir listede her karenin bütün öğeleri yeniden oluşturmasının kare bütçesini kaç karede aştığını ölçer.

### Çizim Akıcılığı

Source: https://academia.sh/tr/kurslar/android-uretim/cizim-akiciligi

Akıcı kaydırma, her karenin bütçesi içinde kalmasına bağlıdır. Ders kaydırılan bir listede her karenin bütün öğeleri yeniden oluşturmasını ölçer, kararlı anahtarın yeniden oluşturulan işi nasıl daralttığını ve kuralın aşımın büyüklüğünü neden görmediğini gösterir.

Önceki derste ilk kare çizildi ve soğuk başlatmanın ağır işi başka bir bütçeye taşındı. Kullanıcı artık ekrana bakıyor ve bir listeyi kaydırıyor. Kaydırma sürdükçe her kare yeniden çizilir; her kare, ekranın yenileme hızına bağlı bir süre içinde hazır olmak zorundadır. Bu süreye **kare bütçesi** (frame budget) diyoruz. Bütçeye yetişemeyen kare **atlanan kare** (dropped frame) olur; kullanıcı bunu takılma olarak görür.

Bu derste süre ölçmüyoruz, çünkü bir ölçüm ortamının süresi güvenilir bir sayı değildir. Onun yerine kare başına yapılan işi sayıyoruz: bir karede kaç öğe yeniden oluşturuldu. Bütçe de aynı birimdedir. Ölçü kursun ölçüsüdür, yani bu bütçe kaç karede aşıldı.

Ölçü, kursun her dersinde aynı iki sınıfla kurulur. `Budget` ölçümün adını ve sınırını taşır, `Overrun` aşan örnekleri sayıp dört aşımdan birini verir, `countOverruns` dört aşımı sabit sırayla sayar.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

#### Kaydırılan Liste

Bir listede yirmi öğe olsun, ekrana aynı anda beşi sığsın. Kaydırma her karede listenin ilk görünen öğesini kaydırır. Aşağıdaki yardımcı dosya öğeyi, görünen pencereyi ve en kaba kare işini tanımlar.

```kotlin
// Frames.kt — kaydirilan listede bir karede yeniden olusturulan ogeleri hesaplar
class Item(val key: Int)

fun buildItems(count: Int): List<Item> = List(count) { Item(it) }

fun visible(items: List<Item>, first: Int, window: Int): List<Item> =
    items.drop(first).take(window)

fun frameWork(items: List<Item>): Int = items.size
```

`Item` bir öğeyi ve ona verilen anahtarı taşır. `visible` ilk görünen öğeden başlayarak ekrana sığan öğeleri verir. `frameWork` ise en kaba kuralı kurar: kare, listenin bütün öğelerini yeniden oluşturur. Önce kaydırmanın kendisine bakalım. Sekiz karede ilk görünen öğenin yeri sabit bir diziden gelir: yavaş bir kaydırma, bir duraklama ve ardından hızlı bir kaydırma.

```kotlin
// Main.kt — sekiz karede ekrana giren ogeler
fun main() {
    val items = buildItems(20)
    val positions = listOf(0, 1, 2, 2, 3, 8, 9, 9)
    for ((index, first) in positions.withIndex()) {
        val keys = visible(items, first, 5).map { it.key }
        println("kare-$index: $keys")
    }
}
```

```text
kare-0: [0, 1, 2, 3, 4]
kare-1: [1, 2, 3, 4, 5]
kare-2: [2, 3, 4, 5, 6]
kare-3: [2, 3, 4, 5, 6]
kare-4: [3, 4, 5, 6, 7]
kare-5: [8, 9, 10, 11, 12]
kare-6: [9, 10, 11, 12, 13]
kare-7: [9, 10, 11, 12, 13]
```

**BA1.** Kaydırma dizisi modelin girdisidir. Kare-3 duraklamadır, çünkü ilk görünen öğe değişmedi. Kare-5, parmağın hızla sürüklediği bir kaydırmadır: görünen pencere bir kareden ötekine beş öğe atladı, yani önceki karedeki öğelerin hiçbiri ekranda kalmadı. Gerçek bir kaydırmanın bu biçimde olduğu söylenmiyor; dizi, kuralı göstermek için seçildi.

#### Her Karede Her Şey

Kaba kuralı bütçeyle ölçelim. Kare bütçesi dört öğe olsun. Yirmi öğelik listede her karenin işi yirmi olur, çünkü kare ekranda görünen beşini değil, listenin hepsini yeniden oluşturur. Ölçümü üç liste uzunluğuyla yapıyoruz. Örnekler bir model işlevinin dönüşüdür: her kare için `frameWork` çağrılır.

```kotlin
// Main.kt — her karede butun ogeler yeniden olusturulur
fun main() {
    val positions = listOf(0, 1, 2, 2, 3, 8, 9, 9)
    val budget = Budget("kare", 4)

    val overruns = listOf(20, 12, 4).map { count ->
        val items = buildItems(count)
        val samples = positions.map { frameWork(items) }
        Overrun("liste-$count", budget, samples)
    }
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
liste-20: butce=kare<=4 | asan=8/8 -> hepsinde
liste-12: butce=kare<=4 | asan=8/8 -> hepsinde
liste-4: butce=kare<=4 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 2
```

Yirmi ve on iki öğelik listelerde sekiz karenin sekizi bütçeyi aştı: **hepsinde**. Dört öğelik listede hiçbiri aşmadı: **içinde**. Kod aynıdır ve yalnız listenin uzunluğu değişti. Bunun nedeni modelin kurduğu kuraldır: kare işi ekranda görünen öğe sayısına değil, listedeki öğe sayısına bağlıdır. Liste kısayken kural sezilmez, uzadıkça her kare aynı oranda ağırlaşır.

**BA2.** Bütçenin dört öğe olması ders kararıdır. Kare bütçesi gerçekte süredir ve kaç öğenin bu süreye sığdığı öğenin ne kadar karmaşık olduğuna, aygıta ve ekranın yenileme hızına bağlıdır. Modeldeki dört öğe bir sığma sınırı iddiası değil, ölçüm biriminin bir seçimidir. Öğelerin hepsinin aynı ağırlıkta olduğu da bir varsayımdır.

Burada hepsinde, kötü bir kodun değil, kuralın görebildiği bir durumun adıdır: her kare bütçeyi aştı. Kural karelerin ne kadar aştığını söylemez. Yirmi öğelik listenin her karesi bütçenin beş katıdır, on iki öğelik listeninki üç katı; ikisi de aynı satıra, **hepsinde**ye düşer. Büyüklüğü görmek için ya `frameWork` çıktısına bakılır ya da bütçe aşamalı olarak sıkılaştırılır.

Platformda atlanan kareleri üretimde izlemek için bir kare ölçüm kitaplığı kullanılır. Aşağıdaki parça pencerenin her karesini dinler ve atlanan kareleri kaydeder.

```kotlin
// Platform kodu: atlanan kareleri izleme
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.metrics.performance.JankStats

class ListActivity : ComponentActivity() {
    private lateinit var jankStats: JankStats

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        jankStats = JankStats.createAndTrack(window) { frameData ->
            if (frameData.isJank) {
                recordJank(frameData.states)
            }
        }
    }

    override fun onResume() {
        super.onResume()
        jankStats.isTrackingEnabled = true
    }

    override fun onPause() {
        super.onPause()
        jankStats.isTrackingEnabled = false
    }
}
```

Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. `JankStats` her kare için bir veri nesnesi verir ve bu nesne o karenin atlanıp atlanmadığını söyler. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Gerçek kitaplık atlanan kareyi süreye göre belirler, modelin adım sayısına göre değil.

#### Kare Bütçesini Okumak

Kare bütçesi, bir karenin çizilmesi için ayrılan iş miktarının üst sınırıdır. Ekran belirli aralıklarla yeni bir kare ister; o aralıkta hazır olmayan kare atlanır ve kullanıcı bunu takılma olarak görür. Modelde aralık yerine iş adımı sayısını kullanıyoruz, çünkü bu depoda süre ölçmüyoruz. Bütçe, bir karede yapılabilecek en çok iş adımıdır ve kural kaç karenin bu sınırı geçtiğini sayar.

Bu çerçevenin iki sonucu var. Birincisi, tek bir pahalı kare bile aşım sayılır; kullanıcı da çoğu zaman tek bir takılmayı fark eder. İkincisi, kural karelerin sırasını bilmez. Art arda üç kare aşan bir kaydırma ile aralara dağılmış üç aşım, aynı sayıyla raporlanır. Oysa kullanıcı için ilki belirgin bir donma, ikincisi hafif bir pürüzdür.

Yeniden oluşturma bu çerçevede maliyetin kaynağıdır. Bir kare, ekranda görünen her öğeyi baştan oluşturuyorsa, iş öğe sayısıyla büyür. Listede yalnız değişen öğeler yeniden oluşturulursa iş, değişen öğe sayısıyla sınırlı kalır. Kararlı anahtar, hangi öğenin değiştiğini ayırt etmeyi sağlayan kimliktir.

#### Kaydırma Hızı Neden Önemlidir

Dersin kaydırma dizisinde pencere çoğu karede bir öğe ilerledi, bir karede hiç ilerlemedi, bir karede beş öğe birden atladı. Bu üç durumun maliyeti aynı değildir. Duraklayan karede yeni öğe yoktur; yavaş kaydırmada her kare bir öğe getirir; hızlı kaydırmada pencerenin tamamı yenilenir. Akıllı bir liste ilk durumda neredeyse hiç iş yapmaz, ikincide az, üçüncüde pencere boyu iş yapar.

Bu fark ölçüm tasarımını etkiler. Yalnız yavaş kaydırmayla sınanan bir liste temiz çıkar, hızlı kaydırmayla sınanan aynı liste bütçeyi aşar. Bu yüzden kare örneklerini tek bir kullanım biçiminden değil, kullanıcının gerçekten yaptığı hareketlerin karışımından seçmek gerekir. Modelde karışımı biz yazdık; gerçek bir uygulamada karışımı kullanım kayıtları söyler ve bunu bir model değil yalnız o kayıtlar bilir.

#### Pencere Dışındaki İş

Tembel liste ekrana sığandan biraz fazlasını önceden hazırlayabilir; kullanıcı kaydırdığında öğe hazırdır. Bu önceden hazırlama işi ortadan kaldırmaz, zamanını kaydırmadan önceye çeker. Kare bütçesi açısından kazanç gerçektir, çünkü iş kaydırma karelerinden çıkar; ama hazırlık da bir yerde ödenir ve o yer bir başka bütçedir. Aynı ilke başlatma dersinde ertelenen iş için de geçerliydi: iş bir bütçeden ötekine taşınabilir ve iki bütçe birlikte okunmadıkça iyileşme tam sayılmaz.

Modelin sınırı burada belirgindir. `frameWork` yalnız bir karede yeniden oluşturulan öğeleri sayar ve önceden hazırlanan öğeleri hiçbir yere yazmaz. Önceden hazırlamanın etkisini ölçmek isteyen biri modele ikinci bir sayaç eklemelidir; bu derste eklenmedi, çünkü ders tek bir soruya, yeniden oluşturmanın kare bütçesini kaç karede aştığına cevap veriyor.

Ölçümü kare bütçesine bağlamanın bir yararı da hedefin açık olmasıdır. "Liste akıcı olsun" bir dilektir; "yirmi öğelik listede sekiz karenin hiçbiri dört öğeyi aşmasın" ise sınanabilir bir cümledir. Sınanabilir hedef, değişiklik yapıldığında sonucun iyileşip iyileşmediğini tartışmaya yer bırakmadan gösterir. Bütçeyi ve örnek dizisini kodla birlikte saklamak, bu cümlenin zamanla unutulmamasını sağlar.

Kare bütçesini aşan karelerin hangi öğeden geldiğini bulmak ikinci adımdır. İlk adım kaç karenin aştığını bilmektir; ikincisi o karelerin ortak noktasını aramaktır. Bu derste ortak nokta, kaydırma dizisindeki ilk kare ile beş öğelik sıçramaydı ve ikisi de örneklerden okunabildi. Gerçek bir uygulamada bu bilgiyi profil aracının kare başına döküm verdiği çıktı sağlar.

#### Yaygın Yanılgılar

**"Liste kısaysa çizim sorunu yoktur."** Kısa liste bütçenin içinde kalır; ders bunu dört öğelik listeyle gösterdi. Ama bu, aynı kodun uzun listede de bütçeyi tutacağı anlamına gelmez. İş, öğe sayısıyla büyür ve ölçüm yalnız sınadığı boyut için geçerlidir.

**"Kararlı anahtar çizimi hızlandırır."** Anahtar işin kendisini azaltmaz; hangi işin tekrarlanmayacağını belirler. Anahtar yanlış seçilirse, örneğin sıra numarası kimlik diye kullanılırsa, öğeler yer değiştirdiğinde aynı öğe farklı kimlikle görünür ve iş yeniden başlar.

**"Takılma tek sayıyla özetlenir."** Aşan kare sayısı takılmanın sıklığını verir; büyüklüğünü ve ardışıklığını vermez. Bu yüzden dersin sınırlayıcı ölçümü, iyileşmeyi aşım kategorisiyle değil, kare başına işle birlikte okumayı öğretir.

**"Çizim yalnız çizim koduyla ilgilidir."** Bir karenin işi, çizimden önce yapılan hazırlıkları da içerir: veri dönüşümü, yerleşim hesabı, görünür öğelerin belirlenmesi. Modelde bunların hepsi `frameWork` içinde toplandı; gerçek uygulamada hangi parçanın pahalı olduğunu profil aracı söyler.

#### Sınırlayıcı Ölçüm

Kaba kuralın çaresi, her öğeye kararlı bir anahtar vermek ve yalnız anahtarı değişen ya da yeni görünen öğeyi yeniden oluşturmaktır. Kaydırılan bir listede bir öğe ekrandan çıkıp girmedikçe aynı anahtarla tanınır; platform onun içeriğini yeniden kurmak zorunda kalmaz. Aşağıdaki parça bunun platformdaki karşılığıdır.

```kotlin
// Platform kodu: kararli anahtarli kaydirilan liste
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(messages, key = { it.id }) { message ->
            MessageRow(message)
        }
    }
}
```

Tembel liste yalnız ekrana sığan öğeleri oluşturur ve anahtar her öğeye değişmeyen bir kimlik verir. Modelde bunu şöyle kuruyoruz: bir karenin işi, önceki karede görünmeyen öğelerin sayısıdır. İlk karede önceki kare yoktur, bu yüzden pencerenin hepsi yeniden oluşturulur.

```kotlin
// Main.kt — kararli anahtarla yalniz ekrana giren ogeler yeniden olusturulur
fun keyedWork(previous: List<Item>, current: List<Item>): Int {
    val seen = previous.map { it.key }.toSet()
    return current.count { it.key !in seen }
}

fun main() {
    val items = buildItems(20)
    val positions = listOf(0, 1, 2, 2, 3, 8, 9, 9)
    val windows = positions.map { visible(items, it, 5) }
    val samples = windows.mapIndexed { index, now ->
        keyedWork(if (index == 0) emptyList() else windows[index - 1], now)
    }
    println("is: $samples")

    val overrun = Overrun("anahtarli", Budget("kare", 4), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
is: [5, 1, 1, 0, 1, 5, 1, 0]
anahtarli: butce=kare<=4 | asan=2/8 -> azinda
icinde: 0
azinda: 1
cogunda: 0
hepsinde: 0
```

Aynı kaydırma, aynı bütçe ve aynı yirmi öğe: aşım **hepsinde**den **azında**ya indi. Yeniden oluşturulan iş yirmiden bire, bazen sıfıra düştü. Ama iki kare hâlâ bütçeyi aşıyor. Birincisi ilk karedir; liste ilk kez kurulurken beş öğe de yenidir. İkincisi hızlı kaydırmanın beşinci karesidir; pencere beş öğe birden atladığı için beşi de yeni sayılır. Anahtar, yeniden oluşturmayı ekrana giren öğelere daraltır ama giren öğe sayısı bütçeden büyükse kareyi kurtarmaz.

**BA3.** Modeldeki "yeni görünen öğe" ile gerçek bir listedeki yeniden oluşturma birebir aynı değildir. Gerçekte bir öğenin içeriği değişirse, anahtarı aynı kalsa bile yeniden oluşturulur; modelde içerik değişmez. Anahtar yalnız bir öğenin kimliğini koruyarak gereksiz yeniden oluşturmayı önler. Kural bu farkı da görmez.

Kuralın görmediği şeyi açıkça söyleyelim. Hepsinde ile azında arasındaki fark, kuralın gözünde bir sınıf farkıdır, ama iş açısından çok daha büyüktür: toplam iş yirmi öğe çarpı sekiz karedir, yani yüz altmış öğeden on dörde indi. Kural bu oranı vermez, yalnız aşan kare sayısını verir. Aşımın büyüklüğünü görmek istiyorsak örneklerin kendisine, yani `is:` satırına bakarız. Kural nedeni de söylemez: ilk karedeki aşım liste kurulumundan, beşinci karedeki aşım hızlı kaydırmadan gelir ve ikisi aynı sayıda aşım olarak sayılır.

#### Özet

- Bu dersin aşımı: hepsinde. Yirmi öğelik listede her kare bütün öğeleri yeniden oluşturdu ve sekiz karenin sekizi dört öğelik bütçeyi aştı.
- Kare bütçesi gerçekte süredir; bu derste aynı birimde bir iş sayısı olarak, kare başına yeniden oluşturulan öğe diye ölçüldü ve süre yazılmadı.
- Aynı kod, dört öğelik listede içinde, on iki ve yirmi öğelikte hepsinde çıktı: kaba kuralda kare işi görünen öğeye değil, liste uzunluğuna bağlıdır.
- Kararlı anahtar yeniden oluşturmayı ekrana giren öğelerle sınırladı ve aşımı hepsinde sınıfından azında sınıfına indirdi. Kalan iki aşım ilk kare ile beş öğelik sıçramaydı.
- Kural aşan kare sayısını görür; aşımın büyüklüğünü ve nedenini görmez.

#### Sonraki Adım

Kaydırma akıcı olsa bile her kare belleğe yeni nesneler bırakır. Bazıları kullanıldıktan sonra serbest kalmaz ve ekran kapansa da bellekte durur. Sıradaki ders, `Bellek Yönetimi`, kaydı kaldırılmayan bir dinleyicinin her açma ve kapama döngüsünde bir ekranı tutmasının tutulan nesne sayısını kaç döngüde bütçenin üstüne çıkardığını ölçer.

### Bellek Yönetimi

Source: https://academia.sh/tr/kurslar/android-uretim/bellek-yonetimi

Kapanan bir ekranın bellekte kalması bellek sızıntısıdır. Ders kaydı kaldırılmayan bir dinleyicinin her döngüde bir ekranı tuttuğunu, tutulan nesne sayısının bütçeyi kaç döngüde aştığını ve aynı sızıntının bütçeye göre kategori değiştirdiğini ölçer.

Önceki derste kaydırma kare bütçesine sığdı, ama her kare belleğe nesne bırakır ve bu nesnelerin bir kısmı işi bittiğinde serbest kalmaz. Bellek sorunu çizim sorunundan farklı görünür. Takılma anında fark edilir; bellek ise sessizce dolar ve uygulama ancak platform onu sonlandırdığında ya da bellek yetmediğinde fark edilir. Bu yüzden ölçümü bir kare yerine bir döngüye bağlıyoruz: ekranı açıp kapatmak bir döngüdür ve her döngünün sonunda bellekte kaç nesne kaldığına bakılır.

Sorunun adı **bellek sızıntısı**dır (memory leak). Çöp toplama, kimsenin erişemediği nesneyi serbest bırakır. Bir nesneye hâlâ erişen bir yol varsa, yani başka bir nesne ona referans veriyorsa, o nesne işi bitmiş olsa da serbest kalmaz. Bu yolla bellekte kalan nesneye **tutulan nesne** (retained object) diyoruz. Sızıntı, tutulan nesnelerin döngü başına biriktiği durumdur.

Ölçü yine kursun ölçüsüdür. Tutulan nesne sayısının bütçesi kaç döngünün sonunda aşıldı.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

Bu dosya kursun her dersinde aynıdır. `Budget` ölçümün adını ve sınırını taşır, `Overrun` aşan örnekleri sayıp dört aşımdan birini verir.

#### Kaydı Kaldırılmayan Dinleyici

Sızıntının en yaygın biçimi şudur: uzun yaşayan bir nesne, kısa yaşayan bir nesneye referans tutar. Uygulama boyunca yaşayan bir veri kaynağı olsun. Ekran açıldığında kendini bu kaynağa dinleyici olarak kaydeder, çünkü güncellemeleri almak ister. Ekran kapandığında kaydını kaldırmazsa kaynak, ekranı dinleyici listesinde tutmaya devam eder. Ekranın işi bitmiştir ama ona giden yol sürmektedir; çöp toplama onu serbest bırakamaz.

```kotlin
// Retention.kt — kaydi kaldirilmayan dinleyici kapanan ekrani bellekte tutar
class Screen(val id: Int) {
    var closed: Boolean = false
}

class Source {
    private val strong = mutableListOf<Screen>()
    private val weak = mutableListOf<Screen>()

    fun register(screen: Screen) {
        strong.add(screen)
    }

    fun registerWeak(screen: Screen) {
        weak.add(screen)
    }

    fun unregister(screen: Screen) {
        strong.remove(screen)
        weak.remove(screen)
    }

    // zayif referans kapanan ekrani tutmaz; guclu referans tutar
    fun retainedObjects(): Int = strong.size + weak.count { !it.closed }
}

fun cycle(source: Source, id: Int, how: String): Int {
    val screen = Screen(id)
    when (how) {
        "guclu" -> source.register(screen)
        "zayif" -> source.registerWeak(screen)
        "yasam" -> {
            source.register(screen)
            source.unregister(screen)
        }
    }
    screen.closed = true
    return source.retainedObjects()
}
```

`Source` uygulama boyunca yaşayan kaynaktır. `register` güçlü bir referansla, `registerWeak` zayıf bir referansla kayıt tutar. `cycle` bir döngüdür: ekran açılır, kaynağa kaydolur, kapanır ve kaynağın o anda kaç ekran tuttuğu döner. `how` kaydın nasıl yapıldığını söyler. Önce kaydı hiç kaldırmayan, yani `guclu` olan biçimi sekiz döngü boyunca çalıştıralım. Bütçe üç nesne olsun.

```kotlin
// Main.kt — kaydi kaldirilmayan dinleyici sekiz dongu boyunca ekran biriktirir
fun main() {
    val source = Source()
    val samples = (1..8).map { cycle(source, it, "guclu") }
    println("tutulan: $samples")

    val overrun = Overrun("sizinti", Budget("tutulan", 3), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
tutulan: [1, 2, 3, 4, 5, 6, 7, 8]
sizinti: butce=tutulan<=3 | asan=5/8 -> cogunda
icinde: 0
azinda: 0
cogunda: 1
hepsinde: 0
```

Her döngü bir ekran bıraktı ve tutulan nesne sayısı birer birer arttı. İlk üç döngü bütçenin içinde kaldı, dördüncüden sekizinciye kadar beş döngü aştı: 5/8, yani **çoğunda**. Sızıntının doğası burada görünür: bir döngüde fark edilmez, ama biriken sayı er ya da geç her bütçeyi geçer. Bu yüzden sızıntı, bir tek döngüye bakarak değil, döngüler boyunca ölçülür.

**BA1.** Modelde her ekran bir nesnedir ve her sızan döngü tam bir nesne bırakır. Gerçek bir ekran, kendisinin yanında görünümlerini, görsellerini ve veri yapılarını da tutar; tutulan belleğin boyutu ekrandan ekrana çok değişir. Model ekranı sayar; tuttuğu belleğin boyutunu kurmaz.

#### Bütçe Sızıntıyı Gizler

Aynı sızıntıyı dört farklı bütçeye vuralım. Sızıntı bir kez ölçülür, örnekler aynıdır; yalnız sınır değişir.

```kotlin
// Main.kt — ayni sizinti dort butceyle olculur
fun main() {
    val source = Source()
    val samples = (1..8).map { cycle(source, it, "guclu") }

    val overruns = listOf(0, 3, 6, 8).map { limit ->
        Overrun("sinir-$limit", Budget("tutulan", limit), samples)
    }
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
sinir-0: butce=tutulan<=0 | asan=8/8 -> hepsinde
sinir-3: butce=tutulan<=3 | asan=5/8 -> cogunda
sinir-6: butce=tutulan<=6 | asan=2/8 -> azinda
sinir-8: butce=tutulan<=8 | asan=0/8 -> icinde
icinde: 1
azinda: 1
cogunda: 1
hepsinde: 1
```

Dört satır, dört aşım ve tek bir sızıntı. Sıfır nesne bütçesinde her döngü aştı. Sekiz nesne bütçesinde hiçbir döngü aşmadı, çünkü sekizinci döngüde tutulan sayı sekizdi ve sınıra eşit örnek aşım sayılmaz. Sızıntı sekiz nesnede de oradaydı; bütçe onu görünmez kıldı. Bu, kuralın zayıf yanıdır: bütçe sızıntıdan geniş seçilirse sızıntı **içinde** diye raporlanır. Bu yüzden sızıntı bütçesi, döngü sayısına göre değil, uygulamanın hiç tutmaması gereken sayıya göre konur. Sızıntıyı yakalayan bütçe çoğu zaman sıfıra ya da sıfıra yakın bir sayıya dayanır.

Platformda tutulan ekranları yakalamak için sanal makine denetimi ve yığın dökümü kullanılır. Aşağıdaki parça ekranların sızmasını günlüğe yazdıran bir denetim kurar ve bir yığın dökümü alan işlevi gösterir.

```kotlin
// Platform kodu: sizinti denetimi ve yigin dokumu
import android.app.Application
import android.os.Debug
import android.os.StrictMode

class SampleApp : Application() {
    override fun onCreate() {
        super.onCreate()
        StrictMode.setVmPolicy(
            StrictMode.VmPolicy.Builder()
                .detectActivityLeaks()
                .detectLeakedRegistrationObjects()
                .penaltyLog()
                .build()
        )
    }
}

fun dumpHeap(path: String) {
    Debug.dumpHprofData(path)
}
```

Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. `StrictMode` sızan ekranları ve kaydı kaldırılmayan yayın alıcılarıyla servis bağlantılarını günlüğe yazdırır; uygulamanın kendi dinleyicileri bu denetime girmez; yığın dökümü ise hangi nesnenin hangi yolla tutulduğunu geliştirme ortamının profil aracında gösterir. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Dökümün neyi göstereceği, yani tutulan yolun hangi nesneden geçtiği, modelin göstermediği bilgidir.

#### Zayıf Referans

Sızıntıyı çözmenin bir yolu, kaynağın ekranı güçlü değil zayıf bir referansla tutmasıdır. **Zayıf referans** (weak reference) çöp toplamayı engellemez: nesneye başka güçlü yol kalmadıysa nesne serbest bırakılır ve zayıf referans boşalır. Modelde bunu kapanan ekranın artık sayılmaması diye kuruyoruz.

```kotlin
// Main.kt — zayif referansla kaydolan ekran kapaninca tutulmaz
fun main() {
    val source = Source()
    val samples = (1..8).map { cycle(source, it, "zayif") }
    println("tutulan: $samples")

    val overrun = Overrun("zayif", Budget("tutulan", 3), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
tutulan: [0, 0, 0, 0, 0, 0, 0, 0]
zayif: butce=tutulan<=3 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 0
```

**BA2.** Modelde zayıf referans kapanan ekranı anında bırakır. Gerçekte çöp toplama bir an gelince çalışır; ekran kapandıktan sonra bir süre, toplama gerçekleşene kadar bellekte durur ve bu süre uygulamadan uygulamaya değişir. Modelin anındalığı bir basitleştirmedir. Ayrıca zayıf referans, kaydı kaldırmanın yerini tutmaz: kaynak, boşalmış kayıtları listesinde taşımaya devam edebilir ve bu da küçük de olsa bir birikimdir.

#### Neden Döngü Boyunca Ölçeriz

Bellek sorunları anlık değil birikimlidir. Tek bir ekranı açıp kapatmak, tutulan nesne sayısını bir artırır ya da hiç artırmaz; bu fark tek bir gözlemde bütçenin altında kalır. Sızıntı, aynı işlemin tekrar tekrar yapılmasıyla görünür olur. Bu yüzden ölçümün örnekleri, art arda yapılan döngülerin sonunda alınan tutulan nesne sayılarıdır ve her örnek bir öncekini içerir.

Bu yapı kuralın okunuşunu da etkiler. Birikimli bir dizide aşım, genellikle dizinin sonuna doğru yoğunlaşır: ilk döngüler içinde kalır, sonrakiler aşar. Dersin ilk ölçümünde aşan döngüler dördüncüden sekizinciye kadardı. Bu yüzden döngü sayısını artırmak sızıntıyı daha görünür kılar; döngü sayısını azaltmak sızıntıyı gizler.

Ölçüm tasarımında bir ayrıntı daha önemlidir: her döngü aynı koşulda başlamalıdır. Bir önceki döngüden kalan durum sonrakini etkilerse, aşım sızıntıdan değil ölçüm düzeninden gelebilir. Bu yüzden modelde her döngü aynı `cycle` işleviyle ve aynı kaynak üzerinde çalışır; farklı olan tek şey kaydın nasıl yapıldığıdır.

Tutulan nesnenin ne olduğu da açık söylenmelidir. Ekran bir nesnedir, ama onu tutan her şey onunla birlikte kalır: ekranın görünümleri, verisi, kullandığı yardımcılar. Bir sızıntıyı bulmak bu yüzden tutulan nesnenin kendisine değil, onu tutan yola bakmayı gerektirir. Yığın dökümü tam olarak bu yolu gösterir.

Sızıntıyı aramak için üç soru sırayla sorulur. Hangi nesne tutuluyor, onu hangi yol tutuyor ve bu yol ne zaman kesilmeliydi. İlk soruya sayaç cevap verir, ikinciye yığın dökümü, üçüncüye uygulamanın yaşam döngüsü. Modelde yol tek bir halkadır: kaynağın listesi. Gerçekte yol birkaç nesneden geçer ve her halka ayrı bir sahibe aittir; sızıntıyı kesmek için doğru halkayı seçmek, kaydı kaldırmaktan daha çok düşünce ister.

Bellek bütçesi bir alt sınır arayışı da değildir. Uygulama açıkken tutulan bir miktar nesne meşrudur: açık ekran, önbelleğe alınmış veri, kullanıcı oturumu. Sızıntı bütçesi bu meşru payı hesaba katar ve yalnız ekran kapandıktan sonra kalan fazlayı sınırlar. Bu yüzden ölçüm ekranın açık olduğu ana değil, kapandığı ana bakar ve model de sayacı her döngünün sonunda okur.

Sızıntı bütçesini sürümler arasında karşılaştırmak da yararlıdır. Önceki sürümde sekiz döngüde sıfır nesne tutan bir ekran, sonraki sürümde sekiz döngüde bir nesne tutuyorsa, bu tek nesne yeni bir sızıntının ilk işaretidir. Bütçe sıfır olduğunda bu işaret hemen aşım olarak görünür; bütçe geniş bırakıldığında yeni sızıntı fark edilmeden birkaç sürüm boyunca birikir.

#### Yaygın Yanılgılar

**"Çöp toplama sızıntıyı temizler."** Çöp toplama yalnız erişilemeyen nesneleri serbest bırakır. Kaynak hâlâ ekrana bir referans tutuyorsa ekran erişilebilirdir ve toplanmaz. Sızıntı tam olarak, işi bitmiş bir nesneye hâlâ erişen bir yolun bulunmasıdır.

**"Zayıf referans her sızıntıyı çözer."** Zayıf referans, nesnenin toplanmasına engel olmaz; ama kaydı kaldırmanın yerini de tutmaz. Kaynak boşalmış kayıtları taşımaya devam edebilir ve bu birikim, nesne kadar büyük olmasa da gerçektir. Doğru çözüm, kaydı nesnenin yaşam döngüsüne bağlamaktır.

**"Ekran kapandıysa bellekten gitmiştir."** Kapanmak, ekranın kullanıcıya görünmemesi demektir; bellekten silinmesi demek değildir. Modelde kaydı kaldırılmayan ekran kapandıktan sonra da kaynağın listesinde kaldı ve sayaç bir daha düşmedi.

**"Düzeltme eski sızıntıyı geri alır."** Dersin sınırlayıcı ölçümü bunu açıkça gösterdi: düzeltmeden önce sızan dört ekran, düzeltmeden sonra da bellekte kaldı. Düzeltme yalnız yeni sızıntıyı durdurur.

**"Bütçe sızıntıyı tanımlar."** Bütçe sızıntıyı değil, bir kabul sınırını tanımlar. Aynı sızıntı dört bütçede dört farklı aşım verdi. Sızıntıyı yakalayan bütçe, uygulamanın hiç tutmaması gereken sayıya yakın bir değere konur.

#### Sınırlayıcı Ölçüm

Sızıntıyı kökünden çözen düzen, dinleyiciyi ekranın yaşam döngüsüne bağlamaktır: ekran görünür olunca kaydolur, görünmez olunca kaydını kaldırır. Platformdaki biçimi şöyledir.

```kotlin
// Platform kodu: dinleyiciyi yasam dongusune baglama
import androidx.appcompat.app.AppCompatActivity

class OrdersActivity : AppCompatActivity() {
    private val listener = OrderListener { orders -> render(orders) }

    override fun onStart() {
        super.onStart()
        OrderSource.register(listener)
    }

    override fun onStop() {
        OrderSource.unregister(listener)
        super.onStop()
    }
}
```

Kayıt `onStart`ta yapılır ve `onStop`ta kaldırılır, böylece kaynağın ekrana giden yolu ekran görünmez olduğunda kesilir. Modelde bu, `yasam` biçimidir: ekran kaydolur ve kapanırken kaydını kaldırır. İki ölçüm yapıyoruz. Birincisinde düzen en baştan doğrudur. İkincisinde ilk dört döngü eski, sızdıran düzenle çalışır ve sonra düzeltme gelir.

```kotlin
// Main.kt — yasam dongusune baglama sonraki dongulerin tutulan nesnesini artirmaz
fun main() {
    val fixed = Source()
    val fromStart = (1..8).map { cycle(fixed, it, "yasam") }

    val patched = Source()
    val afterPatch = (1..8).map { cycle(patched, it, if (it <= 4) "guclu" else "yasam") }

    println("bastan: $fromStart")
    println("sonradan: $afterPatch")

    val overruns = listOf(
        Overrun("bastan", Budget("tutulan", 3), fromStart),
        Overrun("sonradan", Budget("tutulan", 3), afterPatch),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
bastan: [0, 0, 0, 0, 0, 0, 0, 0]
sonradan: [1, 2, 3, 4, 4, 4, 4, 4]
bastan: butce=tutulan<=3 | asan=0/8 -> icinde
sonradan: butce=tutulan<=3 | asan=5/8 -> cogunda
icinde: 1
azinda: 0
cogunda: 1
hepsinde: 0
```

En baştan doğru kurulan düzen **içinde**dir: hiçbir döngüde tutulan nesne yok. Sonradan yapılan düzeltme ise aynı bütçede **çoğunda** çıktı. Düzeltme ilk dörtten sonra yeni ekran sızdırmadı, sayı dörtte durdu; ama dördü de bellekte kaldı. Tutulan nesne sayısı sonraki döngülerde artmadı, önceki sızıntı da kendiliğinden gitmedi. Kuralın gözünde iki satırın farkı, bütçenin altında kalmak ile üstünde kalmak arasındadır; kuralın görmediği şey, ikinci satırda artışın durduğudur. Artışın durduğunu görmek için örneklerin kendisine, yani `sonradan:` satırına bakılır.

**BA3.** İkinci ölçümde ilk dört ekranın serbest kalmadığı, modelin kurduğu bir kuraldır. Gerçek uygulamada sızan ekran, onu tutan kaynak ortadan kalkana kadar, çoğu zaman süreç sonlanana kadar bellekte durur. Düzeltmeyi yayımlamak bu yüzden, düzeltmeden önce sızmış oturumları kurtarmaz. Bunlar ancak uygulama yeniden başlatıldığında temizlenir.

#### Özet

- Bu dersin aşımı: çoğunda. Kaydı kaldırılmayan dinleyici her döngüde bir ekran tuttu ve sekiz döngünün beşi üç nesnelik bütçeyi aştı.
- Bellek sızıntısı, tutulan nesnelerin döngüler boyunca birikmesidir; tek bir döngüye bakarak görülmez.
- Aynı sızıntı sıfır nesnelik bütçede hepsinde, üçte çoğunda, altıda azında, sekizde içinde çıktı. Bütçe sızıntıdan geniş seçilirse sızıntı raporda görünmez.
- Zayıf referansla kaydolan ekran, modelde kapanınca tutulmadı. Gerçekte bu, çöp toplamanın ne zaman çalıştığına bağlıdır.
- Dinleyiciyi yaşam döngüsüne bağlamak sonraki döngülerde tutulan nesne sayısını artırmadı; ama önceden sızmış dört ekranı serbest bırakmadı.

#### Sonraki Adım

Bellek, uygulamanın içinde tutulan şeydir. Uygulama aynı zamanda dışarıyla konuşur: ağa istek yollar, aygıtı uyandırır, pil harcar. Sıradaki ders, `Ağ ve Pil Profili`, bir eşitleme döngüsünde gönderilen istek sayısının bütçeyi kaç döngüde aştığını ve istekleri toplu göndermenin bu sayıyı nasıl düşürdüğünü ölçer.

### Ağ ve Pil Profili

Source: https://academia.sh/tr/kurslar/android-uretim/ag-ve-pil-profili

Bir eşitleme döngüsünde gönderilen istek sayısı pili ve ağı belirler. Ders istekleri tek tek, toplu ve yoklamayla göndermeyi aynı değişiklik akışında ölçer; yoklamanın değişiklik olmayan döngülerde de bütçeyi aştığını gösterir.

Bellek, uygulamanın kendi içinde tuttuğu şeydi. Ağ ve pil ise uygulamanın dışarıya karşı harcadığı şeydir. Bir sızıntı belleği sessizce doldurur; sık gönderilen küçük istekler de pili sessizce boşaltır. Kullanıcı bunu tek bir ekranda görmez, günün sonunda pilin ne kadar çabuk bittiğine bakınca fark eder. Bu dersin sorusu o yüzden şudur: eşitleme çalıştığında bir döngüde kaç istek gidiyor ve bu sayı bütçeyi kaç döngüde aşıyor.

İsteğin pile maliyeti, gönderdiği veriden çok onu göndermek için aygıtın uyanmasından gelir. Aygıt boştayken ağ donanımı düşük güçte bekler; bir istek donanımı **uyandırma** (wake-up) durumuna sokar ve istek bittikten sonra da bir süre uyanık kalır. Üç küçük isteği arka arkaya göndermek, aynı üç değişikliği tek bir istekte göndermekten genellikle daha pahalıdır, çünkü uyandırma bir kez ödenir ve beş kez ödenmez. Değişiklikleri biriktirip birlikte yollamaya **toplu gönderim** (batching) diyoruz. Sunucuyu belirli aralıklarla "yeni bir şey var mı" diye sormaya ise **yoklama** (polling) diyoruz.

Ölçü yine kursun ölçüsüdür: bir döngüde gönderilen istek sayısı, istek bütçesini kaç döngüde aştı.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Budget` ölçümün adını ve sınırını taşır, `Overrun` aşan örnekleri sayıp dört aşımdan birini verir. Bu dosya kursun her dersinde aynıdır.

#### Eşitleme Döngüsü Modeli

Modelde bir döngü, eşitlemenin bir turudur. Her turda uygulamaya belli sayıda yerel değişiklik gelir: kullanıcının eklediği bir not, silinen bir kayıt, değişen bir ayar. Bu sayılara **varış** diyelim; sekiz turluk akış `3, 0, 5, 1, 0, 4, 2, 0` olsun. Eşitleme kipi, bu değişikliklerin kaç isteğe dönüştüğünü belirler.

```kotlin
// Sync.kt — bir esitleme dongusunde gonderilen istekleri sayar
fun requestCount(arrivals: List<Int>, mode: String): List<Int> =
    arrivals.map { count ->
        when (mode) {
            "teker" -> count
            "toplu" -> if (count > 0) 1 else 0
            "yoklama" -> 1
            else -> 0
        }
    }
```

`requestCount` her tur için o turda gönderilen istek sayısını döndürür. `teker` kipinde her değişiklik ayrı bir istektir. `toplu` kipinde tur boyunca biriken değişiklikler tek istekle gider ve hiç değişiklik yoksa istek gitmez. `yoklama` kipinde ise değişiklik gelse de gelmese de her turda bir istek gider. Önce tek tek göndermeyi, istek bütçesi iki olacak şekilde ölçelim.

```kotlin
// Main.kt — her degisiklik ayri bir istek olarak gonderilir
fun main() {
    val arrivals = listOf(3, 0, 5, 1, 0, 4, 2, 0)
    val samples = requestCount(arrivals, "teker")
    println("istek: $samples")

    val overrun = Overrun("teker", Budget("istek", 2), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
istek: [3, 0, 5, 1, 0, 4, 2, 0]
teker: butce=istek<=2 | asan=3/8 -> azinda
icinde: 0
azinda: 1
cogunda: 0
hepsinde: 0
```

Üç tur aştı: üç, beş ve dört değişiklik gelen turlar. İki değişiklik gelen tur sınıra eşit olduğu için aşım sayılmadı. Sekiz turun üçü aşmış olduğundan sonuç **azında**.

**BA1.** Model, bir değişikliği bir isteğe eşler ve her isteği aynı ağırlıkta sayar. Gerçekte istekler gövde boyutu, bağlantı kurma ve yeniden deneme bakımından birbirinden çok farklıdır. Model gönderilen istekleri sayar; bir isteğin ağda ne kadar kaldığını, ne kadar veri taşıdığını ve pilden ne aldığını kurmaz.

#### Toplu Gönderim

Aynı varış akışını biriktirip tek istekle göndermek, istek sayısını değişiklik sayısından ayırır.

```kotlin
// Main.kt — ayni degisiklikler tur basina tek istekle gonderilir
fun main() {
    val arrivals = listOf(3, 0, 5, 1, 0, 4, 2, 0)
    val samples = requestCount(arrivals, "toplu")
    println("istek: $samples")

    val overrun = Overrun("toplu", Budget("istek", 2), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
istek: [1, 0, 1, 1, 0, 1, 1, 0]
toplu: butce=istek<=2 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 0
```

Hiçbir tur iki isteği geçmedi: **içinde**. Aynı on beş değişiklik, beş istekle gitti. Önceki ölçümle bu ölçüm arasındaki fark, bütçeyi kaç turun aştığıdır ve toplu gönderim bu sayıyı üçten sıfıra indirdi.

Platformda toplu gönderim, işi sistemin iş yöneticisine devrederek yapılır: uygulama işi ne zaman çalışması gerektiğini söyleyen koşullarla kaydeder ve sistem birçok uygulamanın işini aynı uyanıklık penceresinde çalıştırabilir.

```kotlin
// Platform kodu: kosullu periyodik esitleme
import android.content.Context
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit

fun scheduleSync(context: Context) {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED)
        .setRequiresBatteryNotLow(true)
        .build()

    val request = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS)
        .setConstraints(constraints)
        .build()

    WorkManager.getInstance(context)
        .enqueueUniquePeriodicWork("sync", ExistingPeriodicWorkPolicy.KEEP, request)
}
```

Bu parça platform kodudur ve burada çıktı, süre ya da pil iddia etmiyoruz. Koşullar işin yalnız uygun ağda ve pil düşük değilken çalışmasını ister; sistem, işi koşulların sağlandığı bir anda başlatır. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Sistemin işleri gerçekte nasıl birleştirdiği, modelin göstermediği bilgidir.

#### Yoklama

Yoklama, değişiklik olsun ya da olmasın sunucuya sormaktır. Değişiklik sık geliyorsa yoklama makul görünebilir, ama değişikliğin gelmediği zamanlarda da aygıtı uyandırır. Bunu ölçmek için tüm turların sessiz geçtiği bir akış kuralım: hiçbir tur değişiklik getirmiyor.

```kotlin
// Main.kt — hic degisiklik gelmeyen akista uc kip ayni butceyle olculur
fun main() {
    val quiet = List(8) { 0 }

    val overruns = listOf("teker", "toplu", "yoklama").map { mode ->
        Overrun(mode, Budget("istek", 0), requestCount(quiet, mode))
    }
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
teker: butce=istek<=0 | asan=0/8 -> icinde
toplu: butce=istek<=0 | asan=0/8 -> icinde
yoklama: butce=istek<=0 | asan=8/8 -> hepsinde
icinde: 2
azinda: 0
cogunda: 0
hepsinde: 1
```

Hiç değişiklik yokken `teker` ve `toplu` kipleri hiç istek göndermedi, `yoklama` ise sekiz turun sekizinde istek gönderdi: **hepsinde**. Yoklamanın maliyeti değişiklik sayısına bağlı değildir; zamana bağlıdır. Kullanıcı uygulamayı kullanmasa bile, yoklama her tur kadar uyandırma ödetir.

**BA2.** Modelde yoklama tur başına tam bir istektir. Gerçekte yoklama aralığı uzatılabilir, sunucu "değişiklik yok" cevabını çok küçük bir yanıtla verebilir ve sistem isteği başka işlerle aynı pencerede çalıştırabilir. Bu hafifletmeler modelde yoktur, bu yüzden yoklamanın hepsinde çıkması gerçek bir uygulamanın en kötü durumuna yakındır.

#### Neden İstek Sayısını Sayıyoruz

Pil tüketimi doğrudan ölçülmez, çünkü onu ölçmenin güvenilir bir yolu bu depoda yoktur. Onun yerine pili etkileyen davranışlardan birini sayıyoruz: gönderilen istek sayısı. Bu seçimin bir bedeli var. İstek sayısı pilin yerine geçmez; yalnız onunla ilişkili bir vekildir. İki istek bazen tek büyük istekten ucuza gelir, bazen pahalıya. Ders bu yüzden pil hakkında hiçbir sayı vermez ve pili yalnız uyandırma kavramıyla anar.

İstek sayısının iyi bir ölçü olmasının nedeni, doğrudan tasarım kararına bağlanmasıdır. Toplu göndermek ya da yoklamak, geliştiricinin verdiği bir karardır ve istek sayısı bu kararın sonucudur. Ölçüm, kararın etkisini kodun içinden okumayı sağlar.

Sessiz akış ölçümü de bu yüzden ayrı tutulur. Gerçek bir uygulama her zaman yoğun kullanılmaz; gecenin büyük bölümünde aygıt boştadır ve uygulamaya hiçbir değişiklik gelmez. O saatlerde pili tüketen şey kullanıcının yaptığı iş değil, uygulamanın kendi kendine yaptığı iştir. Yoklama tam olarak bu tür bir iştir. Bu yüzden bir eşitleme tasarımı yalnız yoğun akışta değil, sessiz akışta da sınanır.

İş yöneticisine devredilen iş ayrıca bir tasarım sorusu doğurur: işin ne kadar gecikebileceği. Bir not kaydı birkaç dakika gecikebilir, bir mesaj gecikemez. Koşulları gevşetmek sistemin işleri birleştirme şansını artırır, sıkılaştırmak azaltır. Model bu pazarlığı kurmaz; yalnız istek sayısını verir ve gecikmeyi ölçmez.

Eşitleme kipi seçiminde üç soru sorulur. Değişiklik ne kadar sık geliyor, karşı taraf ne kadar gecikmeyi kabul ediyor ve uygulama kapalıyken de eşitleme gerekiyor mu. Sık gelen değişiklikte toplu gönderim kazancı artar, çünkü her toplu istek daha çok değişikliği taşır. Gecikmeyi kabul etmeyen akışta toplu gönderim yerini anlık göndermeye bırakır ve bütçe bu durumda gevşetilir. Uygulama kapalıyken de eşitleme isteniyorsa iş, uygulama süreci olmadan çalışabilen bir iş olarak kaydedilir. Modeldeki üç kip, bu üç sorunun cevabına karşılık gelen en yalın seçimlerdir.

Ölçümü üretimde yorumlarken aygıtın durumu da hesaba girer. Aynı istek, ağ donanımı zaten uyanıkken ucuza, boştayken pahalıya gelir. Bu fark modele girmez: model her isteği bağımsız sayar. Bu yüzden istek sayısı, iki tasarımı karşılaştırmak için sağlam bir ölçüdür ama bir tasarımın gerçek pil maliyetini söylemek için yetersizdir.

Toplu gönderimde pencerenin uzunluğu da bir ayardır. Pencere uzadıkça her toplu istek daha çok değişiklik taşır ve istek sayısı düşer; ama değişikliklerin karşı tarafa ulaşma gecikmesi de uzar. Modelde pencere bir tur kadardır ve değiştirilmez. Gerçek bir uygulamada pencereyi seçmek, istek sayısı ile gecikme arasında bir denge kurmaktır ve bu dengeyi yalnız ürünün gereksinimi belirler.

Hata durumları da istek sayısını etkiler. Başarısız bir istek yeniden denenirse aynı değişiklik için ikinci bir uyandırma ödenir. Yeniden deneme aralığı uzayan bir tasarım bu maliyeti sınırlar. Model her isteği başarılı sayar ve yeniden denemeyi içermez; bu yüzden gerçek bir akışın istek sayısı modelin verdiğinden büyük olabilir.

Son olarak ölçümün hangi kipte alındığı rapora yazılır. Aynı akışta üç kip üç farklı sayı verir ve sayıyı tek başına okuyan biri hangi tasarımı ölçtüğünü bilemez. Kip adını, bütçeyi ve toplam istek sayısını birlikte yazdırmak, bir satırı sonradan yorumlamayı mümkün kılar; dersin çıktılarında bu üçü her satırda yer alır.

#### Yaygın Yanılgılar

**"Yoklama sık değişiklik olan uygulamalarda zararsızdır."** Değişiklik sık geliyorsa yoklamanın boşa giden turları azalır, ama sıfır olmaz. Dersin sessiz akış ölçümünde yoklama, hiç değişiklik yokken bile her turda istek gönderdi.

**"Toplu gönderim her zaman daha iyidir."** Toplu gönderim, değişikliklerin gönderilmesini geciktirir. Değişikliğin anında karşı tarafa ulaşması gereken bir akışta bu gecikme kabul edilemeyebilir. Ders yalnız istek sayısını ölçtüğü için gecikmeyi göstermez; bu, toplu gönderimin bedelsiz olduğu anlamına gelmez.

**"Aynı aşım, aynı tasarruf demektir."** Sıfır bütçede tek tek gönderim ile toplu gönderim aynı aşımı verdi, oysa biri on beş, diğeri beş istek yolladı. Kural bu farkı görmez; toplam sayı ayrıca yazdırıldığında görünür.

**"Platform işi sistem birleştirir, bu yüzden ölçmeye gerek yoktur."** Sistem uygun bir pencerede işleri bir araya getirebilir, ama uygulamanın kaç iş kaydettiği ve bunların ne sıklıkla çalışması istendiği hâlâ uygulamanın kararıdır. Sistemin birleştirme payı, kaydedilen işin kendisini azaltmaz.

#### Bütçeyi Nasıl Seçmeli

İstek bütçesi, tur başına izin verilen istek sayısıdır. Bütçeyi sıfır koymak, hiçbir turun istek göndermemesini ister; bu, değişiklik gelen her turu da aşım yapar ve yalnız sessiz turları temiz bırakır. Bütçeyi bire koymak, tur başına tek isteği kabul eder; bu toplu gönderimi içinde tutar ve tek tek göndermeyi, bu akışta, çoğunda bırakır. Dersin ilk iki ölçümünde bütçe ikiydi. İkisi arasında bir yerde seçilen değer, uygulamanın ne kadar gecikmeyi kabul ettiğine bağlıdır.

#### Sınırlayıcı Ölçüm

Şimdiye kadar iki şey gördük: toplu gönderim istek sayısını düşürür, yoklama sessiz akışta bile istek yollar. Kuralın göremediği şeyi görmek için aynı varış akışını üç kiple, bu kez sıfır bütçeyle ölçelim ve toplam istek sayısını da yazdıralım.

```kotlin
// Main.kt — ayni akis uc kiple sifir butceyle olculur ve toplam istek yazilir
fun main() {
    val arrivals = listOf(3, 0, 5, 1, 0, 4, 2, 0)

    val overruns = listOf("teker", "toplu", "yoklama").map { mode ->
        val samples = requestCount(arrivals, mode)
        println("$mode toplam: ${samples.sum()}")
        Overrun(mode, Budget("istek", 0), samples)
    }
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
teker toplam: 15
toplu toplam: 5
yoklama toplam: 8
teker: butce=istek<=0 | asan=5/8 -> cogunda
toplu: butce=istek<=0 | asan=5/8 -> cogunda
yoklama: butce=istek<=0 | asan=8/8 -> hepsinde
icinde: 0
azinda: 0
cogunda: 2
hepsinde: 1
```

Sıfır bütçede `teker` ve `toplu` aynı aşımı verdi: ikisi de **çoğunda**. Oysa biri on beş, diğeri beş istek gönderdi. Kural, bir turun bütçeyi aşıp aşmadığına bakar, kaç istek fazla gönderdiğine bakmaz; bu yüzden toplu gönderimin on istekle ölçülen tasarrufunu bu bütçede göremez. Aynı işlev iki bütçede iki aşım verir: bütçe iki iken `teker` azındaydı, sıfırken çoğunda. Tasarrufu görmek için ya toplam sayıya bakmak ya da bütçeyi toplu gönderimin ulaşabileceği bir sınıra, yani turda bir isteğe çekmek gerekir. Dersin ilk iki ölçümü bu yüzden iki bütçeli bir resim çizdi: bütçe iki iken fark görünür, sıfırken kaybolur.

Yoklama ise bu ölçümde de tek başına `hepsinde` çıktı, çünkü değişiklik gelmeyen üç turda da istek gönderdi. Sekiz istek toplu gönderimin beş isteğinden fazla, tek tek göndermenin on beş isteğinden azdır; kuralın hepsinde demesi, yoklamanın en çok istek gönderdiği anlamına gelmez, her turda istek gönderdiği anlamına gelir.

**BA3.** Modeldeki varış akışı sabit ve sekiz turdur. Gerçek kullanımda değişiklikler kümeler hâlinde gelir, uzun sessiz dönemlerle kesilir ve kullanıcıya göre çok değişir. Tek bir akışta kazanılan sonuç, başka bir akışa taşınmadan önce o akışta yeniden ölçülmelidir.

#### Özet

- Bu dersin aşımı: içinde. Toplu gönderimle aynı değişiklikler tur başına en çok bir isteğe dönüştü ve iki isteklik bütçeyi hiçbir tur aşmadı.
- İstek, aygıtı uyandırdığı için pil harcar; toplu gönderim uyandırma sayısını değişiklik sayısından ayırır.
- Tek tek gönderim aynı akışta bütçeyi sekiz turun üçünde aştı: azında.
- Yoklama hiç değişiklik gelmeyen akışta da her turda istek gönderdi ve sıfır bütçede hepsinde çıktı.
- Kural turun bütçeyi aşıp aşmadığını sayar. Sıfır bütçede on beş ve beş istek aynı aşımı verdi; tasarrufu görmek için toplam sayıya da bakmak gerekir.

#### Sonraki Adım

Şimdiye kadar ölçtüğümüz her sayı bir kez ölçüldü ve o bir sayı kabul edildi. Oysa ölçüm gürültülüdür: aynı işi iki kez çalıştırınca farklı sayı çıkabilir. Sıradaki ders, `Karşılaştırmalı Ölçüm`, tekrarlanan ölçümlerde bir gerilemenin nasıl ayırt edildiğini ve ısınma yinelemelerinin neden ölçüme katılmadığını ele alır.

### Karşılaştırmalı Ölçüm

Source: https://academia.sh/tr/kurslar/android-uretim/karsilastirmali-olcum

Bir işin iki sürümünü karşılaştırmak için ölçümü tekrarlamak gerekir. Ders ısınma yinelemelerinin ölçümü nasıl bozduğunu, atıldıklarında gerilemenin nasıl göründüğünü ve bütçe gerilemeden geniş seçilirse kuralın onu nasıl kaçırdığını ölçer.

Önceki derste istek sayısını bir akış üzerinde bir kez ölçtük ve sonucu kesin kabul ettik. Gerçek bir ölçüm tek bir sayı değildir. Aynı işi arka arkaya çalıştırdığınızda ilk çalıştırmalar sonrakilerden farklı sürer: sınıflar yüklenir, kod derlenir, önbellekler dolar. Bu ilk yinelemelere **ısınma** (warm-up) diyoruz. Ölçüm ısınmayı da kattığında, işin kararlı hâlini değil, kararlı hâle ulaşana kadar geçen yolu ölçer.

Bu dersin sorusu şudur: bir değişikliğin işi yavaşlatıp yavaşlatmadığını, yani bir **gerileme** (regression) olup olmadığını, ısınma ölçümü bozarken nasıl ayırt ederiz. Cevabın iki parçası var: ısınmayı ölçümden ayırmak ve bütçeyi gerilemeyi yakalayacak kadar dar seçmek.

Ölçü yine kursun ölçüsüdür: yinelemelerin bütçeyi kaçında aştığı.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Budget` ölçümün adını ve sınırını taşır, `Overrun` aşan örnekleri sayıp dört aşımdan birini verir. Bu dosya kursun her dersinde aynıdır.

#### Yinelemeli Ölçüm Modeli

Modelde bir işin maliyeti iş adımı sayısıdır. İşin iki sürümü var: `eski` ve `yeni`. Yeni sürüm, bir değişiklikten sonra her çalıştırmada üç adım fazla iş yapıyor; bu bizim gerilememiz. Isınma ise ilk yinelemelere eklenen fazladan adımdır ve yinelemeler ilerledikçe azalır.

```kotlin
// Bench.kt — bir isin yinelemeli olcumu; ilk yinelemeler isinma payi tasir
fun workOf(version: String, size: Int): Int = when (version) {
    "eski" -> size * 2
    "yeni" -> size * 2 + 3
    else -> size
}

fun iterations(version: String, size: Int, runs: Int, warmup: Int): List<Int> =
    (0 until runs).map { run ->
        workOf(version, size) + if (run < warmup) (warmup - run) * 4 else 0
    }

fun dropWarmup(samples: List<Int>, warmup: Int): List<Int> = samples.drop(warmup)
```

`workOf` bir sürümün kararlı hâldeki maliyetini verir. `iterations` bu maliyeti `runs` kez yineler ve ilk `warmup` yinelemeye azalan bir ısınma payı ekler. `dropWarmup` ısınma yinelemelerini ölçümden çıkarır. Beş birimlik iş için `eski` sürümün kararlı maliyeti on, `yeni` sürümünki on üçtür. Önce eski sürümü sekiz yinelemeyle, ısınmayı da katarak ölçelim. Bütçe on iki adım olsun.

```kotlin
// Main.kt — eski surum isinma yinelemeleri dahil olculur
fun main() {
    val samples = iterations("eski", 5, 8, 3)
    println("eski: $samples")

    val overrun = Overrun("eski", Budget("adim", 12), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
eski: [22, 18, 14, 10, 10, 10, 10, 10]
eski: butce=adim<=12 | asan=3/8 -> azinda
icinde: 0
azinda: 1
cogunda: 0
hepsinde: 0
```

Sürümün kararlı maliyeti on iki adımlık bütçenin altında, ama ilk üç yineleme ısınma yüzünden aştı: 3/8, **azında**. Eski sürüm bütçeye uygun bir işi yapıyor; ölçüm ise ona üç aşım yazdı. Bu, ısınmanın ölçümü bozmasıdır.

**BA1.** Modelde ısınma payı doğrusal azalır ve yineleme sayısı kesin bilinir. Gerçekte ısınmanın ne zaman bittiği işe, aygıta ve sistemin o anki durumuna bağlıdır; hangi yinelemeden sonra kararlı hâle girildiğini ölçmeden söylemek mümkün değildir. Model her yinelemenin kaç iş adımı yaptığını sayar; yinelemenin ne kadar sürdüğünü kurmaz.

#### Isınmayı Atmak ve Gerilemeyi Görmek

Karşılaştırma yapmak için iki sürümü de ısınmayı atarak ölçeriz ve aynı bütçeye vururuz.

```kotlin
// Main.kt — iki surum isinma atilarak ayni butceyle karsilastirilir
fun main() {
    val warmup = 3
    val oldSamples = dropWarmup(iterations("eski", 5, 8, warmup), warmup)
    val newSamples = dropWarmup(iterations("yeni", 5, 8, warmup), warmup)
    println("eski: $oldSamples")
    println("yeni: $newSamples")

    val overruns = listOf(
        Overrun("eski", Budget("adim", 12), oldSamples),
        Overrun("yeni", Budget("adim", 12), newSamples),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
eski: [10, 10, 10, 10, 10]
yeni: [13, 13, 13, 13, 13]
eski: butce=adim<=12 | asan=0/5 -> icinde
yeni: butce=adim<=12 | asan=5/5 -> hepsinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 1
```

Isınma atılınca ölçüm temizlendi: eski sürüm **içinde**, yeni sürüm **hepsinde**. Önceki ölçümde eski sürüme yazılan üç aşım ısınmanın payıydı; bu ölçümde ise yeni sürümün beşinin beşinde aşması gerçek bir gerilemeyi gösteriyor. Aradaki fark kararlı hâlde üç adımdır ve bütçe tam bu farkın içinden geçecek şekilde, on iki adımda seçildi.

Platformda bu iki ayrım, ölçümü tekrarlayan bir kural nesnesiyle kurulur. Aşağıdaki parçada ısınma yinelemelerinin sayısı ve ölçülecek yinelemelerin sayısı açıkça verilir.

```kotlin
// Platform kodu: tekrarlanan karsilastirmali olcum
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun coldStartup() = benchmarkRule.measureRepeated(
        packageName = "com.example.app",
        metrics = listOf(StartupTimingMetric()),
        iterations = 5,
        startupMode = StartupMode.COLD,
        compilationMode = CompilationMode.Partial(warmupIterations = 3),
    ) {
        pressHome()
        startActivityAndWait()
    }
}
```

Bu parça platform kodudur ve burada çıktı, süre ya da oran iddia etmiyoruz. Kural nesnesi `measureRepeated` ile ölçümü beş kez yineler ve ısınma yinelemelerini ayrıca belirtilen sayıda çalıştırır; hangi sürelerin çıkacağı aygıta ve uygulamaya bağlıdır. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Aracın ısınmayı ne zaman bitmiş saydığı, modelin göstermediği bilgidir.

#### Yineleme Sayısı Ölçümü Değiştirir

Isınma atılmazsa, aşım sayısı yineleme sayısına da bağlıdır, çünkü ısınma payı sabittir ve yineleme sayısı arttıkça payı küçülür. Aynı eski sürümü, ısınmayı katarak, dört, sekiz ve on altı yinelemeyle ölçelim.

```kotlin
// Main.kt — ayni surum isinma dahil uc farkli yineleme sayisiyla olculur
fun main() {
    val overruns = listOf(4, 8, 16).map { runs ->
        Overrun("yineleme-$runs", Budget("adim", 12), iterations("eski", 5, runs, 3))
    }
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
yineleme-4: butce=adim<=12 | asan=3/4 -> cogunda
yineleme-8: butce=adim<=12 | asan=3/8 -> azinda
yineleme-16: butce=adim<=12 | asan=3/16 -> azinda
icinde: 0
azinda: 2
cogunda: 1
hepsinde: 0
```

Sürüm aynı, ısınma aynı, aşan yineleme sayısı üç. Yalnız yineleme sayısı değişti ve aşım çoğundan azına geçti. Bu yüzden ısınma dahil edilen bir ölçümde sonucu yinelemeler belirler; ısınma atılan bir ölçümde ise kararlı hâl belirler. Karşılaştırmalı ölçümün güvenilir olması için ikisi birlikte sabitlenir: kaç yineleme ölçülecek ve kaçı ısınma sayılıp atılacak.

#### Mikro ve Makro Karşılaştırmalı Ölçüm

Karşılaştırmalı ölçüm iki düzeyde yapılır. Mikro düzeyde tek bir işlev ya da küçük bir kod parçası, yalıtılmış bir ortamda tekrar tekrar çalıştırılır; amaç o parçanın maliyetini diğer işlerden ayırmaktır. Makro düzeyde uygulamanın bir akışı, örneğin başlatma ya da bir ekranın açılması, uçtan uca ölçülür; amaç kullanıcının yaşadığı sonucu görmektir.

İkisi birbirinin yerine geçmez. Mikro ölçüm iyileşmiş görünürken makro ölçüm aynı kalabilir, çünkü ölçülen parça akışın küçük bir bölümüdür. Makro ölçüm gerilemeyi gösterirken mikro ölçüm temiz çıkabilir, çünkü sorun parçaların etkileşimindedir. Bu derste model tek bir iş birimini ölçer; gerçek bir proje iki düzeyi de kullanır.

#### Temel Değer

Gerilemeyi yakalamak için bir karşılaştırma noktası gerekir. Bu nokta, işin kabul edilmiş sürümünün ölçümüdür ve **temel değer** (baseline) olarak saklanır. Yeni ölçüm bu değerle karşılaştırılır; fark belirli bir sınırı aşarsa gerileme sayılır. Dersin modelinde temel değer, eski sürümün kararlı hâldeki maliyetidir.

Temel değerin kendisi bir ölçümdür ve ısınma ile gürültüden etkilenir. Temel değeri ısınma dahil almak, tüm sonraki karşılaştırmaları bozar. Temel değer bu yüzden yalnız kararlı yinelemelerden hesaplanır ve ölçüldüğü koşullarla birlikte kaydedilir.

#### Isınma Neden Vardır

Isınma, işin ilk çalıştırmalarının sonrakilerden pahalı olmasıdır. İlk çalıştırmada kod henüz derlenmemiş olabilir, ön bellekler boştur, kullanılan kaynaklar ilk kez açılır. Birkaç yinelemeden sonra bu maliyetler ödenmiş olur ve iş kararlı hâline varır. Modelde ısınma, ilk yinelemelere eklenen ve azalan bir paydır; gerçek ısınmanın şekli işe göre değişir.

Isınmayı ölçümden atmak kararlı hâli ölçmeyi seçmektir. Bu seçim doğrudur, çünkü bir gerilemeyi yakalamak için işin kendisi ölçülmeli, ilk çalıştırma şansı değil. Ama ısınmayı atan bir ölçüm kullanıcının ilk karşılaştığı maliyeti görmez. Bu yüzden başlangıcı ölçen bir ölçüm ile gerilemeyi ölçen bir ölçüm ayrı tutulur: ilkinde ısınma ölçümün konusudur, ikincisinde gürültüdür.

#### Ölçüm Düzeneği Sabit Kalmalı

Karşılaştırmalı ölçümde iki sürüm yan yana değil, aynı düzenekte art arda ölçülür. Düzenek değişirse, örneğin bir ölçüm başka bir aygıtta ya da başka bir sistem durumunda alınırsa, fark gerilemeden değil düzenekten gelebilir. Bu derste düzenek modelin kendisidir ve her ölçümde aynıdır; gerçek bir projede aynı güvence ancak aygıtı, sistem durumunu ve yineleme sayısını sabitleyerek sağlanır.

Bu sabitlik ölçüm sonuçlarının nasıl saklanacağını da belirler. Bir temel değer kaydedilirken, hangi sürümde, kaç yinelemeyle ve ısınma atılarak mı atılmadan mı alındığı yanına yazılır. Bu bilgi olmadan iki sayı karşılaştırılamaz; çünkü ders, aynı sürümün yineleme sayısına göre iki farklı aşım verebildiğini gösterdi.

Bütçenin nereden geldiği de sorulmalıdır. Bu derste on iki adım, eski sürümün kararlı maliyeti olan on ile yeni sürümün maliyeti olan on üç arasına yerleştirildi ve hangi sürümün içinde kalacağını baştan belirledi. Gerçek bir projede bütçe, kabul edilmiş sürümün ölçümüne küçük bir pay eklenerek konur; pay gürültünün genişliğinden büyük, gerilemenin büyüklüğünden küçük olmalıdır. Pay çok büyükse kural gerilemeyi göremez, çok küçükse her ölçümde yanlış alarm verir.

Sonuçları raporlarken tek bir sayı yerine sayıların dağılımı verilir: en iyi yineleme, en kötü yineleme ve ortadaki. Bu derste yineleme başına iş adımı sayısı örneklerin kendisidir ve her ölçümün altında yazdırılır. Kuralın yazdığı tek satır aşım sayısını özetler; örnekler okunduğunda ise ısınmanın nerede bittiği ve gerilemenin nerede başladığı görülür.

Karşılaştırmalı ölçüm sonucunda alınan karar da yazılı olmalıdır. Bir değişiklik gerileme çıkardıysa üç yol vardır: değişikliği geri almak, gerilemeyi kabul edip temel değeri yükseltmek ya da gerilemeyi başka bir yerde telafi etmek. Hangisinin seçildiği ve nedeni kayda geçmezse, sonraki ölçüm yeni temel değerin neden öyle olduğunu açıklayamaz. Bu derste üç adımlık fark açıkça bir gerilemeydi; ürün kararı onu kabul etmek ya da etmemek arasındadır ve bunu kural vermez.

Yinelemelerin sırası da bilgi taşır. Isınma payı ilk yinelemelerde yoğunlaştığından, örnek dizisinin başı sonundan farklı bir şey ölçer. Diziyi karıştırıp ortalamasını almak ısınmayı kararlı hâle karıştırır ve iki ayrı olguyu tek sayıda birleştirir. Sıralı yazdırılan örnekler, ısınmanın bittiği yinelemeyi göz kararı olmadan gösterir ve atılacak yineleme sayısını seçmenin dayanağı olur.

#### Yaygın Yanılgılar

**"Bir kez ölçmek yeter."** Tek ölçüm, ısınmayı ve gürültüyü ayıramaz. Ders bunu, aynı sürümün yineleme sayısına göre farklı aşım vermesiyle gösterdi.

**"Isınmayı atmak ölçümü güzelleştirir."** Isınma atılırken ölçümün neyi hedeflediği belli olmalıdır. Kullanıcı uygulamayı ilk kez açtığında ısınma maliyetini öder; bu maliyeti ölçmek isteyen bir ölçüm, ısınmayı atmaz, onu ayrı bir ölçüm olarak alır.

**"Küçük fark gerilemedir."** Gürültü varken küçük bir fark, gerilemeden de gelebilir. Bu derste model gürültüsüzdür, bu yüzden üç adımlık fark kesindir. Gerçek ölçümde farkın gürültüden büyük olduğundan emin olmak için yinelemeler çoğaltılır.

**"Kural gerilemeyi bulur."** Kural yalnız bütçeyi aşan örnekleri sayar. Bütçe gerilemenin üstündeyse kural gerilemeyi göremez; dersin sınırlayıcı ölçümü bunu gösterdi.

#### Sınırlayıcı Ölçüm

Kuralın en önemli sınırı şudur: bütçe gerilemeden geniş seçilirse kural gerilemeyi göremez. İki sınamayla gösterelim. Birincisinde ısınma dahil, bütçe yirmi adım. İkincisinde ısınma atılmış, bütçe on üç adım.

```kotlin
// Main.kt — genis butcede gerileme kuralin gozunden kacar
fun main() {
    val warmup = 3
    val oldAll = iterations("eski", 5, 8, warmup)
    val newAll = iterations("yeni", 5, 8, warmup)
    val oldSteady = dropWarmup(oldAll, warmup)
    val newSteady = dropWarmup(newAll, warmup)

    val overruns = listOf(
        Overrun("eski-isinmali", Budget("adim", 20), oldAll),
        Overrun("yeni-isinmali", Budget("adim", 20), newAll),
        Overrun("eski-kararli", Budget("adim", 13), oldSteady),
        Overrun("yeni-kararli", Budget("adim", 13), newSteady),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
eski-isinmali: butce=adim<=20 | asan=1/8 -> azinda
yeni-isinmali: butce=adim<=20 | asan=2/8 -> azinda
eski-kararli: butce=adim<=13 | asan=0/5 -> icinde
yeni-kararli: butce=adim<=13 | asan=0/5 -> icinde
icinde: 2
azinda: 2
cogunda: 0
hepsinde: 0
```

Yirmi adımlık bütçede iki sürüm de **azında**; on üç adımlık bütçede ikisi de **içinde**. Yeni sürüm üç adım yavaşlamış olmasına karşın kural, iki durumda da iki sürüme aynı aşımı yazdı. İsınmalı ölçümde yeni sürümün aşan yineleme sayısı iki, eski sürümünki bir: fark bir yinelemedir ve yalnız örnek sayısına bakarak gerilemeden mi, ısınmadan mı geldiği ayırt edilemez. Isınması atılmış ölçümde ise on üç adım, yeni sürümün tam sınırıdır ve sınıra eşit örnek aşım sayılmaz. Kural aşımın sayısını görür; büyüklüğünü, yani üç adımlık farkı, görmez. Aynı işlev iki bütçede iki aşım verebilir; bütçe gerilemenin altında kalan bir değere konmazsa gerileme kuralın içinde kalır.

**BA2.** Modeldeki gerileme, her yinelemede aynı üç adımdır. Gerçek gerilemeler her yinelemede aynı gelmez; bazıları yalnız belirli girdilerde ya da belirli aygıtlarda ortaya çıkar. Tek bir ölçüm düzeneğinde görünmeyen bir gerilemenin başka düzenekte görünmeyeceği sonucuna varılamaz.

#### Özet

- Bu dersin aşımı: azında. Eski sürümün kararlı maliyeti bütçeyi aşmadı, ama ilk üç ısınma yinelemesi aştı ve sekiz yinelemenin üçü bütçeyi geçti.
- Isınma yinelemeleri işin kararlı hâlini değil, kararlı hâle ulaşma yolunu ölçer. Karşılaştırma için ısınma ölçümden atılır.
- Isınma atılınca eski sürüm içinde, yeni sürüm hepsinde çıktı; üç adımlık gerileme görünür oldu.
- Isınma dahil ölçümde aynı sürüm, yineleme sayısına göre çoğunda ya da azında çıktı.
- Bütçe gerilemeden geniş seçilirse kural iki sürüme aynı aşımı yazar: aşımın sayısını görür, büyüklüğünü görmez.

#### Sonraki Adım

Bu ders ölçümün neyi ölçtüğünü sabitledi. Geriye ölçülen şeyin yayımlanacak pakete nasıl girdiği kalıyor. Sıradaki ders, `Küçültme ve Karartma`, uygulama paketinden kullanılmayan sınıfları ayıklarken koruma kurallarının hangi sınıfları ayakta tuttuğunu ve geniş bir kuralın küçültmeyi nasıl boşa çıkardığını ölçer.

### Kod Küçültme ve Karartma

Source: https://academia.sh/tr/kurslar/android-uretim/kod-kucultme-ve-karartma

Yayımlanan uygulama paketinden kullanılmayan sınıflar ayıklanır ve kalan sınıfların adları kısaltılır. Ders küçültmeden sonra kalan sınıf sayısının bütçeyi kaç modülde aştığını, karartmanın bu sayıyı değiştirmediğini ve geniş bir koruma kuralının küçültmeyi boşa çıkardığını ölçer.

Önceki derste bir değişikliğin işi yavaşlatıp yavaşlatmadığını ölçmenin yolunu kurduk. Bu ders, ölçülen şeyin kullanıcıya ulaşan pakete nasıl girdiğine bakıyor. Yayımlanan uygulama, geliştirme sırasında yazılan her sınıfı taşımak zorunda değildir. Bir kitaplığın yüzlerce sınıfından uygulama yalnız birkaçını çağırıyorsa, çağrılmayan sınıflar pakete girdiğinde yalnız yer kaplar; indirmeyi büyütür ve yükleme işini artırır.

Pakete girecek kodu azaltmak iki ayrı işlemdir. **Küçültme** (shrinking), uygulamanın giriş noktalarından ulaşılamayan sınıfları ve üyeleri pakete koymaz. **Karartma** (obfuscation), kalan sınıfların ve üyelerin adlarını kısa, anlamsız adlarla değiştirir. İkisi de derleme sırasında, yayın paketini üreten adımda çalışır; geliştirme sırasında yazılan kaynak koda dokunmaz. Küçültücü bir kodu kaldırmadan önce o koda erişilebildiğini ya da erişilemediğini çözümler ve çözümleme yansımayla erişilen kodu göremez. Bu yüzden geliştirici, atılmaması gereken sınıfları bir **koruma kuralıyla** (keep rule) bildirir.

Ölçü yine kursun ölçüsüdür: küçültmeden sonra bir modülde kalan sınıf sayısı, bütçeyi kaç modülde aşıyor.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Budget` ölçümün adını ve sınırını taşır, `Overrun` aşan örnekleri sayıp dört aşımdan birini verir. Bu dosya kursun her dersinde aynıdır.

#### Erişilebilirlik Çözümlemesi Modeli

Modelde bir modül, sınıf numaralarından ve sınıflar arasındaki çağrı kenarlarından oluşur. Uygulamanın giriş noktası sıfır numaralı sınıftır; küçültücü o noktadan başlayarak çağrı kenarlarını izler ve ulaştığı sınıfları tutar. Ulaşamadığı sınıfları atar. Yansımayla erişilen bir sınıf çağrı kenarı taşımaz; küçültücü onu görmez ve yalnız koruma kuralı sayesinde kalır.

```kotlin
// Shrink.kt — kokten ulasilabilen siniflar kalir, digerleri atilir
class Module(val name: String, val edges: Map<Int, List<Int>>, val total: Int)

fun buildModule(name: String, total: Int, used: Int): Module =
    Module(name, (0 until used - 1).associateWith { listOf(it + 1) }, total)

fun sampleModules(): List<Module> {
    val sizes = listOf(12 to 4, 20 to 6, 14 to 3, 30 to 9, 16 to 5, 11 to 4, 24 to 7, 14 to 5)
    return sizes.mapIndexed { index, (total, used) -> buildModule("modul-${index + 1}", total, used) }
}

fun reachableFrom(module: Module, root: Int): Set<Int> {
    val seen = mutableSetOf(root)
    val queue = ArrayDeque(listOf(root))
    while (queue.isNotEmpty()) {
        val current = queue.removeFirst()
        for (next in module.edges[current].orEmpty()) {
            if (seen.add(next)) queue.addLast(next)
        }
    }
    return seen
}

fun keptClasses(module: Module, rule: String): Int = when (rule) {
    "kucult", "karart" -> reachableFrom(module, 0).size
    "dar" -> (reachableFrom(module, 0) + (module.total - 1)).size
    else -> module.total
}

fun mappingLines(module: Module): List<String> =
    reachableFrom(module, 0).sorted().map { "C$it -> ${'a' + it}" }
```

`sampleModules` sekiz modül kurar; her modül `total` sınıf taşır ve bunların `used` kadarı giriş noktasından ulaşılabilir. `reachableFrom` ulaşılabilen sınıfları bulur. `keptClasses` bir modülde kalan sınıf sayısını kurala göre verir: `kucult` yalnız ulaşılabilenleri tutar, `karart` aynı sayıyı verir, `dar` ulaşılabilenlere yansımayla erişilen tek bir sınıfı ekler, öteki kurallar modülün tüm sınıflarını tutar. `mappingLines` karartmanın yazdığı adların karşılıklarını döndürür. Önce küçültmeyi açık bırakıp, kalan sınıf sayısını on sınıflık bütçeyle ölçelim.

```kotlin
// Main.kt — kucultmeden sonra kalan sinif sayisi modul basina olculur
fun main() {
    val modules = sampleModules()
    val samples = modules.map { keptClasses(it, "kucult") }
    println("kalan: $samples")

    val overrun = Overrun("kucult", Budget("sinif", 10), samples)
    overrun.line()
    countOverruns(listOf(overrun))
}
```

```text
kalan: [4, 6, 3, 9, 5, 4, 7, 5]
kucult: butce=sinif<=10 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 0
```

Hiçbir modülde kalan sınıf sayısı ona çıkmadı: **içinde**. Kural, bu kodun üretime çıkıp çıkmadığını değil, küçültmeden sonra kalan işin bütçeyi kaç örnekte aştığını sayar.

**BA1.** Modelde sınıf sayısı bir paket boyutunun yerine kullanılır ve her sınıf aynı ağırlıktadır. Gerçek bir sınıfın boyutu birkaç satırdan binlerce satıra kadar değişir; model sınıfları sayar; her birinin ne kadar yer tuttuğunu kurmaz ve paketin ne kadar küçüldüğünü bir oran olarak söylemez. Küçültmeden sonra kalan sayının bütçeye göre doğru olup olmadığını model değil, ürün kararı belirler.

#### Küçültme Kapalıyken

Küçültmenin kazancını görmek için aynı sekiz modülü küçültme olmadan ölçelim: bu durumda her modül tüm sınıflarını taşır.

```kotlin
// Main.kt — kucultme kapaliyken ve acikken ayni butce
fun main() {
    val modules = sampleModules()
    val closed = modules.map { keptClasses(it, "kapali") }
    val shrunk = modules.map { keptClasses(it, "kucult") }
    println("kapali: $closed")
    println("kucult: $shrunk")

    val overruns = listOf(
        Overrun("kapali", Budget("sinif", 10), closed),
        Overrun("kucult", Budget("sinif", 10), shrunk),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
kapali: [12, 20, 14, 30, 16, 11, 24, 14]
kucult: [4, 6, 3, 9, 5, 4, 7, 5]
kapali: butce=sinif<=10 | asan=8/8 -> hepsinde
kucult: butce=sinif<=10 | asan=0/8 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 1
```

Küçültme kapalıyken her modül ona yakın ya da onu aşan sayıda sınıf taşıdı: **hepsinde**. Küçültme açıkken sekizi de bütçenin içinde kaldı. Aradaki fark, kodun kendisinin değişmesi değil, pakete girecek kodun azalmasıdır.

Platformda bu karar derleme betiğinde verilir. Aşağıdaki parça yayın derlemesi için küçültmeyi açar ve kural dosyasını bağlar.

```kotlin
// Platform kodu: yayin derlemesinde kucultmeyi acma
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro",
            )
        }
    }
}
```

Bu parça platform kodudur ve burada çıktı, boyut ya da süre iddia etmiyoruz. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Küçültücünün gerçek bir uygulamada hangi sınıfları ulaşılabilir saydığı, modelin göstermediği bilgidir ve derleme sonunda üretilen raporlardan okunur.

#### Karartma Sayıyı Değiştirmez

Karartma sınıfların adlarını kısaltır. Kalan sınıf sayısını değiştirmez, çünkü hiçbir sınıfı atmaz; yalnız adını değiştirir. Ayrıca özgün adlar ile kısa adlar arasındaki karşılıklar bir **eşleme dosyasına** (mapping file) yazılır. Bu dosya olmadan çökme izlerindeki kısa adlar özgün adlara çevrilemez.

```kotlin
// Main.kt — karartma adlari degistirir, sinif sayisini degistirmez
fun main() {
    val modules = sampleModules()
    println("eslesme:")
    mappingLines(modules[0]).forEach { println("  $it") }

    val overruns = listOf("kucult", "karart").map { rule ->
        Overrun(rule, Budget("sinif", 10), modules.map { keptClasses(it, rule) })
    }
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
eslesme:
  C0 -> a
  C1 -> b
  C2 -> c
  C3 -> d
kucult: butce=sinif<=10 | asan=0/8 -> icinde
karart: butce=sinif<=10 | asan=0/8 -> icinde
icinde: 2
azinda: 0
cogunda: 0
hepsinde: 0
```

İlk modülün dört sınıfı `a`'dan `d`'ye kısaltıldı ve iki satır aynı aşımı verdi. Karartma tek başına bütçeyi etkilemez; ayrı bir amaca hizmet eder, yani adları okunmaz kılar. Eşleme dosyasının kaybolması ise kısa adların çözülememesine yol açar. Bu dersin ölçümü yalnız sınıf sayısına baktığı için eşleme dosyasının varlığını ya da yokluğunu görmez.

**BA2.** Modelde karartma adı tek harfle değiştirir ve çakışma olmaz. Gerçek karartıcı çok sayıda sınıfı iç içe ad uzayında kısaltır, bazı adları (örneğin yansımayla aranan adları) korur ve eşleme dosyasını her derlemede yeniden üretir. Önceki bir derlemenin eşleme dosyası sonraki derlemenin adlarını çözmez.

#### Küçültmenin Neyi Çözdüğü

Küçültmenin amacı, pakete girmeyen kodun maliyetini sıfırlamaktır. Kod pakete girmezse indirilmez, yüklenmez ve yüklenme sırasında taranmaz. Ancak bu kazanç yalnız ulaşılamayan kodda geçerlidir. Giriş noktasından ulaşılabilen bir sınıf, kullanıcı hiç çağırmasa bile pakette kalır; çünkü küçültücü, çalışma zamanındaki kullanımı değil, kodun yapısal olarak ulaşılabilirliğini çözümler.

Bu yüzden küçültme sonrası kalan sayı bir alt sınır değil, bir üst tahmindir. Modelde ulaşılabilirlik bir zincirdir: her sınıf bir sonrakini çağırır. Gerçek uygulamalarda çağrı grafiği çok daha dallıdır ve küçültücünün bırakacağı sınıf sayısı, grafiğin biçimine göre değişir.

Koruma kuralının bir maliyeti vardır ve bu maliyet yazan kişinin gözünden kaçar. Her kural, küçültücüye "bunu at" yerine "bunu tut" der. Bir kuralı genişletmek kolaydır, çünkü hata yalnız çalışma zamanında görünür: eksik bir kural uygulamayı çökertir, fazla bir kural yalnız paketi büyütür. Bu asimetri yüzünden kurallar zamanla genişler ve küçültmenin kazancı sessizce erir. Bu dersin sayısal ölçümü tam bu erimeyi bir bütçeyle görünür kılar.

Dar bir kural yazmak, hangi sınıfa neden yansımayla erişildiğini bilmeyi gerektirir. Bu bilgi çoğu zaman bir kitaplığın belgesinde ya da kendi kodunuzda bir ad dizisinin çalışma zamanında sınıfa çevrildiği satırdadır. Kuralın yanına gerekçesini yazmak, bir sonraki kişinin kuralı güvenle daralttığı ya da kaldırdığı bir iz bırakır.

Yayın derlemesini sınamak da ayrı bir iştir. Küçültme çoğunlukla yalnız yayın derlemesinde açıldığından, geliştirme sırasında çalışan bir uygulama küçültmeden sonra çökebilir. Bu yüzden küçültülmüş derleme, en az bir kez uçtan uca çalıştırılır ve yansımayla erişilen yollar özellikle gezilir. Çökmeyi önceden yakalamak, koruma kuralını gerekçesiyle birlikte yazmaktan daha ucuza gelir.

Küçültmenin sonucu uygulama güncellendikçe değişir. Yeni bir kitaplık eklemek ulaşılabilen sınıf sayısını artırır, kullanılmayan bir özelliği kaldırmak azaltır. Bu yüzden kalan sınıf sayısı bir kez ölçülüp unutulmaz; her yayın derlemesinde aynı bütçeyle yeniden sayılır ve bütçeyi aşan modül, hangi değişiklikle büyüdüğü sorulacak bir gerileme olarak ele alınır. Bu dersin sekiz modülü bu izlemenin tek bir anının görüntüsüdür.

Eşleme dosyasının saklanması bir süreç sorusudur. Dosya derlemeyle birlikte üretilir ve yayımlanan paketle eşleşmesi gerekir; yanlış derlemenin dosyası kısa adları yanlış özgün adlara çevirir ve çökme incelemesini yanıltır. Bu yüzden dosya, paket yayımlandığı anda sürüm numarasıyla birlikte arşivlenir.

#### Yaygın Yanılgılar

**"Karartma paketi küçültür."** Karartma adları kısaltır; bu, adlar uzun olduğunda yer kazandırabilir ama sınıf sayısını azaltmaz. Dersin karartma ölçümünde iki satır aynı aşımı verdi.

**"Küçültme kodu güvenli kılar."** Karartma, kodu okumayı zorlaştırır; ama koruma sağlamaz. Kısa adlar kodu çözmeyi engellemez, yalnız yavaşlatır. Gizli tutulması gereken bir bilgi pakete hiç girmemelidir.

**"Koruma kuralı yazınca sorun çözülür."** Koruma kuralı yansımayla erişilen sınıfı kurtarır, ama kuralın genişliği maliyeti belirler. Geniş bir kural bütün paketi tutar ve küçültmenin kazancını siler. Dersin sınırlayıcı ölçümü bunu gösterdi.

**"Eşleme dosyası isteğe bağlıdır."** Eşleme dosyası olmadan çökme izlerindeki kısa adlar özgün adlara çevrilemez. Her yayın derlemesinin eşleme dosyası, o derlemeyle birlikte saklanır.

#### Bütçe ve Kuralın Gördüğü

Sınıf sayısı bütçesi, bir modülün küçültmeden sonra taşıyabileceği en çok sınıfı söyler. Bütçeyi hesaplamanın tek bir doğru yolu yoktur: bazı projeler önceki sürümün ortalamasını, bazıları paketin toplam sınırından geriye doğru bir dağılımı kullanır. Kural bütçenin nereden geldiğini bilmez; yalnız onu örneklere uygular.

#### Sınırlayıcı Ölçüm

Küçültücü yansımayla erişilen kodu göremez. Yansımayla erişilen tek bir sınıf varsa o sınıfı korumak gerekir. Bunu iki biçimde yapabiliriz. Dar kural yalnız o sınıfı korur. Geniş kural ise o sınıfın paketindeki her şeyi korur. Platformda kural dosyasındaki karşılıkları şöyledir.

```proguard
# dar: yalniz yansimayla erisilen sinif korunur
-keep class com.example.plugin.PluginEntry { *; }

# genis: paketteki her sinif korunur
-keep class com.example.plugin.** { *; }
```

Bu kural dosyası çalıştırılmaz ve çıktısı yoktur; iki satır, aynı yansıma sorusuna iki farklı cevap verir. Aynı iki cevabı modelde ölçelim.

```kotlin
// Main.kt — dar ve genis koruma kurali ayni butceyle olculur
fun main() {
    val modules = sampleModules()
    val narrow = modules.map { keptClasses(it, "dar") }
    val broad = modules.map { keptClasses(it, "genis") }
    println("dar: $narrow")
    println("genis: $broad")

    val overruns = listOf(
        Overrun("dar", Budget("sinif", 10), narrow),
        Overrun("genis", Budget("sinif", 10), broad),
    )
    overruns.forEach { it.line() }
    countOverruns(overruns)
}
```

```text
dar: [5, 7, 4, 10, 6, 5, 8, 6]
genis: [12, 20, 14, 30, 16, 11, 24, 14]
dar: butce=sinif<=10 | asan=0/8 -> icinde
genis: butce=sinif<=10 | asan=8/8 -> hepsinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 1
```

Dar kural ulaşılabilen sınıflara yalnız yansımayla erişilen sınıfı ekledi ve **içinde** kaldı; dördüncü modül tam ona, sınıra eşit olduğu için aşım sayılmadı. Geniş kural ise her modülde kullanılmayan sınıfları da tuttu ve küçültme kapalıyken ölçülen sayılarla aynı değerleri verdi: **hepsinde**. Geniş kuralın küçültmeyi nasıl boşa çıkardığı bu iki satırdan okunur: kuralın yazılması küçültücüyü kapatmadı, ama kapalı olduğu durumla aynı sayıyı doğurdu.

Kural, aşımın sayısını görür, nedenini görmez. Hepsinde çıkan satırdan kalan sınıfların neden kaldığı okunamaz; bunun için hangi koruma kuralının hangi sınıfı tuttuğu, küçültücünün derleme sonunda yazdığı raporlarda aranır. Karartma da burada hiçbir şeyi düzeltmez: adlar kısalsa da sayı aynı kalır.

**BA3.** Modelde geniş kural paketteki her sınıfı tutar ve paket, modülle aynıdır. Gerçek bir koruma kuralı yalnız belirli üyeleri ya da yalnız belirli ad kalıplarını koruyabilir; bu derste yazılan iki kural en dar ve en geniş uçları gösterir. Bunların arasında kalan kuralların sonucu, ölçüm yapılmadan modelden çıkarılamaz.

#### Özet

- Bu dersin aşımı: içinde. Küçültme açıkken sekiz modülün hiçbirinde kalan sınıf sayısı on sınıflık bütçeyi aşmadı.
- Küçültme, giriş noktasından ulaşılamayan sınıfları atar. Karartma yalnız adları kısaltır; kalan sınıf sayısı değişmez ve eşleme dosyası kısa adları geri çevirmek için saklanır.
- Küçültme kapalıyken aynı sekiz modül bütçeyi hepsinde aştı. Fark kodun değişmesinden değil, pakete girecek kodun azalmasından geldi.
- Dar koruma kuralı içinde kaldı. Yansımayla erişilen paketi kapsayan geniş koruma kuralı, her modülde kullanılmayan sınıfları da tuttu ve sonuç hepsinde çıktı.
- Kural aşımın sayısını görür, nedenini görmez. Kalan sınıfların hangi kuralla tutulduğu küçültücünün raporlarından okunur.

#### Sonraki Adım

Kod küçültülüp karartıldıktan sonra uygulama bir pakete dönüşür ve bu paketin birden çok biçimi vardır: hepsini tek dosyada taşıyan biçim ile aygıta özel parçalar üreten biçim. Sıradaki ders, `Uygulama Paketi Biçimleri`, bu biçimlerin neyi nasıl dağıttığını ve aygıt başına indirilen parça sayısının bütçeyi kaç aygıt türünde aştığını ölçer.

## Dağıtım

Aygıta göre optimize edilmiş uygulama paketi biçimleri, sürüm kodu ve adı düzeni, otomatik derleme ve imzalama, iç, kapalı ve açık test kanalları, aşamalı yayın ve durdurma, mağaza incelemesi ve yayın sonrası izleme.

### Uygulama Paketi Biçimleri

Source: https://academia.sh/tr/kurslar/android-uretim/uygulama-paketi-bicimleri

Uygulama paketi, APK ve bölünmüş paket aygıta hangi dosyaların indiğini belirler. Ders bir yapılandırmaya inen dosya sayısını bütçeyle karşılaştırır ve evrensel paketin her yapılandırmada bütçeyi aştığını gösterir.

Önceki ders kod küçültmeyi ve karartmayı ölçtü: koruma kuralından sonra modülde kaç sınıf kaldı. O iş derlemenin içindeydi. Bu ders derlemenin çıktısına bakıyor: derleme bittiğinde ortaya çıkan paket bir aygıta ulaşırken kaç dosya taşıyor? Kursun sorusu değişmedi: **bu ölçüm bütçesini kaç örnekte aşıyor?** Ölçüm bir sayıdır, bütçe o sayının sınırıdır, örnek ise aynı ölçümün farklı bir girdide alınmış hâlidir. Sınırı kesin olarak aşan örnekler sayılır ve sayı dört yanıttan birini verir: **içinde**, **azında**, **çoğunda**, **hepsinde**.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Overrun` bütçeyi aşan örnekleri sayar. Hiçbiri aşmıyorsa yanıt içinde, hepsi aşıyorsa hepsinde, yarıdan azı aşıyorsa azında, geri kalan her durum çoğunda olur; tam yarı çoğundadır. Kural sayıyı görür, aşımın büyüklüğünü ve nedenini görmez. Kursun açılış dersi dört ölçümü bu kuralla sınıflayıp her yanıtı bir kez gördü; bu ders o dağılıma yalnız atıf yapar.

#### Üç Biçim, Bir Yayın

Android'de üç ayrı şeye paket denir ve karışmaları sık görülür. APK, aygıta kurulan dosyadır. Uygulama paketi (app bundle), uygulama mağazasına yüklenen yayın biçimidir; kendisi aygıta kurulmaz. Mağaza bu paketten her aygıt yapılandırması için bölünmüş paket (split APK) üretir: aygıtın işlemci mimarisine (ABI) uyan yerel kitaplıkları, ekran yoğunluğuna uyan kaynakları ve ortak kodu içeren küçük bir dosya kümesi. Evrensel paket (universal APK) ise bu seçimin hiç yapılmadığı biçimdir: tek dosya kümesi bütün ABI'lerin kitaplıklarını ve bütün yoğunlukların kaynaklarını taşır, her aygıta aynı küme iner.

Yapılandırmaya göre bölmenin üç ekseni vardır: dil, ekran yoğunluğu ve ABI. Bunları derleme sisteminde modül derleme betiği açar ya da kapatır. Betik parçası şöyle görünür.

```kotlin
// Platform kodu: modül derleme betiği
android {
    bundle {
        language { enableSplit = true }
        density { enableSplit = true }
        abi { enableSplit = true }
    }
}
```

Bu parça platform kodudur; burada çıktı, süre ya da boyut iddia etmiyoruz. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde paket bir dosya listesidir, aygıt yapılandırması bir ABI ve bir yoğunluk çiftidir.

Betikteki üç satırın her biri ayrı bir soruya karşılık gelir. `abi` ekseni açıkken yerel kitaplıklar işlemci mimarisine göre ayrı dosyalara bölünür; aygıt kendi mimarisine uyan kitaplığı alır, ötekileri almaz. `density` ekseni açıkken ekran yoğunluğuna bağlı kaynaklar aynı şekilde ayrılır. `language` ekseni açıkken dil kaynakları ayrılır ve aygıt yalnız kendi dillerini ister. Eksenlerden biri kapalıysa o eksenin dosyaları ayrılmadan ortak parçada kalır ve her aygıta iner. Ders bu betiği çalıştırmaz; betiğin ifade ettiği ayrımı modelde bir `wants` ilkesine çevirir ve ilkenin sonucunu sayar.

#### Dosya Listesi ve Yapılandırma

Aşağıdaki yardımcı dosya paketi kurar. Her `PackageFile` ya bir ABI'ye ya da bir yoğunluğa bağlıdır ya da hiçbirine bağlı değildir. `filesForConfig` bir yapılandırmaya kaç dosyanın ineceğini sayar; hangi dosyanın ineceğine `wants` ilkesi karar verir.

```kotlin
// PackageModel.kt — paket dosyalarini ve bir yapilandirmaya inen dosyalari modeller
class PackageFile(val name: String, val abi: String? = null, val density: String? = null)

class DeviceConfig(val label: String, val abi: String, val density: String)

fun buildPackage(abis: List<String>, densities: List<String>): List<PackageFile> =
    listOf(PackageFile("kod.dex"), PackageFile("yapi.xml")) +
        abis.map { PackageFile("kitaplik-$it.so", abi = it) } +
        densities.map { PackageFile("kaynak-$it.bin", density = it) }

fun samplePackage(): List<PackageFile> =
    buildPackage(listOf("arm64", "arm32", "x86"), listOf("mdpi", "hdpi", "xhdpi", "xxhdpi"))

fun sampleConfigs(): List<DeviceConfig> = listOf(
    DeviceConfig("dar-telefon", "arm32", "mdpi"),
    DeviceConfig("orta-telefon", "arm64", "hdpi"),
    DeviceConfig("genis-telefon", "arm64", "xhdpi"),
    DeviceConfig("tablet", "arm64", "xxhdpi"),
    DeviceConfig("eski-aygit", "arm32", "hdpi"),
    DeviceConfig("emulator", "x86", "xhdpi"),
)

fun filesForConfig(
    files: List<PackageFile>,
    config: DeviceConfig,
    wants: (PackageFile, DeviceConfig) -> Boolean,
): Int = files.count { wants(it, config) }

val everything: (PackageFile, DeviceConfig) -> Boolean = { _, _ -> true }

val matching: (PackageFile, DeviceConfig) -> Boolean = { file, config ->
    (file.abi == null || file.abi == config.abi) && (file.density == null || file.density == config.density)
}
```

Paket dokuz dosyadır: iki ortak dosya, üç ABI kitaplığı, dört yoğunluk kaynağı. Yapılandırma listesi altı örnek verir. `everything` ilkesi yapılandırmaya bakmaz; evrensel paketin tanımı budur. `matching` ilkesi ise yalnız yapılandırmanın ABI'sine ve yoğunluğuna uyan dosyaları ister.

#### Evrensel Paket

Bütçeyi beş dosya olarak koyalım. Bu sayı modelin girdisidir: bir platform eşiği ya da mağaza kuralı değildir, dersin soruyu sormak için seçtiği sınırdır. Evrensel paketin altı yapılandırmaya kaç dosya indirdiğini ölçelim.

```kotlin
// Main.kt — evrensel paket her yapilandirmaya butun dosyalari indirir
fun main() {
    val files = samplePackage()
    val samples = sampleConfigs().map { filesForConfig(files, it, everything) }
    println("paket dosyasi: ${files.size}")
    println("inen dosyalar: $samples")
    Overrun("evrensel-paket", Budget("dosya", 5), samples).line()
}
```

```text
paket dosyasi: 9
inen dosyalar: [9, 9, 9, 9, 9, 9]
evrensel-paket: butce=dosya<=5 | asan=6/6 -> hepsinde
```

Altı örneğin altısı da bütçeyi aştı: yanıt **hepsinde**. Bu sonucu kuralın cömert davranmasından değil, ilkenin yapılandırmaya bakmamasından alıyoruz. Dar ekranlı eski bir telefon da geniş bir tablet de dokuz dosya indirdi; `samples` listesi bu yüzden dokuzdan başka bir sayı içermiyor. Yoğunluk kaynaklarının çoğu o aygıt için hiç kullanılmayacak.

**DA1.** Model dosyayı bir iş birimi sayar ve her dosyayı eşit ağırlıkta görür. Gerçekte bir yoğunluk kaynağı dosyası bir ortak kod dosyasından çok farklı bir büyüklükte olabilir; model boyut, sıkıştırma ve indirme koşullarını kurmaz. Dosya sayısı boyutun yerine konmuş bir iş birimidir ve bir bayt ölçümü değildir.

#### Bütçe Modelin Girdisidir

Aynı işlevin iki bütçede iki farklı aşım vermesi bir çelişki değildir; ölçünün kendisidir. Bütçeyi dokuza çıkaralım: evrensel paket en fazla dokuz dosya indiriyor, hiçbir örnek bu sınırı kesin olarak aşmaz. Bölünmüş paket için bütçeyi üçe indirelim: dört dosya inen her yapılandırma bu sefer sınırı aşar.

```kotlin
// Main.kt — ayni olcum iki butceyle iki asim verir
fun main() {
    val files = samplePackage()
    val configs = sampleConfigs()
    val loose = Overrun("evrensel-paket", Budget("dosya", 9), configs.map { filesForConfig(files, it, everything) })
    val strict = Overrun("bolunmus-paket", Budget("dosya", 3), configs.map { filesForConfig(files, it, matching) })
    loose.line()
    strict.line()
    countOverruns(listOf(loose, strict))
}
```

```text
evrensel-paket: butce=dosya<=9 | asan=0/6 -> icinde
bolunmus-paket: butce=dosya<=3 | asan=6/6 -> hepsinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 1
```

Sınır sınıra eşit olan örneği aşım saymaz: dokuz dosya indiren evrensel paket dokuz bütçesinde içinde kaldı. Bölünmüş paket dört dosya indirdi ve üç bütçesinde hepsinde çıktı. Bu iki sonuç paketin iyi ya da kötü olduğunu söylemez; yalnız bütçeyle sayının karşılaştırıldığını gösterir. Bu yüzden bütçeyi dersin başında koyuyoruz ve gerekçesini söylüyoruz: dosya sayısı bir paket biçimini değil, bir yapılandırmanın indirme işini temsil ediyor.

#### Yarım Bölme

Üç ekseni birlikte bölmek zorunlu değildir. Yalnız ABI'ye göre bölen bir paket, yoğunluk kaynaklarının hepsini her aygıta yine de indirir. Modelde bu, ABI'ye uyan ama yoğunluğa bakmayan bir ilkedir.

```kotlin
// Main.kt — yalniz ABI'ye gore bolunen paket yogunluk dosyalarini yine indirir
fun main() {
    val files = samplePackage()
    val abiOnly: (PackageFile, DeviceConfig) -> Boolean = { file, config -> file.abi == null || file.abi == config.abi }
    val samples = sampleConfigs().map { filesForConfig(files, it, abiOnly) }
    println("inen dosyalar: $samples")
    Overrun("yalniz-abi", Budget("dosya", 5), samples).line()
}
```

```text
inen dosyalar: [7, 7, 7, 7, 7, 7]
yalniz-abi: butce=dosya<=5 | asan=6/6 -> hepsinde
```

Her yapılandırma yedi dosya indirdi: iki ortak dosya, tek ABI kitaplığı ve dört yoğunluk kaynağı. Kısmi bölme dokuzu yediye düşürdü ama aşım değişmedi. Kural dokuzun yediden kötü olduğunu görmez; yalnız ikisinin de beşten büyük olduğunu görür.

#### Dil Ekseni

Üçüncü eksen dildir ve DA2'de belirteceğimiz gibi ana modelde yoktur. Onu ayrı bir blokta, dosya adıyla taklit edelim: pakete üç dil dosyası ekleyelim ve her aygıta bir ya da iki dil verelim. Dil ekseni açıkken aygıt yalnız kendi dillerinin dosyalarını indirir; ABI ve yoğunluk kuralı `matching` ilkesinde aynı kalır.

```kotlin
// Main.kt — dil ekseni acikken birden fazla dili olan aygit bir dosya daha indirir
fun main() {
    val languages = listOf("tr", "en", "de")
    val files = samplePackage() + languages.map { PackageFile("dil-$it.bin") }
    val deviceLanguages = listOf(
        listOf("tr"), listOf("tr", "en"), listOf("en"),
        listOf("de", "en"), listOf("tr"), listOf("en"),
    )
    val samples = sampleConfigs().mapIndexed { index, config ->
        files.count { file ->
            if (file.name.startsWith("dil-")) file.name.removePrefix("dil-").removeSuffix(".bin") in deviceLanguages[index]
            else matching(file, config)
        }
    }
    println("inen dosyalar: $samples")
    Overrun("dil-ekseni", Budget("dosya", 5), samples).line()
}
```

```text
inen dosyalar: [5, 6, 5, 6, 5, 5]
dil-ekseni: butce=dosya<=5 | asan=2/6 -> azinda
```

Dört dosya ortak parça, ABI kitaplığı ve yoğunluk kaynağından geliyor; bir dil bir dosya, iki dil iki dosya ekliyor. Tek dilli dört aygıt beş dosyada kaldı ve bütçeyi aşmadı. İki dilli iki aygıt altıya çıktı ve aştı. Altı örneğin ikisi aştığı için yanıt **azında**: sayı yarının altında kaldı. Bu sonuç ilginç bir şeyi gösteriyor. Paketi tam bölmek, dosya sayısını aygıttan bağımsız tek bir sayıya indirmiyor; aygıtın kendi özellikleri, burada kaç dil kullandığı, sayıyı oynatıyor. Sınır beşe konduğu için tek bir ek dil bile yanıtı değiştirir.

**DA3.** Dil dosyaları adlarından ayrıştırılıyor (`dil-tr.bin` gibi) ve her dil tek dosyadır. Gerçekte bir dilin kaynakları birden fazla dosyaya yayılabilir; modelde bir dil bir dosyadır.

#### Evrensel Paket Neden Var

Evrensel paket yanlış bir seçim değildir; farklı bir bedelle gelen kolaylıktır. Tek dosya olduğu için bir ekip arkadaşına, bir test aygıtına ya da mağaza dışındaki bir dağıtım kanalına olduğu gibi verilebilir ve hangi yapılandırmanın kurulacağı sorusu hiç çıkmaz. Mağazanın bölünmüş paket üretmesi ise mağazanın bu yapılandırma bilgisine sahip olmasına bağlıdır. Bu yüzden iki biçim aynı yayın içinde birlikte yaşar: biri kolay elden ele aktarım, öteki aygıta özgü indirme içindir.

Seçimi pratikte belirleyen soru şudur: paketi kim, hangi yapılandırmaya göre seçecek? Cevap "mağaza, aygıt başına" ise bölünmüş paket doğal olandır. Cevap "kimse, olduğu gibi kurulacak" ise evrensel paket tek yoldur. Ölçüm bu iki durumun bedelini aynı birimle, dosya sayısıyla yan yana koyar.

#### Yaygın Yanılgılar

**"Uygulama paketi aygıta kurulur."** Kurulan şey APK'dır. Uygulama paketi mağazaya yüklenir ve mağaza onu yapılandırmaya göre bölünmüş paketlere çevirir. İkisi aynı dosya değildir ve aynı adımda üretilmez.

**"Bölünmüş paket her zaman daha az dosya indirir."** Bölme eksenlerinin hangisinin açık olduğuna bağlıdır. Yarım bölme örneğinde yedi dosya indi, tam bölmede dört. Yalnız ABI ekseni açık bırakılırsa yoğunluk kaynaklarının hepsi yine iner.

**"Bölünmüş paket her aygıta aynı sayıda dosya indirir."** Tam bölünmüş pakette ABI ve yoğunluk kaynakları için bu doğrudur, çünkü her aygıtın bir ABI'si ve bir yoğunluğu vardır. Dil ekseni açılınca doğru olmaktan çıkar: iki dilli aygıt altı, tek dilli aygıt beş dosya indirdi. Aygıtın sahip olduğu değer sayısı arttıkça indirilen dosya sayısı da artar.

**"Evrensel paket yalnızca yanlış bir tercihtir."** Evrensel paket mağaza dışı dağıtımda hâlâ gereklidir; yukarıdaki bölüm bunu gerekçesiyle anlattı. Yanlış olan, bütçesi olan bir mağaza yayınında hangi aygıta hangi dosyanın ineceğini hiç tartmadan evrensel paketi tek yol saymaktır.

**"Dosya sayısı küçüldüyse paket küçüldü."** Bu iki ifade aynı şey değildir. Sayı yapılandırma seçiminin etkisini gösterir; büyüklük için ayrı bir ölçüm gerekir ve bu derste yapılmadı.

#### Sınırlayıcı Ölçüm

Bu ölçümün adı sınırlayıcıdır, çünkü dersin sorusuna verilebilecek en iyi yanıtı arar: bütçe aynı, paket aynı, örnekler aynı ve yalnız hangi dosyaların ineceğine karar veren ilke değişiyor. Evrensel paketin hepsinde çıkması paketin bir kusuru olarak okunmasın; ilke yapılandırmaya bakmadığında başka bir sonuç mümkün değildir. Sınırlayıcıyı ayrıca koymamızın nedeni, hepsinde yanıtını gören okurun "bu bütçe hiç tutulamaz" sonucuna varmasını önlemektir. Tutulabildiğini aynı bütçede göstermek, bütçenin gerçekçi olduğunu da kanıtlar.

Yapılandırmaya göre tam bölünmüş paket, her yapılandırmaya yalnız eşleşen dosyaları indirir. Bütçe yine beş dosyadır.

```kotlin
// Main.kt — bolunmus paket her yapilandirmaya yalniz eslesen dosyalari indirir
fun main() {
    val files = samplePackage()
    val samples = sampleConfigs().map { filesForConfig(files, it, matching) }
    println("inen dosyalar: $samples")
    Overrun("bolunmus-paket", Budget("dosya", 5), samples).line()
}
```

```text
inen dosyalar: [4, 4, 4, 4, 4, 4]
bolunmus-paket: butce=dosya<=5 | asan=0/6 -> icinde
```

Aynı paket, aynı altı yapılandırma, aynı bütçe: tek fark ilkedir ve yanıt hepsinde iken içinde oldu. Evrensel paket bu dersin hepsinde örneğiydi; bölünmüş paket onun içinde karşılığı. Sonucu tek başına paketin biçimi belirlemedi: aynı uygulama paketi hangi dosyaların ineceğine karar veren ilkeye göre iki yanıt verdi.

Kuralın bu sonuçta göremediği bir şey var: dosyaların büyüklüğü. Dört küçük dosya ile dört büyük dosya aynı sayılır, dokuz küçük dosya ise dört büyük dosyadan daha az indirme işi olabilir. Model dosya sayısını sayar ve dosya büyüklüğünü kurmaz. Dosya sayısı yalnız yapılandırmaya bağlı seçimin etkisini görünür kılar.

**DA2.** Model yapılandırmayı bir ABI ve bir yoğunluk çifti olarak kurar. Gerçekte dil de bir bölme eksenidir ve bir aygıtın birden fazla dili olabilir; ana modelde dil ekseni yoktur ve yukarıdaki `Dil Ekseni` bölümü onu yalnız bir blokta ayrıca kurar.

#### Özet

- Bu dersin aşımı: hepsinde. Evrensel paket altı yapılandırmanın altısında bütçeyi aştı, çünkü ilke yapılandırmaya bakmayıp dokuz dosyanın dokuzunu da indirdi.
- Uygulama paketi mağazaya yüklenen yayın biçimidir; APK aygıta kurulan dosyadır; bölünmüş paket ise bir yapılandırmaya uyan dosya kümesidir.
- Aynı ölçüm bütçeye göre değişti: dokuz bütçesinde evrensel paket içinde, üç bütçesinde bölünmüş paket hepsinde çıktı. Bütçe modelin girdisidir.
- Yalnız ABI'ye göre bölünen paket her yapılandırmaya yedi dosya indirdi ve hepsinde kaldı.
- Dil ekseni açılınca tek dilli aygıtlar beş, iki dilli aygıtlar altı dosya indirdi ve yanıt azında oldu; aygıtın özelliği sayıyı oynatır.
- Yapılandırmaya göre bölünmüş paket her yapılandırmaya dört dosya indirdi ve beş bütçesinde içinde kaldı; kural dosya sayısını görür, büyüklüğünü görmez.

#### Sonraki Adım

Bu derste bir derleme aygıta hangi dosyaları indirdi diye saydık. Ama aygıtların hepsi son derlemeyi kullanmıyor: kimi yeni sürümü indirdi, kimi eski sürümde kaldı. Sıradaki ders, `Sürüm Yönetimi`, sekiz aygıtın kurulu sürüm kodunun son sürümden kaç sürüm geride kaldığını ölçer.

### Sürüm Yönetimi

Source: https://academia.sh/tr/kurslar/android-uretim/surum-yonetimi

Sürüm kodu platformun karşılaştırdığı tamsayıdır, sürüm adı ise kullanıcıya gösterilen dizedir. Ders sekiz aygıtın kurulu sürüm kodundan son sürüme uzaklığını bütçeyle karşılaştırır ve sürüm adını değiştirmenin uzaklığı değiştirmediğini gösterir.

Önceki ders derlemenin çıktısına baktı: bir yapılandırmaya kaç dosya iniyor. Bu ders çıktının kendisine bir kimlik vermeyi ele alıyor. Aygıtlar derlemelerin hepsini aynı anda almaz; bir kısmı son sürümde, bir kısmı eski bir sürümde kalır. Bu yüzden her derlemeye, hangisinin hangisinden yeni olduğunu söyleyen bir sayı gerekir. Soru yine kursun sorusudur: **bu ölçüm bütçesini kaç örnekte aşıyor?** Ölçüm bir aygıtın son sürümden kaç sürüm geride kaldığıdır, bütçe desteklenen geri sürüm sayısıdır, her aygıt bir örnektir.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Overrun` sınırı kesin olarak aşan örnekleri sayar ve aşım yaygınlığını dört adla verir: içinde, azında, çoğunda, hepsinde. Kural sayıyı görür; bir aygıtın bir sürüm mü yoksa on sürüm mü geride olduğu, aşan her örnek için aynı sayılır.

#### Sürüm Kodu ve Sürüm Adı

Android her uygulama sürümüne iki ayrı değer verir. Sürüm kodu (version code) bir tamsayıdır; platform ve uygulama mağazası iki sürümden hangisinin daha yeni olduğunu bu sayıya bakarak anlar. Sürüm adı (version name) ise kullanıcıya gösterilen dizedir ve platform onu karşılaştırmaz. `2.2.1` bir sürüm adıdır; platform için onun kendinden önceki sürümden yeni olup olmadığı hakkında hiçbir anlamı yoktur.

```kotlin
// Platform kodu: modül derleme betiği
android {
    defaultConfig {
        versionCode = 12
        versionName = "2.2.1"
    }
}
```

Bu parça platform kodudur; burada çıktı iddia etmiyoruz. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde bir yayın `Release` olarak iki alan taşır: kod ve ad. Karşılaştırmayı yalnız kod yapar.

Betikte iki değer yan yana durur ve ikisi de elle yazılmıştır, ama okurları farklıdır. `versionCode` makineye söyler: bu derleme öncekinden sonradır. `versionName` insana söyler: bu derleme hangi sürümdür. Bir ekip ikisini aynı anda artırabilir, yalnız birini artırabilir ya da kodu ad üzerinden hesaplayabilir; hangisinin yapıldığı, ders boyunca ölçeceğimiz uzaklığı doğrudan etkiler. Modelde kodu ve adı ayrı alanlara koymamızın nedeni budur: biri değişince öteki değişmek zorunda değildir.

İki kural sürüm kodunu yönetir. Birincisi tekdüze artıştır: her yeni yayının kodu bir öncekinden büyük olmalıdır. İkincisi kodun düşürülememesidir: platform, kurulu olandan düşük kodlu bir paketle güncellemeyi kabul etmez. Bu yüzden hatalı çıkan bir sürümü geri almak, eski kodu yeniden yayımlamak değildir; eski davranışı taşıyan ama daha yüksek kodlu yeni bir derleme yayımlamaktır.

#### Geride Kalan Sürüm Sayısı

Model yayın geçmişini, kurulu aygıtları ve bir uzaklık işlevini yardımcı dosyada toplar. `isMonotonic` kodların kesin biçimde arttığını denetler. `versionsBehind` bir aygıtın kurulu kodundan büyük kaç ayrı kod olduğunu sayar; aynı kodu taşıyan iki yayın tek sayılır, çünkü platform onları aynı sürüm sayar.

```kotlin
// VersionModel.kt — surum kodunu ve adini modeller, geride kalan surum sayisini hesaplar
class Release(val code: Int, val name: String)

class InstalledDevice(val label: String, val installedCode: Int)

fun isMonotonic(releases: List<Release>): Boolean =
    releases.zipWithNext().all { (previous, next) -> next.code > previous.code }

fun latestCode(releases: List<Release>): Int = releases.maxOf { it.code }

fun versionsBehind(releases: List<Release>, device: InstalledDevice): Int =
    releases.map { it.code }.distinct().count { it > device.installedCode }

fun releaseHistory(): List<Release> = listOf(
    Release(5, "1.8.0"),
    Release(6, "1.8.1"),
    Release(7, "1.9.0"),
    Release(8, "2.0.0"),
    Release(9, "2.0.1"),
    Release(10, "2.1.0"),
    Release(11, "2.2.0"),
    Release(12, "2.2.1"),
)

fun installedDevices(): List<InstalledDevice> = listOf(
    InstalledDevice("aygit-a", 12),
    InstalledDevice("aygit-b", 11),
    InstalledDevice("aygit-c", 10),
    InstalledDevice("aygit-d", 9),
    InstalledDevice("aygit-e", 8),
    InstalledDevice("aygit-f", 6),
    InstalledDevice("aygit-g", 5),
    InstalledDevice("aygit-h", 12),
)
```

Sekiz yayın sekiz farklı kod taşır ve sekiz aygıttan ikisi son sürümdedir. Bütçeyi bir kararın sonucu olarak türetelim: ekip, kurulu kodu 10'dan küçük olan aygıtları desteklemeyeceğine karar verdi. En düşük desteklenen sürüm kodu 10 ise ve son kod 12 ise desteklenen geri sürüm sayısı ikidir.

```kotlin
// Main.kt — en dusuk desteklenen surumden butce turetilir, sekiz aygit olculur
fun main() {
    val releases = releaseHistory()
    println("kodlar tekduze mu: ${isMonotonic(releases)}")
    val minSupported = 10
    val budget = Budget("surum", latestCode(releases) - minSupported)
    val samples = installedDevices().map { versionsBehind(releases, it) }
    println("geride kalan: $samples")
    Overrun("surum-uzakligi", budget, samples).line()
}
```

```text
kodlar tekduze mu: true
geride kalan: [0, 1, 2, 3, 4, 6, 7, 0]
surum-uzakligi: butce=surum<=2 | asan=4/8 -> cogunda
```

Dört aygıt ikiden fazla sürüm geride: üç, dört, altı ve yedi. Sekizin yarısı sınırı aştığı için yanıt **çoğunda**; tam yarı çoğunda sayılır, azında değil. Bu, dersin aşımıdır. Kural aygıt-g'nin yedi, aygıt-d'nin üç sürüm geride olduğunu görmez; ikisi de aşan bir örnektir.

**DA3.** Model uzaklığı sürüm sayısıyla ölçer ve her sürümü eşit ağırlıkta görür. Gerçekte bir sürüm küçük bir düzeltme, bir başkası büyük bir değişiklik taşıyabilir; model bu farkı kurmaz. Ayrıca model aygıtların kurulu kodunu bir girdi olarak alır; gerçekte güncellemelerin ne zaman indiği kullanıcının ayarlarına ve ağa bağlıdır.

#### Bütçe Bir Karardır

En düşük desteklenen sürüm değişirse bütçe değişir, aynı sekiz aygıt başka bir aşım verir. Üç ayrı bütçe koyalım: ekip yedi sürüm geriyi destekliyor, dört sürüm geriyi destekliyor, iki sürüm geriyi destekliyor.

```kotlin
// Main.kt — ayni sekiz aygit uc ayri butceyle uc ayri asim verir
fun main() {
    val releases = releaseHistory()
    val samples = installedDevices().map { versionsBehind(releases, it) }
    val wide = Overrun("yedi-surum", Budget("surum", 7), samples)
    val middle = Overrun("dort-surum", Budget("surum", 4), samples)
    val narrow = Overrun("iki-surum", Budget("surum", 2), samples)
    listOf(wide, middle, narrow).forEach { it.line() }
    countOverruns(listOf(wide, middle, narrow))
}
```

```text
yedi-surum: butce=surum<=7 | asan=0/8 -> icinde
dort-surum: butce=surum<=4 | asan=2/8 -> azinda
iki-surum: butce=surum<=2 | asan=4/8 -> cogunda
icinde: 1
azinda: 1
cogunda: 1
hepsinde: 0
```

Bütçe yediyken hiçbir aygıt sınırı kesin olarak aşmaz; yedi sürüm geride kalan aygıt sınıra eşittir, aşım değildir. Bütçe dörde inince yalnız altı ve yedi sürüm geride olanlar aşar. İkiye inince dört aygıt aşar. Dört yanıttan üçü aynı ölçümden çıktı; **hepsinde** hiç görünmedi, çünkü iki aygıt son sürümdedir ve sıfır, hiçbir geçerli bütçeyi aşmaz.

Bu üç sonuç bir çelişki değildir; ölçünün kendisidir. Desteklenen geri sürüm sayısı bir ürün kararıdır. Ekip eski sürümleri ne kadar süre desteklerse destek yükü o kadar artar; destekledikçe bütçe genişler ve aşım azalır. Ders bütçeyi dayatmaz, yalnız bütçenin aşımı nasıl belirlediğini gösterir.

#### Sürüm Adı Kuralı Etkilemez

Sürüm adı kullanıcıya aittir: bir sürüme `2.3.0` demek pazarlama ya da iletişim kararıdır. Platform ise yalnız kodu okur. Ekibin sürüm adını `2.3.0` yapıp kodu 12'de bıraktığını düşünelim; ikinci bir senaryoda aynı sürüm adı verilir ama kod 13'e artırılır.

#### Sınırlayıcı Ölçüm

Sınırlayıcı ölçüm, dersin sorusunu aşımın dört yanıtından birine bağlamadan önce hangi değişikliğin kategoriyi oynattığını sorar. Üç senaryoyu aynı sekiz aygıtla ve iki bütçesiyle ölçelim: yalnız ad değişti, hem ad hem kod değişti. İlk senaryoda yayın geçmişine kodu 12 olan ama adı `2.3.0` olan bir yayın ekleniyor.

```kotlin
// Main.kt — surum adi degisip kod artmazsa uzaklik ayni kalir
fun main() {
    val devices = installedDevices()
    val budget = Budget("surum", 2)

    val before = releaseHistory()
    val renamed = before + Release(12, "2.3.0")
    val bumped = before + Release(13, "2.3.0")

    val baseline = Overrun("ilk-hal", budget, devices.map { versionsBehind(before, it) })
    val nameOnly = Overrun("yalniz-ad", budget, devices.map { versionsBehind(renamed, it) })
    val nameAndCode = Overrun("ad-ve-kod", budget, devices.map { versionsBehind(bumped, it) })
    listOf(baseline, nameOnly, nameAndCode).forEach { it.line() }
    println("kodlar tekduze mu: ${isMonotonic(renamed)} ${isMonotonic(bumped)}")
}
```

```text
ilk-hal: butce=surum<=2 | asan=4/8 -> cogunda
yalniz-ad: butce=surum<=2 | asan=4/8 -> cogunda
ad-ve-kod: butce=surum<=2 | asan=5/8 -> cogunda
kodlar tekduze mu: false true
```

Yalnız ad değişen senaryo ilk hâlle birebir aynı satırı verdi: aşan örnek sayısı dört, yanıt çoğunda. Kural sürüm adını görmez; `versionsBehind` yalnız kodları sayar ve kodlar değişmedi. Kod 13'e artınca aşan örnek sayısı beşe çıktı, yanıt yine **çoğunda** kaldı. Burada kuralın ikinci sınırı görünür: aşım sayısı arttı ama kategori sabit kaldı, çünkü kural sınırı aşan örnek sayısını dört yanıttan birine indirger.

Sonuç iki yönde okunur. Ad değişikliği kullanıcıya yeni bir sürüm olduğunu söyler ama bir aygıtı güncel yapmaz; güncelleme kararı kodla verilir. Kod artışı ise aynı sekiz aygıtı bir sürüm daha geride bırakır: son sürüm yeniden yayımlandığında herkes bir basamak geriye düşer. Her yayın bütçeyi bir yönde zorlar.

**DA4.** Model aynı kodlu ikinci yayını kabul eder ve onu mevcut koda sayar. Yayın konsolu gerçekte aynı sürüm koduyla ikinci bir yüklemeyi kabul etmez; model reddi kurmaz, çünkü ders kuralın sürüm adına bakmadığını göstermek istiyor. Bu senaryoda `isMonotonic` ilk durumda yanlış döner: bu dönüş reddin modeldeki karşılığıdır.

#### Kodu Adtan Türetmek

Birçok ekip kodu elle artırmaz; sürüm adından hesaplar. Yaygın bir biçim `major`, `minor` ve `patch` değerlerini ağırlıklarla toplamaktır. Bu kuralın sessiz bir tuzağı vardır: ağırlıklar dar seçilirse farklı adlar aynı koda iner. Tuzağı ölçelim. Girdi dokuz sürüm adıdır; bu kez listede `1.10.0` da var. Bütçe sıfırdır: aynı kodu paylaşan her yayın bir aşımdır. Her yayın için aynı kodu taşıyan öteki yayınların sayısı örnektir.

```kotlin
// Main.kt — addan kod uretiminde dar carpanlar farkli adlari ayni koda indirger
fun foldCode(name: String, minorWeight: Int, majorWeight: Int): Int {
    val (major, minor, patch) = name.split(".").map { it.toInt() }
    return major * majorWeight + minor * minorWeight + patch
}

fun collisions(names: List<String>, minorWeight: Int, majorWeight: Int): List<Int> {
    val codes = names.map { foldCode(it, minorWeight, majorWeight) }
    return codes.map { code -> codes.count { it == code } - 1 }
}

fun main() {
    val names = listOf("1.8.0", "1.8.1", "1.9.0", "1.10.0", "2.0.0", "2.0.1", "2.1.0", "2.2.0", "2.2.1")
    val budget = Budget("cakisma", 0)
    val narrow = collisions(names, 10, 100)
    val wide = collisions(names, 100, 10000)
    println("dar carpan: $narrow")
    println("genis carpan: $wide")
    Overrun("dar-carpan", budget, narrow).line()
    Overrun("genis-carpan", budget, wide).line()
}
```

```text
dar carpan: [0, 0, 0, 1, 1, 0, 0, 0, 0]
genis carpan: [0, 0, 0, 0, 0, 0, 0, 0, 0]
dar-carpan: butce=cakisma<=0 | asan=2/9 -> azinda
genis-carpan: butce=cakisma<=0 | asan=0/9 -> icinde
```

Dar çarpanla `1.10.0` ve `2.0.0` ikisi de 200 üretir. Dokuz yayından ikisi çakıştığı için yanıt **azında**; geniş çarpanda çakışma yoktur ve yanıt içinde. Çakışma yalnız bir tablo kusuru değildir: iki yayın aynı kodu taşıyınca platform onları ayırt edemez, yani ikincisini güncelleme olarak göremez. Kural çakışmanın nedenini görmez, yalnız kaç yayının etkilendiğini sayar.

**DA5.** Model kodu üç alanlı bir ad ve iki ağırlıkla katlar. Gerçek ekipler derleme numarası ya da tarih gibi başka şemalar da kullanır; model bunları kurmaz ve hangi şemanın seçilmesi gerektiğini söylemez. Ölçülen şey yalnız çakışma sayısıdır.

#### Yaygın Yanılgılar

**"Sürüm adı büyüdüyse sürüm yenidir."** Platform için yeni olan, kodu büyük olandır. Adı büyük kodu küçük bir paket, kurulu olandan eski sayılır.

**"Sürüm kodu `2.2.1` gibi bir biçim taşır."** Sürüm kodu tamsayıdır. Noktalı bir gösterim sürüm adının işidir; kodu birden çok alana bölen ekipler bunu kendi kuralıyla, tek bir tamsayıya katlayarak yapar.

**"Kodu addan türetmek her zaman güvenlidir."** Türetme tekdüzeliği korur gibi görünür ama ağırlıklar dar seçilirse çakışır. Yukarıdaki ölçümde `1.10.0` ve `2.0.0` aynı kodu verdi. Türetme kuralı değişmeden önce yayın geçmişindeki tüm adlarla denenmelidir.

**"Yalnız sürüm adını değiştirmek bir güncelleme yayımlar."** Platform için güncelleme, kodu artan pakettir. Yalnız adı değişen paket aynı sürüm sayılır; bu derste aşan örnek sayısı dört kaldı.

**"Geri alınan sürümün kodu geri alınır."** Kod düşürülemez. Geri alma, daha yüksek kodlu yeni bir derlemeyle yapılır; bu derse göre bütçe tek bir sürüm daha zorlanır.
**"Sürüm kodunu atlamak zararsızdır."** Kod yalnız artmalıdır; atlanan sayılar sıralamayı bozmaz ama boşa gider. Türetilmiş bir kodda atlama bir yan etki olarak da çıkar: ağırlıklar geniş seçilirse ardışık iki yayın arasındaki fark büyür. Ölçüm yalnız iki yayın arasındaki sıralamaya bakar; aradaki boşluğu görmez, bu yüzden boşluğun bir maliyeti olup olmadığına ekip kendi kuralıyla karar verir.

**"Çakışma yalnız aynı kodu üretir."** Çakışmanın asıl zararı iki ayrı yayının platform için aynı yayın sayılmasıdır. İkinci paket birincinin üzerine kurulamaz, çünkü kodu daha büyük değildir. Bu yüzden çakışma bütçesi sıfırdır ve tek bir çakışma bile aşımdır.


#### Özet

- Bu dersin aşımı: çoğunda. Sekiz aygıttan dördü, ikiden fazla sürüm geride kaldı: yarısı sınırı aştığı için yanıt azında değil, çoğunda.
- Sürüm kodu platformun karşılaştırdığı tamsayıdır ve tekdüze artar; sürüm adı kullanıcıya gösterilen dizedir ve karşılaştırmaya girmez.
- Bütçe en düşük desteklenen sürümden türedi: 10 ve 12 arasındaki fark ikiydi. Aynı sekiz aygıt yedi bütçesinde içinde, dört bütçesinde azında, iki bütçesinde çoğunda çıktı.
- Yalnız sürüm adı değişince aşan örnek sayısı dört kaldı; kural sürüm adını görmez.
- Kodu addan türeten bir kural dar ağırlıkla `1.10.0` ile `2.0.0` adlarını aynı koda indirdi: dokuz yayından iki aşım, yanıt azında; geniş ağırlıkta içinde.
- Sürüm kodu 13'e artınca aşan örnek sayısı beşe çıktı ama yanıt çoğunda kaldı; kural sayıyı sayar, büyüklüğü görmez.

#### Sonraki Adım

Bu derste sürümün kimliği elle yazıldı: bir betik parçasında `versionCode = 12`. Gerçek bir ekipte bu sayıyı her değişiklikte birinin artırması ve her derlemeyi birinin üretmesi gerekir. Sıradaki ders, `Sürekli Tümleştirme ve Teslimat`, bir değişiklik geldiğinde kaç işin koştuğunu ölçer.

### Sürekli Tümleştirme ve Teslimat

Source: https://academia.sh/tr/kurslar/android-uretim/surekli-tumlestirme-ve-teslimat

Bir değişiklik geldiğinde hattın kaç iş koşturduğunu ölçer. Değişiklik süzgeci yalnız etkilenen modüllerin işlerini çalıştırır; ortak modüle dokunan değişiklikler bütçeyi aşar, süzgeçsiz hat her değişiklikte bütün işleri koşar.

Önceki ders sürüm kimliğini elle verdi: bir derleme betiğinde bir tamsayı. Bu sayıyı her değişiklikte birinin artırması, her derlemeyi birinin üretip sınaması gerekir. İş elle yapılırsa atlanır; atlanan iş de ancak bir kullanıcı sorunla karşılaşınca fark edilir. Sürekli tümleştirme (continuous integration) bu işi bir hatta bağlar: her değişiklik geldiğinde derleme ve sınama kendiliğinden koşar. Sürekli teslimat (continuous delivery) aynı hattın ucunu uzatır: sınamadan geçen derleme dağıtıma hazır bir çıktı olarak bekler.

Hat iyi çalıştığında bir değişiklik yeşil ya da kırmızı döner ve kimse neyin koştuğunu sormaz. Soru, hat büyüyüp her değişiklikte uzun süre beklenmeye başlayınca ortaya çıkar. Bu yüzden kursun sorusunu bu derste hatta uyarlıyoruz: **bu ölçüm bütçesini kaç örnekte aşıyor?** Ölçüm bir değişiklik için koşan iş sayısıdır, bütçe ekibin bir değişiklik başına kabul ettiği iş sayısıdır, her değişiklik bir örnektir.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Overrun` sınırı kesin aşan örnekleri sayar ve yaygınlığı dört adla verir: içinde, azında, çoğunda, hepsinde. Kural kaç değişikliğin aştığını görür; bir değişikliğin altı mı yoksa yedi iş mi koşturduğu aşan her örnek için aynıdır.

#### Hat, İş ve Süzgeç

Hat (pipeline) işlerden kurulur. İş (job), tek bir amacı olan bir adımdır: statik denetim, bir modülün birim sınaması, uygulamanın derlenmesi, aygıt üzerinde koşan sınama. Her iş hangi modüllere bağlı olduğunu söyler. Değişiklik süzgeci (change filter) bu bağı kullanır: bir değişiklik yalnız `data` modülüne dokunduysa, `data` ile ilgisi olmayan bir işin koşması gerekmez.

```yaml
# Platform kodu: iş akışı tanımı
on:
  pull_request:
    paths:
      - "feature/**"
      - "core/**"
jobs:
  unit-feature:
    steps:
      - run: ./derle.sh :feature:test
```

Bu parça bir iş akışı tanımıdır; çalıştırılmaz ve burada çıktı iddia etmiyoruz. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde süzgeç bir küme kesişimidir: iş koşar eğer bağlı olduğu modüllerden biri değişmişse. Hiçbir modüle bağlı olmayan iş, yani bağımlılık listesi boş olan iş, her değişiklikte koşar.

Yukarıdaki parçada `paths` alanı süzgeçtir: yalnız `feature` ya da `core` altındaki dosyalara dokunan bir değişiklik bu iş akışını başlatır. Süzgecin iki yüzü vardır. Yararı, ilgisiz değişikliklerde işi hiç koşturmamasıdır. Riski, yolların elle yazılmasıdır: yeni bir modül eklenip yola yazılmazsa o modüldeki değişiklik hiçbir işi tetiklemez. Modelde bu riski `Job` sınıfındaki `modules` kümesi taşır; küme eksik yazılırsa iş koşmaz ve sayı düşer.

```kotlin
// PipelineModel.kt — isleri, degisiklikleri ve degisiklik suzgecini modeller
class Job(val name: String, val modules: Set<String>)

class Change(val label: String, val modules: Set<String>)

fun runs(job: Job, change: Change): Boolean =
    job.modules.isEmpty() || job.modules.any { it in change.modules }

fun jobsForChange(jobs: List<Job>, change: Change): List<String> =
    jobs.filter { runs(it, change) }.map { it.name }

fun pipelineJobs(): List<Job> = listOf(
    Job("denetim", emptySet()),
    Job("birim-cekirdek", setOf("core")),
    Job("birim-veri", setOf("data")),
    Job("birim-ozellik", setOf("feature", "core")),
    Job("birim-uygulama", setOf("app", "core")),
    Job("aygit-testi", setOf("app", "feature")),
    Job("derleme", emptySet()),
)

fun sampleChanges(): List<Change> = listOf(
    Change("yorum", emptySet()),
    Change("veri", setOf("data")),
    Change("ozellik", setOf("feature")),
    Change("uygulama", setOf("app")),
    Change("veri-ozellik", setOf("data", "feature")),
    Change("uygulama-veri", setOf("app", "data")),
    Change("cekirdek", setOf("core")),
    Change("yazim-duzeltme", emptySet()),
)
```

Yedi iş ve sekiz değişiklik var. `denetim` ve `derleme` modül listesi boş olduğu için her değişiklikte koşar; öteki beş iş yalnız bağlı olduğu modül değişince koşar. Sekiz değişikliğin ikisi yalnız yorum ve yazım düzeltmesidir, hiçbir modüle dokunmaz.

#### Değişiklik Başına İş

Bütçeyi dört iş olarak koyalım: ekip bir değişikliğin dört işten fazlasını beklemesini istemiyor. Ölçüm, her değişiklik için `jobsForChange` sonucunun boyutudur.

```kotlin
// Main.kt — her degisiklik icin kosan is sayisi olculur, butce dort is
fun main() {
    val jobs = pipelineJobs()
    val changes = sampleChanges()
    for (change in changes) {
        println("${change.label}: ${jobsForChange(jobs, change)}")
    }
    val samples = changes.map { jobsForChange(jobs, it).size }
    println("kosan is: $samples")
    Overrun("degisiklik-basina-is", Budget("is", 4), samples).line()
}
```

```text
yorum: [denetim, derleme]
veri: [denetim, birim-veri, derleme]
ozellik: [denetim, birim-ozellik, aygit-testi, derleme]
uygulama: [denetim, birim-uygulama, aygit-testi, derleme]
veri-ozellik: [denetim, birim-veri, birim-ozellik, aygit-testi, derleme]
uygulama-veri: [denetim, birim-veri, birim-uygulama, aygit-testi, derleme]
cekirdek: [denetim, birim-cekirdek, birim-ozellik, birim-uygulama, derleme]
yazim-duzeltme: [denetim, derleme]
kosan is: [2, 3, 4, 4, 5, 5, 5, 2]
degisiklik-basina-is: butce=is<=4 | asan=3/8 -> azinda
```

Üç değişiklik dört işi aşıyor: iki modüle dokunan iki değişiklik ve ortak modüle dokunan bir değişiklik. Sekizin yarısından azı aştığı için yanıt **azında**. Bu, dersin aşımıdır. Süzgeç çoğu değişikliği bütçenin içinde tutar; aşan örnekler hattın zayıf yeridir.

`cekirdek` satırı ayrıca bir şey gösterir: yalnız `core` değişti, ama `core` üç işin bağımlılığındadır, bu yüzden bu değişiklik tek bir modüle dokunmasına rağmen en geniş koşuyu tetikledi. Ortak modül, hattın içinde bir kavşak gibi davranır: ne kadar çok modülün bağımlı olduğu, o modüldeki bir değişikliğin o kadar çok iş çalıştırması demektir.

**DA3.** Model iş sayısını sayar ve her işi eşit görür. Gerçek bir hatta bir birim sınaması çok kısa, bir aygıt testi çok uzun sürebilir; model bu farkı kurmaz. Ayrıca modül bağları bir girdidir: gerçekte bağlar derleme betiğindeki bağımlılıklardan çıkar, burada elle yazıldı.

#### Geçişli Bağımlılık

Şimdiye dek her işin bağlı olduğu modülleri elle yazdık. Gerçekte bir modülün değişmesi, ona bağlı modülleri de etkiler: `core` değişince `core`'a dayanan `data` ve `feature`, onlara dayanan `app` da değişmiş sayılır. Bağlar bir grafikte durur ve değişikliğin etkilediği küme grafikten türetilir. Aşağıdaki ölçüm aynı sekiz değişikliği iki yoldan sayar. Birincisi yalnız doğrudan değişen modülleri kullanır. İkincisi değişikliği bağımlı modüllere yayar. İşler bu kez her modül için ayrı bir birim sınamasıdır; `denetim` ve `derleme` yine her değişiklikte koşar.

```kotlin
// Main.kt — gecisli bagimlilik hesaba katilinca ayni degisiklikler daha fazla is kosar
val dependsOn = mapOf(
    "data" to setOf("core"),
    "feature" to setOf("core"),
    "app" to setOf("feature", "data"),
)

fun affected(changed: Set<String>): Set<String> {
    var result = changed
    while (true) {
        val next = result + dependsOn.filter { (_, deps) -> deps.any { it in result } }.keys
        if (next == result) return result
        result = next
    }
}

fun main() {
    val jobs = listOf(
        Job("denetim", emptySet()),
        Job("birim-cekirdek", setOf("core")),
        Job("birim-veri", setOf("data")),
        Job("birim-ozellik", setOf("feature")),
        Job("birim-uygulama", setOf("app")),
        Job("derleme", emptySet()),
    )
    val changes = sampleChanges()
    val direct = changes.map { jobsForChange(jobs, it).size }
    val spread = changes.map { jobsForChange(jobs, Change(it.label, affected(it.modules))).size }
    println("dogrudan is: $direct")
    println("gecisli is: $spread")
    val budget = Budget("is", 4)
    Overrun("dogrudan", budget, direct).line()
    Overrun("gecisli", budget, spread).line()
}
```

```text
dogrudan is: [2, 3, 3, 3, 4, 4, 3, 2]
gecisli is: [2, 4, 4, 3, 5, 4, 6, 2]
dogrudan: butce=is<=4 | asan=0/8 -> icinde
gecisli: butce=is<=4 | asan=2/8 -> azinda
```

Aynı değişiklikler aynı bütçede doğrudan sayımda **içinde**, geçişli sayımda **azında** çıktı. Fark `cekirdek` satırında en büyüktür: doğrudan sayımda üç iş, geçişli sayımda altı iş koştu, çünkü çekirdek değişince dört modülün hepsi etkilendi. Ortak modülün kavşak olduğunu söylemiştik; bu ölçüm onun maliyetini grafikten türetir. Doğrudan sayım hattı gerçekte olduğundan ucuz gösterir.

**DA5.** Model bağımlılık grafiğini elle yazılmış bir tablo olarak alır ve döngü içermediğini varsayar. Gerçek bir derleme sistemi grafiği betiklerden çıkarır; burada çıkarma yoktur. Ayrıca model her etkilenen modülün sınamasını koşturur; gerçek bir hat yalnız arayüzü değişen modüllerde koşmayı da seçebilir ve model bunu kurmaz.

#### Bütçe Bir Karardır

Aynı sekiz değişikliği dört ayrı bütçeyle ölçelim. Bütçe değişince aşım da değişir; bu çelişki değildir, ölçünün kendisidir.

```kotlin
// Main.kt — ayni sekiz degisiklik dort ayri butceyle dort ayri asim verir
fun main() {
    val jobs = pipelineJobs()
    val samples = sampleChanges().map { jobsForChange(jobs, it).size }
    val roomy = Overrun("bes-is", Budget("is", 5), samples)
    val normal = Overrun("dort-is", Budget("is", 4), samples)
    val tight = Overrun("uc-is", Budget("is", 3), samples)
    val minimal = Overrun("bir-is", Budget("is", 1), samples)
    listOf(roomy, normal, tight, minimal).forEach { it.line() }
    countOverruns(listOf(roomy, normal, tight, minimal))
}
```

```text
bes-is: butce=is<=5 | asan=0/8 -> icinde
dort-is: butce=is<=4 | asan=3/8 -> azinda
uc-is: butce=is<=3 | asan=5/8 -> cogunda
bir-is: butce=is<=1 | asan=8/8 -> hepsinde
icinde: 1
azinda: 1
cogunda: 1
hepsinde: 1
```

Dört bütçe dört farklı kategori verdi. Beş işlik bütçede hiçbir değişiklik aşmadı, çünkü en geniş koşu beş işti. Bir işlik bütçede ise `denetim` ve `derleme` her değişiklikte koştuğu için sekiz değişikliğin hepsi aştı. Burada bir ders çıkar: her değişiklikte koşan işlerin sayısı, süzgecin hiçbir zaman altına inemeyeceği tabandır. Bütçe o tabanın altına konursa, hangi süzgeç kurulursa kurulsun yanıt hepsinde olur.

#### Teslimat Ucu

Hattın teslimat ucu tümleştirmeden farklı bir soru sorar: bu derleme dağıtıma hazır mı? Burada imza ve paketleme devreye girer. İmzalı bir derleme, yayın anahtarıyla imzalanmış çıktıdır; imza anahtarı hattın gizli girdisidir ve sıradan değişikliklerde koşan işlerden ayrı tutulur. Teslimat adımı bu yüzden her değişiklikte değil, yalnız bir yayın etiketi geldiğinde koşar.

```kotlin
// Platform kodu: imzali derleme
android {
    signingConfigs {
        create("release") {
            storeFile = file(providers.gradleProperty("keystorePath").get())
            keyAlias = providers.gradleProperty("keyAlias").get()
        }
    }
}
```

Anahtar yolu ve takma adı derleme betiğine gömülmez, hat dışarıdan verir. Bu parçanın çıktısı yoktur; yalnız imzanın yayın girdisi olduğunu gösterir. Teslimat ucundaki iş, bütçeye girmez: onu sayan bir model değişiklik başına değil yayın başına işler ve ders onu ölçmez.

#### Sınırlayıcı Ölçüm

Bu bölüm süzgecin değerini, süzgeci tümden kaldırarak ölçer; böylece bütçenin ulaşılabilir olup olmadığını görürüz. Süzgeci kaldıralım. Hattın bütün işleri bütün modülleri kapsayacak şekilde yazılır: her iş her değişiklikte koşar. Bu, kurmak için en kolay hattır ve çoğu ekibin ilk hâlidir.

```kotlin
// Main.kt — suzgecsiz hat her degisiklikte butun isleri kosar
fun main() {
    val everyModule = setOf("app", "core", "data", "feature")
    val unfiltered = pipelineJobs().map { Job(it.name, emptySet()) }
    val filtered = pipelineJobs()
    val changes = sampleChanges()
    val budget = Budget("is", 4)

    val withFilter = Overrun("suzgecli", budget, changes.map { jobsForChange(filtered, it).size })
    val noFilter = Overrun("suzgecsiz", budget, changes.map { jobsForChange(unfiltered, it).size })
    listOf(withFilter, noFilter).forEach { it.line() }
    println("modul sayisi: ${everyModule.size}, is sayisi: ${filtered.size}")
}
```

```text
suzgecli: butce=is<=4 | asan=3/8 -> azinda
suzgecsiz: butce=is<=4 | asan=8/8 -> hepsinde
modul sayisi: 4, is sayisi: 7
```

Süzgeçli hat aynı bütçede **azında**, süzgeçsiz hat **hepsinde** çıktı. Süzgeçsiz hatta bütün işler boş bağımlılık listesiyle yazıldığı için yedi iş her değişiklikte koşar ve sekiz değişikliğin sekizi dört işi aşar. Yorum düzeltmesi bile yedi iş çalıştırır.

Burada kuralın sınırı görünür. Kural, süzgecin bir iş koşmasını engellediğini bilmez; yalnız sayıyı görür. Süzgeç yanlış kurulmuşsa, yani gereken bir iş bağımlılığı eksik yazıldığı için koşmuyorsa, iş sayısı düşer ve hat daha iyi görünür, oysa sınanması gereken bir değişiklik sınanmamıştır. Aşım sayısı hattın doğruluğunu ölçmez; yalnız maliyetini ölçer.

**DA4.** Model süzgecin doğru kurulduğunu varsayar. Süzgecin yanlış kurulması, yani bir bağımlılığın unutulması modelde yoktur; bunu görmek için her iş için hangi değişikliklerde koşması gerektiğini ayrıca doğrulamak gerekir.

#### Yaygın Yanılgılar

**"Sürekli tümleştirme ve sürekli teslimat aynı şeydir."** Tümleştirme her değişikliği derler ve sınar. Teslimat sınamadan geçen derlemeyi dağıtıma hazır tutar. Bu derste ikincisi ölçülmedi, çünkü teslimat adımı değişiklik başına değil yayın başına koşar.

**"Süzgeç kurulduysa hat ucuzlamıştır."** Süzgeç yalnız koşan iş sayısını düşürür. Doğrudan sayım ile geçişli sayım arasındaki fark, yanlış kurulmuş bir süzgecin hattı olduğundan ucuz gösterebileceğini gösterdi. Süzgecin doğruluğu ayrıca denetlenir.

**"Daha az iş her zaman daha iyi bir hattır."** Kural iş sayısını sayar; hangi işin atlandığını görmez. Gereken bir iş atlandıysa sayı düşer ama hat kötüleşir.
**"Hat ne kadar hızlıysa o kadar iyidir."** Bu ders süreyi ölçmedi; yalnız bir değişiklikte koşan iş sayısını saydı. İş sayısı süreyi vermez, çünkü işler paralel koşabilir ve bir işin süresi ötekinden çok farklı olabilir. Süre iddiası ayrı bir ölçüm ister.

**"Bağımlılık grafiği bir kez çıkarılır ve değişmez."** Modüller arası bağımlılık her değişiklikte yeniden doğar. Grafik elle tutulursa eskir ve geçişli sayım sessizce yanlış olur; bu dersin modeli grafiği elle yazılmış bir girdi olarak aldı ve gerçek bir hatta grafiğin yapılandırmadan okunması gerekir.

**"Her değişiklik her modülü etkiler."** Etkilemez. Yalnız bir modülü değiştiren bir düzenleme, o modüle bağımlı olmayanları koşturmak zorunda bırakmaz; süzgecin işi bu ayrımı yapmaktır.


#### Özet

- Bu dersin aşımı: azında. Sekiz değişikliğin üçü dört işlik bütçeyi aştı; yarısından azı aştığı için yanıt azında.
- Sürekli tümleştirme her değişiklikte derleme ve sınamayı koşturur, sürekli teslimat aynı hattı dağıtıma hazır bir çıktıya uzatır.
- Süzgeç yalnız bağlı modülü değişen işleri koşturur; ortak modüle dokunan değişiklik üç işi birden tetikleyerek en geniş koşuyu verdi.
- Aynı değişiklikler beş, dört, üç ve bir işlik bütçelerde sırayla içinde, azında, çoğunda ve hepsinde çıktı; her değişiklikte koşan iki iş tabanı belirler.
- Bağımlılık grafiği hesaba katılınca aynı değişiklikler daha fazla iş koşturdu: doğrudan sayım içinde, geçişli sayım azında çıktı; doğrudan sayım hattı ucuz gösterir.
- Süzgeçsiz hat dört işlik bütçeyi sekiz değişikliğin sekizinde aştı: hepsinde. Kural maliyeti sayar, süzgecin doğruluğunu sayamaz.

#### Sonraki Adım

Hat bir derlemeyi sınadı ve dağıtıma hazır bıraktı. Derlemenin gerçek kullanıcılara ulaşmadan önce geçeceği yollar vardır: iç sınama, kapalı sınama, açık sınama. Sıradaki ders, `Test Kanalları`, bir derlemenin bu yollardan kaç adımda geçtiğini ölçer.

### Test Kanalları

Source: https://academia.sh/tr/kurslar/android-uretim/test-kanallari

Bir derlemenin iç, kapalı ve açık test kanallarından geçip üretime çıkana kadar kaç adım attığını ölçer. Geri çevrilen derlemenin yeniden derlenip adımları baştan saydığı durumda aşımın nasıl değiştiğini gösterir.

Önceki ders bir değişikliğin hatta kaç iş koşturduğunu ölçtü. Hattın ucunda dağıtıma hazır bir derleme bekler, ama hazır olması kullanıcılara ulaşması anlamına gelmez. Derleme önce ekibin kendi aygıtlarına, sonra seçilmiş bir test grubuna, sonra daha geniş bir test kitlesine iner; en sonda üretime çıkar. Her duraktaki kanal (track) o derlemenin farklı bir kitleyle sınanmasıdır.

Kursun sorusu burada da aynıdır: **bu ölçüm bütçesini kaç örnekte aşıyor?** Ölçüm bir derlemenin üretime çıkana kadar attığı terfi adımıdır, bütçe ekibin kabul ettiği adım sayısıdır, her derleme bir örnektir.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Overrun` sınırı kesin aşan örnekleri sayar ve yaygınlığı dört adla verir: içinde, azında, çoğunda, hepsinde. Kural kaç derlemenin aştığını görür; bir derlemenin bir mi yoksa üç adım fazla mı attığı aşan her örnek için aynı sayılır.

#### Dört Kanal

Yayın yolu dört durağa ayrılır. İç test kanalı (internal testing) ekibin kendisine aittir: derleme yüklenir ve ekip aygıtlarında hemen denenebilir. Kapalı test kanalı (closed testing) ekibin seçtiği bir test grubuna açılır. Açık test kanalı (open testing) mağazada görünen ve herkesin katılabildiği bir teste açılır. Üretim kanalı (production) kullanıcıların kendisidir.

Dört durağın sırası yayın yolunu verir: iç, kapalı, açık, üretim. Bir derleme bir sonraki kanala terfi eder (promotion) ya da bir kanalda geri çevrilir. Geri çevrilme bir hatanın bulunmasıdır: derleme düzeltilir ve yeniden denenir.

Model yolu bir liste olarak kurar. Her kanala girmek bir adım sayılır. Bir kanalda geri çevrilen derleme, geri çevrilmeyi tüketir ve yeniden girer; bu da bir adım daha demektir.

```kotlin
// ReleaseTrackModel.kt — kanal yolunu ve terfi adimi sayisini modeller
class Build(val label: String, val rejectedAt: List<String>)

fun trackPath(): List<String> = listOf("ic", "kapali", "acik", "uretim")

fun promotionSteps(path: List<String>, build: Build, restartOnReject: Boolean): Int {
    var steps = 0
    var index = 0
    val pending = build.rejectedAt.toMutableList()
    while (index < path.size) {
        steps++
        val track = path[index]
        if (track in pending) {
            pending.remove(track)
            index = if (restartOnReject) 0 else index
        } else {
            index++
        }
    }
    return steps
}

fun sampleBuilds(): List<Build> = listOf(
    Build("derleme-a", emptyList()),
    Build("derleme-b", emptyList()),
    Build("derleme-c", emptyList()),
    Build("derleme-d", emptyList()),
    Build("derleme-e", listOf("kapali")),
    Build("derleme-f", listOf("acik")),
    Build("derleme-g", listOf("ic")),
    Build("derleme-h", emptyList()),
)
```

Sekiz derlemenin dördü hiç geri çevrilmedi, üçü bir kez geri çevrildi: biri iç kanalda, biri kapalı kanalda, biri açık kanalda. Son derleme de temiz geçti.

#### Yolun Ölçümü

Ekip bir derlemenin dört kanal artı bir yeniden denemeyi kabul ettiğini söylüyor: bütçe beş adım. Önce geri çevrilen derlemenin yalnız o kanalı yeniden denediği durumu ölçelim.

```kotlin
// Main.kt — geri cevrilen derleme ayni kanalda yeniden denenir, butce bes adim
fun main() {
    val path = trackPath()
    val builds = sampleBuilds()
    val samples = builds.map { promotionSteps(path, it, false) }
    for (build in builds) {
        println("${build.label}: ret=${build.rejectedAt}")
    }
    println("adim: $samples")
    Overrun("terfi-adimi", Budget("adim", 5), samples).line()
}
```

```text
derleme-a: ret=[]
derleme-b: ret=[]
derleme-c: ret=[]
derleme-d: ret=[]
derleme-e: ret=[kapali]
derleme-f: ret=[acik]
derleme-g: ret=[ic]
derleme-h: ret=[]
adim: [4, 4, 4, 4, 5, 5, 5, 4]
terfi-adimi: butce=adim<=5 | asan=0/8 -> icinde
```

Hiçbir derleme beş adımı aşmadı: geri çevrilen üç derleme tam beş adımda, geri kalanı dört adımda tamamladı. Yanıt **içinde**. Bu, dersin aşımıdır. Beş adım sınıra eşittir ve eşitlik aşım sayılmaz.

Bu sonuç bir şeyi saklar: bütçe, bir kez geri çevrilen derlemenin payını kapsayacak kadar geniş koyuldu. Bütçeyi dörde indirmek, tek bir geri çevirmeyi bile aşım yapar.

**DA3.** Model her kanalı bir adım sayar ve kanalda kalınan süreyi kurmaz. Gerçekte bir test grubunun derlemeyi kullanması bekleme ister; model beklemenin uzunluğunu da, bir kanalın diğerinden daha çok zaman almasını da görmez. Geri çevirmeler de girdidir: hangi derlemenin hangi kanalda geri çevrildiği elle yazıldı.

#### Bütçe Bir Karardır

Aynı sekiz derlemeyi üç bütçeyle ölçelim: beş adım, dört adım, üç adım.

```kotlin
// Main.kt — ayni sekiz derleme uc ayri butceyle uc ayri asim verir
fun main() {
    val path = trackPath()
    val samples = sampleBuilds().map { promotionSteps(path, it, false) }
    val roomy = Overrun("bes-adim", Budget("adim", 5), samples)
    val normal = Overrun("dort-adim", Budget("adim", 4), samples)
    val tight = Overrun("uc-adim", Budget("adim", 3), samples)
    listOf(roomy, normal, tight).forEach { it.line() }
    countOverruns(listOf(roomy, normal, tight))
}
```

```text
bes-adim: butce=adim<=5 | asan=0/8 -> icinde
dort-adim: butce=adim<=4 | asan=3/8 -> azinda
uc-adim: butce=adim<=3 | asan=8/8 -> hepsinde
icinde: 1
azinda: 1
cogunda: 0
hepsinde: 1
```

Dört adımlık bütçede yalnız geri çevrilen üç derleme aştı, yanıt azında. Üç adımlık bütçede dört kanalın kendisi bütçeyi aştığı için sekiz derlemenin sekizi de aştı: temiz geçen derleme bile dört adım atar. Burada tabana dikkat edin: yol dört kanaldan oluştuğu sürece hiçbir derleme dört adımın altına inemez. Üç adımlık bütçe bir ekip kararı olarak yazılsa, kanallardan birini atlamayı gerektirir.

#### Yeniden Derleme

Geri çevrilen derleme düzeltilir ve yeniden derlenir. Yeniden derleme yeni bir pakettir ve sürüm kodu, bir önceki derste gördüğümüz gibi, bir öncekinden büyük olmak zorundadır.

```kotlin
// Platform kodu: yeniden derleme
android {
    defaultConfig {
        versionCode = 13
    }
}
```

Bu parça platform kodudur; çıktı iddia etmiyoruz. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Önemli olan sonuçtur: yeniden derlenen paket, önceki derlemeyle aynı olmadığı için önceki kanallarda alınan sonuç yeni pakete otomatik geçmeyebilir. Ekip bu durumda ya yalnız geri çevrildiği kanalı yeniden dener, ya da yolu baştan yürür. Bu seçim bir politikadır; model ikisini de bir bayrakla kurar.

Betikteki `versionCode = 13` satırı iki şeyi birden anlatır. Birincisi, yeniden derlemenin yeni bir sürüm kodu taşımasıdır; kod önceki dersin kuralıyla yalnız artabilir. İkincisi, kanallarda alınan sonucun paketin kendisine değil o paketin kodu taşıdığı kimliğe bağlı olmasıdır. Kimlik değişince önceki kanalın onayı, ekibin politikası aksini söylemedikçe yeni pakete taşınmaz. Modelde `restartOnReject` bayrağı bu politikayı kurar: yanlışsa yalnız geri çevrilen kanal yinelenir, doğruysa yol başa döner.

#### Sınırlayıcı Ölçüm

Dersin aşımı içindedir; sınırlayıcı ölçüm bu rahat sonucun neyin üzerinde durduğunu sorar. Yeniden derleme baştan başlıyorsa, her geri çevirme önceki kanalları da yeniden sayar. Aynı sekiz derlemeyi iki ayrı kuralla ölçelim ve dokuzuncu olarak aynı kanalda iki kez geri çevrilen bir derleme ekleyelim.

```kotlin
// Main.kt — yeniden derleme yolu bastan yurutur, adimlar yeniden sayilir
fun main() {
    val path = trackPath()
    val builds = sampleBuilds()
    val budget = Budget("adim", 5)

    val retry = builds.map { promotionSteps(path, it, false) }
    val restart = builds.map { promotionSteps(path, it, true) }
    println("ayni kanalda yeniden: $retry")
    println("bastan yeniden: $restart")
    Overrun("ayni-kanal", budget, retry).line()
    Overrun("bastan", budget, restart).line()

    val twice = Build("derleme-i", listOf("kapali", "kapali"))
    val withTwice = builds + twice
    Overrun("ayni-kanal-iki-ret", budget, withTwice.map { promotionSteps(path, it, false) }).line()
    Overrun("bastan-iki-ret", budget, withTwice.map { promotionSteps(path, it, true) }).line()
}
```

```text
ayni kanalda yeniden: [4, 4, 4, 4, 5, 5, 5, 4]
bastan yeniden: [4, 4, 4, 4, 6, 7, 5, 4]
ayni-kanal: butce=adim<=5 | asan=0/8 -> icinde
bastan: butce=adim<=5 | asan=2/8 -> azinda
ayni-kanal-iki-ret: butce=adim<=5 | asan=1/9 -> azinda
bastan-iki-ret: butce=adim<=5 | asan=3/9 -> azinda
```

Aynı kanalda yeniden denenen yolda aşım **içinde**, yolun baştan yürüdüğü kuralda **azında** çıktı. Baştan yürüyen yolda kapalı kanalda geri çevrilen derleme altı, açık kanalda geri çevrilen yedi adım attı; iç kanalda geri çevrilen derleme beşte kaldı, çünkü geri çevrildiği anda yolun başındaydı. Geri çevrilme ne kadar ileride olursa baştan sayılan adım o kadar çok olur.

Dokuzuncu derleme tabloyu da değiştirir. Aynı kanalda iki kez geri çevrilen derleme yeniden denemeyle altı adım, baştan yürüyen kuralda sekiz adım tutar. Kategori iki durumda da **azında** kaldı, ama aşan örnek sayısı birden üçe çıktı. Kural sayıyı sayar, büyüklüğü görmez: bir derlemenin bir adım, bir başkasının üç adım aşması aynı kategoriyi verir.

**DA4.** Model geri çevrilmeyi bir girdi olarak alır ve düzeltmenin başarılı olacağını varsayar. Gerçekte bir düzeltme yeni bir hata getirebilir; model bunu kurmaz. Geri çevirme sayısı da bir kural değil, elle yazılmış bir listedir.

#### Kanal Atlamak

Adım sayısını azaltmanın en kısa yolu bir kanalı yoldan çıkarmaktır. Kapalı kanalı atlayan yol üç duraktan oluşur ve temiz bir derleme üç adımda biter. Ama kanalın işi adım saymak değildir; o kanalda yakalanacak bir hatayı yakalamaktır. Aşağıdaki ölçüm yolu iç, açık ve üretim olarak kurar ve bütçeyi üç adıma indirir. Ayrıca kapalı kanalda geri çevrilecek olan derlemelerin, yani kanal yolda olsaydı yakalanacak olanların hangileri olduğunu listeler.

```kotlin
// Main.kt — kapali kanali atlayan yol adimi azaltir ama o kanalda yakalanacak reti gozden kacirir
fun main() {
    val builds = sampleBuilds()
    val skipped = listOf("ic", "acik", "uretim")
    val samples = builds.map { promotionSteps(skipped, it, false) }
    println("adim: $samples")
    val missed = builds.filter { build -> build.rejectedAt.any { it !in skipped } }
    println("gozden kacan ret: ${missed.map { it.label }}")
    Overrun("kapali-atlandi", Budget("adim", 3), samples).line()
}
```

```text
adim: [3, 3, 3, 3, 3, 4, 4, 3]
gozden kacan ret: [derleme-e]
kapali-atlandi: butce=adim<=3 | asan=2/8 -> azinda
```

Dört kanallı yol üç adımlık bütçede hepsinde çıkmıştı; üç kanallı yol aynı bütçede **azında** çıktı. Ama kazanç bir bedelle geldi: `derleme-e` kapalı kanalda geri çevrilecekti, yol o kanalı içermediği için geri çevirme hiç görülmedi ve derleme düzeltilmeden açık kanala ilerledi. Adım sayısı düştü; hatanın yakalandığı durak yok oldu.

**DA5.** Model atlanan kanalda geri çevirmenin başka bir kanalda yakalanacağını varsaymaz ve bunu `gozden kacan ret` listesiyle gösterir. Ama hatanın daha sonra açık kanalda ya da üretimde ortaya çıkıp çıkmayacağını, ortaya çıkarsa maliyetinin ne olacağını kurmaz.

#### Yaygın Yanılgılar

**"Kanal sayısı kalite ölçüsüdür."** Kanal sayısı bir derlemenin geçtiği durak sayısıdır. Bir derlemenin çok kanaldan geçmesi iyi sınandığını göstermez; sınamanın kalitesi kanaldaki kitleye ve geri bildirime bağlıdır.

**"Geri çevrilen derleme kötü bir derlemedir."** Geri çevirme bir kanalın işini yapmasıdır. Kanallar hatayı üretime ulaşmadan bulmak için vardır.

**"Bütçeyi tutturmak için kanal çıkarmak yeterlidir."** Kanal çıkarmak adım sayısını düşürür ve ölçümü bütçenin içine ya da yakınına getirebilir. Ama kanalın yakalayacağı hata yakalanmaz. Yukarıdaki ölçüm bunu bir derleme üzerinde gösterdi: aşım azaldı, gözden kaçan geri çevirme birdi.

**"Yeniden derleme önceki kanalların sonucunu taşır."** Taşımaz, çünkü yeni bir pakettir ve yeni bir sürüm kodu taşır. Taşınıp taşınmayacağı ekibin politikasıdır; bu derste iki politika da ayrı ayrı ölçüldü.

**"Adım sayısı süreyi verir."** Verilmez. Bir adım bir gün de sürebilir, birkaç saat de; model süreyi ölçmez.
**"İç kanalda geçen derleme açık kanalda da geçer."** Geçmeyebilir. Kitle büyüdükçe aygıt ve kullanım çeşitliliği artar; iç kanalda hiç görünmeyen bir hata ancak daha geniş bir kitlede ortaya çıkar. Kanalların sırası bu yüzden büyüyen bir kitleye göredir.

**"Kanalları atlamak yalnız süre kazandırır."** Süre kazandırdığı doğrudur, ama kanalın yakalayacağı hata da üretime geçer. Bu derste kapalı kanalı atlayan yol adım bütçesini tutturdu ve gözden kaçan geri çevirme sayısı bire çıktı; kazanç ile kayıp aynı tabloda görüldü.

**"Kanal sırası ekibin zevkine bağlıdır."** Sıra, bir derlemenin kime ulaşmadan önce kimin elinden geçeceğini belirler. Sırayı değiştirmek, bir derlemeyi daha az denetlenmiş bir kitleye daha erken göstermek demektir. Bu bir tasarım kararıdır ve ölçümün kendisi sırayı önermez.

**"Geri bildirim sayısı kanalın başarısıdır."** Çok geri bildirim, çok hata olduğu anlamına da gelebilir, kitlenin ilgili olduğu anlamına da. Model geri bildirim sayısını ölçmez; adım sayısını ölçer.

**"Aşım azaldıysa süreç iyileşmiştir."** Aşım yalnız bir bütçeye göre sayılır. Bütçe gevşetilirse ya da kanal çıkarılırsa aşım kendiliğinden azalır; sürecin iyileşip iyileşmediği, hangi hataların hâlâ üretimden önce yakalandığına bakılarak anlaşılır. Bu yüzden aşım sayısı ile gözden kaçan geri çevirme sayısı yan yana okunmalıdır. Tek bir sayıya bakıp karar vermek, öteki sayının söyleyeceğini duymamaktır.

**"Her derleme dört kanaldan da geçmelidir."** Bu bir ekip politikasıdır, bir kural değildir. Küçük bir düzeltme ile büyük bir özellik aynı yolu izlemek zorunda değildir; ama yolu kısaltan her karar, yakalanamayacak hataların bir listesini de beraberinde getirir. Bu listeyi yazmak ve kimin kabul ettiğini bilmek, yolu kısaltmanın ön koşuludur.


#### Özet

- Bu dersin aşımı: içinde. Sekiz derlemenin hiçbiri beş adımlık bütçeyi aşmadı; bir kez geri çevrilen üç derleme tam beşte kaldı.
- Yayın yolu iç, kapalı, açık ve üretim kanallarından oluşur ve her kanala giriş bir adım sayıldı; bu yüzden hiçbir derleme dört adımın altına inemez.
- Aynı sekiz derleme beş, dört ve üç adımlık bütçelerde sırayla içinde, azında ve hepsinde çıktı.
- Geri çevrilen derleme yolu baştan yürürse adımlar altı ve yediye çıktı, aşım içinden azına geçti.
- Kapalı kanalı yoldan çıkarmak üç adımlık bütçede hepsinden azına indirdi, ama kapalı kanalda yakalanacak bir geri çevirme gözden kaçtı.
- İki kez geri çevrilen dokuzuncu derleme aşan örnek sayısını birden üçe çıkardı; kategori azında kaldı, çünkü kural büyüklüğü görmez.

#### Sonraki Adım

Derleme kanallardan geçti ve üretime çıkmaya hazır, ama üretime çıkış hemen tüm kullanıcılara olmak zorunda değildir. Sıradaki ders, `Aşamalı Yayın`, yayını aşama aşama genişletir ve her aşamada çökme sayısını bir eşikle karşılaştırır.

### Aşamalı Yayın

Source: https://academia.sh/tr/kurslar/android-uretim/asamali-yayin

Yayını kullanıcı kümesine aşama aşama genişletir ve her aşamada çökme sayısını hata eşiğiyle karşılaştırır. Aşım görülen aşamada durdurmanın sonraki aşamaları örnek toplamayı bıraktığını ve örnek sayısının neden en az dört olması gerektiğini gösterir.

Önceki ders bir derlemenin kanallardan nasıl geçtiğini ölçtü. Üretim kanalına çıkan derleme kullanıcıların kendi aygıtlarına iner; ne kadar iyi sınanmış olursa olsun bir kısım hata ancak gerçek kullanımda görünür. Aşamalı yayın (staged rollout) bu hatanın bedelini sınırlar: derleme önce küçük bir kullanıcı kümesine açılır, sorun görülmezse sonraki aşamada daha geniş bir kümeye genişler.

Her aşamada ekip yayını izler. İzleme penceresi (monitoring window) bir aşamanın açık kaldığı ve çökmelerin sayıldığı dönemdir. Çökme sayısı hata eşiğini (error threshold) aşarsa ekibin elinde iki karar vardır: yayını durdurmak (halt), yani sonraki aşamaya geçmemek ve sürümü yeni kullanıcılara sunmayı kesmek, ama sürümü almış aygıtlar eski sürüme dönmez; ya da geri almak (rollback), yani önceki sürümü daha yüksek bir sürüm koduyla yeniden yayımlamak. Aşamaların kaçıncı olduğu bu derste sırayla anılır; her ekibin kendi aşama düzeni vardır.

Kursun sorusu burada da aynıdır: **bu ölçüm bütçesini kaç örnekte aşıyor?** Ölçüm bir aşamada, aynı büyüklükteki bir kullanıcı kümesinden gelen çökme sayısıdır (`crashCount`), bütçe hata eşiğidir, her aşama bir örnektir.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Overrun` sınırı kesin aşan örnekleri sayar ve yaygınlığı dört adla verir: içinde, azında, çoğunda, hepsinde. Kural kaç aşamanın aştığını görür; bir aşamanın bir mi yoksa on çökme fazla mı taşıdığı aşan her örnek için aynıdır.

#### Çökmeyi Toplayan Kod

Uygulama bir çökme olduğunda sistem varsayılan bir işleyiciye gelir; ekipler çökmeyi kaydetmek için bu işleyiciyi kendi koduna yönlendirir.

```kotlin
// Platform kodu: CrashRecorder
Thread.setDefaultUncaughtExceptionHandler { thread, error ->
    recordCrash(thread.name, error)
}
```

Bu parça bir platform çağrısıdır; çıktı iddia etmiyoruz. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde çökme, bir oturumun sonucudur: oturum ya sağlam biter ya da çöker.

İşleyici sistemin çökme yolunu değiştirmez; yalnız çökmenin kaydedilmesini ekibin koduna bağlar. İşleyici kayıt aldıktan sonra uygulama yine sonlanır. Bu ayrım ölçüm için önemlidir: çökme sayısı, kayıt edilebilen çökmelerin sayısıdır. Kaydı hiç alınamayan çökme ya da hiç çökmeyen ama bozuk çalışan oturum sayıya girmez. Modelde oturum ya `cokme` ya `saglam` olduğundan bu iki eksik de modelin dışında kalır.

#### Aşamaları Modellemek

Yardımcı dosya her aşamayı sabit sayıda oturumdan oluşan bir küme olarak kurar. Aşamalar aynı büyüklükte olduğundan çökme sayıları doğrudan karşılaştırılabilir; aşamaların büyüklüğü farklı olsaydı sayı yanıltırdı.

```kotlin
// RolloutModel.kt — asamali yayini ve asama basina cokme sayisini modeller
class Stage(val label: String, val outcomes: List<String>)

fun crashCount(stage: Stage): Int = stage.outcomes.count { it == "cokme" }

fun stageNames(): List<String> = listOf(
    "ilk-asama", "ikinci-asama", "ucuncu-asama",
    "dorduncu-asama", "besinci-asama", "son-asama",
)

fun rolloutStages(crashes: List<Int>): List<Stage> =
    stageNames().zip(crashes).map { (name, crashed) ->
        Stage(name, List(10) { if (it < crashed) "cokme" else "saglam" })
    }

fun releaseA(): List<Int> = listOf(1, 1, 2, 5, 3, 4)

fun decide(count: Int, limit: Int): String = if (count > limit) "durdur" else "devam"

fun firstOverIndex(counts: List<Int>, limit: Int): Int {
    val index = counts.indexOfFirst { it > limit }
    return if (index == -1) counts.lastIndex else index
}
```

Her aşama on oturumdan kurulur; `releaseA` aşama başına çökme sayısını verir. Altı aşama, altı örnek demektir.

#### Aşamaların Ölçümü

Hata eşiğini üç çökme olarak koyalım: ekip bir aşamada üçten fazla çökmeyi kabul etmiyor.

```kotlin
// Main.kt — her asamadaki cokme sayisi olculur, hata esigi uc cokme
fun main() {
    val stages = rolloutStages(releaseA())
    val samples = stages.map { crashCount(it) }
    for (stage in stages) {
        println("${stage.label}: cokme=${crashCount(stage)}/${stage.outcomes.size}")
    }
    println("cokme: $samples")
    Overrun("asama-cokmesi", Budget("cokme", 3), samples).line()
}
```

```text
ilk-asama: cokme=1/10
ikinci-asama: cokme=1/10
ucuncu-asama: cokme=2/10
dorduncu-asama: cokme=5/10
besinci-asama: cokme=3/10
son-asama: cokme=4/10
cokme: [1, 1, 2, 5, 3, 4]
asama-cokmesi: butce=cokme<=3 | asan=2/6 -> azinda
```

İki aşama üç çökmeyi aştı: dördüncü aşama beş, son aşama dört çökme taşıdı. Beşinci aşama üçte durdu ve sınıra eşit olduğu için aşım sayılmadı. Altı aşamanın üçten azı aştığı için yanıt **azında**. Bu, dersin aşımıdır.

**DA3.** Model her aşamayı aynı büyüklükte on oturumdan kurar ve çökmeyi oturum başına bir sonuç sayar. Gerçekte aşamalar farklı büyüklüktedir ve bir kullanıcının çok oturumu bir çökmeyi tekrarlayabilir; model ikisini de görmez. Çökme sayıları elle yazılmış girdilerdir, bir ölçüm değildir.

#### Eşik Bir Karardır

Aynı altı aşamayı dört ayrı eşikle ölçelim. Eşik değişince aşım değişir; bu çelişki değil, ölçünün kendisidir.

```kotlin
// Main.kt — ayni alti asama dort ayri esikle dort ayri asim verir
fun main() {
    val samples = rolloutStages(releaseA()).map { crashCount(it) }
    val loose = Overrun("bes-cokme", Budget("cokme", 5), samples)
    val normal = Overrun("uc-cokme", Budget("cokme", 3), samples)
    val strict = Overrun("bir-cokme", Budget("cokme", 1), samples)
    val zero = Overrun("sifir-cokme", Budget("cokme", 0), samples)
    listOf(loose, normal, strict, zero).forEach { it.line() }
    countOverruns(listOf(loose, normal, strict, zero))
}
```

```text
bes-cokme: butce=cokme<=5 | asan=0/6 -> icinde
uc-cokme: butce=cokme<=3 | asan=2/6 -> azinda
bir-cokme: butce=cokme<=1 | asan=4/6 -> cogunda
sifir-cokme: butce=cokme<=0 | asan=6/6 -> hepsinde
icinde: 1
azinda: 1
cogunda: 1
hepsinde: 1
```

Dört eşik dört kategori verdi. Beş çökmeye izin veren eşik hiçbir aşamayı aşım saymadı. Sıfır çökmeye izin veren eşik ise altı aşamanın altısını aştı, çünkü her aşamada en az bir çökme vardı. Eşiğin hangi değerde olması gerektiği bir ürün kararıdır; model karar vermez, kararın aşımı nasıl değiştirdiğini gösterir.

#### Aşım Bir Karar Değildir

Burada bir ayrım yapmak gerekir. Durdurma, aşımın dört kategorisinden biri değildir; aşımın sonucunda verilen bir karardır. Kural yalnız sayıyı görür ve içinde, azında, çoğunda ya da hepsinde der. Yayını durdurmak ise ekibin, aşımı gördükten sonra aldığı karardır: bir aşamada eşik aşıldığında sonraki aşamaya geçilmez.

Bu yüzden karar ayrı bir işlevdedir. `Overrun` hiçbir noktada "durdur" demez; `decide` her aşama için ayrıca karar verir.

```kotlin
// Main.kt — kategori ile karar ayri islevlerdir
fun main() {
    val limit = 3
    val stages = rolloutStages(releaseA())
    val samples = stages.map { crashCount(it) }
    val overrun = Overrun("asama-cokmesi", Budget("cokme", limit), samples)
    overrun.line()
    for (stage in stages) {
        println("${stage.label}: karar=${decide(crashCount(stage), limit)}")
    }
    println("kategori: ${overrun.verdict}")
}
```

```text
asama-cokmesi: butce=cokme<=3 | asan=2/6 -> azinda
ilk-asama: karar=devam
ikinci-asama: karar=devam
ucuncu-asama: karar=devam
dorduncu-asama: karar=durdur
besinci-asama: karar=devam
son-asama: karar=durdur
kategori: azinda
```

Kategori **azında**; karar ise dördüncü aşamada durdurmaktır. İkisi farklı sorulara yanıt verir: kategori "aşım ne kadar yaygın?", karar "ne yapacağız?". İlk aşım görüldüğü anda yayın zaten durdurulacaksa, beşinci ve son aşamalar hiç açılmaz ve onların sayıları ölçülmez.

#### Sınırlayıcı Ölçüm

Bu bölüm bir kategorinin ne zaman okunabilir olmadığını sorar. Durdurma, sonraki aşamaların örneklenmesini keser. Örnek sayısı azalır ve kategori değişebilir. Üç ayrı yayın ele alalım: ilkinde aşım dördüncü aşamada, ikincisinde ikinci aşamada, üçüncüsünde ilk aşamada görünür. Her biri tam altı aşama ile ve ilk aşımda durdurulmuş hâliyle ölçülür.

```kotlin
// Main.kt — ilk asimda durdurmak ornek sayisini azaltir, kategori degisebilir
fun main() {
    val limit = 3
    val releases = listOf(
        "yayin-a" to releaseA(),
        "yayin-b" to listOf(1, 4, 1, 1, 2, 1),
        "yayin-c" to listOf(4, 1, 1, 1, 2, 1),
    )
    for ((name, crashes) in releases) {
        val full = rolloutStages(crashes).map { crashCount(it) }
        val halted = full.take(firstOverIndex(full, limit) + 1)
        Overrun("$name-tam", Budget("cokme", limit), full).line()
        Overrun("$name-durdurulmus", Budget("cokme", limit), halted).line()
    }
}
```

```text
yayin-a-tam: butce=cokme<=3 | asan=2/6 -> azinda
yayin-a-durdurulmus: butce=cokme<=3 | asan=1/4 -> azinda
yayin-b-tam: butce=cokme<=3 | asan=1/6 -> azinda
yayin-b-durdurulmus: butce=cokme<=3 | asan=1/2 -> cogunda
yayin-c-tam: butce=cokme<=3 | asan=1/6 -> azinda
yayin-c-durdurulmus: butce=cokme<=3 | asan=1/1 -> hepsinde
```

İlk yayında dört aşama örneklendi ve kategori **azında** kaldı. İkinci yayında yalnız iki örnek kaldı ve aynı yayın tam ölçümde azında iken durdurulmuş ölçümde **çoğunda** göründü. Üçüncü yayında tek örnek kaldı ve yanıt **hepsinde** oldu. Üç yayının da tam ölçümü azında; değişen yayının davranışı değil, ölçülen örnek sayısıdır.

Buradan örnek sayısı kuralı çıkar: bir ölçümden kategori çıkarmak için en az dört örnek gerekir. Dört örneğin altında kategori yalnız örnek toplamanın durdurulduğunu anlatır; yayının kendisini değil. İki örnekte bir aşım, örneklerin yarısıdır. Bu yüzden durdurulmuş bir yayının kategorisi okunmaz; kararın kendisi okunur.

**DA4.** Model durdurmanın hemen ilk aşımda yapıldığını varsayar. Gerçekte ekipler birkaç aşım bekleyebilir ya da aşımı bir insan değerlendirir; model bu gecikmeyi kurmaz. Model ayrıca durdurulan yayında sonraki aşamaların ne olacağını bilmez: durdurulan aşamalar sonradan açılırsa ölçüm yeniden başlar.

#### Aşama Büyüklüğü

Üçüncü varsayımda aşamaların aynı büyüklükte olduğunu söyledik. Gerçek bir yayın böyle ilerlemez: ilk aşama küçüktür, son aşama kullanıcıların çoğunu kapsar. Büyüklük değişince ham çökme sayısı yanıltıcı olur, çünkü büyük bir aşamada aynı çökme oranı daha çok çökme demektir. Aşağıdaki ölçüm altı aşamayı büyüyen boyutlarla kurar ve iki ölçümü yan yana koyar: ham çökme sayısı ve bin oturumdaki çökme sayısı. İkincisi tamsayı bölmesiyle hesaplanır, yani yuvarlama aşağıya doğrudur. İki ölçümün bütçesi farklı birimdedir ve birbirinden türetilmemiştir; ham sayı için üç çökme, oran için bin oturumda elli çökme koyduk.

```kotlin
// Main.kt — buyuyen asamalarda ham cokme sayisi ile bin oturumdaki oran baska yanit verir
fun main() {
    val sizes = listOf(10, 20, 50, 100, 200, 500)
    val crashes = listOf(1, 1, 2, 5, 6, 40)
    val stages = stageNames().indices.map { index ->
        Stage(stageNames()[index], List(sizes[index]) { if (it < crashes[index]) "cokme" else "saglam" })
    }
    val counts = stages.map { crashCount(it) }
    val rates = stages.map { crashCount(it) * 1000 / it.outcomes.size }
    println("cokme: $counts")
    println("bin oturumda cokme: $rates")
    Overrun("ham-sayi", Budget("cokme", 3), counts).line()
    Overrun("oran", Budget("cokme-bin", 50), rates).line()
}
```

```text
cokme: [1, 1, 2, 5, 6, 40]
bin oturumda cokme: [100, 50, 40, 50, 30, 80]
ham-sayi: butce=cokme<=3 | asan=3/6 -> cogunda
oran: butce=cokme-bin<=50 | asan=2/6 -> azinda
```

Ham sayıda üç aşama bütçeyi aştı ve yanıt **çoğunda** oldu; oranda yalnız iki aşama aştı ve yanıt **azında** kaldı. Fark ilk ve son aşamada görünür. İlk aşamada tek çökme bin oturumda yüz çökme demektir ve ham sayıda hiç görünmez. Son aşamada kırk çökme ham sayıda çok görünür ama beş yüz oturuma bölününce bin oturumda seksen çökmedir. Dördüncü aşamanın beş çökmesi bin oturumda elli çökmeye denk gelir ve her iki ölçümde de bütçenin üstünde değildir.

Sonuç ölçüm seçiminin kategoriyi oynattığıdır. Hangi ölçümün doğru olduğuna kural karar vermez; ekip bütçeyi bir birimle koyar ve o birimde sayar.

**DA5.** Model bir oturumu bir çökme sonucu sayar ve oranı tamsayı bölmesiyle hesaplar. Gerçekte oranlar kesirlidir ve küçük aşamalarda tek bir çökmenin oranı çok oynak kılması istatistiksel bir güvensizlik taşır; model bu güvensizliği ölçmez. Boyutlar ve çökmeler girdidir.

#### Yaygın Yanılgılar

**"Aşamalı yayın hatayı önler."** Önlemez; hatanın kaç kullanıcıyı etkilediğini sınırlar.

**"Çökme sayısı az ise yayın sağlıklıdır."** Sayı yalnız çöken oturumları sayar. Çökmeyen ama bozuk çalışan bir sürüm sayıda görünmez.

**"Küçük bir aşamadaki tek çökme önemsizdir."** Aşama küçükse tek çökme yüksek bir orandır; ham sayı bunu saklar. Yukarıdaki ilk aşama bin oturumda yüz çökmeye denk geldi.

**"Büyük aşamada çok çökme olması kötü bir işarettir."** Çok olması, büyük olmasının sonucu olabilir. Son aşamada kırk çökme bin oturumda seksene denk geldi ve bütçeyi aşan iki orandan yalnız biriydi.

**"Durdurma bir sonuçtur."** Durdurma kategorilerden biri değil, bir karardır. Aynı kategori iki ekipte iki ayrı kararla sonuçlanabilir.
**"Aşama sayısı artarsa yayın daha güvenli olur."** Her ek aşama çökmenin görüneceği bir pencere ekler, ama yayının tamamlanmasını da geciktirir. Aşama sayısı bir denge kararıdır; bu ders yalnız verilen aşamaların ölçümünü yaptı ve ideal sayıyı önermedi.

**"Oran ham sayıdan her zaman doğrudur."** Oran büyüklüğü hesaba katar ama küçük bir aşamada tek bir çökme bile oranı sıçratır. Yukarıdaki ilk aşamada bu görüldü. Oranın güvenilir olması için yeterli oturum gerekir; kaç oturumun yeterli olduğu bu derste verilmedi, çünkü ölçülmedi.

**"Çökme izlemesi yayın bitince biter."** Yayın bütün kullanıcılara ulaştıktan sonra da çökmeler gelir. Aşamalar yalnız hatanın erken yakalanma şansını artırır; izleme pencerelerinin kapanması, sürümün sorunsuz olduğunu söylemez.

**"Eşiği bir kez koymak yeter."** Eşik yayının büyüklüğüne ve kullanıcı sayısına göre yeniden düşünülür. Aynı sayıyı her aşamaya koymak, aşamalar büyüdükçe farklı bir anlam taşır; ham sayı eşiği büyük aşamada daha kolay aşar, oran eşiği ise büyüklükten bağımsızdır. Bu yüzden eşiğin hangi ölçüye bağlandığı, eşiğin kendisi kadar önemlidir. Eşik değiştiğinde önceki aşamaların sonuçları da yeniden yorumlanır.


#### Özet

- Bu dersin aşımı: azında. Altı aşamadan ikisi üç çökmelik eşiği aştı; yarısından azı aştığı için yanıt azında.
- Aşamalı yayın hatanın bedelini sınırlar; aşım görüldüğünde ekip yayını durdurur ya da geri alır.
- Aynı altı aşama beş, üç, bir ve sıfır çökmelik eşiklerde içinde, azında, çoğunda ve hepsinde çıktı.
- Durdurma kategori değil, aşımın sonucunda verilen karardır; kategori azında, karar dördüncü aşamada durdurmaktır.
- Aşama büyüklükleri büyüyünce ham çökme sayısı çoğunda, bin oturumdaki oran azında çıktı; ölçüm seçimi kategoriyi oynatır.
- Durdurma sonrası kalan örnek dörtten az olduğunda kategori yayını değil örnek toplamanın durduğunu anlatır: iki örnekte çoğunda, tek örnekte hepsinde çıktı.

#### Sonraki Adım

Yayın aşama aşama genişledi ve her aşama bir eşikle karşılaştırıldı. Ama yayına çıkmadan önce bir engel daha vardır: uygulama mağazasının inceleme kuralları. Sıradaki ders, `Mağaza İnceleme ve Politikalar`, uygulamanın beyan ettiği ama kullanmadığı izinleri sayar.

### Mağaza İnceleme ve Politikalar

Source: https://academia.sh/tr/kurslar/android-uretim/magaza-inceleme-ve-politikalar

Uygulamanın beyan ettiği ama kullanmadığı izinleri modül başına sayar ve bütçeyle karşılaştırır. Kullanılmayan izinler çıkarılınca aşımın nasıl kapandığını, kuralın ise bulgunun ağırlığını görmediğini gösterir.

Önceki ders yayını aşama aşama genişletti ve her aşamada çökme sayısına baktı. Ama bir derleme kullanıcıya ulaşmadan önce, uygulama mağazasının kendi denetiminden de geçer. Mağaza, yüklenen her sürümü politikalara göre inceler (store review): uygulamanın beyan ettiği izinler gerçekten kullanılıyor mu, kullanıcı verisi nasıl işleniyor, mağaza sayfasındaki açıklama uygulamanın yaptığıyla örtüşüyor mu.

İncelemenin ayrıntısını bir mağazadan ötekine ve bir zamandan ötekine değişen politikalar belirler; bu ders politikaların içeriğini ya da kaç tane olduğunu saymaz. Onun yerine ekibin kendi elinde olan bir şeyi ölçer: uygulamanın beyan edip kullanmadığı izinler. Kursun sorusu burada da aynıdır: **bu ölçüm bütçesini kaç örnekte aşıyor?** Ölçüm bir modüldeki, beyan edilen ama kullanılmayan izin sayısıdır (`policyFindings`), bütçe ekibin kabul ettiği sayıdır, her modül bir örnektir.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Overrun` sınırı kesin aşan örnekleri sayar ve yaygınlığı dört adla verir: içinde, azında, çoğunda, hepsinde. Kural kaç modülün aştığını görür; bir modülün bir mi yoksa üç kullanılmayan izin mi taşıdığı aşan her örnek için aynıdır.

#### İzin Beyanı

Bir uygulama, kullanıcının verisine ya da aygıtın bir donanımına erişmek istediğinde bu isteği bir bildirim dosyasında önceden beyan eder. Beyan, uygulamanın ne yapabileceğini kullanıcıya ve mağazaya söyleyen sözleşmedir.

```xml
<!-- Platform kodu: izin beyani -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
```

Bu parça bir yapılandırma dosyasıdır; çalıştırılmaz ve çıktı iddia etmiyoruz. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde bir izin bir addır; bir modül hem beyan ettiği izin kümesini hem de kodunda gerçekten kullandığı izin kümesini taşır. Bulgu, ikisinin farkıdır: beyan edilip kullanılmayan izin.

Bu bulgunun neden önemli olduğunu düşünmek kolaydır. Kullanılmayan bir izin kullanıcıya gereksiz bir istek gibi görünür ve incelemede açıklama gerektirir. Bu yüzden ekipler izinleri yalnız gerçekten kullandıkları kadar beyan etmeye çalışır.

Beyan iki ayrı okura söylenir ve ikisi farklı şeyi arar. Kullanıcı beyanı bir istek olarak görür: uygulama kameraya erişmek istiyor, buna izin verilsin mi? Mağaza beyanı bir tutarlılık sorusu olarak okur: beyan edilen şey uygulamanın açıklamasıyla ve davranışıyla örtüşüyor mu? Yukarıdaki iki satırlık parça bu yüzden yalnız bir yapılandırma değildir; ekibin kullanıcıya ve mağazaya verdiği bir sözdür. Söz gereğinden geniş verilirse kullanıcı gereksiz bir izin görür ve mağaza açıklama ister. Dar verilirse özellik çalışmaz. İkisinin arasındaki yol, beyan kümesiyle kullanım kümesini eşitlemektir ve bu dersin ölçümü bu eşitsizliği sayar.

#### Modeli Kurmak

Yardımcı dosya sekiz modülü, her birinin beyan ettiği ve kullandığı izinleri ve bulgu sayısını kurar. İzin adları düz sözcüklerdir; platformdaki gerçek adlar değil, derste kullanılan kısa etiketlerdir.

```kotlin
// PolicyModel.kt — beyan edilen ama kullanilmayan izinleri modul basina modeller
class AppModule(val label: String, val declared: Set<String>, val used: Set<String>)

fun unusedPermissions(module: AppModule): List<String> =
    (module.declared - module.used).sorted()

fun policyFindings(module: AppModule): Int = unusedPermissions(module).size

fun trimmed(module: AppModule): AppModule =
    AppModule(module.label, module.declared.intersect(module.used), module.used)

fun appModules(): List<AppModule> = listOf(
    AppModule("ana", setOf("konum", "kamera"), setOf("konum")),
    AppModule("giris", setOf("kisiler"), setOf("kisiler")),
    AppModule("odeme", setOf("kamera", "konum", "kisiler"), setOf("kamera")),
    AppModule("harita", setOf("konum", "depolama"), setOf("konum")),
    AppModule("profil", setOf("kamera", "depolama", "kisiler"), setOf("kamera", "depolama")),
    AppModule("arama", setOf("mikrofon"), setOf("mikrofon")),
    AppModule("yardim", setOf("konum", "takvim"), emptySet()),
    AppModule("ayarlar", setOf("depolama", "takvim"), setOf("takvim")),
)
```

#### Bulguların Ölçümü

Ekibin kuralı basittir: bir modül, kullanmadığı hiçbir izni beyan etmemeli. Bütçe sıfırdır: tek bir kullanılmayan izin bile aşımdır.

```kotlin
// Main.kt — her modulun kullanilmayan izinleri sayilir, butce sifir
fun main() {
    val modules = appModules()
    for (module in modules) {
        println("${module.label}: kullanilmayan=${unusedPermissions(module)}")
    }
    val samples = modules.map { policyFindings(it) }
    println("bulgu: $samples")
    Overrun("kullanilmayan-izin", Budget("bulgu", 0), samples).line()
}
```

```text
ana: kullanilmayan=[kamera]
giris: kullanilmayan=[]
odeme: kullanilmayan=[kisiler, konum]
harita: kullanilmayan=[depolama]
profil: kullanilmayan=[kisiler]
arama: kullanilmayan=[]
yardim: kullanilmayan=[konum, takvim]
ayarlar: kullanilmayan=[depolama]
bulgu: [1, 0, 2, 1, 1, 0, 2, 1]
kullanilmayan-izin: butce=bulgu<=0 | asan=6/8 -> cogunda
```

Altı modül, bütçe sıfırken aştı; yalnız `giris` ve `arama` temiz çıktı. Sekizin yarısından fazlası aştığı için yanıt **çoğunda**. Bu, dersin aşımıdır.

**DA3.** Model bir izni bir ad olarak sayar. Gerçekte bir izin yalnız bir modülden değil birden çok modülden kullanılabilir, bir izin başka bir izinle birlikte gelebilir ya da bir kitaplık bir izni uygulamanın haberi olmadan ekleyebilir. Model bu üç durumu kurmaz. Beyan ve kullanım kümeleri de elle yazılmış girdilerdir; gerçek bir uygulamada kullanım, kodun taranmasından çıkar.

#### Bütçe Bir Karardır

Aynı sekiz modülü üç ayrı bütçeyle ölçelim: iki, bir, sıfır kullanılmayan izin.

```kotlin
// Main.kt — ayni sekiz modul uc ayri butceyle uc ayri asim verir
fun main() {
    val samples = appModules().map { policyFindings(it) }
    val roomy = Overrun("iki-izin", Budget("bulgu", 2), samples)
    val normal = Overrun("bir-izin", Budget("bulgu", 1), samples)
    val strict = Overrun("sifir-izin", Budget("bulgu", 0), samples)
    listOf(roomy, normal, strict).forEach { it.line() }
    countOverruns(listOf(roomy, normal, strict))
}
```

```text
iki-izin: butce=bulgu<=2 | asan=0/8 -> icinde
bir-izin: butce=bulgu<=1 | asan=2/8 -> azinda
sifir-izin: butce=bulgu<=0 | asan=6/8 -> cogunda
icinde: 1
azinda: 1
cogunda: 1
hepsinde: 0
```

Bütçe ikiyken hiçbir modül aşmadı. Bire inince yalnız iki kullanılmayan izni olan `odeme` ve `yardim` aştı. Sıfırda altı modül aştı. Üç sonuç aynı ölçümden çıktı; birbirini çürütmez, yalnız bütçenin neyi kabul ettiğini gösterir. Bütçeyi sıfıra koymak sıkı bir ekip kuralıdır ve kural gevşetilmediği sürece yanıt hepsinde çıkamaz, çünkü iki modül hiç bulgu taşımaz.

#### Sayım Ağırlığı Görmez

Sayım iki bulguyu eşit sayar, oysa bulguların hepsi aynı ağırlıkta değildir. Kamera, konum, kişiler ve mikrofon izinleri kullanıcının daha hassas verisine uzanır; depolama ve takvim izinleri derste daha az hassas varsayılacaktır. Bu bir model varsayımıdır, platformun sınıflandırması değildir.

```kotlin
// Main.kt — sayim agirligi gormez, hassas izin sayisi ayri olculur
fun main() {
    val sensitive = setOf("kamera", "konum", "kisiler", "mikrofon")
    val modules = appModules()
    for (module in modules) {
        val unused = unusedPermissions(module)
        val heavy = unused.count { it in sensitive }
        println("${module.label}: bulgu=${unused.size} hassas=$heavy")
    }
    val samples = modules.map { policyFindings(it) }
    Overrun("kullanilmayan-izin", Budget("bulgu", 0), samples).line()
}
```

```text
ana: bulgu=1 hassas=1
giris: bulgu=0 hassas=0
odeme: bulgu=2 hassas=2
harita: bulgu=1 hassas=0
profil: bulgu=1 hassas=1
arama: bulgu=0 hassas=0
yardim: bulgu=2 hassas=1
ayarlar: bulgu=1 hassas=0
kullanilmayan-izin: butce=bulgu<=0 | asan=6/8 -> cogunda
```

`harita` ve `ayarlar` modülleri bir bulgu taşır ama hassas bulguları yoktur; `ana` ve `profil` de bir bulgu taşır ve bulgunun hassas olduğu görülür. Kural dördünü aynı sayar: dördü de aşan örnektir. Kategori **çoğunda**, ve bu kategori hangi modülün incelemede daha riskli olduğunu söylemez.

İncelemenin kararı bu sayımdan çıkmaz. Mağaza bir sürümü reddedebilir ya da düzeltme isteyebilir; bu karar kategoriye değil, incelemenin kendi değerlendirmesine bağlıdır. Model ret kararını kurmaz ve burada ret ya da kabul iddia etmiyoruz.

**DA4.** Hassaslık ayrımı bu derste varsayılmış bir kümedir; hangi iznin hassas sayılacağı gerçek bir mağaza politikasında ayrıca belirlenir ve zamanla değişir. Model bu kümeyi bir girdi olarak alır.

#### Sürümler Arası Fark

Beyan bir kez yazılıp kalmaz; her sürümde yeni bir izin eklenebilir. Yeni sürümde eklenen bir izin, incelemede gerekçesi sorulabilecek yeni bir beyandır. Bu yüzden ekip için asıl soru yalnız bugünkü fazlalık değil, bu sürümde ne eklendiğidir. Aşağıdaki ölçüm her modül için önceki sürümün beyanını bir girdi olarak alır ve iki sayım verir: eklenen izinlerin sayısı ve eklenen izinlerden kullanılmayanların sayısı. Bütçe ikisinde de sıfırdır.

```kotlin
// Main.kt — yeni surumde eklenen izinlerin kac tanesi kullanilmiyor
fun main() {
    val previous = mapOf(
        "ana" to setOf("konum"),
        "giris" to setOf("kisiler"),
        "odeme" to setOf("kamera", "konum"),
        "harita" to setOf("konum", "depolama"),
        "profil" to setOf("kamera", "depolama", "kisiler"),
        "arama" to emptySet<String>(),
        "yardim" to setOf("konum"),
        "ayarlar" to setOf("depolama", "takvim"),
    )
    val modules = appModules()
    val added = modules.map { it.declared - previous.getValue(it.label) }
    val addedUnused = modules.mapIndexed { index, module -> (added[index] - module.used).size }
    println("eklenen: ${added.map { it.size }}")
    println("eklenen ve kullanilmayan: $addedUnused")
    val budget = Budget("izin", 0)
    Overrun("eklenen", budget, added.map { it.size }).line()
    Overrun("eklenen-kullanilmayan", budget, addedUnused).line()
}
```

```text
eklenen: [1, 0, 1, 0, 0, 1, 1, 0]
eklenen ve kullanilmayan: [1, 0, 1, 0, 0, 0, 1, 0]
eklenen: butce=izin<=0 | asan=4/8 -> cogunda
eklenen-kullanilmayan: butce=izin<=0 | asan=3/8 -> azinda
```

DA5. Önceki sürümün beyanı elle yazılmış bir girdidir ve modül adlarının sürümler arasında değişmediği varsayılır. Gerçekte bir modül bölünebilir, birleşebilir ya da yeniden adlandırılabilir; model bu durumları kurmaz. Eklenen iznin yeni bir özellik için mi yoksa bir kitaplığın yan etkisi olarak mı geldiğini de ayırt etmez.

#### Sınırlayıcı Ölçüm

Kullanılmayan izinleri çıkaralım: her modülün beyanı yalnız kullandığı izinlere iner. Bu, bulgunun tam olarak kapandığı durumdur. Sınırlayıcı ölçüm bu yüzden dersin sorusuna verilebilecek en iyi yanıtı gösterir ve bütçenin ulaşılabilir olup olmadığını söyler.

```kotlin
// Main.kt — kullanilmayan izinler cikarilinca butun moduller butceye girer
fun main() {
    val before = appModules()
    val after = before.map { trimmed(it) }
    val budget = Budget("bulgu", 0)

    val beforeSamples = before.map { policyFindings(it) }
    val afterSamples = after.map { policyFindings(it) }
    Overrun("once", budget, beforeSamples).line()
    Overrun("sonra", budget, afterSamples).line()
    println("beyan once: ${before.sumOf { it.declared.size }}")
    println("beyan sonra: ${after.sumOf { it.declared.size }}")
}
```

```text
once: butce=bulgu<=0 | asan=6/8 -> cogunda
sonra: butce=bulgu<=0 | asan=0/8 -> icinde
beyan once: 16
beyan sonra: 8
```

Kullanılmayan izinler çıkarılınca aşım **çoğunda** kategorisinden **içinde** kategorisine indi; beyan edilen izin sayısı on altıdan sekize düştü. Bu sonuç bir sayımdır: sekiz izin uygulamanın kodunda hiç kullanılmıyordu ve çıkarılması işlevi değiştirmedi. Çıkarılan izinlerden birinin kodda başka bir yolla kullanıldığı ortaya çıkarsa, kullanım kümesi yanlıştı demektir. O zaman düzeltilmesi gereken beyan değil, kullanımın nasıl tespit edildiğidir.
Bu sonuç bütçenin ulaşılabilir olduğunu gösterir, ama ulaşmanın bedelini göstermez. Sekiz iznin çıkarılması işlevi değiştirmedi, ancak bir iznin gerçekten kullanılmadığını söylemek kullanımın nasıl tespit edildiğine bağlıdır. Model kullanım kümesini girdi olarak aldı; gerçek bir uygulamada bu küme koddan çıkarılır ve çıkarım dolaylı kullanımları, örneğin bir kitaplığın uygulama adına yaptığı çağrıları kaçırabilir. Bu yüzden sınırlayıcı ölçümün sonucu, çıkarmadan önce kullanımın bağımsız bir yolla doğrulanması koşuluyla okunur.

#### Kullanım Kümesi Nereden Gelir

Dersin ölçümü iki kümenin farkına dayanır, ama kümelerden yalnız biri bir dosyada yazılıdır. Beyan kümesi bildirim dosyasından okunur ve açıktır. Kullanım kümesi ise kodun hangi izin gerektiren çağrıları yaptığının sonucudur ve bir tarama ya da bir derleyici çıktısı ister. Bu iki kaynağın güvenilirliği aynı değildir: beyan kesindir, kullanım bir çıkarımdır. Fark bu asimetriden doğar. Kullanım kümesi eksik hesaplanırsa fazlalık olduğundan büyük görünür ve çıkarılan bir izin bir özelliği kırar. Kullanım kümesi fazla hesaplanırsa fazlalık olduğundan küçük görünür ve gereksiz bir izin kalır. İlk hata kullanıcıya, ikincisi incelemeye yansır.

Ekip bu yüzden iki yönü de ayrı ayrı sınar: her çıkarılan izin için o izni kullanan özelliğin gerçekten bulunmadığını, her kalan izin için de kullanan bir özelliğin bulunduğunu doğrular. Model bu doğrulamayı kurmaz; kullanım kümesini doğru kabul eder ve yalnız farkı sayar.

#### Yaygın Yanılgılar

**"Bulgu yoksa inceleme geçilir."** Geçilmez. Sayım yalnız beyan ile kullanım arasındaki farkı ölçer; inceleme başka politikaları da denetler.

**"Bulgu sayısı azsa risk azdır."** Sayı bulgunun ağırlığını göstermez. Bir hassas bulgu, birkaç hassas olmayan bulgudan daha önemli olabilir.

**"Yeni izin eklemek her zaman bir sorundur."** Sorun, eklenip kullanılmayan izindir. `arama` modülü bu sürümde yeni bir izin ekledi ve izin kodda kullanılıyordu; ilk sayımda aşım görünse de ikinci sayımda görünmedi.

**"Beyanı küçültmek özellikleri etkilemez."** Kullanım kümesi doğruysa etkilemez. Kullanım kümesi eksikse, yani bir izin kodda başka bir yolla kullanılıyorsa, beyanı küçültmek o özelliği kırar. Küçültmeden önce kullanım tespitinin güvenilir olduğunu doğrulamak gerekir.

**"Her izin çıkarılmalıdır."** Yalnız kullanılmayanlar çıkarılır. Kullanılan bir izni çıkarmak, o özelliği çalışmaz hâle getirir.

**"Hassas olmayan izin sorun değildir."** Hassaslık kümesi bu derste bir varsayımdı. Hassas sayılmayan bir izin de kullanılmıyorsa bütçeyi aşar ve incelemede açıklama ister. Hassaslık yalnız hangi bulgunun önce ele alınacağını sıralamaya yarar; bulgunun varlığını ortadan kaldırmaz.

**"Beyanı temiz tutmak tek seferlik bir iştir."** Her sürümde yeni bir özellik yeni bir izin getirebilir ve bir özellik kaldırıldığında izin beyanda kalabilir. Bu yüzden sayım bir sürüm kapısında, her yayından önce tekrar koşar; bir kez koşup kapanan bir denetim, bir sonraki sürümde yeniden fazlalık biriktirir.

**"Bütçeyi sıfır tutmak her zaman gerçekçidir."** Sıfır, bu derste ekibin seçtiği sıkı bir kuraldı. Üç bütçe ölçümü gösterdi ki aynı sekiz modül sıfırda çoğunda, birde azında, ikide içinde çıkar. Gerçekçi bütçe, ekibin hangi bulguyu inceleme öncesinde kapatabildiğine bağlıdır.

#### Özet

- Bu dersin aşımı: çoğunda. Sekiz modülden altısı, sıfırlık bütçede kullanılmayan izin taşıdı; yarısından fazlası aştığı için yanıt çoğunda.
- Mağaza incelemesi politikalara göre denetler; bu ders politikaların içeriğini değil, ekibin elindeki bulgu sayımını ölçtü.
- Aynı sekiz modül iki, bir ve sıfır bütçelerinde sırayla içinde, azında ve çoğunda çıktı.
- Kural dört bulguyu aynı sayar ama iki tanesi hassas izindi; sayım ağırlığı ve ret kararını göstermez.
- Önceki sürüme göre eklenen izinlerin kusuru eklenmesinde değil, kullanılmamasındadır; iki sayım bunu ayırır.
- Kullanılmayan izinler çıkarılınca beyan on altıdan sekize indi ve aşım içinde oldu.

#### Sonraki Adım

Uygulama incelemeden geçti, aşama aşama genişledi ve kullanıcılara ulaştı. Ancak yayın bitmedi; kullanıcılar uygulamayı gerçek koşullarda kullanmaya başlayınca asıl veri gelir. Sıradaki ders, `Yayın Sonrası İzleme`, yayından sonraki günlerin çökme sayısını bütçeyle karşılaştırır.

### Yayın Sonrası İzleme

Source: https://academia.sh/tr/kurslar/android-uretim/yayin-sonrasi-izleme

Yayından sonraki izleme penceresinde her günün çökme sayısını hata eşiğiyle karşılaştırır. İlk günlerdeki sıçramanın aşımı azında bıraktığını, aynı sayılar haftalık toplanınca örnek sayısının düştüğünü ve kategorinin değiştiğini ölçer; ardından kursun 19 dersini tek tabloda toplar.

Önceki ders uygulamayı incelemeden geçirdi ve kullanıcılara ulaştırdı; `Yayın Sonrası İzleme` dersi yayından sonraki günlerin çökme sayısını bütçeyle karşılaştırır. Yayın bir kapıdan geçmek değil, bir pencerenin açılmasıdır: derleme gerçek kullanıcıların elindeyken ekip, **izleme penceresi** (monitoring window) denen bir süre boyunca verinin gelmesini bekler. Bu ders pencerenin her gününü bir ölçüm olarak ele alır.

Kursun sorusu burada da aynıdır: **bu ölçüm bütçesini kaç örnekte aşıyor?** Ölçüm bir günün çökme sayısıdır (`crashCount`), bütçe ekibin bir günde kabul ettiği hata eşiğidir, pencerenin her günü bir örnektir.

```kotlin
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Overrun` sınırı kesin aşan örnekleri sayar ve yaygınlığı dört adla verir: içinde, azında, çoğunda, hepsinde. Kural kaç günün aştığını görür; bir günün bir mi yoksa on çökme fazla mı taşıdığı aşan her gün için aynıdır.

#### Ayrım: Rapor mu, Eşik mi

Kursun önceki bölümünde Analitik ve Çökme Raporlama dersi, çökme raporunun alıcıya kaç kopya olarak ulaştığını saydı. O ders raporun teslimini ölçtü. Bu ders teslimi ölçmez: raporların geldiğini varsayar ve günler boyunca eşiği kaç kez aştığını sayar. Biri bir olayın yolunu, öteki bir sayının bütçeyle ilişkisini inceler.

#### Çökmeyi Gösteren Platform Parçası

Bir çökme raporu, uygulamanın beklenmedik bir hatayla durduğu anda üretilen kayıttır. Kayıt, yanıt vermeyen uygulama (ANR) durumunu da ayrı bir türde taşıyabilir. Ekipler izleme sırasında genellikle çökmesiz oturum oranına bakar; bu derste oran değil sayı ölçüleceği için yalnız ham sayıyı kullanıyoruz.

```kotlin
// Platform kodu: CrashReporting
val reporter = crashReportingFramework.instance()
reporter.setCustomKey("release_window_day", dayIndex)
reporter.recordException(error)
```

Bu parça bir platform çağrısıdır; çıktı iddia etmiyoruz. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde bir oturum ya `cokme` ya `saglam` biter; çerçevenin topladığı ek bilgi, yığın ya da ANR türü modelin dışında kalır.

#### İzleme Penceresini Modellemek

Yardımcı dosya pencerenin her gününü on oturumdan kurar. Günler aynı büyüklükte olduğundan çökme sayıları doğrudan karşılaştırılabilir.

```kotlin
// MonitoringModel.kt — izleme penceresini ve gun basina cokme sayisini modeller
class ReleaseDay(val label: String, val outcomes: List<String>)

fun crashCount(day: ReleaseDay): Int = day.outcomes.count { it == "cokme" }

fun releaseWindow(crashes: List<Int>): List<ReleaseDay> =
    crashes.mapIndexed { index, crashed ->
        ReleaseDay("gun-${index + 1}", List(10) { if (it < crashed) "cokme" else "saglam" })
    }

fun launchWindow(): List<Int> = listOf(6, 5, 4, 2, 2, 1, 1, 2, 1, 1, 2, 1, 1, 1)

fun weeklyTotals(counts: List<Int>): List<Int> = counts.chunked(7).map { it.sum() }
```

`launchWindow` ilk günleri yüksek, sonrasını alçak tutar. Yayın sonrası ilk günlerde çökmeler kümelenir: yayının ilk saatlerinde en hevesli kullanıcılar, en çok farklı yolu deneyen oturumlar gelir.

#### Günlük Ölçüm

Hata eşiğini günde üç çökme olarak koyalım.

```kotlin
// Main.kt — izleme penceresinin her gunu olculur, hata esigi gunde uc cokme
fun main() {
    val days = releaseWindow(launchWindow())
    val samples = days.map { crashCount(it) }
    println("cokme: $samples")
    Overrun("gunluk-cokme", Budget("cokme", 3), samples).line()
}
```

```text
cokme: [6, 5, 4, 2, 2, 1, 1, 2, 1, 1, 2, 1, 1, 1]
gunluk-cokme: butce=cokme<=3 | asan=3/14 -> azinda
```

Ondört günün üçü eşiği aştı: ilk üç gün sırasıyla altı, beş ve dört çökme taşıdı. Yarıdan azı aştığı için yanıt **azında**. Bu, dersin aşımıdır.

**DA6.** Model her günü aynı büyüklükte on oturumdan kurar ve çökmeyi oturum başına bir sonuç sayar. Gerçekte günlük oturum sayısı yayın büyüdükçe artar ve bir kullanıcı aynı çökmeyi tekrarlayabilir; model ikisini de görmez. `launchWindow` elle yazılmış bir girdidir; günlerin çökme sayıları `days.map { crashCount(it) }` ile modelden çıkar, ama sayıların kendisi gerçek bir izlemeden gelmez.

#### Sıçramayı Ayırmak

İlk üç günün diğerlerinden farkı kuralda görünmez; kural yalnız üç günün aştığını söyler. Pencereyi ikiye bölüp her parçayı ayrı ölçersek sıçrama görünür olur.

```kotlin
// Main.kt — pencere ilk uc gun ve sonraki gunler diye ikiye bolunup olculur
fun main() {
    val samples = releaseWindow(launchWindow()).map { crashCount(it) }
    val spike = Overrun("ilk-uc-gun", Budget("cokme", 3), samples.take(3))
    val rest = Overrun("sonraki-gunler", Budget("cokme", 3), samples.drop(3))
    spike.line()
    rest.line()
    countOverruns(listOf(spike, rest))
}
```

```text
ilk-uc-gun: butce=cokme<=3 | asan=3/3 -> hepsinde
sonraki-gunler: butce=cokme<=3 | asan=0/11 -> icinde
icinde: 1
azinda: 0
cogunda: 0
hepsinde: 1
```

İlk üç günde her gün aştı, sonraki onbir günde hiçbiri aşmadı. Bütün pencere azında çıktı çünkü onbir sakin gün üç sıçrama gününü seyreltti. Bu bir sorun değil, ölçünün özelliğidir: pencere uzadıkça aşım değeri sıçramadan çok pencerenin uzunluğuna bağlanır.

**DA7.** Üç günlük bölme, sıçramanın nerede bittiğini baştan bilen bir ekibin bölmesidir. Gerçek izlemede sıçramanın sonu bilinmez; bölme noktasını seçmek ekibin kararıdır ve model bu kararı vermez.

#### Sınırlayıcı Ölçüm

Aynı ondört gün, günlük değil haftalık toplanırsa ne olur? Ekipler çoğu zaman tek bir sayıyla rapor verir, o sayı da genellikle haftalıktır.

```kotlin
// Main.kt — ayni cokme sayilari haftalik toplanir, iki ayri butceyle olculur
fun main() {
    val samples = releaseWindow(launchWindow()).map { crashCount(it) }
    val weekly = weeklyTotals(samples)
    println("haftalik: $weekly")
    val same = Overrun("haftalik-ayni-butce", Budget("cokme", 3), weekly)
    val scaled = Overrun("haftalik-yedi-kati", Budget("cokme", 21), weekly)
    same.line()
    scaled.line()
}
```

```text
haftalik: [21, 9]
haftalik-ayni-butce: butce=cokme<=3 | asan=2/2 -> hepsinde
haftalik-yedi-kati: butce=cokme<=21 | asan=0/2 -> icinde
```

Aynı ondört gün iki haftaya toplandı: ilk hafta 21, ikinci hafta 9 çökme. Günlük bütçe olduğu gibi bırakılınca iki hafta da aştı ve yanıt **hepsinde** oldu. Bütçe yedi katına, yani 21'e çıkarılınca 21 sınıra eşit olduğu için ilk hafta da aşım sayılmadı ve yanıt **içinde** oldu. Günlük kesimin **azında** yanıtı haftalık ölçümlerin hiçbirinde çıkmadı. İki örnekle azında kurulamaz: `over * 2 < total` koşulu 2 örnekte ancak sıfır aşımla sağlanır, ve sıfır aşım içinde demektir.

**DA8.** Haftalık toplamın bütçesini günlük bütçenin yedi katı almak, günlerin eşit dağıldığını varsayar. Yayın sonrası çökmeler eşit dağılmaz; bu yüzden yedi katı bütçe ilk haftanın sıçramasını gizler. Bütçeyi başka bir sayıya çekmek başka bir aşım verirdi.

Bu ölçüm bir kuralı değil, bir kesme biçimini sınar: ders, örneğin nasıl kesildiğinin aşımı belirlediğini söyler. Günlük kesimde ondört örnek vardı ve aşım azında çıktı; haftalık kesimde iki örnek kaldı ve aşım örnek sayısının kendisine bağlandı. Kursun örnek sayısı en az dört olmalı ilkesi bu yüzden vardır.

#### Bütçe Bir Karardır

Günde üç çökme ekibin seçtiği bir sayıdır. Daha sıkı bir eşik daha çok günü aşım sayar; daha gevşek bir eşik ilk günlerin sıçramasını da içeri alır. Sayıyı seçmek, hangi yayını durduracağınıza karar vermektir. Model bu kararı vermez; yalnız seçilen sayının kaç günü aştırdığını söyler.

#### Yaygın Yanılgılar

**"Pencere uzadıkça veri daha doğru olur."** Pencere uzadıkça sakin günler çoğalır ve sıçrama seyrelir; aşım değeri değişir, ama bu değişim yayının iyileşmesi değil, örneğin kesilişidir.

**"Haftalık toplam günlükten iyi bir özettir."** Haftalık toplam sayıyı korur ama örnek sayısını yediye böler. İki örnekle dört kategoriden biri, azında, zaten kurulamaz.

**"Azında olan bir yayın sağlıklıdır."** Azında, aşan günlerin yarıdan az olduğunu söyler; hangi günlerin aştığını söylemez. Bu derste aşan günler ilk üç gündü ve yayının en çok kullanıcıya ulaştığı anlardı.

#### Kapanış Tablosu

Kurs 19 derste aynı soruyu sordu. Her dersin aşımı o dersin `## Özet` bölümündeki "Bu dersin aşımı:" satırıdır; ölçülen iş birimi ve sınırlayıcı ölçüm kaynak dersin kendisindedir.

| Ders | Ölçülen iş birimi | Aşım | Sınırlayıcı ölçüm |
|---|---|---|---|
| Birim Testleri | girdi doğrulama adımı | içinde | bütçe 5 adıma inince azında |
| Sahteleme | işlemdeki ağ çağrısı | hepsinde | güdük önbellekle çağrı sıfır, içinde |
| Aygıt Üstü Testler | test başına iş | hepsinde | kurulum paylaşılınca azında |
| Arayüz Testleri | düğümü bulma adımı | çoğunda | test etiketiyle tek adım, içinde |
| Test Kararlılığı | hazır olma beklemesi | azında | koşula bağlı yoklamayla içinde |
| Statik Çözümleme | modül başına ihlal | çoğunda | temel değer dosyasıyla azında |
| Başlangıç Süresi | ilk kareye kadar iş | azında | bağımlılığı ertelemekle içinde |
| Çizim Akıcılığı | karede yeniden oluşan öğe | hepsinde | kararlı anahtarla azında |
| Bellek Yönetimi | tutulan nesne | çoğunda | dinleyiciyi yaşam döngüsüne bağlayınca içinde |
| Ağ ve Pil Profili | tur başına istek | içinde | tek tek gönderimde azında |
| Karşılaştırmalı Ölçüm | yineleme başına iş | azında | ısınma atılınca yeni sürüm hepsinde |
| Kod Küçültme ve Karartma | modül başına kalan sınıf | içinde | geniş koruma kuralıyla hepsinde |
| Uygulama Paketi Biçimleri | yapılandırmaya inen dosya | hepsinde | bölünmüş pakette içinde |
| Sürüm Yönetimi | aygıtın sürümden uzaklığı | çoğunda | yalnız sürüm adı değişince aynı kalır |
| Sürekli Tümleştirme ve Teslimat | değişiklik başına koşan iş | azında | süzgeçsiz hatta hepsinde |
| Test Kanalları | üretime kadar adım | içinde | geri çevrilen derleme baştan sayılınca azında |
| Aşamalı Yayın | aşama başına çökme | azında | durdurmayla örnek azalınca hepsinde |
| Mağaza İnceleme ve Politikalar | modül başına kullanılmayan izin | çoğunda | izinler çıkarılınca içinde |
| Yayın Sonrası İzleme | günlük çökme sayısı | azında | haftalık toplamayla örnek sayısı düşer |

19 dersin 4'ü içinde, 6'sı azında, 5'i çoğunda, 4'ü hepsinde kapanır. Açılış dersindeki çekirdek başka bir dağılım verdi: dört ölçüm 1 içinde, 1 azında, 1 çoğunda, 1 hepsinde çıktı. İki dağılım aynı şeyi saymaz; çekirdek dört ölçümü, tablo on dokuz dersi sayar.

#### Özet

- Bu dersin aşımı: azında. Ondört günün üçü, ilk üç gün, günde üç çökme eşiğini aştı.
- İlk günlerin sıçraması bütün pencereye yayılınca seyrelir: ilk üç gün hepsinde, sonraki onbir gün içinde çıktı.
- Aynı sayılar haftalık toplanınca örnek sayısı ondörtten ikiye düşer ve kategori değişir. Örneğin nasıl kesildiği aşımı belirler.
- Bu ders çökme raporunun teslimini değil, raporların günler boyunca eşiği kaç kez aştığını sayar.
- Çökme raporlama çerçevesi platform kodudur ve bu derste ölçülmez.

#### Kurs Kapanışı

Kurs test, başarım ve dağıtım boyunca tek bir soruyu sordu: bu ölçüm bütçesini kaç örnekte aşıyor? Tablo, aynı soruya dört ayrı cevabın hepsinin gerçekten verildiğini gösterir. Sıradaki müfredat M32, çok platformlu mobil geliştirme konu alanına geçer.

