Journal TapTempo en PHP

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
2
12
mar.
2018

TapTempo, c'est plus ce que c'était, on a vu a de tout, mais pas encore de PHP, redonnons lui sa gloire.

J'ai écrit plusieurs versions différentes de ce code, mais je vais seulement en donner une, que je trouve intéressante pour plusieurs raisons.

Déjà, le PHP en lui même donne un code plutôt lisible, et sans surprise, ensuite:

  • PHP n'a pas de types de liste avancés (ou peu) dans sa bibliothèque standard, j'ai opté ici pour une implémentation très primitive d'un ring buffer pour compter les entrées. Aussi bizarre que ça puisse paraître, je pense pas qu'on puisse faire plus performant (cf. la classe TapRing dans le code),
  • la magie de la fonction overwriteLine() que vous pouvez admirer, hideuse mais fonctionnelle (merci au composant console de Symfony qui m'a donné cette solution),
  • le trick bien hideux qui va avec la ligne \system("stty -icanon"); trouvé au hasard sur Stack Overflow (une méthode plus native existe, je n'ai juste pas pris le temps de la copier-coller l'implémenter).

Maintenant passons aux points positive, avec quelques évolution récentes du language :

  • l'utilisation de declare(strict_types=1); qui rend le typage strict (et oui, PHP a ça maintenant, malheureusement, ce n'est pas statique mais reste dynamique, à savoir que c'est évalué à l’exécution et non à la compilation),
  • il en va de même pour le typage utilisé dans l'ensemble du code,
  • bien entendu, le code vit dans son propre namespace, d'où les \ précédent les appels de fonctions de la bibliothèque standard qui vivent dans le namespace global,
  • et puis c'est tout, mais c'est pas si mal.

Roulements de tambours, voici le code:

#!/usr/bin/env php
<?php

declare(strict_types=1);

namespace TapTempo;

/**
 * Primitive ring buffer implementation
 */
final class TapRing
{
    const MAX_SIZE = 30;
    const VALIDITY_THRESHOLD = 5; // Seconds

    private $buffer = [];
    private $index = 0;
    private $thresold;

    /**
     * Default constructor
     *
     * @param int $validityThreshold
     *   Lower is the number, more precise will be the BPM calculation,
     *   nevertheless you don't want to miss any taps, so default value
     *   should be OK for slow typers.
     */
    public function __construct(int $validityThreshold = self::VALIDITY_THRESHOLD)
    {
        $this->thresold = $validityThreshold;
    }

    public function tap(): void
    {
        $this->buffer[$this->index] = \microtime(true);
        $this->index = ($this->index + 1) % self::MAX_SIZE;
    }

    public function getTempo(): int
    {
        $now = microtime(true);
        $first = $last = 0;

        $count = 0;
        foreach ($this->buffer as $value) {
            if (!$value) {
                continue; // Unused buffer position
            }
            if ($value < $now - $this->thresold) {
                continue; // This value is too old to be used
            }

            if (!$first || $value < $first) {
                $first = $value;
            }
            if (!$last || $value > $last) {
                $last = $value;
            }
            ++$count;
        }

        if ($count > 2) {
            return (int)\round($count / ($last - $first) * 60, 0);
        }

        return 0;
    }

    public function reset(): void
    {
        $this->buffer = [];
        $this->index = 0;
    }
}

function overwriteLine(string $message): void
{
    print "\x0D"; // Move the cursor to the beginning of the line
    print "\x1B[2K"; // Erase the line
    print str_repeat("\x1B[1A\x1B[2K", 1); // Erase previous line
    print $message;
}

/**
 * Main loop
 */
function loop(): void
{
    $buffer = new TapRing();
    do {
        // fread() would to the trick too
        $char = \stream_get_contents(STDIN, 1);

        if ('q' === $char) {
            print "\n";
            break;
        } else if (10 === \ord($char)) { // 10 is Enter key ASCII code
            $buffer->tap();
        }

        overwriteLine(\str_pad((string)$buffer->getTempo(), 10, " ", STR_PAD_LEFT));
    } while (true);
}

print "Appuyez sur la touche entrée en cadence (\"q\" pour quitter)\n";

// Only hack in this file, please read:
//   https://stackoverflow.com/a/3684565
// @todo write it using https://stackoverflow.com/a/21628935 instead
\system("stty -icanon");

loop();

Et pour les barbus, l'OPCode généré pour la fonction TapTempo::tap():

function name: tap
L33-37 TapTempo\TapRing::tap() /home/pounard/taptempo/vanilla.php - 0x7f084c088000 + 14 ops
 L35   #0     FETCH_OBJ_R             THIS                 "index"              @1                  
 L35   #1     INIT_FCALL<1>           96                   "microtime"                              
 L35   #2     SEND_VAL                true                 1                                        
 L35   #3     DO_ICALL                                                          @3                  
 L35   #4     FETCH_OBJ_W             THIS                 "buffer"             @0                  
 L35   #5     ASSIGN_DIM              @0                   @1                                       
 L35   #6     OP_DATA                 @3                                                            
 L36   #7     FETCH_OBJ_R             THIS                 "index"              @5                  
 L36   #8     ADD                     @5                   1                    ~6                  
 L36   #9     FETCH_CLASS_CONSTANT                         "MAX_SIZE"           ~7                  
 L36   #10    MOD                     ~6                   ~7                   ~8                  
 L36   #11    ASSIGN_OBJ              THIS                 "index"                                  
 L36   #12    OP_DATA                 ~8                                                            
 L37   #13    RETURN<-1>              null                 
  • # Ou pas

    Posté par  (site web personnel) . Évalué à 9.

    TapTempo, c'est plus ce que c'était, on a vu a de tout, mais pas encore de PHP, redonnons lui sa gloire.

    https://linuxfr.org/users/napin/journaux/portage-de-taptempo-en-php

    • [^] # Re: Ou pas

      Posté par  (site web personnel) . Évalué à 6.

      Tant mieux si l'auteur n'était pas au courant de l'autre version comme ça c'est une deuxième approche avec le même langage, ça reste intéressant à lire !

      D'ailleurs la première version avait des soucis avec l'entrée standard, est-ce que l'utilisation de $char = \stream_get_contents(STDIN, 1); permet d'éviter les mêmes écueils ?

      • [^] # Re: Ou pas

        Posté par  . Évalué à 5.

        Il utilise le même hack que j'avais proposé pour corriger le soucis du buffering de STDIN : stty -icanon .

        Et oui je trouve intéressant de voir une 2e implémentation, ça m'a permis de découvrir "la magie de la fonction overwriteLine()" et une implémentation différente du ring buffer.

        Petit bug : Pas de fuite mémoire dans celle là, par contre si on utilise un autre touche que Entrée, il efface les lignes précédentes du terminal !

        • [^] # Re: Ou pas

          Posté par  (site web personnel) . Évalué à 2. Dernière modification le 13 mars 2018 à 09:11.

          Pas de fuite mémoire dans celle là

          En théorie c'est facile de faire du PHP sans fuite mémoire, faut éviter les static et les global (EDIT: et fgets apparament).

          par contre si on utilise un autre touche que Entrée, il efface les lignes précédentes du terminal

          Oui en effet, j'ai pas pris le temps de donner un peu d'amour à ce code !

          • [^] # Re: Ou pas

            Posté par  . Évalué à 0.

            En théorie c'est facile de faire du PHP sans fuite mémoire, faut éviter les static et les global

            Moi en pratique j'y arrive : je ne code pas e PHP.

      • [^] # Re: Ou pas

        Posté par  (site web personnel) . Évalué à 2. Dernière modification le 13 mars 2018 à 09:12.

        Le soucis avec l'entrée standard en PHP, c'est que tant que tu n'as pas de line feed, les fonctions de lecture du flux ne te donnent rien. Je t'avoue que je n'ai pas réellement pris le temps de chercher pourquoi la réponse est dans le commentaire lié plus haut: stty -icanon désactive le buffering de STDIN, j'image qu'il est bufferisé pour des raisons de performance à l'origine (quand on lit le thread Stack Overflow là: https://stackoverflow.com/questions/3684367/php-cli-how-to-read-a-single-character-of-input-from-the-tty-without-waiting-f/3684565#3684565 on se rend compte que c'est bien dans le cas des remote terminals). Dans l'absolu, fread() et stream_get_contents() ici sont équivalents (j'ai testé les deux).

        • [^] # Re: Ou pas

          Posté par  . Évalué à 3.

          la réponse est dans le commentaire lié plus haut: stty -icanon désactive le buffering de STDIN, j'image qu'il est bufferisé pour des raisons de performance à l'origine

          C'est le fonctionnement canonique (d'où le nom de l'option ;-) de l'entrée standard d'un terminal dans les systèmes Unix. man termios pour de plus amples informations (en désactivant la fonction echo, l'entrée standard ne renvoie pas ce qu'elle reçoit sur la sortie standard, par exemple).

          Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

        • [^] # Re: Ou pas

          Posté par  . Évalué à 5. Dernière modification le 13 mars 2018 à 10:46.

          Cela étant tu devrais peut être faire un appel à stty icanon avant de quitter ton programme, pour remettre le terminal dans sa configuration initiale.

          Pour l'option echo, tu peux jouer avec cette commande dans ton terminal :

          $ stty -echo

          Attention : après on ne voit plus à l'écran ce que l'on tape (pratique pour la saisie d'un mot de passe ;-), il faut taper à l'aveugle la commande :

          $ stty echo

          pour remettre les choses en ordre. :-)

          Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

        • [^] # Re: Ou pas

          Posté par  . Évalué à 2.

          Après réflexion, vu les contraintes du programme, le mieux serait sans doute de faire :

          stty raw -echo # raw mode plus générique que-icanon, et sans echo sur la sortie standard

          avant de rentrer dans la boucle, et :

          stty sane
          

          pour rétablir le terminal dans un état sain avant de quitter le programme (cf man stty pour les explications).

          Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

    • [^] # Re: Ou pas

      Posté par  (site web personnel) . Évalué à 2.

      Ah c'est marrant, je suis pourtant remonté dans mon historique je l'avais pas trouvé !

  • # Shame on you.

    Posté par  . Évalué à -10.

    Mec, tu devrais avoir honte.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.