USB-Meter / Documentation / External Contributions.md
1 contributor
199 lines | 9.567kb
# External Contributions

This document tracks contributions made by collaborators outside the core project.
Its purpose is to support ongoing evaluation of each contributor's work quality,
patterns, and reliability over time.

---

## Contributor A — anonim, prieten al autorului

Toate intervențiile cunoscute sunt în `AppData.swift`, `ChargeInsightsStore.swift`, și `Meter.swift`.

---

### Intervenție 1 — Fix lag UI la formulare (aprilie 2026)

**Context:** La conectarea unui contor live, formularele din UI (typing, picker)
deveneau iresponsive. `reloadChargedDevices()` rula sincron pe main thread la fiecare
snapshot BLE (~1/s), blocând UIKit.

**Commit:** `4f79218` *(înglobat cu Intervenția 2)*

**Modificări introduse:**

- `chargeInsightsReadStore` — context separat de citire (`privateQueueConcurrencyType`)
- `chargedDevicesReloadQueue` — fetch mutat pe background queue
- `chargedDevicesReloadGeneration` — counter de invalidare pentru fetch-uri async depășite

**Bug introdus (descoperit ulterior):**

Pattern-ul generation+invalidare era prea agresiv. Contorul era incrementat la fiecare
apel `reloadChargedDevices()`, inclusiv cele declanșate în burst de
`NSManagedObjectContextObjectsDidChange`. Orice fetch async dura >0ms → generația era
deja depășită la finalizare → rezultatul era aruncat. Lista rămânea goală permanent
atât timp cât un contor live era conectat, indiferent că SQLite conținea date reale.

**Evaluare:** Problema identificată era reală. Soluția era funcțională în absența unui
contor conectat (test static), dar s-a rupt sub load real. Eroare clasică de
async invalidation, probabil netestat cu dispozitiv real.

---

### Intervenție 2 — Fix starvation reload (după aprilie 2026)

**Context:** Lista de charged devices rămânea goală permanent când un contor era activ.
Cauzat de bug-ul din Intervenția 1. Autorul a identificat și corectat singur problema.

**Commit:** `4f79218`

**Modificări introduse:**

- Înlocuire generation counter cu pattern `inFlight`/`pending`
  - `chargedDevicesReloadInFlight: Bool` — un singur fetch în zbor la un moment dat
  - `chargedDevicesReloadPending: Bool` — cerere nouă marcată pentru după finalizare
- `scheduleChargedDevicesReload(delay: 0.15)` — debounce pentru burst-uri de notificări

**Evaluare:** Corect și complet. Soluția standard pentru problema dată. Nu introduce
regresii. Diagnosticul era precis (cita dimensiunile exacte din SQLite: 10 devices,
15 sesiuni, 49 checkpoints, 24.279 samples).

---

### Intervenție 3 — Optimizare CPU pe Catalyst (aprilie 2026)

**Context:** Procesul Catalyst consuma CPU ridicat și constant. Identificat prin profiling.

**Commits:** `406be0e` (împreună cu ajustări ulterioare ale intervalelor)

**Cauze identificate de contributor:**

1. BLE polling continuu — `dataDumpRequest()` recursiv imediat după fiecare răspuns
2. Fiecare pachet BLE atingea CoreData/iCloud direct, cu reload UI frecvent
3. `noteMeterSeen` scria în `MeterNameStore` la fiecare observație

**Modificări introduse:**

- `Meter.swift` — `minimumLivePollingInterval = 0.4s`; înlocuit recursie imediată cu
  `scheduleNextLiveDataDumpRequest()` care respectă intervalul minim
- `AppData.swift` — coalescere snapshot-uri BLE în memorie (`pendingChargeObservationSnapshots`);
  flush periodic la 30s cu flush explicit la pause/stop/checkpoint/terminate;
  throttle `noteMeterSeen` la 15s; observer schimbat din `ObjectsDidChange` → `DidSave`
- `AppData.swift` — `writeContext` privat pentru `ChargeInsightsStore`
  (anterior store-ul scria direct pe `viewContext`, i.e. main thread)
- `ChargeInsightsStore.swift` — `maximumLiveIntegrationGap` 20s → 90s
  (aliniat cu fereastra de coalescere de 30s)

**Probleme rămase după livrare (rezolvate în același commit):**

- Work item-ul de flush era dispatchat pe `DispatchQueue.main`; apela
  `context.performAndWait` → bloca main thread la fiecare 30s (spike vizibil în profiler)
- `flushPendingChargeObservation` chema explicit `reloadChargedDevices()`, iar observer-ul
  DidSave chema și el `scheduleChargedDevicesReload()` — double reload per flush

Aceste două probleme au fost remediate separat (fix în `406be0e`):
flush-ul periodic mută scrierea CoreData pe `DispatchQueue.global(qos: .utility)`;
reload-ul explicit eliminat din flush, lăsat exclusiv pe seama observer-ului DidSave.

**Note privind originea problemelor rezolvate:**

`NSManagedObjectContextObjectsDidChange` ca observer și `viewContext` pentru scrieri
existau în codul proiectului **înainte** de orice intervenție a acestui contributor.
Intervenția 3 rezolvă probleme de design preexistente, nu regresii proprii.
Singura auto-corecție este `maximumLiveIntegrationGap`, ajustat pentru coalescing-ul
introdus tot în această intervenție.

