Cum să faci performanța vizibilă cu GitLab CI și Hoodoo Of GitLab Artefacts

Publicat: 2022-03-10
Rezumat rapid ↬ Nu este suficient să optimizați o aplicație. Trebuie să preveniți degradarea performanței, iar primul pas pentru a face acest lucru este să faceți vizibile modificările de performanță. În acest articol, Anton Nemtsev arată câteva moduri de a le afișa în cererile de îmbinare GitLab.

Degradarea performanței este o problemă cu care ne confruntăm zilnic. Am putea depune eforturi pentru ca aplicația să devină fulgerător, dar în curând ajungem de unde am început. Se întâmplă din cauza adăugării de noi funcții și a faptului că uneori nu ne gândim la pachetele pe care le adăugăm și le actualizăm în mod constant, sau ne gândim la complexitatea codului nostru. În general, este un lucru mic, dar tot este vorba despre lucruri mărunte.

Nu ne putem permite să avem o aplicație lentă. Performanța este un avantaj competitiv care poate aduce și păstra clienți. Nu ne putem permite să petrecem în mod regulat timp optimizând aplicațiile din nou. Este costisitor și complex. Și asta înseamnă că, în ciuda tuturor beneficiilor performanței din perspectiva afacerii, aceasta nu este profitabilă. Ca prim pas în găsirea unei soluții pentru orice problemă, trebuie să facem problema vizibilă. Acest articol vă va ajuta exact cu asta.

Notă : dacă aveți o înțelegere de bază a Node.js, o idee vagă despre cum funcționează CI/CD-ul dvs. și vă pasă de performanța aplicației sau de avantajele de afaceri pe care le poate aduce, atunci suntem gata să mergem.

Cum se creează un buget de performanță pentru un proiect

Primele întrebări pe care ar trebui să ni le punem sunt:

„Care este proiectul performant?”

„Ce valori ar trebui să folosesc?”

„Ce valori ale acestor valori sunt acceptabile?”

Selecția valorilor este în afara domeniului de aplicare al acestui articol și depinde în mare măsură de contextul proiectului, dar vă recomand să începeți prin a citi Valorile de performanță centrate pe utilizator de Philip Walton.

Din perspectiva mea, este o idee bună să folosiți dimensiunea bibliotecii în kiloocteți ca măsurătoare pentru pachetul npm. De ce? Ei bine, pentru că, dacă alți oameni includ codul tău în proiectele lor, ar dori probabil să minimizeze impactul codului tău asupra dimensiunii finale a aplicației lor.

Pentru site, aș considera Time To First Byte (TTFB) ca o valoare. Această valoare arată cât timp îi ia serverului să răspundă cu ceva. Această măsurătoare este importantă, dar destul de vagă, deoarece poate include orice - începând de la timpul de randare a serverului și terminând cu probleme de latență. Așa că este plăcut să îl folosiți împreună cu Server Timing sau OpenTracing pentru a afla în ce constă exact.

De asemenea, ar trebui să luați în considerare valori precum Time to Interactive (TTI) și First Meaningful Paint (cel din urmă va fi înlocuit în curând cu Largest Contentful Paint (LCP)). Cred că ambele sunt cele mai importante - din perspectiva performanței percepute.

Dar rețineți: valorile sunt întotdeauna legate de context , așa că vă rugăm să nu considerați acest lucru ca de la sine înțeles. Gândește-te la ceea ce este important în cazul tău specific.

Cel mai simplu mod de a defini valorile dorite pentru valori este să vă folosiți concurenții – sau chiar pe dvs. De asemenea, din când în când, instrumente precum Calculatorul de buget de performanță pot fi la îndemână - doar joacă-te puțin cu el.

Degradarea performanței este o problemă cu care ne confruntăm zilnic. Am putea depune eforturi pentru ca aplicația să devină fulgerător, dar în curând ajungem de unde am început.

Folosește concurenții în beneficiul tău

Dacă ți s-a întâmplat să fugi vreodată de un urs extatic supraexcitat, atunci știi deja că nu trebuie să fii campion olimpic la alergare pentru a scăpa din această problemă. Trebuie doar să fii puțin mai rapid decât celălalt tip.

