Come rendere visibili le prestazioni con GitLab CI e Hoodoo of GitLab Artifacts

Pubblicato: 2022-03-10
Riassunto veloce ↬ Non è sufficiente ottimizzare un'applicazione. È necessario impedire il degrado delle prestazioni e il primo passaggio per farlo è rendere visibili le modifiche alle prestazioni. In questo articolo, Anton Nemtsev mostra un paio di modi per mostrarli nelle richieste di unione di GitLab.

Il degrado delle prestazioni è un problema che affrontiamo quotidianamente. Potremmo impegnarci per rendere l'applicazione velocissima, ma presto arriviamo al punto di partenza. Succede a causa dell'aggiunta di nuove funzionalità e del fatto che a volte non ci ripensiamo sui pacchetti che aggiungiamo e aggiorniamo costantemente, o pensiamo alla complessità del nostro codice. In genere è una piccola cosa, ma è ancora tutta una questione di piccole cose.

Non possiamo permetterci di avere un'app lenta. Le prestazioni sono un vantaggio competitivo che può portare e fidelizzare i clienti. Non possiamo permetterci di dedicare regolarmente tempo all'ottimizzazione delle app di nuovo. È costoso e complesso. E ciò significa che, nonostante tutti i vantaggi delle prestazioni dal punto di vista aziendale, non è affatto redditizio. Come primo passo per trovare una soluzione per qualsiasi problema, dobbiamo rendere visibile il problema. Questo articolo ti aiuterà esattamente con questo.

Nota : se hai una conoscenza di base di Node.js, una vaga idea di come funziona il tuo CI/CD e ti interessano le prestazioni dell'app o i vantaggi aziendali che può apportare, allora siamo a posto.

Come creare un budget di prestazioni per un progetto

Le prime domande che dovremmo porci sono:

"Qual è il progetto performante?"

"Quali metriche dovrei usare?"

"Quali valori di queste metriche sono accettabili?"

La selezione delle metriche è al di fuori dell'ambito di questo articolo e dipende fortemente dal contesto del progetto, ma ti consiglio di iniziare leggendo le metriche delle prestazioni incentrate sull'utente di Philip Walton.

Dal mio punto di vista, è una buona idea usare la dimensione della libreria in kilobyte come metrica per il pacchetto npm. Come mai? Bene, è perché se altre persone includono il tuo codice nei loro progetti, forse vorrebbero ridurre al minimo l'impatto del tuo codice sulla dimensione finale della loro applicazione.

Per il sito, considererei Time To First Byte (TTFB) come una metrica. Questa metrica mostra quanto tempo impiega il server per rispondere con qualcosa. Questa metrica è importante, ma piuttosto vaga perché può includere qualsiasi cosa, a partire dal tempo di rendering del server e finendo con problemi di latenza. Quindi è bello usarlo insieme a Server Timing o OpenTracing per scoprire in cosa consiste esattamente.

Dovresti anche considerare metriche come Time to Interactive (TTI) e First Meaningful Paint (quest'ultimo sarà presto sostituito con Largest Contentful Paint (LCP)). Penso che entrambi siano i più importanti, dal punto di vista della performance percepita.

Ma tieni presente: le metriche sono sempre correlate al contesto , quindi per favore non darlo per scontato. Pensa a cosa è importante nel tuo caso specifico.

Il modo più semplice per definire i valori desiderati per le metriche è utilizzare i tuoi concorrenti o anche te stesso. Inoltre, di tanto in tanto, strumenti come Performance Budget Calculator possono tornare utili: basta giocarci un po'.

Il degrado delle prestazioni è un problema che affrontiamo quotidianamente. Potremmo impegnarci per rendere l'applicazione velocissima, ma presto arriviamo al punto di partenza.

Usa i concorrenti a tuo vantaggio

Se ti è mai capitato di scappare da un orso estaticamente sovraeccitato, allora sai già che non è necessario essere un campione olimpico nella corsa per uscire da questo problema. Devi solo essere un po' più veloce dell'altro ragazzo.

Quindi fai una lista di concorrenti. Se si tratta di progetti dello stesso tipo, di solito sono costituiti da tipi di pagina simili tra loro. Ad esempio, per un negozio online, potrebbe essere una pagina con un elenco di prodotti, una pagina dei dettagli del prodotto, un carrello degli acquisti, un checkout e così via.

  1. Misura i valori delle metriche selezionate su ogni tipo di pagina per i progetti dei tuoi concorrenti;
  2. Misura le stesse metriche sul tuo progetto;
  3. Trova il più vicino migliore del tuo valore per ogni metrica nei progetti della concorrenza. Aggiungendo loro il 20% e fissando i tuoi prossimi obiettivi.

Perché il 20%? Questo è un numero magico che presumibilmente significa che la differenza sarà evidente ad occhio nudo. Puoi leggere di più su questo numero nell'articolo di Denys Mishunov "Why Perceived Performance Matters, Part 1: The Perception Of Time".

Una lotta con un'ombra

Hai un progetto unico? Non hai concorrenti? O sei già migliore di tutti loro in tutti i sensi? Non è un problema. Puoi sempre competere con l'unico avversario degno, cioè te stesso. Misura ogni metrica di rendimento del tuo progetto su ogni tipo di pagina e poi migliorale dello stesso 20%.

Altro dopo il salto! Continua a leggere sotto ↓

Test sintetici

Esistono due modi per misurare le prestazioni:

  • Sintetico (in ambiente controllato)
  • RUM (Misurazioni dell'utente reale)
    I dati vengono raccolti da utenti reali in produzione.

In questo articolo, utilizzeremo test sintetici e presumeremo che il nostro progetto utilizzi GitLab con il suo CI integrato per la distribuzione del progetto.

Libreria e le sue dimensioni come metrica

Supponiamo che tu abbia deciso di sviluppare una libreria e pubblicarla in NPM. Vuoi mantenerlo leggero, molto più leggero dei concorrenti, in modo che abbia un impatto minore sulle dimensioni finali del progetto risultante. Ciò consente di risparmiare il traffico dei clienti, a volte il traffico per il quale il cliente sta pagando. Consente inoltre di caricare il progetto più velocemente, il che è piuttosto importante per quanto riguarda la crescente quota di dispositivi mobili e nuovi mercati con velocità di connessione lente e copertura Internet frammentata.

Pacchetto per misurare le dimensioni della libreria

Per ridurre al minimo le dimensioni della libreria, dobbiamo osservare attentamente come cambia nel tempo di sviluppo. Ma come puoi farlo? Bene, potremmo usare il pacchetto Size Limit creato da Andrey Sitnik di Evil Martians.

Installiamolo.

 npm i -D size-limit @size-limit/preset-small-lib

Quindi, aggiungilo a package.json .

 "scripts": { + "size": "size-limit", "test": "jest && eslint ." }, + "size-limit": [ + { + "path": "index.js" + } + ],

Il blocco "size-limit":[{},{},…] contiene un elenco delle dimensioni dei file di cui vogliamo controllare. Nel nostro caso, è solo un singolo file: index.js .

La size dello script NPM esegue semplicemente il pacchetto size-limit , che legge il blocco di configurazione size-limit menzionato in precedenza e controlla la dimensione dei file lì elencati. Eseguiamolo e vediamo cosa succede:

 npm run size 
Il risultato dell'esecuzione del comando mostra la dimensione di index.js
Il risultato dell'esecuzione del comando mostra la dimensione di index.js. (Grande anteprima)

Possiamo vedere la dimensione del file, ma questa dimensione non è effettivamente sotto controllo. Risolviamolo aggiungendo limit a package.json :

 "size-limit": [ { + "limit": "2 KB", "path": "index.js" } ],

Ora, se eseguiamo lo script, verrà convalidato rispetto al limite che abbiamo impostato.

Uno screenshot del terminale; la dimensione del file è inferiore al limite e viene visualizzata in verde
Uno screenshot del terminale; la dimensione del file è inferiore al limite e viene visualizzata in verde. (Grande anteprima)

Nel caso in cui un nuovo sviluppo modifichi la dimensione del file al punto da superare il limite definito, lo script verrà completato con codice diverso da zero. Questo, a parte altre cose, significa che interromperà la pipeline nel CI GitLab.

Uno screenshot del terminale in cui la dimensione del file supera il limite e viene visualizzato in rosso. Lo script è stato terminato con un codice diverso da zero.
Uno screenshot del terminale in cui la dimensione del file supera il limite e viene visualizzato in rosso. Lo script è stato terminato con un codice diverso da zero. (Grande anteprima)

Ora possiamo usare git hook per controllare la dimensione del file rispetto al limite prima di ogni commit. Potremmo anche usare il pacchetto husky per farlo in un modo semplice e piacevole.

Installiamolo.

 npm i -D husky

Quindi, modifica il nostro package.json .

 "size-limit": [ { "limit": "2 KB", "path": "index.js" } ], + "husky": { + "hooks": { + "pre-commit": "npm run size" + } + },

E ora, prima che ogni commit venga eseguito automaticamente, il comando npm run size e se finirà con un codice diverso da zero, il commit non avverrebbe mai.

Uno screenshot del terminale in cui il commit viene interrotto perché la dimensione del file supera il limite
Uno screenshot del terminale in cui il commit viene interrotto perché la dimensione del file supera il limite. (Grande anteprima)

Ma ci sono molti modi per saltare gli hook (intenzionalmente o anche per caso), quindi non dovremmo fare troppo affidamento su di essi.

Inoltre, è importante notare che non dovrebbe essere necessario bloccare questo controllo. Come mai? Perché va bene che la dimensione della libreria cresca mentre aggiungi nuove funzionalità. Dobbiamo rendere visibili i cambiamenti, tutto qui. Ciò consentirà di evitare un aumento accidentale delle dimensioni a causa dell'introduzione di una libreria di supporto di cui non abbiamo bisogno. E, forse, dare agli sviluppatori e ai proprietari di prodotti un motivo per considerare se la funzionalità aggiunta vale l'aumento delle dimensioni. O, forse, se ci sono pacchetti alternativi più piccoli. Bundlephobia ci consente di trovare un'alternativa per quasi tutti i pacchetti NPM.

Quindi cosa dovremmo fare? Mostriamo la modifica della dimensione del file direttamente nella richiesta di unione! Ma non spingi per padroneggiare direttamente; ti comporti come uno sviluppatore adulto, giusto?

Esecuzione del nostro controllo su GitLab CI

Aggiungiamo un artefatto GitLab del tipo metriche. Un artefatto è un file che «vivrà» al termine dell'operazione di pipeline. Questo specifico tipo di artefatto ci consente di mostrare un widget aggiuntivo nella richiesta di unione, mostrando qualsiasi modifica nel valore della metrica tra artefatto nel master e il ramo di funzionalità. Il formato dell'artefatto metrics è un formato di testo Prometheus. Per i valori GitLab all'interno dell'artefatto, è solo testo. GitLab non capisce cosa è cambiato esattamente nel valore, sa solo che il valore è diverso. Quindi, cosa dovremmo fare esattamente?

  1. Definire gli artefatti nella pipeline.
  2. Modificare lo script in modo che crei un artefatto nella pipeline.

Per creare un artefatto dobbiamo cambiare .gitlab-ci.yml questo modo:

 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'artefatto esisterà per 7 giorni.
  2.  paths: metric.txt

    Verrà salvato nel catalogo principale. Se salti questa opzione, non sarebbe possibile scaricarla.
  3.  reports: metrics: metric.txt

    L'artefatto avrà il tipo reports:metrics

Ora facciamo in modo che Size Limit generi un rapporto. Per fare ciò dobbiamo cambiare package.json :

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

size-limit con chiave --json genererà i dati in formato json:

Il comando size-limit --json restituisce JSON alla console. JSON contiene una matrice di oggetti che contengono un nome file e una dimensione, oltre a farci sapere se supera il limite di dimensione
Il comando size-limit --json restituisce JSON alla console. JSON contiene una matrice di oggetti che contengono un nome e una dimensione di file, oltre a farci sapere se supera il limite di dimensione. (Grande anteprima)

E il reindirizzamento > size-limit.json salverà JSON nel file size-limit.json .

Ora dobbiamo creare un artefatto da questo. Il formato si riduce a [metrics name][space][metrics value] . Creiamo lo script generate-metric.js :

 const report = require('./size-limit.json'); process.stdout.write(`size ${(report[0].size/1024).toFixed(1)}Kb`); process.exit(0);

E aggiungilo a package.json :

 "scripts": { "size": "size-limit --json > size-limit.json", + "postsize": "node generate-metric.js > metric.txt", "test": "jest && eslint ." },

Poiché abbiamo utilizzato il prefisso post , il comando npm run size eseguirà prima lo script size , quindi, automaticamente, eseguirà lo script postsize , che risulterà nella creazione del file metric.txt , il nostro artefatto.

Di conseguenza, quando uniamo questo ramo al master, cambiamo qualcosa e creiamo una nuova richiesta di unione, vedremo quanto segue:

Screenshot con una richiesta di unione, che ci mostra un widget con un valore di metrica nuovo e vecchio tra parentesi tonde
Screenshot con una richiesta di unione, che ci mostra un widget con un valore di metrica nuovo e vecchio tra parentesi tonde. (Grande anteprima)

Nel widget che appare nella pagina, vediamo per prima cosa il nome della metrica ( size ) seguito dal valore della metrica nel ramo delle funzionalità oltre al valore nel master tra parentesi tonde.

Ora possiamo effettivamente vedere come modificare le dimensioni del pacchetto e prendere una decisione ragionevole se unirlo o meno.

  • Potresti vedere tutto questo codice in questo repository.

Riprendere

OK! Quindi, abbiamo capito come gestire il caso banale. Se hai più file, separa le metriche con interruzioni di riga. In alternativa a Size Limit, potresti prendere in considerazione la dimensione del pacchetto. Se stai usando WebPack, puoi ottenere tutte le dimensioni di cui hai bisogno costruendo con i --profile e --json :

 webpack --profile --json > stats.json

Se stai usando next.js, puoi usare il plugin @next/bundle-analyzer. Tocca a voi!

Usando il faro

Lighthouse è lo standard de facto nell'analisi dei progetti. Scriviamo uno script che ci permetta di misurare le prestazioni, a11y, le migliori pratiche e fornirci un punteggio SEO.

Script per misurare tutte le cose

Per iniziare, dobbiamo installare il pacchetto faro che effettuerà le misurazioni. Abbiamo anche bisogno di installare burattinaio che useremo come browser senza testa.

 npm i -D lighthouse puppeteer

Quindi, creiamo uno script lighthouse.js e avviamo il nostro browser:

 const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); })();

Ora scriviamo una funzione che ci aiuterà ad analizzare un determinato URL:

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

Grande! Ora abbiamo una funzione che accetterà l'oggetto browser come argomento e restituirà una funzione che accetterà URL come argomento e genererà un rapporto dopo aver passato URL al lighthouse .

Stiamo passando i seguenti argomenti al lighthouse :

  1. L'indirizzo che vogliamo analizzare;
  2. opzioni del lighthouse , port del browser in particolare e output (formato di output del rapporto);
  3. report configurazione e lighthouse:full (tutto ciò che possiamo misurare). Per una configurazione più precisa, consultare la documentazione.

Meraviglioso! Ora abbiamo il nostro rapporto. Ma cosa possiamo farci? Bene, possiamo controllare le metriche rispetto ai limiti e uscire dallo script con un codice diverso da zero che arresterà la pipeline:

 if (report.categories.performance.score < 0.8) process.exit(1);

Ma vogliamo solo rendere le prestazioni visibili e non bloccanti? Quindi adottiamo un altro tipo di artefatto: GitLab performance artefact.

Artefatto delle prestazioni di GitLab

Per comprendere questo formato di artefatti, dobbiamo leggere il codice del plugin sitespeed.io. (Perché GitLab non può descrivere il formato dei propri artefatti all'interno della propria documentazione? Mistero. )

 [ { "subject":"/", "metrics":[ { "name":"Transfer Size (KB)", "value":"19.5", "desiredSize":"smaller" }, { "name":"Total Score", "value":92, "desiredSize":"larger" }, {…} ] }, {…} ]

Un artefatto è un file JSON che contiene una matrice di oggetti. Ciascuno di essi rappresenta un rapporto su un URL .

 [{page 1}, {page 2}, …]

Ogni pagina è rappresentata da un oggetto con i seguenti attributi:

  1. subject
    Identificatore di pagina (è abbastanza comodo usare un tale percorso);
  2. metrics
    Un array di oggetti (ciascuno di essi rappresenta una misurazione effettuata sulla pagina).
 { "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }

Una measurement è un oggetto che contiene i seguenti attributi:

  1. name
    Nome della misurazione, ad es. può essere Time to first byte o Time to interactive .
  2. value
    Risultato della misurazione numerica.
  3. desiredSize
    Se il valore target deve essere il più piccolo possibile, ad esempio per la metrica Time to interactive , il valore dovrebbe essere smaller . Se dovrebbe essere il più grande possibile, ad esempio per il Performance score del faro, utilizzare larger .
 { "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }

Modifichiamo la nostra funzione buildReport in modo che restituisca un report per una pagina con metriche standard del faro.

Screenshot con il rapporto del faro. Ci sono punteggio delle prestazioni, punteggio a11y, punteggio delle migliori pratiche, punteggio SEO
Screenshot con il rapporto del faro. Ci sono punteggio delle prestazioni, punteggio a11y, punteggio delle migliori pratiche, punteggio SEO. (Grande anteprima)
 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, }; }

Ora, quando abbiamo una funzione che genera un report. Applichiamolo ad ogni tipo di pagine del progetto. Innanzitutto, devo affermare che process.env.DOMAIN dovrebbe contenere un dominio di staging (in cui è necessario distribuire in anticipo il progetto da un ramo di funzionalità).

 + 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(); })();
  • Puoi trovare la fonte completa in questo succo e l'esempio funzionante in questo repository.

Nota : a questo punto, potresti volermi interrompere e urlare invano: "Perché mi stai prendendo il mio tempo? Non puoi nemmeno usare Promise.all correttamente!" A mia discolpa, oserei dire, che non è consigliabile eseguire più di un'istanza di faro contemporaneamente perché ciò influisce negativamente sull'accuratezza dei risultati della misurazione. Inoltre, se non mostri la dovuta ingegnosità, ciò comporterà un'eccezione.

Utilizzo di più processi

Ti piacciono ancora le misurazioni parallele? Bene, potresti voler usare il cluster di nodi (o anche i thread di lavoro se ti piace giocare in grassetto), ma ha senso discuterne solo nel caso in cui la tua pipeline sia in esecuzione nell'ambiente con più cors disponibili. E anche in questo caso, dovresti tenere presente che, a causa della natura di Node.js, avrai un'istanza Node.js a tutto peso generata in ogni fork del processo (invece di riutilizzare lo stesso che comporterà un crescente consumo di RAM). Tutto ciò significa che sarà più costoso a causa della crescente richiesta di hardware e un po' più veloce. Può sembrare che il gioco non valga la candela.

Se vuoi correre questo rischio, dovrai:

  1. Dividi l'array URL in blocchi per numero di core;
  2. Crea un fork di un processo in base al numero dei core;
  3. Trasferisci parti dell'array ai fork e quindi recupera i report generati.

