JavaScript performansıyla ilgili sırları çözmek için adli tıp ve dedektif çalışmalarından yararlanın

John McCutchan
John McCutchan

Giriş

Son yıllarda web uygulamaları önemli ölçüde hızlandı. Birçok uygulama artık o kadar hızlı çalışıyor ki bazı geliştiricilerin "web yeterince hızlı mı?" diye sorduğunu duydum. Bazı uygulamalar için bu olabilir, ancak yüksek performanslı uygulamalar üzerinde çalışan geliştiriciler için bunun yeterince hızlı olmadığını biliyoruz. JavaScript sanal makine teknolojisindeki olağanüstü gelişmelere rağmen, yakın zamanda yapılan bir çalışma Google uygulamalarının, zamanlarının% 50 ila% 70'ini V8'de geçirdiğini göstermiştir. Uygulamanızın sınırlı bir süresi vardır. Bir sistemi kısaltmak, başka bir sistemin daha fazlasını yapabileceği anlamına gelir. 60 fps'de çalışan uygulamaların kare başına yalnızca 16 ms olduğunu veya aksi takdirde jank'ın kullanılabileceğini unutmayın. Oz'a Giden Yolu Bulun bölümündeki V8 ekibindeki performans dedektiflerinin belirsiz bir performans sorununu arayışını konu alan bir araştırma öyküsünden, JavaScript ve profil JavaScript uygulamalarını optimize etme hakkında bilgi edinmek için okumaya devam edin.

Google I/O 2013 Oturumu

Bu materyali Google I/O 2013'te sundum. Aşağıdaki videoyu izleyin:

Performans neden önemlidir?

CPU döngüleri sıfır toplamlı bir oyundur. Sisteminizin bir kısmını daha az kullanmanız, başka bir bölümde daha fazlasını kullanmanıza veya genel olarak daha sorunsuz çalışmanıza olanak tanır. Daha hızlı koşmak ve daha fazlasını yapmak çoğu zaman birbiriyle rekabet eden hedeflerdir. Kullanıcılar yeni özellikler talep ederken uygulamanızın daha sorunsuz çalışmasını da beklerler. JavaScript sanal makineleri gittikçe daha hızlı hale geliyor, ancak web uygulamalarında performans sorunlarıyla uğraşan birçok geliştiricinin olduğu gibi, bu, bugün giderebileceğiniz performans sorunlarını göz ardı etmenizin bir nedeni değil. Gerçek zamanlı ve yüksek kare hızında, uygulamaların olumsuz etkilenmemesi için baskı çok önemlidir. Insomniac Games, oyunun başarısında sağlam ve kesintisiz kare hızının önemli olduğunu gösteren bir çalışma yaptı: "Yüksek bir kare hızı yine de profesyonel ve iyi yapılmış bir ürünün işaretidir." Web geliştiricileri bunu dikkate alır.

Performans Sorunlarını Çözme

Performans sorunlarını çözmek, suç çözmek gibidir. Kanıtları dikkatlice incelemeniz, şüpheli nedenleri kontrol etmeniz ve farklı çözümlerle denemeler yapmanız gerekir. Sorunu gerçekten çözdüğünüzden emin olmak için tüm ölçümlerinizi belgelemeniz gerekir. Bu yöntem ile suçlu dedektiflerin vakaları çözme yöntemi arasında çok az fark vardır. Dedektifler kanıtları inceler, şüphelileri sorgular ve sigara içen silahı bulma umuduyla deneyler yapar.

V8 CSI: Oz

Oz'a Giden Yolu Bulun'u geliştiren muhteşem sihirbazlar, V8 ekibine kendi başlarına çözemedikleri bir performans sorunuyla ulaştı. Oz, bazen donup kalır ve duraksamaya neden olur. Oz geliştiricileri, Chrome Geliştirici Araçları'ndaki Zaman Çizelgesi Panelini kullanarak ilk araştırmaları yapmıştı. Bellek kullanımına baktıklarında korkunç teste dişi grafiğiyle karşılaştılar. Saniyede bir kez çöp toplayıcı 10 MB'lık atık topluyor ve çöp toplama duraklamaları jank'a karşılık geliyordu. Chrome Geliştirici Araçları'ndaki Zaman Çizelgesi'nde yer alan aşağıdaki ekran görüntüsüne benzer:

Geliştirici Araçları zaman çizelgesi

