Come rendere visibili le prestazioni con GitLab CI e Hoodoo of GitLab Artifacts
Pubblicato: 2022-03-10Il 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.
- Misura i valori delle metriche selezionate su ogni tipo di pagina per i progetti dei tuoi concorrenti;
- Misura le stesse metriche sul tuo progetto;
- 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%.
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
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.
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.
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.
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?
- Definire gli artefatti nella pipeline.
- 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
-
expire_in: 7 days
— l'artefatto esisterà per 7 giorni. paths: metric.txt
Verrà salvato nel catalogo principale. Se salti questa opzione, non sarebbe possibile scaricarla.reports: metrics: metric.txt
L'artefatto avrà il tiporeports: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:
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:
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
:
- L'indirizzo che vogliamo analizzare;
- opzioni del
lighthouse
,port
del browser in particolare eoutput
(formato di output del rapporto); -
report
configurazione elighthouse: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:
-
subject
Identificatore di pagina (è abbastanza comodo usare un tale percorso); -
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:
-
name
Nome della misurazione, ad es. può essereTime to first byte
oTime to interactive
. -
value
Risultato della misurazione numerica. -
desiredSize
Se il valore target deve essere il più piccolo possibile, ad esempio per la metricaTime to interactive
, il valore dovrebbe esseresmaller
. Se dovrebbe essere il più grande possibile, ad esempio per ilPerformance score
del faro, utilizzarelarger
.
{ "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.
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:
- Dividi l'array URL in blocchi per numero di core;
- Crea un fork di un processo in base al numero dei core;
- 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:
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.
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?
- Abbiamo bisogno di leggere l'artefatto dal ramo
master
; - Crea un commento nel formato
markdown
; - Ottieni l'identificatore della richiesta di unione dal ramo di funzionalità corrente al master;
- 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.
Inoltre, avrai bisogno di un ID del progetto. Puoi trovarlo nel repository 'Impostazioni' (nel sottomenu 'Generale'):
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:
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!