Catégories
Astuces et Design

Comment rendre les performances visibles avec GitLab CI et Hoodoo d'artefacts GitLab – Smashing Magazine

A propos de l'auteur

  • Développeur indépendant depuis 16 ans. Vendu aux sociétés.
  • Jack de tous les métiers, maître de rien. Plus de la dernière partie.
  • Fondateur et rédacteur en chef de la…

Plus à propos
Anton

Il ne suffit pas d'optimiser une application. Vous devez empêcher les performances de se dégrader et la première étape consiste à rendre les modifications de performances visibles. Dans cet article, Anton Nemtsev montre quelques façons de les afficher dans les demandes de fusion GitLab.

La dégradation des performances est un problème auquel nous sommes confrontés quotidiennement. Nous pourrions nous efforcer de rendre l'application flamboyante rapidement, mais nous finissons bientôt là où nous avons commencé. Cela se produit en raison de l'ajout de nouvelles fonctionnalités et du fait que nous n'avons parfois pas une seconde pensée sur les packages que nous ajoutons et mettons à jour en permanence, ou 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 des clients. Nous ne pouvons pas nous permettre de passer régulièrement du temps à optimiser les applications à nouveau. C'est coûteux et complexe. Et cela signifie qu'en dépit de 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'il peut apporter, alors nous sommes prêts à partir.

Comment créer un budget de performance pour un projet

Les premières questions que nous devons nous poser sont:

"Quel est le projet performant?"

"Quelles mesures dois-je utiliser?"

"Quelles valeurs de ces métriques sont acceptables?"

La sélection des mesures est en dehors de la portée de cet article et dépend fortement du contexte du projet, mais je vous recommande de commencer par lire les mesures de performances 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 mesure indique le temps qu'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 terminant par des problèmes de latence. Il est donc agréable de l'utiliser en conjonction avec la synchronisation du serveur ou OpenTracing pour savoir en quoi il consiste exactement.

Vous devez également prendre en compte des mesures telles que le délai d'interactivité (TTI) et la première peinture significative (cette dernière sera bientôt remplacée par la plus grande peinture au contenu (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: les métriques sont toujours liées au contexte, alors s'il vous plaît ne prenez pas cela pour acquis. Réfléchissez à ce qui est important dans votre cas spécifique.

La façon la 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 Performance Budget Calculator peuvent être 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 l'effort de rendre l'application flamboyante rapidement, mais nous finissons bientôt là où nous avons commencé.

Utilisez des concurrents à votre avantage

Si vous vous êtes déjà échappé d'un ours extatique surexcité, alors vous savez déjà que vous n'avez pas besoin d'être un champion olympique de la course à pied pour sortir de ce problème. Vous devez juste ê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. Par exemple, pour une boutique Internet, il peut s'agir d'une page avec une liste de produits, une page de détails sur le produit, un panier, une caisse, etc.

  1. Mesurez les valeurs de vos mesures sélectionnées sur chaque type de page pour les projets de vos concurrents;
  2. Mesurez les mêmes métriques sur votre projet;
  3. Trouvez la valeur la plus proche de votre valeur pour chaque statistique des projets du concurrent. Ajouter 20% à eux et définir comme vos prochains objectifs.

Pourquoi 20%? Il s'agit d'un nombre magique qui signifie que la différence sera perceptible à l'œil nu. Vous pouvez en savoir plus sur ce nombre dans l'article de Denys Mishunov «Pourquoi les performances perçues sont importantes, partie 1: la perception du temps».

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 mesure de performance de votre projet sur chaque type de page, puis améliorez-les de 20%.

Tests synthétiques

