# Kod İnceleme + Performans Analizi Tarih: 2026-08-14 Kapsam: Tüm `lib/` (13 dosya, 1799 satır) Bağlam: Downsampling + full-res swap + RAM leak fix (v1.2.0) sonrası genel inceleme --- ## 1. Tespit Edilen Hatalar ### HATA-1 (Önemli): `_imageSize` resim değişiminde sıfırlanmıyor `lib/widgets/image_canvas.dart` `_imageSize`, `loadingBuilder` içinde `_imageSize.isEmpty` koşuluyla **yalnızca bir kez** dolduruluyor. Ama `_resetView()` ve `didUpdateWidget()` bu alanı **sıfırlamıyor**. Sonuç: ikinci resme geçince `_imageSize` eski resmin boyutunu tutar, yeni resmin boyutu hiç okunmaz. `_checkFullResSwap()` eski boyutla stretch hesabı yapar → swap yanlış zamanda tetiklenir (ya hiç, ya da gereksiz erken). Örnek: 2000px bir resimden 12000px panoramaya geçince, stretch hesabı hâlâ 2000px üzerinden yapılır; swap ya geç tetiklenir ya da downsample 2048px'te takılı kalır (zoom'da kalite düşer). Ters yönde ise gereksiz erken full-res yükleme olur. Düzeltme: `_resetView()` içine `_imageSize = Size.zero;` ekle. ### HATA-2 (Önemli): Downsample yalnızca genişliğe uygulanıyor `_buildImage()` içinde `ResizeImage.resizeIfNeeded(2048, null, ...)` çağrısı `cacheHeight = null` geçiyor. Bu, **yalnızca genişlik** kısıtı demek. Sonuç: Uzun dikey resimler (ör. 800×20000 ekran görüntüsü) genişliği 2048'in altında olduğu için **hiç küçültülmez**, tam çözünürlükte (20000px) decode edilir. Tek resim yüzlerce MB RAM yer — downsampling'in amacı boşa çıkar. Aynı sorun panoramik 20000×800 resimlerde genişlik kısıtıyla zaten yakalanır, ama dikeyde kaçar. Düzeltme: `ResizeImage.resizeIfNeeded(2048, 2048, ...)` (iki boyut da sınırla). Flutter codec aspect oranını koruyarak sığdırır (fit). ### HATA-3 (Performans): `loadingBuilder` içinde listener sızıntısı `loadingBuilder` her frame'de çağrılabilir. `_imageSize.isEmpty` olduğu sürece her çağrıda `provider.resolve()` + `addListener()` yapılıyor; listener **hiç remove edilmiyor**. `_imageSize` bir kez dolunca duruyor ama dolana kadar (ya da HATA-1 yüzünden hiç dolmazsa) her rebuild'de yeni listener birikir. Ayrıca `resolve()` ikinci bir decode stream'i açar — `Image` widget'ı zaten kendi decode'ini yaparken boyut öğrenmek için ikinci kez çözmek gereksiz iş. Düzeltme: `_imageSize`'i `loadingBuilder` yerine `initState`/`didUpdateWidget`'ta tek sefer `resolve()` ile oku ve `ImageStreamListener`'ı `ImageStream.removeListener` ile kaldır. Ya da `Image` widget'ının `ImageInfo`'sini `imageStream` üzerinden tek listener ile dinle. ### HATA-4 (Küçük): Full-res swap'ta eski downsample provider evict edilmiyor `_checkFullResSwap()` `_cachedProvider = _cachedFileImage` yapıyor ama eski `ResizeImage` provider'ı `ImageCache`'ten evict etmiyor. Downsamlı `ui.Image` (~16MB) cache'te gereksiz kalır. `maximumSizeBytes = 256MB` altında LRU temizler ama tamamen gereksiz bir tutma. ### HATA-5 (Yorum yanlışlığı): `imageCache.clear()` canlı görseli de siler `image_manager.dart:73` — `PaintingBinding.instance.imageCache.clear()` tüm cache'i boşaltır, **ekrandaki mevcut görsel de dahil**. Üstündeki yorum "canlı görsel korunur" diyor ama bu doğru değil. `clear()` o an render edilen image'ı da atar → galeri kurulumunda kısa bir flaş/boşluk riski. Düzeltme: Yalnızca eski klasöre ait dosyaları `evict` et (tek tek) ya da bu satırı kaldırıp `didUpdateWidget` evict mantığına bırak. --- ## 2. RAM Modeli (mevcut davranış) - Downsample 2048px → ~16MB/resim (RGBA 2048×2048×4) - Full-res swap sonrası → 100MB+ (büyük resimlerde) - `ImageCache`: `maximumSize = 16` entry, `maximumSizeBytes = 256MB` **Kullanıcının "50 resim = 5GB" endişesi gerçekleşmez.** `maximumSizeBytes = 256MB` LRU evict ile toplamı ~256MB–1.6GB arasında sınırlar: - Tüm resimler downsample ise: 16 entry × ~16MB ≈ 256MB (tam limit) - Full-res resimler varsa: 2–3 tane tutulur, gerisi LRU ile atılır Yani ölçülen "her resim ~100MB, artmıyor" davranışı doğru ve beklenen. **Kalan risk:** `maximumSize = 16` entry limiti, `maximumSizeBytes`'tan bağımsız işler. Full-res ağırlıklı kullanımda 16 × 100MB = 1.6GB'a kadar tutulabilir. `maximumSizeBytes`'ı düşürmek (ör. 128MB) ya da entry limitini küçültmek bu tavanı düşürür. --- ## 3. Performans İyileştirme Önerileri ### ÖN-1: `dir.listSync()` → async/isolate `image_manager.dart:43` — klasör `listSync()` ile senkron taranıyor. Büyük klasörde (10k+ dosya) UI thread bloklanır, pencere donar. `dir.list()` (async) ya da `Isolate.run` ile arka planda tarama önerilir. Sonuç `notifyListeners` ile gelir. ### ÖN-2: `_printImage` tam dosyayı belleğe alıyor `viewer_screen.dart:173` — `File(path).readAsBytes()` tüm dosyayı RAM'e yükler. 12000px resimde print sırasında +200MB spike. Nadir işlem ama downsample edilmiş kopya üzerinden print etmek düşünülebilir. ### ÖN-3: `_buildImage` içindeki `_imageKey` + `ResizeImage` memo cache HATA-1 çözülünce `_imageSize` doğru çalışır; ayrıca `_checkFullResSwap`'ın `setState` çağrısı scroll handler içinde — bu zaten `_markRenderDirty` ile çift rebuild üretebilir. `_checkFullResSwap`'ın `setState` yerine `_markRenderDirty` kullanması yeterli (ValueListenableBuilder zaten tick'i dinliyor). ### ÖN-4: Warmup katmanı her açılışta `_startWarmup` shader derlemesini her resim açılışında yapıyor; uygulama ömründe bir kez yapılması yeterli olabilir (global static flag). Küçük kazanç. --- ## 4. Doğrulama Matrisi | Madde | Durum | |---|---| | `flutter analyze` | 0 error (mevcut info/deprecated uyarıları hariç) | | Release build | ✓ | | RAM stabilitesi (kullanıcı testi) | ✓ "her resim ~100MB, artmıyor" | | HATA-1 (`_imageSize` reset) | ✅ düzeltildi | | HATA-2 (dikey downsample) | ✅ düzeltildi | | HATA-3 (listener sızıntısı) | ✅ düzeltildi | | HATA-4 (eski provider evict) | ✅ düzeltildi | | HATA-5 (`clear()` canlı görsel) | ✅ düzeltildi | --- ## 5. Öncelik Sırası 1. **HATA-1 + HATA-2** — doğruluk + RAM (uzun dikey resimler hâlâ patlayabilir) 2. **HATA-3** — listener sızıntısı (uzun oturumlarda) 3. **ÖN-1** — büyük klasörde UI donması 4. HATA-4, HATA-5 — kozmetik/ince ayar