- _imageSize reset on image switch (wrong swap threshold on nav) - downsample caps both dims (tall images no longer full-res decode) - single _readImageSize instead of leaking loadingBuilder listeners - evict old downsample provider on full-res swap - remove imageCache.clear() that dropped live image (gallery flash)
140 lines
6.2 KiB
Markdown
140 lines
6.2 KiB
Markdown
# 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
|