Il existe deux façons de mesurer les performances:

  • Synthétique (dans un environnement contrôlé)
  • RHUM (Mesures réelles de l'utilisateur)
    Les données sont collectées auprès de vrais utilisateurs 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 de projet.

Bibliothèque et sa taille en tant que métrique

Supposons que vous avez 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 vos concurrents – afin qu'il ait moins d'impact sur la taille finale du projet résultant. Cela permet d'économiser du trafic client – parfois du trafic pour lequel le client paie. Cela permet également de charger le projet plus rapidement, ce qui est assez important en ce qui concerne la part mobile croissante et les nouveaux marchés avec des vitesses de connexion lentes et une couverture Internet fragmentée.

Package 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 fil du temps de 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 "size-limit":({},{},…) Le bloc contient une liste de la taille des fichiers dont nous voulons vérifier. Dans notre cas, il s'agit d'un seul fichier: index.js.

Script NPM size exécute juste le size-limit package, qui lit le bloc de configuration size-limit mentionné 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
Le résultat de l'exécution de la commande montre la taille de index.js
Le résultat de l'exécution de la commande montre la taille de index.js. (Grand aperçu)

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.

Une capture d'écran du terminal; la taille du fichier est inférieure à la limite et s'affiche en vert
Une capture d'écran du terminal; la taille du fichier est inférieure à la limite et s'affiche en vert. (Grand aperçu)

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 différent de zéro. Cela, outre d'autres choses, signifie qu'il arrêtera le pipeline dans le CI GitLab.

Une capture d'écran du terminal où la taille du fichier dépasse la limite et est affichée en rouge. Le script a été terminé avec un code différent de zéro.
Une capture d'écran du terminal où la taille du fichier dépasse la limite et est affichée en rouge. Le script a été terminé avec un code différent de zéro. (Grand aperçu)

Maintenant, nous pouvons 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 de manière simple et agréable.

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 commit ne soit exécuté automatiquement npm run size et si elle se termine par un code différent de zéro, la validation ne se produira jamais.

Une capture d'écran du terminal où la validation est abandonnée car la taille du fichier dépasse la limite
Une capture d'écran du terminal où la validation est abandonnée car la taille du fichier dépasse la limite. (Grand aperçu)

Mais il existe de nombreuses façons de sauter les crochets (intentionnellement ou même par accident), nous ne devons donc pas trop compter sur eux.

De plus, il est important de noter que nous ne devrions pas avoir à effectuer ce blocage de 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 permettra d'éviter une augmentation de taille accidentelle en raison de l'introduction d'une bibliothèque d'aide dont nous n'avons pas besoin. Et, peut-être, donnez 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? Voyons la modification de la taille du fichier directement dans la demande de fusion! Mais vous ne poussez pas pour maîtriser directement; vous agissez comme un développeur adulte, non?

Exécution de notre vérification sur GitLab CI

Ajoutons un artefact GitLab de type métrique. Un artefact est un fichier qui «vivra» une fois l'opération de pipeline terminée. Ce type d'artefact spécifique nous permet d'afficher un widget supplémentaire dans la demande de fusion, montrant tout changement dans la valeur de la métrique entre l'artefact dans le maître et la branche de fonctionnalité. Le format du metrics l'artefact est un format texte Prométhée. Pour les valeurs GitLab à l'intérieur de l'artefact, c'est juste du 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?

  1. Définissez les artefacts dans le pipeline.
  2. Modifiez le script afin qu'il crée un artefact sur le pipeline.

Pour créer un artefact, nous devons changer .gitlab-ci.yml par ici:

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
  1. expire_in: 7 days – l'artefact existera pendant 7 jours.
  2. paths:
      metric.txt

    Il sera enregistré dans le catalogue racine. Si vous sautez cette option, il ne sera pas possible de la télécharger.

  3. reports:
      metrics: metric.txt
    

    L'artefact aura le type reports:metrics

Maintenant, faisons en sorte que Size Limit génère un rapport. Pour ce faire, nous devons changer package.json:

"scripts": {
-  "size": "size-limit",
+  "size": "size-limit --json > size-limit.json",
  "test": "jest && eslint ."
},

size-limit avec clé --json affichera les données au format json:

La commande size-limit --json affiche JSON sur la console. JSON contient un tableau d'objets qui contiennent un nom et une taille de fichier, ainsi que nous permet de savoir s'il dépasse la limite de taille
La commande size-limit --json sortie JSON sur console. JSON contient un tableau d'objets qui contiennent un nom et une taille de fichier, ainsi que nous permet de savoir s'il dépasse la limite de taille. (Grand aperçu)

Et redirection > size-limit.json va enregistrer JSON dans un 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 ."
},

Parce que nous avons utilisé le post préfixe, le npm run size exécutera la size script d'abord, puis, automatiquement, exécutez le postsize script, qui se traduira par la création de la metric.txt fichier, notre artefact.

Par conséquent, lorsque nous fusionnons cette branche en master, modifions quelque chose et créons une nouvelle demande de fusion, nous verrons ce qui suit:

Capture d'écran avec une demande de fusion, qui nous montre un widget avec une nouvelle et une ancienne valeur métrique entre crochets
Capture d'écran avec une demande de fusion, qui nous montre un widget avec une nouvelle et une ancienne valeur métrique entre crochets. (Grand aperçu)

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 fonction ainsi que de la valeur dans le masque entre crochets.

Maintenant, nous pouvons réellement voir comment modifier la taille du package et prendre une décision raisonnable si nous devons le fusionner ou non.

CV

D'ACCORD! Nous avons donc compris comment gérer le cas trivial. Si vous avez plusieurs fichiers, séparez simplement les mesures par des sauts de ligne. Comme alternative à la taille limite, vous pouvez envisager la taille de lot. Si vous utilisez WebPack, vous pouvez obtenir toutes les tailles dont vous avez besoin en créant avec --profile et --json drapeaux:

webpack --profile --json > stats.json

Si vous utilisez next.js, vous pouvez utiliser le plugin @ next / bundle-analyzer. C'est à vous!

