Aller au contenu

Tests de performance

Phasis est environ cent fois plus lent que V8 pour les charges JavaScript limitées par la distribution. Ce plafond est le coût inhérent d’un interpréteur hébergé par PHP par rapport à un compilateur JIT natif. Pour un intégrateur, la question n’est pas « comment se compare-t-il à V8 ? », mais « est-il assez rapide pour ce que je fais ? ». Pour la plupart des usages embarqués, la réponse est oui.

Quand le plafond importe peu

  • Génération de modèles — rendu SSR d’une page typique en moins de 10 ms.
  • Règles de validation — le JS fourni par l’utilisateur pour valider un formulaire s’exécute en quelques microsecondes.
  • Transformations de données — array.map(transform) sur des centaines d’éléments s’effectue en quelques dizaines de millisecondes.
  • Exécution en bac à sable — les scripts courts fournis par l’utilisateur avec une limite hôte d’une seconde laissent une grande marge.

Quand le plafond importe

  • Boucles internes gourmandes en calcul — traitement d’images, réductions de grands tableaux, simulations. Déplacez les calculs vers une bibliothèque native PHP et exposez-la comme fonction hôte.
  • Animation en temps réel — tout ce qui vise 60 fps. Cette logique JS doit se trouver côté client (navigateur, application native), pas dans le moteur intégré.
  • Réimplémentations en JS d’algorithmes coûteux en calcul — cryptographie, compression, analyse syntaxique. PHP propose des équivalents natifs 1000× plus rapides ; utilisez-les.

Le bon modèle mental : Phasis est une couche de liaison pour la logique de type JS intégrée à des applications de type PHP. L’hôte PHP gère les calculs ; la couche JS gère la configuration, les branchements et les petites transformations.

Ce qui rend le moteur rapide (par rapport à un parcours d’arbre naïf)

  • VM à bytecode — la plupart des traitements de dispatch à chaud passent par le switch d’opcodes de VM::execute, et non par l’AST.
  • Pile d’appels personnalisée — chaque appel JS conserve son état dans VM::execute et sauvegarde la frame de l’appelant dans un pool ; aucune nouvelle frame PHP par appel JS. Voir VM à bytecode § Pile d’appels personnalisée.
  • Caches inline — un cache monomorphe par PC permet aux lectures de méthodes de prototype sur une instance (la forme c.method()) d’éviter le parcours de la chaîne de prototypes.
  • Générateurs à instantané de frame — les corps de générateur simples suspendent leur exécution en capturant l’état du dispatch dans un objet du tas, sans passer par une Fiber PHP.
  • Transpileur JsToPhp — les corps riches en calculs numériques ou en variables locales (y compris les lectures obj.method(), new Ctor() et this.member) sont compilés en closures PHP, que le JIT de traçage de PHP 8.5 convertit en code machine natif.
  • Cache JSON.stringify — JSON.stringify(o) met son résultat en cache sur l’objet d’entrée, avec invalidation transitive des versions, ce qui accélère l’idiome de clonage profond JSON.parse(JSON.stringify(o)).

Mesurer votre propre charge de travail

Shell
php bench/run.php

Cette commande exécute la suite de microbenchmarks fournie dans bench/microbench.js — quelques charges synthétiques (calculs dans des boucles, fonctions récursives, création d’objets, appels de méthodes de prototype, ajout à un tableau, aller-retour JSON, etc.) — puis affiche le temps mural médian de chaque test. Pratique pour vérifier que tout fonctionne ; les chiffres actuels sont enregistrés dans BENCH.md à chaque exécution des benchmarks de CI.

Pour tester votre propre code, remplacez le script :

Shell
php -d xdebug.mode=off bench/run.php path/to/your-script.js

Pour profiler au niveau PHP, le profileur de Xdebug fonctionne avec Phasis comme avec n’importe quelle application PHP. Exécutez un script sous xdebug.mode=profile, puis examinez la sortie de cachegrind pour voir quelles classes Phasis\ dominent.

Feuille de route

Voici, par ordre approximatif de faisabilité, les pistes qui réduiraient le plus l’écart de V8 :

  • Caches inline polymorphes — permettre à un même site d’appel LOAD_MEMBER de conserver en cache 2 à 4 formes avant de revenir au dispatch lent. Utile pour le code qui traite des récepteurs hétérogènes.
  • yield* et try-with-yield pour les générateurs à instantané de frame — ces formes passent actuellement par l’interpréteur arborescent basé sur Fiber. Chacune nécessite une gestion explicite par machine à états (transmission des itérateurs délégués, pile de gestionnaires conservée pendant la suspension) avant de pouvoir prendre en charge les instantanés.
  • Générateurs asynchrones et await sur les instantanés — la spécification combine la suspension des générateurs avec la file des microtâches. Une fois que le corps en bytecode prendra en charge yield et await, les générateurs asynchrones pourront eux aussi utiliser les instantanés.
  • Inférence des types de paramètres dans JsToPhp — les paramètres de fonction sont de type numérique par défaut, si bien que function (obj) { obj.method() } fait abandonner le compilateur. Déduire les paramètres de type objet à partir de leur utilisation dans le corps permettrait d’appeler des méthodes sur les arguments de fonction sans exiger que le récepteur soit créé localement via new.
  • Émission directe d’opcodes opcache — éviter l’étape eval() de JsToPhp en écrivant directement des opcodes au format opcache. C’est le chantier le plus important ; les autres peuvent être réalisés dans l’architecture actuelle.

Aucun de ces changements n’est nécessaire pour les charges de travail d’intégration visées par Phasis.