fix: image_canvas correctness + RAM fixes (v1.3.0)
- _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)
This commit is contained in:
139
docs/rapor-kod-inceleme-performans.md
Normal file
139
docs/rapor-kod-inceleme-performans.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user