Așa că faceți o listă de concurenți. Dacă acestea sunt proiecte de același tip, atunci ele constau de obicei în tipuri de pagini similare între ele. De exemplu, pentru un magazin de internet, poate fi o pagină cu o listă de produse, o pagină cu detalii despre produs, un coș de cumpărături, o casă și așa mai departe.

  1. Măsurați valorile valorilor selectate de dvs. pe fiecare tip de pagină pentru proiectele concurenței dvs.;
  2. Măsurați aceleași valori pentru proiectul dvs.;
  3. Găsiți cea mai apropiată valoare mai bună decât valoarea dvs. pentru fiecare măsură din proiectele concurentului. Adăugând 20% la ele și stabiliți-vă următoarele obiective.

De ce 20%? Acesta este un număr magic care se presupune că înseamnă că diferența va fi vizibilă cu ochiul liber. Puteți citi mai multe despre acest număr în articolul lui Denys Mishunov „De ce contează performanța percepută, partea 1: percepția timpului”.

O Luptă Cu O Umbră

Ai un proiect unic? Nu ai concurenți? Sau ești deja mai bun decât oricare dintre ei în toate sensurile posibile? Nu este o problemă. Poți oricând să concurezi cu singurul adversar demn, adică cu tine însuți. Măsurați fiecare măsură de performanță a proiectului dvs. pe fiecare tip de pagină și apoi îmbunătățiți-le cu același 20%.

Mai multe după săritură! Continuați să citiți mai jos ↓

Teste sintetice

Există două moduri de măsurare a performanței:

  • Sintetic (într-un mediu controlat)
  • RUM (măsurători reale ale utilizatorului)
    Datele sunt colectate de la utilizatori reali în producție.

În acest articol, vom folosi teste sintetice și vom presupune că proiectul nostru folosește GitLab cu CI încorporat pentru implementarea proiectului.

Biblioteca și dimensiunea sa ca metrică

Să presupunem că ați decis să dezvoltați o bibliotecă și să o publicați în NPM. Vrei să-l păstrezi ușor – mult mai ușor decât concurenții – astfel încât să aibă un impact mai mic asupra dimensiunii finale a proiectului rezultat. Acest lucru economisește traficul clienților - uneori trafic pentru care clientul plătește. De asemenea, permite încărcarea mai rapidă a proiectului, ceea ce este destul de important în ceea ce privește cota de telefonie mobilă în creștere și noile piețe cu viteze reduse de conectare și acoperire fragmentată a internetului.

Pachet pentru măsurarea dimensiunii bibliotecii

Pentru a menține dimensiunea bibliotecii cât mai mică posibil, trebuie să urmărim cu atenție cum se schimbă în timpul dezvoltării. Dar cum poți să o faci? Ei bine, am putea folosi pachetul Size Limit creat de Andrey Sitnik de la Evil Martians.

Să-l instalăm.

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

Apoi, adăugați-l la package.json .

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

Blocul "size-limit":[{},{},…] conține o listă cu dimensiunea fișierelor pe care dorim să le verificăm. În cazul nostru, este doar un singur fișier: index.js .

size scriptului NPM rulează doar pachetul size-limit , care citește size-limit a blocului de configurare menționată mai înainte și verifică dimensiunea fișierelor enumerate acolo. Hai să-l rulăm și să vedem ce se întâmplă:

 npm run size 
Rezultatul execuției comenzii arată dimensiunea index.js
Rezultatul execuției comenzii arată dimensiunea index.js. (Previzualizare mare)

Putem vedea dimensiunea fișierului, dar această dimensiune nu este de fapt sub control. Să remediem asta adăugând limit la package.json :

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

Acum, dacă rulăm scriptul, acesta va fi validat în raport cu limita pe care am stabilit-o.

O captură de ecran a terminalului; dimensiunea fișierului este mai mică decât limita și este afișată ca verde
O captură de ecran a terminalului; dimensiunea fișierului este mai mică decât limita și este afișată ca verde. (Previzualizare mare)

În cazul în care noua dezvoltare modifică dimensiunea fișierului până la depășirea limitei definite, scriptul se va completa cu cod diferit de zero. Acest lucru, în afară de alte lucruri, înseamnă că va opri conducta în GitLab CI.

