Skip to content

Instantly share code, notes, and snippets.

@dsferruzza
Created May 5, 2020 22:19
Show Gist options
  • Save dsferruzza/54834a4df15e6a4dfdfac9059cb45eac to your computer and use it in GitHub Desktop.
Save dsferruzza/54834a4df15e6a4dfdfac9059cb45eac to your computer and use it in GitHub Desktop.
[FR] Small exercise in Rust based on https://github.com/makers-for-life/makair-telemetry

TP : Ajouter une commande de stats dans l’outil de debug de la télémétrie

Contexte

Fonctionnalités

La lib de télémétrie (dans src/software/telemetry/) est une crate Rust qui comporte 2 éléments :

  1. La lib en elle-même (point d’entrée src/lib.rs)

  2. Un outil CLI (point d’entrée src/bin.rs)

L’outil CLI permet actuellement :

  • de récupérer, de parser et d’afficher le protocole de télémétrie qui passe sur un port série donné : debug

  • de récupérer, de parser et d’afficher le protocole de télémétrie qui a été enregistré dans un fichier donné : play

  • de récupérer les octets qui passent sur un port série donné et de les enregistrer dans un fichier donné : record

Outils

Tip
Le setup le plus simple est de se placer dans src/software/telemetry/ avec un terminal et avec VS Code (de manière à avoir le support de RLS).

Pour lancer l’outil, le plus pratique est d’appeler scripts/run.sh :

./scripts/run.sh --help

Pour lancer rustfmt, Clippy (le linter Rust) et les tests du même coup, il faut la commande suivante :

cargo fmt && cargo clippy --all-features && cargo test

Je recommande de le faire très régulièrement, voire avant chaque essai.

Objectif

On veut ajouter un 4e mode au programme en CLI : stats.

Ce mode prend le path d’un fichier en entrée (comme play) mais au lieu de le rejouer et d’afficher les messages au fur et à mesure dans stdout (avec des temporisations pour faire réaliste), on va agréger les différents messages pour récupérer le nombre d’occurences de chaque message dans le fichier.

1. Modifier la config du CLI

On utilise clap (version 3, pas encore sortie) pour parser les arguments passés au lancement de l’exécutable et gérer l’affichage de leur documentation (quand on le lance avec -h par exemple).

Il faut donc modifier ça pour gérer notre nouveau mode :

  1. ajouter une struct Stats basée sur Play

  2. modifier le enum Mode pour ajouter cette struct

Tip
À ce stade là, lancer Clippy ou une compilation devrait indiquer les endroits où on n’exploite pas le enum de manière complète.

Ensuite, on va pouvoir :

  1. ajouter une nouvelle fonction stats(cfg: Stats)

  2. mettre un println!("whatever") dedans

  3. vérifier qu’on arrive bien à afficher ce message lorsqu’on appelle l’exécutable avec les bonnes options (on a besoin que ça plante avec un message d’erreur si on n’a pas fourni le path du fichier)

Warning
Il y aura probablement un warning de compilation qui dit qu’on ne fait rien du paramètre cfg mais on va l’ignorer pour l’instant.

2. Récupérer les messages de télémétrie (approche 1)

On va faire une 1ère implémentation sans utiliser/modifier la lib, en gérant tout dans la fonction stats (pas grave si ça fait de la duplication de code avec le reste de la lib ou du programme).

C’est parti :

  1. il faut ouvrir le fichier en utilisant la méthode open de File (un objet de type File = un handle pour manipuler le fichier)

  2. on crée un BufRead du File (pas obligatoire mais ça permet, grâce à un buffer, de décorréler les I/O réelles du traitement qu’on va faire des données issues du fichier)

  3. on utilise la méthode lines du BufRead pour obtenir une chaîne de caractères d’une ligne du fichier ou une erreur ; exemple d’utilisation ici (on boucle sur les lignes ; si on a autre chose d’un Ok(String), alors on ne fait rien et probablement que la boucle s’arrêtera là vu que c’est la fin du fichier)

Tip
En faisant un println!("{}", &la_chaine_de_la_ligne_en_cours) dans la boucle, on devrait voir apparaitre chaque ligne du fichier dans stdout.
Caution
Je conseille de tester avec un record qui est petit pour réduire le spam ^^

Maintenant qu’on a chaque ligne, on peut les traiter :

  1. on a déjà la crate base64 dans le projet (voir Cargo.toml)

  2. pour chaque ligne, on va utiliser la méthode decode pour obtenir la liste d’octets (Vec<u8>) correspondants ; decode renvoie un Result<Vec<u8>, DecodeError> car bien sûr ça peut échouer (si on lui donne une chaîne qui n’est pas du base64), mais on va appeler .unwrap() sur ce Result pour extraire le résultat (ça implique que si c’est une erreur, le programme va panic, mais c’est OK ici ; c’est facile à observer si on passe un fichier texte quelconque au programme)

  3. on peut ensuite vérifier que ça marche en affichant les octets de chaque ligne avec println!("{:?}", &le_vec_des_octets) (le {:?} dans le pattern indique qu’on veut l’affichage en mode debug du Vec<u8> ; avec {} on serait obligé de lui expliquer comment transformer le Vec<u8> en String)

  4. enfin, on va agréger chaque ligne dans un seul vecteur : on le crée en dehors de la boucle avec let mut buffer = Vec::new() et à chaque itération, on peut ajouter les octets de la ligne avec buffer.append(&mut le_vec_des_octets)

