javascript-unit-testing-performance
Jest exécute des milliers de tests sur Facebook à tout moment, soit par une intégration continue, soit par un appel manuel des ingénieurs pendant le développement. Cela a bien fonctionné pendant des années, même lorsque les personnes travaillant sur Jest ont été transférées vers d'autres projets au sein de Facebook.
Cependant, à mesure que les ingénieurs ajoutaient de plus en plus de tests, nous avons remarqué que les performances de Jest n'allaient pas s'améliorer. En outre, au cours de l'année dernière, l'écosystème JavaScript a radicalement changé avec l'introduction de choses comme npm3 et Babel, ce que nous n'avions pas anticipé. Nous avons formé une nouvelle équipe de Jest pour traiter tous ces problèmes et nous partagerons nos progrès et nos plans sur ce blog à partir de maintenant.
Jest est un peu différent de la plupart des frameworks de test. Nous l'avons conçu pour bien fonctionner dans le contexte de l'infrastructure de Facebook :
- Monorepo At Facebook we have a huge monorepo that contains all of our JavaScript code. There are many reasons why this approach is advantageous for us and there is an awesome talk by a Google engineer that highlights all the benefits and drawbacks of monorepos.
- Sandboxing Another feature of Jest that's important to Facebook is how it virtualizes the test environment and wraps
requirein order to sandbox code execution and isolate individual tests. Nous travaillons même à rendre Jest plus modulaire pour que nous puissions tirer parti de cette fonctionnalité dans d'autres cas d'utilisation non liés aux tests. - providesModule If you've looked at any of our open source JavaScript projects before, you may have noticed that we use a
@providesModuleheader to assign globally unique IDs to modules. Cela nécessite des outils personnalisés, mais cela nous permet de référencer des modules sans chemins relatifs, ce qui nous a aidés à avancer incroyablement vite. a évolué ainsi que notre organisation d'ingénierie a grandi et a favorisé le partage de code dans toute l'entreprise. Check out RelayContainer for an example of how this works in practice. Un inconvénient à cette approche, cependant, est que nous sommes forcés de lire et d'analyser toute notre base de code JavaScript afin de résoudre une seule requête requise. Cela serait évidemment prohibitif sans mise en cache complète, en particulier pour un processus de courte durée comme Jest.
En raison de ces contraintes uniques, Jest ne pourra peut-être jamais être aussi rapide que les autres exécuteurs de test lorsqu'il s'exécute sur toute notre suite de tests. Cependant, les ingénieurs ont rarement besoin d'exécuter Jest sur toute notre suite de tests. Powered by static analysis in the node-haste project – we've been able to make the default mode for running Jest at Facebook jest --onlyChanged, or jest -o. Dans ce mode, nous construisons un graphique de dépendance inversée pour ne trouver que les tests concernés qui doivent être exécutés en fonction des modules qui ont été modifiés.
Planification optimale d'une exécution de test
La plupart du temps, notre analyse statique détermine que plusieurs tests doivent être exécutés. Le nombre de tests affectés peut aller de quelques tests à des milliers. Afin d'accélérer ce processus, Jest effectue des parallélisations de tests entre les travailleurs. C'est génial parce que la majeure partie du développement de Facebook se produit sur des serveurs distants avec de nombreux cœurs de processeur.
Recently we noticed Jest often seemed stuck “Waiting for 3 tests” for up to a minute toward the end of a run. Il s'est avéré que nous avions quelques tests vraiment lents dans notre base de code qui dominaient l'exécution. Bien que nous ayons pu accélérer de manière significative ces tests individuels, nous avons également modifié la façon dont Jest planifie le fonctionnement des tests. Auparavant, nous avions l'habitude de planifier des exécutions de tests basées sur la traversée du système de fichiers, qui était en fait assez aléatoire. Voici un exemple de 11 tests en blocs gris sur deux travailleurs. La taille du bloc est le temps d'exécution du test :
Nous avons fait un mélange aléatoire de tests rapides et lents, et l'un de nos tests les plus lents a fini par fonctionner comme presque tous les autres tests ont été terminés, au cours desquels le second worker a été inactif.
Nous avons modifié la planification des tests en fonction de leur taille de fichier, ce qui est généralement un bon proxy pour la durée à laquelle un test va prendre. Un test avec quelques milliers de lignes de code prend probablement plus de temps qu'un test avec 15 lignes de code. Tandis que cette vitesse accélère le test entier d'environ 10%, nous avons fini par trouver une meilleure heuristique : maintenant Jest stocke l'exécution de chaque test dans une cache et lors des exécutions suivantes, il planifie les tests les plus lents à exécuter en premier. Dans l'ensemble, cela a contribué à améliorer le temps d'exécution de tous les tests d'environ 20%.
Voici un exemple du même test avec une meilleure planification :
Parce que nous lançons d'abord des tests lents, Jest peut parfois prendre beaucoup de temps pour démarrer – nous n’imprimons les résultats qu’une fois le premier test terminé. Pour l'avenir, nous prévoyons d'abord d'exécuter les tests qui ont échoué précédemment, parce que l'obtention de cette information aux développeurs le plus rapidement possible importe le plus.
Inline Requires and Lazy Mocking
Si vous avez déjà écrit des tests avec Jasmine, ils ressemblent probablement à ceci :
const sum = require('sum');
describe('sum', () => {
it('works', () => {
expect(sum(5, 4)).toBe(9);
});
});
One special thing we do in Jest is reset the entire module registry after every single test (call to it) to make sure tests don't depend on each other. Avant Jest, les tests individuels dépendaient les uns des autres et l'état interne des modules se divulguait souvent entre eux. Au fur et à mesure que les ingénieurs ont enlevé, réordonné ou refaçonné les tests, certains ont commencé à échouer, ce qui rend difficile pour les gens de comprendre ce qui se passait.
Chaque test de Jest reçoit une nouvelle copie de tous les modules, y compris les nouvelles versions de toutes les dépendances fictives qui prennent beaucoup de temps à générer pour chaque test. A side effect of this is that we had to call require manually before every test, like this:
let sum;
describe('sum', () => {
beforeEach(() => {
sum = require('sum');
});
it('works', () => {
expect(sum(5, 4)).toBe(9);
});
it('works too', () => {
// This copy of sum is not the same as in the previous call to `it`.
expect(sum(2, 3)).toBe(5);
});
});
We built a babel transform called inline-requires that removes top-level require statements and inlines them in code. For example, the line const sum = require('sum'); will be removed from code, but every use of sum in the file will be replaced by require('sum'). Avec cette transformation, nous pouvons écrire des tests comme vous vous y attendiez dans Jasmine et le code se transforme en ceci :
describe('sum', () => {
it('works', () => {
expect(require('sum')(5, 4)).toBe(9);
});
});
Un grand effet secondaire de la ligne exige que nous n'ayons besoin que des modules que nous utilisons réellement dans le test lui-même, au lieu de tous les modules que nous avons utilisés dans le fichier entier.
Ce qui mène à une autre optimisation : le bouchon paresseux. L'idée est de bouchonner uniquement les modules à la demande, qui est combiné avec inline nous sauve de bouchonner beaucoup de modules et toutes leurs dépendances récursives.
We were able to update all tests using a codemod in no time – it was a simple 50,000 line code change. Inline nécessite et paresseux mocking a amélioré le temps de test de 50%.
Le plugin babel inline-require est non seulement utile pour Jest mais aussi pour JavaScript normal. It was shipped by Bhuwan to all users of facebook.com just a week ago and significantly improved startup time.
Pour l'instant, si vous souhaitez utiliser cette transformation dans Jest, vous devrez l'ajouter manuellement à votre configuration Babel. Nous travaillons sur des moyens de faciliter la mise en œuvre de cette option.
Repo-Sync et mise en cache
La version open source de Jest était un fork de notre version interne, et nous synchroniserions Jest une fois tous les deux mois. Ce fut un processus manuel douloureux qui nécessitait de corriger de nombreux tests à chaque fois. Nous avons récemment mis à jour Jest et apporté la parité à toutes les plateformes (iOS, Android et web) et avons ensuite activé notre processus de synchronisation. Maintenant, chaque changement de Jest en open source est exécuté contre tous nos tests internes, et il n'y a qu'une seule version de Jest qui soit cohérente partout.
La première fonctionnalité dont nous avons pu profiter après le déblocage était le cache du préprocesseur. Si vous utilisez Babel avec Jest, Jest doit prétraiter chaque fichier JavaScript avant de pouvoir l'exécuter. Nous avons construit un calque de mise en cache de sorte que chaque fichier, lorsqu'il n'est pas modifié, ne doit être transformé qu'une seule fois. Après avoir libéré Jest, nous avons pu facilement réparer l'implémentation de l'open source et l'expédier sur Facebook. Cela a entraîné une autre victoire de 50% sur la performance. Parce que la cache ne fonctionne que sur la seconde exécution, l'heure de début froide de Jest n'a pas été affectée.
Nous avons également réalisé que nous faisions beaucoup d’opérations de trajectoire lorsque nous résolvons des besoins relatifs. Parce que le registre des modules est réinitialisé pour chaque test, il y a eu des milliers d'appels qui pourraient être mémoisés. Une grande optimisation a été d'ajouter beaucoup plus de mise en cache, pas seulement autour d'un seul test, mais aussi à travers les fichiers de test. Auparavant, nous générerions des métadonnées de module pour la fonction d'automocking une fois pour chaque fichier de test. L'objet qu'un module exporte ne change jamais, donc nous partageons maintenant ce code entre les fichiers de test. Malheureusement, comme JavaScript et Node.js n'ont pas de mémoire partagée, nous devons faire tout ce travail au moins une fois par processus de travail.
Tout remettre en question
Lorsque vous essayez d'améliorer les performances, il est important de plonger aussi dans les systèmes qui s'installent au-dessus et en dessous de votre système. Dans le cas de Jest, des choses comme Node.js et les fichiers de test eux-mêmes, par exemple. L'une des premières choses que nous avons faites était de mettre à jour Node.js à Facebook à partir des années 0,10 à iojs et ensuite à Node 4. La nouvelle version de V8 a contribué à améliorer les performances et a été assez facile à mettre à jour.
We noticed that the path module in Node.js is slow when making thousands of path operations which was fixed in Node 5.7. Until we drop support for Node 4 internally at Facebook, we'll ship our own version of the fastpath module.
We next started questioning the outdated node-haste. As mentioned before, the entire project has to be parsed for @providesModule headers to build a dependency graph. When this system was originally built, node_modules didn't exist and our file system crawler wasn't excluding them properly.
In previous versions, Jest would actually read every file in node_modules – which contributed to the slow startup time of Jest. Lorsque nous avons repris Jest, nous avons remplacé l'ensemble du projet par une nouvelle implémentation, basée sur l'empaquetage de react-native. Le temps de démarrage de Jest est maintenant inférieur à une seconde, même sur les grands projets. The react-native team, specifically David, Amjad and Martin did an outstanding job on this project.
Tout additionner
Un grand nombre des changements ci-dessus ont amélioré le temps d'exécution du test de 10% ou parfois même de 50%. Nous avons commencé à une durée d'exécution d'environ 10 minutes pour tous les tests, et sans ces améliorations, nous serions probablement à environ 20 minutes. Après ces améliorations, cependant, il faut maintenant toujours environ 1 minute et 35 secondes pour exécuter tous nos tests !
Plus important encore, l'ajout de nouveaux tests entraîne une croissance très lente du temps d'exécution total. Les ingénieurs peuvent écrire et exécuter plus de tests sans en ressentir les coûts.
With Jest's recent 0.9 release and performance improvements from the node-haste2 integration, the runtime of the Relay framework's test suite went down from 60 seconds to about 25 and the react-native test suite now finishes in less than ten seconds on a 13” MacBook Pro.
Nous sommes très heureux des victoires que nous avons vues jusqu'à présent, et nous allons continuer à travailler sur Jest et à le rendre meilleur. If you are curious about contributing to Jest, feel free get in touch on GitHub, Discord or Facebook :)
