Stocarea şi retrieval-ul curbelor de încărcare în persistență.
ChargeSession
├── id: UUID
├── chargedDeviceID: UUID
├── startTime: Date
├── endTime: Date
├── startBatteryPercent: Double?
├── endBatteryPercent: Double?
├── totalEnergyWh: Double
└── checkpoints: [ChargeCheckpoint]
ChargeCheckpoint (measurement)
├── id: UUID
├── sessionID: UUID
├── timestamp: Date
├── voltage: Double
├── current: Double
├── power: Double
├── temperature: Double?
└── batteryPercent: Double?
Memory cache (24h)
↓
Core Data (local)
/ \
↓ ↓
iCloud Archive (JSON)
(CloudKit) (5+ years old)
ChargeSession are id unicChargeCheckpoint are reference valid la sessionIDstartTime < endTime pentru completed sessionslet session = meter.startChargeRecord(for: device)
// session object in-memory
// accumulate measurements în session.measurements array
while isCharging {
let measurement = meter.lastDataPoint
session.addMeasurement(measurement) // In-memory
}
Properties: - Rapid (no I/O) - Loss on app crash - Limits: max 1MB per session (~10k measurements @ 1Hz)
func endChargeRecord(_ session: ChargeRecord) {
// 1. Isolate valid curve
let isolated = isolateChargeRange(session.measurements)
// 2. Save to Core Data
let coreDataSession = ChargeSession()
coreDataSession.id = session.id
coreDataSession.chargedDeviceID = device.id
coreDataSession.startTime = isolated.realStartTime
coreDataSession.endTime = isolated.realEndTime
coreDataSession.totalEnergyWh = calculateEnergy(isolated.measurements)
// 3. Save checkpoints
for measurement in isolated.measurements {
let checkpoint = ChargeCheckpoint()
checkpoint.timestamp = measurement.timestamp
checkpoint.voltage = measurement.voltage
checkpoint.current = measurement.current
checkpoint.power = measurement.power
// ... other fields
coreDataSession.checkpoints.append(checkpoint)
}
// 4. Persist
managedObjectContext.insert(coreDataSession)
try? managedObjectContext.save()
}
Guarantees: - ACID transaction - Survives app crash - Instantly queryable
// NSPersistentCloudKitContainer handles automatically
// New/modified ChargeSession → CloudKit
// Happens in background, doesn't block UI
Flow:
Core Data save
↓
NSPersistentCloudKitContainer observes change
↓
Serializes to CloudKit record
↓
Uploads in background (when network available)
↓
Notifies on success/failure
CloudSync constraints: - MUST: Sessions < 5 min nu se syncă (noise filter) - MUST: Checkpoints limitate la max 1000 per CloudKit record - SHOULD: Aggregate samples dacă > 1000 checkpoints
func loadSessions(for device: ChargedDevice) -> [ChargeSession] {
let request: NSFetchRequest<ChargeSession> = ChargeSession.fetchRequest()
request.predicate = NSPredicate(format: "chargedDeviceID == %@", device.id as NSUUID)
request.sortDescriptors = [NSSortDescriptor(key: "startTime", ascending: false)]
let sessions = try? managedObjectContext.fetch(request)
return sessions ?? []
}
func loadCheckpoints(for session: ChargeSession) -> [ChargeCheckpoint] {
let request: NSFetchRequest<ChargeCheckpoint> = ChargeCheckpoint.fetchRequest()
request.predicate = NSPredicate(format: "sessionID == %@", session.id as NSUUID)
request.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
let checkpoints = try? managedObjectContext.fetch(request)
return checkpoints ?? []
}
Performance: - O(log n) for device lookup (indexed) - O(k) for checkpoint load (k = checkpoint count, typically 1000-5000)
func reconstructCurve(from session: ChargeSession) -> ChargeCurve {
let checkpoints = loadCheckpoints(for: session)
return ChargeCurve(
startTime: session.startTime,
endTime: session.endTime,
totalEnergy: session.totalEnergyWh,
measurements: checkpoints.map { cp in
Measurement(
timestamp: cp.timestamp,
voltage: cp.voltage,
current: cp.current,
power: cp.power
)
}
)
}
// Keep last 24h in cache
var recentCurves: [UUID: ChargeCurve] = [:]
func cachedCurve(for sessionID: UUID) -> ChargeCurve? {
if let cached = recentCurves[sessionID] {
return cached // Memory hit
}
// Core Data miss → fetch + cache
guard let coreDataSession = loadSessionFromCoreData(sessionID) else {
return nil
}
let curve = reconstructCurve(from: coreDataSession)
recentCurves[sessionID] = curve
return curve
}
Store: All 1Hz measurements Cost: ~1000 checkpoints per session
Strategy: Downsample to 10Hz
swift
func downsampleCurve(measurements: [Measurement], factor: Int) -> [Measurement] {
return measurements.enumerated()
.filter { $0.offset % factor == 0 }
.map { $0.element }
}
Reduction: 10x fewer points Loss: Minimal (still captures curve shape) Cost: ~100 checkpoints per session
Strategy: Store metadata only
swift
struct SessionMetadata {
let id: UUID
let chargedDeviceID: UUID
let startTime: Date
let endTime: Date
let totalEnergyWh: Double
let peakPowerW: Double
let averagePowerW: Double
// No measurements stored
}
Cost: Minimal (constant per session) Retrieval: Summary only, not full curve
func syncSessionsToCloudKit() {
let allSessions = loadSessionsFromCoreData()
for session in allSessions {
// Filter: only sessions > 5 minutes
guard session.duration >= 300 else { continue }
// Aggregate: if checkpoints > 2000, downsample
var checkpoints = loadCheckpoints(for: session)
if checkpoints.count > 2000 {
checkpoints = downsampleCurve(checkpoints, factor: 2)
}
// Push to CloudKit (NSPersistentCloudKitContainer handles)
}
}
MUST: Filtrare sesiuni mici (< 5 min) SHOULD: Agregate checkpoint-uri dacă > 2000 MAY: Archive > 1 an local-only
// Save completed session
func saveChargeSession(_ session: ChargeRecord)
// Isolates curve → persists to Core Data → CloudKit async
// Append checkpoint
func appendCheckpoint(_ measurement: Measurement, to sessionID: UUID)
// In-memory only, flushed on session end
// Query by device
func loadSessions(for device: ChargedDevice) -> [ChargeSession]
// Load single session
func loadSession(id: UUID) -> ChargeSession?
// Load curve with checkpoints
func loadFullCurve(sessionID: UUID) -> ChargeCurve?
// Load metadata only (fast)
func loadSessionMetadata(sessionID: UUID) -> SessionMetadata?
// Archive old sessions (local)
func archiveOldSessions(olderThan: Date) -> Int
// Returns count archived
// Cleanup from iCloud
func deleteFromCloudKit(sessionID: UUID)
// MUST: permanent (CloudKit deletion, can't undo)
User: Start session on meter A
App: Saves session A
User: Also start session on meter B
App: Saves session B
⟹ Both saved (transactions independent)
MUST: Core Data transactions isolate (ACID) MUST: CloudKit syncs maintain order
Device offline
App saves to Core Data ✓
CloudKit sync queued
User goes online
Sync resumes automatically
⟹ Transparent
MUST: Queue pending changes SHOULD: Retry with backoff MUST: Persist queue to disk (survive app restart)
Device A: 100 sessions synced to iCloud
User: Restore from backup
Device B: Pulls 100 sessions
Device A: Restarts, sees 100 sessions (already synced)
⟹ Deduplication needed
MUST: Deduplicate by session ID SHOULD: Keep most recent version
test_saveSession_PersistsToCoreData()
test_loadSessions_ByDevice()
test_loadCheckpoints_Ordered()
test_downsampleCurve_ReducesFactor()
test_cloudSyncFilters_SessionsUnder5Min()
test_archiveOldSessions_By Date()
test_deduplicateOnRestore()
| Operation | Time | Notes |
|---|---|---|
| Save session | < 100ms | Core Data write |
| Load 100 sessions | < 50ms | Indexed query |
| Load curve (1000 pts) | < 200ms | Reconstruct array |
| CloudKit sync | ~1-5s | Network dependent |
| Downsample 5000 pts | < 20ms | CPU bound |