Utilisation du phare

Le phare est la norme de facto en 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 fera les mesures. Nous devons également installer le marionnettiste que nous utiliserons comme navigateur sans tête.

npm i -D lighthouse puppeteer

Ensuite, créons un lighthouse.js script et lancez 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 URL comme argument et générer un rapport après l'avoir passé URL à la lighthouse.

Nous transmettons les arguments suivants au lighthouse:

  1. L'adresse que nous voulons analyser;
  2. lighthouse options, navigateur port en particulier, et output (format de sortie du rapport);
  3. report configuration et lighthouse:full (tout ce que nous pouvons mesurer). Pour une configuration plus précise, consultez la documentation.

Magnifique! Nous avons maintenant notre rapport. Mais que pouvons-nous 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 

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 leurs artefacts dans leur 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 JSON fichier qui contient un tableau des objets. Chacun d'eux représente un rapport sur un URL.

({page 1}, {page 2}, …)

Chaque page est représentée par un objet avec les attributs suivants:

  1. subject
    Identifiant de page (c'est assez pratique pour utiliser un tel chemin);
  2. metrics
    Un tableau des objets (chacun d'eux représente une mesure qui a été faite sur la page).
{
  "subject":"/login/",
  "metrics":({measurement 1}, {measurement 2}, {measurement 3}, …)
}

UNE measurement est un objet qui contient les attributs suivants:

  1. name
    Nom de la mesure, par ex. c'est possible Time to first byte ou Time to interactive.
  2. value
    Résultat de mesure numérique.
  3. desiredSize
    Si la valeur cible doit être aussi petite que possible, par ex. pour le Time to interactive métrique, la valeur doit être smaller. S'il doit être aussi grand que possible, par ex. pour le phare Performance score, puis utilisez larger.
{
  "name":"Time to first byte (ms)",
  "value":240,
  "desiredSize":"smaller"
}

Modifions notre buildReport fonctionner de manière à renvoyer un rapport pour une page avec des mesures de phare standard.

Capture d'écran avec rapport du phare. Il y a le score de performance, le score a11y, le score des meilleures pratiques, le score SEO
Capture d'écran avec rapport du phare. Il y a le score de performance, le score a11y, le score des meilleures pratiques, le score SEO. (Grand aperçu)
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, quand nous avons une fonction qui génère un rapport. Appliquons-le à chaque type de pages du projet. Tout d'abord, je dois dire que process.env.DOMAIN doit contenir un domaine intermédiaire (sur lequel vous devez déployer votre projet à partir d'une branche de fonctionnalité au préalable).

+ 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();
})();

Remarque: À ce stade, vous voudrez peut-être m'interrompre et crier en vain: "Pourquoi prenez-vous mon 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 nuit à la précision des résultats de mesure. De plus, si vous ne faites pas preuve d'ingéniosité, cela entraînera une exception.

Utilisation de plusieurs processus

Êtes-vous toujours dans des mesures parallèles? Très bien, vous souhaiterez 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 de n'en discuter que dans le cas où votre pipeline fonctionne 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 pleine pondération dans chaque fourche de processus (au lieu de réutiliser la même, ce qui entraînera une augmentation de la consommation de RAM). Tout cela signifie qu'il sera plus coûteux en raison des besoins matériels croissants et un peu plus rapide. Il peut sembler que le jeu ne vaut pas la chandelle.

Si vous voulez prendre ce risque, vous devrez:

  1. Divisez le tableau d'URL en morceaux par nombre de cœurs;
  2. Créez un fork d'un processus en fonction du nombre de cœurs;
  3. 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 à plusieurs fichiers. 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;
}

Faire 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 ensemble de morceaux aux processus enfants et aux rapports rétrospectifs:

(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();
+     });
  }
})();

Enfin, réassemblez les rapports dans un seul tableau et générez un artefact.

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 pouvons-nous 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();
    });
  }

Et maintenant, nous pouvons ajouter lighthouse dans le pipeline.

L'ajouter au pipeline

Créez d'abord 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:

Une capture d'écran de la page de demande de fusion. Il y a un widget qui montre quelles mesures de phare ont changé et comment exactement
Une capture d'écran de la page de demande de fusion. Il y a un widget qui montre quelles mesures de phare ont changé et comment exactement. (Grand aperçu)

Agréable?

CV

Nous en avons finalement 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 les produits open source pilotés par la communauté à ceux appartenant à un monstre d'entreprise?

Pas de repos pour les méchants

Il peut sembler que nous y sommes enfin, mais non, pas encore. Si vous utilisez une version GitLab payante, des artefacts avec des types de rapports metrics et performance sont présents dans les plans à partir de premium et silver qui coûte 19 $ par mois pour chaque utilisateur. En outre, vous ne pouvez pas simplement acheter une fonctionnalité spécifique dont vous avez besoin – vous pouvez uniquement modifier le plan. Désolé. Donc ce que nous pouvons faire? Contrairement à GitHub avec son API Checks et son API d'état, GitLab ne vous permettrait pas de créer vous-même un widget réel dans la demande de fusion. Et voici aucun espoir de les obtenir de sitôt.

