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ırıldı. 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 yeterli olabilir ancak yüksek performanslı uygulamalar üzerinde çalışan geliştiriciler için yeterince hızlı olmadığını biliyoruz. JavaScript sanal makine teknolojisindeki muazzam ilerlemelere 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österdi. Uygulamanızın sınırlı bir süresi vardır. Bir sistemden döngüleri azaltmak, 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 süresi olduğunu unutmayın. Aksi takdirde takılma yaşanır. V8 ekibindeki performans dedektiflerinin Find Your Way to Oz'daki belirsiz bir performans sorununu takip ettiği bir hikayeden JavaScript'i optimize etme ve JavaScript uygulamalarının profilini çıkarma 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 videoya göz atın:

Performans neden önemlidir?

CPU döngüleri sıfır toplamlı bir oyundur. Sisteminizin bir bölümünün daha az kaynak kullanmasını sağlarsanız başka bir bölümünde daha fazla kaynak kullanabilir veya genel olarak daha sorunsuz çalışabilirsiniz. Daha hızlı çalışmak ve daha fazla işlem yapmak genellikle birbiriyle çelişen hedeflerdir. Kullanıcılar yeni özellikler talep ederken uygulamanızın daha sorunsuz çalışmasını da bekler. JavaScript sanal makineleri giderek daha hızlı hale geliyor ancak bu, bugün düzeltebileceğiniz performans sorunlarını göz ardı etmeniz için bir neden değil. Web uygulamalarındaki performans sorunlarıyla uğraşan birçok geliştirici de bunu biliyor. Gerçek zamanlı, yüksek kare hızlı uygulamalarda takılma olmaması çok önemlidir. Insomniac Games, bir oyunun başarısı için sağlam ve tutarlı bir kare hızının önemli olduğunu gösteren bir çalışma yayınladı: "Sağlam bir kare hızı, profesyonel ve iyi yapılmış bir ürünün göstergesidir." Web geliştiricileri dikkat.

Performans sorunlarını çözme

Performans sorunlarını çözmek, suç çözmeye benzer. Kanıtları dikkatlice incelemeniz, şüpheli nedenleri kontrol etmeniz ve farklı çözümler denemeniz gerekir. Sorunu gerçekten çözdüğünüzden emin olabilmek için tüm süreç boyunca ölçümlerinizi belgelemeniz gerekir. Bu yöntemle dedektiflerin bir vakayı çözme yöntemi arasında çok az fark vardır. Dedektifler kanıtları inceler, şüphelileri sorgular ve suçluyu bulmak için deneyler yapar.

V8 CSI: Oz

Find Your Way to Oz'u oluşturan muhteşem sihirbazlar, kendi başlarına çözemedikleri bir performans sorunuyla V8 ekibine başvurdu. Oz bazen donarak takılmaya neden oluyordu. Oz geliştiricileri, Chrome Geliştirici Araçları'ndaki Zaman Çizelgesi Paneli'ni kullanarak bazı ilk incelemeler yapmıştı. Bellek kullanımına baktıklarında korkunç testere dişi grafiğiyle karşılaştılar. Çöp toplayıcı, saniyede bir kez 10 MB çöp topluyordu ve çöp toplama duraklamaları takılmaya karşılık geliyordu. Chrome Geliştirici Araçları'ndaki Zaman Çizelgesi'nden alınan aşağıdaki ekran görüntüsüne benzer:

Geliştirici Araçları zaman çizelgesi

V8 dedektifleri Jakob ve Yang bu vakayı incelemeye başladı. V8 ekibinden Jakob ve Yang ile Oz ekibi arasında uzun bir görüşme süreci yaşandı. Bu görüşmeyi, sorunun tespit edilmesine yardımcı olan önemli olaylara indirgedim.

Kanıt

İlk adım, ilk kanıtları toplamak ve incelemektir.

Ne tür bir uygulamadan bahsediyoruz?

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

Oz, double değerler üzerinde çok sayıda aritmetik hesaplama yapar ve WebAudio ile WebGL'yi sık sık çağırır.

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

Duraklamalar (kare düşüşleri veya takılmalar) görüyoruz. Bu duraklamalar, çöp toplama işlemleriyle ilişkilidir.

Geliştiriciler en iyi uygulamaları takip ediyor mu?

