Chrome DevTools yığın izlemeleri nasıl 10 kat hızlandırdık?

Benedikt Meurer
Benedikt Meurer

Web geliştiricileri, kodlarında hata ayıklamanın performans üzerinde çok az etki yaratmasını beklemektedir. Ancak bu beklenti kesinlikle evrensel değildir. C++ geliştiricileri, uygulamalarının hata ayıklama derlemesinin üretim performansına ulaşmasını asla beklemezdi. Chrome'un ilk yıllarında yalnızca Geliştirici Araçları'nı açmak sayfanın performansını önemli ölçüde etkiledi.

Performanstaki düşüşün nedeni, yıllarca DevTools ve V8'in hata ayıklama özelliklerinde yaptığımız yatırımdır. Yine de DevTools'un performans ek yükünü hiçbir zaman sıfıra indiremeyeceğiz. Ayrılma noktaları ayarlama, kodda adım adım ilerleme, yığın izlemeleri toplama, performans izlemesi yakalama vb. tüm işlemler yürütme hızını farklı derecelerde etkiler. Sonuçta, bir şeyi gözlemlemek o şeyi değiştirir.

Ancak elbette tüm hata ayıklayıcılar gibi DevTools'un ek yükü makul olacaktır. Kısa süre önce, DevTools'un bazı durumlarda uygulamayı artık kullanılamayacak kadar yavaşlattığına dair raporların sayısında önemli bir artış gördük. Aşağıda, chromium:1069425 raporundaki bir yan yana karşılaştırmayı görebilirsiniz. Bu karşılaştırmada, yalnızca Geliştirici Araçları'nın açık olmasının performans ek yükü gösterilmektedir.

Videoda görebileceğiniz gibi, yavaşlama 5-10x aralığındadır ve bu kabul edilebilir bir durum değildir. İlk adım, Geliştirici Araçları açıkken bu kadar uzun zamanın nerelere gittiğini ve yaşanan büyük yavaşlamaya neyin neden olduğunu anlamaktı. Chrome Oluşturucu işleminde Linux perf kullanıldığında, genel oluşturucu yürütme süresi aşağıdaki dağılımla görüntülenir:

Chrome Oluşturucu yürütme süresi

Yığın izlemelerle ilgili bir durum görmeyi beklemiş olsak da genel yürütme süresinin yaklaşık 90%'ının yığın çerçevelerini simgelemeye gitmesini beklemiyorduk. Buradaki simgeleme, ham yığın çerçevelerinden işlev adlarını ve somut kaynak konumlarını (komut dosyalarındaki satır ve sütun numaralarını) çözümleme işlemini ifade eder.

Yöntem adı çıkarımı

Daha da şaşırtıcı olan, neredeyse her zaman V8'deki JSStackFrame::GetMethodName() işlevine gitmesiydi. Ancak, önceki araştırmalardan JSStackFrame::GetMethodName() ürününün, performans sorunları diyarına yabancı olmadığını biliyorduk. Bu işlev, yöntem çağrıları olarak kabul edilen çerçeveler (func() yerine obj.func() biçimindeki işlev çağrılarını temsil eden çerçeveler) için yöntemin adını hesaplamaya çalışır. Koda hızlı bir şekilde göz atıldığında, koda hızlıca bakıldığında, nesnenin ve prototip zincirinin tam geçişini gerçekleştirerek ve bunu arayarak

  1. value değeri func olan veri mülkleri veya
  2. get veya set değerinin func kapatmaya eşit olduğu erişimci özellikleri.

