CPU Flamegraph Visualizer: เปลี่ยน Stack Trace ให้เป็น Flame Graph แบบอินเทอร์แอกทีฟ
CPU Flamegraph Visualizer เปลี่ยนผลลัพธ์ perf script และ py-spy ให้เป็น SVG flame graph แบบอินเทอร์แอกทีฟในเบราว์เซอร์ ซูมเข้า frame ที่กิน CPU ค้นหาตามชื่อฟังก์ชัน และหาคอขวดได้โดยไม่ต้องติดตั้งอะไรเลย
Table of Contents
นักพัฒนาทุกคนเคยเจอกำแพงเดียวกัน: บริการช้าลง CPU ทำงานเต็มร้อย แล้วมีคนพูดว่า "ก็จัดการ profile สิ" คุณรัน perf หรือ py-spy ได้ stack trace มาหลายพันบรรทัด แล้วจึงพบว่าไม่มีวิธีที่ดีนักในการอ่านมัน ช่องว่างระหว่างการเก็บ profile กับการทำความเข้าใจมัน คือสิ่งที่ CPU Flamegraph Visualizer ถูกสร้างขึ้นมาเพื่อปิดลง
เครื่องมือนี้รับ folded stack หรือผลลัพธ์ดิบจาก profiler — ทั้ง perf script บน Linux และ dump จาก py-spy — แล้วเรนเดอร์เป็น SVG flame graph แบบอินเทอร์แอกทีฟในเบราว์เซอร์ของคุณ ไม่ต้องติดตั้งอะไร ไม่มีข้อมูลถูกอัปโหลด ภายในไม่กี่วินาทีคุณซูมเข้าไปดู frame ใดก็ได้ ค้นหาฟังก์ชันด้วยชื่อ และอ่านยอด sample รวมต่อฟังก์ชันที่บอกชัดเจนว่า CPU หมดเวลาไปกับอะไร
ไม่ว่าคุณกำลังไล่ regex ที่ร้อนแรง วินิจฉัยแรงกดดันจาก garbage collection หรือแค่อยากรู้ว่าโปรแกรมใช้เวลาไปกับอะไรบ้าง flame graph จะเปลี่ยน stack sample หลายพันรายการให้เป็นภาพเดียวที่นำไปลงมือแก้ได้จริง บทความนี้จะพาไล่ตั้งแต่ขั้นตอนการใช้งาน วิธีอ่านกราฟ ไปจนถึงนิสัยที่ทำให้การ profiling ให้ผลลัพธ์จริงแทนที่จะน่ากลัว
ทำไมต้องใช้ CPU Flamegraph Visualizer?
- Profile ได้โดยไม่ต้องติดตั้ง: เครื่องมือ flame graph แบบเดิมต้องโคลน repository รันสคริปต์ Perl และปั่นหัวกับ dependency ที่นี่แค่วาง stack ลงในหน้าเว็บก็ได้กราฟเลย ใช้ได้ทั้งบนเครื่องทำงานที่ล็อกสิทธิ์เข้มงวดและเครื่องส่วนตัว
- ใช้กับเครื่องมือที่มีอยู่แล้ว: ถ้าคุณผลิต perf script output บน Linux หรือ py-spy output สำหรับ Python ได้ คุณมีข้อมูลครบแล้ว ไม่ต้องติดตั้ง agent ไม่ต้องใส่ SDK ไม่ต้องแก้โค้ดแม้แต่บรรทัดเดียว
- ซูมและค้นหาแบบอินเทอร์แอกทีฟ: ภาพนิ่งของ profile ใหญ่ ๆ แทบใช้ไม่ได้ คลิก frame เพื่อซูมเข้า subtree ของมัน และค้นหาด้วยชื่อฟังก์ชันเพื่อไฮไลต์ทุก frame ที่ตรงกันพร้อมกันทั้งกราฟ
- ยอด sample รวมต่อฟังก์ชัน: ตัวเลขที่ชัดเจนดีกว่าการเดาความกว้างด้วยตา คุณบอกได้อย่างมั่นใจใน code review หรือ incident call ว่า "ฟังก์ชันนี้กิน sample ไป 30% ของทั้งหมด"
- ข้อมูลอยู่กับเครื่องคุณ: profile มักมีชื่อฟังก์ชันภายในและ path ของ endpoint การเรนเดอร์เกิดขึ้นฝั่ง client ล้วน ๆ stack ที่ละเอียดอ่อนจึงไม่หลุดออกจากเครื่องของคุณ
- ใช้เวลาไม่กี่นาที ไม่ใช่หลายชั่วโมง: ระยะทางจาก "เซิร์ฟเวอร์ช้า" ไปจนถึง "เจอ frame ที่ร้อนแล้ว" สั้นลงจากช่วงบ่ายทั้งช่วงของการเซ็ตอัปเครื่องมือ เหลือแค่สองสามนาที
ฟีเจอร์หลัก
| ฟีเจอร์ | ทำอะไรได้ |
|---|---|
| รับ folded stack | วาง stack รูปแบบคลาสสิก frame;frame;frame count แล้วเรนเดอร์ได้ทันที |
| รองรับผลลัพธ์ดิบจาก profiler | วาง perf script หรือ py-spy output ดิบ ๆ แล้วเครื่องมือ fold stack ให้เอง |
| เรนเดอร์ SVG แบบอินเทอร์แอกทีฟ | ทุก frame เป็นสี่เหลี่ยมที่คลิกและชี้ได้ พร้อมชื่อและสัดส่วน sample ของมัน |
| ซูมเข้า frame | คลิก frame ใดก็ได้เพื่อขยาย subtree เป็นความกว้างเต็ม แล้วรีเซ็ตเพื่อซูมกลับออก |
| ค้นหาตามชื่อ | ไฮไลต์ทุก frame ที่ตรงกับฟังก์ชัน เห็น call path ทั้งหมดของมันพร้อมกัน |
| ยอด sample ต่อฟังก์ชัน | เห็นจำนวน sample รวมต่อฟังก์ชัน สรุป profile ทั้งหมดในลิสต์เดียว |
รายละเอียดที่ควรรู้เพิ่มเติม:
- การ fold — การรวม sample ดิบหลายพันรายการให้เป็นบรรทัดที่รวมกันแล้ว — คือขั้นตอนที่คนติดบ่อยที่สุดกับเครื่องมือ command line และเครื่องมือนี้จัดการให้ทั้งรูปแบบ perf script และ py-spy
- ผลลัพธ์เป็น SVG จึง screenshot ได้คมชัดทุกระดับ ใช้ลงรายงาน incident, pull request และสไลด์ได้เลย
- profile ที่มี unique stack หลายพันรายการก็เรนเดอร์ได้สบาย จับ production นาน 30 วินาทีก็ไม่ทำให้หน้าเว็บอึดอัด
วิธีใช้งาน CPU Flamegraph Visualizer
- เก็บ profile ก่อน บน Linux รัน perf record โดยเปิด call graph นาน 15-30 วินาทีภายใต้โหลดจริง แล้วรัน perf script เพื่อ dump sample ออกมา ส่วน Python ใช้ py-spy record หรือ py-spy dump ก็ได้ข้อมูลที่เทียบเท่ากัน
- วาง stack ลงไป เปิด CPU Flamegraph Visualizer วาง folded stack หรือ perf script / py-spy output ดิบลงในช่อง input แล้วสั่งเรนเดอร์
- อ่านกราฟ เริ่มจากแถวล่างสุด — main หรือจุดเริ่มต้นของ runtime — แล้วมองขึ้นไปด้านบน หอที่กว้างที่สุดคือจุดที่ sample กระจุกตัว ส่วนกล่องด้านบนบอกว่า call เหล่านั้นไปจบที่ไหน
- ซูมเข้า frame ที่ร้อน คลิก frame กว้าง ๆ ที่น่าสงสัย subtree จะขยายเต็มความกว้าง ทำให้ leaf function อย่าง regex_exec ที่ร้อนแรงหรือตัว serialize ที่ชวนปวดหัวเด่นขึ้นมาทันที
- ลงมือแก้ที่กล่องที่กว้างที่สุด ทำ cache ย้ายงานที่ซ้ำซ้อนออกนอก loop แก้ pattern หรือรวม call เป็นชุด แล้ว profile ซ้ำเพื่อยืนยันว่ากล่องนั้นเล็กลงจริง
อ่าน Flame Graph ให้เป็น โดยไม่ต้องกลัว
Flame graph ดูน่ากลัวจนกว่าใครสักคนจะบอกกฎของมัน หลังจากนั้นมันคือกราฟด้าน performance ที่เป็นมิตรที่สุดเท่าที่เคยมีมา
แกน x ไม่ใช่เวลา แต่เป็นสัดส่วนของ sample ถ้า handleRequest กินความกว้าง 40% แปลว่ามันอยู่บน stack ในช่วงราว ๆ 40% ของ sample ทั้งหมดในการจับครั้งนั้น และเพราะ frame ถูกเรียงตามตัวอักษรภายในแต่ละระดับ ไม่ใช่ตามเวลา จึงอย่าอ่านกราฟจากซ้ายไปขวาเหมือนไทม์ไลน์เด็ดขาด
แกน y คือความลึกของ call stack แต่ละแถวคือ frame ที่ลึกขึ้นหนึ่งชั้น แถวล่างสุดคือรากของทุก stack — มักเป็น main — และแต่ละชั้นด้านบนคือสิ่งที่ frame นั้นเรียกใช้ หอสูง ๆ แปลว่า call chain ลึก ส่วนกล่องบนสุดคือ leaf หรือฟังก์ชันที่กำลังทำงานจริง ๆ ในตอนที่ sample ถูกเก็บ
ความกว้างคือต้นทุน ประโยคที่ใช้ได้จริงที่สุดในการอ่าน flame graph คือ frame ที่กว้างที่สุดกิน CPU มากที่สุด กล่องกว้างที่มีลูกหลายตัวคือการกระจายต้นทุนให้ callee หลายตัว ส่วนกล่องกว้างที่มีลูกตัวเดียวยักษ์ ชี้ตรง ๆ ว่า call site เดียวนั่นแหละคือปัญหา
ซูมและ search คือของจำเป็น ในมุมมองเต็มจอ frame สำคัญอาจกว้างแค่ไม่กี่พิกเซล คลิก frame เพื่อขยาย subtree ของมันให้เต็มจอ แล้วลอง search ด้วยชื่อ พิมพ์ gc แล้วคุณจะเห็นทันทีว่า sample ไหลลงสู่ garbage collection กี่เปอร์เซ็นต์ของทั้งหมด
รูปแบบ folded stack ง่ายมาก แต่ละบรรทัดคือหนึ่ง call path ที่รวมแล้ว: frame คั่นด้วยเซมิโคลอน เว้นวรรค ตามด้วยจำนวน sample อย่างเช่น main;handleRequest;parseQuery;regex_exec 87 หมายความว่ามี 87 sample ที่วิ่งตาม stack นี้พอดี เครื่องมืออ่านรูปแบบนี้ได้ตรง ๆ และยัง fold ผลลัพธ์ดิบจาก perf script และ py-spy ให้เป็นรูปแบบเดียวกันให้อีกด้วย
เรื่อง icicle orientation บางเครื่องมือวาดกราฟให้โตจากบนลงล่าง เรียกว่ามุมมอง icicle ส่วน flame graph คลาสสิกโตจากล่างขึ้นบน ความหมายของทั้งสองแบบเหมือนกันทุกประการ แค่เช็กทิศทางให้ชัดก่อนจะพูดว่า "ขึ้นไปตาม stack" ในที่ประชุม
กรณีใช้งานจริง
ตามหา Regex ที่กิน CPU
จับ profile ของ API service ดู แล้วเจอ frame regex_exec กว้างผิดปกติอยู่ใต้ parseQuery กิน sample ไปราวหนึ่งในสามของทั้งหมด ซูมเข้าไปยืนยัน call path แล้วเอา pattern กลับไปดูในโค้ด ตัวการมักเป็น nested quantifier หรือ pattern ที่ถูก compile ใหม่ทุกครั้งที่มี request เข้ามา ลอง precompile หรือทำให้ pattern ง่ายลง แล้วยืนยันด้วยการจับ profile รอบใหม่ว่า frame นั้นเล็กลงจริง ถ้าต้องการเทียบ pattern หลายตัวอย่างปลอดภัยก่อน ใช้ Regex Performance Tester ที่ลิงก์ไว้ด้านล่างได้เลย
วินิจฉัยแรงกดดัน GC ผ่าน gc frame
Search คำว่า gc ใน profile ของ Python หรือ JVM กราฟจะบอกได้ทั้งว่า collector กิน CPU ไปเท่าไรและ call path ไหน allocate หนักที่สุด หอกว้าง ๆ ที่จบลงที่ฟังก์ชัน allocation เผยให้เห็น hot spot ที่สร้าง object ขึ้นมาแล้วทิ้ง: copy โครงสร้างข้อมูลใหญ่ทุก request, ต่อ string กันใน loop หรือ deserialize ข้อมูลทั้งก้อนเพื่อเอาแค่สองสามฟิลด์ ลด allocation ลง ดูสัดส่วน gc ที่หดตัวลง แล้วพิสูจน์ด้วย profile รอบที่สอง
เทียบก่อนและหลังการปรับปรุงประสิทธิภาพ
Flame graph ทำให้การ optimize กลายเป็นเรื่องที่วัดผลได้ จับ profile หนึ่งรอบ เก็บยอดต่อฟังก์ชันไว้ แก้โค้ด แล้วจับซ้ำภายใต้โหลดเดิม frame กว้าง ๆ ควรหดหายเห็นได้ชัด และคุณจะได้ตัวเลขลง changelog ว่า parseQuery ลดจาก 31% เหลือ 4% ของ sample ถ้าไม่มีการจับรอบที่สอง คุณแค่กำลังเดา แต่ถ้ามี คุณมีหลักฐาน
สอนวิธีคิดเรื่องประสิทธิภาพ
เพราะเครื่องมือนี้ไม่ต้องติดตั้งอะไรเลย มันจึงเหมาะกับการใช้สอนมาก ยื่นโปรแกรมที่ตั้งใจทำให้ช้าให้เด็กจบใหม่ ให้เขาจับ profile เอง แล้วปล่อยให้สำรวจกราฟ: ให้เดาก่อนว่าเวลาหมดไปกับอะไร แล้วค่อยเช็กคำตอบ การเรียนรู้ว่า "เริ่มจากกล่องที่กว้างที่สุด" ชนะสัญชาตญาณเสมอ เป็นบทเรียนที่ใช้ได้ทั้งอาชีพ และกราฟแบบอินเทอร์แอกทีฟช่วยให้จำได้นานกว่าสไลด์เสียอีก
แนวปฏิบัติที่ดี
- จับ profile ภายใต้โหลดจริง profile ของ service ที่ว่างงานบอกเรื่องตัว profiler มากกว่าคอขวดของคุณ ทำ traffic ให้ใกล้เคียง production ก่อนเสมอ
- จับนานพอควร สองสามวินาทีคือ noise ช่วง 15-60 วินาทีภายใต้โหลดถึงจะทำให้ความกว้างของ frame มีความหมาย
- มองหาภาพซ้ำ ๆ ไม่ใช่อาการแวบเดียว frame กว้าง ๆ ที่โผล่ทุกรอบการจับคือตัวกำหนดต้นทุนจริง ส่วนอาการแวบเดียวไม่ค่อยสำคัญนัก
- ยืนยันการแก้ด้วยการจับรอบที่สอง profile แก้ แล้ว profile ซ้ำภายใต้เงื่อนไขเดิม คู่กราฟนั้นคือหลักฐานของคุณ
- search ก่อนสรุป พิมพ์ gc ในช่องค้นหาก่อนตัดสินว่า garbage collection บริสุทธิ์ เพราะ frame ที่บางเฉียบรวมกันอาจมีน้ำหนักมากกว่าที่คิด
- เก็บ capture เอาไว้ โฟลเดอร์ folded stack ที่มีวันที่กำกับคือประวัติ performance ของ service ที่ตัวคุณในอนาคตจะต้องขอบคุณ
ครั้งต่อไปที่ service ร้อนจนแทบไหม้ ข้ามการไปเสียเวลาเซ็ตอัปเครื่องมือ เปิด CPU Flamegraph Visualizer วาง stack ของคุณลงไป แล้วปล่อยให้กล่องที่กว้างที่สุดบอกจุดที่ควรเริ่มมอง การ profiling ควรเป็นนิสัย ไม่ใช่ทางสุดท้าย — และเมื่อไม่ต้องติดตั้งอะไรเลย ก็ไม่เหลือข้ออ้างให้หาแล้ว
เครื่องมือที่เกี่ยวข้องที่คุณอาจสนใจ:
- Regex Performance Tester — ทดสอบความเร็ว regex pattern ก่อนที่มันจะไปอยู่ใน hot path ของคุณ
- Log Viewer — จับคู่สิ่งที่ flame graph แสดงกับสิ่งที่ log ของคุณบันทึกไว้
- Cron Gap Simulator — เช็กตารางเวลา cron ที่คุม batch job ของคุณให้แน่ใจ
ขอให้สนุกกับการ profiling!
คำถามที่พบบ่อย
ถ: ข้อมูล profiling ของฉันถูกอัปโหลดขึ้นเซิร์ฟเวอร์หรือเปล่า? ตอบ: ไม่เลย การ parse, fold และเรนเดอร์ทั้งหมดเกิดขึ้นในเบราว์เซอร์ของคุณ stack ที่วางลงไปจึงไม่เคยออกจากเครื่อง ซึ่งสำคัญมากเพราะ profile มักมีชื่อฟังก์ชันภายในและชื่อ endpoint ที่ไม่ควรเผยแพร่
ถ: รองรับ input รูปแบบไหนบ้าง? ตอบ: สองแบบ คือ folded stack ที่ fold มาแล้ว (frame คั่นด้วยเซมิโคลอน ตามด้วยจำนวน sample ท้ายบรรทัด) และผลลัพธ์ดิบจาก profiler โดยตรง ได้แก่ perf script output และ py-spy output วางแบบไหนก็ได้ เครื่องมือจัดการการ fold ให้เองทั้งหมด
ถ: กราฟเต็มไปด้วยกล่องจิ๋ว ควรเริ่มจากตรงไหน? ตอบ: คลิก frame ที่กว้างที่สุดในแถวล่างสุดเพื่อซูม การซูมจะขยาย subtree นั้นให้เต็มความกว้าง เปลี่ยนแถบบาง ๆ ที่อ่านไม่ออกให้กลายเป็นกล่องที่อ่านได้ ถ้ามีตัวต้องสงสัยอยู่แล้ว ใช้การค้นหาด้วยชื่อฟังก์ชันแทนก็ได้เช่นกัน
ถ: ใช้กับภาษาอื่นนอกจาก C และ Python ได้ไหม? ตอบ: ได้ ตราบใดที่คุณผลิต folded stack ได้ หรือแปลงผลลัพธ์จาก profiler ให้อยู่ในรูปแบบนั้นได้ sampling profiler ที่แสดง stack เป็นบรรทัดแบบ frame;frame;frame count — ทั้ง Go, Rust, Node.js และ Java ผ่าน async-profiler — จะเรนเดอร์ได้ปกติทั้งหมด