Less.js: La version 3.10.x utilise beaucoup plus de mémoire et est nettement plus lente que la 3.9.0

Créé le 12 sept. 2019  ·  95Commentaires  ·  Source: less/less.js

Nos builds ont récemment commencé à échouer car nous exécutons environ 80 builds Less en parallèle pendant le processus de build de notre projet et la nouvelle version de Less.js utilise tellement de mémoire que Node plante. Nous avons retracé le crash à la mise à niveau de Less.js de 3.9.0 à 3.10.3.

J'ai modifié notre script Less pour compiler les fichiers de manière séquentielle (création de 2 fichiers à la fois) et échantillonné l'utilisation de la mémoire de Node pendant le processus et j'ai obtenu les résultats suivants :

less graph

Less.js semble utiliser 130% de mémoire en plus et prend environ 100% plus de temps à compiler pour nous.

Je me demande simplement si vous avez comparé Less.js et si vous voyez des résultats similaires

Commentaire le plus utile

Ok l'équipe ! Je vais publier la version 3.x, et un peu plus tard, peut-être la semaine prochaine, publier la version 4.0. Les deux devraient maintenant avoir les correctifs de performances. Merci à tous ceux qui ont aidé au débogage !

Tous les 95 commentaires

C'est étrange. Quelle est votre version de Node ?

@PatSmuk360 Pourriez-vous tester et obtenir un profil de mémoire de 3.10.0 et voir s'il diffère ?

Nous utilisons la dernière version de 10 (10.16.3).

Instantané de tas avant :

image

Instantané de tas après :

image

J'ai également essayé sur Node 12.10.0 et cela semble bien pire, atteignant 587 Mo d'utilisation de la mémoire à un moment donné de la construction séquentielle.

Profil du processeur avant :
CPU-20190916T133934.cpuprofile.zip

image

Profil du processeur après :
CPU-20190916T134917.cpuprofile.zip

image

@PatSmuk360 Donc, le long et le court est que la différence entre ces versions est que la base de code a été transformée en syntaxe ES6. Ce qui n'est pas techniquement un changement décisif, c'est pourquoi ce n'était pas une version majeure.

MAIS... je soupçonne que certaines des conversions Babel pour des choses comme la syntaxe de propagation objet / tableau sont moins efficaces que les versions ES5 plus détaillées. C'est pourquoi je vous demandais si vous pouviez tester 3.10.0, car à l'origine j'exportais un package transpilé compatible avec Node 6 et versions ultérieures, mais cela a interrompu une intégration avec une bibliothèque particulière qui ne pouvait pas gérer la syntaxe de classe. Je suis donc descendu au nœud 4 sans construction de classe.

Si nous pouvons déterminer exactement quelle transformation ES5 ne fonctionne pas bien, nous pourrions théoriquement définir de manière plus granulaire ces paramètres d'exportation Babel sur une exportation plus performante.

@PatSmuk360 Incidemment, quelle est cette fonction split qui prend autant de temps ?

@matthew-dean Cela semble être le String.prototype.split . Si vous ouvrez le profil dans vos outils de développement Chrome, vous pouvez voir toutes les données codées par couleur. J'essaie de modifier le profil pour créer un lien vers https://cdn.jsdelivr.net/npm/[email protected]/dist/less.cjs.js comme source afin qu'il soit plus facile d'inspecter les goulots d'étranglement. Existe-t-il une carte source disponible pour le fichier *.cjs.js à *.js ? Le fichier https://cdn.jsdelivr.net/npm/[email protected]/dist/less.min.js.map semble mapper le fichier .min à la source ES6. Peut-être pouvons-nous fournir la carte source aux outils de développement afin que nous puissions déterminer où la transpilation provoque des goulots d'étranglement.

Cette ligne dans https://github.com/less/less.js/blob/cae5021358a5fca932c32ed071f652403d07def8/lib/less/source-map-output.js#L78 spécifique semble avoir un temps CPU élevé. Mais vu l'opération qu'il effectue cela ne me semble pas déplacé.

Je n'ai pas beaucoup d'expérience avec les profils de tas, mais ce qui me frappe, c'est l'augmentation du montant de (closures), (system), (array), system / Context . En essayant de lier cela au profil du processeur, il semble que l'augmentation de ces objets entraîne une augmentation massive du temps passé à ramasser les ordures.

@kevinramharak En général, la conversion d'un AST comme Less avec _n_ depth en un arbre de sortie sérialisé et aplati comme CSS implique de nombreuses créations d'objets temporaires. Donc, ce qui pourrait se passer, c'est qu'avec certaines transformations, cela ajoute une quantité supplémentaire de _x_ d'objets. Même temporairement, vous pouvez obtenir un effet exponentiel où chaque nœud ne crée que 2 à 3 fois plus d'objets, multiplié par le nombre de nœuds multiplié par le nombre de fois où il doit aplatir les règles... Je pouvais le voir s'additionner. Nous étions probablement naïfs en général en pensant essentiellement à la syntaxe ES6 comme au sucre essentiellement syntaxique de la syntaxe ES5. (Les développeurs JavaScript sont probablement coupables de cela en général.) En réalité, transpiler une syntaxe plus récente peut créer des modèles ES5 qui ne sont pas très performants. Pour 99% des développeurs, ce n'est pas grave car ils n'itérent pas ce code des centaines ou des milliers de fois par seconde. Mais c'est ma supposition sur ce qui se passe, car il n'y a pas eu d'autres changements majeurs.

@kevinramharak Re: les lignes source - c'est parce que l'analyseur Less d'origine ne suit pas les lignes / colonnes d'entrée, donc lorsque le mappage de la source a été ajouté, il a fallu essentiellement fragmenter l'entrée en lignes pour déterminer comment elle devait être mappée à l'original la source. Ce ne sera pas un problème dans la version 4.x+, mais cela explique pourquoi il y passerait beaucoup de temps maintenant.

J'utilise less.min.js dans un navigateur et 3.10.3 est deux fois plus lent que 2.7.3 que j'utilisais auparavant. À la fois dans Chrome et Firefox.

@PatSmuk360 Pouvez-vous consulter cette branche ? https://github.com/matthew-dean/less.js/tree/3.11.0

En bref, la transpilation de Babel vers ES5 est assez horrible et utilise des tonnes d'appels Object.defineProperty pour transpiler des classes. J'ai basculé la transpilation vers TypeScript, qui a une sortie beaucoup plus saine de prototypes de fonction.

Tout va bien, mais après cela, les tests du navigateur de Less ne fonctionneront pas car il utilise le très ancien et obsolète PhantomJS, et jusqu'à présent, personne (y compris moi) n'a réussi à migrer les tests hors de PhantomJS sur Headless Chrome.

Mais, un problème à la fois : si la source dist sur cette branche n'a pas les problèmes de mémoire, alors peut-être que nous pourrions nous attaquer au désordre qui est le test du navigateur.

J'ai réussi à migrer les tests de navigateur Less vers Headless Chrome, mais en termes de performances/stabilité, j'ai besoin de beaucoup de retours des utilisateurs avant de pouvoir fusionner en toute sécurité, en raison du changement complet du pipeline de transpilation de Babel à TypeScript.

Les branches actuelles peuvent être trouvées ici : https://github.com/less/less.js/pull/3442

Toujours 2x plus lent que 2.7.

@alecpl C'est une bonne information, mais je veux vraiment savoir si 3.11.0 est une amélioration par rapport à 3.10.

La chose étrange à ce sujet est que, _en théorie_, le code Less d'origine a été converti avec Lebab, ce qui devrait être le contraire de Babel. Ce qui signifie que ES5 -> ES6 -> ES5 devrait être presque identique, mais ce n'est évidemment pas le cas. Je vais donc devoir enquêter (à moins que quelqu'un d'autre n'ait le temps, ce qui est un support bienvenu) à quel point le code ES6-> ES5 diffère du code ES5 d'origine.

@alecpl

J'ai donc effectué divers tests et passé du temps à effectuer des analyses comparatives de 3.11.0 contre 3.10.3 contre 3.9.0 contre 2.7.3 dans Headless Chrome

Je n'ai trouvé aucune preuve que le compilateur Less soit plus lent pour n'importe quelle version, et encore moins 2x plus lent. Il pourrait toujours être vrai que 3.10.0 a plus de mémoire en raison des paramètres de transpilation, et peut-être que si un système était limité en espace et faisait plus d'échanges de mémoire ou plus de GC en conséquence, cela pourrait entraîner un ralentissement ? Mais je ne peux pas le vérifier.

Vous pouvez vous tester en exécutant grunt benchmark sur la branche 3.11.0. Ils peuvent rapporter des nombres différents sur une course individuelle, mais si vous courez suffisamment de fois, vous devriez voir que les temps sont à peu près égaux. Donc je ne sais pas où vous obtenez vos données.

@PatSmuk360 Avez-vous pu tester la surcharge mémoire de 3.11.0 ?

Je ne construis pas votre code moi-même. Je n'utilise pas nodejs. Je prends juste le fichier less.min.js du dossier dist pour deux versions différentes que j'ai mentionnées et les utilise sur ma page. Ensuite, dans la console, je vois les horaires imprimés par moins de code. Mon code moins utilise plusieurs fichiers et le fichier de sortie contient environ 100 Ko de css minifié. C'est une référence dans la "vraie vie". Je parle du code Roundcube .

Chrome est beaucoup plus rapide que Firefox, mais dans les deux cas, la différence entre les versions est similaire.

@alecpl Hmm... c'est peut-être une différence particulière pour ce code Less particulier. C'est vrai que le repère est arbitraire. Si j'ai le temps, j'ajouterai ces fichiers Less à l'analyse comparative.

Ce problème a été automatiquement marqué comme obsolète car il n'a pas eu d'activité récente. Il sera fermé si aucune autre activité ne se produit. Merci pour vos contributions.

Je voulais juste poster une mise à jour rapide : j'ai essayé 3.11.1 et c'était aussi lent que 3.10.3. Je vais voir si je peux concocter une référence représentative pour tester/profiler cela.

J'ai mis à niveau vers 3.11.1 et j'ai maintenant les mêmes problèmes de consommation de mémoire.
Un projet de taille modeste nécessite environ 600 Mo de RAM pour être construit via webpack et less-loader .

J'ai pris un calendrier d'allocation de tas résultant en cela;

heap-timeline

Quelque chose cause d'énormes allocations folles maintenues en vie par Ruleset .


[ÉDITER]
Je suis en train de rétrograder à 3.9 et de récupérer une chronologie d'allocation de tas à partir de cela. Je vais voir si je repère quelque chose de radicalement différent.

@matthew-dean
Trouvé un indice pour vous.

Dans 3.11, l'un des éléments de rétention répertoriés pour RuleSet est ImportManager.
En 3.9, ce n'est _pas le cas_.

Si tout est maintenu en vie par ImportManager et que ImportManager est un singleton tout au long du processus de compilation. Hé bien oui; cela augmenterait considérablement l'utilisation de la mémoire car rien ne peut être récupéré. Pas même des ensembles de règles intermédiaires.

@rjgotten Hmm...... si un objet a une référence à un autre objet, pourquoi cela empêcherait-il GC? Je veux dire, techniquement, tous les objets conservent des références aux nœuds d'API publics via la chaîne de prototypes. Cela n'empêcherait GC que si l'inverse est vrai, c'est-à-dire qu'un objet conserve des références à chaque instance de l'ensemble de règles.

Vous semblez avoir mal compris.

Lorsque « A est une retenue de B », cela signifie que A conserve des références à B qui empêchent la récupération de place de B. Ainsi, lorsque j'ai écrit ImportManager est répertorié comme un élément de retenue de l'ensemble de règles, j'ai exprimé exactement ce que vous concluez également : ImportManager contient des références aux instances de l'ensemble de règles, ce qui empêche le GC de ces instances de l'ensemble de règles.

@rjgotten Oh c'est le cas ? Je me demande comment ce changement est arrivé là. Il est fort possible que je sois à blâmer, ou il y a quelque chose à propos du refactor ES6 qui l'a fait. Mais oui, cela pourrait certainement le faire! Merci d'avoir enquêté !

Bon, qu'en est-il de cette idée radicale.

J'ai créé une branche avec tout cueillir SAUF la conversion ES6. Ce n'était pas simple, et cela nuirait à tout ce travail, mais si un fichier Babelified / Typescript ne peut pas surpasser le JS natif existant, cela n'en vaut tout simplement pas la peine.

Je n'ai aucune idée de comment nous allons réconcilier l'historique de git, mais voici la branche -> https://github.com/less/less.js/tree/release_v3.12.0-RC1. Nuking la conversion est un gros problème, donc je pense que cette branche aurait une véritable référence solide à comparer.

Je me demande comment ce changement est arrivé là. Il est fort possible que je sois à blâmer, ou il y a quelque chose à propos du refactor ES6 qui l'a fait. Mais oui, cela pourrait certainement le faire! Merci d'avoir enquêté !

C'est certainement bizarre. D'après ce que j'ai pu déchiffrer, il semble qu'ImportManager finisse en quelque sorte par un dispositif de retenue pour les ensembles de règles via le gluecode/polyfill ajouté par la conversion ES6.

Je me demande s'il y a une solution ici qui se termine à mi-chemin. C'est-à-dire conserver la version ES6 pour Node.js mais également avoir une cible de navigateur transpilée. Ce serait une perte énorme de supprimer la clarté du code ajoutée par la conversion ES6. ??

@rjget

Ce serait une perte énorme de supprimer la clarté du code ajoutée par la conversion ES6.

Ce n'est peut-être pas aussi grave qu'il y paraît, pour deux raisons :

  1. La configuration de cumul existe toujours et peut être extraite d'un commit si quelqu'un souhaite la rechercher.
  2. Je travaille activement sur un Less 4.0 basé sur TypeScript depuis un certain temps. Je préfère passer mon temps là-dessus plutôt que de traquer cette régression des performances. C'est un refactor de fond, donc ce n'est pas du tout proche, mais de par la nature de TS, il est peu probable qu'il y ait ces mutations d'objets uniques qui conservent les références et empêchent la GC. Et le code est beaucoup plus clair à suivre . Alors il y a ça.

Il semble un peu lourd de tout ramener à ES5 juste pour résoudre ce problème, mais c'est compréhensible étant donné qu'une réécriture est en cours. Pourtant, nous devrions considérer combien de temps nous aurions à vivre avec cette décision. Comme @rjgotten l'a mentionné, nous

Y a-t-il quelqu'un qui a vécu cela et qui serait prêt à partager son code ? Ce serait formidable si nous pouvions comparer à l'aide d'un projet du monde réel afin que nous puissions être sûrs que les changements aident ou non. Si nous pouvions obtenir cela, je serais prêt à aider à essayer de réduire le goulot d'étranglement.

@matthew-dean, en passant, avez-vous essayé de compiler avec Babel en mode libre pour voir s'il produisait un code plus sain?

@seanCodes Je suis tout à fait pour ne pas prendre l'option nucléaire, si quelqu'un a le temps d'étudier comment / pourquoi la sortie de Rollup / Typescript fonctionne moins bien et a ces effets de mémoire supplémentaires. Il s'agirait en partie d'examiner le code d'origine, de le comparer à la sortie et de rechercher des indices.

Une hypothèse que j'ai est qu'une partie de la logique de déstructuration a beaucoup de passe-partout de création d'objets.

Fondamentalement, quelqu'un devrait se porter volontaire pour s'approprier ce problème.

@matthew-dean, en passant, avez-vous essayé de compiler avec Babel en mode libre pour voir s'il produisait un code plus sain?

@seanCodes Le code est compilé en utilisant TypeScript, pas Babel. Mon idée était d'ajouter progressivement des types JSDoc pour renforcer la vérification de type, mais le TS en v4 est une réécriture suffisante, je ne sais pas si cela est nécessaire. Ainsi, quelqu'un pourrait expérimenter avec Babel v. TypeScript (et différents paramètres pour chacun) pour voir si Babel produit un code plus performant. Soyez simplement très conscient de ce problème, qui a été causé par la production initiale d'une version non ES5 pour Node.js : https://github.com/less/less.js/issues/3414

@seanCodes De plus, j'ai toujours du mal à VÉRIFIER la différence de performance. Personne n'a produit de PR / étapes pour PROUVER, définitivement, la différence de performance. Il y a un certain nombre d'anecdotes dans ce fil, mais sans code ni étapes reproductibles, je ne sais pas comment quelqu'un enquête sur cela. Idéalement, il y aurait un PR qui lancerait un outil de profilage (via le débogueur Chrome ou autre) pour sortir un nombre sur un système, en faisant la moyenne sur un certain nombre de tests. Donc, parce que cela, jusqu'à présent, n'est pas reproductible à 100%, dans la mesure où quelqu'un a pu proposer des étapes de reproduction, c'est en partie pourquoi je ne voulais personnellement pas aller dans ce terrier de lapin. (En d'autres termes, les commentaires sur "J'ai utilisé le débogueur Chrome" ne sont pas un ensemble d'étapes de reproduction. Il est utile de le savoir, mais cela n'aide personne à enquêter. Nous devons savoir ce que nous suivons et pourquoi / quel est le résultat attendu.)

Donc, si les gens veulent améliorer les performances en utilisant la base de code actuelle, il faudra probablement plusieurs volontaires et une personne pour s'approprier ce problème.

Personnellement, je n'ai pas vu beaucoup de différence de performances comme dans _speed_ mais la différence de consommation de mémoire est étonnante et semble être en corrélation avec la quantité d'importations, ce que mon analyse rapide du tas semble suggérer comme étant également liée au problème.

Si c'est correct, ~vous~ n'importe qui devrait être capable de créer un projet de test viable tant qu'il consiste en un grand nombre de fichiers importés contenant chacun des ensembles de règles, pour voir également la différence.

@rjget

Si cela est correct, vous devriez être capable de créer un projet de test avec de nombreux fichiers importés, chacun contenant des ensembles de règles et de voir également la différence.

Le « vous » est le point important ici. ??

Le « vous » est le point important ici. ??

Malheureux choix de mots. Je veux dire que dans le sens général - c'est-à-dire "n'importe qui".
Je creuserais moi-même plus loin, mais j'ai déjà trop d'assiettes qui tournent actuellement.

@rjgotten Pouvez-vous me donner une meilleure idée de ce que pourraient être "beaucoup de fichiers importés" ? Est-ce que 100 fichiers séparés le feraient ou parlons-nous de 1000 ?

De plus, comment avez-vous remarqué le problème pour la première fois ? Une compilation a-t-elle échoué en raison d'une consommation de mémoire ou avez-vous examiné l'utilisation de la mémoire ? Ou avez-vous remarqué que votre machine ralentissait ?

Je n'ai pas encore essayé de reproduire cela, mais j'espère toujours savoir exactement ce qu'il faut rechercher afin de ne pas avoir à recourir à des devinettes et à des vérifications.

100 environ le feraient. C'est à peu près la taille du projet réel sur lequel j'ai remarqué le problème.

Je l'ai remarqué pour la première fois lorsqu'une version exécutée sur notre environnement CI d'entreprise a commencé à échouer. Nous les exécutons dans des conteneurs Docker avec des restrictions de mémoire configurées.

@rjgotten Avons-nous toujours le problème de mémoire sur 3.11.3 ? J'ai supprimé toute mise en cache (référencement) de l'AST dans les importations dans une version précédente, donc si les importations étaient suspendues et que les arbres AST y étaient conservés, cela augmenterait l'utilisation de la mémoire, mais si nous supprimons les arbres, est-ce que ça le résout ?

@matthew-dean Oui, le problème est toujours présent sur 3.11.3.

Je vais essayer de créer une preuve de concept mais j'ai du pain sur la planche. J'ai mis cela sur ma liste de choses à faire une fois que j'aurais un peu de temps supplémentaire.

@matthew-dean Je souhaite tester cela sur un projet relativement important, qui échouait auparavant. Quelque chose que j'ai besoin de savoir ? Dois-je utiliser cette branche, https://github.com/less/less.js/tree/release_v3.12.0-RC1 ?

@nfq Vous pouvez l'essayer, mais la sagesse qui prévaut dans ce fil n'est pas de faire exploser la conversion ES6, ce que fait cette branche, mais plutôt d'obtenir plus d'informations pour diagnostiquer correctement le problème.

Incidemment, j'ai essayé d'inspecter l'objet less pendant la compilation pour tenter de trouver des objets persistants, et je n'en ai trouvé aucun. 🤷‍♂️

Je rencontre également des problèmes de performances. Pour une même suite de tests :

| Versions | Temps |
| -- | -- |
| v3.9.0 | ~1.6s |
| v3.10.0~v3.11.1 | ~3.6s |
| v3.11.2+ | ~12s |

En dehors de 3.9.0 → 3.10.0 discuté ici, il semble y avoir une dégradation significative des performances à v3.11.2 . Ce changement dans le changelog semble suspect :

3498 Supprimer la mise en cache de l'arborescence dans le gestionnaire d'importation (#3498)

Pareil pour moi.

Timings pour les builds identiques (tous sur le nœud 10.19.0) :

  • v3.9 : 29,9 secondes
  • v3.10 : 76,0 secondes
  • v3.12 : 89,3 secondes

@jrnail23

Avez-vous un dépôt qui peut démontrer ces résultats ?

@matthew-dean j'en ai un pour https://github.com/less/less.js/issues/3434#issuecomment -672580467 : https://github.com/ecomfe/dls-tooling/tree/master/packages/ less-plugin-dls (c'est un monorepo, qui inclut un plugin Less.)

Désolé, @matthew-dean, je ne le fais pas. Ces résultats proviennent du produit de mon employeur.

Ces résultats proviennent du produit de mon employeur.

Même raison pour laquelle je ne peux fournir aucun fichier non plus. ??

@rjgotten @jrnail23 @Justineo - Juste curieux, avez-vous un certain nombre de cas où le même import est importé plusieurs fois dans différents fichiers ?

Dans mon cas, nous avons un plugin qui injecte une instruction @import et fournit un tas de fonctions personnalisées. Le fichier importé importe d'autres parties du projet qui produira finalement plus de 1000 variables de moins.

@matthew-dean, nous avons un fichier base.less qui importe des choses courantes (par exemple, les variables de couleur, la typographie, etc.).
Une analyse rapide de notre application montre qu'une référence base.less (c'est-à-dire <strong i="8">@import</strong> (reference) "../../../less/base.less"; ) est importée par 66 autres fichiers less spécifiques à base.less ).
Fait intéressant, nous avons apparemment aussi un autre fichier less qui (probablement accidentellement ?) s'importe lui-même comme référence.

@rjgotten @jrnail23 @Justineo - Juste curieux, avez-vous un certain nombre de cas où le même import est importé plusieurs fois dans différents fichiers ?

Pour moi, c'est un « oui ». Le projet sur lequel j'ai rencontré des problèmes pour la première fois est une solution personnalisable qui utilise un fichier de variables centralisé qui est inclus dans tous les autres. De plus, nous utilisons une conception basée sur des composants où nous avons des usines de mixage qui produisent par exemple certains types de boutons, des icônes de police, etc. et des parties de ceux-ci sont également importées dans plusieurs fichiers.

Essentiellement; chaque dépendant est configuré pour importer strictement ses dépendances et nous comptons sur le compilateur Less pour dédupliquer et séquencer les importations dans le bon ordre et assurer une sortie CSS correcte.

@rjget

Je l'ai posté et supprimé (parce que je pensais l'avoir compris, mais je ne pouvais toujours pas le reproduire), il n'y a toujours pas d'étapes à reproduire dans ce que les gens signalent, y compris sur quel processus.

Même quelque chose d'aussi simple que ça :

Dans 3.11, l'un des éléments de rétention répertoriés pour RuleSet est ImportManager.

Je ne peux trouver aucune preuve de cela, mais je ne sais pas non plus comment vous avez pu le déterminer. Je ne connais pas assez Chrome DevTools pour savoir comment déterminer ce qui est retenu par quoi, et c'est un sujet assez vague pour que Google ne m'ait pas aidé. Par exemple, les gens s'attachent-ils au processus Node ? L'exécuter dans le navigateur et définir des points d'arrêt ? Quelles sont les étapes ?

Bref, dans l'année où ce numéro a été ouvert, aucun des rapports n'a d'étapes à reproduire. Je suis sûr que toutes ces anecdotes signifient QUELQUE CHOSE, mais comment avez-vous déterminé ce que VOUS avez trouvé ? Ou pouvez-vous penser à une configuration qui le démontrera ? J'aimerais aider à le comprendre, mais je ne sais pas comment le reproduire.

@Justineo Sur votre repo, quelles étapes avez-vous

@Justineo Puisque vous avez signalé la suppression du cache (ce qui était peut-être une mauvaise idée), pouvez-vous tester cette branche et voir si cela aide votre vitesse de construction? https://github.com/less/less.js/tree/cache-restored

Juste pour donner quelques mises à jour sur les expérimentations/tests d'aujourd'hui :

Grunt shell:test fois

  • Moins 3,9 -- 1,8 s
  • Moins 3,12 (Babel transpilé en ES5) -- 9,2 s
  • Moins 3,12 (Babel transpilé en ES6) -- 3,1 s
  • Moins 3.12 (TypeScript transpilé vers ES5) -- 3.2s

Donc, je pense que le long et le court est que le code transpilé est toujours plus lent, et nous étions naïfs de penser le contraire. Je suis un peu surpris car la transpilation est maintenant si "standard" dans le monde JavaScript. Y a-t-il d'autres recherches qui confirment cela?

@Justineo Sur votre repo, quelles étapes avez-vous

npm run test produira le temps total écoulé. Et dans d'autres projets, nous expérimentons le MOO lors du passage à 3.12.

Si nous regardons le code généré par TypeScript, nous obtenons quelque chose comme :

Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var node_1 = tslib_1.__importDefault(require("./node"));
var variable_1 = tslib_1.__importDefault(require("./variable"));
var property_1 = tslib_1.__importDefault(require("./property"));
var Quoted = /** <strong i="6">@class</strong> */ (function (_super) {
    tslib_1.__extends(Quoted, _super);
    function Quoted(str, content, escaped, index, currentFileInfo) {
        var _this = _super.call(this) || this;
        _this.escaped = (escaped == null) ? true : escaped;
        _this.value = content || '';
        _this.quote = str.charAt(0);
        _this._index = index;
        _this._fileInfo = currentFileInfo;
        _this.variableRegex = /@\{([\w-]+)\}/g;
        _this.propRegex = /\$\{([\w-]+)\}/g;
        _this.allowRoot = escaped;
        return _this;
    }

vs:

var Node = require('./node'),
    Variable = require('./variable'),
    Property = require('./property');

var Quoted = function (str, content, escaped, index, currentFileInfo) {
    this.escaped = (escaped == null) ? true : escaped;
    this.value = content || '';
    this.quote = str.charAt(0);
    this._index = index;
    this._fileInfo = currentFileInfo;
    this.variableRegex = /@\{([\w-]+)\}/g;
    this.propRegex = /\$\{([\w-]+)\}/g;
};

Donc, je suppose que toutes ces définitions et appels de fonctions supplémentaires s'additionnent au fil du temps ? À moins que ce ne soit un hareng rouge, mais je ne sais pas à quoi d'autre attribuer cela, à part la transpilation. Ce que je ne comprends pas, c'est pourquoi TS ne peut pas produire de code plus proche de l'original.

METTRE À JOUR:

Si j'essaie de mettre les tests 3.9 sur un pied d'égalité avec ce qui est en 3.12, alors j'obtiens essentiellement 1,2 s contre 1,3 s. Je ne suis donc plus sûr de cette différence car les tests ont changé. Il devrait être exécuté sur exactement les mêmes fichiers de moins.

@Justineo @rjgotten J'ai https://github.com/less/less.js/tree/cache-restored

@matthew-dean Merci ! Je vais l'essayer plus tard dans la journée.

J'ai testé la branche cache-restored et elle est beaucoup plus rapide que la v3.11.2+, à peu près à la même vitesse que la v.3.1.0~3.11.1.

@Justineo

J'ai testé la branche restaurée en cache et elle est beaucoup plus rapide que la v3.11.2+, à peu près à la même vitesse que la v.3.1.0~3.11.1.

Eh bien, c'est prometteur. Obtenons des commentaires de @jrnail23 et d'autres dans ce fil. Cette suppression du cache a eu lieu après la publication de ce fil (3.11.2); en fait, c'était une tentative de supprimer une partie de la surcharge mémoire, mais dans les cas où vous importez le même fichier plusieurs fois, cela aurait certainement pu aggraver les choses.

Donc, en bref, je ne suis toujours pas sûr du problème d'origine ou de sa cause (autre que QUELQUE CHOSE qui s'est passé dans la conversion de code), et je serais curieux de savoir si cette branche a toujours ces problèmes, mais comme je l'ai déjà mentionné , il n'y a pas d'étapes de reproduction claires, donc je ne suis pas sûr.

@matthew-dean, j'ai un peu de mal à essayer d'utiliser la branche cache-restored (je ne sais pas très bien ce qu'il faut faire pour l'utiliser localement, car npm link échoue moi).
Pouvez-vous publier une version Canary/pre-release que je puisse essayer ?

@jrnail23

Je pense que cela a fonctionné. Essayez de supprimer Less et d'installer avec npm i [email protected]+84d40222

@matthew-dean, je viens d'essayer cette version, et c'est toujours mauvais pour moi.
Pour les nouvelles versions de packs Web (pas de mise en cache), cela prend 62,4 secondes pour la v3.9, mais 121 secondes pour la v3.13.1.
Pour les builds en cache, la v3.9 prend 30 secondes et la v3.13.1 prend 83-87 secondes.

@jrnail23 Pouvez-vous essayer de supprimer tous les modules de nœud et d'installer [email protected]+b2049010

Moins 3.9 Référence :
Screen Shot 2020-12-05 at 2 36 09 PM

Moins 4.0.1-alpha.0 :
Screen Shot 2020-12-05 at 1 12 26 PM

Moins 4.0.1-alpha.2 :
Screen Shot 2020-12-05 at 2 35 20 PM

@Justineo @rjgotten Pouvez-vous essayer cela aussi ?

@jrnail23 @Justineo @rjgotten

Notez qu'il s'agit d'une version 4.0, vous pouvez donc rencontrer des erreurs si votre code comporte ces modifications majeures :

  • Les parenthèses sont requises pour les appels mixin (par exemple, .mixin; n'est pas autorisé)
  • Le mode mathématique par défaut de Less est maintenant la division parens, donc les barres obliques (conçues comme mathématiques) doivent être entre parenthèses

Ainsi, bien que je sois maintenant beaucoup plus optimiste, j'ai résolu le problème, sur la base des derniers repères, je n'ai pas encore compris pourquoi ce problème s'est produit. Si les performances résistent à tout le monde, je peux donner un aperçu de la façon dont j'ai finalement réduit le problème et de ce que j'ai trouvé. En d'autres termes, je pense avoir trouvé _où_ le problème est/était, mais pas nécessairement pourquoi.

Le résultat de la même suite de tests que https://github.com/less/less.js/issues/3434#issuecomment -672580467 :

| Versions | Temps |
| -- | -- |
| v3.9.0 | ~1.6s |
| v3.10.0~v3.11.1 | ~3.6s |
| v3.11.2+ | ~12s |
| 4.0.1-alpha.2+b2049010 | ~1.6s |

Je peux confirmer que pour ma suite de tests spécifique, le niveau de performance s'est amélioré jusqu'à la version 3.9.0. Très apprécié Matt ! Bien que je ne sois pas sûr du changement de pause en mode mathématique. Changer cela peut entraîner la rupture de bon nombre de nos applications, nous pouvons donc être bloqués sur la version 3.9.0 même si le problème de performances est résolu.

@Justineo

Bien que je ne sois pas sûr du changement de pause en mode mathématique.

Vous pouvez explicitement compiler en mode math=always pour obtenir le comportement mathématique précédent. C'est juste une valeur par défaut différente.

Décomposition du problème

TL;DR - Méfiez-vous du modèle de classe

Le problème était dans la transpilation des classes à la fois dans Babel et TypeScript. (En d'autres termes, les deux avaient les mêmes problèmes de performances avec le code transpilé, Babel étant légèrement pire.) Maintenant, il y a des années, lorsque le modèle de classe a été introduit, on m'a dit - et jusqu'à ce problème, je croyais - cette classe l'héritage en JavaScript est du sucre syntaxique pour l'héritage fonctionnel.

Bref, non. _(Edit : eh bien... c'est et ce n'est pas. Cela dépend de ce que vous entendez par "héritage" et de la façon dont vous l'avez défini comme vous le verrez sous peu... c'est-à-dire qu'il y a plusieurs modèles à créez "l'héritage" dans la chaîne de prototypes en JavaScript, et les classes ne représentent qu'un seul modèle, mais ce modèle est quelque peu différent de tous les autres, d'où le besoin de TS / Babel d'avoir un code d'aide pour "imiter" ce modèle à l'aide de fonctions spécialisées.)_

Moins de code ressemblait à ceci :

var Node = function() {
  this.foo = 'bar';
}

var Inherited = function() {
  this.value = 1;
}
Inherited.prototype = new Node();

var myNode = new Inherited();

Supposons maintenant que vous vouliez réécrire ceci en utilisant JS "moderne". En fait, vous pouvez automatiser ce processus, ce que j'ai fait, mais dans tous les cas, je l'aurais écrit de la même manière, à savoir :

class Node {
  constructor() {
    this.foo = 'bar';
  }
}
class Inherited extends Node {
  constructor() {
    super();
    this.value = 1;
  }
}
var myNode = new Inherited();

Même chose, non ? En fait non. Le premier va créer un objet avec une propriété de { value: 1 } , et sur sa chaîne de prototype, il a un objet de { foo: 'bar' } .

Le second, car il appellera le constructeur à la fois sur Inherited et Node , créera un objet avec une structure comme { value: 1, foo: 'bar' } .

Maintenant, pour l'_utilisateur_, cela n'a pas vraiment d'importance, car vous pouvez accéder à la fois à value et à foo partir de myNode dans les deux cas. _Fonctionnellement_, ils semblent se comporter de la même manière.

C'est ici que j'entre dans la pure spéculation

D'après ce dont je me souviens dans des articles sur les moteurs JIT comme V8, les structures d'objets sont en fait très importantes. Si je crée un tas de structures comme { value: 1 } , { value: 2 } , { value: 3 } , { value: 4 } , V8 crée une représentation statique interne de cette structure. Vous stockez essentiellement la structure + les données une fois, puis les données 3 fois de plus.

MAIS si j'y ajoute des propriétés différentes à chaque fois, comme : { a: 'a', value: 1 } , { b: 'b', value: 2 } , { c: 'c', value: 3 } , { d: 'd', value: 4 } , alors ce sont 4 structures différentes avec 4 ensembles de données , même s'ils ont été créés à partir du même ensemble de classes d'origine. Chaque mutation d'un objet JS désoptimise les recherches de données, et un modèle de classe qui est transpilé en fonctions provoque (peut-être) des mutations plus uniques. (Honnêtement, je ne sais pas si cela est vrai pour la prise en charge des classes natives dans les navigateurs.)

AFAIK, ce qui est également vrai, c'est que plus vous avez de propriétés sur un objet, plus la recherche d'une propriété individuelle prend du temps.

Encore une fois pour les utilisateurs finaux, cela compte rarement car les moteurs JS sont si rapides. Buuuuut dites que vous maintenez un moteur qui crée et recherche des propriétés / méthodes sur BEAUCOUP d'objets aussi rapidement que possible. (Ding ding ding.) Soudain, ces petites différences entre la façon dont TypeScript / Babel "étendent" les objets et l'héritage de prototype fonctionnel natif s'additionnent très rapidement.

J'ai écrit une implémentation simple d'un nœud et d'une fonction d'héritage, en utilisant l'ancienne syntaxe Less et le modèle de classe transpilé avec TS. Dès le départ, le nœud hérité consomme 25 % de mémoire/ressources en plus, et c'est AVANT que des instances du nœud hérité ne soient créées.

Considérons maintenant que certains nœuds héritent d'autres nœuds. Cela signifie que la représentation en mémoire des classes commence à se multiplier, tout comme leurs instances héritées.

Encore une fois, c'est de la spéculation

Je dois insister sur le fait de prendre tout cela avec précaution, car je n'ai jamais entendu parler d'une conversion en cours aussi désastreuse pour la performance. Ce que je pense, c'est qu'il existe une combinaison spéciale de recherches d'objets/d'instances que Less utilise et qui désoptimise sérieusement le JIT. _(Si un expert du moteur JavaScript sait pourquoi les classes transpilées fonctionnent tellement moins bien que les méthodes d'héritage JS natives dans ce cas, j'aimerais le savoir.)_ J'ai essayé de créer une mesure de performance pour créer des milliers d'objets hérités en utilisant les deux méthodes, et Je n'ai jamais pu voir une différence constante dans les performances.

Donc, avant de vous en aller avec "les classes en JavaScript sont mauvaises", il pourrait s'agir d'un autre modèle vraiment malheureux dans la base de code Less, associé à la différence entre les classes transpilées et le JS natif, qui a créé cette baisse de performances.

Comment j'ai finalement compris

Honnêtement, je n'ai jamais soupçonné le modèle de classe. Je savais que le code ES5 d'origine fonctionnait plus rapidement que le code babélifié inversé, mais je soupçonnais quelque chose autour des fonctions de flèche ou de la syntaxe répandue quelque part. J'avais toujours la branche ES5 à jour, alors un jour j'ai décidé d'exécuter à nouveau lebab et d'utiliser uniquement ces transformations : let,class,commonjs,template . C'est alors que j'ai découvert qu'il avait à nouveau des problèmes de performances. Je savais que ce n'était pas un modèle de chaîne ou un let-to-var ; J'ai pensé que les exigences d'importation faisaient peut-être quelque chose, alors j'ai joué avec ça pendant un moment. Cela a quitté les cours. Donc, par intuition, j'ai réécrit toutes les classes étendues en héritage fonctionnel. Bam, la performance était de retour.

Un récit édifiant

Donc! Leçon apprise. Si votre projet est sur l'ancien code ES5 et que vous avez envie de cette bonté "moderne" Babelified ou TypeScripted, rappelez-vous que tout ce que vous transpilez, vous ne l'avez pas écrit.

J'étais toujours capable de réécrire les classes dans quelque chose de plus "classique", avec un modèle légèrement plus maintenable , mais en conservant plus ou moins le modèle d'héritage fonctionnel d'origine de Less node, et je laisserai une note forte dans le projet pour un futur mainteneur de ne plus jamais recommencer.

Vous pouvez explicitement compiler en mode math=always pour obtenir le comportement mathématique précédent. C'est juste une valeur par défaut différente.

J'en suis conscient. Nous avons de nombreuses bases de code Less dans de nombreuses équipes différentes, donc des changements même dans les options de compilation par défaut augmenteraient les coûts de communication. Je ne sais pas si les avantages en valent la peine.

Merci pour la répartition détaillée !

J'ai vu l'utilisation de Object.assign dans la sortie compilée, ce qui signifie que cela ne fonctionne que dans les navigateurs prenant en charge la syntaxe ES native class moins que les polyfills ne soient maintenant requis. Alors, pouvons-nous simplement utiliser la syntaxe native sans transpiler vers ES5 si nous avons l'intention d'abandonner la prise en charge des environnements plus anciens (par exemple, IE11, Node 4, ...) ?

En même temps, je pense qu'il est préférable que nous puissions séparer le correctif de la dégradation des performances et les changements de rupture, ce qui signifie que le correctif de performances est mis en place dans la v3 et n'inclut les changements de rupture que dans la v4.

@matthew-dean
Le fait que les classes ES sont le coupable est fou effrayant.
Merci pour la ventilation détaillée.

Va montrer: _composition sur héritage_ 😛

Bien que pour info ; si vous voulez une chaîne d'héritage protypale plus propre, vous devriez vraiment le faire un peu différemment de votre exemple.
Si vous utilisez Object.create comme ceci :

var Node = function() {
  this.foo = 'bar';
}
Node.prototype = Object.create();
Node.prototype.constructor = Node;

var Inherited = function() {
  Node.prototype.constructor.call( this );
  this.value = 1;
}
Inherited.prototype = Object.create( Node.prototype );
Inherited.prototype.constructor = Inherited;

var myNode = new Inherited();

puis vous aplatissez les propriétés sur les instances elles-mêmes qui partagent alors la même forme ; les prototypes partagent la forme ; _et_ vous évitez d'avoir à parcourir les chaînes de prototypes pour chaque accès à la propriété. ??

@Justineo

J'ai vu l'utilisation de Object.assign dans la sortie compilée, ce qui signifie qu'il ne fonctionne que dans les navigateurs prenant en charge la syntaxe de classe ES native, à moins que les polyfills ne soient désormais requis. Alors, pouvons-nous simplement utiliser la syntaxe native sans transpiler vers ES5 si nous avons l'intention d'abandonner la prise en charge des environnements plus anciens (par exemple, IE11, Node 4, ...) ?

Je ne pense pas que ce soit tout à fait exact, c'est-à-dire que Object.assign a atterri peu de temps avant la mise en œuvre des classes, mais votre argument est pris en compte. J'espérais juste éviter d'écrire [Something].prototype.property encore et encore :/

Techniquement, tout est toujours en cours de transpilation, donc je ne sais pas. L'objectif initial était une base de code plus maintenable / lisible. Si vous avez besoin d'un polyfill Object.assign dans un environnement, qu'il en soit ainsi. Ce serait sur une version de quelque chose que Less ne prend pas en charge.

En même temps, je pense qu'il est préférable que nous puissions séparer le correctif de la dégradation des performances et les changements de rupture, ce qui signifie que le correctif de performances est mis en place dans la v3 et n'inclut les changements de rupture que dans la v4.

J'y ai pensé, et je suppose que c'est aussi un bon point. J'essaie juste de ne pas exploser la tête en construisant / maintenant 3 versions majeures de Less.

@rjgotten AFAIK c'est ce qu'un modèle de classe est censé faire, exactement comme vous l'avez défini. Sauf si je ne vois pas de différence critique. Alors 🤷‍♂️. Je ne vais pas essayer de modifier à nouveau le modèle d'héritage de Less s'il fonctionne bien (autre que je peux supprimer cet appel Object.assign comme @Justineo l'a suggéré.)

@Justineo @rjgotten Pouvez-vous essayer ceci : [email protected]+b1390a54

Je viens d'essayer :

| Versions | Temps |
| -- | -- |
| v3.9.0 | ~1.6s |
| v3.10.0~v3.11.1 | ~3.6s |
| v3.11.2+ | ~12s |
| 4.0.1-alpha.2+b2049010 | ~1.6s |
| 3.13.0-alpha.10+b1390a54 | ~4.7s |

Ps. Testé avec Node.js v12.13.1.

Je suis quoi.

Je n'ai pas eu le temps de mettre en place des repères séparés.
Mais du point de vue des tests d'intégration, voici quelques chiffres réels : les résultats cumulés d'un projet Webpack de niveau production qui utilise Less.

Versions | Temps | Mémoire de pointe
:-------------------------|--------:|------------:
3.9 | 35376ms | 950 Mo
3.11.3 | 37878ms | 920 Mo
3.13.0-alpha.10+b1390a54 | 34801ms | 740 Mo
3.13.1-alpha.1+84d40222 | 37367ms | 990 Mo
4.0.1-alpha.2+b2049010 | 35857ms | 770 Mo

Pour moi, le 3.13.0 donne les _meilleurs_ résultats... 🙈

3.11.3 semble également ne pas avoir l'utilisation de mémoire incontrôlable que j'ai vue auparavant avec la version 3.11.1.
Mais les bêtas 4.0.1 et 3.13.0 sont encore meilleurs.

Le 3.13.1 qui a restauré le cache n'aide pas à améliorer mes temps de compilation dans le monde réel et ne fait qu'augmenter l'utilisation de la mémoire.

@rjgotten Ouais, je pense que le problème est que, dans ce fil, les gens mesurent des choses différentes, et jusqu'à présent, tout le monde a essentiellement des données privées qu'il est impossible de reproduire (sans se soumettre à Less benchmarking, ou traduire cela en PR). Less a un test de référence, que je pourrais utiliser contre différents clones du référentiel, pour vérifier par moi-même les différences de temps d'analyse / évaluation, mais où la mise en cache ne s'applique pas (actuellement).

Voici une version publiée avec les modifications d'héritage sur l'arborescence et avec le cache de l'arborescence d'analyse restauré : [email protected]+e8d05c61

Cas de test

https://github.com/ecomfe/dls-tooling/tree/master/packages/less-plugin-dls

Résultats

| Versions | Temps |
| -- | -- |
| v3.9.0 | ~1.6s |
| v3.10.0~v3.11.1 | ~3.6s |
| v3.11.2+ | ~12s |
| 4.0.1-alpha.2| ~1.6s |
| 3.13.0-alpha.10| ~4.7s |
| 3.13.0-alpha.12 | ~1.6s |

Version de Node.js

v12.13.1


Pour moi, cette version semble fonctionner parfaitement. Je vais demander à mes collègues d'essayer et de vérifier.

Fonctionne légèrement moins bien que le alpha.10 pour moi, mais cela pourrait aussi être simplement une variance :

Versions | Temps | Mémoire de pointe
:-------------------------|--------:|------------:
3.9 | 35376ms | 950 Mo
3.11.3 | 37878ms | 920 Mo
3.13.0-alpha.10+b1390a54 | 34801ms | 740 Mo
3.13.0-alpha.12+e8d05c61 | 36263ms | 760 Mo
3.13.1-alpha.1+84d40222 | 37367ms | 990 Mo
4.0.1-alpha.2+b2049010 | 35857ms | 770 Mo

@matthew-dean, il semble que mon code ne soit pas encore compatible avec les modifications de 4.0.1-alpha.2+b2049010 . Je vais voir si je peux résoudre mes problèmes et essayer.

@rjgotten Ouais, la différence semble mineure pour votre cas de test.

Je pense que le résultat de ceci est que je pourrais préparer à la fois une version 3.x et une version 4.0. Je n'avais pas envie de pousser 4.0 avec ce problème non résolu. Heureusement, on dirait que c'est le cas.

L'autre élément que je vais faire est d'ouvrir un problème à saisir pour quiconque consiste à mettre dans un pipeline CI qui échoue moins s'il tombe en dessous d'un certain seuil de référence.

@jrnail23 Ce serait formidable si vous pouviez modifier pour exécuter l'alpha 4.0, mais pouvez-vous en attendant essayer [email protected]+e8d05c61 ?

@matthew-dean, j'obtiens ce qui suit pour ma construction :

  • v3.9 : 98 secondes
  • v3.10.3 : 161 secondes
  • v3.13.0-alpha.12 : 93-96 secondes

On dirait donc que vous êtes de retour là où vous voulez être ! Bon travail!

Bon travail!

Ouais, je vais en second; troisième; et quatrième cela.
Il s'agissait d'une régression de performance désagréable et désagréable qui a vraiment demandé beaucoup d'efforts à nettoyer.

Travail _très bien_ fait.

Ok l'équipe ! Je vais publier la version 3.x, et un peu plus tard, peut-être la semaine prochaine, publier la version 4.0. Les deux devraient maintenant avoir les correctifs de performances. Merci à tous ceux qui ont aidé au débogage !

Soit dit en passant, je suis actuellement en train de convertir la base de code Less.js en TypeScript et j'ai l'intention d'en profiter pour effectuer des réglages / refactorisation des performances. Je suis prêt à aider si quelqu'un est intéressé ! https://github.com/matthew-dean/less.js/compare/master...matthew-dean : suivant

Quelque chose de spécifique où vous préféreriez des yeux supplémentaires? Le PR est très grand, donc je ne sais pas vraiment par où commencer

@kevinramharak C'est une bonne question. Pour être honnête, j'ai rencontré des obstacles inattendus lors de la conversion en TypeScript (erreurs qui sont devenues difficiles à résoudre), et je repense maintenant à le faire. Moins fonctionne bien tel quel, et de nombreuses raisons pour lesquelles je voulais convertir (pour faciliter la refactorisation / l'ajout de nouvelles fonctionnalités linguistiques) ne sont plus pertinentes car j'ai décidé de déplacer mes idées pour de nouvelles fonctionnalités linguistiques vers un nouveau pré- langage de traitement. Je ne veux pas trop me promouvoir, alors envoyez-moi un DM sur Twitter ou Gitter si vous voulez des détails.

Cette page vous a été utile?
0 / 5 - 0 notes