Tip
Pour tester sans spammer stdout, on peut retirer les println!() qui servaient au debug jusque là et en ajouter un après la boucle qui affiche le nombre d’éléments (d’octets ici) de notre buffer (voir Vec.len()). Normalement ça doit être plus petit que le poids du fichier lu car le base64 implique un overhead.

Ensuite, on va pouvoir analyser ces octets. La démarche est la suivante :

  1. on donne une référence à notre buffer d’octets en appelant crate::parsers::parse_telemetry_message

  2. il y a 3 résultats possibles :

    • le parser a réussi à trouver un message au début du buffer : on obtient un Ok qui contient un tuple (reste_du_buffer, message) ; on va dire que le buffer est maintenant égal à reste_du_buffer (= on retire les octets qui ont été parsés) et on peut faire ce qu’on veut avec le message

    • le parser renvoie une erreur de type Err(nom::Err::Incomplete(_)) : ça partait bien mais il manque des octets pour avoir un message complet ; ici pas besoin de traiter ce cas vu qu’on a récupéré tous les octets dans notre buffer, on n’aura rien de plus

    • le parser échoue et renvoie un Err(e) : la stratégie ici est de retirer uniquement le 1er octet du buffer (buffer.remove(0)) et de ré-essayer

  3. avec ce fonctionnement, le buffer va se vider progressivement (on retire les octets parsés et les octets en erreur) ; on peut donc boucler jusqu’à ce qu’il soit complètement consommé (while !buffer.is_empty() { …​ })

  4. on va pouvoir également créer un Vec<TelemetryMessage> avant cette boucle, et le remplir avec mon_vec.push(un_message_de_telemetry) à chaque fois qu’on parse un message ; après la boucle, ce vecteur contiendra tous les messages qui ont été trouvés

Enfin, on va pouvoir calculer nos stats :

  1. on crée une variable (initialisée à 0) pour chaque type de message

  2. on boucle sur le vecteur de messages (for message in vecteur { …​ }), et grâce à un bloc de pattern matching, on va déterminer pour chaque message quelle variable incrémenter

  3. après la boucle, on affiche les comptes !

3. Calculer la durée totale théorique

Lorsqu’on rejoue les messages (avec le mode play), on va faire artificiellement une pause après chaque message restitué :

  • après un StoppedMessage, on attend 100 ms

  • après un DataSnapshot, on attend 10 ms

  • après n’importe quel autre message, on attend 0 ms

On va donc créer une fonction compute_duration qui prend en paramètre le Vec<TelemetryMessage> et va renvoyer le nombre de ms nécessaires à la lecture de ce fichier.

C’est l’occasion d’écrire quelques tests unitaires pour vérifier que compute_duration fait son job :

  1. si on lui donne un vecteur vide (vec![]), elle renvoie 0

  2. si on lui donne un vecteur qui contient quelques messages, elle renvoie le résultat attendu

Ensuite, on va appeler cette fonction à la fin de stats() pour afficher le résultats (en secondes).

4. Récupérer les messages de télémétrie (approche 2)

Normalement à ce stade tout fonctionne bien mais il y a quelques limitations :

  • on est obligé de monter tout le fichier en mémoire

  • on a déjà une fonction gather_telemetry_from_file dans la lib et donc on a dupliqué du code

On va donc refactorer stats() pour éviter ça !

La bonne nouvelle c’est que gather_telemetry_from_file ne monte pas le fichier complet en mémoire. Pour l’utiliser, le plus simple est d"étudier la fonction play :

  1. on crée le handle du fichier à lire

  2. on crée une paire de Sender/Receiver ; c’est un mécanisme qui permet la communication entre plusieurs threads (plusieurs producers, un seul consummer)

  3. on crée un thread dans lequel on appelle gather_telemetry_from_file ; on lui passe le File et le Sender qui sont alors moved dans le thread : impossible par la suite d’utiliser ces variables, on les a données au thread

  4. on boucle à l’infini et à chaque itération on va essayer de récupérer un message dans le channel grâce au Receiver :

    • s’il y a un message, c’est cool on peut l’utiliser

    • s’il n’y a rien, on attend un peu et on reboucle (si on n’attend pas ça marche mais consomme du CPU pour rien)

    • si le channel est cassé, on stoppe le programme ; cause : le thread a terminé son exécution (le fichier a été lu ou alors le thread a panic)

Avec ceci en tête, on peut maintenant faire un mélange entre le code de play() et le code qu’on a écrit pour les stats pour avoir le même résultat qu’avant mais en utilisant un thread et gather_telemetry_from_file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment