Come abbiamo ottimizzato le nostre prestazioni di risparmio dei dati principali


Scritto da Louis Debaere, Software Engineer watchOS

Introduzione: una libreria di database condivisa

L'app Runtastic per iPhone ha una controparte Apple Watch che offre la stessa fantastica esperienza di tracciamento. Su iOS, il nostro livello di persistenza è gestito da una libreria in cima al framework Core Data di Apple . Core Data è disponibile anche su watchOS, il sistema operativo in esecuzione su Apple Watch. Sembrava naturale per noi sfruttare lo stesso framework di persistenza per entrambe le piattaforme in una libreria condivisa, riutilizzare il più possibile il nostro codice esistente.

Le prestazioni reggono?

Prima di iniziare a usarlo, tuttavia , dovevamo assicurarci che funzionasse secondo i nostri standard. Per fare ciò, abbiamo creato un prototipo che simula il funzionamento del database più critico per le prestazioni: il salvataggio dei dati durante una sessione sportiva attiva. Aggiungiamo dati come punti GPS e frequenza cardiaca a un oggetto sessione che viene salvato automaticamente in tempo reale. Se l'app dovesse terminare improvvisamente, potremmo quindi ripristinare e ripristinare in modo sicuro una sessione attiva.

Simulazione

Abbiamo creato un semplice prototipo che simula una tipica sessione in esecuzione. Proprio come nell'app Runtastic, salviamo la sessione nel database ogni cinque secondi e aggiungiamo una nuova posizione GPS ogni due secondi.

 private var coreDataController: CoreDataController
private var locationTimer: Timer?
private var saveTimer: timer?
private func start () {
coreDataController.createSession ()
locationTimer = .scheduledTimer (withTimeInterval: 2.0, ripete: true) {[weak self] _ in
auto? .coreDataController.addLocationToSession ()
}
saveTimer = .scheduledTimer (withTimeInterval: 5.0, ripete: true) {[weak self] _ in
auto? .coreDataController.save ()
}
}

Cosa succede realmente nella nostra classe CoreDataController ? È un semplice wrapper Core Data basato su un contesto di oggetto gestito.

 classe finale CoreDataController {
contesto privato: NSManagedObjectContext
sessione var privata: sessione?
func save () {
let start = CFAbsoluteTimeGetCurrent ()
context.performAndWait {
fare {
prova context.save ()
} catturare {
assertionFailure ("Errore durante il salvataggio del contesto dell'oggetto gestito:  (errore)")
}
}
let stop = CFAbsoluteTimeGetCurrent ()
os_log ("Il salvataggio ha richiesto:% .2f", stop - start)
}
func addLocationToSession () {
context.performAndWait {
let location: Location = .random (in: contesto)
sessione? .addToLocations (posizione)
}
}
} 

Teniamo traccia di quanto tempo impiega il salvataggio registrando la differenza in tempo assoluto prima e dopo un'operazione di salvataggio con blocco. Questo è importante: non possiamo misurare il tempo di esecuzione di una chiamata asincrona.

Ogni nuova posizione che viene aggiunta alla sessione viene popolata con dati casuali.

Risultati

Naturalmente, per una rappresentazione accurata di i dati registrati, abbiamo dovuto testare su dispositivi reali invece del simulatore watchOS che abbiamo usato per lo sviluppo.
La nostra simulazione ha continuato a funzionare per oltre un'ora. I numeri sono presentati di seguito. Sull'asse y si trova la durata del salvataggio in relazione al tempo trascorso sull'asse x. Ogni generazione di Apple Watch è raffigurata per il confronto: S0 rappresenta l'originale, rilasciato nel 2015, e S4 è l'ultima generazione.

Due cose saltano immediatamente fuori. Innanzitutto, la durata del risparmio aumenta in modo lineare, ma dovrebbe essere costante. Il nostro team è stato piuttosto sorpreso da questo risultato, quindi il passo successivo è stato quello di trovarne una possibile causa.

In secondo luogo, l'orologio serie 4 ha prestazioni significativamente migliori e si avvicina a prestazioni simili a quelle di un iPhone (dove ovviamente non notiamo un problema

Indagine

