Améliorer les performances d'accès SQLite d'une application UWP Windows 10 IOT CORE sur un Raspberry PI
Vous le savez déjà, il est possible d'éxecuter des applications UWP 10 sur un Raspberry PI. Cela fonctionne parfaitement mais parfois il faut optimiser votre application pour le périphérique ciblé et c'est notamment le cas pour l'utilisation d'SQLite sur un Raspberry.
Pour rappel, cet article fait partie d'une série :
- SQLite et performances : ne récupérez de la base que ce dont vous avez besoin #UWP
- Comment gérer les montées de version du stockage SQLite des applications #Windows
- SQLite, UWP et Windows Store : optimisation des performances - ma démarche
- Cet article
TL;DR;
L'écriture sur la carte SD du PI peut être très lente, on ne va donc le faire qu'à des moments clefs :
- Choisir un journal de type WAL.
- Activer le mode de synchronisme « Normal » sur la base de données.
- Désactiver l’intégration automatique du journal WAL dans la base de données principale au bout de 1000 pages.
- Déclencher l’intégration du journal WAL toutes les x secondes via un code personnalisé.
Identification du problème
En utilisant la méthode décrite précédemment on se rend compte que les requêtes sont effectuées globalement assez rapidement puis tout d'un coup on se retrouve avec des pics de plusieurs secondes pour des insertions simples. Le comportement semble donc échapper à toute logique (toute allusion à William est purement fortuite ici) et SQLite ne semble pas forcément le coupable au premier abord.
En fouillant un peu on trouve finalement sur l'internet mondial des informations indiquant que l'écriture sur une carte SD (même la plus rapide trouvée sur Amazon) peut être lente et on commence à imaginer d'où vient le problème : l'écriture sur le disque. Il va donc falloir travailler au niveau d'SQLite pour obtenir des performances acceptables.
Le journal WAL
Les modifications faites dans une base de données se font souvent en deux étapes : d'abord dans une sorte de base temporaire en mémoire, le journal qui est ensuite intégré dans la base principale. Ce mécanisme d'intégration porte le joli nom de synchronisation/checkpoint et nous verrons plus tard que l'on peut configurer celui-ci.
Ce qui nous importe ici c'est de choisir le meilleur mode journal et il en existe beaucoup. Dans notre cas (et quasiment toujours en réalité) je vous recommande de passer en WAL (Write Ahead Log) très bien décrit par Sqlite. Ce mode est un des plus performants, supporte mieux les accès concurrents mais créé des fichiers supplémentaires à côté de la base de données.
L'intérêt notoire ici est que nos modifications seront donc faites sur un fichier séparé de la base de données principale : nous verrons bientôt pourquoi c'est intéressant. La ligne de code pour activer ce mode est très simple :
PRAGMA journal_mode WAL
Mode de synchronisation SQLite
Il est possible de configurer cette synchronisation via la PRAGMA "synchronous" en choisissant un de ces modes :
- OFF : désactivé complétement.
- NORMAL : activé mais le moins souvent possible.
- FULL : activé de manière sûre mais relativement lent, Cela est la valeur par défaut.
- EXTRA : le top du top.
Puisque l'écriture sur disque est lente, on va choisir ici de synchroniser les données le moins souvent possible en activant le mode "NORMAL" :
PRAGMA synchronous NORMAL
Etant donné que nous sommes passés sur un mode de journal WAL, cela a aussi l'avantage de n'activer l'écriture sur le disque qu'à partir du moment où un checkpoint est atteint et non pas à chaque fois qu'une transaction est faite sur la base comme c'est le cas avec le mode "FULL" par défaut.
Piloter l'intégration du WAL à la base principale.
Pour limiter l'écriture sur le disque, il va falloir limiter les checkpoints (aka synchronisations) et cela est heureusement possible facilement. Par défaut, cela est fait dès que 1000 pages sont écrites dans le journal : on comprend maintenant pourquoi on constatait de bonnes performances et puis soudain des pics de lenteurs. À chaque fois que le checkpoint se passait, la requête déclenchait une écriture sur disque et était donc lente à s'exécuter.
Ce comportement est désactivé en spécifiant une limite de -1 à la pragma wal_autocheckpoint.
PRAGMA wal_autocheckpoint=-1;
Il reste ensuite à persister ces données pour ne pas les laisser données uniquement en mémoire. Cela est fait simplement avec la pragma wal_checkpoint:
PRAGMA wal_checkpoint;
Avec SQLite-net, on peut par exemple le faire sans trop de risque toutes les minutes :
private async Task StartWriteProcess() { while (true) { await Task.Delay(60 * 1000); lock (locker) { db.ExecuteScalar<string>("PRAGMA wal_checkpoint"); } } }
Cela va donc déclencher un checkpoint (intégration des données du journal dans la base principale) qui va lui même déclencher une persistence sur le disque des données car on a activé le mode NORMAL.
Le défaut de cette solution
Cette solution fonctionne sans problème mais présente un inconvénient : en cas de perte de courant ou de défaillance système les données peuvent être perdues irrémédiablement.
Notre choix du mode WAL prends tout son sens ici : en ayant un fichier à part, on ne perdra au pire que les x dernières secondes depuis la dernière synchronisation au lieu de perdre la base dans son ensemble.
Happy coding !
Commentaires