นักพัฒนาเว็บคาดหวังว่าการแก้ไขข้อบกพร่องโค้ดจะไม่ส่งผลกระทบต่อประสิทธิภาพมากนักหรือไม่ส่งผลกระทบเลย อย่างไรก็ตาม ก็ไม่ได้หมายความว่าทุกคนจะคาดหวังเช่นนั้น นักพัฒนาซอฟต์แวร์ C++ คงคาดไม่ถึงว่าบิลด์แก้ไขข้อบกพร่องของแอปพลิเคชันจะมีประสิทธิภาพเทียบเท่าเวอร์ชันที่ใช้งานจริง และในช่วงปีแรกๆ ของ Chrome การเปิดเครื่องมือสำหรับนักพัฒนาเว็บเพียงอย่างเดียวก็ส่งผลต่อประสิทธิภาพของหน้าเว็บอย่างมาก
การที่ผู้ใช้ไม่รู้สึกถึงประสิทธิภาพที่ลดลงอีกต่อไปเป็นผลมาจากการลงทุนในความสามารถด้านการแก้ไขข้อบกพร่องของ DevTools และ V8 เป็นเวลาหลายปี อย่างไรก็ตาม เราไม่สามารถลดค่าใช้จ่ายเพิ่มเติมด้านประสิทธิภาพของ DevTools ให้เป็น 0 ได้ การตั้งจุดหยุดพัก การสเต็ปผ่านโค้ด การเก็บรวบรวมสแต็กเทรซ การบันทึกการติดตามประสิทธิภาพ ฯลฯ ล้วนส่งผลต่อความเร็วในการดำเนินการในระดับที่แตกต่างกัน ท้ายที่สุดแล้ว การสังเกตสิ่งหนึ่งๆ จะทําให้สิ่งนั้นเปลี่ยนแปลง
แต่แน่นอนว่าค่าใช้จ่ายเพิ่มเติมของ DevTools ควรอยู่ในระดับที่เหมาะสม เช่นเดียวกับเครื่องมือแก้ไขข้อบกพร่องอื่นๆ เมื่อเร็วๆ นี้ เราพบว่ามีรายงานจำนวนเพิ่มขึ้นอย่างมากว่าในบางกรณี DevTools จะทําให้แอปพลิเคชันช้าลงจนใช้งานไม่ได้ ด้านล่างนี้คุณจะเห็นการเปรียบเทียบแบบเคียงข้างกันจากรายงาน chromium:1069425 ซึ่งแสดงถึงค่าใช้จ่ายด้านประสิทธิภาพของการแค่เปิดเครื่องมือสำหรับนักพัฒนาเว็บไว้
ดังที่คุณเห็นจากวิดีโอ วิดีโอช้าลงประมาณ 5-10 เท่า ซึ่งยอมรับไม่ได้ ขั้นตอนแรกคือทำความเข้าใจว่าเวลาหายไปไหนและสาเหตุที่ทำให้เกิดความล่าช้าอย่างมากเมื่อเปิด DevTools การใช้ Linux perf ในกระบวนการแสดงผลของ Chrome แสดงให้เห็นการแจกแจงเวลาในการเรียกใช้โปรแกรมแสดงผลโดยรวมดังต่อไปนี้
แม้ว่าเราคาดหวังที่จะเห็นข้อมูลบางอย่างที่เกี่ยวข้องกับการเก็บรวบรวมสแต็กเทรซ แต่ก็ไม่ได้คาดคิดว่าเวลาในการดำเนินการโดยรวมประมาณ 90% จะใช้ไปกับการแสดงสัญลักษณ์เฟรมสแต็ก การแทนที่สัญลักษณ์ในที่นี้หมายถึงการแก้ไขชื่อฟังก์ชันและตำแหน่งแหล่งที่มาที่เฉพาะเจาะจง เช่น หมายเลขบรรทัดและคอลัมน์ในสคริปต์ จากสแต็กเฟรมดิบ
อนุมานชื่อเมธอด
สิ่งที่น่าประหลาดใจยิ่งกว่านั้นคือเวลาเกือบทั้งหมดหมดไปกับฟังก์ชัน JSStackFrame::GetMethodName()
ใน V8 แม้ว่าเราจะทราบจากการตรวจสอบก่อนหน้านี้ว่า JSStackFrame::GetMethodName()
ไม่ใช่คนแปลกหน้าในดินแดนแห่งปัญหาด้านประสิทธิภาพ ฟังก์ชันนี้จะพยายามคํานวณชื่อของเมธอดสําหรับเฟรมที่ถือว่าเป็นการเรียกใช้เมธอด (เฟรมที่แสดงการเรียกใช้ฟังก์ชันในรูปแบบ obj.func()
ไม่ใช่ func()
) การตรวจสอบโค้ดอย่างรวดเร็วพบว่าฟังก์ชันทํางานโดยการเรียกใช้การเรียกใช้ออบเจ็กต์และเชนโปรโตไทป์ทั้งหมด และมองหา
- พร็อพเพอร์ตี้ข้อมูลที่มี
value
เป็นfunc
ปิด หรือ - พร็อพเพอร์ตี้ตัวรับค่าที่
get
หรือset
เท่ากับการปิดfunc
แม้ว่าการอัปเดตนี้จะไม่ทำให้อุปกรณ์ช้าลง แต่ก็ไม่ได้อธิบายความช้าอย่างรุนแรงนี้ เราจึงเริ่มตรวจสอบตัวอย่างที่รายงานใน chromium:1069425 และพบว่ามีการรวบรวมสแต็กเทรซสําหรับงานแบบแอสซิงค์ รวมถึงสําหรับข้อความบันทึกที่มาจาก classes.js
ซึ่งเป็นไฟล์ JavaScript ขนาด 10 MiB เมื่อตรวจสอบอย่างละเอียดแล้ว พบว่านี่เป็นรันไทม์ Java บวกกับโค้ดแอปพลิเคชันที่คอมไพล์เป็น JavaScript บันทึกสแต็กมีเฟรมหลายเฟรมที่มีการเรียกใช้เมธอดในออบเจ็กต์ A
เราจึงคิดว่าควรทำความเข้าใจว่ากำลังจัดการกับออบเจ็กต์ประเภทใด
ดูเหมือนว่าคอมไพเลอร์ Java เป็น JavaScript จะสร้างออบเจ็กต์เดียวที่มีฟังก์ชัน 82,203 รายการ ซึ่งนี่เป็นเรื่องที่น่าสนมาก จากนั้นเรากลับไปที่ JSStackFrame::GetMethodName()
ของ V8 เพื่อดูว่ามีอะไรที่เราทำได้ง่ายๆ ไหม
- โดยวิธีทํางานคือจะค้นหา
"name"
ของฟังก์ชันเป็นพร็อพเพอร์ตี้ในออบเจ็กต์ก่อน หากพบ ก็จะตรวจสอบว่าค่าพร็อพเพอร์ตี้ตรงกับฟังก์ชันหรือไม่ - หากฟังก์ชันไม่มีชื่อหรือออบเจ็กต์ไม่มีพร็อพเพอร์ตี้ที่ตรงกัน ระบบจะเปลี่ยนไปใช้การค้นหาย้อนกลับโดยเรียกใช้พร็อพเพอร์ตี้ทั้งหมดของออบเจ็กต์และโปรโตไทป์
ในตัวอย่างนี้ ฟังก์ชันทั้งหมดเป็นแบบไม่ระบุชื่อและมีพร็อพเพอร์ตี้ "name"
ว่าง
A.SDV = function() {
// ...
};
ผลการค้นหาแรกคือ การค้นหาย้อนกลับแบ่งออกเป็น 2 ขั้นตอน (ดำเนินการสำหรับออบเจ็กต์เองและแต่ละออบเจ็กต์ในเชนโปรโตไทป์) ดังนี้
- ดึงข้อมูลชื่อของพร็อพเพอร์ตี้ที่นับทั้งหมดได้ทั้งหมด และ
- ทำการค้นหาพร็อพเพอร์ตี้ทั่วไปสำหรับชื่อแต่ละรายการ โดยทดสอบว่าค่าพร็อพเพอร์ตี้ที่ได้ตรงกับคำปิดที่เราต้องการหรือไม่
ดูเหมือนว่าจะเป็นวิธีที่ง่ายที่สุดแล้ว เนื่องจากการดึงข้อมูลชื่อต้องเรียกใช้พร็อพเพอร์ตี้ทั้งหมดอยู่แล้ว แทนที่จะทำ 2 รอบ - O(N) สำหรับการดึงข้อมูลชื่อและ O(N log(N)) สำหรับการทดสอบ - เราทําทุกอย่างได้ในการผ่านเพียงครั้งเดียวและตรวจสอบค่าพร็อพเพอร์ตี้โดยตรง ซึ่งทำให้ฟังก์ชันทั้งหมดทำงานได้เร็วขึ้นประมาณ 2-10 เท่า
ข้อมูลการค้นพบที่ 2 น่าสนใจยิ่งขึ้น แม้ว่าในทางเทคนิคแล้วฟังก์ชันเหล่านี้จะเป็นฟังก์ชันนิรนาม แต่เครื่องยนต์ V8 ก็ได้บันทึกสิ่งที่เราเรียกว่าชื่อที่อิงตามข้อมูลที่มีอยู่ไว้ให้ฟังก์ชันเหล่านั้น สําหรับลิเทอรัลฟังก์ชันที่ปรากฏทางด้านขวามือของการกำหนดในรูปแบบ obj.foo = function() {...}
โปรแกรมแยกวิเคราะห์ V8 จะจดจํา "obj.foo"
เป็นชื่อที่อิงตามบริบทสําหรับลิเทอรัลฟังก์ชัน ในกรณีของเรา หมายความว่าแม้ว่าจะไม่มีชื่อที่เหมาะสมที่จะค้นหาได้ แต่เราก็มีชื่อที่ใกล้เคียงพอ ในตัวอย่างนี้ A.SDV = function() {...}
เรามี "A.SDV"
เป็นชื่อที่อนุมาน และสามารถดึงชื่อพร็อพเพอร์ตี้จากชื่อที่อนุมานได้โดยมองหาจุดสุดท้าย จากนั้นค้นหาพร็อพเพอร์ตี้ "SDV"
ในออบเจ็กต์ วิธีนี้ได้ผลเกือบทุกกรณี โดยแทนที่การเรียกดูทั้งหมดที่สิ้นเปลืองทรัพยากรด้วยการค้นหาพร็อพเพอร์ตี้รายการเดียว การปรับปรุง 2 รายการนี้เป็นส่วนหนึ่งของ CL นี้ และช่วยลดการทํางานช้าลงอย่างมากสําหรับตัวอย่างที่รายงานใน chromium:1069425
Error.stack
เราอาจจบการแชทกันตรงนี้ แต่มีความผิดปกติเกิดขึ้นเนื่องจากเครื่องมือสำหรับนักพัฒนาเว็บไม่เคยใช้ชื่อเมธอดสำหรับเฟรมสแต็ก อันที่จริงแล้ว คลาส v8::StackFrame
ใน C++ API ไม่ได้แสดงวิธีเข้าถึงชื่อเมธอดด้วยซ้ำ ดังนั้นการโทรหา JSStackFrame::GetMethodName()
ในตอนแรกจึงดูไม่ถูกต้อง แต่เราจะใช้ (และแสดง) ชื่อเมธอดใน JavaScript Stack Trace API เท่านั้น ลองดูตัวอย่างง่ายๆ ต่อไปนี้ error-methodname.js
เพื่อทําความเข้าใจการใช้งานนี้
function foo() {
console.log((new Error).stack);
}
var object = {bar: foo};
object.bar();
ที่นี่เรามีฟังก์ชัน foo
ที่ติดตั้งภายใต้ชื่อ "bar"
ใน object
การแสดงผลของข้อมูลโค้ดนี้ใน Chromium จะแสดงผลลัพธ์ต่อไปนี้
Error
at Object.foo [as bar] (error-methodname.js:2)
at error-methodname.js:6
ในที่นี้ เราจะเห็นการค้นหาชื่อเมธอด: เฟรมสแต็กด้านบนสุดแสดงขึ้นเพื่อเรียกใช้ฟังก์ชัน foo
ในอินสแตนซ์ของ Object
ผ่านเมธอดชื่อ bar
ดังนั้นพร็อพเพอร์ตี้ error.stack
ที่ไม่เป็นไปตามมาตรฐานจึงใช้ JSStackFrame::GetMethodName()
เป็นจำนวนมาก และจากการทดสอบประสิทธิภาพพบว่าการเปลี่ยนแปลงของเราทําให้ทุกอย่างเร็วขึ้นอย่างมาก
แต่กลับไปที่หัวข้อเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome การที่ระบบประมวลผลชื่อเมธอดแม้ว่าจะไม่มีการใช้ error.stack
นั้นดูไม่ถูกต้อง ประวัติบางส่วนที่เป็นประโยชน์มีดังนี้ เดิมที V8 มีกลไก 2 กลไกที่แตกต่างกันในการรวบรวมและแสดงสแต็กเทรซสําหรับ API 2 รายการที่อธิบายไว้ข้างต้น (v8::StackFrame
API ของ C++ และ API สแต็กเทรซของ JavaScript) การมี 2 วิธีที่แตกต่างกันในการทํางานแบบเดียวกัน (โดยประมาณ) มีแนวโน้มที่จะเกิดข้อผิดพลาดและมักทําให้เกิดความไม่สอดคล้องและข้อบกพร่อง ดังนั้นในช่วงปลายปี 2018 เราได้เริ่มโปรเจ็กต์เพื่อหาทางออกเดียวสําหรับการบันทึกสแต็กเทรซ
โปรเจ็กต์ดังกล่าวประสบความสําเร็จอย่างมากและลดจํานวนปัญหาที่เกี่ยวข้องกับการเก็บรวบรวมสแต็กเทรซได้อย่างมาก ข้อมูลส่วนใหญ่ที่ระบุผ่านพร็อพเพอร์ตี้ error.stack
ที่ไม่เป็นไปตามมาตรฐานได้รับการคํานวณแบบล่าช้าและเฉพาะเมื่อจําเป็นเท่านั้น แต่เราใช้เทคนิคเดียวกันกับออบเจ็กต์ v8::StackFrame
ในการรีแฟกทอริงด้วย ระบบจะคํานวณข้อมูลทั้งหมดเกี่ยวกับเฟรมสแต็กเมื่อมีการเรียกใช้เมธอดใดก็ตามในเฟรมนั้นเป็นครั้งแรก
โดยทั่วไปแล้วการดำเนินการนี้จะปรับปรุงประสิทธิภาพ แต่น่าเสียดายที่การดำเนินการนี้กลับขัดแย้งกับวิธีใช้ออบเจ็กต์ C++ API เหล่านี้ใน Chromium และเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ โดยเฉพาะอย่างยิ่งเมื่อเราเปิดตัวคลาส v8::internal::StackFrameInfo
ใหม่ซึ่งมีข้อมูลทั้งหมดเกี่ยวกับสแต็กเฟรมที่แสดงผ่าน v8::StackFrame
หรือผ่าน error.stack
เราจะคำนวณชุดข้อมูลย่อยของข้อมูลที่ได้จากทั้ง 2 API เสมอ ซึ่งหมายความว่าสำหรับการใช้งาน v8::StackFrame
(และโดยเฉพาะอย่างยิ่งสำหรับ DevTools) เราจะคำนวณชื่อเมธอดด้วยทันทีที่มีคำขอข้อมูลเกี่ยวกับสแต็กเฟรม ปรากฏว่าเครื่องมือสำหรับนักพัฒนาเว็บจะขอข้อมูลแหล่งที่มาและสคริปต์ทันทีเสมอ
จากการค้นพบนี้ เราจึงสามารถปรับโครงสร้างและลดความซับซ้อนของการแสดงสแต็กเฟรมได้อย่างมาก และทำให้ทำงานได้น้อยลงกว่าเดิม เพื่อให้การใช้งานใน V8 และ Chromium เสียค่าใช้จ่ายเฉพาะสำหรับการประมวลผลข้อมูลที่ขอเท่านั้น การดำเนินการนี้ช่วยเพิ่มประสิทธิภาพของเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์และกรณีการใช้งานอื่นๆ ของ Chromium อย่างมาก ซึ่งต้องใช้ข้อมูลเพียงเศษเสี้ยวเกี่ยวกับเฟรมสแต็ก (โดยพื้นฐานแล้วคือชื่อสคริปต์และตำแหน่งแหล่งที่มาในรูปแบบออฟเซตบรรทัดและคอลัมน์) และเปิดโอกาสให้ปรับปรุงประสิทธิภาพได้มากขึ้น
ชื่อฟังก์ชัน
เมื่อการแยกส่วนโค้ดข้างต้นเสร็จสิ้นแล้ว ค่าใช้จ่ายเพิ่มเติมของการทำสัญลักษณ์ (เวลาที่ใช้ใน v8_inspector::V8Debugger::symbolize
) ลดลงเหลือประมาณ 15% ของเวลาในการดำเนินการโดยรวม และเราเห็นได้ชัดเจนขึ้นว่า V8 ใช้เวลาไปกับอะไรบ้างเมื่อ (รวบรวมและ) ทำสัญลักษณ์เฟรมสแต็กเพื่อใช้งานใน DevTools
สิ่งแรกที่โดดเด่นคือต้นทุนสะสมสําหรับการคํานวณจํานวนแถวและคอลัมน์ ส่วนที่เป็นค่าใช้จ่ายสูงจริงๆ คือการคำนวณออฟเซ็ตของตัวละครภายในสคริปต์ (อิงตามออฟเซ็ตของไบต์โค้ดที่เราได้รับจาก V8) และปรากฏว่าเนื่องจากการรีแฟกทอริงข้างต้น เราทําการคำนวณนี้ 2 ครั้ง 1 ครั้งเมื่อคํานวณหมายเลขบรรทัด และอีก 1 ครั้งเมื่อคํานวณหมายเลขคอลัมน์ การแคชตําแหน่งแหล่งที่มาในอินสแตนซ์ v8::internal::StackFrameInfo
ช่วยแก้ปัญหานี้ได้อย่างรวดเร็วและนํา v8::internal::StackFrameInfo::GetColumnNumber
ออกจากโปรไฟล์ทั้งหมด
สิ่งที่น่าสนใจกว่าคือ v8::StackFrame::GetFunctionName
สูงอย่างน่าประหลาดใจในโปรไฟล์ทั้งหมดที่เราดู เมื่อเจาะลึกเรื่องนี้ เราพบว่าการคำนวณชื่อที่จะแสดงสำหรับฟังก์ชันในสแต็กเฟรมในเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์นั้นไม่จำเป็น
- ก่อนอื่นให้มองหาพร็อพเพอร์ตี้
"displayName"
ที่ไม่เป็นไปตามมาตรฐาน และหากพร็อพเพอร์ตี้ดังกล่าวให้พร็อพเพอร์ตี้ข้อมูลที่มีค่าสตริง เราจะใช้พร็อพเพอร์ตี้นั้น - ไม่เช่นนั้นให้กลับไปค้นหาพร็อพเพอร์ตี้
"name"
มาตรฐาน และตรวจสอบอีกครั้งว่าพร็อพเพอร์ตี้ดังกล่าวให้พร็อพเพอร์ตี้ข้อมูลที่มีค่าเป็นสตริงหรือไม่ - และสุดท้ายจะกลับไปใช้ชื่อการแก้ไขข้อบกพร่องภายในที่แยกแยะโดยโปรแกรมแยกวิเคราะห์ V8 และจัดเก็บไว้ในนิพจน์ฟังก์ชัน
เราได้เพิ่มพร็อพเพอร์ตี้ "displayName"
ไว้เป็นวิธีแก้ปัญหาสำหรับพร็อพเพอร์ตี้ "name"
ในอินสแตนซ์ Function
ที่เป็นแบบอ่านอย่างเดียวและไม่สามารถกําหนดค่าได้ใน JavaScript แต่ไม่เคยมีการทำให้เป็นมาตรฐานและไม่ได้ใช้งานอย่างแพร่หลาย เนื่องจากเครื่องมือสําหรับนักพัฒนาซอฟต์แวร์เบราว์เซอร์ได้เพิ่มการอนุมานชื่อฟังก์ชันที่ทํางานได้ 99.9% ของกรณี นอกจากนี้ ES2015 ยังทำให้พร็อพเพอร์ตี้ "name"
ในอินสแตนซ์ Function
กำหนดค่าได้ จึงไม่จำเป็นต้องใช้พร็อพเพอร์ตี้ "displayName"
พิเศษอีกต่อไป เนื่องจากการค้นหาเชิงลบสำหรับ "displayName"
มีค่าใช้จ่ายค่อนข้างสูงและไม่จำเป็นจริงๆ (ES2015 เปิดตัวไปกว่า 5 ปีแล้ว) เราจึงตัดสินใจนำการรองรับพร็อพเพอร์ตี้ fn.displayName
ที่ไม่เป็นไปตามมาตรฐานออกจาก V8 (และเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์)
เมื่อการค้นหาเชิงลบของ "displayName"
เสร็จสิ้นแล้ว ระบบได้นำต้นทุนของ v8::StackFrame::GetFunctionName
ออกครึ่งหนึ่ง ส่วนที่เหลือจะนำไปที่การค้นหาพร็อพเพอร์ตี้ "name"
ทั่วไป แต่โชคดีที่เรามีตรรกะบางอย่างอยู่แล้วเพื่อหลีกเลี่ยงการค้นหาพร็อพเพอร์ตี้ "name"
ในอินสแตนซ์ Function
(ที่ไม่ได้แตะต้อง) ซึ่งเราเปิดตัวใน V8 เมื่อสักครู่เพื่อทําให้ Function.prototype.bind()
ทำงานได้เร็วขึ้น เราได้พอร์ตการตรวจสอบที่จําเป็น ซึ่งช่วยให้เราข้ามการค้นหาทั่วไปที่มีค่าใช้จ่ายสูงตั้งแต่แรกได้ ผลที่ได้คือ v8::StackFrame::GetFunctionName
ไม่แสดงในโปรไฟล์ใดๆ ที่เราพิจารณาอีกต่อไป
บทสรุป
การปรับปรุงข้างต้นช่วยลดค่าใช้จ่ายเพิ่มเติมของ DevTools ในแง่ของสแต็กเทรซได้อย่างมาก
เราทราบดีว่ายังมีการปรับปรุงที่เป็นไปได้อีกมากมาย เช่น ค่าใช้จ่ายเพิ่มเติมเมื่อใช้ MutationObserver
ยังคงสังเกตได้อยู่ตามที่รายงานใน chromium:1077657 แต่ตอนนี้เราได้แก้ไขปัญหาหลักๆ แล้ว และอาจกลับมาดำเนินการเพิ่มเติมในอนาคตเพื่อปรับปรุงประสิทธิภาพการแก้ไขข้อบกพร่องให้ดียิ่งขึ้น
ดาวน์โหลดแชแนลตัวอย่าง
ลองใช้ Chrome Canary, Dev หรือ เบต้า เป็นเบราว์เซอร์สำหรับนักพัฒนาซอฟต์แวร์เริ่มต้น ช่องทางเวอร์ชันตัวอย่างเหล่านี้จะช่วยให้คุณเข้าถึงฟีเจอร์ล่าสุดของ DevTools, ทดสอบ API ของแพลตฟอร์มเว็บที่ล้ำสมัย และช่วยคุณค้นหาปัญหาในเว็บไซต์ได้ก่อนที่ผู้ใช้จะพบ
ติดต่อทีมเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome
ใช้ตัวเลือกต่อไปนี้เพื่อพูดคุยเกี่ยวกับฟีเจอร์ใหม่ การอัปเดต หรือสิ่งอื่นๆ ที่เกี่ยวข้องกับเครื่องมือสำหรับนักพัฒนาเว็บ
- ส่งความคิดเห็นและคำขอฟีเจอร์ถึงเราได้ที่ crbug.com
- รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บโดยใช้ ตัวเลือกเพิ่มเติม > ความช่วยเหลือ > รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บในเครื่องมือสำหรับนักพัฒนาเว็บ
- ทวีตถึง @ChromeDevTools
- แสดงความคิดเห็นในวิดีโอ YouTube เกี่ยวกับข่าวสารใน DevTools หรือวิดีโอ YouTube เกี่ยวกับเคล็ดลับใน DevTools