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.
Toate intervențiile cunoscute sunt în AppData.swift, ChargeInsightsStore.swift, și Meter.swift.
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 queuechargedDevicesReloadGeneration — counter de invalidare pentru fetch-uri async depășiteBug 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.
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:
inFlight/pending
chargedDevicesReloadInFlight: Bool — un singur fetch în zbor la un moment datchargedDevicesReloadPending: Bool — cerere nouă marcată pentru după finalizarescheduleChargedDevicesReload(delay: 0.15) — debounce pentru burst-uri de notificăriEvaluare: 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).
Context: Procesul Catalyst consuma CPU ridicat și constant. Identificat prin profiling.
Commits: 406be0e (împreună cu ajustări ulterioare ale intervalelor)
Cauze identificate de contributor:
dataDumpRequest() recursiv imediat după fiecare răspunsnoteMeterSeen scria în MeterNameStore la fiecare observațieModificări introduse:
Meter.swift — minimumLivePollingInterval = 0.4s; înlocuit recursie imediată cu
scheduleNextLiveDataDumpRequest() care respectă intervalul minimAppData.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 → DidSaveAppData.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):
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 flushAceste 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).
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.
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.
Meter.restoreChargeRecordIfNeeded(from:) — USB Meter/Model/Meter.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.
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ș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 |
| 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.