GitLabCIとGitLabアーティファクトのフードゥーでパフォーマンスを可視化する方法
公開: 2022-03-10パフォーマンスの低下は、私たちが日常的に直面している問題です。 アプリケーションを高速化するために努力することはできましたが、すぐに開始したところに行き着きました。 これは、新しい機能が追加されたことと、常に追加および更新するパッケージについて考え直したり、コードの複雑さについて考えたりすることがない場合があるために発生しています。 それは一般的に小さなことですが、それでもすべては小さなことです。
遅いアプリを使う余裕はありません。 パフォーマンスは、顧客を獲得して維持できる競争上の優位性です。 アプリの最適化に定期的に時間を費やす余裕はありません。 それは費用がかかり、複雑です。 つまり、ビジネスの観点から見たパフォーマンスのすべての利点にもかかわらず、それはほとんど利益を生まないということです。 問題の解決策を考え出すための最初のステップとして、問題を可視化する必要があります。 この記事はまさにそれを支援します。
注: Node.jsの基本を理解し、CI / CDがどのように機能するかについて漠然とした考えを持っていて、アプリのパフォーマンスやアプリがもたらすビジネス上の利点に関心がある場合は、これで問題ありません。
プロジェクトのパフォーマンス予算を作成する方法
私たちが自分自身に尋ねるべき最初の質問は次のとおりです。
「パフォーマープロジェクトとは何ですか?」
「どのメトリックを使用する必要がありますか?」
「これらのメトリックのどの値が受け入れられますか?」
メトリックの選択はこの記事の範囲外であり、プロジェクトコンテキストに大きく依存しますが、PhilipWaltonによるユーザー中心のパフォーマンスメトリックを読むことから始めることをお勧めします。
私の観点からは、ライブラリのサイズ(キロバイト単位)をnpmパッケージのメトリックとして使用することをお勧めします。 なんで? 他の人があなたのコードをプロジェクトに含めている場合、彼らはおそらくあなたのコードがアプリケーションの最終的なサイズに与える影響を最小限に抑えたいと思うからです。
このサイトでは、Time To First Byte(TTFB)をメトリックと見なします。 このメトリックは、サーバーが何かで応答するのにかかる時間を示します。 このメトリックは重要ですが、サーバーのレンダリング時間から始まり、遅延の問題が発生するまで、何でも含めることができるため、非常にあいまいです。 したがって、サーバータイミングまたはOpenTracingと組み合わせて使用すると、正確に何で構成されているかを確認できます。
Time to Interactive(TTI)やFirst Meaningful Paint(後者はまもなくLargest Contentful Paint(LCP)に置き換えられます)などの指標も考慮する必要があります。 知覚されるパフォーマンスの観点から、これらの両方が最も重要だと思います。
ただし、メトリックは常にコンテキストに関連しているため、これを当然のことと見なさないでください。 特定のケースで何が重要かを考えてください。
メトリックの望ましい値を定義する最も簡単な方法は、競合他社、または自分自身を使用することです。 また、パフォーマンスバジェット計算機などのツールが便利な場合もあります。少し試してみてください。
パフォーマンスの低下は、私たちが日常的に直面している問題です。 アプリケーションを高速化するために努力することはできましたが、すぐに開始したところに行き着きました。
「「
あなたの利益のために競合他社を使用する
恍惚とした興奮のクマから逃げ出したことがあるなら、この問題から抜け出すために走るのにオリンピックチャンピオンである必要はないことをすでに知っています。 あなたは他の人より少し速くする必要があります。
したがって、競合他社のリストを作成します。 これらが同じタイプのプロジェクトである場合、通常、それらは互いに類似したページタイプで構成されます。 たとえば、インターネットショップの場合、商品リスト、商品詳細ページ、ショッピングカート、チェックアウトなどのページである可能性があります。
- 競合他社のプロジェクトの各タイプのページで、選択したメトリックの値を測定します。
- プロジェクトで同じ指標を測定します。
- 競合他社のプロジェクトの各メトリックについて、自分の値よりも近いものを見つけます。 それらに20%を追加し、次の目標として設定します。
なぜ20%? これは、違いが肉眼で目立つことを意味すると思われるマジックナンバーです。 この数値の詳細については、DenysMishunovの記事「WhyPerceivedPerformance Matters、Part 1:The PerceptionOfTime」を参照してください。
影との戦い
ユニークなプロジェクトはありますか? 競合他社はありませんか? それとも、あなたはすべての可能な意味でそれらのどれよりもすでに優れていますか? それは問題ではありません。 あなたはいつでも唯一の価値のある敵、すなわちあなた自身と競争することができます。 各タイプのページでプロジェクトの各パフォーマンスメトリックを測定し、同じ20%向上させます。
合成テスト
パフォーマンスを測定する方法は2つあります。
- 合成(制御された環境で)
- RUM (Real User Measurements)
データは、本番環境の実際のユーザーから収集されています。
この記事では、合成テストを使用し、プロジェクトでプロジェクトのデプロイに組み込みのCIを備えたGitLabを使用することを前提としています。
ライブラリとメトリックとしてのそのサイズ
ライブラリを開発してNPMに公開することにしたとしましょう。 競合他社よりもはるかに軽量な状態を維持したいので、結果として得られるプロジェクトの最終サイズへの影響は少なくなります。 これにより、クライアントのトラフィックが節約されます。クライアントが料金を支払っているトラフィックも節約できます。 また、プロジェクトの読み込みを高速化することもできます。これは、モバイルシェアの拡大と、接続速度が遅く、インターネットカバレッジが断片化されている新しい市場に関して非常に重要です。
ライブラリサイズを測定するためのパッケージ
ライブラリのサイズをできるだけ小さくするには、開発期間中にライブラリがどのように変化するかを注意深く監視する必要があります。 しかし、どうすればそれを行うことができますか? ええと、EvilMartiansのAndreySitnikによって作成されたパッケージSizeLimitを使用できます。
インストールしましょう。
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":[{},{},…]
ブロックには、チェックするファイルのサイズのリストが含まれています。 私たちの場合、それはただ1つのファイルindex.js
です。
NPMスクリプトsize
は、 size-limit
パッケージを実行するだけです。このパッケージは、前述の構成ブロックのsize-limit
を読み取り、そこにリストされているファイルのサイズをチェックします。 それを実行して、何が起こるか見てみましょう:
npm run size
ファイルのサイズを確認できますが、このサイズは実際には制御されていません。 package.json
にlimit
を追加して修正しましょう:
"size-limit": [ { + "limit": "2 KB", "path": "index.js" } ],
スクリプトを実行すると、設定した制限に対して検証されます。
新しい開発でファイルサイズが定義された制限を超えるポイントに変更された場合、スクリプトはゼロ以外のコードで完了します。 これは、他のことは別として、GitLabCIのパイプラインを停止することを意味します。
これで、gitフックを使用して、すべてのコミットの前にファイルサイズを制限と照合できます。 ハスキーパッケージを使用して、素晴らしくシンプルな方法で作成することもできます。
インストールしましょう。
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パッケージの代替手段を見つけることができます。
だから何をすべきか? マージリクエストでファイルサイズの変更を直接表示しましょう! ただし、マスターに直接プッシュすることはありません。 あなたは大人の開発者のように振る舞いますよね?
GitLabCIでチェックを実行する
メトリックスタイプのGitLabアーティファクトを追加しましょう。 アーティファクトはファイルであり、パイプライン操作が終了した後に「ライブ」になります。 この特定のタイプのアーティファクトを使用すると、マージリクエストに追加のウィジェットを表示して、マスターのアーティファクトと機能ブランチの間のメトリックの値の変化を表示できます。 metrics
アーティファクトの形式は、テキストPrometheus形式です。 アーティファクト内のGitLab値の場合、それは単なるテキストです。 GitLabは、値が正確に変更されたものが何であるかを理解していません。値が異なることを知っているだけです。 それで、私たちは正確に何をすべきですか?
- パイプラインでアーティファクトを定義します。
- パイプラインにアーティファクトを作成するようにスクリプトを変更します。
アーティファクトを作成するには、 .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
-
expire_in: 7 days
—アーティファクトは7日間存在します。 paths: metric.txt
ルートカタログに保存されます。 このオプションをスキップすると、ダウンロードできなくなります。reports: metrics: metric.txt
アーティファクトのタイプreports:metrics
になります
次に、サイズ制限でレポートを生成します。 そのためには、 package.json
を変更する必要があります。
"scripts": { - "size": "size-limit", + "size": "size-limit --json > size-limit.json", "test": "jest && eslint ." },
キー--json
を使用したsize-limit
は、データをjson形式で出力します。
また、リダイレクト> 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
ファイルが作成されます。
その結果、このブランチをマスターにマージし、何かを変更して新しいマージリクエストを作成すると、次のように表示されます。
ページに表示されるウィジェットで、最初に、メトリックの名前( size
)に続いて、機能ブランチのメトリックの値と、丸括弧内のマスターの値を確認します。
これで、パッケージのサイズを変更する方法を実際に確認し、それをマージするかどうかを合理的に決定できます。
- このリポジトリにこのすべてのコードが表示される場合があります。
履歴書
わかった! だから、私たちは些細なケースを処理する方法を理解しました。 複数のファイルがある場合は、メトリックを改行で区切ってください。 サイズ制限の代わりに、バンドルサイズを検討することもできます。 WebPackを使用している場合は、 --json
--profile
を使用してビルドすることで、必要なすべてのサイズを取得できます。
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
に次の引数を渡します。
- 分析したいアドレス。
-
lighthouse
オプション、特にブラウザport
、およびoutput
(レポートの出力形式)。 -
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
ファイルです。 それぞれが1つのURL
に関するレポートを表します。
[{page 1}, {page 2}, …]
各ページは、次の属性を持つオブジェクトによって表されます。
-
subject
ページ識別子(このようなパス名を使用すると非常に便利です)。 -
metrics
オブジェクトの配列(それぞれがページで行われた1つの測定値を表します)。
{ "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }
measurement
は、次の属性を含むオブジェクトです。
-
name
測定名。たとえばTime to first byte
Time to interactive
の時間の場合があります。 -
value
数値測定結果。 -
desiredSize
目標値をできるだけ小さくする必要がある場合(たとえば、Time to interactive
など)、値をsmaller
する必要があります。 灯台のPerformance score
など、できるだけ大きくする必要がある場合は、larger
値を使用します。
{ "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }
buildReport
関数を変更して、標準の灯台メトリックを含む1ページのレポートを返すようにします。
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を正しく使用することさえできません!」 私の弁護では、測定結果の精度に悪影響を与えるため、同時に複数の灯台インスタンスを実行することはお勧めできません。 また、工夫を凝らさないと例外となります。
複数のプロセスの使用
あなたはまだ並行測定に興味がありますか? 結構です。ノードクラスター(または太字で再生したい場合はワーカースレッド)を使用することもできますが、パイプラインが複数の利用可能なcorを備えた環境で実行されている場合にのみ説明するのが理にかなっています。 それでも、Node.jsの性質上、各プロセスフォークでフルウェイトのNode.jsインスタンスが生成されることを覚えておく必要があります(同じインスタンスを再利用してRAM消費量を増やすのではなく)。 これはすべて、ハードウェア要件が増大し、少し高速になるため、コストが高くなることを意味します。 ゲームはろうそくの価値がないように見えるかもしれません。
そのリスクを冒したい場合は、次のことを行う必要があります。
- URL配列をコア数でチャンクに分割します。
- コアの数に応じてプロセスのフォークを作成します。
- アレイの一部をフォークに転送してから、生成されたレポートを取得します。
配列を分割するには、複数のアプローチを使用できます。 次のコード(ほんの数分で書かれています)は、他のコードよりも悪くはありません。
/** * 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(); + }); } })();
そして最後に、レポートを1つの配列に再アセンブルし、アーティファクトを生成します。
- 複数のプロセスで灯台を使用する方法を示す例を使用して、完全なコードとリポジトリを確認してください。
測定の精度
さて、測定を並列化したため、 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
の使用を検討することもできます。 それとは別に、アーティファクトのタイプをパフォーマンスとして設定することは理にかなっています。 そして、マスターブランチと機能ブランチの両方がそれを取得するとすぐに、マージリクエストに次のようなウィジェットが表示されます。
良い?
履歴書
最終的に、より複雑なケースが完了しました。 明らかに、灯台以外にも同様のツールが複数あります。 たとえば、sitespeed.ioです。 GitLabのドキュメントには、GitLabのパイプラインでsitespeed
を使用する方法を説明する記事も含まれています。 アーティファクトを生成できるGitLab用のプラグインもあります。 しかし、企業のモンスターが所有する製品よりも、コミュニティ主導のオープンソース製品を好むのは誰でしょうか。
邪悪な者のために休むことはありません
ようやくそこにいるように見えるかもしれませんが、まだです。 有料のGitLabバージョンを使用している場合、レポートタイプのmetrics
とperformance
を備えたアーティファクトが、ユーザーごとに月額19ドルのpremium
とsilver
から始まるプランに存在します。 また、必要な特定の機能を購入するだけでなく、プランを変更することしかできません。 ごめん。 では、私たちに何ができるでしょうか? ChecksAPIとStatusAPIを備えたGitHubとは異なり、GitLabではマージリクエストで実際のウィジェットを自分で作成することはできません。 そして、すぐにそれらを取得する希望はありません。
これらの機能が実際にサポートされているかどうかを確認する1つの方法:パイプラインで環境変数GITLAB_FEATURES
を検索できます。 リストにmerge_request_performance_metrics
とmetrics_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»
というメッセージが表示されます。
非常に奇妙で自明でない行動。 私は何が起こっているのかを理解するために注意深く考えなければなりませんでした。
それで、計画は何ですか?
-
master
ブランチからアーティファクトを読み取る必要があります。 -
markdown
形式でコメントを作成します。 - 現在の機能ブランチからマスターへのマージリクエストの識別子を取得します。
- コメントを追加します。
マスターブランチからアーティファクトを読み取る方法
master
ブランチと機能ブランチの間でパフォーマンスメトリックがどのように変更されるかを示したい場合は、 master
からアーティファクトを読み取る必要があります。 そのためには、 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を取得できる関数は次のようになります。
// 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:
履歴書
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. Good luck with that!