diff --git a/docs/DEVICE_VALIDATION_LEDGER.md b/docs/DEVICE_VALIDATION_LEDGER.md index 6d5548c5..22aacebc 100644 --- a/docs/DEVICE_VALIDATION_LEDGER.md +++ b/docs/DEVICE_VALIDATION_LEDGER.md @@ -17,6 +17,7 @@ | # | Özellik | Nerede | Kabul ölçütü (cihazda ne gözlemlenmeli) | Eklendi | |---|---------|--------|------------------------------------------|---------| +| 47 | **🔴 Device Validation Pending — VisionOverlay AR-kapalı idle loop gate** — GÖREV 2 kök-neden araştırmasının (docs-local/device-runs/20260712T000500-map-idle-root-cause) bulduğu **birincil** idle-CPU kaynağını kapatır. `VisionOverlay.renderAR` (AR kamera/şerit katmanı) 60 fps rAF döngüsü, **AR/kamera KAPALIYKEN bile** (`!frame \|\| !isHybrid \|\| confidenceLevel==='off'`) `requestAnimationFrame(renderAR)` ile KOŞULSUZ yeniden planlanıyordu → VisionOverlay FullMapView'de koşulsuz mount olduğundan, harita açıkken (çizim yapmasa da) her kare `getContext('2d',{willReadFrequently:true})` (CPU-backed canvas) + `getBoundingClientRect` (layout) + `clearRect` yapıp ana thread'i ve compositor'ı boşa yüküyordu. **KANIT (gerçek cihaz, DevTools + /proc, A/B):** rAF çağrılarının %84'ü VisionOverlay (~138/sn); VisionOverlay rAF canlı düşürülünce (settled idle harita) **process 35%→20%, main-thread 11%→6%, gfx 5.2→0.3 fps** — Ayarlar tabanına + ~0-2fps hedefine ulaştı. FullMapView tick (PR #61) zaten settled'da 0/sn (idle-gate çalışıyor); sorun ayrı bileşendeydi. **FIX (tek dosya, 1 satır davranış):** off-branch'te `requestAnimationFrame(renderAR)` yerine `rafRef.current = null` → döngü DURUR. Yeniden başlatma OTOMATİK: renderAR deps'i (isHybrid/confidenceLevel/vision.frame/currentLat/currentLon) değişince renderAR yeniden yaratılır → mevcut `useEffect([renderAR])` döngüyü tekrar başlatır. **Aktif AR çizim yolu (satır 623 reschedule) DEĞİŞMEZ** → AR/navigasyon işlevi korunur. Aynı "reschedule etmeden dur" deseni satır ~550 (`if(!canvas) return;`) zaten kullanıyor. Zero-leak: unmount cleanup + rafRef=null. YENİ timer/rAF/bağımlılık YOK. Kapsam-dışı dokunulmadı: FullMapView PR #61, GPS PR #62, native OBD/CAN, SQL, Platform Kernel #55. | `components/map/VisionOverlay.tsx` (renderAR off-branch), `__tests__/visionOverlayIdleLoopGate.test.ts` (yeni, 5 kaynak-kilidi test — bu repo SSR render kullandığından rAF davranışsal testi mümkün değil, kilit off-branch'e requestAnimationFrame geri konmasını engeller) | Gerçek Xiaomi cihazda (önce/sonra, tam ekran harita idle, park, dokunma yok, `/proc` + DevTools rAF attribution — **screencap ölçüm penceresinde YOK**): (1) VisionOverlay rAF idle'da **~0/sn** (önce ~60-138/sn); (2) idle harita process CPU **≤ ~20-22%** (Ayarlar tabanına yakın, önce ~35%); (3) gfx idle kare **≤ ~2 fps** (önce ~5.2); (4) **AR/HYBRID navigasyon açılınca** şerit/ok/tabela çizimi ÇALIŞIYOR (döngü yeniden başlıyor); (5) background/cold-warm start/30dk soak regresyonsuz, crash/ANR 0; (6) **Mali-400 (K24)**: CPU-canvas maliyeti orada oransal daha ağır → kazanç muhtemelen daha büyük ama ölçümsüz "çalışır" denmez. Test edilmeden 🟢 yapma. | 2026-07-12 | | 43 | **🔴 Device Validation Pending — Compass Foreground Demand Gating** — PR #42'nin (Ledger #42) bilinçli bıraktığı "Compass Settings ekranında KAPATILMADI" sınırlamasını KAPATIR. Önce `gpsService._startCompassListener()` `startGPSTracking()` ile KOŞULSUZ açılıyor, uygulama ön plandayken (Ayarlar/profil/medya/haritasız ekran dahil) HİÇ kapanmıyordu → cihaz QA'sında boşta ana ekranda `rot_vec`+`game_rotvec` 60 Hz açık. **BU PR:** küçük, ref-count'lu, owner-bazlı **compass demand contract** (`compassDemand.ts`) — `acquireCompassDemand/releaseCompassDemand/hasCompassDemand/getCompassDemandCount` + `subscribeCompassDemand` (0↔1 geçişlerinde bildirir). gpsService bu talebi dinler: **ilk acquire → gate compass aboneliği AÇILIR; son release → KAPANIR** (yalnız GPS tracking açıkken). **Tüketiciler:** `FullMapView` mount'ta `map:full` acquire / unmount'ta release (heading-up harita gerçekten heading kullanır); `MiniMapWidget` YALNIZ kendi mevcut `isDriving` histerezisinde (giriş >5 km/h, çıkış <3 km/h) `map:mini` acquire — park/kuzey-yukarı statikte talep YOK (yeni eşik eklenmedi). **KORUNAN:** GPS konum takibi (talep yokken watch aynen sürer, yalnız compass kapanır), blend/smoothing (`computeBlendedHeading`/`applyCompassSmoothing`/`COMPASS_THROTTLE_MS`), izin ve fallback yolu, background/foreground gate (visibility) davranışı — HİÇBİRİ değişmez. **YALIN:** owner bounded(32), duplicate acquire yok, release idempotent, timer/polling YOK, import yan etkisiz, gate'in sahibi değil, zero-leak (stopGPSTracking demand unsub'ı bırakır; MiniMap unmount cleanup'ı release eder). ⚠️ **Native samplingPeriod düşüşü İDDİA EDİLMEZ** (Chromium DeviceOrientation 60 Hz sabit); Generic Sensor API EKLENMEZ. Kapsam-dışı dokunulmadı: blackBox, smartDriving, FullMapView PR #61, GPS PR #62, Platform Kernel PR #55, SystemBoot, OBD/CAN, SQL. | `platform/gps/compassDemand.ts` (yeni), `platform/gpsService.ts` (compass talep-güdümlü), `components/map/FullMapView.tsx` (acquire/release), `components/map/MiniMapWidget.tsx` (isDriving'e bağlı acquire/release), `__tests__/compassForegroundDemand.test.ts` (yeni, 29 test), `__tests__/orientationDemandGating.test.ts` (PR #58 1/3/5/6/7 kilitleri yeni talep-güdümlü davranışa güncellendi) | Gerçek Xiaomi cihazda (önce/sonra, `dumpsys sensorservice` + idle CPU/`SensorsHandlerThread`): (1) **Ayarlar/haritasız ekranda** uygulama adına `rot_vec`/`game_rotvec` (rotation-vector/game-rotation) sensörleri AÇIK OLMAMASI; (2) harita/navigasyon açılınca compass sensörlerinin GERİ GELMESİ; (3) heading/pusula yönünün DOĞRU olması; (4) harita kapanınca compass sensörlerinin SÖKÜLMESİ; (5) background davranışının BOZULMAMASI; (6) duplicate listener OLMAMASI; (7) Xiaomi'de idle CPU ve `SensorsHandlerThread`'in önce/sonra ölçülmesi (düşüş beklenir); (8) Mali-400'de regresyon yok. ⚠️ **Mali-400 cihaz doğrulaması bekliyor.** Test edilmeden 🟢 yapma. | 2026-07-11 | | 42 | **🔴 Device Validation Pending — Orientation Sensor Demand Gating (PR 3/3)** — İki always-on tüketiciyi (gpsService compass + smartDrivingEngine accelerometer) talep-güdümlü hale getirir. **ACCEL (gerçek foreground demand-gate):** `_accelMagnitude` YALNIZ `detectDrivingMode` Kademe 4'te (OBD+GPS+decayed hepsi yok/stale) okunuyor → yardımcı son-çare. `_evaluateAccelDemand()` yalnız TAZE güvenilir hız kaynağı YOKKEN gate'ten `subscribeMotion` ile abone olur; taze kaynak (OBD **veya** GPS — `recordSpeed` ile beslenen `_lastSpeedEstimate`, yaş>DECAY_MAX_SEC=stale) varken release. `attachAccelerometer` fail-safe (kaynak yoksa acquire), `recordSpeed` her çağrıda re-eval, `detachAccelerometer` release. İvmeölçer TEK BAŞINA safety-critical/kesin hareket üretmez (yalnız Kademe 4 ipucu). Magic number YOK (DECAY_MAX_SEC mevcut sabit). **COMPASS (gpsService):** `_startCompassListener` ham `window.addEventListener` → gate `subscribeOrientationAbsolute/subscribeOrientation`; mevcut JS throttle (COMPASS_THROTTLE_MS, mevcut) + heading/smoothing/blend davranışı KORUNUR. Kazanım: **background auto-pause** (gate visibility hidden→fiziksel listener söker) + dedup. **YALIN:** yalnız 2 dosya değişti; yeni timer/rAF/polling YOK, Generic Sensor API YOK, yeni JS throttle YOK, yeni bağımlılık yok, import yan etkisiz; gate'in sahibi değil. ⚠️ **DÜRÜST SINIRLAMALAR:** (1) **Compass Settings ekranında KAPATILMADI** — foreground Settings-gating "harita ekranda mı" tüketici sinyali gerektirir (global görünüm store'u yok, MainLayout lokal `drawer` state'i; tüketiciye dokunmak KAPSAM DIŞI). Compass foreground davranışı bilinçli KORUNDU (map-heading-while-not-navigating bozulmasın). (2) **Accel mid-drive kaynak kaybı** (fresh sonra stale) re-acquire'ı sonraki `recordSpeed`/attach-döngüsünde olur (timer yok) — accel non-critical fallback + decay(20s)+Kademe5 kapsar. ⚠️ **Bu PR native samplingPeriod düşüşü İDDİA ETMEZ.** | `platform/gpsService.ts` (compass→gate), `platform/smartDrivingEngine.ts` (accel→gate+demand), `__tests__/orientationDemandGating.test.ts` (yeni, 26 test), `__tests__/orientationGateConsumerMigration.test.ts` (PR2 #21/#22 scope-kilidi yeni davranışa güncellendi) | Gerçek Xiaomi cihazda (önce/sonra): (1) sürüş sırasında (GPS/OBD taze) `dumpsys sensorservice` altında smartDriving accel `devicemotion` kaydı KALMAMASI; (2) GPS fix yok + OBD yok senaryosunda accel fallback'in ÇALIŞMASI (mod tespiti sürüyor); (3) background'da (app hidden) compass+accel fiziksel listener'ın SÖKÜLMESİ; (4) foreground dönüşünde geri bağlanması; (5) heading/pusula/navigation bearing DEĞİŞMEMESİ; (6) duplicate listener/cleanup leak OLMAMASI; (7) idle CPU (SensorsHandlerThread+CrRendererMain) sürüş+background senaryolarında düşüşü; (8) BASIC_JS/Mali-400'de regresyon yok. ⚠️ **Compass Settings-gating bu PR'da YOK (tüketici demand sinyali gerektirir — ayrı iş). Mali-400 cihaz doğrulaması bekliyor.** Test edilmeden 🟢 yapma. | 2026-07-11 | | 41 | **🔴 Device Validation Pending — Orientation Sensor Gate On-Demand Consumer Migration (PR 2/3)** — Yaşam-döngüsüyle açılıp kapanan DÖRT sensör tüketicisini ham `window.addEventListener` yerine Orientation Sensor Gate (#40) üzerinden çalıştırır. **Taşınanlar:** `arAlignmentService` (deviceorientationabsolute+deviceorientation+devicemotion → `subscribeOrientationAbsolute/subscribeOrientation/subscribeMotion`, 3 release ref, capture-fazı kaldırıldı=pencere-hedefli device event teslimatı faz-bağımsız), `dashcamService` (devicemotion → `subscribeMotion`, `_motionRelease`), `blackBoxService._startAccelerometer` (gate release aynen döner→`_accelUnsub` sözleşmesi değişmez), `deviceApi.subscribeToAccelerometer` (gate release döner→sentryEngine `_unsubAccel` yolu değişmez). **Model:** her tüketici gate release'i kendi lifecycle alanında tutar; start/stop İDEMPOTENT (mevcut guard'lar korundu); duplicate acquire yok; stop/dispose'ta release; gate'in SAHİBİ DEĞİL, gate'i dispose ETMEZ. **Davranış korundu:** handler'lar/callback sırası/işaretleri/smoothing/filtering/permission (feature-detect `'DeviceMotionEvent' in window` korundu, iOS requestPermission EKLENMEDİ) aynı; event payload aynı callback'e ulaşır. **Visibility:** tüketiciler kendi visibilitychange listener'ını EKLEMEZ (gate yönetir; çift listener yok). **Kazanım:** aynı ham event için birden fazla fiziksel JS listener yerine tek gate listener + background'da otomatik pause + temiz ownership/cleanup. **YALIN:** event başına yeni allocation yok, yeni timer/rAF/polling yok, callback gövdesi büyümedi, yeni bağımlılık yok, import yan etkisiz. ⚠️ **gpsService ve smartDrivingEngine bu PR'da DEĞİŞMEDİ** (hâlâ always-on) → **Settings ekranında native sensör kaydı ve native 60 Hz sampling DEVAM EDEBİLİR; bu PR native samplingPeriod'u düşürdüğünü İDDİA ETMEZ ve Settings CPU sorununu ÇÖZMEZ** — bunlar PR 3 kapsamı (always-on talep-güdümlü acquire). | `platform/arAlignmentService.ts`, `platform/dashcamService.ts`, `platform/security/blackBoxService.ts`, `platform/deviceApi.ts`, `__tests__/orientationGateConsumerMigration.test.ts` (yeni, 30 test) | Gerçek cihazda (AR navigasyon/dashcam/black-box/sentry aktifken): (1) AR/Dashcam/BlackBox/Sentry aktifken sensör olayları çalışıyor (heading/G-force/crash-detect aynı); (2) özellik durduğunda ilgili consumer release oluyor; (3) birden fazla consumer tek fiziksel listener paylaşıyor; (4) background'da (visibility hidden) fiziksel listener sökülüyor; (5) foreground dönüşünde yalnız aktif consumer geri bağlanıyor; (6) duplicate listener/cleanup leak yok; (7) BASIC_JS/Mali-400'de regresyon yok. ⚠️ **gpsService ve smartDrivingEngine bu PR KAPSAMI DIŞI; Settings ve native 60 Hz sorunu PR 3'te çözülür. Mali-400 cihaz doğrulaması bekliyor.** Test edilmeden 🟢 yapma. | 2026-07-11 | diff --git a/src/__tests__/visionOverlayIdleLoopGate.test.ts b/src/__tests__/visionOverlayIdleLoopGate.test.ts new file mode 100644 index 00000000..5ec54266 --- /dev/null +++ b/src/__tests__/visionOverlayIdleLoopGate.test.ts @@ -0,0 +1,77 @@ +/** + * visionOverlayIdleLoopGate.test.ts — VisionOverlay AR-kapalı idle loop gate (saha fix 2026-07-12). + * + * SAHA KANITI (Xiaomi zircon, WebView DevTools + /proc trace): idle harita ana thread'inin + * ~yarısını (+compositor) `VisionOverlay.renderAR` 60 fps rAF döngüsü yakıyordu. Döngü, AR/kamera + * KAPALIYKEN bile (`!frame || !isHybrid || confidenceLevel==='off'`) `requestAnimationFrame(renderAR)` + * ile KOŞULSUZ yeniden planlanıyor, FullMapView mount olduğu sürece asla durmuyordu. + * + * A/B nedensellik (cihaz, geri alınabilir DevTools trace): VisionOverlay rAF düşürülünce + * process 35%→20%, main-thread 11%→6%, gfx 5.2→0.3 fps (idle harita hedefe ulaştı). + * + * FIX: AR kapalıyken döngüyü DURDUR (rafRef=null, reschedule YOK). Yeniden başlatma otomatik: + * renderAR deps (isHybrid/confidenceLevel/vision.frame/currentLat/currentLon) değişince renderAR + * yeniden yaratılır → `useEffect([renderAR])` döngüyü tekrar başlatır. Aktif AR çizim yolu DEĞİŞMEZ. + * + * NOT: bu repo component-render testinde `renderToStaticMarkup` (SSR) kullanır — useEffect/rAF + * ÇALIŞMAZ ve jsdom'da `createRoot` import edilemez (bkz. safetyContext.test.tsx). Bu yüzden fix + * KAYNAK-KİLİDİ ile korunur: off-path'in döngüyü durdurduğu + aktif yolun bozulmadığı doğrulanır. + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const SRC = readFileSync( + join(process.cwd(), 'src', 'components', 'map', 'VisionOverlay.tsx'), + 'utf8', +); + +/** renderAR fonksiyon gövdesini kabaca izole et (useCallback → dep dizisi sonu). */ +function renderARBody(): string { + const start = SRC.indexOf('const renderAR = useCallback('); + expect(start).toBeGreaterThan(-1); + // renderAR useCallback'in dep dizisine kadar (currentStepIndex]) al + const depEnd = SRC.indexOf('currentStepIndex]', start); + expect(depEnd).toBeGreaterThan(start); + return SRC.slice(start, depEnd); +} + +/** off-branch (AR kapalı skip bloğu) metnini izole et. */ +function offBranch(): string { + const body = renderARBody(); + const cond = body.indexOf("!isHybrid || confidenceLevel === 'off'"); + expect(cond).toBeGreaterThan(-1); + // koşuldan sonraki ilk `}` kapanışına kadarki blok + const braceOpen = body.indexOf('{', cond); + const braceClose = body.indexOf('}', braceOpen); + expect(braceClose).toBeGreaterThan(braceOpen); + return body.slice(braceOpen, braceClose + 1); +} + +describe('VisionOverlay — AR-kapalı idle loop gate', () => { + it('1. off-branch döngüyü DURDURUR: rafRef.current = null', () => { + expect(offBranch()).toMatch(/rafRef\.current\s*=\s*null/); + }); + + it('2. off-branch requestAnimationFrame ÇAĞIRMAZ (60fps boşa dönme regresyonu)', () => { + // Bu kilidin ASIL amacı: kimse off-branch'e `requestAnimationFrame(renderAR)` geri koymasın. + expect(offBranch()).not.toMatch(/requestAnimationFrame/); + }); + + it('3. AKTİF AR yolu korunur: renderAR sonunda hâlâ requestAnimationFrame(renderAR) var', () => { + // Döngü aktif çizimde kendini yeniden planlamaya devam etmeli (AR bozulmasın). + const body = renderARBody(); + expect(body).toMatch(/rafRef\.current\s*=\s*requestAnimationFrame\(renderAR\)/); + }); + + it('4. yeniden başlatma mekanizması: useEffect [renderAR] rAF başlatır (dep değişince restart)', () => { + // renderAR deps'i off/on koşulunu (isHybrid/confidenceLevel/vision.frame) içermeli, + // ve bir useEffect [renderAR] ile döngüyü kurmalı → AR açılınca otomatik restart. + expect(SRC).toMatch(/\}, \[isHybrid, confidenceLevel[\s\S]*?currentStepIndex\]\);/); + expect(SRC).toMatch(/useEffect\(\(\) => \{\s*rafRef\.current = requestAnimationFrame\(renderAR\);[\s\S]*?\}, \[renderAR\]\);/); + }); + + it('5. cleanup korunur (zero-leak): cancelAnimationFrame unmount\'ta', () => { + expect(SRC).toMatch(/cancelAnimationFrame\(rafRef\.current\)/); + }); +}); diff --git a/src/components/map/VisionOverlay.tsx b/src/components/map/VisionOverlay.tsx index e353d6a8..23b0a943 100644 --- a/src/components/map/VisionOverlay.tsx +++ b/src/components/map/VisionOverlay.tsx @@ -571,9 +571,19 @@ export const VisionOverlay = memo(function VisionOverlay({ const frame = vision.frame; - // Skip all drawing when AR is off or not in hybrid mode + // Skip all drawing when AR is off or not in hybrid mode. + // SAHA FIX (2026-07-12, cihaz QA + DevTools trace): AR/kamera KAPALIYKEN bu döngü + // eskiden `requestAnimationFrame(renderAR)` ile KOŞULSUZ yeniden planlanıyordu → + // FullMapView mount olduğu sürece 60 fps boşa dönüp (getContext(willReadFrequently) + // + getBoundingClientRect + clearRect her kare) idle harita ana thread'inin ~yarısını + // + compositor'ı yakıyordu (ölçüm: process 35%→20%, main 11%→6%, 5.2→0.3 fps). + // ÇÖZÜM: AR kapalıyken döngüyü DURDUR (yukarıdaki clearRect canvas'ı zaten temizledi). + // Yeniden başlatma otomatiktir: renderAR'ın deps'i (isHybrid/confidenceLevel/vision.frame/ + // currentLat/currentLon) değişince renderAR yeniden yaratılır → aşağıdaki useEffect + // ([renderAR]) döngüyü tekrar başlatır. AR aktif çizim yolu DEĞİŞMEZ. Zero-leak: rafRef=null. + // (Aynı desen satır ~550'de `if (!canvas) return;` ile de kullanılıyor.) if (!frame || !isHybrid || confidenceLevel === 'off') { - rafRef.current = requestAnimationFrame(renderAR); + rafRef.current = null; return; }