**Evaluare:** Diagnostic corect pentru toate cele trei cauze. Modificările sunt
coerente între ele. Livrarea conținea două bug-uri minore (main thread + double reload)
care au necesitat remediere imediată, detectabile printr-un review atent al codului
fără a fi nevoie de runtime. Efectul net: CPU redus de la consum constant la 0–4%
cu spike-uri reziduale la ~30s (CloudKit sync inerent).

---

## Session Trim Feature — Buffer Restore Bug (pentru externalizare)

### Contextul feature-ului

S-a implementat un feature de "Session Trim": detectare automată a ferestrei reale de încărcare (ex: Apple Watch se încarcă 3h dar sesiunea rulează 16h overnight) + editor manual pentru trim. Feature-ul este complet implementat și compilează fără erori.

**Există un bug pre-existent** care blochează funcționarea feature-ului și trebuie rezolvat separat.

---

### Bug: istoricul sesiunii se pierde la fiecare restart de aplicație

**Simptom:** La restart cu sesiune activă, graficul afișează intervalul complet al sesiunii (ex: 16h) dar cu date doar din momentul reconectării BLE. Istoricul din Core Data nu este restaurat.

**Consecință pentru trim:** Banner-ul de trim nu apare (se bazează pe `aggregatedSamples` gol), editorul de trim are graficul gol.

---

### Unde este problema

**`Meter.restoreChargeRecordIfNeeded(from:)`** — `USB Meter/Model/Meter.swift`

```swift
func restoreChargeRecordIfNeeded(from activeSession: ChargeSessionSummary) {
    guard chargeRecordState == .waitingForStart else { return }  // ← problema
    guard chargeRecordStartTimestamp == nil else { return }
    guard chargeRecordAH == 0, chargeRecordWH == 0, chargeRecordDuration == 0 else { return }

    measurements.restorePersistedChargeSessionSamplesIfNeeded(from: activeSession)
    ...
}
```

**Race condition:** Datele BLE ajung înaintea completării `reloadChargedDevices` (async). La primul pachet BLE, `chargeRecordState` trece `waitingForStart → active`. Când `reloadChargedDevices` finalizează și încearcă restore, primul guard blochează — permanent pentru sesiunea curentă.

**`Measurements.restorePersistedChargeSessionSamplesIfNeeded`** are propriul guard: returnează dacă vreun series este non-gol. Dacă se rezolvă problema de mai sus și datele BLE au umplut deja buffer-ul, acesta trebuie resetat înainte de restore — dar **numai dacă `aggregatedSamples` nu este gol**, altfel graficul rămâne gol definitiv.

---

### Lanțul de apel la pornire

```text
SceneDelegate.activateCloudDeviceSync
  → AppData.activateChargeInsights        // setup writeContext + readContext
  → AppData.scheduleChargedDevicesReload  // async, ~100–500ms

[BLE se conectează, date ajung — pe main queue]
  → Meter.processData → Meter.updateChargeRecord
    → chargeRecordState = .active  // ← guard-ul din restore va bloca

[reloadChargedDevices completează]
  → AppData.restoreChargeMonitoringStateIfNeeded(for: meter)
    → Meter.restoreChargeRecordIfNeeded(from: session)  // ← guard blochează
```

---

### Fișiere modificate de feature (necommitted, diff disponibil)

| Fișier | Modificare |
| ------ | ---------- |
| `USB Meter/Model/CKModel.xcdatamodeld/USB_Meter 15.xcdatamodel/` | Nou — adaugă `trimStart`/`trimEnd` opționale pe `ChargeSession` |
| `USB Meter/Model/CKModel.xcdatamodeld/.xccurrentversion` | → v15 |
| `USB Meter/Model/ChargingWindowDetector.swift` | Nou — detectează fereastra activă de încărcare |
| `USB Meter/Views/Meter/Tabs/ChargeRecord/SessionTrimEditorView.swift` | Nou — editor trim cu drag handles |
| `USB Meter/Model/ChargeInsightsModel.swift` | `trimStart`, `trimEnd`, `isTrimmed` pe `ChargeSessionSummary` |
| `USB Meter/Model/ChargeInsightsStore.swift` | `setSessionTrim(sessionID:start:end:)` — recalculează energie, șterge checkpoints |
| `USB Meter/Model/AppData.swift` | Wrapper `setSessionTrim` |
| `USB Meter/Views/Meter/Tabs/ChargeRecord/MeterChargeRecordTabView.swift` | Banner detecție + sheet editor |
| `USB Meter.xcodeproj/project.pbxproj` | Referințe fișiere noi |

---

## Tipare observate

| | Interv. 1 | Interv. 2 | Interv. 3 |
|---|---|---|---|
| Diagnostic corect | Da | Da | Da |
| Funcționează fără load real | Da | Da | Da |
| Funcționează sub load real | Nu | Da | Parțial |
| Auto-corecție necesară | — | Da (proprie) | Parțial (externă) |
| Regresii introduse | Da (starvation) | Nu | Nu |

**Observație recurentă:** Contribuțiile funcționează corect în condiții statice dar
au tendința să rateze edge case-uri sub load real (contor live conectat). Testele
înainte de livrare par să nu includă scenariul cu dispozitiv activ.