Evet, Oz geliştiricileri JavaScript sanal makine performansı ve optimizasyon teknikleri konusunda oldukça bilgili. Oz geliştiricilerinin kaynak dil olarak CoffeeScript kullandığını ve CoffeeScript derleyicisi aracılığıyla JavaScript kodu ürettiğini belirtmekte fayda var. Oz geliştiricilerinin yazdığı kod ile V8 tarafından kullanılan kod arasındaki bağlantı koptuğu için incelemenin bazı kısımları daha zor hale geldi. Chrome Geliştirici Araçları artık bu işlemi kolaylaştıracak kaynak haritalarını destekliyor.

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

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

Genç nesil, eski nesle kıyasla çok daha yüksek bir sıklıkta toplanır. Bu, genç nesil toplama çok daha ucuz olduğu için bilinçli bir tercihtir. Sık sık gerçekleşen GC duraklamalarının genellikle genç nesil toplama işleminden kaynaklandığını varsaymak güvenlidir.

V8'de genç bellek alanı, eşit boyutlu iki bitişik bellek bloğuna bölünür. Bu iki bellek bloğundan yalnızca biri herhangi bir zamanda kullanılır ve buna hedef alan adı verilir. Hedef alanda boş bellek olduğu sürece yeni bir nesne ayırmak kolaydır. Hedef alandaki bir imleç, yeni nesne için gereken bayt sayısı kadar ileri taşınır. Bu işlem, hedef alan dolana kadar devam eder. Bu noktada program durdurulur ve veri toplama işlemi başlar.

V8 genç bellek

Bu noktada, kaynak alan ve hedef alan yer değiştirir. Önceden "to" alanı olan ve artık "from" alanı olan alan baştan sona taranır ve hâlâ etkin olan tüm nesneler "to" alanına kopyalanır veya eski nesil yığına yükseltilir. Ayrıntılı bilgi edinmek isterseniz Cheney'nin Algoritması hakkında bilgi edinmenizi öneririz.

Bir nesne dolaylı veya doğrudan (new, [] veya {} çağrısı aracılığıyla) her tahsis edildiğinde uygulamanızın çöp toplama işlemine ve korkunç uygulama duraklatma işlemine daha da yaklaştığını sezgisel olarak anlamanız gerekir.

Bu uygulama için saniyede 10 MB çöp veri normal mi?

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

Şüpheliler

Soruşturmanın bir sonraki aşaması, olası şüphelileri belirlemek ve ardından şüpheli sayısını azaltmaktır.

1. Şüpheli

Kare sırasında yeni çağrı yapılıyor. Tahsis edilen her nesnenin sizi GC duraklatması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. Bu genellikle dikkatlice düşünülmüş, uygulamaya özel bir nesne geri dönüşüm sistemi gerektirir. V8 dedektifleri, Oz ekibiyle görüştü ve yeni bir çağrı yapmadıklarını öğrendi. Aslında Oz ekibi bu şartın zaten farkındaydı ve "Bu utanç verici olur" dedi. Bu maddeyi listeden çıkarabilirsiniz.

2. Şüpheli

Bir nesnenin "şeklini" oluşturucunun dışında 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 optimizasyon iptali tetiklenir ve kod, sık kullanılan olarak sınıflandırılıp tekrar optimize edilene kadar optimize edilmemiş kod yürütülür. Bu optimizasyonu kaldırma ve yeniden optimize etme karmaşası,takılmaya neden olur ancak aşırı çöp oluşturmayla doğrudan ilişkili değildir. Kodun dikkatli bir şekilde denetlenmesinin ardından nesne şekillerinin statik olduğu doğrulandı ve böylece 2 numaralı şüpheli elendi.

3. Şüpheli

Optimize edilmemiş kodda aritmetik. Optimize edilmemiş kodda tüm hesaplamalar, gerçek nesnelerin tahsis edilmesiyle sonuçlanır. Örneğin, şu snippet:

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

5 HeapNumber nesnesi oluşturulur. İlk üçü a, b ve c değişkenleri içindir. Dördüncü, anonim değer (a * b) için, beşinci ise 4. * c'den gelir. Beşinci, nihayetinde point.x'e atanır.

Oz, kare başına bu işlemlerden binlercesini gerçekleştirir. Bu hesaplamalardan herhangi biri hiçbir zaman optimize edilmeyen işlevlerde gerçekleşirse çöpün nedeni olabilir. Çünkü optimize edilmemiş hesaplamalar, geçici sonuçlar için bile bellek ayırır.

