So machen Sie die Leistung mit GitLab CI und Hoodoo von GitLab-Artefakten sichtbar

Veröffentlicht: 2022-03-10
Kurzzusammenfassung ↬ Es reicht nicht aus, eine Anwendung zu optimieren. Sie müssen verhindern, dass sich die Leistung verschlechtert, und der erste Schritt dazu besteht darin, Leistungsänderungen sichtbar zu machen. In diesem Artikel zeigt Anton Nemtsev einige Möglichkeiten, sie in den GitLab-Merge-Requests anzuzeigen.

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

  1. Messen Sie die Werte Ihrer ausgewählten Metriken auf jedem Seitentyp für die Projekte Ihrer Mitbewerber;
  2. Messen Sie dieselben Metriken für Ihr Projekt;
  3. 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 %.

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

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 
Das Ergebnis der Befehlsausführung zeigt die Größe von index.js
Das Ergebnis der Befehlsausführung zeigt die Größe von index.js. (Große Vorschau)

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.

Ein Screenshot des Terminals; Die Größe der Datei liegt unter dem Limit und wird grün angezeigt
Ein Screenshot des Terminals; Die Größe der Datei liegt unter dem Limit und wird grün angezeigt. (Große Vorschau)

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.

Ein Screenshot des Terminals, bei dem die Dateigröße das Limit überschreitet und rot angezeigt wird. Das Skript wurde mit einem Nicht-Null-Code beendet.
Ein Screenshot des Terminals, bei dem die Dateigröße das Limit überschreitet und rot angezeigt wird. Das Skript wurde mit einem Nicht-Null-Code beendet. (Große Vorschau)

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.

Ein Screenshot des Terminals, bei dem der Commit abgebrochen wird, weil die Größe der Datei das Limit überschreitet
Ein Screenshot des Terminals, bei dem der Commit abgebrochen wird, weil die Größe der Datei das Limit überschreitet. (Große Vorschau)

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?

  1. Definieren Sie Artefakte in der Pipeline.
  2. Ä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
  1. expire_in: 7 days – Artefakt wird 7 Tage lang existieren.
  2.  paths: metric.txt

    Es wird im Root-Katalog gespeichert. Wenn Sie diese Option überspringen, ist es nicht möglich, sie herunterzuladen.
  3.  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:

Der Befehl size-limit --json gibt JSON an die Konsole aus. JSON enthält ein Array von Objekten, die einen Dateinamen und eine Dateigröße enthalten und uns wissen lassen, ob die Größenbeschränkung überschritten wird
Der Befehl size-limit --json JSON an die Konsole aus. JSON enthält ein Array von Objekten, die einen Dateinamen und eine Dateigröße enthalten und uns wissen lassen, ob die Größenbeschränkung überschritten wird. (Große Vorschau)

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:

Screenshot mit einer Zusammenführungsanfrage, die uns ein Widget mit neuem und altem Metrikwert in runden Klammern zeigt
Screenshot mit einer Zusammenführungsanfrage, die uns ein Widget mit neuem und altem Metrikwert in runden Klammern zeigt. (Große Vorschau)

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 :

  1. Die Adresse, die wir analysieren möchten;
  2. lighthouse -Optionen, insbesondere Browser- port und output (Ausgabeformat des Berichts);
  3. report configuration und lighthouse: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:

  1. subject
    Seitenkennung (es ist ziemlich praktisch, einen solchen Pfadnamen zu verwenden);
  2. 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:

  1. name
    Name der Messung, z. B. Time to first byte oder Time to interactive .
  2. value
    Numerisches Messergebnis.
  3. desiredSize
    Wenn der Zielwert so klein wie möglich sein soll, z. B. für die Metrik Time to interactive , dann sollte der Wert smaller sein . Wenn es möglichst groß sein soll, zB für den Leuchtturm Performance score , dann verwenden Sie larger .
 { "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.

Screenshot mit Leuchtturmbericht. Es gibt Performance-Score, A11y-Score, Best-Practices-Score, SEO-Score
Screenshot mit Leuchtturmbericht. Es gibt Performance-Score, A11y-Score, Best-Practices-Score, SEO-Score. (Große Vorschau)
 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:

  1. Teilen Sie das URL-Array nach Anzahl der Kerne in Blöcke auf;
  2. Erstellen Sie einen Fork eines Prozesses entsprechend der Anzahl der Kerne;
  3. Ü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:

Ein Screenshot der Zusammenführungsanforderungsseite. Es gibt ein Widget, das anzeigt, welche Leuchtturm-Metriken sich wie genau geändert haben
Ein Screenshot der Zusammenführungsanforderungsseite. Es gibt ein Widget, das anzeigt, welche Leuchtturm-Metriken sich wie genau geändert haben. (Große Vorschau)

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.

Ein Screenshot des von Ilya Klimov (GitLab-Mitarbeiter) geposteten Tweets schrieb über die Wahrscheinlichkeit des Auftretens von Analoga für Github Checks und Status API: „Extremely unwahrscheinlich. Überprüfungen sind bereits über die Commit-Status-API verfügbar, und was den Status betrifft, streben wir danach, ein geschlossenes Ökosystem zu sein.“
Ein Screenshot des von Ilya Klimov (GitLab-Mitarbeiter) geposteten Tweets, der über die Wahrscheinlichkeit des Auftretens von Analoga für Github Checks und Status API geschrieben hat. (Große Vorschau)

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?

  1. Wir müssen Artefakte aus dem master Zweig lesen;
  2. Erstellen Sie einen Kommentar im markdown -Format;
  3. Holen Sie sich die Kennung der Zusammenführungsanforderung vom aktuellen Feature-Zweig zum Master;
  4. 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.

Screenshot, der das Token-Generierungsformular und die oben erwähnten Menüoptionen zeigt.
Screenshot, der das Token-Generierungsformular und die oben erwähnten Menüoptionen zeigt. (Große Vorschau)

Außerdem benötigen Sie eine ID des Projekts. Sie finden es im Repository 'Einstellungen' (im Untermenü 'Allgemein'):

Der Screenshot zeigt die Einstellungsseite, auf der Sie die Projekt-ID finden
Der Screenshot zeigt die Einstellungsseite, auf der Sie die Projekt-ID finden. (Große Vorschau)

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:

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. (Große Vorschau)

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!