V8 dedektifleri Jakob ve Yang davayı ele aldı. V8 ekibinden Jakob ile Yang ve Oz ekibi arasında uzun süren bir sohbet oldu. Bu konuşmayı, bu sorunun tespit edilmesine yardımcı olan önemli olaylara ayrıntılarıyla indirdim.

Kanıt

İlk adım, başlangıçtaki kanıtları toplayıp incelemektir.

Ne tür bir uygulamayı değerlendiriyoruz?

Oz demosu, etkileşimli bir 3D uygulamadır. Bu nedenle, atık toplama işlemlerinin neden olduğu duraklamalara karşı çok hassastır. 60 fps'de çalışan etkileşimli bir uygulamanın tüm JavaScript çalışmasının 16 ms olduğunu ve Chrome'un grafik çağrılarını işleyip ekranı çizmesi için bu sürenin bir kısmını bırakması gerektiğini unutmayın.

Oz, çift değerler üzerinde oldukça fazla aritmetik hesaplama yapıp WebAudio ve WebGL'ye sık sık çağrı yapar.

Ne tür bir performans sorunuyla karşılaşıyoruz?

Duraklamalar, yani kare düşüşleri, yani duraklamalar görüyoruz. Bu duraklamalar, atık toplama çalıştırmalarıyla ilişkilidir.

Geliştiriciler en iyi uygulamaları izliyor mu?

Evet. Oz geliştiricileri JavaScript sanal makine performansı ve optimizasyon teknikleri konusunda oldukça tecrübelidir. Oz geliştiricilerinin kaynak dil olarak CoffeeScript'i kullandıklarını ve CoffeeScript derleyicisi aracılığıyla JavaScript kodu ürettiğini belirtmekte fayda var. Bu durum, Oz geliştiricileri tarafından yazılan kod ile V8 tarafından kullanılan kod arasındaki kopukluk nedeniyle incelemeyi biraz zorlaştırdı. Chrome Geliştirici Araçları artık bu işlemi kolaylaştıracak kaynak eşlemelerini destekliyor.

Çöp toplayıcı neden çalışıyor?

JavaScript'teki bellek, geliştirici için sanal makine tarafından otomatik olarak yönetilir. V8, belleğin iki (veya daha fazla) generations ayrıldığı yaygın bir çöp toplama sistemi kullanır. Genç nesil, yakın zamanda tahsis edilen nesneleri barındırır. Bir nesne yeterince uzun süre hayatta kalırsa eski nesle taşınır.

Genç nesil, eski kuşakla karşılaştırıldığında çok daha sık toplanır. Genç kuşak koleksiyon çok daha ucuz olduğundan bu tasarım gereğidir. Genelde GC'nin sık sık duraksamalarının nedeninin genç nesil veri toplama işlemi olduğunu düşünmek genellikle güvenlidir.

V8'de genç bellek alanı, eşit büyüklükte iki bitişik bellek bloğuna bölünür. Herhangi bir zamanda bu iki bellek bloğundan yalnızca biri kullanılır ve buna alan adı verilir. Alanda bellek kalırken yeni bir nesne ayırmak ucuzdur. Boşluğun üzerindeki imleç, yeni nesne için gereken bayt sayısı kadar ileri hareket ettiriliyor. Bu işlem, alan tükenene kadar devam eder. Bu noktada program durdurulur ve veri toplama işlemi başlar.

V8 genç bellek

Bu noktada boşluktan alana geçiş yapılır. Önceden uzaydan, daha önce uzaydan olmak üzere olan alan baştan sona taranır ve hâlâ aktif olan nesneler alana kopyalanır veya eski nesil yığına yükseltilir. Ayrıntılar için Cheney's Algorithm (Cheney Algoritması) bölümünü okumanızı öneririm.

Sezgisel olarak, bir nesne örtülü veya açık olarak ayrıldığında (new, [] veya {} çağrılarıyla), uygulamanızın bir çöp toplama işlemine ve ürkütücü uygulama duraklamaya giderek daha da yaklaşmakta olduğunu anlamalısınız.

Bu uygulama için 10 MB/sn'de atık gerekir mi?

Kısacası hayır. Geliştirici 10 MB/sn'lik çöp bekleyecek bir şey yapmıyor.

Şüpheliler

Araştırmanın bir sonraki aşamasında, olası şüphelileri belirleyip azaltmaya çalışırız.

1. şüpheli

