Optimiser l'usage de la RAM
La problématique
Lorsque l'on code sur une base à micro-contrôleur, à la différence d'un nano-pc, on reste malgré tout dans un contexte avec des ressources matérielles restreintes qu'il faut optimiser.
Une de ces contraintes, qui est probablement la contrainte n°1 sur les cartes Micropython, c'est l'usage de la RAM.
En pratique, autant on n'a pas à s'en occuper lorsque l'on écrit des codes qui n'utilisent que une ou deux librairies ou encore lorsque l'on ne manipule pas de fichiers, autant lorsque le nombre de librairies utilisées simultanément augmente ou encore lorsque l'on manipule des fichiers en ouverture notamment, on est rapidement confronté au problème de la limite de RAM
Voici un petit comparatif des RAM par carte, parmi les plus utilisées :
On voit que sur la plupart des cartes, c'est plutôt serré... et la carte où on est le plus à l'aise, c'est l'ESP 32.
C'est quoi le problème concrètement ?
En pratique, une carte à microcontrôleur utilisée avec Micropython dispose d'une quantité de RAM prédéfinie. Généralement de quelques dizaines de Ko à 500Ko selon les cartes (ESP 32)
Micropython pour son fonctionnement utilise de base une certaine quantité de RAM. Et la RAM disponible pour le programme n'est pas la RAM totale : c'est la RAM de la carte moins la RAM utilisée par MicroPython. Ce qui reste, c'est la free-RAM.
La freeRAM est utilisée au fur et à mesure de l'exécution par les chargements des modules, les variables, des fichiers que l'on ouvre ou ferme.
En pratique, on va se retrouver avec un niveau de FreeRAM mettons de 20% d'usage. Sur une base 100Ko libre, on en a 20Ko déjà utilisés.
Si à présent on charge un fichier de 30Ko, même transitoirement, on va passer à 50Ko transitoirement. Mais si on ouvre un fichier de 80Ko ou plus, on va atteindre le maximum possible et on va avoir une erreur / levée d'exception liée à la RAM.
IL FAUT DONC QUE TOUTES LES OPERATIONS DURANT L'EXECUTION D'UN CODE NE DEPASSE JAMAIS LA RAM RESTANTE DISPONIBLE !
On comprend dès lors que le problème peut vite être rencontré, dès lors que l'on va ouvrir des fichiers, etc. Car, 100Ko, c'est vite atteint par une librairie javascript ou un fichier texte voire une image !
Comment connaître l'utilisation de la RAM de la carte Micropython
Module micropython
Pour connaître la RAM de la carte Micropython, on dispose de la fonction mem_info()
du module micropython
.
Concrètement, on fera :
import micropython as upy
print(upy.mem_info())
ce qui donne par exemple :
stack: 736 out of 15360
GC: total: 111168, used: 5360, free: 105808
No. of 1-blocks: 163, 2-blocks: 14, max blk sz: 32, max free sz: 6557
Ici, c'est sur un ESP 32 : on a 111Ko de RAM libre et il reste 105 000 au démarrage.
Il est même possible d'avoir accès au contenu de la mémoire sous différents formats à l'aide d'indices passés à la fonction mem_info()
Voir : https://docs.micropython.org/en/latest/reference/constrained.html#reporting
Module gc
Dans un code, on pourra placer cette commande à différents endroits pour afficher l'utilisation de la RAM.
On dispose aussi pour certaines versions de Micropython de l'linstruction mem_free()
et mem_alloc
du module gc
A noter que la somme des 2 correspond à la somme totale de la freeRAM, obtenue avec :
gc.mem_free() + gc.mem_alloc()
Voir : https://docs.micropython.org/en/latest/reference/constrained.html#reporting
Etat de la RAM au démarrage selon la carte
Pyboard
stack: 556 out of 15360
GC: total: 103360, used: 3504, free: 99856
No. of 1-blocks: 81, 2-blocks: 14, max blk sz: 40, max free sz: 6217
Pybstick
stack: 524 out of 15360
GC: total: 87424, used: 3280, free: 84144
No. of 1-blocks: 70, 2-blocks: 13, max blk sz: 40, max free sz: 5235
ESP 32
stack: 736 out of 15360
GC: total: 111168, used: 5360, free: 105808
No. of 1-blocks: 163, 2-blocks: 14, max blk sz: 32, max free sz: 6557
Micro:bit
stack: 384 out of 7680
GC: total: 64512, used: 2016, free: 62496
No. of 1-blocks: 55, 2-blocks: 9, max blk sz: 13, max free sz: 3866
Pi Pico
stack: 516 out of 7936
GC: total: 192064, used: 6672, free: 185392
No. of 1-blocks: 110, 2-blocks: 22, max blk sz: 64, max free sz: 11563
```
### Synthèse
| Carte | Free RAM |
| ---- | ---- |
| Pyboard| 100 Ko |
| PYBStick| 84 Ko |
| ESP 8266| 34 Ko |
| ESP 32| 110 Ko |
| Micro:Bit | 64 Ko |
| Pi Pico | 190 Ko |
!!! note
J'ai essayé la version spiRAM sur un ESP 32 simple pour voir si on avait accès à davantage de RAM : çà ne fonctionne (logiquement) pas.
!!! note "On peut donc retenir que l'on a grosso-modo 100Ko de FreeRAM pour les codes sur les cartes Micropython, avec un minimum de 64K sur la Micro:Bit et un maximum de 190Ko sur la Pi Pico"
## Nettoyer la RAM de ce qui n'est plus utilisé
Au fur et à mesure de l'utilisation de la RAM au fil de l'exécution d'un programme, des objets sont stockés en RAM. Ceux-ci s'accumulent au fil de l'exécution. Mais certains pour ne pas dire la plupart, ne sont pas ou plus utile. Il est donc intéressant de libérer la mémoire qui peut l'être lorsque que cela est possible. Concrètement, à intervalle régulier dans un code.
Pour cela on dispose d'un module dédié, qui s'appelle `gc` pour "garbage collector". Ce module fournit la fonction `collect()` qui permet de libérer la freeRAM des éléments qui ne sont plus utilisés :
gc.collect()
## Prévenir l'erreur de mémoire
On peut prévenir l'erreur de mémoire en définissant la taille d'un buffer d'urgence. Mais çà ne suffira pas à empêcher les erreurs par débordement "massif".
A mettre en début de code.
## Extraire les valeurs utiles à partir de mem_info()
TODO
## Analyse de l'impact en RAM de différentes manip'
Test sur un ESP 32 en appelant ```upy.mem_info()``` après chaque commande :
TO FINISH - faire un code automatique de test avec affichage par calcul
## Mesures et trucs pour empêcher le débordement de mémoire
### Gérer l'exception par try : ... except :
On peut encadrer le code suceptible d'entraîner un débordement de RAM par un `try: ... except: ...`
### S'assurer que l'on a toujours assez de mémoire libre pour les opérations gourmandes
C'est un calcul à faire par anticipation et également analyse du comportement du programme au fil de son exécution. Grosso modo, prévoir que 50% de la free RAM reste libre pour les opérations gourmandes même ponctuelles. Et prévoir un fonctionnement de base le plus bas possible.
### Prévenir un problème pour les data reçues de l'extérieur ?
Cas d'une requête web
### Ouvrir les fichiers par ligne ou par blocs
Limite : c'est long dans le cas d'un serveur web
### Utiliser des librairies javascript de petite taille (quelques ko max) et minifiée
Dans le cas des librairies javanscript, que l'on devra ouvrir d'un coup si on veut avoir une bonne réactivité, il faut utiliser des petites librairies Javascript. Et il y a de quoi faire !
### Faire des réponses AJAX succintes et/ou favoriser AJAX "goutte à goutte"
### Alternative : mettre les gros fichiers sur un nanopc configuré en serveur sur le réseau local
Nano pc qui pourra assurer plusieurs fonctions en général : serveur de librairies, mais aussi datalogging (Domoticz notamment), serveur MQTT pour le réseau local
### Eviter les variables intermédiaires
### Faire un gc.collect() juste avant une opération potentiellement gourmande ou critique
### Eviter de mettre du texte dans le code
Etc... cf remarques déjà faîtes ...
cf autour serveur AJAX
On obtient une exception de dépassement de mémoire à intervalles réguliers (carte avec 512K de mémoire). Une solution a été de passer la page web sous forme d'un fichier séparé qui est lu plutôt que mis dans le programme. Dans ce cas de figure, on se retrouve avec des réponses qui sont parfois de 1 à 2 seconces (panneau webdevelopper de Firefox), comme si Micropython transférait des éléments du programmes hors RAM, mais ce faisant prenait du temps... En ne mettant pas le texte du fichier HTML dans le programme, il n'est donc pas chargé en RAM lorsque ce n'est pas utile. Les délai de réponse sont alors de 130ms voire 200ms occasionnellement pour un délai de réponse de l'ordre de 40-50ms en "base".
Il faudrait probablement également ne pas lire le fichier entier mais faire en sorte qu'il soit lu ligne à ligne et envoyé ligne à ligne. Il faudrait donc que l'entête et la conclusion soit mise dans des fonctions qui sont appelées au niveau des réponses.
Se pose la question de traiter la présence d'une valeur dans la page web. Mettre une chaîne <xxx> par exemple qui lorsqu'elle est trouvée est remplacée avant envoi. Mais cela n'est pas essentiel si on utilise ajax
Attention : dans le délai : noter que l'état de la connexion wifi intervient.
On peut utiliser micropython.mem_info() pour analyser l'état de la RAM.
Bien mettre le gc.collect() que si get/
On peut aussi empêcher le blocage en mettant tout le code de la boucle while dans un try: except: et dans le except on fait redémarrer le serveur.
Le problème de "memory allocation" survient lorsque l'on recharge le fichier html. Il faudrait donc le lire par ligne
En ne passant pas par une variable pour l'ouverture de la lib js, on libère la RAM du "volume" de la lib... (57 000 on passe à 13 000 - lib fait 44K...) Lors du rechargement de la page, la RAM repasse à 57 mais est rapidement libérée à 13 000. On pourrait donc lire la lib' ligne à ligne ce qui éviterait même ce problème. Mais c'est beaucoup moins rapide... Autant çà passe pour le fichier HTML, autant pour la lib', c'est beaucoup trop lent et çà crée des erreurs. De plus la RAM de base est plus élevée de cette façon (code en plus ? )
Donc viser lib le plus petites possibles... pour ne pas avoir de problème de chargement.
## Monitorer l'utilisation de la RAM
Voici un code pour monitorer l'utilisation de la RAM
## Intercepter les erreurs de mémoires
Pour intercepter les erreurs de mémoire, on peut définir un buffer pour obtenir de l'information sur l'exception : on mettra en début de code quelque chose de la forme :
```python
# pour eviter probleme memmoire - au tout debut
import micropython
micropython.alloc_emergency_exception_buf(100) # 100 conseille...
Liens utiles
- doc officielle sur la RAM : https://docs.micropython.org/en/latest/reference/constrained.html#ram
- doc officielle sur les techniques d'optimisation : https://docs.micropython.org/en/latest/reference/constrained.html#execution-phase
- doc officielle sur le heap : https://docs.micropython.org/en/latest/reference/constrained.html#the-heap
Une discussion sur le sujet ici : https://forum.micropython.org/viewtopic.php?f=2&t=1747