Bu tek başına kulağa çok ucuz gelmese de bu korkunç yavaşlamayı açıklamıyormuş gibi geliyor. chromium:1069425 ile bildirilen örneği ayrıntılı bir şekilde incelemeye başladık ve yığın izlemelerin eşzamansız görevler ile classes.js kaynaklı günlük iletileri için (10 MiB'lık JavaScript dosyası) toplandığını tespit ettik. Daha yakından bakınca, bunun temelde bir Java çalışma zamanı artı JavaScript'e derlenmiş bir uygulama kodu olduğu anlaşıldı. Yığın izlemeler (stack trace), A nesnesinde çağrılan yöntemlere sahip birkaç kare içeriyordu. Bu nedenle, ne tür bir nesneyle çalıştığımızı anlamanın yararlı olabileceğini düşündük.

bir nesnenin yığın izlemeleri

Görünüşe göre, Java'dan JavaScript'e derleyicisi, üzerinde büyük bir 82.203 işlev bulunan tek bir nesne oluşturdu. Bu durum açıkça ilginç olmaya başladı. Daha sonra, burada toplayabileceğimiz kolay anlaşılır meyveler olup olmadığını anlamak için V8'in JSStackFrame::GetMethodName() özelliğine geri döndük.

  1. Önce nesne üzerinde bir özellik olarak işlevin "name"'sini arayarak çalışır ve bulunursa özellik değerinin işlevle eşleşip eşleşmediğini kontrol eder.
  2. İşlevin adı yoksa veya nesnenin eşleşen bir özelliği yoksa, nesnenin ve prototiplerinin tüm özelliklerinden geçerek ters aramaya döner.

Örneğimizde tüm işlevler anonimdir ve boş "name" özelliklerine sahiptir.

A.SDV = function() {
   // ...
};

İlk bulgu, ters aramanın iki adıma bölünmesiydi (nesnenin kendisi ve prototip zincirindeki her bir nesne için gerçekleştirilen):

  1. Tüm numaralandırılabilir özelliklerin adlarını çıkarın ve
  2. Her ad için genel bir mülk araması gerçekleştirin ve elde edilen mülk değerinin, aradığımız kapatma işlemiyle eşleşip eşleşmediğini test edin.

Adları çıkarmak için zaten tüm mülklerin üzerinden geçmek gerektiğinden bu kolay bir meyve gibi görünüyordu. Ad ayıklama için O(N) ve testler için O(N log(N)) yerine her şeyi tek bir kartta yapabilir ve özellik değerlerini doğrudan kontrol edebiliriz. Bu da tüm işlevi yaklaşık 2-10 kat hızlandırdı.

İkinci bulgu ise daha da ilginçti. İşlevler teknik olarak anonim işlevler olsa da V8 motoru, bunlara çıkarım yoluyla ad dediğimiz işlemi kaydetmişti. obj.foo = function() {...} biçiminde atamaların sağ tarafında görünen işlev sabit değerleri için V8 ayrıştırıcı, "obj.foo" değerini işlev değişmez değerinin öngörülen ad olarak hatırlar. Örneğimizde bu, yalnızca arayabileceğimiz doğru isme sahip olmasak da yeterince yakın bir şeyimiz olduğu anlamına gelir: Yukarıdaki A.SDV = function() {...} örneğinde, tahmin edilen ad olarak "A.SDV" kullanılır ve son noktayı arayarak mülk adını türetebiliriz ve ardından nesnede "SDV" özelliğini bulmaya başlayabiliriz. Bu, neredeyse tüm durumlarda işe yaradı ve pahalı bir tam geçiş işlemi, tek bir mülk aramasıyla değiştirildi. Bu CL kapsamında sunulan bu iki iyileştirme, chromium:1069425 dosyasında bildirilen örnekteki yavaşlamayı önemli ölçüde azalttı.

Error.stack

Biz burada bir gün diyebiliriz. Ancak DevTools, yığın çerçeveleri için yöntem adını hiçbir zaman kullanmadığından akıl almaz bir şeyler vardı. Aslında, C++ API'sindeki v8::StackFrame sınıfı, yöntem adına ulaşmanın bir yolunu bile göstermez. Bu yüzden en başta JSStackFrame::GetMethodName() numaralı telefonu çağırmamız yanlış oldu. Bunun yerine, yöntem adını kullandığımız (ve açığa çıkardığımız) tek yer JavaScript yığın izleme API'sidir. Bu kullanımı anlamak için aşağıdaki basit örneği (error-methodname.js) inceleyin:

function foo() {
    console.log((new Error).stack);
}

var object = {bar: foo};
object.bar();

Burada object üzerinde "bar" adı altında yüklenmiş bir foo işlevimiz var. Bu snippet'in Chromium'da çalıştırılması aşağıdaki çıkışı sağlar:

Error
    at Object.foo [as bar] (error-methodname.js:2)
    at error-methodname.js:6

Burada, oynatmadaki yöntem adı arandığını görüyoruz: En üstteki yığın çerçevesinin, bir Object örneğinde bar adlı yöntemle foo işlevini çağırması gösterilmektedir. Dolayısıyla, standart olmayan error.stack özelliği JSStackFrame::GetMethodName() ürününün yoğun bir şekilde kullanılmasını sağlar. Hatta performans testlerimiz de değişikliklerimizin işleri önemli ölçüde hızlandırdığını gösteriyor.

StackTrace mikro karşılaştırmalarında hız kazanma

Chrome Geliştirici Araçları konusuna dönecek olursak, error.stack kullanılmasa bile yöntem adının hesaplandığı gerçeği doğru görünmüyor. Bize yardımcı olan bazı geçmişler var: Geleneksel olarak V8'in yukarıda açıklanan iki farklı API'ye (C++ v8::StackFrame API ve JavaScript yığın izleme API'si) ilişkin yığın izlemeyi (stack trace) toplamak ve temsil etmek üzere iki farklı mekanizması vardır. Aynı işlemi (kabaca) iki farklı şekilde yapmak hatalara meyilliydi ve çoğu zaman tutarsızlıklara ve hatalara yol açıyordu. Bu nedenle 2018'in sonlarında yığın izlemeyi yakalamayla ilgili tek bir darboğazı ortadan kaldırmak için bir proje başlattık.

Bu proje büyük bir başarıya imza attı ve yığın izleme (stack trace) toplamayla ilgili sorunların sayısını önemli ölçüde azalttı. Standart olmayan error.stack özelliği aracılığıyla sağlanan bilgilerin çoğu da temkinli bir şekilde ve yalnızca gerçekten gerekli olduğunda hesaplanmıştır. Ancak, yeniden düzenleme kapsamında v8::StackFrame nesneye de aynı yöntemi uyguladık. Yığın çerçevesiyle ilgili tüm bilgiler, üzerinde herhangi bir yöntem ilk kez çağrıldığında hesaplanır.

Bu yaklaşım genel olarak performansı artırır ancak maalesef bu C++ API nesnelerinin Chromium ve Geliştirici Araçları'nda kullanılma şekliyle biraz çelişki ortaya çıktı. Özellikle, v8::StackFrame veya error.stack aracılığıyla sunulan bir yığın çerçevesiyle ilgili tüm bilgilerin bulunduğu yeni bir v8::internal::StackFrameInfo sınıfını kullanıma sunduğumuzdan, her zaman her iki API tarafından sağlanan bilgilerin üst grubunu da hesaplıyorduk. Diğer bir deyişle, v8::StackFrame (özellikle de DevTools için) kullanımlarında bir yığın çerçevesi hakkında herhangi bir bilgi istendiğinde yöntemin adını da hesaplıyorduk. Geliştirici Araçları'nın her zaman kaynak ve komut dosyası bilgilerini anında istediği ortaya çıkar.

Bu durumun farkına vararak yığın çerçevesi temsilini yeniden düzenleyip büyük ölçüde basitleştirerek işlemi daha da yavaş hale getirmeyi başardık. Böylece, V8 ve Chromium'daki kullanımlar artık yalnızca istedikleri bilgileri hesaplama maliyetini ödüyor. Bu durum, yığın çerçeveleri hakkındaki bilgilerin yalnızca küçük bir kısmına (aslında sadece satır ve sütun ofseti biçiminde komut dosyası adı ve kaynak konumu) ihtiyaç duyan Geliştirici Araçları ve diğer Chromium kullanım alanlarında büyük bir performans artışı sağladı ve performansı artırmak için yeni fırsatlar yarattı.

İşlev adları

Yukarıda bahsedilen yeniden düzenlemelerle, sembolleştirmenin ek yükü (v8_inspector::V8Debugger::symbolize uygulamasında harcanan süre) genel yürütme süresinin yaklaşık 15%'ine düşürüldü ve DevTools'da yığın çerçevelerini tüketim için sembolize ederken (toplama ve) V8'in nerede zaman harcadığını daha net bir şekilde görebildik.

Sembolasyon maliyeti

Göze çarpan ilk şey satır ve sütun numarasının kümülatif maliyetiydi. Buradaki pahalı kısım, aslında komut dosyası içindeki karakter ofsetini hesaplamaktır (V8'den aldığımız bayt kodu ofsetine dayanarak) ve yukarıda yaptığımız yeniden düzenleme nedeniyle bunu bir kez satır numarasını hesaplarken, bir kez de sütun numarasını hesaplarken iki kez yaptık. v8::internal::StackFrameInfo örneklerinde kaynak konumunun önbelleğe alınması, bu sorunun hızla çözülmesine yardımcı oldu ve v8::internal::StackFrameInfo::GetColumnNumber adlı parçanın tüm profillerden tamamen çıkarılmasına yardımcı oldu.

Bizim için en ilginç bulgu, v8::StackFrame::GetFunctionName incelenen tüm profillerde şaşırtıcı derecede yüksekti. Burada daha derine indiğimizde, DevTools'daki yığın çerçevesindeki işlev için göstereceğimiz adı hesaplamanın gereksiz derecede maliyetli olduğunu fark ettik.

  1. Önce standart olmayan "displayName" özelliğini ararız. Bu, dize değerine sahip bir veri özelliği sağladıysa bunu kullanırız.
  2. Aksi takdirde, standart "name" özelliğini arayabilir ve bunun, değeri dize olan bir veri özelliği sağlayıp sağlamadığını tekrar kontrol edebilirsiniz.
  3. ve sonunda V8 ayrıştırıcı tarafından tahmin edilen ve işlev değişmez değerinde depolanan dahili hata ayıklama adına geri dönüyor.

"displayName" özelliği, JavaScript'te salt okunur ve yapılandırılamaz olan Function örneklerinde "name" özelliği için geçici bir çözüm olarak eklendi.Ancak tarayıcı geliştirici araçları durumların% 99,9'unda işi yapan işlev adı çıkarımı eklediğinden, hiçbir zaman standartlaştırılmadı ve geniş yaygın kullanım görmedi. Bunun üzerine ES2015, Function örneklerindeki "name" özelliğini yapılandırılabilir hale getirerek özel "displayName" özelliğine olan ihtiyacı tamamen ortadan kaldırdı. "displayName" için olumsuz arama oldukça maliyetli olduğundan ve gerçekten gerekli olmadığından (ES2015 beş yıldan uzun bir süre önce kullanıma sunuldu), standart olmayan fn.displayName mülküne yönelik desteği V8'den (ve Geliştirici Araçları'ndan) kaldırmaya karar verdik.

"displayName" için olumsuz arama yapılması nedeniyle v8::StackFrame::GetFunctionName maliyetinin yarısı kaldırıldı. Diğer yarısı ise genel "name" mülk aramasına gider. Neyse ki, Function.prototype.bind()'in kendisini daha hızlı hale getirmek için bir süre önce V8'de kullanıma sunduğumuz (dokunulmamış) Function örneklerinde "name" mülkünün maliyetli aramalarından kaçınmak için bir mantığa sahibiz. Maliyetli genel aramayı en başta atlamamıza olanak tanıyan gerekli kontrolleri taşıdık. Sonuç olarak, v8::StackFrame::GetFunctionName artık dikkate aldığımız profillerde görünmüyor.

Sonuç

Yukarıdaki iyileştirmeler sayesinde DevTools'un yığın izlemelere (stack trace) ilişkin ek yükünü önemli ölçüde azalttık.

Hâlâ çeşitli olası iyileştirmeler olduğunun farkındayız. Örneğin, chromium:1077657 adresinde bildirildiği üzere MutationObserver kullanımıyla ilgili ek yük dikkate alınmaya devam etmektedir, ancak şimdilik en önemli sorunları giderdik ve gelecekte hata ayıklama performansını daha da basitleştirmek için geri dönebiliriz.

Önizleme kanallarını indirme

Varsayılan geliştirme tarayıcınız olarak Chrome Canary, Yeni geliştirilenler veya Beta sürümünü kullanabilirsiniz. Bu önizleme kanalları ile Geliştirici Araçları'nın en yeni özelliklerine erişebilir, son teknoloji ürünü web platformu API'lerini test edebilir ve sitenizdeki sorunları kullanıcılarınızdan önce tespit edebilirsiniz.

Chrome Geliştirici Araçları ekibiyle iletişime geçme

Yayındaki yeni özellikler ve değişiklikler ya da Geliştirici Araçları ile ilgili diğer konular hakkında konuşmak için aşağıdaki seçenekleri kullanın.

  • crbug.com adresinden bize öneri veya geri bildirim gönderin.
  • Geliştirici Araçları'nda Diğer seçenekler > Yardım > Geliştirici Araçları sorunu bildir'i kullanarak Geliştirici Araçları sorunlarını bildirin.Daha fazla
  • @ChromeDevTools adresine tweet gönderin.
  • Geliştirici Araçları YouTube videoları bölümündeki yenilikler veya Geliştirici Araçları İpuçları YouTube videoları bölümlerimize yorum yapın.