USB-Meter / Documentation / External Contributions.md
Newer Older
199 lines | 9.567kb
Bogdan Timofte authored a month ago
1
# External Contributions
2

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

            
7
---
8

            
9
## Contributor A — anonim, prieten al autorului
10

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

            
13
---
14

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

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

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

            
23
**Modificări introduse:**
24

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

            
29
**Bug introdus (descoperit ulterior):**
30

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

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

            
41
---
42

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

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

            
48
**Commit:** `4f79218`
49

            
50
**Modificări introduse:**
51

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

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

            
61
---
62

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

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

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

            
69
**Cauze identificate de contributor:**
70

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

            
75
**Modificări introduse:**
76

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

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

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

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

            
98
**Note privind originea problemelor rezolvate:**
99

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

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

            
112
---
113

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

            
116
### Contextul feature-ului
117

            
118
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.
119

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

            
122
---
123

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

            
126
**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.
127

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

            
130
---
131

            
132
### Unde este problema
133

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

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

            
142
    measurements.restorePersistedChargeSessionSamplesIfNeeded(from: activeSession)
143
    ...
144
}
145
```
146

            
147
**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ă.
148

            
149
**`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.
150

            
151
---
152

            
153
### Lanțul de apel la pornire
154

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

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

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

            
169
---
170

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

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

            
185
---
186

            
187
## Tipare observate
188

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

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