So machen Sie die Leistung mit GitLab CI und Hoodoo von GitLab-Artefakten sichtbar
Veröffentlicht: 2022-03-10Leistungsabfall ist ein Problem, mit dem wir täglich konfrontiert sind. Wir könnten uns Mühe geben, die Anwendung blitzschnell zu machen, aber wir landen bald dort, wo wir angefangen haben. Dies geschieht, weil neue Funktionen hinzugefügt werden und wir uns manchmal keine Gedanken über Pakete machen, die wir ständig hinzufügen und aktualisieren, oder über die Komplexität unseres Codes nachdenken. Es ist im Allgemeinen eine kleine Sache, aber es geht immer noch um die kleinen Dinge.
Wir können es uns nicht leisten, eine langsame App zu haben. Leistung ist ein Wettbewerbsvorteil, der Kunden bringen und halten kann. Wir können es uns nicht leisten, regelmäßig Zeit damit zu verbringen, Apps immer wieder neu zu optimieren. Es ist teuer und komplex. Und das bedeutet, dass Performance trotz aller Vorteile aus betriebswirtschaftlicher Sicht kaum rentabel ist. Als erster Schritt, um eine Lösung für jedes Problem zu finden, müssen wir das Problem sichtbar machen. Dieser Artikel wird Ihnen genau dabei helfen.
Hinweis : Wenn Sie ein grundlegendes Verständnis von Node.js haben, eine vage Vorstellung davon haben, wie Ihr CI/CD funktioniert, und sich für die Leistung der App oder geschäftliche Vorteile interessieren, die es bringen kann, dann sind wir startklar.
So erstellen Sie ein Leistungsbudget für ein Projekt
Die ersten Fragen, die wir uns stellen sollten, sind:
„Was ist das performante Projekt?“
„Welche Metriken soll ich verwenden?“
„Welche Werte dieser Metriken sind akzeptabel?“
Die Auswahl der Metriken würde den Rahmen dieses Artikels sprengen und hängt stark vom Projektkontext ab, aber ich empfehle Ihnen, mit dem Lesen von User-centric Performance Metrics von Philip Walton zu beginnen.
Aus meiner Sicht ist es eine gute Idee, die Größe der Bibliothek in Kilobyte als Metrik für das npm-Paket zu verwenden. Warum? Nun, das liegt daran, dass andere Leute, die Ihren Code in ihre Projekte aufnehmen, vielleicht die Auswirkungen Ihres Codes auf die endgültige Größe ihrer Anwendung minimieren möchten.
Für die Site würde ich Time To First Byte (TTFB) als Metrik betrachten. Diese Metrik zeigt, wie lange es dauert, bis der Server mit etwas antwortet. Diese Metrik ist wichtig, aber ziemlich vage, da sie alles beinhalten kann – angefangen von der Server-Renderingzeit bis hin zu Latenzproblemen. Es ist also schön, es in Verbindung mit Server Timing oder OpenTracing zu verwenden, um herauszufinden, woraus es genau besteht.
Sie sollten auch Metriken wie Time to Interactive (TTI) und First Meaningful Paint (letzteres wird bald durch Largest Contentful Paint (LCP) ersetzt) berücksichtigen. Ich denke, dass beides am wichtigsten ist – aus der Perspektive der wahrgenommenen Leistung.
Aber bedenken Sie: Metriken sind immer kontextbezogen , also bitte nicht einfach so hinnehmen. Überlegen Sie, was in Ihrem konkreten Fall wichtig ist.
Der einfachste Weg, gewünschte Werte für Metriken zu definieren, besteht darin, Ihre Konkurrenten – oder sogar sich selbst – heranzuziehen. Von Zeit zu Zeit können auch Tools wie der Performance Budget Calculator nützlich sein – spielen Sie einfach ein wenig damit herum.
Leistungsabfall ist ein Problem, mit dem wir täglich konfrontiert sind. Wir könnten uns Mühe geben, die Anwendung blitzschnell zu machen, aber bald landen wir dort, wo wir angefangen haben.
„
Verwenden Sie Konkurrenten zu Ihrem Vorteil
Wenn Sie schon einmal vor einem ekstatisch überdrehten Bären davongelaufen sind, dann wissen Sie bereits, dass Sie kein Olympiasieger im Laufen sein müssen, um aus dieser Misere herauszukommen. Du musst nur ein bisschen schneller sein als der andere.
Erstellen Sie also eine Mitbewerberliste. Handelt es sich um gleichartige Projekte, dann bestehen sie in der Regel aus einander ähnlichen Seitentypen. Bei einem Internetshop kann dies beispielsweise eine Seite mit einer Produktliste, einer Produktdetailseite, einem Einkaufswagen, einer Kasse usw. sein.
- Messen Sie die Werte Ihrer ausgewählten Metriken auf jedem Seitentyp für die Projekte Ihrer Mitbewerber;
- Messen Sie dieselben Metriken für Ihr Projekt;
- Finden Sie für jede Metrik in den Projekten des Mitbewerbers den Wert, der Ihrem Wert am nächsten kommt. Füge ihnen 20 % hinzu und setze sie als deine nächsten Ziele.
Warum 20 %? Dies ist eine magische Zahl, die angeblich bedeutet, dass der Unterschied mit bloßem Auge erkennbar ist. Mehr über diese Zahl können Sie in Denys Mishunovs Artikel „Why Perceived Performance Matters, Part 1: The Perception Of Time“ lesen.
Ein Kampf mit einem Schatten
Haben Sie ein einzigartiges Projekt? Sie haben keine Konkurrenten? Oder bist du bereits besser als alle anderen in allen möglichen Sinnen? Es ist kein Problem. Du kannst immer mit dem einzig würdigen Gegner, nämlich dir selbst, konkurrieren. Messen Sie jede Leistungsmetrik Ihres Projekts auf jedem Seitentyp und verbessern Sie sie dann um die gleichen 20 %.
Synthetische Tests
Es gibt zwei Möglichkeiten, die Leistung zu messen:
- Synthetisch (in einer kontrollierten Umgebung)
- RUM (Reale Benutzermessungen)
Daten werden von echten Benutzern in der Produktion gesammelt.
In diesem Artikel verwenden wir synthetische Tests und gehen davon aus, dass unser Projekt GitLab mit seinem integrierten CI für die Projektbereitstellung verwendet.
Bibliothek und ihre Größe als Metrik
Nehmen wir an, Sie haben sich entschieden, eine Bibliothek zu entwickeln und in NPM zu veröffentlichen. Sie möchten es leicht halten – viel leichter als die Konkurrenz – damit es weniger Einfluss auf die Endgröße des resultierenden Projekts hat. Dies spart Clients Datenverkehr – manchmal Datenverkehr, für den der Kunde bezahlt. Es ermöglicht auch, dass das Projekt schneller geladen wird, was im Hinblick auf den wachsenden Mobilfunkanteil und neue Märkte mit langsamen Verbindungsgeschwindigkeiten und fragmentierter Internetabdeckung ziemlich wichtig ist.
Paket zum Messen der Bibliotheksgröße
Um die Größe der Bibliothek so klein wie möglich zu halten, müssen wir sorgfältig beobachten, wie sie sich im Laufe der Entwicklungszeit ändert. Aber wie können Sie es tun? Nun, wir könnten die Paketgrößenbegrenzung verwenden, die von Andrey Sitnik von Evil Martians erstellt wurde.
Lassen Sie es uns installieren.
npm i -D size-limit @size-limit/preset-small-lib
Fügen Sie es dann zu package.json
.
"scripts": { + "size": "size-limit", "test": "jest && eslint ." }, + "size-limit": [ + { + "path": "index.js" + } + ],
Der Block "size-limit":[{},{},…]
enthält eine Liste der Größe der Dateien, die wir überprüfen möchten. In unserem Fall ist es nur eine einzige Datei: index.js
.
NPM script size
führt einfach das Paket size size-limit
size-limit
aus, das die zuvor erwähnte Größenbeschränkung des Konfigurationsblocks liest und die Größe der dort aufgelisteten Dateien überprüft. Lassen Sie es uns ausführen und sehen, was passiert:
npm run size
Wir können die Größe der Datei sehen, aber diese Größe ist nicht wirklich unter Kontrolle. Lassen Sie uns das beheben, indem wir ein limit
zu package.json
:
"size-limit": [ { + "limit": "2 KB", "path": "index.js" } ],
Wenn wir nun das Skript ausführen, wird es anhand des von uns festgelegten Limits validiert.
Für den Fall, dass eine neue Entwicklung die Dateigröße so weit ändert, dass das definierte Limit überschritten wird, wird das Skript mit einem Nicht-Null-Code abgeschlossen. Dies bedeutet unter anderem, dass die Pipeline im GitLab CI gestoppt wird.
Jetzt können wir git hook verwenden, um die Dateigröße vor jedem Commit mit dem Limit zu vergleichen. Wir können sogar das Husky-Paket verwenden, um es auf nette und einfache Weise zu machen.
Lassen Sie es uns installieren.
npm i -D husky
Ändern Sie dann unsere package.json
.
"size-limit": [ { "limit": "2 KB", "path": "index.js" } ], + "husky": { + "hooks": { + "pre-commit": "npm run size" + } + },
Und jetzt würde vor jedem Commit automatisch der Befehl npm run size
ausgeführt werden, und wenn es mit einem Nicht-Null-Code enden würde, würde das Commit niemals stattfinden.
Aber es gibt viele Möglichkeiten, Haken zu überspringen (absichtlich oder sogar aus Versehen), also sollten wir uns nicht zu sehr auf sie verlassen.
Außerdem ist es wichtig zu beachten, dass wir diese Prüfung nicht blockieren müssen. Warum? Weil es in Ordnung ist, dass die Größe der Bibliothek wächst, während Sie neue Funktionen hinzufügen. Wir müssen die Änderungen sichtbar machen, das ist alles. Dies wird dazu beitragen, eine versehentliche Vergrößerung durch die Einführung einer Hilfsbibliothek zu vermeiden, die wir nicht benötigen. Und geben Sie vielleicht Entwicklern und Produktbesitzern einen Grund zu überlegen, ob die hinzugefügte Funktion die Vergrößerung wert ist. Oder vielleicht, ob es kleinere Alternativpakete gibt. Bundlephobia ermöglicht es uns, für fast jedes NPM-Paket eine Alternative zu finden.
Also, was sollten wir tun? Lassen Sie uns die Änderung der Dateigröße direkt in der Zusammenführungsanforderung anzeigen! Aber Sie drängen nicht direkt zum Meistern; Sie verhalten sich wie ein erwachsener Entwickler, oder?
Ausführen unseres Tests auf GitLab CI
Lassen Sie uns ein GitLab-Artefakt des Metriktyps hinzufügen. Ein Artefakt ist eine Datei, die nach Abschluss der Pipeline-Operation „lebt“. Dieser spezielle Artefakttyp ermöglicht es uns, ein zusätzliches Widget in der Zusammenführungsanforderung anzuzeigen, das jede Änderung des Werts der Metrik zwischen dem Artefakt im Master- und dem Feature-Branch anzeigt. Das Format des metrics
ist ein Text-Prometheus-Format. Bei GitLab-Werten innerhalb des Artefakts handelt es sich lediglich um Text. GitLab versteht nicht, was sich genau im Wert geändert hat – es weiß nur, dass der Wert anders ist. Also, was genau sollen wir tun?
- Definieren Sie Artefakte in der Pipeline.
- Ändern Sie das Skript so, dass es ein Artefakt in der Pipeline erstellt.
Um ein Artefakt zu erstellen, müssen wir .gitlab-ci.yml
ändern:
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
– Artefakt wird 7 Tage lang existieren. paths: metric.txt
Es wird im Root-Katalog gespeichert. Wenn Sie diese Option überspringen, ist es nicht möglich, sie herunterzuladen.reports: metrics: metric.txt
Das Artefakt hat den Typ "reports:metrics
".
Lassen Sie uns jetzt die Größenbeschränkung veranlassen, einen Bericht zu erstellen. Dazu müssen wir package.json
ändern:
"scripts": { - "size": "size-limit", + "size": "size-limit --json > size-limit.json", "test": "jest && eslint ." },
size-limit
mit key --json
gibt Daten im json-Format aus:
Und Umleitung > size-limit.json
speichert JSON in der Datei size-limit.json
.
Jetzt müssen wir daraus ein Artefakt erstellen. Das Format läuft auf [metrics name][space][metrics value]
. Lassen Sie uns das Skript generate-metric.js
:
const report = require('./size-limit.json'); process.stdout.write(`size ${(report[0].size/1024).toFixed(1)}Kb`); process.exit(0);
Und fügen Sie es zu package.json
:
"scripts": { "size": "size-limit --json > size-limit.json", + "postsize": "node generate-metric.js > metric.txt", "test": "jest && eslint ." },
Da wir das post
-Präfix verwendet haben, führt der Befehl npm run size
size
das Größenskript aus und führt dann automatisch das postsize
Skript aus, was zur Erstellung der Datei metric.txt
, unseres Artefakts, führt.
Als Ergebnis sehen wir Folgendes, wenn wir diesen Zweig mit dem Master zusammenführen, etwas ändern und eine neue Zusammenführungsanforderung erstellen:
Im Widget, das auf der Seite erscheint, sehen wir zunächst den Namen der Metrik ( size
), gefolgt vom Wert der Metrik im Feature-Branch sowie dem Wert im Master in runden Klammern.
Jetzt können wir tatsächlich sehen, wie wir die Größe des Pakets ändern und eine vernünftige Entscheidung treffen, ob wir es zusammenführen sollen oder nicht.
- Sie können diesen gesamten Code in diesem Repository sehen.
Fortsetzen
OK! Wir haben also herausgefunden, wie wir mit dem trivialen Fall umgehen. Wenn Sie mehrere Dateien haben, trennen Sie die Messwerte einfach durch Zeilenumbrüche. Als Alternative zur Größenbegrenzung können Sie Bundlesize in Betracht ziehen. Wenn Sie WebPack verwenden, erhalten Sie möglicherweise alle benötigten Größen, indem Sie mit den --profile
und --json
:
webpack --profile --json > stats.json
Wenn Sie next.js verwenden, können Sie das Plugin @next/bundle-analyzer verwenden. Es liegt an dir!
Leuchtturm verwenden
Lighthouse ist der De-facto-Standard in der Projektanalyse. Lassen Sie uns ein Skript schreiben, das es uns ermöglicht, Leistung, a11y, Best Practices zu messen und uns einen SEO-Score zu liefern.
Skript, um das ganze Zeug zu messen
Zunächst müssen wir das Lighthouse-Paket installieren, das Messungen durchführt. Wir müssen auch Puppeteer installieren, das wir als Headless-Browser verwenden werden.
npm i -D lighthouse puppeteer
Als Nächstes erstellen wir ein lighthouse.js
-Skript und starten unseren Browser:
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); })();
Lassen Sie uns nun eine Funktion schreiben, die uns hilft, eine bestimmte URL zu analysieren:
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); // … }
Toll! Wir haben jetzt eine Funktion, die das Browserobjekt als Argument akzeptiert und eine Funktion zurückgibt, die die URL
als Argument akzeptiert und einen Bericht generiert, nachdem sie diese URL
an das lighthouse
übergeben hat.
Wir übergeben die folgenden Argumente an den lighthouse
:
- Die Adresse, die wir analysieren möchten;
-
lighthouse
-Optionen, insbesondere Browser-port
undoutput
(Ausgabeformat des Berichts); -
report
configuration undlighthouse:full
(alles was wir messen können). Eine genauere Konfiguration finden Sie in der Dokumentation.
Wunderbar! Wir haben jetzt unseren Bericht. Aber was können wir damit machen? Nun, wir können die Metriken mit den Grenzwerten vergleichen und das Skript mit einem Nicht-Null-Code beenden, der die Pipeline stoppt:
if (report.categories.performance.score < 0.8) process.exit(1);
Aber wir wollen nur Leistung sichtbar und nicht blockierend machen? Nehmen wir dann einen anderen Artefakttyp an: GitLab-Leistungsartefakt.
GitLab-Leistungsartefakt
Um dieses Artefaktformat zu verstehen, müssen wir den Code des sitespeed.io-Plugins lesen. (Warum kann GitLab das Format ihrer Artefakte nicht in ihrer eigenen Dokumentation beschreiben? Rätsel. )
[ { "subject":"/", "metrics":[ { "name":"Transfer Size (KB)", "value":"19.5", "desiredSize":"smaller" }, { "name":"Total Score", "value":92, "desiredSize":"larger" }, {…} ] }, {…} ]
Ein Artefakt ist eine JSON
-Datei, die ein Array der Objekte enthält. Jeder von ihnen repräsentiert einen Bericht über eine URL
.
[{page 1}, {page 2}, …]
Jede Seite wird durch ein Objekt mit den folgenden Attributen dargestellt:
-
subject
Seitenkennung (es ist ziemlich praktisch, einen solchen Pfadnamen zu verwenden); -
metrics
Ein Array der Objekte (jedes repräsentiert eine Messung, die auf der Seite vorgenommen wurde).
{ "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }
Eine measurement
ist ein Objekt, das die folgenden Attribute enthält:
-
name
Name der Messung, z. B.Time to first byte
oderTime to interactive
. -
value
Numerisches Messergebnis. -
desiredSize
Wenn der Zielwert so klein wie möglich sein soll, z. B. für die MetrikTime to interactive
, dann sollte der Wertsmaller
sein . Wenn es möglichst groß sein soll, zB für den LeuchtturmPerformance score
, dann verwenden Sielarger
.
{ "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }
Ändern wir unsere buildReport
-Funktion so, dass sie einen Bericht für eine Seite mit standardmäßigen Lighthouse-Metriken zurückgibt.
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, }; }
Nun, wenn wir eine Funktion haben, die einen Bericht generiert. Wenden wir es auf jeden Typ von Seiten des Projekts an. Zunächst muss ich angeben, dass process.env.DOMAIN
eine Staging-Domäne enthalten sollte (in der Sie Ihr Projekt zuvor aus einem Feature-Branch bereitstellen müssen).
+ 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(); })();
- Sie finden die vollständige Quelle in diesem Kern und ein funktionierendes Beispiel in diesem Repository.
Hinweis : An diesem Punkt möchten Sie mich vielleicht unterbrechen und vergeblich schreien: „Warum nehmen Sie meine Zeit in Anspruch – Sie können Promise.all nicht einmal richtig verwenden!“ Zu meiner Verteidigung wage ich zu sagen, dass es nicht empfehlenswert ist, mehr als eine Lighthouse-Instanz gleichzeitig zu betreiben, da dies die Genauigkeit der Messergebnisse beeinträchtigt. Auch wenn Sie nicht den nötigen Einfallsreichtum zeigen, führt dies zu einer Ausnahme.
Verwendung mehrerer Prozesse
Stehen Sie immer noch auf Parallelmessungen? Gut, vielleicht möchten Sie Node-Cluster verwenden (oder sogar Worker-Threads, wenn Sie gerne mutig spielen), aber es ist sinnvoll, dies nur dann zu besprechen, wenn Ihre Pipeline in der Umgebung mit mehreren verfügbaren Cors ausgeführt wird. Und selbst dann sollten Sie bedenken, dass Sie aufgrund der Natur von Node.js in jeder Prozessverzweigung eine vollwertige Node.js-Instanz erzeugen werden (anstatt dieselbe wiederzuverwenden, was zu einem wachsenden RAM-Verbrauch führt). All dies bedeutet, dass es aufgrund des wachsenden Hardwarebedarfs teurer und etwas schneller wird. Es mag den Anschein haben, dass das Spiel die Kerze nicht wert ist.
Wenn Sie dieses Risiko eingehen möchten, müssen Sie:
- Teilen Sie das URL-Array nach Anzahl der Kerne in Blöcke auf;
- Erstellen Sie einen Fork eines Prozesses entsprechend der Anzahl der Kerne;
- Übertragen Sie Teile des Arrays an die Gabeln und rufen Sie dann generierte Berichte ab.
Um ein Array aufzuteilen, können Sie Multipile-Ansätze verwenden. Der folgende Code – geschrieben in nur ein paar Minuten – wäre nicht schlechter als die anderen:
/** * 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; }
Gabeln nach Anzahl der Kerne herstellen:
// 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 } })();
Lassen Sie uns ein Array von Chunks an untergeordnete Prozesse übertragen und Berichte zurückholen:
(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(); + }); } })();
Und fügen Sie schließlich Berichte zu einem Array zusammen und generieren Sie ein Artefakt.
- Sehen Sie sich den vollständigen Code und das Repository mit einem Beispiel an, das zeigt, wie Lighthouse mit mehreren Prozessen verwendet wird.
Genauigkeit der Messungen
Nun, wir haben die Messungen parallelisiert, was den ohnehin schon unglücklich großen Messfehler des lighthouse
noch vergrößert hat. Aber wie reduzieren wir es? Nun, machen Sie ein paar Messungen und berechnen Sie den Durchschnitt.
Dazu schreiben wir eine Funktion, die den Durchschnitt zwischen aktuellen und vorherigen Messergebnissen berechnet.
// 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, } }), } }
Ändern Sie dann unseren Code, um sie zu verwenden:
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(); }); }
- Schauen Sie sich das Wesentliche mit dem vollständigen Code und dem Repository mit einem Beispiel an.
Und jetzt können wir lighthouse
in die Pipeline aufnehmen.
Hinzufügen zur Pipeline
Erstellen Sie zunächst eine Konfigurationsdatei mit dem Namen .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
Die mehrfach installierten Pakete werden für den puppeteer
benötigt. Alternativ können Sie docker
verwenden. Abgesehen davon macht es Sinn, dass wir die Art des Artefakts als Leistung festlegen. Und sobald sowohl der Master- als auch der Feature-Branch es haben, sehen Sie ein Widget wie dieses in der Merge-Anfrage:
Hübsch?
Fortsetzen
Wir sind endlich mit einem komplexeren Fall fertig. Offensichtlich gibt es neben dem Leuchtturm mehrere ähnliche Tools. Zum Beispiel sitespeed.io. Die GitLab-Dokumentation enthält sogar einen Artikel, der erklärt, wie man sitespeed
in der Pipeline von GitLab verwendet. Es gibt auch ein Plugin für GitLab, mit dem wir ein Artefakt generieren können. Aber wer würde Community-gesteuerte Open-Source-Produkte denen vorziehen, die einem Unternehmensmonster gehören?
Keine Ruhe für die Bösen
Es mag so aussehen, als wären wir endlich da, aber nein, noch nicht. Wenn Sie eine kostenpflichtige GitLab-Version verwenden, sind Artefakte mit Berichtstypen, metrics
und performance
in den Plänen ab premium
und silver
vorhanden, die für jeden Benutzer 19 US-Dollar pro Monat kosten. Außerdem können Sie nicht einfach eine bestimmte Funktion kaufen, die Sie benötigen – Sie können nur den Plan ändern. Es tut uns leid. Was können wir also tun? Im Gegensatz zu GitHub mit seiner Checks-API und Status-API würde GitLab es Ihnen nicht erlauben, ein tatsächliches Widget in der Merge-Anfrage selbst zu erstellen. Und es gibt keine Hoffnung, sie bald zu bekommen.
Eine Möglichkeit zu überprüfen, ob Sie diese Features tatsächlich unterstützen: Sie können in der Pipeline nach der Umgebungsvariable GITLAB_FEATURES
suchen. Wenn merge_request_performance_metrics
und metrics_reports
in der Liste fehlen, werden diese Funktionen nicht unterstützt.
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
Wenn es keine Unterstützung gibt, müssen wir uns etwas einfallen lassen. Beispielsweise können wir der Zusammenführungsanforderung einen Kommentar hinzufügen, der die Tabelle kommentiert, die alle benötigten Daten enthält. Wir können unseren Code unberührt lassen – es werden Artefakte erstellt, aber Widgets zeigen immer eine Meldung «metrics are unchanged»
.
Sehr seltsames und nicht offensichtliches Verhalten; Ich musste genau nachdenken, um zu verstehen, was geschah.
Also, was ist der Plan?
- Wir müssen Artefakte aus dem
master
Zweig lesen; - Erstellen Sie einen Kommentar im
markdown
-Format; - Holen Sie sich die Kennung der Zusammenführungsanforderung vom aktuellen Feature-Zweig zum Master;
- Fügen Sie den Kommentar hinzu.
So lesen Sie Artefakte aus dem Master-Zweig
Wenn wir zeigen möchten, wie sich Leistungsmetriken zwischen master
und Feature-Branches ändern, müssen wir Artefakte aus dem master
lesen. Und dazu müssen wir fetch
verwenden.
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; };
Kommentartext erstellen
Wir müssen Kommentartext im markdown
-Format erstellen. Lassen Sie uns einige Servicefunktionen erstellen, die uns helfen werden:
/** * 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} `; };
Skript, das einen Kommentar erstellt
Sie benötigen ein Token, um mit der GitLab-API zu arbeiten. Um eines zu generieren, müssen Sie GitLab öffnen, sich anmelden, die Option „Einstellungen“ des Menüs öffnen und dann „Zugriffstoken“ auf der linken Seite des Navigationsmenüs öffnen. Sie sollten dann das Formular sehen können, mit dem Sie das Token generieren können.
Außerdem benötigen Sie eine ID des Projekts. Sie finden es im Repository 'Einstellungen' (im Untermenü 'Allgemein'):
Um der Zusammenführungsanforderung einen Kommentar hinzuzufügen, müssen wir seine ID kennen. Die Funktion, mit der Sie die ID der Zusammenführungsanforderung abrufen können, sieht folgendermaßen aus:
// 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:
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.
Authentifizierung
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. Getan.
Zusammenfassung
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!