Abbiamo esaminato più da vicino il modello di dati della nostra sessione e i suoi attributi, in particolare la relazione sulla posizione, che è gestita interamente da Core Data.

Nel nostro codice sopra, solo una chiamata influisce sulla sessione: sessione? .addToLocations (posizione) . Trova la definizione di addToLocations di seguito.

 // MARK: accessori generati per le posizioni
sessione di estensione {
@objc (addLocationsObject :)
@NSManaged public func addToLocations (_ value: Location)
@objc (removeLocationsObject :)
@NSManaged public func removeFromLocations (_ value: Location)
@objc (addLocations :)
@NSManaged public func addToLocations (_ valori: NSOrderedSet)
@objc (removeLocations :)
@NSManaged public func removeFromLocations (_ valori: NSOrderedSet)
}

È una funzione generata automaticamente da Core Data, insieme ad altre utili. Questi accessori manipolano il tipo di dati sottostante di una relazione Dati principali. Il tipo di dati che alimenta una relazione varia in base alla sua cardinalità, disposizione e altro. locations è una relazione uno-a-molti con un accordo ordinato.

 extension Session {
@NSManaged var var id: String?
@NSManaged public var startDate: Date?
@NS Posizioni var pubbliche gestite: NSOrderedSet?
}

Ciò significa che le posizioni associate alla nostra sessione sono memorizzate in un `NSOrderedSet`, un tipo di dati di raccolta che tiene traccia della disposizione dei suoi elementi oltre a garantire l'unicità come un insieme normale.

Prima vittoria per l'ottimizzazione [19659003] Dopo aver verificato i nostri requisiti e aver verificato il nostro codice, ci siamo resi conto che non avevamo bisogno di questa caratteristica dell'ordine, almeno non al momento del salvataggio. Potremmo semplicemente ordinare le posizioni in base alla loro proprietà `unixTimestamp` in seguito.

Si è scoperto, solo un singolo cambiamento nella disposizione ha fatto un'enorme differenza. Guarda tu stesso:

Il passaggio dall'ordinamento al non ordinato ha mostrato un immediato aumento delle prestazioni. Sul dispositivo di base, dopo mezz'ora, il salvataggio richiede circa 100 ms rispetto a 165 ms e dopo un'ora solo 150 ms, quando impiegava oltre 300 ms!

Anche se ha ridotto drasticamente il tempo di risparmio, non eravamo " t soddisfatto. Il tempo necessario per risparmiare stava ancora aumentando, quindi iniziamo a chiederci se le stesse relazioni con i dati fondamentali siano venute con un certo sovraccarico. Fortunatamente, durante il WWDC, abbiamo avuto la possibilità di condividere le nostre scoperte con un ingegnere Apple che lavora nel team Core Data. Ha confermato il nostro sospetto: ci è stato detto che set ordinati e non ordinati in combinazione con relazioni creano un sovraccarico di memoria che aumenta nel tempo. A differenza del telefono, l'orologio semplicemente non ha le risorse per gestirlo. Almeno non ancora. Possiamo chiaramente vedere che sta raggiungendo rapidamente.

Come test finale abbiamo provato a collegare manualmente le posizioni alla sessione. Invece della sessione che contiene una raccolta di posizioni come relazione, abbiamo aggiunto un attributo sessionID a ciascuna posizione salvata, in modo da poterli recuperare utilizzando l'ID della rispettiva sessione come chiave.

Questo si è rivelato l'approccio ideale: abbiamo finalmente visto le prestazioni costanti che stavamo puntando dall'inizio! ?

Conclusione

Alla fine della giornata, sono i dati che contano. Dopo aver dimostrato che le nostre prestazioni di risparmio non erano ottimali, abbiamo studiato la causa principale e trovato una soluzione più ottimizzata.

Apple ha suggerito lo stesso approccio di cui sopra. Abbiamo deciso di collegare tutte le informazioni sulla sessione che raccogliamo manualmente, come elevazione e frequenza cardiaca, per prestazioni ottimali.

Un altro vantaggio della condivisione del nostro framework di database è che questa ottimizzazione arriva anche su iOS, quindi possiamo essere ancora più sicuri dell'efficienza delle sessioni di lunga durata al telefono.

***



Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *