ภาพวาดมาสคอตขนมปังกลมต้นฉบับใช้แว่นขยายส่องแถบกระดาษจากเครื่องที่ชี้กองงานหนักที่สุด

Bun 1.4 อ่าน CPU Profile เป็น Markdown: หา Hot Function ก่อนเดา

น้องกายEngineering

เวลา endpoint ช้า เรามักเริ่มจาก function ที่ “ดูน่าจะแพง” แล้วแก้ก่อนวัด ผลคือ code ซับซ้อนขึ้นแต่ latency เท่าเดิม CPU profile ช่วยบอกว่า sampler พบ process อยู่ใน function ใดบ่อยเพียงใด เหมือนถ่ายรูปครัวทุกเสี้ยววินาทีแล้วนับว่าพ่อครัวติดอยู่หน้าเตาไหนนานที่สุด

Bun 1.4 เพิ่ม --cpu-prof-md ให้เขียนสรุป profile เป็น Markdown ที่อ่านและแนบ PR ได้โดยตรง ไม่ต้องเปิด DevTools ก่อน เหมาะกับการคัดกรอง bottleneck และเก็บหลักฐานก่อน/หลัง แต่ยังไม่แทน flame graph เมื่อ call stack ซับซ้อนมาก

WHY: เวลารวมบอกปลายทาง แต่ไม่บอกต้นเหตุ

console.time วัด block ที่เราครอบไว้ หากไม่รู้ว่าควรครอบตรงไหนก็ต้องเดา CPU profiler สุ่มเก็บ stack ตลอด process จึงเห็นทั้ง function ที่เราสงสัยและงานข้างเคียง เช่น parser, serializer หรือ garbage collection

อย่างไรก็ตาม “เปอร์เซ็นต์สูง” ไม่ได้แปลว่า bug เสมอ Function หลักของงานคำนวณย่อมใช้ CPU มาก สิ่งที่ต้องถามคือเวลานั้นสอดคล้องกับคุณค่าที่ได้หรือไม่ และลดได้โดยเปลี่ยน algorithm, ลดจำนวนข้อมูล หรือย้าย cache มากกว่าปรับ micro-optimization หรือเปล่า

HOW: สร้าง Workload ที่มี Hot Function ชัดเจน

บันทึกตัวอย่างเป็น profile.ts ฟังก์ชัน checksum ตั้งใจทำ arithmetic ซ้ำเพื่อให้ profiler มีสัญญาณพอ ส่วน assertion ป้องกัน compiler หรือการแก้โค้ดในอนาคตทำให้งานหายไปโดยไม่รู้ตัว

function checksum(iterations: number): number {
  let value = 0;
  for (let index = 0; index < iterations; index += 1) {
    value = (value + Math.imul(index, 31)) >>> 0;
  }
  return value;
}

const iterations = Number(Bun.argv[2] ?? "20000000");
const result = checksum(iterations);

if (!Number.isInteger(result) || result < 0) {
  throw new Error(`invalid checksum: ${result}`);
}
console.log({ iterations, result });

จากนั้นรัน profiler โดยแยก directory ของ artifact ไม่ให้ปนกับ source

bun --cpu-prof-md --cpu-prof-dir ./profiles profile.ts

Bun จะสร้างไฟล์ Markdown ชื่อขึ้นต้นด้วย CPU. ใน directory นั้น การตรวจบทความนี้รัน workload 20 ล้านรอบจริงและพบ checksum เป็น hot function อันดับต้น ๆ ซึ่งตรงกับสิ่งที่โปรแกรมทำ จึงมีความหมายกว่าการตรวจเพียงว่าไฟล์ถูกสร้าง

อ่านรายงานอย่างไม่หลงตัวเลข

เริ่มจาก total duration และ sample count ก่อน รายงานสั้นเกินไปอาจมี sample น้อยจนผลแกว่ง จากนั้นดู self time ซึ่งเป็นเวลาที่อยู่ใน function นั้นเอง เทียบกับ total time ซึ่งรวมลูกที่ถูกเรียกต่อ หาก total สูงแต่ self ต่ำ ต้นทุนจริงอาจอยู่ในลูก

รัน workload เดิมอย่างน้อยหลายครั้งบนเครื่องและภาระใกล้เคียงกัน อย่าเปรียบเทียบเปอร์เซ็นต์จาก input คนละชุด เพราะ profiler แสดงสัดส่วนของงานทั้ง process เมื่อ function หนึ่งเร็วขึ้น เปอร์เซ็นต์ของอีก function อาจสูงขึ้นทั้งที่ใช้เวลาเท่าเดิม

Markdown เหมาะกับ CI artifact หรือ comment ใน PR แต่ profile อาจมีชื่อไฟล์, path และชื่อ function ภายใน อย่าเผยแพร่สู่ที่สาธารณะโดยไม่ตรวจข้อมูล หากต้องไล่ call path ลึก ให้เก็บรูปแบบ profile ปกติด้วย --cpu-prof แล้วเปิดในเครื่องมือที่แสดง flame graph

Experiment ที่ดีต้องมี Before และ After

ก่อนแก้ให้จด command, runtime version, input size, duration และ hot functions หลังแก้รัน command เดิม แล้วตรวจทั้ง performance และผลลัพธ์เชิงธุรกิจ การลด CPU แล้ว checksum หรือ response ผิดไม่ใช่ optimization

สำหรับ server อย่า profile production ยาวโดยไม่ประเมิน overhead เริ่มจาก staging หรือช่วงสั้นที่ควบคุมได้ และเก็บ sample ภายใต้ traffic ที่เป็นตัวแทน เพราะ endpoint เร็วที่ไม่มี traffic จริงอาจไม่บอก bottleneck ที่ผู้ใช้พบ

Recap

--cpu-prof-md ลดระยะจาก “รู้สึกว่าช้า” ไปสู่รายงานที่อ่านและ review ได้ ใช้ workload คงที่, ตรวจว่ารายงานชี้ hot function ที่คาดได้ และเปรียบเทียบก่อน/หลังด้วยเงื่อนไขเดียวกัน

พรุ่งนี้เลือกคำสั่งที่ใช้ CPU สูงหนึ่งตัว เก็บ profile 30–60 วินาที แล้วเขียนสมมติฐานเพียงข้อเดียวจากหลักฐาน ก่อนแตะ code วิธีนี้มักประหยัดเวลากว่าการ optimize ตามสัญชาตญาณครับ