O captură de ecran a terminalului în care dimensiunea fișierului depășește limita și este afișată cu roșu. Scriptul a fost terminat cu un cod diferit de zero.
O captură de ecran a terminalului în care dimensiunea fișierului depășește limita și este afișată cu roșu. Scriptul a fost terminat cu un cod diferit de zero. (Previzualizare mare)

Acum putem folosi git hook pentru a verifica dimensiunea fișierului față de limita înainte de fiecare comitere. S-ar putea chiar să folosim pachetul husky pentru a-l face într-un mod frumos și simplu.

Să-l instalăm.

 npm i -D husky

Apoi, modificați package.json .

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

Și acum, înainte ca fiecare commit să fie executată automat, comanda npm run size și dacă se va termina cu un cod diferit de zero, commit-ul nu s-ar întâmpla niciodată.

O captură de ecran a terminalului în care commit-ul este anulat deoarece dimensiunea fișierului depășește limita
O captură de ecran a terminalului în care commit-ul este anulat deoarece dimensiunea fișierului depășește limita. (Previzualizare mare)

Dar există multe modalități de a sări peste cârlige (intenționat sau chiar accidental), așa că nu ar trebui să ne bazăm prea mult pe ele.

De asemenea, este important să rețineți că nu ar trebui să facem această blocare a verificării. De ce? Pentru că este în regulă ca dimensiunea bibliotecii să crească în timp ce adăugați noi funcții. Trebuie să facem schimbările vizibile, asta-i tot. Acest lucru va ajuta la evitarea creșterii accidentale a dimensiunii din cauza introducerii unei biblioteci de ajutor de care nu avem nevoie. Și, poate, oferiți dezvoltatorilor și proprietarilor de produse un motiv pentru a lua în considerare dacă funcția adăugată merită creșterea dimensiunii. Sau, poate, dacă există pachete alternative mai mici. Bundlephobia ne permite să găsim o alternativă pentru aproape orice pachet NPM.

Deci ce ar trebui sa facem? Să arătăm modificarea dimensiunii fișierului direct în cererea de îmbinare! Dar nu împingi să stăpânești direct; te comporți ca un dezvoltator adult, nu?

Executarea verificării noastre pe GitLab CI

Să adăugăm un artefact GitLab de tip metrics. Un artefact este un fișier care va „vii” după terminarea operațiunii de conductă. Acest tip specific de artefact ne permite să arătăm un widget suplimentar în cererea de îmbinare, arătând orice modificare a valorii valorii între artefactul din master și ramura caracteristică. Formatul artefactului de metrics este un format text Prometheus. Pentru valorile GitLab din interiorul artefactului, este doar text. GitLab nu înțelege ce anume s-a schimbat exact în valoare - știe doar că valoarea este diferită. Deci, ce ar trebui să facem mai exact?

  1. Definiți artefactele în conductă.
  2. Schimbați scriptul astfel încât să creeze un artefact pe conductă.

Pentru a crea un artefact, trebuie să schimbăm .gitlab-ci.yml astfel:

 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 — artefactul va exista timp de 7 zile.
  2.  paths: metric.txt

    Va fi salvat în catalogul rădăcină. Dacă omiteți această opțiune, atunci nu ar fi posibil să o descărcați.
  3.  reports: metrics: metric.txt

    Artefactul va avea tipul reports:metrics

Acum să facem ca Limita de dimensiune să genereze un raport. Pentru a face acest lucru, trebuie să schimbăm package.json :

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

size-limit cu cheia --json va scoate date în format json:

Comanda size-limit --json scoate JSON în consolă. JSON conține o serie de obiecte care conțin un nume de fișier și o dimensiune, precum și ne anunță dacă depășește limita de dimensiune
Comanda size-limit --json scoate JSON în consolă. JSON conține o serie de obiecte care conțin un nume de fișier și o dimensiune, precum și ne anunță dacă depășește limita de dimensiune. (Previzualizare mare)

Și redirecționarea > size-limit.json va salva JSON în fișierul size-limit.json .

Acum trebuie să creăm un artefact din asta. Formatul se rezumă la [metrics name][space][metrics value] . Să creăm scriptul generate-metric.js :

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

Și adăugați-l la package.json :

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

Deoarece am folosit prefixul post , comanda npm run size va rula mai întâi scriptul size , iar apoi, automat, va executa scriptul postsize , ceea ce va duce la crearea fișierului metric.txt , artefactul nostru.

