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 : 
import gc

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". 
mport micropython micropython.alloc_emergency_exception_buf(100) # 100 conseille...
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 : 
from machine import *
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... 

https://docs.micropython.org/en/v1.9.3/esp8266/library/micropython.html#micropython.alloc_emergency_exception_buf

Liens utiles

Une discussion sur le sujet ici : https://forum.micropython.org/viewtopic.php?f=2&t=1747