Web geliştiricileri, kodlarında hata ayıklama işlemi yaparken performansın çok az etkilenmesini veya hiç etkilenmemesini beklemeye başladı. Ancak bu beklenti her zaman geçerli değildir. C++ geliştiricileri, uygulamalarının hata ayıklama derlemesinin üretim performansına ulaşmasını asla beklemez. Chrome'un ilk yıllarında, DevTools'un açılması bile sayfanın performansını önemli ölçüde etkiliyordu.
Bu performans düşüşünden artık etkilenmememiz, DevTools ve V8'in hata ayıklama özelliklerine yıllarca yapılan yatırımların sonucudur. Yine de DevTools'un performans yükü hiçbir zaman sıfıra indirilemez. Kesme noktaları ayarlama, kodda adımlama, 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 onu değiştirir.
Ancak elbette, tüm hata ayıklayıcılarda olduğu gibi DevTools'un da yükü makul olmalıdır. Son zamanlarda, DevTools'un belirli durumlarda uygulamayı artık kullanılamayacak kadar yavaşlattığına dair raporların sayısında önemli bir artış gözlemledik. Aşağıda, chromium:1069425 raporundan alınan ve yalnızca DevTools'un açık olmasının performans üzerindeki etkisini gösteren bir yan yana karşılaştırma görebilirsiniz.
Videoda da görebileceğiniz gibi, yavaşlama 5-10 kat düzeyinde. Bu kabul edilemez bir durum. İlk adım, tüm zamanın nereye gittiğini ve DevTools açıkken bu büyük yavaşlamaya neyin neden olduğunu anlamaktı. Chrome Oluşturucu işleminde Linux perf kullanılarak genel oluşturucu yürütme süresinin aşağıdaki dağılımı ortaya çıkarıldı:
Yığın izlemelerinin toplanmasıyla ilgili bir şey görmeyi bekliyorduk ancak genel yürütme süresinin yaklaşık %90'ının yığın çerçevelerinin sembolize edilmesine harcandığını beklemiyorduk. Buradaki sembolleştirme, işlev adlarını ve ham yığın çerçevelerinden somut kaynak konumlarını (komut dosyalarındaki satır ve sütun numaraları) çözme işlemini ifade eder.
Yöntem adı çıkarım
Daha da şaşırtıcı olan, neredeyse tüm zamanların V8'deki JSStackFrame::GetMethodName()
işlevine gitmesiydi. Ancak önceki araştırmalardan JSStackFrame::GetMethodName()
'ün performans sorunlarının dünyasında yabancı olmadığını biliyorduk. Bu işlev, yöntem çağrısı 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. Kodu kısaca incelediğimizde, işlevin nesnenin ve prototip zincirinin tam bir tarama işlemi gerçekleştirerek ve
value
değerifunc
kapanış olan veri mülkleri veyaget
veyaset
'ninfunc
kapatma değerine eşit olduğu erişim özelliklerini belirtir.
Bu durum, tek başına çok ucuz bir çözüm gibi görünmese de bu korkunç yavaşlamayı açıklamıyor. Bu nedenle, chromium:1069425 adresinde bildirilen örneği incelemeye başladık ve yığın izlemelerinin, 10 MiB boyutunda bir JavaScript dosyası olan classes.js
kaynaklı günlük mesajlarının yanı sıra ayarsız görevler için de toplandığını tespit ettik. Daha ayrıntılı bir inceleme, bunun temelde bir Java çalışma zamanı ve JavaScript'e derlenmiş uygulama kodu olduğunu ortaya çıkardı. Yığın izlemeleri, A
nesnesinde çağrılan yöntemlerin bulunduğu birkaç çerçeve içeriyordu. Bu nedenle, ne tür bir nesneyle uğraştığımızı anlamanın faydalı olabileceğini düşündük.
Java'dan JavaScript'e derleyici,82.203 işlev içeren tek bir nesne oluşturmuş. Bu durum ilginç olmaya başlamıştı. Ardından, kolayca ulaşabileceğimiz bazı kolay hedefler olup olmadığını anlamak için V8'in JSStackFrame::GetMethodName()
bölümüne geri döndük.
- Öncelikle nesnenin bir özelliği olarak işlevin
"name"
değerini arar ve bulunursa özellik değerinin işlevle eşleşip eşleşmediğini kontrol eder. - İşlevin adı yoksa veya nesnenin eşleşen bir özelliği yoksa işlev, nesnenin ve prototiplerinin tüm özelliklerini tarayarak ters aramaya geçer.
Örneğimizde tüm işlevler anonimdir ve boş "name"
mülklerine sahiptir.
A.SDV = function() {
// ...
};
İlk bulgu, ters aramanın iki adıma bölünmüş olduğuydu (nesnenin kendisi ve prototip zincirindeki her nesne için gerçekleştirilir):
- Tüm listelenebilir mülklerin adlarını ayıklayın ve
- Her ad için genel mülk araması gerçekleştirerek, elde edilen mülk değerinin aradığımız kapatma ile eşleşip eşleşmediğini test edin.
Adları ayıklamak için zaten tüm mülkleri incelemek gerektiğinden bu, kolayca elde edilebilecek bir sonuç gibi görünüyordu. İki geçiş yapmak yerine (ad ayıklaması için O(N) ve testler için O(N log(N))) her şeyi tek bir geçişte yapabilir ve mülk değerlerini doğrudan kontrol edebiliriz. Bu sayede işlevin tamamı 2-10 kat daha hızlı çalıştı.
İkinci bulgu daha da ilgi çekiciydi. İşlevler teknik olarak anonim işlevler olsa da V8 motoru, bunlar için tahmine dayalı ad olarak adlandırdığımız bir ad kaydetmişti. obj.foo = function() {...}
biçimindeki atamaların sağ tarafında görünen işlev sabitleri için V8 ayrıştırıcısı, "obj.foo"
değerini işlev sabiti için tahmine dayalı ad olarak saklar. Bu durumda, arama yapabileceğimiz doğru ada sahip olmasak da yeterince yakın bir isme sahiptik: Yukarıdaki A.SDV = function() {...}
örneğinde, "A.SDV"
tahmini ad olarak kullanılıyordu. Son noktayı bulup ardından nesnede "SDV"
mülkünü arayarak tahmini addan mülk adını türetebiliriz. Bu, neredeyse tüm durumlarda işe yaradı ve pahalı bir tam geçişi tek bir mülk aramasıyla değiştirdi. Bu iki iyileştirme, bu CL kapsamında kullanıma sunuldu ve chromium:1069425'te bildirilen örnekte yavaşlamayı önemli ölçüde azalttı.
Error.stack
Buradan ayrılabilirdik. Ancak DevTools, yığın çerçeveleri için yöntem adını hiçbir zaman kullanmadığından, burada bir sorun olduğu anlaşılıyordu. Hatta C++ API'deki v8::StackFrame
sınıfı, yöntem adına ulaşmanın bir yolunu bile sunmaz. Bu nedenle, ilk başta JSStackFrame::GetMethodName()
ile iletişime geçmemiz yanlış bir karardı. Bunun yerine, yöntem adını kullandığımız (ve gösterdiğimiz) 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üklü bir foo
işlevi var. Bu snippet'i Chromium'da çalıştırdığınızda aşağıdaki çıkışı elde edersiniz:
Error
at Object.foo [as bar] (error-methodname.js:2)
at error-methodname.js:6
Burada, yöntem adı aramasının nasıl kullanıldığını görüyoruz: En üst yığın çerçevesinin, bar
adlı yöntem aracılığıyla Object
örneğinde foo
işlevini çağırdığı gösterilmektedir. Bu nedenle, standart olmayan error.stack
mülkü JSStackFrame::GetMethodName()
'u yoğun şekilde kullanıyor. Ayrıca performans testlerimiz, yaptığımız değişikliklerin önemli ölçüde hız sağladığını da gösteriyor.
Ancak Chrome Geliştirici Araçları konusuna dönecek olursak, error.stack
kullanılmamasına rağmen yöntem adının hesaplanması doğru görünmüyor. Bu konuda geçmişte bize yardımcı olan bazı bilgiler var: Geleneksel olarak V8'de, yukarıda açıklanan iki farklı API (C++ v8::StackFrame
API ve JavaScript yığın izleme API'si) için yığın izlemeyi toplayıp temsil etmek üzere iki ayrı mekanizma mevcuttu. Yaklaşık olarak aynı işlemi iki farklı şekilde yapmak hatalara yol açıyordu ve genellikle tutarsızlıklara ve hatalara neden oluyordu. Bu nedenle 2018'in sonlarında, yığın izleme yakalama için tek bir darboğaza karar vermek üzere bir proje başlattık.
Bu proje büyük bir başarı elde etti ve yığın izleme toplama ile ilgili sorunların sayısını önemli ölçüde azalttı. Standart olmayan error.stack
mülkü aracılığıyla sağlanan bilgilerin çoğu da yalnızca gerçekten ihtiyaç duyulduğunda ve yavaşça hesaplanıyordu. Ancak yeniden yapılandırmanın bir parçası olarak aynı hileyi v8::StackFrame
nesnelerine de uyguladık. Yığın çerçevesiyle ilgili tüm bilgiler, üzerinde herhangi bir yöntem ilk kez çağrıldığında hesaplanır.
Bu genellikle performansı artırır ancak maalesef bu C++ API nesnelerinin Chromium ve DevTools'ta kullanılma şekline biraz aykırı olduğu ortaya çıktı. Özellikle, v8::StackFrame
veya error.stack
aracılığıyla sunulan bir yığın çerçevesiyle ilgili tüm bilgileri içeren yeni bir v8::internal::StackFrameInfo
sınıfı kullanıma sunduğumuzdan, her zaman her iki API tarafından sağlanan bilgilerin süper kümesini hesaplardık. Bu da, v8::StackFrame
'un (ve özellikle DevTools'un) kullanımlarında, bir yığın çerçevesiyle ilgili herhangi bir bilgi istendiğinde yöntem adını da hesaplayacağımız anlamına geliyordu. DevTools'un her zaman kaynak ve komut dosyası bilgilerini hemen istediği anlaşılıyor.
Bu tespite dayanarak yığın çerçevesi temsilini yeniden yapılandırıp büyük ölçüde basitleştirdik ve daha da tembel hale getirdik. Böylece V8 ve Chromium'daki kullanıcılar artık yalnızca istedikleri bilgileri hesaplamanın maliyetini ödüyor. Bu, yığın çerçeveleriyle ilgili bilgilerin yalnızca bir kısmına (temel olarak satır ve sütun ofseti biçiminde komut dosyası adı ve kaynak konumu) ihtiyaç duyan DevTools ve diğer Chromium kullanım alanları için büyük bir performans artışı sağladı ve daha fazla performans iyileştirmesine kapı açtı.
İşlev adları
Yukarıda belirtilen yeniden yapılanma işlemleri tamamlandıktan sonra, sembolleştirmenin ek maliyeti (v8_inspector::V8Debugger::symbolize
içinde harcanan süre) toplam yürütme süresinin yaklaşık %15'ine düşürüldü ve V8'in DevTools'da kullanılmak üzere yığın çerçevelerini sembolize ederken (toplarken ve) nerede zaman harcadığını daha net görebildik.
İlk dikkat çeken şey, satır ve sütun numarasını hesaplamanın kümülatif maliyetiydi. Buradaki pahalı kısım, komut dosyasındaki karakter ofsetini hesaplamaktır (V8'den aldığımız bayt kodu ofsetine göre). Yukarıdaki yeniden düzenleme işlemimiz nedeniyle bunu bir kez satır numarasını hesaplarken, bir kez de sütun numarasını hesaplarken yaptığımız ortaya çıktı. v8::internal::StackFrameInfo
örneklerindeki kaynak konumunu önbelleğe alma, bu sorunun hızlı bir şekilde çözülmesine yardımcı oldu ve v8::internal::StackFrameInfo::GetColumnNumber
'ı tüm profillerden tamamen ortadan kaldırdı.
Bizim için daha ilginç olan bulgu, incelediğimiz tüm profillerde v8::StackFrame::GetFunctionName
değerinin şaşırtıcı derecede yüksek olmasıydı. Bu konuyu daha ayrıntılı bir şekilde incelediğimizde, DevTools'taki yığın çerçevesinde işlev için göstereceğimiz adı hesaplamanın gereksiz yere maliyetli olduğunu fark ettik.
- önce standart olmayan
"displayName"
mülkünü ararız ve bu arama sonucunda dize değeri içeren bir veri mülkü elde edersek bunu kullanırız. - Aksi takdirde, standart
"name"
mülkünü aramaya geçer ve bu aramanın sonucunda değeri dize olan bir veri mülkünün bulunup bulunmadığını tekrar kontrol eder. - ve sonunda V8 ayrıştırıcısı tarafından tahmin edilen ve işlev değişmezinde depolanan dahili bir hata ayıklama adına geri döner.
"displayName"
özelliği, Function
örneklerindeki "name"
özelliğinin JavaScript'te salt okunur ve yapılandırılamaz olması nedeniyle geçici bir çözüm olarak eklendi ancak hiçbir zaman standartlaştırılmadı ve yaygın olarak kullanılmadı.Bunun nedeni, tarayıcı geliştirici araçlarının, işlevi% 99,9 oranında yerine getiren işlev adı çıkarımının eklenmiş olmasıdır. Ayrıca ES2015, Function
örneklerindeki "name"
mülkünü yapılandırılabilir hale getirerek özel bir "displayName"
mülküne olan ihtiyacı tamamen ortadan kaldırdı. "displayName"
için negatif arama oldukça maliyetli ve gerçekten gerekli olmadığından (ES2015 beş yıldan uzun bir süre önce yayınlandı), V8'den (ve DevTools'dan) standart olmayan fn.displayName
mülkü için desteği kaldırmaya karar verdik.
"displayName"
için negatif arama işlemi kaldırıldığından v8::StackFrame::GetFunctionName
maliyetinin yarısı kaldırıldı. Diğer yarısı ise genel "name"
mülk aramasına gider. Neyse ki, (dokunulmamış) Function
örneklerindeki "name"
mülkünün maliyetli aramalarını önlemek için zaten bazı mantıklarımız vardı. Bu mantığı, Function.prototype.bind()
'ı daha hızlı hale getirmek için bir süre önce V8'de kullanıma sunduk. İlk olarak maliyetli genel aramayı atlamamıza olanak tanıyan gerekli kontrolleri taşıdık. Sonuç olarak v8::StackFrame::GetFunctionName
, dikkate aldığımız hiçbir profilde artık görünmüyor.
Sonuç
Yukarıdaki iyileştirmelerle, yığın izlemeleri açısından DevTools'un ek yükünü önemli ölçüde azalttık.
Hâlâ çeşitli iyileştirmeler yapılabileceğinin farkındayız. Örneğin, MutationObserver
kullanırken ortaya çıkan ek yük hâlâ belirgindir (chromium:1077657 adresinde belirtildiği gibi). Ancak şimdilik en önemli sorunları ele aldık. Gelecekte hata ayıklama performansını daha da kolaylaştırmak için bu konuyu tekrar ele alabiliriz.
Ö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ı, en son DevTools özelliklerine erişmenize, en yeni web platformu API'lerini test etmenize ve sitenizdeki sorunları kullanıcılarınızdan önce bulmanıza yardımcı olur.
Chrome Geliştirici Araçları Ekibi ile iletişime geçme
Yeni özellikler, güncellemeler veya Geliştirici Araçları ile ilgili başka herhangi bir konu hakkında konuşmak için aşağıdaki seçenekleri kullanın.
- crbug.com adresinden bize geri bildirim ve özellik isteği gönderin.
- Geliştirici Araçları'nda Diğer seçenekler > Yardım > Geliştirici Araçları sorunu bildir'i kullanarak bir Geliştirici Araçları sorununu bildirin.
- @ChromeDevTools hesabına tweet gönderin.
- Geliştirici Araçları'ndaki yenilikler veya Geliştirici Araçları'yla ilgili ipuçları konulu YouTube videolarına yorum bırakın.