Per dividere un array, puoi usare approcci multipli. Il codice seguente, scritto in un paio di minuti, non sarebbe peggiore degli altri:

 /** * 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; }

Crea i fork in base al conteggio dei core:

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

Trasferiamo una matrice di blocchi ai processi figlio e recuperiamo i report:

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

Infine, riassembla i report in un array e genera un artefatto.

  • Dai un'occhiata al codice completo e al repository con un esempio che mostra come utilizzare il faro con più processi.

Precisione delle misurazioni

Ebbene, abbiamo messo in parallelo le misurazioni, il che ha aumentato il già sfortunato grande errore di misurazione del lighthouse . Ma come lo riduciamo? Bene, fai alcune misurazioni e calcola la media.

Per fare ciò, scriveremo una funzione che calcolerà la media tra i risultati della misurazione corrente e quelli precedenti.

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

Quindi, cambia il nostro codice per usarli:

 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(); }); }
  • Dai un'occhiata al succo con il codice completo e il repository con un esempio.

E ora possiamo aggiungere il lighthouse all'oleodotto.

Aggiungendolo alla pipeline

Innanzitutto, crea un file di configurazione denominato .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

I pacchetti multipli installati sono necessari per il puppeteer . In alternativa, potresti prendere in considerazione l'utilizzo di docker . A parte questo, ha senso il fatto che impostiamo il tipo di artefatto come performance. E, non appena lo avranno sia master che feature branch, vedrai un widget come questo nella richiesta di unione:

Uno screenshot della pagina della richiesta di unione. C'è un widget che mostra quali metriche del faro sono cambiate e come esattamente
Uno screenshot della pagina della richiesta di unione. C'è un widget che mostra quali metriche del faro sono cambiate e come esattamente. (Grande anteprima)

Carino?

Riprendere

Abbiamo finalmente chiuso con un caso più complesso. Ovviamente, ci sono molti strumenti simili oltre al faro. Ad esempio, sitespeed.io. La documentazione di GitLab contiene anche un articolo che spiega come utilizzare sitespeed nella pipeline di GitLab. C'è anche un plugin per GitLab che ci permette di generare un artefatto. Ma chi preferirebbe i prodotti open source guidati dalla comunità a quelli di proprietà di un mostro aziendale?

Non c'è riposo per i malvagi

Può sembrare che finalmente ci siamo, ma no, non ancora. Se stai utilizzando una versione GitLab a pagamento, nei piani sono presenti artefatti con tipi di report metrics e performance a partire da premium e silver che costano $ 19 al mese per ciascun utente. Inoltre, non puoi semplicemente acquistare una funzionalità specifica di cui hai bisogno: puoi solo modificare il piano. Scusa. Allora cosa possiamo fare? A differenza di GitHub con le sue Checks API e Status API, GitLab non ti permetterebbe di creare tu stesso un vero widget nella richiesta di unione. E non c'è speranza di ottenerli presto.

Uno screenshot del tweet pubblicato da Ilya Klimov (impiegato GitLab) ha scritto sulla probabilità di analoghi dell'aspetto per Github Checks e Status API: "Estremamente improbabile. I controlli sono già disponibili tramite l'API dello stato del commit e, per quanto riguarda gli stati, ci sforziamo di essere un ecosistema chiuso".
Uno screenshot del tweet pubblicato da Ilya Klimov (dipendente GitLab) che ha scritto sulla probabilità di analoghi dell'aspetto per Github Checks e Status API. (Grande anteprima)

Un modo per verificare se si dispone effettivamente del supporto per queste funzionalità: è possibile cercare la variabile di ambiente GITLAB_FEATURES nella pipeline. Se nell'elenco mancano merge_request_performance_metrics e metrics_reports , queste funzionalità non sono supportate.

 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

Se non c'è supporto, dobbiamo inventare qualcosa. Ad esempio, possiamo aggiungere un commento alla richiesta di unione, commentare con la tabella, contenente tutti i dati di cui abbiamo bisogno. Possiamo lasciare inalterato il nostro codice: verranno creati artefatti, ma i widget mostreranno sempre un messaggio «metrics are unchanged» .

Comportamento molto strano e non ovvio; Ho dovuto riflettere attentamente per capire cosa stava succedendo.

Allora, qual è il piano?

  1. Abbiamo bisogno di leggere l'artefatto dal ramo master ;
  2. Crea un commento nel formato markdown ;
  3. Ottieni l'identificatore della richiesta di unione dal ramo di funzionalità corrente al master;
  4. Aggiungi il commento.

Come leggere l'artefatto dal ramo principale

Se vogliamo mostrare come le metriche delle prestazioni vengono modificate tra i rami master e di funzionalità, dobbiamo leggere l'artefatto dal master . E per farlo, dovremo usare 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; };

Creazione di un testo di commento

Abbiamo bisogno di costruire il testo del commento nel formato markdown . Creiamo alcune funzioni di servizio che ci aiuteranno:

 /** * 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 che creerà un commento

Avrai bisogno di un token per lavorare con l'API GitLab. Per generarne uno, devi aprire GitLab, accedere, aprire l'opzione "Impostazioni" del menu, quindi aprire "Token di accesso" che si trova sul lato sinistro del menu di navigazione. Dovresti quindi essere in grado di vedere il modulo, che ti consente di generare il token.

Screenshot, che mostra il modulo di generazione dei token e le opzioni di menu che ho menzionato sopra.
Screenshot, che mostra il modulo di generazione dei token e le opzioni di menu che ho menzionato sopra. (Grande anteprima)

Inoltre, avrai bisogno di un ID del progetto. Puoi trovarlo nel repository 'Impostazioni' (nel sottomenu 'Generale'):

Lo screenshot mostra la pagina delle impostazioni, dove puoi trovare l'ID progetto
Lo screenshot mostra la pagina delle impostazioni, dove puoi trovare l'ID progetto. (Grande anteprima)

Per aggiungere un commento alla richiesta di unione, è necessario conoscerne l'ID. La funzione che ti consente di acquisire l'ID della richiesta di unione è simile alla seguente:

 // 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:

A screenshot of the merge request which shows comment with a table that contains a table with lighthouse metrics change
A screenshot of the merge request which shows comment with a table that contains a table with lighthouse metrics change. (Grande anteprima)

Riprendere

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.

Autenticazione

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. Fatto.

Sommario

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!