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