Une capture d'écran du tweet publié par Ilya Klimov (employé de GitLab) a écrit sur la probabilité d'apparition d'analogues pour Github Checks et Status API: «Extrêmement improbable. Les contrôles sont déjà disponibles via l'API de statut de validation, et comme pour les statuts, nous nous efforçons d'être un écosystème fermé. »
Une capture d'écran du tweet publié par Ilya Klimov (employé de GitLab) qui a écrit sur la probabilité d'apparition d'analogues pour Github Checks et Status API. (Grand aperçu)

Une façon de vérifier si vous avez réellement pris en charge ces fonctionnalités: Vous pouvez rechercher la variable d'environnement GITLAB_FEATURES en cours. 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, un 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 soigneusement pour comprendre ce qui se passait.

Alors quel est le plan?

  1. Nous devons lire l'artefact du master branche;
  2. Créez un commentaire dans le markdown format;
  3. Récupère l'identifiant de la demande de fusion de la branche de fonctionnalité actuelle vers le maître;
  4. Ajoutez le commentaire.

Comment lire l'artefact de la branche principale

Si nous voulons montrer comment les mesures de performance sont modifiées entre master et des branches de fonctionnalités, 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;
};

Nous devons créer un texte de commentaire dans le markdown format. 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}
`;
};

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 les «jetons d’accès» qui se trouvent 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.

Capture d'écran, qui montre le formulaire de génération de jetons et les options de menu que j'ai mentionnés ci-dessus.
Capture d'écran, qui montre le formulaire de génération de jetons et les options de menu que j'ai mentionnés ci-dessus. (Grand aperçu)

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»):

La capture d'écran montre la page des paramètres, où vous pouvez trouver l'ID du projet
La capture d'écran montre la page des paramètres, où vous pouvez trouver l'ID du projet. (Grand aperçu)

Pour ajouter un commentaire à la demande de fusion, nous devons connaître son ID. La fonction qui vous permet d'acquérir un 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;
};

Nous devons obtenir un nom de branche de fonctionnalité. Vous pouvez utiliser la variable d'environnement CI_COMMIT_REF_SLUG à l'intérieur du pipeline. En dehors du pipeline, vous pouvez utiliser le current-git-branch paquet. De plus, vous devrez former un corps de message.

Installons les packages dont nous avons besoin à ce sujet:

npm i -S current-git-branch form-data

Et maintenant, enfin, fonction pour ajouter un commentaire:

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,
  });
};

Et maintenant, nous pouvons générer et ajouter un commentaire:

    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);
      }
    });

Créez maintenant une demande de fusion et vous obtiendrez:

Une capture d'écran de la demande de fusion qui montre un commentaire avec un tableau qui contient un tableau avec le changement des mesures de phare
Une capture d'écran de la demande de fusion qui montre un commentaire avec un tableau qui contient un tableau avec le changement des mesures de phare. (Grand aperçu)
CV

Les commentaires sont beaucoup moins visibles que les widgets, mais c'est toujours mieux que rien. De cette façon, nous pouvons visualiser les performances même sans artefacts.

Authentification

OK, mais qu'en est-il de l'authentification? Les performances des pages qui nécessitent une authentification sont également importantes. C'est simple: nous allons simplement nous connecter. puppeteer est essentiellement un navigateur à part entière et nous pouvons écrire des scripts qui imitent les actions des utilisateurs:

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-testid="submit")', { waitUntil: 'domcontentloaded' });
};

Avant de vérifier une page qui nécessite une authentification, nous pouvons simplement exécuter ce script. Terminé.

Sommaire

De cette façon, j'ai construit le système de surveillance des performances au Werkspot – une entreprise pour laquelle je travaille actuellement. C'est formidable lorsque vous avez la possibilité d'expérimenter la technologie de pointe.

Maintenant, vous savez également comment visualiser les changements de performances, et cela vous aidera à mieux suivre la dégradation des performances. Mais qu'est-ce qui vient ensuite? Vous pouvez enregistrer les données et les visualiser pendant une période afin de mieux comprendre la situation dans son ensemble, et vous pouvez collecter des données de performances directement auprès des utilisateurs.

Vous pouvez également consulter une excellente conférence sur ce sujet: «Mesurer les performances réelles des utilisateurs dans le navigateur». Lorsque vous créez le système qui collectera les données de performances et les visualisera, cela vous aidera à trouver vos goulots d'étranglement de performances et à les résoudre. Bonne chance avec ça!

Smashing Editorial(rail)

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *