1 contributor
# 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.