Ca rezultat, când îmbinăm această ramură pentru a stăpâni, schimbăm ceva și creăm o nouă solicitare de îmbinare, vom vedea următoarele:

Captură de ecran cu o solicitare de îmbinare, care ne arată un widget cu valoare de metrică nouă și veche în paranteze rotunde
Captură de ecran cu o solicitare de îmbinare, care ne arată un widget cu valoare de metrică nouă și veche în paranteze rotunde. (Previzualizare mare)

În widgetul care apare pe pagină vedem, mai întâi, numele metricii ( size ) urmat de valoarea metricii din ramura caracteristică, precum și valoarea din master în parantezele rotunde.

Acum putem vedea de fapt cum să schimbăm dimensiunea pachetului și să luăm o decizie rezonabilă dacă ar trebui să îl îmbinăm sau nu.

  • Este posibil să vedeți tot acest cod în acest depozit.

Relua

BINE! Așadar, ne-am dat seama cum să gestionăm acest caz trivial. Dacă aveți mai multe fișiere, separați doar valorile cu întreruperi de linie. Ca alternativă pentru Limită de dimensiune, puteți lua în considerare dimensiunea pachetului. Dacă utilizați WebPack, puteți obține toate dimensiunile de care aveți nevoie, construind cu steagurile --profile și --json :

 webpack --profile --json > stats.json

Dacă utilizați next.js, puteți utiliza pluginul @next/bundle-analyzer. Depinde de tine!

Folosind Lighthouse

Lighthouse este standardul de facto în analiza proiectelor. Să scriem un script care ne permite să măsurăm performanța, a11y, cele mai bune practici și să ne furnizăm un scor SEO.

Script pentru a măsura toate lucrurile

Pentru început, trebuie să instalăm pachetul far care va face măsurători. De asemenea, trebuie să instalăm puppeteer pe care îl vom folosi ca browser fără cap.

 npm i -D lighthouse puppeteer

Apoi, să creăm un script lighthouse.js și să pornim browserul nostru:

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

Acum să scriem o funcție care ne va ajuta să analizăm o anumită adresă 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); // … }

Grozav! Acum avem o funcție care va accepta obiectul browser ca argument și va returna o funcție care va accepta URL -ul ca argument și va genera un raport după transmiterea acelui URL la lighthouse .

Transmitem următoarele argumente la lighthouse :

  1. Adresa pe care dorim să o analizăm;
  2. opțiunile lighthouse , în special port browserului și output (formatul de ieșire al raportului);
  3. report de configurare și lighthouse:full (tot ce putem măsura). Pentru o configurare mai precisă, verificați documentația.

Minunat! Acum avem raportul nostru. Dar ce putem face cu el? Ei bine, putem verifica valorile în raport cu limitele și scriptul de ieșire cu cod diferit de zero, care va opri conducta:

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

Dar vrem doar să facem performanța vizibilă și fără blocare? Atunci să adoptăm un alt tip de artefact: artefact de performanță GitLab.

Artefact de performanță GitLab

Pentru a înțelege acest format de artefacte, trebuie să citim codul pluginului sitespeed.io. (De ce GitLab nu poate descrie formatul artefactelor lor în propria documentație? Mister. )

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

Un artefact este un fișier JSON care conține o matrice de obiecte. Fiecare dintre ele reprezintă un raport despre o URL .

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

Fiecare pagină este reprezentată de un obiect cu următoarele atribute:

  1. subject
    Identificator de pagină (este destul de util să folosești un astfel de nume de cale);
  2. metrics
    O matrice a obiectelor (fiecare dintre ele reprezintă o măsurătoare care a fost făcută pe pagină).
 { "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }

O measurement este un obiect care conține următoarele atribute:

  1. name
    Numele măsurării, de exemplu, poate fi Time to first byte sau Time to interactive .
  2. value
    Rezultatul măsurătorii numerice.
  3. desiredSize
    Dacă valoarea țintă ar trebui să fie cât mai mică posibil, de exemplu pentru valoarea Time to interactive , atunci valoarea ar trebui să fie smaller . Dacă ar trebui să fie cât mai mare posibil, de exemplu, pentru Performance score al farului , atunci utilizați larger .
 { "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }

Să modificăm funcția buildReport astfel încât să returneze un raport pentru o pagină cu valori standard lighthouse.

Captură de ecran cu raportul farului. Există scorul de performanță, scorul a11y, scorul celor mai bune practici, scorul SEO
Captură de ecran cu raportul farului. Există scorul de performanță, scorul a11y, scorul celor mai bune practici, scorul SEO. (Previzualizare mare)
 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, }; }

Acum, când avem o funcție care generează un raport. Să-l aplicăm fiecărui tip de pagini ale proiectului. În primul rând, trebuie să precizez că process.env.DOMAIN ar trebui să conțină un domeniu intermediar (la care trebuie să-ți implementezi proiectul dintr-o ramură de caracteristici în prealabil).

 + 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(); })();
  • Puteți găsi sursa completă în acest exemplu general și de lucru în acest depozit.

Notă : în acest moment, poate doriți să mă întrerupeți și să țipi în zadar: „De ce îmi iei timpul – nici măcar nu poți folosi Promise.all corect!” În apărarea mea, îndrăznesc să spun, că nu este recomandat să rulați mai mult de o instanță de far în același timp, deoarece acest lucru afectează negativ acuratețea rezultatelor măsurătorilor. De asemenea, dacă nu dai dovadă de ingeniozitate cuvenită, va duce la o excepție.

Utilizarea mai multor procese

Sunteți încă în măsurători paralele? Bine, poate doriți să utilizați clusterul de noduri (sau chiar Worker Threads, dacă vă place să jucați bold), dar este logic să discutați despre asta numai în cazul în care conducta dvs. rulează pe mediul cu mai multe coruri disponibile. Și chiar și atunci, ar trebui să rețineți că, din cauza naturii Node.js, veți avea o instanță Node.js cu greutate completă generată în fiecare furcă de proces (în loc să o reutilizați pe aceeași, ceea ce va duce la un consum de memorie în creștere). Toate acestea înseamnă că va fi mai costisitor din cauza cerinței hardware în creștere și un pic mai rapid. Poate părea că jocul nu merită lumânarea.

Dacă doriți să vă asumați acest risc, va trebui să:

  1. Împărțiți matricea URL în bucăți după numărul de nuclee;
  2. Creați o furcă a unui proces în funcție de numărul de nuclee;
  3. Transferați părți ale matricei în furci și apoi preluați rapoartele generate.

Pentru a împărți o matrice, puteți utiliza abordări multipile. Următorul cod, scris în doar câteva minute, nu ar fi mai rău decât celelalte:

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

Faceți furculițe în funcție de numărul de miezuri:

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

Să transferăm o serie de bucăți către procesele copil și să recuperăm rapoartele:

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

Și, în sfârșit, reasamblați rapoartele într-o singură matrice și generați un artefact.

  • Consultați codul complet și depozitul cu un exemplu care arată cum să utilizați Lighthouse cu mai multe procese.

Precizia măsurătorilor

Ei bine, am paralelizat măsurătorile, ceea ce a crescut deja nefericită eroare mare de măsurare a lighthouse . Dar cum o reducem? Ei bine, fă câteva măsurători și calculează media.

Pentru a face acest lucru, vom scrie o funcție care va calcula media dintre rezultatele măsurătorilor curente și cele anterioare.

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

Apoi, modificați codul nostru pentru a le folosi:

 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(); }); }
  • Consultați esenta cu codul complet și depozitul cu un exemplu.

Și acum putem adăuga un lighthouse în conductă.

Adăugarea lui la conductă

Mai întâi, creați un fișier de configurare numit .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

Multiplele pachete instalate sunt necesare pentru puppeteer . Ca alternativă, puteți lua în considerare utilizarea docker . În afară de asta, are sens faptul că am setat tipul de artefact ca performanță. Și, de îndată ce atât master cât și ramura caracteristică îl vor avea, veți vedea un widget ca acesta în cererea de îmbinare:

O captură de ecran a paginii de solicitare de îmbinare. Există un widget care arată ce valori de far s-au schimbat și cum exact
O captură de ecran a paginii de solicitare de îmbinare. Există un widget care arată ce valori de far s-au schimbat și cum exact. (Previzualizare mare)

Grozav?

Relua

Am terminat în sfârșit cu un caz mai complex. Evident, există mai multe instrumente similare în afară de far. De exemplu, sitespeed.io. Documentația GitLab conține chiar și un articol care explică cum să utilizați sitespeed în pipeline-ul GitLab. Există, de asemenea, un plugin pentru GitLab care ne permite să generăm un artefact. Dar cine ar prefera produsele open source conduse de comunitate față de cele deținute de un monstru corporativ?

Nu există odihnă pentru cei răi

Poate părea că am ajuns în sfârșit acolo, dar nu, nu încă. Dacă utilizați o versiune GitLab plătită, atunci artefactele cu metrics și performance tipurilor de raport sunt prezente în planurile, începând de la premium și silver , care costă 19 USD pe lună pentru fiecare utilizator. De asemenea, nu puteți cumpăra doar o funcție specifică de care aveți nevoie - puteți doar să schimbați planul. Îmi pare rău. Deci ce putem face? Spre deosebire de GitHub cu API-ul Checks și API-ul Status, GitLab nu vă permite să creați singur un widget real în cererea de îmbinare. Și nu există nicio speranță să le obținem în curând.

O captură de ecran a tweet-ului postat de Ilya Klimov (angajat GitLab) a scris despre probabilitatea apariției analogilor pentru Github Checks și Status API: „Extrem de puțin probabil. Verificările sunt deja disponibile prin API-ul pentru starea de confirmare și, în ceea ce privește stările, ne străduim să fim un ecosistem închis.”
O captură de ecran a tweet-ului postat de Ilya Klimov (angajat GitLab) care a scris despre probabilitatea apariției analogilor pentru Github Checks și Status API. (Previzualizare mare)

O modalitate de a verifica dacă aveți de fapt suport pentru aceste caracteristici: puteți căuta variabila de mediu GITLAB_FEATURES în pipeline. Dacă în listă lipsesc merge_request_performance_metrics și metrics_reports , atunci aceste funcții nu sunt acceptate.

 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

Dacă nu există sprijin, trebuie să venim cu ceva. De exemplu, putem adăuga un comentariu la cererea de îmbinare, un comentariu cu tabelul, care conține toate datele de care avem nevoie. Ne putem lăsa codul neatins — vor fi create artefacte, dar widget-urile vor afișa întotdeauna un mesaj «metrics are unchanged» .

Comportament foarte ciudat și neevident; A trebuit să mă gândesc bine ca să înțeleg ce se întâmplă.

Deci, care este planul?

  1. Trebuie să citim artefactul din ramura master ;
  2. Creați un comentariu în formatul de markdown ;
  3. Obțineți identificatorul cererii de îmbinare de la ramura caracteristică curentă la master;
  4. Adauga comentariul.

Cum să citiți artefactul din filiala principală

Dacă vrem să arătăm cum sunt modificate valorile de performanță între ramurile master și caracteristică, trebuie să citim artefactul din master . Și pentru a face acest lucru, va trebui să folosim 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; };

Crearea unui text de comentariu

Trebuie să construim text de comentariu în formatul de markdown . Să creăm câteva funcții de serviciu care ne vor ajuta:

 /** * 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 care va construi un comentariu

Va trebui să aveți un token pentru a lucra cu API-ul GitLab. Pentru a genera unul, trebuie să deschideți GitLab, să vă conectați, să deschideți opțiunea „Setări” din meniu și apoi să deschideți „Jetoane de acces” aflat în partea stângă a meniului de navigare. Apoi, ar trebui să puteți vedea formularul, care vă permite să generați simbolul.

Captură de ecran, care arată formularul de generare a simbolurilor și opțiunile de meniu pe care le-am menționat mai sus.
Captură de ecran, care arată formularul de generare a simbolurilor și opțiunile de meniu pe care le-am menționat mai sus. (Previzualizare mare)

De asemenea, veți avea nevoie de un ID al proiectului. Îl puteți găsi în depozitul „Setări” (în submeniul „General”):

Captura de ecran arată pagina de setări, unde puteți găsi ID-ul proiectului
Captura de ecran arată pagina de setări, unde puteți găsi ID-ul proiectului. (Previzualizare mare)

Pentru a adăuga un comentariu la cererea de îmbinare, trebuie să știm ID-ul acesteia. Funcția care vă permite să obțineți ID-ul cererii de îmbinare arată astfel:

 // 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. (Previzualizare mare)

Resume

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.

Autentificare

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

rezumat

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!