GitLab CI ve GitLab Artifacts Hoodoo ile Performans Nasıl Görünür Yapılır
Yayınlanan: 2022-03-10Performans düşüşü, günlük olarak karşılaştığımız bir sorundur. Uygulamayı hızlandırmak için çaba gösterebilirdik, ancak kısa sürede başladığımız yere varırız. Eklenen yeni özellikler ve sürekli olarak eklediğimiz ve güncellediğimiz paketler hakkında bazen ikinci bir düşüncemizin olmaması veya kodumuzun karmaşıklığı hakkında düşünmemiz nedeniyle oluyor. Genellikle küçük bir şey, ama yine de küçük şeylerle ilgili.
Yavaş bir uygulamaya sahip olmayı göze alamayız. Performans, müşterileri getirebilen ve elde tutabilen bir rekabet avantajıdır. Uygulamaları baştan sona optimize etmek için düzenli olarak zaman harcamayı göze alamayız. Maliyetli ve karmaşıktır. Bu da, iş açısından performansın tüm faydalarına rağmen, pek karlı olmadığı anlamına gelir. Herhangi bir soruna çözüm bulmanın ilk adımı olarak sorunu görünür kılmalıyız. Bu makale tam olarak bu konuda size yardımcı olacaktır.
Not : Node.js hakkında temel bir bilginiz varsa, CI/CD'nizin nasıl çalıştığı hakkında belirsiz bir fikriniz varsa ve uygulamanın performansını veya getirebileceği iş avantajlarını önemsiyorsanız, o zaman hazırız.
Bir Proje İçin Performans Bütçesi Nasıl Oluşturulur
Kendimize sormamız gereken ilk sorular:
“Performans projesi nedir?”
"Hangi metrikleri kullanmalıyım?"
"Bu metriklerin hangi değerleri kabul edilebilir?"
Metrik seçimi bu makalenin kapsamı dışındadır ve büyük ölçüde proje bağlamına bağlıdır, ancak Philip Walton'ın Kullanıcı Odaklı Performans Metriklerini okuyarak başlamanızı tavsiye ederim.
Benim bakış açıma göre, npm paketi için bir metrik olarak kütüphanenin boyutunu kilobayt olarak kullanmak iyi bir fikir. Niye ya? Bunun nedeni, başka insanlar kodunuzu projelerine dahil ediyorsa, kodunuzun uygulamalarının son boyutu üzerindeki etkisini en aza indirmek isteyebilirler.
Site için İlk Bayt Süresini (TTFB) bir metrik olarak düşünürdüm. Bu ölçüm, sunucunun bir şeye yanıt vermesinin ne kadar sürdüğünü gösterir. Bu ölçü önemlidir, ancak oldukça belirsizdir çünkü sunucu oluşturma süresinden başlayarak gecikme sorunlarına kadar her şeyi içerebilir. Bu nedenle, tam olarak neyden oluştuğunu bulmak için Sunucu Zamanlama veya OpenTracing ile birlikte kullanmak güzel.
Ayrıca Etkileşim Süresi (TTI) ve İlk Anlamlı Boyama (ikincisi yakında En Büyük İçerikli Boyama (LCP) ile değiştirilecektir) gibi metrikleri de göz önünde bulundurmalısınız. Algılanan performans açısından bunların her ikisinin de en önemli olduğunu düşünüyorum.
Ancak unutmayın: metrikler her zaman bağlamla ilgilidir , bu nedenle lütfen bunu yalnızca hafife almayın. Özel durumunuzda neyin önemli olduğunu düşünün.
Metrikler için istenen değerleri tanımlamanın en kolay yolu rakiplerinizi, hatta kendinizi kullanmaktır. Ayrıca, zaman zaman Performans Bütçesi Hesaplayıcı gibi araçlar kullanışlı olabilir - sadece biraz oynayın.
Performans düşüşü, her gün karşılaştığımız bir sorundur. Uygulamayı hızlandırmak için çaba gösterebilirdik, ancak kısa sürede başladığımız yere varırız.
“
Kendi Yararınız İçin Rakipleri Kullanın
Kendinden geçmiş bir şekilde aşırı heyecanlı bir ayıdan kaçtıysanız, bu beladan kurtulmak için koşarken Olimpiyat şampiyonu olmanıza gerek olmadığını zaten biliyorsunuzdur. Sadece diğer adamdan biraz daha hızlı olman gerekiyor.
Bu yüzden bir rakip listesi yapın. Bunlar aynı türden projelerse, genellikle birbirine benzer sayfa türlerinden oluşurlar. Örneğin, bir internet mağazası için ürün listesi, ürün ayrıntıları sayfası, alışveriş sepeti, ödeme vb. içeren bir sayfa olabilir.
- Rakibinizin projeleri için her bir sayfa türünde seçtiğiniz metriklerin değerlerini ölçün;
- Projenizde aynı metrikleri ölçün;
- Rakibin projelerindeki her bir metrik için değerinizden daha iyi olanı bulun. Onlara %20 ekleyerek bir sonraki hedefiniz olarak belirleyin.
Neden %20? Bu, sözde farkın çıplak gözle farkedileceği anlamına gelen sihirli bir sayıdır. Bu sayı hakkında daha fazla bilgiyi Denys Mishunov'un “Algılanan Performans Neden Önemlidir, Bölüm 1: Zaman Algısı” başlıklı makalesinde okuyabilirsiniz.
Gölge ile Dövüş
Eşsiz bir projeniz var mı? Rakipleriniz yok mu? Yoksa tüm olası anlamlarda zaten herhangi birinden daha mı iyisiniz? Bu bir sorun değil. Her zaman tek değerli rakiple, yani kendinizle rekabet edebilirsiniz. Projenizin her bir performans metriğini her sayfa türünde ölçün ve ardından aynı %20 oranında daha iyi hale getirin.
Sentetik Testler
Performansı ölçmenin iki yolu vardır:
- Sentetik (kontrollü bir ortamda)
- RUM (Gerçek Kullanıcı Ölçümleri)
Üretimde gerçek kullanıcılardan veriler toplanıyor.
Bu makalede, sentetik testler kullanacağız ve projemizin proje dağıtımı için yerleşik CI ile GitLab kullandığını varsayacağız.
Bir Metrik Olarak Kütüphane ve Boyutu
Bir kitaplık geliştirmeye ve onu NPM'de yayınlamaya karar verdiğinizi varsayalım. Hafif (rakiplerinden çok daha hafif) tutmak istiyorsunuz, bu nedenle sonuçta ortaya çıkan projenin nihai boyutu üzerinde daha az etkisi var. Bu, müşteri trafiğinden, bazen müşterinin ödemesini yaptığı trafikten tasarruf sağlar. Ayrıca, projenin daha hızlı yüklenmesine olanak tanır, bu da artan mobil paylaşım ve yavaş bağlantı hızları ve parçalanmış internet kapsama alanı ile yeni pazarlar açısından oldukça önemlidir.
Kitaplık Boyutunu Ölçmek İçin Paket
Kitaplığın boyutunu olabildiğince küçük tutmak için geliştirme süresi boyunca nasıl değiştiğini dikkatle izlememiz gerekir. Ama bunu nasıl yapabilirsin? Evil Marsians'tan Andrey Sitnik tarafından oluşturulan Size Limit paketini kullanabiliriz.
Hadi yükleyelim.
npm i -D size-limit @size-limit/preset-small-lib
Ardından package.json
ekleyin.
"scripts": { + "size": "size-limit", "test": "jest && eslint ." }, + "size-limit": [ + { + "path": "index.js" + } + ],
"size-limit":[{},{},…]
bloğu, kontrol etmek istediğimiz dosyaların boyutunun bir listesini içerir. Bizim durumumuzda, bu sadece tek bir dosyadır: index.js
.
NPM komut dosyası size
, daha önce bahsedilen yapılandırma bloğu size-limit
okuyan ve burada listelenen dosyaların boyutunu kontrol eden boyut size-limit
paketini çalıştırır. Çalıştıralım ve ne olduğunu görelim:
npm run size
Dosyanın boyutunu görebiliyoruz ama bu boyut aslında kontrol altında değil. Bunu package.json
limit
ekleyerek düzeltelim:
"size-limit": [ { + "limit": "2 KB", "path": "index.js" } ],
Şimdi betiği çalıştırırsak, belirlediğimiz sınıra göre doğrulanacaktır.
Yeni geliştirmenin dosya boyutunu tanımlanan sınırı aşacak şekilde değiştirmesi durumunda, komut dosyası sıfır olmayan kodla tamamlanacaktır. Bu, diğer şeylerin yanı sıra GitLab CI'deki boru hattını durduracağı anlamına gelir.
Artık her işlemden önce dosya boyutunu sınıra göre kontrol etmek için git kancasını kullanabiliriz. Güzel ve basit bir şekilde yapmak için husky paketini bile kullanabiliriz.
Hadi yükleyelim.
npm i -D husky
Ardından package.json
değiştirin.
"size-limit": [ { "limit": "2 KB", "path": "index.js" } ], + "husky": { + "hooks": { + "pre-commit": "npm run size" + } + },
Ve şimdi her bir taahhütten önce otomatik olarak npm run size
komutu yürütülecek ve eğer sıfır olmayan bir kodla bitecekse, taahhüt asla gerçekleşmeyecektir.
Ancak kancaları atlamanın birçok yolu vardır (bilerek veya kazara), bu yüzden onlara çok fazla güvenmemeliyiz.
Ayrıca, bu kontrol engellemesini yapmamız gerekmediğini not etmek önemlidir. Niye ya? Çünkü yeni özellikler eklerken kitaplığın boyutunun büyümesi sorun değil. Değişiklikleri görünür hale getirmemiz gerekiyor, hepsi bu. Bu, ihtiyacımız olmayan bir yardımcı kitaplığın tanıtılması nedeniyle yanlışlıkla boyut artışını önlemeye yardımcı olacaktır. Ve belki de geliştiricilere ve ürün sahiplerine, eklenen özelliğin boyut artışına değip değmediğini düşünmeleri için bir neden verin. Veya belki daha küçük alternatif paketler olup olmadığı. Bundlephobia, neredeyse tüm NPM paketleri için bir alternatif bulmamızı sağlar.
Yani ne yapmalıyız? Dosya boyutundaki değişikliği doğrudan birleştirme isteğinde gösterelim! Ama doğrudan ustalaşmaya zorlamazsınız; yetişkin bir geliştirici gibi davranıyorsun, değil mi?
Kontrolümüzü GitLab CI Üzerinde Çalıştırmak
Metrik türünde bir GitLab yapıtı ekleyelim. Yapı, boru hattı işlemi tamamlandıktan sonra "yaşayacak" bir dosyadır. Bu özel yapı türü, ana yapıdaki yapıt ile özellik dalındaki metrik değerindeki herhangi bir değişikliği gösteren, birleştirme isteğinde ek bir pencere öğesi göstermemize olanak tanır. metrics
yapıtının biçimi, bir metin Prometheus biçimidir. Yapıt içindeki GitLab değerleri için bu yalnızca metindir. GitLab, değerde tam olarak neyin değiştiğini anlamıyor - sadece değerin farklı olduğunu biliyor. Peki, tam olarak ne yapmalıyız?
- İşlem hattındaki yapıtları tanımlayın.
- Komut dosyasını, işlem hattında bir yapı oluşturacak şekilde değiştirin.
Bir yapı oluşturmak için .gitlab-ci.yml
şu şekilde değiştirmemiz gerekiyor:
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 7 gün boyunca var olacaktır. paths: metric.txt
Kök kataloğuna kaydedilecektir. Bu seçeneği atlarsanız, indirmeniz mümkün olmaz.reports: metrics: metric.txt
Yapıt, türreports:metrics
Şimdi Size Limit'in bir rapor oluşturmasını sağlayalım. Bunu yapmak için package.json
değiştirmemiz gerekiyor:
"scripts": { - "size": "size-limit", + "size": "size-limit --json > size-limit.json", "test": "jest && eslint ." },
--json
anahtarıyla size-limit
, verileri json biçiminde verir:
Ve yönlendirme > size-limit.json
, JSON'u size-limit.json
.
Şimdi bundan bir eser yaratmamız gerekiyor. Biçim, [metrics name][space][metrics value]
. Şimdi create generate-metric.js
:
const report = require('./size-limit.json'); process.stdout.write(`size ${(report[0].size/1024).toFixed(1)}Kb`); process.exit(0);
Ve onu package.json
ekleyin:
"scripts": { "size": "size-limit --json > size-limit.json", + "postsize": "node generate-metric.js > metric.txt", "test": "jest && eslint ." },
post
önekini kullandığımız için, npm run size
komutu önce size
betiğini çalıştıracak ve ardından otomatik olarak postsize
betiğini çalıştıracak, bu da bizim eserimiz olan metric.txt
dosyasının yaratılmasıyla sonuçlanacaktır.
Sonuç olarak, bu dalı master, bir şeyi değiştirmek ve yeni bir birleştirme isteği oluşturmak için birleştirdiğimizde aşağıdakileri göreceğiz:
Sayfada görünen pencere öğesinde, önce metriğin adını ( size
) ardından özellik dalındaki metriğin değerini ve yuvarlak parantez içinde kalıptaki değeri görüyoruz.
Artık paketin boyutunu nasıl değiştireceğimizi görebilir ve birleştirip birleştirmeme konusunda mantıklı bir karar verebiliriz.
- Tüm bu kodu bu depoda görebilirsiniz.
Öz geçmiş
TAMAM! Böylece, önemsiz vakayı nasıl ele alacağımızı bulduk. Birden fazla dosyanız varsa, metrikleri satır sonlarıyla ayırmanız yeterlidir. Boyut Sınırı için bir alternatif olarak, paket boyutunu düşünebilirsiniz. WebPack kullanıyorsanız, --profile
ve --json
bayraklarıyla oluşturarak ihtiyacınız olan tüm boyutları elde edebilirsiniz:
webpack --profile --json > stats.json
next.js kullanıyorsanız, @next/bundle-analyzer eklentisini kullanabilirsiniz. O size kalmış!
Deniz Feneri'ni kullanma
Lighthouse, proje analitiğinde fiili standarttır. Performansı, 11y'yi, en iyi uygulamaları ölçmemizi ve bize bir SEO puanı vermemizi sağlayan bir komut dosyası yazalım.
Her Şeyi Ölçmek İçin Komut Dosyası
Başlamak için ölçümleri yapacak olan deniz feneri paketini kurmamız gerekiyor. Ayrıca başsız tarayıcı olarak kullanacağımız kuklacıyı da kurmamız gerekiyor.
npm i -D lighthouse puppeteer
Şimdi bir lighthouse.js
betiği oluşturalım ve tarayıcımızı başlatalım:
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); })();
Şimdi belirli bir URL'yi analiz etmemize yardımcı olacak bir fonksiyon yazalım:
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); // … }
Harika! Artık tarayıcı nesnesini argüman olarak kabul edecek ve URL
argüman olarak kabul edecek ve bu URL
lighthouse
sonra bir rapor oluşturacak bir fonksiyon döndürecek bir fonksiyonumuz var.
Aşağıdaki argümanları lighthouse
:
- Analiz etmek istediğimiz adres;
-
lighthouse
seçenekleri, özellikle tarayıcıport
veoutput
(raporun çıktı formatı); -
report
yapılandırması velighthouse:full
(tüm ölçebildiğimiz). Daha kesin yapılandırma için belgelere bakın.
Müthiş! Artık raporumuzu aldık. Ama onunla ne yapabiliriz? Pekala, metrikleri sınırlara göre kontrol edebilir ve boru hattını durduracak sıfır olmayan kodla komut dosyasından çıkabiliriz:
if (report.categories.performance.score < 0.8) process.exit(1);
Ama biz sadece performansı görünür ve engelleyici olmayan hale getirmek mi istiyoruz? O zaman başka bir yapı türünü benimseyelim: GitLab performans eseri.
GitLab Performans Artefaktı
Bu yapı biçimini anlamak için sitespeed.io eklentisinin kodunu okumamız gerekiyor. (GitLab, eserlerinin biçimini neden kendi belgelerinde tanımlayamıyor? Gizem. )
[ { "subject":"/", "metrics":[ { "name":"Transfer Size (KB)", "value":"19.5", "desiredSize":"smaller" }, { "name":"Total Score", "value":92, "desiredSize":"larger" }, {…} ] }, {…} ]
Yapı, nesnelerin bir dizisini içeren bir JSON
dosyasıdır. Her biri bir URL
hakkında bir raporu temsil eder.
[{page 1}, {page 2}, …]
Her sayfa, aşağıdaki niteliklere sahip bir nesne ile temsil edilir:
-
subject
Sayfa tanımlayıcısı (böyle bir yol adı kullanmak oldukça kullanışlıdır); -
metrics
Nesnelerin bir dizisi (her biri sayfada yapılan bir ölçümü temsil eder).
{ "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }
measurement
, aşağıdaki özellikleri içeren bir nesnedir:
-
name
Ölçüm adı, örneğinTime to first byte
Time to interactive
. -
value
Sayısal ölçüm sonucu. -
desiredSize
Hedef değerin mümkün olduğu kadar küçük olması gerekiyorsa, örneğinTime to interactive
metriği için, değersmaller
olmalıdır. Mümkün olduğu kadar büyük olması gerekiyorsa, örneğin deniz feneriPerformance score
için, dahalarger
kullanın.
{ "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }
Şimdi buildReport
işlevimizi, standart deniz feneri metrikleriyle bir sayfa için bir rapor döndürecek şekilde değiştirelim.
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, }; }
Şimdi, bir rapor oluşturan bir fonksiyonumuz olduğunda. Bunu projenin sayfalarının her türüne uygulayalım. Öncelikle, process.env.DOMAIN
bir hazırlama etki alanı içermesi gerektiğini belirtmem gerekiyor (önceden projenizi bir özellik dalından dağıtmanız gerekir).
+ 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(); })();
- Tam kaynağı bu özde ve çalışma örneğinde bu depoda bulabilirsiniz.
Not : Bu noktada sözümü kesmek ve boş yere “Neden zamanımı alıyorsun - Promise.all'ı bile doğru düzgün kullanamıyorsun!” diye bağırmak isteyebilirsiniz. Savunmamda, aynı anda birden fazla deniz feneri örneğini çalıştırmanın tavsiye edilmediğini söylemeye cesaret ediyorum çünkü bu, ölçüm sonuçlarının doğruluğunu olumsuz etkiliyor. Ayrıca, gereken ustalığı göstermezseniz, bu bir istisnaya yol açacaktır.
Çoklu İşlemlerin Kullanımı
Hala paralel ölçümler yapıyor musunuz? İyi, düğüm kümesini (hatta kalın oynamayı seviyorsanız Worker Threads) kullanmak isteyebilirsiniz, ancak bunu yalnızca boru hattınızın birden fazla kullanılabilir kor ile ortamda çalıştığı durumda tartışmak mantıklıdır. Ve o zaman bile, Node.js doğası gereği, her bir işlem çatalında tam ağırlıklı Node.js örneğine sahip olacağınızı unutmayın (aynısını yeniden kullanmak ve bu da artan RAM tüketimine yol açacaktır). Tüm bunlar, artan donanım gereksinimi nedeniyle daha maliyetli ve biraz daha hızlı olacağı anlamına geliyor. Oyun muma değmez gibi görünebilir.
Bu riski almak istiyorsanız, yapmanız gerekenler:
- URL dizisini çekirdek sayısına göre parçalara ayırın;
- Çekirdek sayısına göre bir işlemin çatalını oluşturun;
- Dizinin parçalarını çatallara aktarın ve ardından oluşturulan raporları alın.
Bir diziyi bölmek için çok kümeli yaklaşımları kullanabilirsiniz. Sadece birkaç dakika içinde yazılan aşağıdaki kod diğerlerinden daha kötü olmazdı:
/** * 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; }
Çekirdek sayısına göre çatal yapın:
// 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 } })();
Bir dizi parçayı alt süreçlere aktaralım ve raporları geri alalım:
(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(); + }); } })();
Ve son olarak, raporları tek bir dizide yeniden birleştirin ve bir yapı oluşturun.
- Deniz fenerinin birden çok işlemle nasıl kullanılacağını gösteren bir örnekle tam koda ve depoya göz atın.
Ölçümlerin Doğruluğu
lighthouse
zaten talihsiz olan büyük ölçüm hatasını artıran ölçümleri paralel hale getirdik. Ama nasıl azaltırız? Pekala, birkaç ölçüm yapın ve ortalamayı hesaplayın.
Bunu yapmak için, mevcut ölçüm sonuçları ile öncekiler arasındaki ortalamayı hesaplayacak bir fonksiyon yazacağız.
// 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, } }), } }
Ardından, bunları kullanmak için kodumuzu değiştirin:
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(); }); }
- Bir örnekle tam kod ve depo ile özü inceleyin.
Ve şimdi boru hattına lighthouse
ekleyebiliriz.
Boru Hattına Ekleme
İlk olarak, .gitlab-ci.yml
adlı bir yapılandırma dosyası oluşturun.
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
için birden çok kurulu paket gereklidir. Alternatif olarak docker
kullanmayı düşünebilirsiniz. Bunun dışında, artefakt türünü performans olarak belirlememiz mantıklı. Ve hem ana hem de özellik dalına sahip olur olmaz, birleştirme isteğinde bunun gibi bir pencere öğesi göreceksiniz:
Güzel?
Öz geçmiş
Sonunda daha karmaşık bir davayla işimiz bitti. Açıkçası, deniz feneri dışında birçok benzer araç var. Örneğin, sitespeed.io. GitLab belgeleri, sitespeed
ardışık düzeninde site hızının nasıl kullanılacağını açıklayan bir makale bile içerir. GitLab için bir yapı oluşturmamıza izin veren bir eklenti de var. Ancak, topluluk odaklı açık kaynaklı ürünleri, kurumsal bir canavarın sahip olduğu ürünlere kim tercih eder?
Kötüler İçin Dinlenme Yok mu
Sonunda oradaymışız gibi görünebilir, ama hayır, henüz değil. Ücretli bir GitLab sürümü kullanıyorsanız, premium
ve silver
başlayan planlarda, her kullanıcı için aylık 19 ABD doları tutarında rapor türleri metrics
ve performance
olan yapılar bulunur. Ayrıca, yalnızca ihtiyacınız olan belirli bir özelliği satın alamazsınız; yalnızca planı değiştirebilirsiniz. Üzgünüm. Bu yüzden ne yapabiliriz? Kontrol API'si ve Durum API'si ile GitHub'dan farklı olarak GitLab, birleştirme isteğinde gerçek bir pencere öğesi oluşturmanıza izin vermez. Ve onları yakın zamanda alma umudu da yok.
Bu özellikleri gerçekten destekleyip desteklemediğinizi kontrol etmenin bir yolu: İşlem hattında GITLAB_FEATURES
ortam değişkenini arayabilirsiniz. Listede merge_request_performance_metrics
ve metrics_reports
yoksa, bu özellikler desteklenmez.
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
Destek yoksa, bir şeyler bulmalıyız. Örneğin, birleştirme isteğine bir yorum ekleyebiliriz, ihtiyacımız olan tüm verileri içeren tabloyla birlikte yorum yapabiliriz. Kodumuza dokunmadan bırakabiliriz - eserler oluşturulacak, ancak widget'lar her zaman «metrics are unchanged»
mesajını gösterecek.
Çok garip ve bariz olmayan davranış; Neler olduğunu anlamak için dikkatlice düşünmem gerekiyordu.
O zaman plan nedir?
-
master
daldan artefakt okumamız gerekiyor; -
markdown
formatında bir yorum oluşturun; - Geçerli özellik dalından ana öğeye birleştirme isteğinin tanımlayıcısını alın;
- Yorumu ekleyin.
Ana Daldan Eser Nasıl Okunur
master
ve özellik dalları arasında performans metriklerinin nasıl değiştirildiğini göstermek istiyorsak, master
yapıdan artefakt okumamız gerekir. Ve bunu yapmak için fetch
kullanmamız gerekecek.
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; };
Yorum Metni Oluşturma
markdown
formatında yorum metni oluşturmamız gerekiyor. Bize yardımcı olacak bazı servis fonksiyonları oluşturalım:
/** * 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} `; };
Yorum Oluşturacak Komut Dosyası
GitLab API ile çalışmak için bir jetona ihtiyacınız olacak. Bir tane oluşturmak için GitLab'ı açmanız, oturum açmanız, menünün 'Ayarlar' seçeneğini açmanız ve ardından navigasyon menüsünün sol tarafında bulunan 'Erişim Jetonları'nı açmanız gerekir. Ardından, belirteci oluşturmanıza izin veren formu görebilmeniz gerekir.
Ayrıca, projenin bir kimliğine ihtiyacınız olacak. Bunu 'Ayarlar' deposunda bulabilirsiniz ('Genel' alt menüsünde):
Birleştirme isteğine yorum eklemek için kimliğini bilmemiz gerekir. Birleştirme isteği kimliği almanızı sağlayan işlev şöyle görünür:
// 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:
Öz geçmiş
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.
kimlik doğrulama
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. Tamamlandı.
Özet
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!