4. Şüpheli

Bir özelliğe çift duyarlıklı sayı depolama. Sayıyı depolamak için bir HeapNumber nesnesi oluşturulmalı ve özellik, bu yeni nesneyi işaret edecek şekilde değiştirilmelidir. Özelliği HeapNumber'ı işaret edecek şekilde değiştirmek, çöp oluşturmaz. Ancak nesne özellikleri olarak depolanan birçok çift duyarlıklı sayı olabilir. Kod, aşağıdaki gibi ifadelerle doludur:

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

Optimize edilmiş kodda, x'e her yeni hesaplanan değer atandığında, görünüşte zararsız bir ifade olan yeni bir HeapNumber nesnesi dolaylı olarak tahsis edilir ve bu da bizi çöp toplama duraklamasına yaklaştırır.

Yazılı bir dizi (veya yalnızca çift duyarlıklı sayıları tutan normal bir dizi) kullanarak bu sorunu tamamen önleyebileceğinizi unutmayın. Bunun nedeni, çift duyarlıklı sayı için depolama alanının yalnızca bir kez ayrılması ve değerin tekrar tekrar değiştirilmesinin yeni depolama alanı ayrılmasını gerektirmemesidir.

4 numaralı şüpheli de olabilir.

Adli tıp

Bu noktada dedektiflerin iki olası şüphelisi vardır: yığın numaralarını nesne özellikleri olarak depolamak ve optimize edilmemiş işlevlerin 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, asıl Oz kaynak kodunda bulunan sorunun bir kopyasını kullanacağım. Bu yeniden üretim, orijinal koddan çok daha küçüktür ve bu nedenle daha kolay anlaşılır.

1 numaralı deneme

3 numaralı şüpheli (optimize edilmemiş işlevlerdeki aritmetik hesaplama) kontrol ediliyor. V8 JavaScript motorunda, arka planda neler olup bittiğiyle ilgili ayrıntılı bilgi sağlayabilen yerleşik bir günlük kaydı sistemi vardır.

Chrome'un hiç çalışmadığı bir durumda Chrome'u flag'lerle başlatma:

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

ve ardından Chrome'dan tamamen çıkıldığında geçerli dizinde bir v8.log günlük dosyası oluşturulur.

v8.log dosyasının içeriğini yorumlamak için Chrome'unuzun kullandığı v8 sürümünün aynısını indirmeniz (about:version sayfasını kontrol edin) ve derlemeniz gerekir.

V8'i başarıyla derledikten sonra tick işlemcisini kullanarak günlüğü işleyebilirsiniz:

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

(Platformunuza bağlı olarak linux yerine mac veya windows yazın.) (Bu araç, v8'deki en üst düzey kaynak dizininden çalıştırılmalıdır.)

Tick processor, en çok tick'e sahip JavaScript işlevlerinin metin tabanlı bir tablosunu gösterir:

[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 yıldız işareti (*) bulunur. opt işlevinin optimize edildiğini ve unopt işlevinin optimize edilmediğini gözlemleyin.

V8 dedektifinin araç çantasındaki bir diğer önemli araç da plot-timer-event'tir. Şu şekilde çalıştırılabilir:

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

Çalıştırıldıktan sonra, geçerli dizinde timer-events.png adlı bir png dosyası oluşturulur. Açtığınızda şuna benzer bir şey görürsünüz:

Zamanlayıcı etkinlikleri

Alttaki grafik dışında veriler satırlar halinde gösterilir. X ekseni zamanı (ms) gösterir. Sol tarafta her satır için etiketler bulunur:

Zamanlayıcı etkinlikleri Y ekseni

V8.Execute satırında, V8'in JavaScript kodunu yürüttüğü her profil işaretinde siyah dikey bir çizgi çizilir. V8.GCScavenger, V8'in yeni nesil bir koleksiyon gerçekleştirdiği her profil işaretinde çizilmiş mavi dikey bir çizgiye sahiptir. Diğer V8 durumları için de benzer şekilde.

En önemli satırlardan biri "yürütülen kod türü"dür. Optimize edilmiş kod yürütülürken yeşil, optimize edilmemiş kod yürütülürken ise kırmızı ve mavi karışımı bir renk gösterilir. Aşağıdaki ekran görüntüsünde, optimize edilmiş koddan optimize edilmemiş koda ve ardından tekrar optimize edilmiş koda geçiş gösterilmektedir:

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

İdeal olarak bu çizgi düz yeşil olur ancak bu durum hiçbir zaman hemen gerçekleşmez. Bu, programınızın optimize edilmiş sabit bir duruma geçtiği anlamına gelir. Optimize edilmemiş kod her zaman optimize edilmiş koddan daha yavaş çalışır.

Bu kadar uğraştıysanız uygulamanızı v8 hata ayıklama kabuğunda (d8) çalışabilecek şekilde yeniden düzenleyerek çok daha hızlı çalışabileceğinizi belirtmekte fayda var. d8'i kullanmak, tick-processor ve plot-timer-event araçlarıyla daha hızlı iterasyon süreleri elde etmenizi sağlar. d8 kullanmanın bir diğer yan etkisi de gerçek sorunu izole etmenin kolaylaşması ve verilerdeki gürültü miktarının azalmasıdır.

Oz kaynak kodundaki zamanlayıcı etkinlikleri grafiğine bakıldığında, optimize edilmiş koddan optimize edilmemiş koda geçiş olduğu 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üldü (ortadaki zamanın kaldırıldığına dikkat edin):

Zamanlayıcı etkinlikleri grafiği

Dikkatli bakarsanız V8'in JavaScript kodunu ne zaman yürüttüğünü gösteren siyah çizgilerin, yeni nesil koleksiyonlarla (mavi çizgiler) tam olarak aynı profil onay sürelerinde eksik olduğunu görebilirsiniz. Bu, çöp toplama işlemi sırasında komut dosyasının duraklatıldığını açıkça gösterir.

Oz kaynak kodundan gelen tick işlemci çıkışına bakıldığında, en üstteki işlevin (updateSprites) optimize edilmediği görülüyor. Başka bir deyişle, programın en çok zaman harcadığı işlev de optimize edilmemişti. Bu durum, 3 numaralı şüphelinin suçlu olduğunu güçlü bir şekilde gösteriyor. updateSprites için kaynak, şuna benzer 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 çok iyi bildikleri için for-i-in döngü yapısının bazen V8 tarafından optimize edilmediğini hemen fark ettiler. Başka bir deyişle, bir işlev for-i-in döngü yapısı içeriyorsa optimize edilemeyebilir. Bu, günümüzde özel bir durumdur ve gelecekte değişebilir. Yani V8 bir gün bu döngü yapısını optimize edebilir. V8 uzmanı olmadığımız ve V8'i avucumuzun içi gibi bilmediğimiz için updateSprites'ın neden optimize edilmediğini nasıl belirleyebiliriz?

2. Deneme

Chrome'u bu işaretle çalıştırma:

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

Optimizasyon ve optimizasyon kaldırma verilerinin ayrıntılı bir günlüğünü gösterir. updateSprites verilerini aradığımızda şunları buluruz:

[disabled optimization for updateSprites, reason: ForInStatement is not fast case]

Dedektiflerin tahmin ettiği gibi, bunun nedeni for-i-in döngü yapısıydı.

Destek kaydı kapatıldı

updateSprites'ın optimize edilmeme nedeni keşfedildikten sonra düzeltme basitti. Hesaplamayı kendi işlevine taşımak yeterliydi:

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 edilecek, bu da çok daha az HeapNumber nesnesiyle sonuçlanacak ve GC duraklamalarının sıklığı azalacaktır. Yeni kodla aynı denemeleri yaparak bunu kolayca doğrulayabilirsiniz. Dikkatli okuyucular, ondalıklı sayıların hâlâ özellik olarak depolandığını fark edecektir. Profil oluşturma işlemi bunun faydalı olacağını gösteriyorsa konumun bir double dizisi veya yazılı veri dizisi olacak şekilde değiştirilmesi, oluşturulan nesne sayısını daha da azaltır.

Son söz

Oz geliştiricileri bununla yetinmedi. V8 dedektiflerinin paylaştığı araçlar ve tekniklerle donanmış olan ekip, optimizasyonun kaldırılması cehenneminde sıkışıp kalmış birkaç işlev daha buldu ve hesaplama kodunu optimize edilmiş yaprak işlevlere dahil ederek daha da iyi performans elde etti.

Hemen işe koyulun ve performans suçlarını çözmeye başlayın.