Çerçeve sırasında yeni çağrı. Ayrılmış her bir nesnenin sizi bir GC duraklamasına daha da yaklaştırdığını unutmayın. Özellikle yüksek kare hızlarında çalışan uygulamalar, kare başına sıfır ayırma için çaba göstermelidir. Bunun için genellikle dikkatlice düşünülmüş, uygulamaya özel bir nesne geri dönüşüm sistemi gerekir. V8 dedektifleri, Oz ekibiyle görüştü ve yeni dediler. Hatta Oz ekibi bu gerekliliğin zaten farkındaydı ve "Bu çok mahcup olurdu" diyordu. Bunu listeden kazıyın.

2. şüpheli

Oluşturucunun dışında bir nesnenin "şeklini" değiştirme. Bu durum, oluşturucunun dışındaki bir nesneye yeni bir özellik eklendiğinde ortaya çıkar. Bu işlem, nesne için yeni bir gizli sınıf oluşturur. Optimize edilmiş kod bu yeni gizli sınıfı gördüğünde bir devre dışı bırakma tetiklenir, optimize edilmemiş kod, kod çalışır durumda olarak sınıflandırılıp tekrar optimize edilene kadar yürütülür. Optimizasyonun azalması ve yeniden optimizasyondan kaynaklanan bu kayıplar, olumsuzlukla sonuçlanır, ancak aşırı çöp oluşturma ile kesin bir şekilde ilişkili değildir. Kod dikkatli bir şekilde incelendikten sonra, nesne şekillerinin statik olduğu, dolayısıyla 2 numaralı şüphelinin göz ardı edildiği teyit edilmiştir.

3. şüpheli

Optimize edilmemiş kodda aritmetik. Optimize edilmemiş kodda tüm hesaplamalar, gerçek nesnelerin ayrılmasına neden olur. Örneğin, bu snippet:

var a = p * d;
var b = c + 3;
var c = 3.3 * dt;
point.x = a * b * c;

5 HeapNumber nesnesi oluşturulmasıyla sonuçlanır. İlk üçü a, b ve c değişkenleri içindir. Dördüncüsü anonim değer (a * b), 5'i ise 4 numara * c içindir; 5. öğe nihai olarak Point.x'e atanır.

Oz, bu işlemlerin her birini kare başına gerçekleştiriyor. Hiçbir zaman optimize edilmeyen işlevlerde bu hesaplamalardan herhangi biri gerçekleşiyorsa çöpün nedeni onlar olabilir. Çünkü optimize edilmemiş hesaplamalar geçici sonuçlar için bile bellek tahsis eder.

4. şüpheli

Bir mülke çift duyarlıklı sayı depolama. Numarayı ve bu yeni nesneyi gösterecek şekilde değiştirilen özelliği depolamak için bir HeapNumber nesnesi oluşturulmalıdır. Özelliği, HeapNumber değerini gösterecek şekilde değiştirmek çöp oluşturmayacaktır. Bununla birlikte, nesne özelliği olarak depolanan çok sayıda çift duyarlıklı sayı bulunması mümkündür. Bu kod, aşağıdaki gibi ifadelerle doludur:

sprite.position.x += 0.5 * (dt);

Optimize edilmiş kodda, x'e her zaman yeni hesaplanan bir değer atandığında, zararsız bir ifade gibi görünen yeni bir HeapNumber nesnesi örtülü olarak ayrılır ve bu da bizi çöp toplama işlemini durdurmaya yaklaştırır.

Çift duyarlıklı sayının depolama alanı yalnızca bir kez tahsis edildiğinden ve değerin sürekli olarak değiştirilmesi yeni depolama alanı ayrılmasına gerek olmadığından, türetilmiş bir dizi (veya yalnızca çift duyarlıklı sayı içeren normal bir dizi) kullanarak bu özel sorundan tamamen kaçınabileceğinizi unutmayın.

4. şüpheli bir olasılıktır.

Adli tıp

Bu noktada, dedektiflerin iki olası şüphelisi vardır: yığın sayılarını nesne özelliği olarak depolama ve optimize edilmemiş işlevler içinde gerçekleşen aritmetik hesaplama. Laboratuvara gidip hangi şüphelinin suçlu olduğunu kesin olarak belirleme zamanı gelmişti. NOT: Bu bölümde, gerçek Oz kaynak kodunda bulunan sorunun bir tekrarını kullanacağım. Bu çoğaltma, orijinal koddan çok daha küçük boyuttadır ve dolayısıyla, bunu kararlaştırmak daha kolaydır.

1. Deneme

3. şüpheli (optimize edilmemiş işlevler içinde aritmetik hesaplama) kontrol ediliyor. V8 JavaScript motoru, arka planda neler olduğuna ilişkin harika bilgiler sağlayabilecek yerleşik bir günlük kaydı sistemine sahiptir.

Chrome'un hiç çalışmamasıyla başlamak ve Chrome'u şu bayraklarla başlatmak:

--no-sandbox --js-flags="--prof --noprof-lazy --log-timer-events"

ve Chrome'dan tamamen çıkarsanız geçerli dizinde bir v8.log dosyası oluşturulur.

v8.log'un içeriğini yorumlamak için Chrome'unuzun kullandığı v8 sürümünü indirmeniz (hakkında:version'ı kontrol edin) ve oluşturmanız gerekir.

v8'i başarıyla oluşturduktan sonra onay işareti işlemcisini kullanarak günlüğü işleyebilirsiniz:

$ tools/linux-tick-processor /path/to/v8.log

(Platformunuza bağlı olarak, Linux yerine Mac veya windows'u kullanın.) (Bu araç, v8'deki üst düzey kaynak dizinden çalıştırılmalıdır.)

Onay işareti işlemcisi, en çok onay işaretine sahip JavaScript işlevleri için metin tabanlı bir tablo görüntüler:

[JavaScript]:
ticks  total  nonlib   name
167   61.2%   61.2%  LazyCompile: *opt demo.js:12
 40   14.7%   14.7%  LazyCompile: unopt demo.js:20
 15    5.5%    5.5%  Stub: KeyedLoadElementStub
 13    4.8%    4.8%  Stub: BinaryOpStub_MUL_Alloc_Number+Smi
  6    2.2%    2.2%  Stub: BinaryOpStub_ADD_OverwriteRight_Number+Number
  4    1.5%    1.5%  Stub: KeyedStoreElementStub
  4    1.5%    1.5%  KeyedLoadIC:  {12}
  2    0.7%    0.7%  KeyedStoreIC:  {13}
  1    0.4%    0.4%  LazyCompile: ~main demo.js:30

demo.js'nin üç işlevi olduğunu görebilirsiniz: opt, unopt ve main. Optimize edilmiş işlevlerin adlarının yanında bir yıldız işareti (*) bulunur. İşlev optimizasyonunun optimize edildiğini ve devre dışı bırakma işleminin optimize edilmediğini kontrol edin.

V8 dedektifinin araç çantasındaki bir diğer önemli araç da plan-zamanlayıcı-etkinliktir. Şu şekilde yürütülebilir:

$ tools/plot-timer-event /path/to/v8.log

Çalıştırıldıktan sonra, zamanlayıcı-events.png adlı bir png dosyası geçerli dizinde olur. Bu dosyayı açtığınızda şuna benzer bir şey görürsünüz:

Zamanlayıcı etkinlikleri

Alt kısımdaki grafiğin yanı sıra veriler satırlarda görüntülenir. X ekseni süreyi ifade eder (ms). Sol tarafta her satır için etiketler bulunur:

Zamanlayıcı etkinlikleri Y ekseni

V8.Execute (Yürütme) satırı, V8'in JavaScript kodunu yürüttüğü her profil onay şeridinde siyah dikey bir çizgiyle çizilmiştir. V8.GCScavenger'ın üzerinde, V8'in yeni nesil koleksiyon gerçekleştirdiği her profil onay kutusunda mavi bir dikey çizgi bulunuyor. V8 eyaletlerinin geri kalanı için de aynı durum söz konusudur.

En önemli satırlardan biri "yürütülmekte olan kod türüdür". Optimize edilmiş kod yürütüldüğünde bu simge yeşil, optimize edilmemiş kod yürütüldüğünde ise kırmızı ve mavinin karışımı bir renkte olur. Aşağıdaki ekran görüntüsünde, optimize edilmiş koddan optimize edilmemiş koda ve ardından optimize edilmiş koda geri dönme gösterilmektedir:

Yürütülen kod türü

İdeal olarak, hemen hiçbir zaman bu çizgi düz yeşil olacaktır. Diğer bir deyişle, programınız optimize edilmiş sabit duruma geçmiştir. Optimize edilmemiş kod, optimize edilmiş koddan her zaman daha yavaş çalışır.

Bu uzunluktaki bir sayfaya giderseniz, uygulamanızı v8 hata ayıklama kabuğunda (d8) çalıştırılabilecek şekilde yeniden düzenleyerek çok daha hızlı çalışabileceğinizi unutmayın. d8'i kullanmak, değer değer işlemcisi ve grafik zamanlayıcı-etkinlik araçlarıyla daha hızlı yineleme süreleri sağlar. d8 kullanmanın bir başka yan etkisi de gerçek problemin izole edilmesinin kolay hale gelmesi ve verilerdeki gürültü miktarının azaltılmasıdır.

Oz kaynak kodundaki zamanlayıcı etkinlikleri grafiğine bakıldığında, optimize edilmiş koddan optimize edilmemiş koda geçiş yapıldığı ve optimize edilmemiş kod yürütülürken, aşağıdaki ekran görüntüsüne benzer şekilde birçok yeni nesil koleksiyonun tetiklendiği görülmüştür (not ortada kaldırılmıştır):

Kronometre etkinlikleri grafiği

Yakından bakarsanız, V8'in JavaScript kodunu çalıştırdığını gösteren siyah çizgilerin, yeni nesil koleksiyonlarla (mavi çizgiler) tam olarak aynı profilin onay zamanlarında eksik olduğunu görebilirsiniz. Bu, çöplerin toplanması sırasında komut dosyasının duraklatıldığını açıkça gösterir.

Oz kaynak kodundan alınan onay işareti işlemci çıkışına bakıldığında üst işlev (updateSprites) optimize edilmemişti. Başka bir deyişle, programın en çok zaman geçirdiği işlev de optimize edilmemişti. Bu, kesinlikle sorunun 3. şüpheli olduğunu gösterir. updateSprites kaynağı, şuna benzeyen döngüler içeriyordu:

function updateSprites(dt) {
    for (var sprite in sprites) {
        sprite.position.x += 0.5 * dt;
        // 20 more lines of arithmetic computation.
    }
}

V8'i olduğu kadar iyi tanıdığından, for-i-in döngü yapısının bazen V8 tarafından optimize edilmediğini hemen fark ettiler. Diğer bir deyişle, bir işlev için-i-in döngü yapısı içeriyorsa optimize edilmeyebilir. Bu, bugün için özel bir durum olup gelecekte değişebilir. Yani V8, bir gün bu döngü yapısını optimize edebilir. V8 dedektifleri olmadığımız ve V8'i epey bilmediğimize göre updateSprites'ın neden optimize edilmediğini nasıl belirleyebiliriz?

2. Deneme

Chrome şu bayrakla çalıştırılıyor:

--js-flags="--trace-deopt --trace-opt-verbose"

optimizasyon ve optimizasyon iptali verilerinin ayrıntılı bir günlüğünü görüntüler. UpdateSprites verilerine arama yaptığımızda şunları bulduk:

[updateSprites için optimizasyon devre dışı bırakıldı, neden: ForInStatement hızlı değil]

Dedektiflerin varsayımı gibi, neden bir yandan içine almak isteyen döngünün oluşturulmasıydı.

Destek Kaydı Kapatıldı

updateSprites'ın optimize edilmeme nedenini öğrendikten sonra çözüm basitti. Hesaplamayı kendi işlevine taşımanız yeterli oldu. Örneğin:

function updateSprite(sprite, dt) {
    sprite.position.x += 0.5 * dt;
    // 20 more lines of arithmetic computation.
}

function updateSprites(dt) {
    for (var sprite in sprites) {
        updateSprite(sprite, dt);
    }
}

updateSprite optimize edildiğinden HeapNumber nesnesi çok daha az olur ve bu da GC duraklatmalarının daha az olmasına neden olur. Aynı denemeleri yeni kodla gerçekleştirerek bunu kolayca doğrulayabilirsiniz. Dikkatli okuyucu, çift sayıların özellik olarak saklanmaya devam ettiğini fark edecektir. Profil oluşturma buna değdiğini gösteriyorsa, konumu çiftler dizisi veya yazılan bir veri dizisi olacak şekilde değiştirmek, oluşturulan nesnelerin sayısını daha da azaltır.

Son söz

Oz geliştiricileri bununla yetinmedi. V8 dedektifleri tarafından kendileriyle paylaşılan araç ve tekniklere sahip olan ekip, optimizasyon cehenneminde sıkışıp kalan ve hesaplama kodunu optimize edilmiş yaprak işlevlerine dahil ederek daha da iyi performans sağlayan birkaç işlev buldu.

Harekete geçin ve performans suçlarını çözmeye başlayın.