Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/DEVICE_VALIDATION_LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
77 changes: 77 additions & 0 deletions src/__tests__/visionOverlayIdleLoopGate.test.ts
Original file line number Diff line number Diff line change
@@ -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\)/);
});
});
14 changes: 12 additions & 2 deletions src/components/map/VisionOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading