如何使用 GitLab CI 和 GitLab Artifacts 的 Hoodoo 使性能可見

已發表: 2022-03-10
快速總結↬優化應用程序是不夠的。 您需要防止性能下降,而要做到這一點的第一步是讓性能變化可見。 在本文中,Anton Nemtsev 展示了幾種在 GitLab 合併請求中顯示它們的方法。

性能下降是我們每天都面臨的問題。 我們可以努力使應用程序快速運行,但我們很快就結束了我們開始的地方。 之所以發生這種情況,是因為添加了新功能,而且我們有時對我們不斷添加和更新的包沒有重新考慮,或者考慮我們代碼的複雜性。 這通常是一件小事,但仍然是關於小事的。

我們無法承受緩慢的應用程序。 績效是可以帶來和留住客戶的競爭優勢。 我們不能經常花時間重新優化應用程序。 它昂貴且複雜。 這意味著,儘管從業務角度來看性能的所有好處,但它幾乎沒有盈利。 作為為任何問題提出解決方案的第一步,我們需要讓問題可見。 本文將幫助您解決這個問題。

注意如果您對 Node.js 有基本的了解,對您的 CI/CD 的工作方式有一個模糊的概念,並且關心應用程序的性能或它可以帶來的業務優勢,那麼我們就可以開始了。

如何為項目創建績效預算

我們應該問自己的第一個問題是:

“什麼是高性能項目?”

“我應該使用哪些指標?”

“這些指標的哪些值是可以接受的?”

指標選擇超出了本文的範圍,並且高度依賴於項目上下文,但我建議您從閱讀 Philip Walton 的以用戶為中心的性能指標開始。

從我的角度來看,使用庫的大小(以千字節為單位)作為 npm 包的度量是一個好主意。 為什麼? 嗯,這是因為如果其他人將您的代碼包含在他們的項目中,他們可能希望盡量減少您的代碼對其應用程序最終大小的影響。

對於該站點,我會考慮第一個字節的時間 (TTFB) 作為一個指標。 該指標顯示服務器響應某些內容所需的時間。 這個指標很重要,但很模糊,因為它可以包括任何東西——從服務器渲染時間開始,到延遲問題結束。 因此,最好將它與 Server Timing 或 OpenTracing 結合使用,以找出它的確切組成部分。

您還應該考慮諸如交互時間 (TTI) 和首次有意義的繪製(後者很快將被最大內容繪製 (LCP) 取代)等指標。 我認為這兩個都是最重要的——從感知性能的角度來看。

但請記住:指標總是與上下文相關的,所以請不要認為這是理所當然的。 想想在你的具體情況下什麼是重要的。

定義指標所需值的最簡單方法是使用您的競爭對手——甚至是您自己。 此外,有時,諸如性能預算計算器之類的工具可能會派上用場 - 只需稍微嘗試一下即可。

性能下降是我們每天都面臨的問題。 我們可以努力使應用程序快速運行,但很快我們就結束了我們開始的地方。

利用競爭對手為您謀取利益

如果你碰巧從一隻欣喜若狂的過度興奮的熊身邊逃跑,那麼你已經知道,你不需要成為奧運會冠軍就可以擺脫這個麻煩。 你只需要比其他人快一點。

所以製作一個競爭對手名單。 如果這些是相同類型的項目,那麼它們通常由彼此相似的頁麵類型組成。 例如,對於一個網店來說,它可能是一個包含產品列表、產品詳細信息頁面、購物車、結帳等的頁面。

  1. 衡量您在競爭對手項目的每種類型頁面上選擇的指標的價值;
  2. 在您的項目中衡量相同的指標;
  3. 為競爭對手的項目中的每個指標找到比您的價值更接近的指標。 為他們增加 20% 並設定為您的下一個目標。

為什麼是 20%? 這是一個神奇的數字,據說這意味著肉眼可以看到差異。 您可以在 Denys Mishunov 的文章“為什麼感知性能很重要,第 1 部分:時間感知”中閱讀更多關於這個數字的信息。

與陰影的戰鬥

你有一個獨特的項目嗎? 沒有競爭對手? 或者你已經在所有可能的意義上比他們中的任何一個都好? 這不是問題。 你總是可以與唯一有價值的對手競爭,即你自己。 在每種類型的頁面上衡量項目的每個性能指標,然後將它們提高 20%。

跳躍後更多! 繼續往下看↓

綜合測試

有兩種衡量性能的方法:

  • 合成(在受控環境中)
  • RUM (真實用戶測量)
    正在從生產中的真實用戶那裡收集數據。

在本文中,我們將使用綜合測試並假設我們的項目使用帶有內置 CI 的 GitLab 進行項目部署。

圖書館及其大小作為衡量標準

假設您決定開發一個庫並將其發佈到 NPM。 您希望它保持輕便——比競爭對手輕得多——因此它對最終項目的最終尺寸的影響較小。 這可以節省客戶流量——有時是客戶支付的流量。 它還允許項目更快地加載,這對於不斷增長的移動份額和連接速度慢且互聯網覆蓋分散的新市場非常重要。

測量庫大小的包

為了使庫的大小盡可能小,我們需要仔細觀察它隨著開發時間的變化。 但是你怎麼能做到呢? 好吧,我們可以使用來自 Evil Martians 的 Andrey Sitnik 創建的包大小限制。

讓我們安裝它。

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

然後,將其添加到package.json

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

"size-limit":[{},{},…]塊包含我們要檢查的文件大小的列表。 在我們的例子中,它只是一個文件: index.js

NPM 腳本size只是運行size-limit包,它讀取前面提到的配置塊size-limit並檢查那裡列出的文件的大小。 讓我們運行它,看看會發生什麼:

 npm run size 
命令執行結果顯示 index.js 的大小
命令執行的結果顯示 index.js 的大小。 (大預覽)

我們可以看到文件的大小,但這個大小實際上是不受控制的。 讓我們通過向package.json添加limit來解決這個問題:

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

現在,如果我們運行腳​​本,它將根據我們設置的限制進行驗證。

終端截圖;文件大小小於限制並顯示為綠色
終端截圖; 文件的大小小於限制並顯示為綠色。 (大預覽)

如果新開發將文件大小更改為超過定義的限制,腳本將以非零代碼完成。 除了其他事情之外,這意味著它將停止 GitLab CI 中的管道。

文件大小超過限制並顯示為紅色的終端屏幕截圖。該腳本以非零代碼完成。
文件大小超過限制並顯示為紅色的終端屏幕截圖。 該腳本以非零代碼完成。 (大預覽)

現在我們可以使用 git hook 在每次提交之前檢查文件大小是否符合限制。 我們甚至可以使用 husky 包以一種簡單易用的方式製作它。

讓我們安裝它。

 npm i -D husky

然後,修改我們的package.json

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

現在在每次提交自動執行之前npm run size命令,如果它以非零代碼結束,那麼提交將永遠不會發生。

由於文件大小超過限製而中止提交的終端的屏幕截圖
由於文件大小超過限製而中止提交的終端屏幕截圖。 (大預覽)

但是有很多方法可以跳過鉤子(故意甚至是偶然),所以我們不應該過分依賴它們。

此外,重要的是要注意我們不需要進行此檢查阻塞。 為什麼? 因為當您添加新功能時,庫的大小會增長是可以的。 我們需要使更改可見,僅此而已。 這將有助於避免由於引入了我們不需要的幫助程序庫而導致的意外大小增加。 而且,也許,讓開發人員和產品所有者有理由考慮所添加的功能是否值得增加尺寸。 或者,也許,是否有更小的替代包。 Bundlephobia 允許我們為幾乎所有 NPM 包找到替代方案。

那麼我們應該怎麼做呢? 讓我們直接在合併請求中顯示文件大小的變化! 但是你不要直接推到掌握; 你表現得像個成熟的開發者​​,對吧?

在 GitLab CI 上運行我們的檢查

讓我們添加一個度量類型的 GitLab 工件。 工件是一個文件,它將在管道操作完成後“存活”。 這種特定類型的工件允許我們在合併請求中顯示一個額外的小部件,顯示 master 和功能分支中的工件之間度量值的任何變化。 metrics工件的格式是文本 Prometheus 格式。 對於工件內的 GitLab 值,它只是文本。 GitLab 不了解值的確切變化是什麼——它只知道值不同。 那麼,我們具體應該怎麼做呢?

  1. 在管道中定義工件。
  2. 更改腳本,以便它在管道上創建工件。

要創建工件,我們需要以這種方式更改.gitlab-ci.yml

 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 — 工件將存在 7 天。
  2.  paths: metric.txt

    它將保存在根目錄中。 如果您跳過此選項,則無法下載它。
  3.  reports: metrics: metric.txt

    該工件將具有reports:metrics

現在讓我們讓 Size Limit 生成一個報告。 為此,我們需要更改package.json

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

size-limit with key --json將以 json 格式輸出數據:

命令 size-limit --json 將 JSON 輸出到控制台。 JSON 包含一個包含文件名和大小的對像數組,並讓我們知道它是否超出大小限制
命令size-limit --json JSON 輸出到控制台。 JSON 包含一個對像數組,其中包含文件名和大小,並讓我們知道它是否超過了大小限制。 (大預覽)

並且 redirection > size-limit.json會將 JSON 保存到文件size-limit.json中。

現在我們需要從中創建一個工件。 格式歸結為[metrics name][space][metrics value] 。 讓我們創建腳本generate-metric.js

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

並將其添加到package.json

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

因為我們使用了post前綴,所以npm run size命令將首先運行size腳本,然後自動執行postsize腳本,這將導致我們的工件metric.txt文件的創建。

結果,當我們將這個分支合併到 master,改變一些東西並創建一個新的合併請求時,我們將看到以下內容:

帶有合併請求的屏幕截圖,它向我們展示了一個在圓括號中包含新舊度量值的小部件
帶有合併請求的屏幕截圖,它向我們展示了一個在圓括號中包含新舊度量值的小部件。 (大預覽)

在頁面上出現的小部件中,我們首先看到度量的名稱( size ),然後是特徵分支中的度量值以及圓括號內的主值。

現在我們實際上可以看到如何更改包的大小並做出是否應該合併它的合理決定。

  • 您可能會在此存儲庫中看到所有這些代碼。

恢復

好的! 所以,我們已經想出瞭如何處理這種瑣碎的情況。 如果您有多個文件,只需使用換行符分隔指標。 作為 Size Limit 的替代方案,您可以考慮 bundlesize。 如果您使用的是 WebPack,則可以通過使用--profile--json標誌構建所需的所有大小:

 webpack --profile --json > stats.json

如果您使用的是 next.js,則可以使用 @next/bundle-analyzer 插件。 由你決定!

使用燈塔

Lighthouse 是項目分析中的事實標準。 讓我們編寫一個腳本,讓我們能夠衡量性能、a11y、最佳實踐,並為我們提供 SEO 分數。

測量所有東西的腳本

首先,我們需要安裝將進行測量的燈塔包。 我們還需要安裝 puppeteer,我們將把它用作無頭瀏覽器。

 npm i -D lighthouse puppeteer

接下來,讓我們創建一個lighthouse.js腳本並啟動我們的瀏覽器:

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

現在讓我們編寫一個函數來幫助我們分析給定的 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); // … }

偉大的! 我們現在有一個函數,它將接受瀏覽器對像作為參數,並返回一個函數,該函數將接受URL作為參數,並在將該URL傳遞給lighthouse後生成報告。

我們將以下參數傳遞給lighthouse

  1. 我們要分析的地址;
  2. lighthouse選項,特別是瀏覽器portoutput (報告的輸出格式);
  3. report配置和lighthouse:full (我們可以測量的所有內容)。 如需更精確的配置,請查看文檔。

精彩的! 我們現在有了我們的報告。 但是我們能用它做什麼呢? 好吧,我們可以根據限制檢查指標並使用非零代碼退出腳本,這將停止管道:

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

但我們只是想讓性能可見且無阻塞? 那麼讓我們採用另一種神器類型:GitLab 性能神器。

GitLab 性能神器

為了理解這種工件格式,我們必須閱讀 sitespeed.io 插件的代碼。 (為什麼 GitLab 不能在他們自己的文檔中描述他們的工件的格式?神秘。

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

工件是包含對像數組的JSON文件。 它們中的每一個都代表一個關於一個URL的報告。

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

每個頁面由具有以下屬性的對象表示:

  1. subject
    頁面標識符(使用這樣的路徑名非常方便);
  2. metrics
    對像數組(每個對象代表在頁面上進行的一次測量)。
 { "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }

measurement是包含以下屬性的對象:

  1. name
    測量名稱,例如它可能是Time to first byteTime to interactive
  2. value
    數值測量結果。
  3. desiredSize
    如果目標值應該盡可能小,例如Time to interactive度量,那麼該值應該smaller 。 如果它應該盡可能大,例如對於燈塔Performance score ,則使用larger
 { "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }

讓我們修改我們的buildReport函數,讓它返回一個帶有標準燈塔指標的頁面的報告。

燈塔報告截圖。有性能得分、a11y得分、最佳實踐得分、SEO得分
燈塔報告截圖。 有性能得分、a11y 得分、最佳實踐得分、SEO 得分。 (大預覽)
 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, }; }

現在,當我們有一個生成報告的函數時。 讓我們將它應用於項目的每種類型的頁面。 首先,我需要聲明process.env.DOMAIN應該包含一個暫存域(您需要事先從功能分支部署項目到該暫存域)。

 + 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(); })();
  • 您可以在此存儲庫中的此要點和工作示例中找到完整的源代碼。

注意此時,你可能想打斷我並徒勞地尖叫,“你為什麼要佔用我的時間——你甚至不能正確使用 Promise.all!” 在我的辯護中,我敢說,不建議同時運行多個燈塔實例,因為這會對測量結果的準確性產生不利影響。 另外,如果你不表現出應有的聰明才智,就會導致異常。

使用多個進程

您還在進行並行測量嗎? 好吧,您可能想要使用節點集群(如果您喜歡大膽的話,甚至可以使用工作線程),但是只有在您的管道在具有多個可用 cors 的環境中運行時才討論它是有意義的。 即使這樣,您也應該記住,由於 Node.js 的性質,您將在每個進程分叉中生成全權重的 Node.js 實例(而不是重複使用會導致 RAM 消耗增加的同一個實例)。 所有這一切都意味著,由於硬件需求的增長,它的成本會更高,而且速度會更快。 看來這場比賽得不償失。

如果你想承擔這個風險,你需要:

  1. 按核心數將 URL 數組拆分為塊;
  2. 根據核數創建一個進程的fork;
  3. 將數組的一部分傳輸到分支,然後檢索生成的報告。

要拆分數組,您可以使用多堆方法。 下面的代碼——只用了幾分鐘的時間——不會比其他代碼差:

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

根據核心數製作分叉:

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

讓我們將一組塊傳輸到子進程並檢索報告:

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

最後,將報告重新組合到一個數組中並生成一個工件。

  • 通過一個示例查看完整的代碼和存儲庫,該示例展示瞭如何將 lighthouse 與多個進程一起使用。

測量精度

好吧,我們將測量並行化,這增加了lighthouse已經不幸的大測量誤差。 但是我們如何減少它呢? 好吧,進行一些測量併計算平均值。

為此,我們將編寫一個函數來計算當前測量結果與先前測量結果之間的平均值。

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

然後,更改我們的代碼以使用它們:

 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(); }); }
  • 通過示例查看包含完整代碼和存儲庫的要點。

現在我們可以將lighthouse添加到管道中。

將其添加到管道

首先,創建一個名為.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

puppeteer需要多個安裝的軟件包。 作為替代方案,您可以考慮使用docker 。 除此之外,我們將工件的類型設置為性能這一事實是有意義的。 而且,一旦 master 和 feature 分支都擁有它,您將在合併請求中看到這樣的小部件:

合併請求頁面的屏幕截圖。有一個小部件可以顯示哪些燈塔指標發生了變化,以及發生了怎樣的變化
合併請求頁面的屏幕截圖。 有一個小部件可以顯示哪些燈塔指標發生了變化以及變化的程度。 (大預覽)

好的?

恢復

我們終於完成了一個更複雜的案例。 顯然,除了燈塔之外,還有多種類似的工具。 例如,sitespeed.io。 GitLab 文檔甚至包含一篇文章,解釋瞭如何在 GitLab 的管道中使用sitespeed 。 還有一個用於 GitLab 的插件,它允許我們生成一個工件。 但是誰會更喜歡由社區驅動的開源產品而不是企業怪物擁有的產品呢?

惡人不休息

看起來我們終於到了,但不,還沒有。 如果您使用的是付費 GitLab 版本,則計劃中包含具有報告類型metricsperformance的工件,從premium版和silver版開始,每位用戶每月花費 19 美元。 此外,您不能只購買您需要的特定功能 - 您只能更改計劃。 對不起。 那麼我們能做些什麼呢? 與 GitHub 的 Checks API 和 Status API 不同,GitLab 不允許您自己在合併請求中創建實際的小部件。 並且沒有希望很快得到它們。

Ilya Klimov(GitLab 員工)發布的推文屏幕截圖寫道,Github Checks 和 Status API 出現類似物的可能性:“極不可能。檢查已經可以通過提交狀態 API 獲得,至於狀態,我們正在努力成為一個封閉的生態系統。”
Ilya Klimov(GitLab 員工)發布的推文截圖,他寫了關於 Github Checks 和 Status API 出現類似物的概率。 (大預覽)

檢查您是否確實支持這些功能的一種方法:您可以在管道中搜索環境變量GITLAB_FEATURES 。 如果列表中缺少merge_request_performance_metricsmetrics_reports ,則不支持此功能。

 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

如果沒有支持,我們需要想出一些辦法。 例如,我們可能會在合併請求中添加評論,對錶格進行評論,其中包含我們需要的所有數據。 我們可以保持我們的代碼不變——將創建工件,但小部件將始終顯示一條消息«metrics are unchanged»

非常奇怪和不明顯的行為; 我必須仔細思考才能理解發生了什麼。

那麼,有什麼計劃呢?

  1. 我們需要從master分支讀取工件;
  2. markdown格式創建評論;
  3. 獲取從當前特性分支到master的合併請求的標識;
  4. 添加評論。

如何從主分支讀取工件

如果我們想展示性能指標在master和 feature 分支之間是如何變化的,我們需要從master讀取 artifact。 為此,我們需要使用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; };

創建評論文本

我們需要以markdown格式構建評論文本。 讓我們創建一些對我們有幫助的服務函數:

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

將建立評論的腳本

您需要有一個令牌才能使用 GitLab API。 為了生成一個,您需要打開 GitLab,登錄,打開菜單的“設置”選項,然後打開導航菜單左側的“訪問令牌”。 然後您應該能夠看到允許您生成令牌的表單。

屏幕截圖,顯示了我上面提到的令牌生成表單和菜單選項。
屏幕截圖,顯示了我上面提到的令牌生成表單和菜單選項。 (大預覽)

此外,您將需要項目的 ID。 您可以在存儲庫“設置”中找到它(在子菜單“常規”中):

屏幕截圖顯示設置頁面,您可以在其中找到項目 ID
屏幕截圖顯示設置頁面,您可以在其中找到項目 ID。 (大預覽)

要向合併請求添加註釋,我們需要知道它的 ID。 允許您獲取合併請求 ID 的函數如下所示:

 // 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. (大預覽)

恢復

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.

驗證

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. 完畢。

概括

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. 祝你好運!