GitLab CI ve GitLab Artifacts Hoodoo ile Performans Nasıl Görünür Yapılır

Yayınlanan: 2022-03-10
Hızlı özet ↬ Bir uygulamayı optimize etmek yeterli değildir. Performansın düşmesini önlemeniz gerekir ve bunu yapmanın ilk adımı performans değişikliklerini görünür kılmaktır. Bu makalede Anton Nemtsev, GitLab birleştirme isteklerinde bunları göstermenin birkaç yolunu gösteriyor.

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

  1. Rakibinizin projeleri için her bir sayfa türünde seçtiğiniz metriklerin değerlerini ölçün;
  2. Projenizde aynı metrikleri ölçün;
  3. 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.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

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 
Komut yürütmenin sonucu, index.js'nin boyutunu gösterir
Komut yürütmenin sonucu, index.js'nin boyutunu gösterir. (Büyük önizleme)

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.

Terminalin bir ekran görüntüsü; dosyanın boyutu sınırdan küçük ve yeşil olarak gösteriliyor
Terminalin bir ekran görüntüsü; dosyanın boyutu sınırdan küçük ve yeşil olarak gösteriliyor. (Büyük önizleme)

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.

Dosya boyutunun sınırı aştığı ve kırmızı ile gösterildiği terminalin ekran görüntüsü. Komut dosyası sıfır olmayan bir kodla tamamlandı.
Dosya boyutunun sınırı aştığı ve kırmızı ile gösterildiği terminalin ekran görüntüsü. Komut dosyası sıfır olmayan bir kodla tamamlandı. (Büyük önizleme)

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.

Dosyanın boyutu sınırı aştığı için taahhüdün iptal edildiği terminalin ekran görüntüsü
Dosyanın boyutu sınırı aştığı için taahhüdün iptal edildiği terminalin ekran görüntüsü. (Büyük önizleme)

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?

  1. İşlem hattındaki yapıtları tanımlayın.
  2. 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
  1. expire_in: 7 days — artefakt 7 gün boyunca var olacaktır.
  2.  paths: metric.txt

    Kök kataloğuna kaydedilecektir. Bu seçeneği atlarsanız, indirmeniz mümkün olmaz.
  3.  reports: metrics: metric.txt

    Yapıt, tür reports: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:

size-limit --json komutu konsola JSON çıktısı verir. JSON, dosya adı ve boyutu içeren bir dizi nesne içerir ve boyut sınırını aşıp aşmadığını bize bildirir.
size-limit --json komutu konsola JSON çıktısı verir. JSON, bir dosya adı ve boyutu içeren bir dizi nesne içerir ve boyut sınırını aşıp aşmadığını bize bildirir. (Büyük önizleme)

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:

Bize yuvarlak parantez içinde yeni ve eski metrik değere sahip bir pencere öğesi gösteren birleştirme isteği içeren ekran görüntüsü
Bize yuvarlak parantez içinde yeni ve eski metrik değere sahip bir pencere öğesi gösteren birleştirme isteği içeren ekran görüntüsü. (Büyük önizleme)

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 :

  1. Analiz etmek istediğimiz adres;
  2. lighthouse seçenekleri, özellikle tarayıcı port ve output (raporun çıktı formatı);
  3. report yapılandırması ve lighthouse: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:

  1. subject
    Sayfa tanımlayıcısı (böyle bir yol adı kullanmak oldukça kullanışlıdır);
  2. 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:

  1. name
    Ölçüm adı, örneğin Time to first byte Time to interactive .
  2. value
    Sayısal ölçüm sonucu.
  3. desiredSize
    Hedef değerin mümkün olduğu kadar küçük olması gerekiyorsa, örneğin Time to interactive metriği için, değer smaller olmalıdır. Mümkün olduğu kadar büyük olması gerekiyorsa, örneğin deniz feneri Performance score için, daha larger 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.

Deniz feneri raporu ile ekran görüntüsü. Performans puanı, a11y puanı, en iyi uygulamalar puanı, SEO puanı vardır.
Deniz feneri raporu ile ekran görüntüsü. Performans puanı, a11y puanı, en iyi uygulamalar puanı, SEO puanı vardır. (Büyük önizleme)
 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:

  1. URL dizisini çekirdek sayısına göre parçalara ayırın;
  2. Çekirdek sayısına göre bir işlemin çatalını oluşturun;
  3. 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:

Birleştirme isteği sayfasının ekran görüntüsü. Hangi deniz feneri metriklerinin değiştiğini ve tam olarak nasıl değiştiğini gösteren bir widget var.
Birleştirme isteği sayfasının ekran görüntüsü. Hangi deniz feneri metriklerinin tam olarak nasıl değiştiğini gösteren bir widget var. (Büyük önizleme)

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.

Ilya Klimov (GitLab çalışanı) tarafından gönderilen tweet'in ekran görüntüsü, Github Kontrolleri ve Durum API'si için görünüm analoglarının olasılığı hakkında şunları yazdı: “Son derece olası değil. Kontroller, taahhüt durumu API'si aracılığıyla zaten mevcut ve durumlara gelince, kapalı bir ekosistem olmaya çalışıyoruz."
Github Kontrolleri ve Durum API'si için görünüm analoglarının olasılığı hakkında yazan Ilya Klimov (GitLab çalışanı) tarafından yayınlanan tweet'in ekran görüntüsü. (Büyük önizleme)

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?

  1. master daldan artefakt okumamız gerekiyor;
  2. markdown formatında bir yorum oluşturun;
  3. Geçerli özellik dalından ana öğeye birleştirme isteğinin tanımlayıcısını alın;
  4. 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.

Yukarıda bahsettiğim belirteç oluşturma formunu ve menü seçeneklerini gösteren ekran görüntüsü.
Yukarıda bahsettiğim belirteç oluşturma formunu ve menü seçeneklerini gösteren ekran görüntüsü. (Büyük önizleme)

Ayrıca, projenin bir kimliğine ihtiyacınız olacak. Bunu 'Ayarlar' deposunda bulabilirsiniz ('Genel' alt menüsünde):

Ekran görüntüsü, Proje Kimliğini bulabileceğiniz ayarlar sayfasını gösterir
Ekran görüntüsü, Proje Kimliğini bulabileceğiniz ayarlar sayfasını gösterir. (Büyük önizleme)

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:

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. (Büyük önizleme)

Ö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!