Files
imajviewer/docs/rapor-kod-inceleme-performans.md
T
Alhan 72c35a8a26
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
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)
2026-08-14 21:54:34 +03:00

140 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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