Comment rendre les performances visibles avec GitLab CI et Hoodoo des artefacts GitLab
Publié: 2022-03-10La dégradation des performances est un problème auquel nous sommes confrontés quotidiennement. Nous pourrions faire des efforts pour rendre l'application ultra rapide, mais nous nous retrouvons bientôt là où nous avons commencé. Cela se produit à cause de l'ajout de nouvelles fonctionnalités et du fait que nous n'avons parfois pas une seconde réflexion sur les packages que nous ajoutons et mettons à jour constamment, ou que nous pensons à la complexité de notre code. C'est généralement une petite chose, mais il s'agit toujours de petites choses.
Nous ne pouvons pas nous permettre d'avoir une application lente. La performance est un avantage concurrentiel qui peut attirer et fidéliser les clients. Nous ne pouvons pas nous permettre de passer régulièrement du temps à optimiser à nouveau les applications. C'est coûteux et complexe. Et cela signifie que malgré tous les avantages de la performance d'un point de vue commercial, elle n'est guère rentable. Comme première étape pour trouver une solution à tout problème, nous devons rendre le problème visible. Cet article vous aidera exactement à cela.
Remarque : Si vous avez une compréhension de base de Node.js, une vague idée du fonctionnement de votre CI/CD et que vous vous souciez des performances de l'application ou des avantages commerciaux qu'elle peut apporter, alors nous sommes prêts à partir.
Comment créer un budget de performance pour un projet
Les premières questions que nous devrions nous poser sont :
« Qu'est-ce que le projet performant ? »
« Quelles mesures dois-je utiliser ? »
"Quelles valeurs de ces métriques sont acceptables ?"
La sélection des métriques sort du cadre de cet article et dépend fortement du contexte du projet, mais je vous recommande de commencer par lire Métriques de performance centrées sur l'utilisateur par Philip Walton.
De mon point de vue, c'est une bonne idée d'utiliser la taille de la bibliothèque en kilo-octets comme métrique pour le package npm. Pourquoi? Eh bien, c'est parce que si d'autres personnes incluent votre code dans leurs projets, elles voudront peut-être minimiser l'impact de votre code sur la taille finale de leur application.
Pour le site, je considérerais Time To First Byte (TTFB) comme une métrique. Cette métrique montre combien de temps il faut au serveur pour répondre avec quelque chose. Cette métrique est importante, mais assez vague car elle peut inclure n'importe quoi - à partir du temps de rendu du serveur et se retrouver avec des problèmes de latence. Il est donc agréable de l'utiliser conjointement avec Server Timing ou OpenTracing pour savoir en quoi il consiste exactement.
Vous devez également prendre en compte des métriques telles que Time to Interactive (TTI) et First Meaningful Paint (cette dernière sera bientôt remplacée par Largest Contentful Paint (LCP)). Je pense que ces deux éléments sont les plus importants - du point de vue de la performance perçue.
Mais gardez à l'esprit que les métriques sont toujours liées au contexte , alors ne tenez pas cela pour acquis. Réfléchissez à ce qui est important dans votre cas spécifique.
Le moyen le plus simple de définir les valeurs souhaitées pour les métriques est d'utiliser vos concurrents - ou même vous-même. De plus, de temps en temps, des outils tels que le calculateur de budget de performance peuvent s'avérer utiles - il suffit de jouer un peu avec.
La dégradation des performances est un problème auquel nous sommes confrontés quotidiennement. Nous pourrions faire des efforts pour rendre l'application ultra rapide, mais bientôt nous nous retrouvons là où nous avons commencé.
"
Utilisez les concurrents à votre avantage
S'il vous est déjà arrivé de fuir un ours extatiquement surexcité, alors vous savez déjà que vous n'avez pas besoin d'être un champion olympique de course à pied pour sortir de ce pétrin. Vous avez juste besoin d'être un peu plus rapide que l'autre gars.
Faites donc une liste de concurrents. S'il s'agit de projets du même type, ils consistent généralement en des types de pages similaires les uns aux autres. Par exemple, pour une boutique en ligne, il peut s'agir d'une page avec une liste de produits, une page de détails du produit, un panier, un paiement, etc.
- Mesurez les valeurs de vos métriques sélectionnées sur chaque type de page pour les projets de vos concurrents ;
- Mesurez les mêmes métriques sur votre projet ;
- Trouvez la meilleure valeur la plus proche de votre valeur pour chaque métrique dans les projets du concurrent. Ajoutez-y 20 % et définissez-les comme vos prochains objectifs.
Pourquoi 20% ? C'est un nombre magique qui signifie soi-disant que la différence sera perceptible à l'œil nu. Vous pouvez en savoir plus sur ce nombre dans l'article de Denys Mishunov "Why Perceived Performance Matters, Part 1: The Perception Of Time".
Un combat avec une ombre
Vous avez un projet unique ? Vous n'avez pas de concurrents ? Ou vous êtes déjà meilleur que n'importe lequel d'entre eux dans tous les sens possibles ? Ce n'est pas un problème. Vous pouvez toujours rivaliser avec le seul adversaire digne, c'est-à-dire vous-même. Mesurez chaque métrique de performance de votre projet sur chaque type de page, puis améliorez-les du même 20 %.
Essais synthétiques
Il existe deux manières de mesurer les performances :
- Synthétique (dans un environnement contrôlé)
- RUM (mesures réelles de l'utilisateur)
Les données sont collectées auprès d'utilisateurs réels en production.
Dans cet article, nous utiliserons des tests synthétiques et supposerons que notre projet utilise GitLab avec son CI intégré pour le déploiement du projet.
Bibliothèque et sa taille en tant que métrique
Supposons que vous ayez décidé de développer une bibliothèque et de la publier sur NPM. Vous voulez le garder léger - beaucoup plus léger que les concurrents - afin qu'il ait moins d'impact sur la taille finale du projet résultant. Cela permet d'économiser du trafic clients - parfois du trafic pour lequel le client paie. Cela permet également au projet d'être chargé plus rapidement, ce qui est assez important en ce qui concerne la part croissante du mobile et les nouveaux marchés avec des vitesses de connexion lentes et une couverture Internet fragmentée.
Forfait pour mesurer la taille de la bibliothèque
Pour garder la taille de la bibliothèque aussi petite que possible, nous devons surveiller attentivement son évolution au cours du développement. Mais comment pouvez-vous le faire? Eh bien, nous pourrions utiliser le package Size Limit créé par Andrey Sitnik de Evil Martians.
Installons-le.
npm i -D size-limit @size-limit/preset-small-lib
Ensuite, ajoutez-le à package.json
.
"scripts": { + "size": "size-limit", "test": "jest && eslint ." }, + "size-limit": [ + { + "path": "index.js" + } + ],
Le bloc "size-limit":[{},{},…]
contient une liste de la taille des fichiers dont on veut vérifier. Dans notre cas, il s'agit d'un seul fichier : index.js
.
La size
du script NPM exécute simplement le package size-limit
, qui lit la size-limit
du bloc de configuration mentionnée précédemment et vérifie la taille des fichiers qui y sont répertoriés. Exécutons-le et voyons ce qui se passe :
npm run size

Nous pouvons voir la taille du fichier, mais cette taille n'est pas réellement sous contrôle. Corrigeons cela en ajoutant limit
à package.json
:
"size-limit": [ { + "limit": "2 KB", "path": "index.js" } ],
Maintenant, si nous exécutons le script, il sera validé par rapport à la limite que nous avons définie.

Dans le cas où un nouveau développement modifie la taille du fichier au point de dépasser la limite définie, le script se terminera avec un code non nul. Ceci, à part d'autres choses, signifie qu'il arrêtera le pipeline dans le GitLab CI.

Nous pouvons maintenant utiliser git hook pour vérifier la taille du fichier par rapport à la limite avant chaque validation. Nous pouvons même utiliser le paquet husky pour le faire d'une manière agréable et simple.
Installons-le.
npm i -D husky
Ensuite, modifiez notre package.json
.
"size-limit": [ { "limit": "2 KB", "path": "index.js" } ], + "husky": { + "hooks": { + "pre-commit": "npm run size" + } + },
Et maintenant, avant que chaque validation ne soit exécutée automatiquement, la commande npm run size
et si elle se termine par un code non nul, la validation ne se produira jamais.

Mais il existe de nombreuses façons de sauter des crochets (intentionnellement ou même par accident), nous ne devons donc pas trop nous fier à eux.
De plus, il est important de noter que nous ne devrions pas avoir besoin de bloquer cette vérification. Pourquoi? Parce que c'est normal que la taille de la bibliothèque augmente pendant que vous ajoutez de nouvelles fonctionnalités. Nous devons rendre les changements visibles, c'est tout. Cela aidera à éviter une augmentation accidentelle de la taille en raison de l'introduction d'une bibliothèque d'assistance dont nous n'avons pas besoin. Et, peut-être, donner aux développeurs et aux propriétaires de produits une raison de se demander si la fonctionnalité ajoutée vaut l'augmentation de taille. Ou, peut-être, s'il existe des packages alternatifs plus petits. Bundlephobia nous permet de trouver une alternative pour presque tous les packages NPM.
Alors, que devrions-nous faire? Montrons le changement de taille de fichier directement dans la demande de fusion ! Mais vous ne poussez pas à maîtriser directement ; vous agissez comme un développeur adulte, n'est-ce pas ?
Exécution de notre vérification sur GitLab CI
Ajoutons un artefact GitLab de type metrics. Un artefact est un fichier qui «vivra» une fois l'opération de pipeline terminée. Ce type spécifique d'artefact nous permet d'afficher un widget supplémentaire dans la demande de fusion, indiquant tout changement dans la valeur de la métrique entre l'artefact dans le maître et la branche de fonctionnalité. Le format de l'artefact de metrics
est un format texte Prometheus. Pour les valeurs GitLab à l'intérieur de l'artefact, il s'agit simplement de texte. GitLab ne comprend pas ce qui a exactement changé dans la valeur — il sait juste que la valeur est différente. Alors, que devons-nous faire exactement ?
- Définissez des artefacts dans le pipeline.
- Modifiez le script afin qu'il crée un artefact sur le pipeline.
Pour créer un artefact, nous devons modifier .gitlab-ci.yml
cette façon :
image: node:latest stages: - performance sizecheck: stage: performance before_script: - npm ci script: - npm run size + artifacts: + expire_in: 7 days + paths: + - metric.txt + reports: + metrics: metric.txt
-
expire_in: 7 days
: l'artefact existera pendant 7 jours. paths: metric.txt
Il sera enregistré dans le catalogue racine. Si vous ignorez cette option, il ne sera pas possible de le télécharger.reports: metrics: metric.txt
L'artefact aura le typereports:metrics
Faisons maintenant en sorte que Size Limit génère un rapport. Pour ce faire, nous devons modifier package.json
:
"scripts": { - "size": "size-limit", + "size": "size-limit --json > size-limit.json", "test": "jest && eslint ." },
size-limit
avec la clé --json
affichera les données au format json :

size-limit --json
génère du JSON sur la console. JSON contient un tableau d'objets qui contiennent un nom de fichier et une taille, ainsi que nous permet de savoir s'il dépasse la limite de taille. ( Grand aperçu ) Et la redirection > size-limit.json
enregistrera JSON dans le fichier size-limit.json
.
Maintenant, nous devons créer un artefact à partir de cela. Le format se résume à [metrics name][space][metrics value]
. Créons le script generate-metric.js
:
const report = require('./size-limit.json'); process.stdout.write(`size ${(report[0].size/1024).toFixed(1)}Kb`); process.exit(0);
Et ajoutez-le à package.json
:
"scripts": { "size": "size-limit --json > size-limit.json", + "postsize": "node generate-metric.js > metric.txt", "test": "jest && eslint ." },
Comme nous avons utilisé le préfixe post
, la commande npm run size
exécutera d'abord le script size
, puis exécutera automatiquement le script postsize
, ce qui entraînera la création du fichier metric.txt
, notre artefact.
Par conséquent, lorsque nous fusionnerons cette branche avec master, modifierons quelque chose et créerons une nouvelle demande de fusion, nous verrons ce qui suit :

Dans le widget qui apparaît sur la page, nous voyons d'abord le nom de la métrique ( size
) suivi de la valeur de la métrique dans la branche de fonctionnalité ainsi que la valeur dans le maître entre parenthèses.
Nous pouvons maintenant voir comment modifier la taille du package et prendre une décision raisonnable si nous devons le fusionner ou non.
- Vous pouvez voir tout ce code dans ce référentiel.
Continuer
D'ACCORD! Donc, nous avons compris comment gérer le cas trivial. Si vous avez plusieurs fichiers, séparez simplement les métriques avec des sauts de ligne. Comme alternative à la limite de taille, vous pouvez envisager la taille du paquet. Si vous utilisez WebPack, vous pouvez obtenir toutes les tailles dont vous avez besoin en construisant avec les drapeaux --profile
et --json
:
webpack --profile --json > stats.json
Si vous utilisez next.js, vous pouvez utiliser le plugin @next/bundle-analyzer. C'est à vous!
Utilisation du phare
Lighthouse est la norme de facto en matière d'analyse de projet. Écrivons un script qui nous permet de mesurer les performances, a11y, les meilleures pratiques, et de nous fournir un score SEO.
Script pour mesurer toutes les choses
Pour commencer, nous devons installer le package phare qui effectuera les mesures. Nous devons également installer puppeteer que nous utiliserons comme navigateur sans tête.
npm i -D lighthouse puppeteer
Ensuite, créons un script lighthouse.js
et démarrons notre navigateur :
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); })();
Écrivons maintenant une fonction qui nous aidera à analyser une URL donnée :
const lighthouse = require('lighthouse'); const DOMAIN = process.env.DOMAIN; const buildReport = browser => async url => { const data = await lighthouse( `${DOMAIN}${url}`, { port: new URL(browser.wsEndpoint()).port, output: 'json', }, { extends: 'lighthouse:full', } ); const { report: reportJSON } = data; const report = JSON.parse(reportJSON); // … }
Génial! Nous avons maintenant une fonction qui acceptera l'objet navigateur comme argument et renverra une fonction qui acceptera l' URL
comme argument et générera un rapport après avoir passé cette URL
au lighthouse
.
Nous passons les arguments suivants au lighthouse
:
- L'adresse que nous voulons analyser ;
- options
lighthouse
,port
du navigateur notamment, etoutput
(format de sortie du rapport) ; - configuration
report
etlighthouse:full
(tout ce que nous pouvons mesurer). Pour une configuration plus précise, consultez la documentation.
Formidable! Nous avons maintenant notre rapport. Mais qu'est-ce qu'on peut en faire ? Eh bien, nous pouvons vérifier les métriques par rapport aux limites et quitter le script avec un code non nul qui arrêtera le pipeline :
if (report.categories.performance.score < 0.8) process.exit(1);
Mais nous voulons juste rendre les performances visibles et non bloquantes ? Adoptons ensuite un autre type d'artefact : l'artefact de performance GitLab.
Artefact de performance GitLab
Afin de comprendre ce format d'artefacts, nous devons lire le code du plugin sitespeed.io. (Pourquoi GitLab ne peut-il pas décrire le format de ses artefacts dans sa propre documentation ? Mystère. )
[ { "subject":"/", "metrics":[ { "name":"Transfer Size (KB)", "value":"19.5", "desiredSize":"smaller" }, { "name":"Total Score", "value":92, "desiredSize":"larger" }, {…} ] }, {…} ]
Un artefact est un fichier JSON
qui contient un tableau des objets. Chacun d'eux représente un rapport sur une URL
.
[{page 1}, {page 2}, …]
Chaque page est représentée par un objet avec les attributs suivants :
-
subject
Identifiant de page (c'est assez pratique d'utiliser un tel nom de chemin); -
metrics
Un tableau des objets (chacun d'eux représente une mesure effectuée sur la page).
{ "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }
Une measurement
est un objet qui contient les attributs suivants :
-
name
Nom de la mesure, par exemple il peut s'agirTime to first byte
ouTime to interactive
. -
value
Résultat de mesure numérique. -
desiredSize
Si la valeur cible doit être aussi petite que possible, par exemple pour la métriqueTime to interactive
, la valeur doit êtresmaller
. S'il doit être aussi grand que possible, par exemple pour lePerformance score
du phare , utilisez alorslarger
.
{ "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }
buildReport
notre fonction buildReport de manière à ce qu'elle renvoie un rapport pour une page avec des métriques phares standard.

const buildReport = browser => async url => { // … const metrics = [ { name: report.categories.performance.title, value: report.categories.performance.score, desiredSize: 'larger', }, { name: report.categories.accessibility.title, value: report.categories.accessibility.score, desiredSize: 'larger', }, { name: report.categories['best-practices'].title, value: report.categories['best-practices'].score, desiredSize: 'larger', }, { name: report.categories.seo.title, value: report.categories.seo.score, desiredSize: 'larger', }, { name: report.categories.pwa.title, value: report.categories.pwa.score, desiredSize: 'larger', }, ]; return { subject: url, metrics: metrics, }; }
Maintenant, lorsque nous avons une fonction qui génère un rapport. Appliquons-le à chaque type de pages du projet. Tout d'abord, je dois indiquer que process.env.DOMAIN
doit contenir un domaine intermédiaire (sur lequel vous devez au préalable déployer votre projet à partir d'une branche de fonctionnalité).

+ const fs = require('fs'); const lighthouse = require('lighthouse'); const puppeteer = require('puppeteer'); const DOMAIN = process.env.DOMAIN; const buildReport = browser => async url => {/* … */}; + const urls = [ + '/inloggen', + '/wachtwoord-herstellen-otp', + '/lp/service', + '/send-request-to/ww-tammer', + '/post-service-request/binnenschilderwerk', + ]; (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); + const builder = buildReport(browser); + const report = []; + for (let url of urls) { + const metrics = await builder(url); + report.push(metrics); + } + fs.writeFileSync(`./performance.json`, JSON.stringify(report)); + await browser.close(); })();
- Vous pouvez trouver la source complète dans ce gist et un exemple de travail dans ce référentiel.
Remarque : À ce stade, vous voudrez peut-être m'interrompre et crier en vain : "Pourquoi me prenez-vous du temps ? Vous ne pouvez même pas utiliser Promise.all correctement !" Pour ma défense, j'ose dire qu'il n'est pas recommandé d'exécuter plus d'une instance de phare en même temps car cela affecte négativement la précision des résultats de mesure. De plus, si vous ne faites pas preuve d'ingéniosité, cela conduira à une exception.
Utilisation de plusieurs processus
Êtes-vous toujours dans les mesures parallèles ? Très bien, vous voudrez peut-être utiliser un cluster de nœuds (ou même des threads de travail si vous aimez jouer en gras), mais il est logique d'en discuter uniquement dans le cas où votre pipeline s'exécute sur l'environnement avec plusieurs cors disponibles. Et même dans ce cas, vous devez garder à l'esprit qu'en raison de la nature de Node.js, vous aurez une instance Node.js complète générée dans chaque branche de processus (au lieu de réutiliser la même, ce qui entraînera une consommation croissante de RAM). Tout cela signifie qu'il sera plus coûteux en raison de l'exigence croissante en matériel et un peu plus rapide. Il peut sembler que le jeu n'en vaut pas la chandelle.
Si vous voulez prendre ce risque, vous devrez :
- Divisez le tableau d'URL en morceaux par nombre de cœurs ;
- Créer un fork d'un processus en fonction du nombre de cœurs ;
- Transférez des parties du tableau vers les fourches, puis récupérez les rapports générés.
Pour diviser un tableau, vous pouvez utiliser des approches multipiles. Le code suivant, écrit en quelques minutes seulement, ne serait pas pire que les autres :
/** * Returns urls array splited to chunks accordin to cors number * * @param urls {String[]} — URLs array * @param cors {Number} — count of available cors * @return {Array } — URLs array splited to chunks */ function chunkArray(urls, cors) { const chunks = [...Array(cors)].map(() => []); let index = 0; urls.forEach((url) => { if (index > (chunks.length - 1)) { index = 0; } chunks[index].push(url); index += 1; }); return chunks; }
/** * Returns urls array splited to chunks accordin to cors number * * @param urls {String[]} — URLs array * @param cors {Number} — count of available cors * @return {Array } — URLs array splited to chunks */ function chunkArray(urls, cors) { const chunks = [...Array(cors)].map(() => []); let index = 0; urls.forEach((url) => { if (index > (chunks.length - 1)) { index = 0; } chunks[index].push(url); index += 1; }); return chunks; }
Faites des fourches en fonction du nombre de cœurs :
// Adding packages that allow us to use cluster const cluster = require('cluster'); // And find out how many cors are available. Both packages are build-in for node.js. const numCPUs = require('os').cpus().length; (async () => { if (cluster.isMaster) { // Parent process const chunks = chunkArray(urls, urls.length/numCPUs); chunks.map(chunk => { // Creating child processes const worker = cluster.fork(); }); } else { // Child process } })();
Transférons un tableau de blocs aux processus enfants et récupérons les rapports :
(async () => { if (cluster.isMaster) { // Parent process const chunks = chunkArray(urls, urls.length/numCPUs); chunks.map(chunk => { const worker = cluster.fork(); + // Send message with URL's array to child process + worker.send(chunk); }); } else { // Child process + // Recieveing message from parent proccess + process.on('message', async (urls) => { + const browser = await puppeteer.launch({ + args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], + }); + const builder = buildReport(browser); + const report = []; + for (let url of urls) { + // Generating report for each URL + const metrics = await builder(url); + report.push(metrics); + } + // Send array of reports back to the parent proccess + cluster.worker.send(report); + await browser.close(); + }); } })();
Et, enfin, réassemblez les rapports dans un tableau et générez un artefact.
- Découvrez le code complet et le référentiel avec un exemple qui montre comment utiliser Lighthouse avec plusieurs processus.
Précision des mesures
Eh bien, nous avons parallélisé les mesures, ce qui a augmenté la grande erreur de mesure déjà malheureuse du lighthouse
. Mais comment fait-on pour le réduire ? Eh bien, faites quelques mesures et calculez la moyenne.
Pour ce faire, nous allons écrire une fonction qui calculera la moyenne entre les résultats de mesure actuels et les précédents.
// Count of measurements we want to make const MEASURES_COUNT = 3; /* * Reducer which will calculate an avarage value of all page measurements * @param pages {Object} — accumulator * @param page {Object} — page * @return {Object} — page with avarage metrics values */ const mergeMetrics = (pages, page) => { if (!pages) return page; return { subject: pages.subject, metrics: pages.metrics.map((measure, index) => { let value = (measure.value + page.metrics[index].value)/2; value = +value.toFixed(2); return { ...measure, value, } }), } }
Ensuite, changez notre code pour les utiliser :
process.on('message', async (urls) => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); const builder = buildReport(browser); const report = []; for (let url of urls) { + // Let's measure MEASURES_COUNT times and calculate the avarage + let measures = []; + let index = MEASURES_COUNT; + while(index--){ const metric = await builder(url); + measures.push(metric); + } + const measure = measures.reduce(mergeMetrics); report.push(measure); } cluster.worker.send(report); await browser.close(); }); }
- Consultez l'essentiel avec le code complet et le référentiel avec un exemple.
Et maintenant, nous pouvons ajouter un lighthouse
dans le pipeline.
L'ajouter au pipeline
Tout d'abord, créez un fichier de configuration nommé .gitlab-ci.yml
.
image: node:latest stages: # You need to deploy a project to staging and put the staging domain name # into the environment variable DOMAIN. But this is beyond the scope of this article, # primarily because it is very dependent on your specific project. # - deploy # - performance lighthouse: stage: performance before_script: - apt-get update - apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget - npm ci script: - node lighthouse.js artifacts: expire_in: 7 days paths: - performance.json reports: performance: performance.json
Les multiples packages installés sont nécessaires pour le puppeteer
. Comme alternative, vous pouvez envisager d'utiliser docker
. En dehors de cela, il est logique que nous définissions le type d'artefact comme performance. Et, dès que la branche principale et la branche de fonctionnalité l'auront, vous verrez un widget comme celui-ci dans la demande de fusion :

Agréable?
Continuer
Nous en avons enfin fini avec un cas plus complexe. De toute évidence, il existe plusieurs outils similaires en dehors du phare. Par exemple, sitespeed.io. La documentation de GitLab contient même un article qui explique comment utiliser sitespeed
dans le pipeline de GitLab. Il existe également un plugin pour GitLab qui nous permet de générer un artefact. Mais qui préférerait des produits open source axés sur la communauté à ceux appartenant à une entreprise monstre ?
Il n'y a pas de repos pour les méchants
Il peut sembler que nous y sommes enfin, mais non, pas encore. Si vous utilisez une version payante de GitLab, les artefacts avec les metrics
et performance
des types de rapports sont présents dans les plans à partir de premium
et silver
qui coûtent 19 $ par mois pour chaque utilisateur. De plus, vous ne pouvez pas simplement acheter une fonctionnalité spécifique dont vous avez besoin - vous ne pouvez que modifier le plan. Pardon. Donc ce que nous pouvons faire? Contrairement à GitHub avec son API Checks et son API Status, GitLab ne vous permettrait pas de créer vous-même un widget réel dans la demande de fusion. Et il n'y a aucun espoir de les avoir de sitôt.

Une façon de vérifier si vous avez réellement un support pour ces fonctionnalités : vous pouvez rechercher la variable d'environnement GITLAB_FEATURES
dans le pipeline. S'il manque merge_request_performance_metrics
et metrics_reports
dans la liste, ces fonctionnalités ne sont pas prises en charge.
GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics, elastic_search, export_issues,group_bulk_edit,group_burndown_charts,group_webhooks, issuable_default_templates,issue_board_focus_mode,issue_weights,jenkins_integration, ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees, multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users, push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board, usage_quotas,visual_review_app,wip_limits
S'il n'y a pas de soutien, nous devons trouver quelque chose. Par exemple, nous pouvons ajouter un commentaire à la demande de fusion, commentaire avec le tableau, contenant toutes les données dont nous avons besoin. Nous pouvons laisser notre code intact — des artefacts seront créés, mais les widgets afficheront toujours un message «metrics are unchanged»
.
Comportement très étrange et non évident; J'ai dû réfléchir attentivement pour comprendre ce qui se passait.
Alors quel est le plan?
- Nous devons lire l'artefact de la branche
master
; - Créez un commentaire au format
markdown
; - Obtenez l'identifiant de la demande de fusion de la branche de fonctionnalité actuelle vers le maître ;
- Ajoutez le commentaire.
Comment lire l'artefact de la branche principale
Si nous voulons montrer comment les métriques de performances sont modifiées entre les branches master
et feature, nous devons lire l'artefact du master
. Et pour ce faire, nous devrons utiliser fetch
.
npm i -S isomorphic-fetch
// You can use predefined CI environment variables // @see https://gitlab.com/help/ci/variables/predefined_variables.md // We need fetch polyfill for node.js const fetch = require('isomorphic-fetch'); // GitLab domain const GITLAB_DOMAIN = process.env.CI_SERVER_HOST || process.env.GITLAB_DOMAIN || 'gitlab.com'; // User or organization name const NAME_SPACE = process.env.CI_PROJECT_NAMESPACE || process.env.PROJECT_NAMESPACE || 'silentimp'; // Repo name const PROJECT = process.env.CI_PROJECT_NAME || process.env.PROJECT_NAME || 'lighthouse-comments'; // Name of the job, which create an artifact const JOB_NAME = process.env.CI_JOB_NAME || process.env.JOB_NAME || 'lighthouse'; /* * Returns an artifact * * @param name {String} - artifact file name * @return {Object} - object with performance artifact * @throw {Error} - thhrow an error, if artifact contain string, that can't be parsed as a JSON. Or in case of fetch errors. */ const getArtifact = async name => { const response = await fetch(`https://${GITLAB_DOMAIN}/${NAME_SPACE}/${PROJECT}/-/jobs/artifacts/master/raw/${name}?job=${JOB_NAME}`); if (!response.ok) throw new Error('Artifact not found'); const data = await response.json(); return data; };
Création d'un texte de commentaire
Nous devons créer un texte de commentaire au format markdown
. Créons quelques fonctions de service qui nous aideront :
/** * Return part of report for specific page * * @param report {Object} — report * @param subject {String} — subject, that allow find specific page * @return {Object} — page report */ const getPage = (report, subject) => report.find(item => (item.subject === subject)); /** * Return specific metric for the page * * @param page {Object} — page * @param name {String} — metrics name * @return {Object} — metric */ const getMetric = (page, name) => page.metrics.find(item => item.name === name); /** * Return table cell for desired metric * * @param branch {Object} - report from feature branch * @param master {Object} - report from master branch * @param name {String} - metrics name */ const buildCell = (branch, master, name) => { const branchMetric = getMetric(branch, name); const masterMetric = getMetric(master, name); const branchValue = branchMetric.value; const masterValue = masterMetric.value; const desiredLarger = branchMetric.desiredSize === 'larger'; const isChanged = branchValue !== masterValue; const larger = branchValue > masterValue; if (!isChanged) return `${branchValue}`; if (larger) return `${branchValue} ${desiredLarger ? '' : '' } **+${Math.abs(branchValue - masterValue).toFixed(2)}**`; return `${branchValue} ${!desiredLarger ? '' : '' } **-${Math.abs(branchValue - masterValue).toFixed(2)}**`; }; /** * Returns text of the comment with table inside * This table contain changes in all metrics * * @param branch {Object} report from feature branch * @param master {Object} report from master branch * @return {String} comment markdown */ const buildCommentText = (branch, master) =>{ const md = branch.map( page => { const pageAtMaster = getPage(master, page.subject); if (!pageAtMaster) return ''; const md = `|${page.subject}|${buildCell(page, pageAtMaster, 'Performance')}|${buildCell(page, pageAtMaster, 'Accessibility')}|${buildCell(page, pageAtMaster, 'Best Practices')}|${buildCell(page, pageAtMaster, 'SEO')}| `; return md; }).join(''); return ` |Path|Performance|Accessibility|Best Practices|SEO| |--- |--- |--- |--- |--- | ${md} `; };
Script qui construira un commentaire
Vous aurez besoin d'un jeton pour travailler avec l'API GitLab. Pour en générer un, vous devez ouvrir GitLab, vous connecter, ouvrir l'option "Paramètres" du menu, puis ouvrir "Access Tokens" qui se trouve sur le côté gauche du menu de navigation. Vous devriez alors pouvoir voir le formulaire, qui vous permet de générer le jeton.

De plus, vous aurez besoin d'un ID du projet. Vous pouvez le trouver dans le référentiel 'Paramètres' (dans le sous-menu 'Général'):

Pour ajouter un commentaire à la demande de fusion, nous devons connaître son ID. La fonction qui vous permet d'acquérir l'ID de demande de fusion ressemble à ceci :
// You can set environment variables via CI/CD UI. // @see https://gitlab.com/help/ci/variables/README#variables // I have set GITLAB_TOKEN this way // ID of the project const GITLAB_PROJECT_ID = process.env.CI_PROJECT_ID || '18090019'; // Token const TOKEN = process.env.GITLAB_TOKEN; /** * Returns iid of the merge request from feature branch to master * @param from {String} — name of the feature branch * @param to {String} — name of the master branch * @return {Number} — iid of the merge request */ const getMRID = async (from, to) => { const response = await fetch(`https://${GITLAB_DOMAIN}/api/v4/projects/${GITLAB_PROJECT_ID}/merge_requests?target_branch=${to}&source_branch=${from}`, { method: 'GET', headers: { 'PRIVATE-TOKEN': TOKEN, } }); if (!response.ok) throw new Error('Merge request not found'); const [{iid}] = await response.json(); return iid; };
We need to get a feature branch name. You may use the environment variable CI_COMMIT_REF_SLUG
inside the pipeline. Outside of the pipeline, you can use the current-git-branch
package. Also, you will need to form a message body.
Let's install the packages we need for this matter:
npm i -S current-git-branch form-data
And now, finally, function to add a comment:
const FormData = require('form-data'); const branchName = require('current-git-branch'); // Branch from which we are making merge request // In the pipeline we have environment variable `CI_COMMIT_REF_NAME`, // which contains name of this banch. Function `branchName` // will return something like «HEAD detached» message in the pipeline. // And name of the branch outside of pipeline const CURRENT_BRANCH = process.env.CI_COMMIT_REF_NAME || branchName(); // Merge request target branch, usually it's master const DEFAULT_BRANCH = process.env.CI_DEFAULT_BRANCH || 'master'; /** * Adding comment to merege request * @param md {String} — markdown text of the comment */ const addComment = async md => { const iid = await getMRID(CURRENT_BRANCH, DEFAULT_BRANCH); const commentPath = `https://${GITLAB_DOMAIN}/api/v4/projects/${GITLAB_PROJECT_ID}/merge_requests/${iid}/notes`; const body = new FormData(); body.append('body', md); await fetch(commentPath, { method: 'POST', headers: { 'PRIVATE-TOKEN': TOKEN, }, body, }); };
And now we can generate and add a comment:
cluster.on('message', (worker, msg) => { report = [...report, ...msg]; worker.disconnect(); reportsCount++; if (reportsCount === chunks.length) { fs.writeFileSync(`./performance.json`, JSON.stringify(report)); + if (CURRENT_BRANCH === DEFAULT_BRANCH) process.exit(0); + try { + const masterReport = await getArtifact('performance.json'); + const md = buildCommentText(report, masterReport) + await addComment(md); + } catch (error) { + console.log(error); + } process.exit(0); } });
- Check the gist and demo repository.
Now create a merge request and you will get:

Continuer
Comments are much less visible than widgets but it's still much better than nothing. This way we can visualize the performance even without artifacts.
Authentification
OK, but what about authentication? The performance of the pages that require authentication is also important. It's easy: we will simply log in. puppeteer
is essentially a fully-fledged browser and we can write scripts that mimic user actions:
const LOGIN_URL = '/login'; const USER_EMAIL = process.env.USER_EMAIL; const USER_PASSWORD = process.env.USER_PASSWORD; /** * Authentication sctipt * @param browser {Object} — browser instance */ const login = async browser => { const page = await browser.newPage(); page.setCacheEnabled(false); await page.goto(`${DOMAIN}${LOGIN_URL}`, { waitUntil: 'networkidle2' }); await page.click('input[name=email]'); await page.keyboard.type(USER_EMAIL); await page.click('input[name=password]'); await page.keyboard.type(USER_PASSWORD); await page.click('button[data-test]', { waitUntil: 'domcontentloaded' }); };
Before checking a page that requires authentication, we may just run this script. Terminé.
Sommaire
In this way, I built the performance monitoring system at Werkspot — a company I currently work for. It's great when you have the opportunity to experiment with the bleeding edge technology.
Now you also know how to visualize performance change, and it's sure to help you better track performance degradation. But what comes next? You can save the data and visualize it for a time period in order to better understand the big picture, and you can collect performance data directly from the users.
You may also check out a great talk on this subject: “Measuring Real User Performance In The Browser.” When you build the system that will collect performance data and visualize them, it will help to find your performance bottlenecks and resolve them. Good luck with that!