อัปเดตล่าสุดเมื่อ

Framework ที่ช้าที่สุด กลับดึงกำลังไฟน้อยที่สุด


สวัสดีครับ กลับมาเจอกับการจับ MacBook Air M5 มาทรมานอีกครั้ง 😅

คราวที่แล้ว ผมลองวัดว่า Bun HTTP Framework แปดตัวเร็วแค่ไหน สิ่งที่น่าสนใจที่สุดกลับไม่ใช่ว่าใครเร็วที่สุด แต่เป็นการค้นพบว่า Benchmark ของผมกำลังวัด ความร้อนของเครื่อง มากกว่าวัดตัว Framework

รอบนี้ผมเลยมีคำถามต่อว่า:

ถ้า Express ช้ากว่า Elysia 2.3 เท่า มันจะใช้พลังงานต่อ Request มากกว่า 2.3 เท่าด้วยหรือเปล่า?

คำตอบสั้น ๆ คือ ไม่ครับ — มากกว่าประมาณ 1.8 เท่า

เหตุผลคือ Express ประมวลผล Request ได้ช้ากว่า แต่ก็ดึงกำลังไฟจากชิปขึ้นมาใช้ได้น้อยกว่าเช่นกัน พูดง่าย ๆ คือมันไม่ได้ประหยัดกว่า แค่ใช้เครื่องได้ไม่เต็มที่เท่า Elysia

แต่พอไล่ดูข้อมูลจริง ๆ ผมเจอเรื่องที่น่าสนใจกว่านั้นอีก: พลังงานส่วนใหญ่ที่เครื่องใช้ระหว่างทดสอบ ไม่ได้มาจาก Framework เพียงอย่างเดียว ตัวยิงโหลดที่สร้าง Request ก็แย่งใช้ CPU อยู่บนเครื่องเดียวกันด้วย

สรุปก่อน เผื่อยังไม่อยากอ่านยาว

  • Elysia ใช้พลังงานต่ำสุดที่ 0.0958 mJ/request ส่วน Express อยู่ที่ 0.1702 mJ/request
  • Express ช้ากว่า Elysia 2.32 เท่า แต่ใช้พลังงานต่อ Request มากกว่า 1.78 เท่า
  • Framework ที่เร็วสามตัวใช้กำลังไฟใกล้กันมาก ดังนั้นตัวที่รับ Request ได้มากกว่าจึงใช้พลังงานต่อ Request ต่ำกว่า
  • พอเปลี่ยนเป็น Route ส่งวิดีโอ Express กลับใช้พลังงานต่อข้อมูลมากกว่าตัวอื่นถึง 2.9 เท่า
  • สำหรับ Framework ที่เร็ว ตัวยิงโหลดอย่าง bombardier ใช้เวลา CPU มากกว่าตัว Server สามถึงสี่เท่า
  • ตัวเลขทั้งหมดเป็นพลังงานของ SoC บนเครื่องทดสอบ ไม่ใช่ค่าไฟจากปลั๊ก และไม่ใช่ตัวเลขจาก Production

ก่อนเริ่ม: ผมทดสอบบนเครื่องอะไร?

รุ่น MacBook Air (Mac17,3)
ชิป Apple M5 — 10 คอร์, 4 Super + 6 Efficiency
แรม 24 GB unified
ระบบปฏิบัติการ macOS 26.6.2 (25G83)
Bun 1.4.0
bombardier 2.0.2 darwin/arm64
อุณหภูมิห้อง 22 °C

ลิงก์ชื่อรุ่นด้านบนเป็นลิงก์ Lazada affiliate ครับ กดซื้อผ่านลิงก์นี้ราคาไม่ต่างจากเดิม แต่บล็อกจะได้ค่าคอมมิชชันเล็กน้อย

macOS 26 เรียกคอร์ประสิทธิภาพสูงว่า “Super” แทนคำว่า “Performance” เรื่องนี้ดูเหมือนเป็นแค่ชื่อ แต่มีผลตอนอ่านข้อมูลจริง เพราะโค้ดรอบแรกของผมมองหา P-Cluster แล้วได้ค่าว่างกลับมาแบบเงียบ ๆ

อีกเรื่องที่ต้องบอกไว้ก่อนคือ โพสต์ที่แล้วทดสอบในห้อง 25 °C แต่รอบนี้อยู่ที่ 22 °C ดังนั้น อย่านำตัวเลข Throughput ของสองโพสต์มาเทียบตรง ๆ ครับ MacBook Air ไม่มีพัดลม อุณหภูมิห้องจึงมีผลกับการระบายความร้อนโดยตรง

ก่อนดูผล: ตัวเลขพลังงานนี้วัดอะไรบ้าง?

ข้อมูลทั้งหมดข้างล่างมาจาก powermetrics เครื่องมือที่ติดมากับ macOS โดยผมเก็บตัวอย่าง 2 ครั้งต่อวินาที บน Apple Silicon มันรายงานพลังงานที่ใช้ในแต่ละช่วงเป็นหน่วยมิลลิจูล (mJ):

cpu_energy + gpu_energy + ane_energy

ก่อนจะไปต่อ ขอแปลคำศัพท์สั้น ๆ ก่อนครับ:

  • วัตต์ (W) คืออัตราการใช้พลังงาน ณ ช่วงเวลาหนึ่ง คล้ายกับความเร็วที่น้ำไหลออกจากก๊อก
  • จูล (J) คือพลังงานรวมที่ใช้ไป ส่วน 1 mJ เท่ากับ 0.001 J
  • mJ/request คือพลังงานเฉลี่ยที่เครื่องใช้เพื่อจัดการ 1 Request ยิ่งน้อยยิ่งดี

วิธีคิดคือ mJ/request = กำลังไฟ (W) ÷ Request ต่อวินาที × 1,000 เพราะ 1 วัตต์เท่ากับ 1 จูลต่อวินาที เพราะฉะนั้น Framework สองตัวอาจใช้ไฟใกล้กัน แต่ตัวที่จัดการ Request ได้มากกว่าจะมีค่า mJ/request ต่ำกว่า

อย่างไรก็ตาม ตัวเลขจาก powermetrics คือ พลังงานประมวลผลของ SoC เท่านั้น มันไม่รวม:

  • DRAM หรือพลังงานของหน่วยความจำ ซึ่งอาจต่างกันตามปริมาณการ Allocate ของแต่ละ Framework นี่คือส่วนที่หายไปและน่าจะกระทบผลมากที่สุด จึงต้องบอกไว้ตั้งแต่ต้น
  • SoC fabric, interconnect และ memory controller
  • จอและ backlight — บ่อยครั้งเป็นตัวกินไฟมากที่สุดตัวเดียวบนโน้ตบุ๊ก
  • SSD, Wi-Fi, Bluetooth, Secure Enclave และโดเมน always-on
  • การสูญเสียจากการแปลงไฟที่ PMU/VRM และที่ตัว adapter เอง

เพราะฉะนั้น ทุกตัวเลขในโพสต์นี้หมายถึง พลังงานประมวลผลของ SoC ต่อ Request ไม่ใช่พลังงานที่ดึงจากปลั๊ก ตัวเลขจริงที่ปลั๊กจะสูงกว่านี้พอสมควร และ powermetrics เองก็เตือนไว้ชัดเจนว่า:

Average power values reported by powermetrics are estimated and may be inaccurate - hence they should not be used for any comparison between devices, but can be used to help optimize apps for energy efficiency.

สรุปง่าย ๆ คือ ใช้เปรียบเทียบ Framework บนเครื่องเดียวกันได้ แต่เอาไปเทียบกับเครื่องอื่นตรง ๆ ไม่ได้ครับ

ผมทดสอบอย่างไร?

ผมเลือก HTTP Server บน Bun มา 5 ตัว ได้แก่ elysia, hono, bun (Bun.serve แบบไม่ครอบ Framework), effect และ express โดยเลือกให้ครอบคลุมทั้งกลุ่มที่เร็วและช้าจากโพสต์ก่อน

การทดสอบแบ่งเป็น 5 รอบ แต่ละ Framework รันรอบละหนึ่งครั้ง รวมทั้งหมด 25 Measurements ในหนึ่ง Measurement จะยิง 4 Route:

  • ping — ตอบข้อความสั้น ๆ ไม่มีงานซับซ้อน
  • query — อ่าน Query String จาก Request
  • body — อ่าน Request Body
  • video — ส่งไฟล์ MP4 ขนาด 14.1 MB

สาม Route แรกยิงนาน Route ละ 10 วินาทีด้วย 500 Concurrent Connections หรือพูดง่าย ๆ คือเปิด Connection พร้อมกัน 500 เส้น ส่วน video ลดเหลือ 10 Connections เพราะ Response มีขนาดใหญ่มาก

ทำไมต้องสลับลำดับการทดสอบ?

ถ้าผมรัน Elysia ก่อนแล้วค่อยรัน Express ทุกครั้ง ตัวหลังอาจเสียเปรียบเพราะเครื่องร้อนกว่าเดิม โพสต์ก่อนเจอปัญหานี้เต็ม ๆ โดย Throughput ลดลงประมาณ 15% จากตัวแรกไปตัวสุดท้าย

คราวก่อนผมแก้ด้วยการสุ่มลำดับ แต่รอบนี้ใช้วิธีที่แน่นอนกว่า เรียกว่า cyclic Latin square ขนาด 5×5 ชื่ออาจดูน่ากลัว แต่แนวคิดง่ายมากครับ: ให้ทุก Framework ได้อยู่ทุกลำดับอย่างละหนึ่งครั้ง

      slot1   slot2   slot3   slot4   slot5
R1    elysia  hono    bun     effect  express
R2    hono    bun     effect  express elysia
R3    bun     effect  express elysia  hono
R4    effect  express elysia  hono    bun
R5    express elysia  hono    bun     effect

ทุก Framework จึงได้เป็นทั้งตัวแรก ตัวกลาง และตัวสุดท้ายเท่า ๆ กัน ถ้าตำแหน่งการรันทำให้ผลค่อย ๆ ดีขึ้นหรือแย่ลง ผลกระทบนั้นจะถูกเฉลี่ยออกด้วยโครงสร้างของตาราง ไม่ต้องหวังพึ่งดวงจากการสุ่มอย่างเดียว

ตอนแรกผมกะว่าจะสลับลำดับหน้า-หลังในแต่ละรอบ แต่พอมี 5 Framework ตัวที่อยู่ตรงกลางจะติดอยู่ตำแหน่ง 3 ทุกครั้ง สลับไปสลับมาแต่เจ้าตัวกลางไม่ได้ไปไหนเลยครับ

ใช้ช่วงพักเป็นค่าพลังงานพื้นฐาน

ผมพัก 120 วินาทีระหว่างทุก Measurement และมีช่วงก่อนเริ่มกับหลังจบอีกอย่างละ 120 วินาที รวมแล้วได้ช่วงที่เครื่องว่างหรือ Idle 26 ช่วง

ค่า Idle ทำหน้าที่เป็น Baseline หรือพลังงานพื้นฐานของเครื่อง จากนั้นผมจึงนำพลังงานตอนรับโหลดมาหักค่านี้ออก โดยใช้ 60 วินาทีท้ายของแต่ละช่วงและตัดขอบออกข้างละ 2 วินาที เพื่อไม่ให้ช่วงปิด Server, เขียน Page Cache หรือช่วงที่ CPU กำลังลดความเร็วปนเข้ามา

นี่เป็นค่า Idle ตอนเครื่อง อุ่นแล้ว ไม่ใช่ตอนเพิ่งเปิดเครื่อง ซึ่งเหมาะกับการทดลองนี้มากกว่า เพราะกระแสรั่วของซิลิคอนจะเพิ่มตามอุณหภูมิ การใช้ Baseline ตอนเครื่องเย็นเกินไปอาจทำให้เราประเมินพลังงานของ Framework สูงเกินจริง

ใช้ Benchmark เดิมให้มากที่สุด

Suite ที่ใช้คือ bun-http-framework-benchmark ของ SaltyAom ที่ Commit 383eddd ผมพยายามไม่แตะโค้ดหลัก เพราะแค่เปลี่ยนวิธี Start Server หรือคำสั่งยิงโหลด ผลก็อาจเทียบกับของเดิมไม่ได้แล้ว

โชคดีที่ bench.ts Export ฟังก์ชันอย่าง startServer, waitForStartup, validateServer, ensurePortFree และ buildBenchmarkArgs ไว้ Runner ของผมจึงเรียกใช้เส้นทางเดิมของ Upstream ได้โดยไม่แก้ไฟล์ต้นฉบับเลย ลองเช็กด้วย git status --porcelain ก็เห็นแค่โฟลเดอร์ energy/ ที่ผมเพิ่มเข้ามา

เหตุผลที่ผมย้ำเรื่องนี้ เพราะ buildBenchmarkArgs เป็นตัวสร้างคำสั่ง bombardier ถ้าคำสั่งต่างกันแม้เพียง Option เดียว เราอาจกำลังวัดคนละอย่างกัน Runner จึงตรวจคำสั่งทุกตัวอักษรก่อนเริ่มงานที่ใช้เวลาประมาณ 75 นาที

ส่วน main() ของ Upstream ไม่ได้ถูกเรียก ทำให้กับดักจากโพสต์ก่อนหายไปสองอย่างโดยบังเอิญ คือโฟลเดอร์ results/ ไม่ถูกลบ และ results.md ไม่ถูกเขียนทับ

อย่างไรก็ตาม มี 4 จุดที่ผมตั้งใจเปลี่ยนและขอเปิดไว้ตรงนี้ทั้งหมด:

  1. bombardier ได้ -p r -o json เพิ่ม — ไม่มีแถบความคืบหน้า ผลลัพธ์อ่านด้วยเครื่องได้ ทำงาน น้อยกว่า ค่าเริ่มต้นของ upstream ด้วยซ้ำ
  2. เว้น 2 วินาทีคงที่ระหว่าง route เพื่อให้หน้าต่างพลังงานแยกกันสะอาด upstream รันติดกัน
  3. build framework ครั้งเดียวตั้งแต่ต้น พลังงานของ Bun.build จึงอยู่นอกทุกหน้าต่างที่วัด
  4. งานทั้งหมดรันใต้ caffeinate -dimsu เครื่องนี้ตั้งเวลาหลับตอนเสียบไฟไว้ที่หนึ่งนาที และโปรเจกต์ก่อนหน้าเคยเสียการรันทั้งรอบไปกับเรื่องนี้พอดี

ผลลัพธ์: Elysia ใช้พลังงานต่อ Request ต่ำสุด

Throughput เทียบกับพลังงานต่อ request

0.050.10.150.20.2550,000100,000150,000200,000250,000expresseffecthonobunelysiarequests / วินาทีmJ ต่อ request
จุดจาง: การวัด 75 ครั้งของ route เล็กทั้งหมด จุดทึบ: ค่าเฉลี่ยต่อ framework ถ่วงน้ำหนักตามจำนวน request ของ ping, query และ body

ตารางนี้รวม ping, query และ body โดยถ่วงน้ำหนักตามจำนวน Request ส่วน video มีลักษณะงานต่างกันมาก ผมจึงแยกไปอีกหัวข้อครับ:

Framework mJ / request SD CV req/s วัตต์
elysia 0.0958 0.0006 0.6% 184,915 17.69
bun 0.0999 0.0002 0.2% 179,218 17.88
hono 0.1010 0.0003 0.3% 176,583 17.81
effect 0.1241 0.0004 0.3% 136,690 16.93
express 0.1702 0.0011 0.6% 79,668 13.54

ถ้าไม่คุ้นกับคอลัมน์ในตาราง: SD คือส่วนเบี่ยงเบนมาตรฐาน ส่วน CV คือความผันผวนเทียบกับค่าเฉลี่ย ยิ่งต่ำแปลว่าผลแต่ละรอบยิ่งใกล้กัน รอบนี้ CV อยู่เพียง 0.2–0.6% ซึ่งนิ่งกว่า 0.5–1.1% ของโพสต์ก่อน

ช้ากว่า แต่ใช้กำลังไฟน้อยกว่าได้อย่างไร?

กำลังไฟเฉลี่ยของ SoC ขณะรับโหลด

05101520elysia17.69 Wbun17.88 Whono17.81 Weffect16.93 Wexpress13.54 W
ค่าเฉลี่ยจากหน้าต่าง ping, query และ body แถบคลาดเคลื่อนคือ ±1 SD จากห้ารอบ ไม่ใช่ช่วงความเชื่อมั่น

ตรงนี้เป็นผลที่ผมไม่คาดคิดครับ

Framework กลุ่มที่เร็วสามตัวดึงกำลังไฟใกล้กันมาก อยู่ที่ 17.7–17.9 W ต่างกันไม่ถึง 1% ส่วน Express ดึงเพียง 13.5 W หรือน้อยกว่าประมาณ 23%

ลองนึกภาพ CPU เป็นครัวครับ Framework ที่เร็วส่ง Order เข้าครัวได้ต่อเนื่องจนพ่อครัวทำงานเกือบเต็มกำลัง ส่วน Express ส่ง Order เข้าไปช้ากว่า ทำให้ครัวมีช่วงว่างและดึงไฟไม่เต็มเพดาน

ผลคือ Express ช้ากว่า Elysia 2.32 เท่า แต่ใช้พลังงานต่อ Request มากกว่าเพียง 1.78 เท่า

ย้ำอีกทีว่า Express ไม่ได้ประหยัดพลังงานกว่า มันแค่ใช้ความสามารถของเครื่องได้ไม่เต็มที่เท่านั้น ค่าปรับด้านพลังงานจึงไม่สูงเท่าค่าปรับด้าน Throughput และช่องว่างตรงนี้คือคำตอบของคำถามตั้งต้น

ทำไมต้องแยกผลตาม Route?

พลังงานต่อ request แยกตาม route

pingquerybody00.050.10.150.20.0850.10.104elysia0.0980.0980.104bun0.0920.1060.107hono0.1130.1240.14effect0.160.1710.182expressmJ/req
ไม่รวม video — ที่ราว 850 req/s เทียบกับ ping ที่ราว 185,000 ค่า mJ ต่อ request ของมันใหญ่กว่าราว 200 เท่า ถ้าวาดรวมกันจะเห็นแต่ video

ตรงนี้มีหลุมพรางเรื่องค่าเฉลี่ยอยู่หนึ่งจุดครับ

Route video ส่งไฟล์ขนาด 14.1 MB ได้ประมาณ 850 req/s ขณะที่ ping ทำได้ราว 185,000 req/s ค่า mJ/request ของวิดีโอจึงใหญ่กว่าประมาณ 200 เท่า ถ้าเอาทั้ง 4 Route มาบวกแล้วหาร 4 ตัวเลขสุดท้ายจะแทบกลายเป็นผลของ video ตัวเดียวที่ปลอมตัวมาเป็นค่าเฉลี่ย

คอลัมน์ Average ในโพสต์ก่อนมีปัญหานี้ รอบนี้ผมจึง ถ่วงน้ำหนักตามจำนวน Request ของ ping, query และ body ส่วน video จะแยกรายงานด้วยหน่วยที่เหมาะกับมันเอง

พอส่งวิดีโอ ผลกลับด้าน

route video: พลังงานต่อเมกะไบต์ที่ส่ง

01234elysia1.229 mJ/MBbun1.246 mJ/MBhono1.23 mJ/MBeffect1.282 mJ/MBexpress3.606 mJ/MB
route video สตรีมไฟล์ MP4 ขนาด 14.1 MB ที่ 10 connections รายงานต่อเมกะไบต์ เพราะต่อ request ไม่มีความหมายที่ขนาด response เท่านี้

สำหรับไฟล์ใหญ่ การนับพลังงานต่อ Request ไม่ค่อยมีความหมาย เพราะหนึ่ง Request อาจส่ง 1 KB หรือ 14 MB ก็ได้ ผมจึงเปลี่ยนมาดูพลังงานต่อข้อมูลที่ส่ง หรือ mJ/MB แทน

สี่ Framework แรกอยู่ใกล้กันภายใน 4% ที่ 1.23–1.28 mJ/MB แต่ Express อยู่ที่ 3.61 mJ/MB หรือแย่กว่าประมาณ 2.9 เท่า

ที่แปลกกว่านั้นคือ video เป็น Route เดียวที่ Express ดึงไฟ มากกว่า ตัวอื่น อยู่ที่ 17.7 W เทียบกับ 15.1–15.2 W ของอีกสี่ตัว และสูงกว่า 12.6–14.3 W ที่ Express ใช้บน Route ขนาดเล็กของตัวเอง

Route อื่น Express ทำให้เครื่องใช้กำลังไม่เต็ม แต่พอมาถึงวิดีโอ มันกลับเป็นตัวเดียวที่ดึงไฟจนแซงคนอื่น

ตอนนี้ผมยังไม่มีคำอธิบายที่มั่นใจพอจะฟันธง โพสต์ก่อนก็ทิ้งคอลัมน์ video ไว้เป็นคำถาม และตอนนี้มันก็ยังเป็นคำถามอยู่ครับ

ดูกำลังไฟระหว่าง 10 วินาทีจริง ๆ

กำลังไฟ SoC ตลอดหน้าต่าง ping 10 วินาที

elysiaexpress81012141618200246810วินาทีนับจากต้นหน้าต่างวัตต์
รอบที่ 3 จาก 5 เก็บที่ 2 Hz ทั้งคู่เริ่มต่ำกว่าสภาวะคงตัว — sample แรกจับช่วงไต่ขึ้นได้

พลังงานที่วัดได้เป็นของ Server จริงแค่ไหน?

การทดสอบนี้มีจุดสำคัญอยู่ข้อหนึ่ง: bombardier ซึ่งทำหน้าที่ยิง Request รันอยู่บน MacBook เครื่องเดียวกับ Server

พูดง่าย ๆ คือผมให้คนวิ่งแข่งและคนจับเวลาแย่งใช้ลู่วิ่งเส้นเดียวกัน ตัวยิงโหลดจึงใช้ CPU และพลังงานรวมอยู่ในทุกตัวเลขด้วย ผมเลยรันเพิ่มอีกรอบโดยใช้ Sampler tasks ของ powermetrics เพื่อดูว่าแต่ละ Process ใช้ทรัพยากรไปแค่ไหน

ตอนแรกผมตั้งใจจะแบ่งให้ได้เลยว่า Server ใช้กี่จูล และ bombardier ใช้กี่จูล แต่ ข้อมูลที่มีไม่สามารถแบ่งพลังงานราย Process ได้อย่างน่าเชื่อถือ และสาเหตุที่แบ่งไม่ได้ก็น่าสนใจกว่าตัวเลขที่ผมอยากได้เสียอีก

ผมลองสร้างสมการ joules = a·P_core_seconds + b·E_core_seconds เพื่อประมาณพลังงานจากเวลา CPU ผลคือ สัมประสิทธิ์ของ E-core ติดลบ ซึ่งถ้าเชื่อตามสมการจะหมายความว่ายิ่งใช้ E-core เครื่องยิ่ง ผลิต พลังงานออกมา แน่นอนว่าเป็นไปไม่ได้ครับ แถมค่า R² มีเพียง 0.48 แปลว่าโมเดลอธิบายข้อมูลได้ไม่ดี ลองดูสองหน้าต่างนี้:

เวลา P-core เวลา E-core พลังงาน
elysia / ping 51,944 ms 43,693 ms 168.5 J
express / ping 27,957 ms 1,146 ms 148.3 J

Elysia สะสมเวลา P-core มากกว่า 1.9 เท่า และเวลา E-core มากกว่าถึง 38 เท่า แต่ใช้พลังงานรวมมากกว่าเพียง 1.14 เท่า

สาเหตุคือเวลา CPU ของหลาย Core สามารถเกิดพร้อมกันได้ 10 วินาทีจริงอาจรวมเป็นเวลา CPU มากกว่า 10 วินาที และเมื่อชิปทำงานใกล้เพดานกำลังไฟแล้ว การเพิ่มเวลา CPU บางส่วนไม่ได้ทำให้กำลังไฟรวมเพิ่มแบบเส้นตรง เราจึงเอาเวลา CPU มาแบ่งสัดส่วนจูลตรง ๆ ไม่ได้

สิ่งที่ วัดได้ คือตัวเวลา CPU เอง:

สัดส่วนเวลา CPU ตลอดหน้าต่าง 10 วินาที

serverbombardierkernel_taskDEAD_TASKSอื่น ๆelysia ping9%42%9%38%100%elysia body13%41%11%33%100%express ping36%23%17%21%100%express body44%18%16%17%100%
เป็นสัดส่วนของเวลา CPU ที่วัดได้ ไม่ใช่จูล — ดูคำอธิบายด้านบน DEAD_TASKS คือถังของ powermetrics สำหรับโปรเซสที่จบไปกลาง sample และระบุที่มาไม่ได้

สำหรับ Framework ที่เร็ว ตัวยิงโหลดใช้เวลา CPU มากกว่า Server ที่กำลังวัดอยู่สามถึงสี่เท่า

ใน elysia/ping ตัว Server ใช้เวลา CPU 9% ของทั้งหมดในหน้าต่าง ส่วน bombardier ใช้ถึง 42% เพราะ Server ตอบเร็วมาก ตัวยิงโหลดจึงต้องทำงานหนักเพื่อสร้าง Request ให้ทัน

ฝั่ง Express กลับกัน ตัว Server ใช้ 36% ส่วน bombardier ใช้ 23% เพราะ Express ใช้ CPU ต่อ Request มากกว่าและตอบได้ช้ากว่า ตัวยิงโหลดเลยมีงานให้ทำน้อยลง

ดังนั้น การอ่านค่า “0.0958 mJ/request” ที่ถูกต้องคือ:

เครื่องทดสอบทั้งเครื่องใช้พลังงานเฉลี่ย 0.0958 mJ ต่อ Request ขณะที่รัน Elysia และยิงโหลดใส่ตัวเอง

มันไม่ใช่พลังงานของ Elysia เพียง Process เดียว และไม่ใช่ต้นทุนของการเสิร์ฟ Request บน Production แต่ยังใช้เปรียบเทียบ Framework ภายใต้เงื่อนไขเดียวกันได้ครับ

แต่การดูเวลา CPU ราย Process ก็มีข้อจำกัดอีก 2 ข้อ:

  • เวลา CPU ประมาณ 17–38% ระบุ Process ต้นทางไม่ได้ DEAD_TASKS คือกลุ่มที่ powermetrics ใช้รวม Process ซึ่งจบไประหว่างเก็บ Sample ค่านี้เปลี่ยนตาม Framework ที่ทดสอบ จึงไม่ใช่งานพื้นหลังคงที่ ผมแยกมันออกมาแทนที่จะซ่อนไว้ใน “อื่น ๆ” ดังนั้นสัดส่วนด้านบนควรอ่านเป็นช่วงประมาณ ไม่ใช่ตัวเลขเป๊ะ ๆ
  • powermetrics มี Option --show-process-energy แต่ค่าที่ได้คือ “Energy Impact” แบบเดียวกับ Activity Monitor ซึ่งไม่มีหน่วยและไม่เปิดเผยสูตรคำนวณ มันไม่ใช่จูล ผมจึงไม่ใช้ค่านี้

เช็กอีกที: ลำดับการรันยังรบกวนผลไหม?

กำลังไฟขณะ idle ตลอดเซสชัน

102030405060020406080นาทีนับจากต้นเซสชันมิลลิวัตต์
แต่ละจุดคือ 60 วินาทีท้ายของช่วงพักหนึ่งช่วง รวม 26 หน้าต่างตลอด 70 นาที

ถึง Latin Square จะออกแบบมาเพื่อหักล้างผลจากลำดับอยู่แล้ว ผมก็ยังอยากเช็กว่ามี Drift รูปแบบอื่นหลงเหลือหรือไม่

วิธีเช็กคือปรับแต่ละค่าด้วยค่าเฉลี่ยของ Framework ตัวเองก่อน แล้วดูความสัมพันธ์กับตำแหน่งที่รัน จากทั้งหมด 75 ค่า ได้ r = +0.03 ค่าใกล้ศูนย์หมายถึงแทบไม่เห็นความสัมพันธ์ระหว่างลำดับกับผลลัพธ์ ค่าเฉลี่ยแยกตามตำแหน่งออกมาเป็นแบบนี้:

ตำแหน่ง 1 2 3 4 5
1.0000 0.9993 1.0000 1.0003 1.0006

ผลแต่ละตำแหน่งต่างกันไม่เกิน 0.07% และถ้าเปลี่ยนไปดูความสัมพันธ์กับเวลาใน Session จะได้ r = −0.018 ซึ่งใกล้ศูนย์เช่นกัน

แต่ต้องระวังภาษาที่ใช้ครับ ค่าใกล้ศูนย์ไม่ได้ พิสูจน์ว่าไม่มีผล มันบอกเพียงว่าข้อมูลชุดนี้ ไม่พบผลที่ชัดพอ ที่ n = 75 ค่าความคลาดเคลื่อนมาตรฐานของ r อยู่ราว 0.118 ดังนั้นค่าต่ำกว่า |r| ≈ 0.23 ยังถือว่าสอดคล้องกับศูนย์ ค่า r = 0.03 รอบนี้ผ่านเกณฑ์นั้นสบาย ๆ ส่วน r = +0.09 ที่ n = 40 ของโพสต์ก่อนก็สอดคล้องกับศูนย์เหมือนกัน

ตัวที่ช่วยจริง ๆ คือโครงสร้าง Latin Square ซึ่งหักล้างผลเชิงเส้นจากลำดับตั้งแต่ตอนออกแบบการทดลอง ส่วนค่าสหสัมพันธ์นี้เป็นเหมือนการตรวจซ้ำว่าไม่มี Drift รูปแบบอื่นโผล่มาให้เห็น

MacBook Air ร้อนจนลดความเร็วหรือเปล่า?

คำตอบคือ ไม่พบว่ามี Thermal Throttling ครับ และคราวนี้มีข้อมูลรองรับมากกว่าการสังเกตเฉย ๆ

กำลังไฟตอน Idle เริ่มที่ 26 mW ในนาทีที่ 0 และจบที่ 21 mW ในนาทีที่ 69 เส้นแนวโน้มแทบราบด้วยความชัน +0.0002 W ต่อชั่วโมง ค่า thermal_pressure เป็น Nominal ครบทั้ง 100 หน้าต่าง และ S-Cluster วิ่งอยู่แถว 4.2 GHz ตลอด

ผลนี้ต่างจากโพสต์ก่อนที่ Throughput ลดลง 15% ภายในรอบเดียว จุดต่างสำคัญคือรอบนี้ยิงโหลดสั้นเพียง 10 วินาที แล้วพัก 120 วินาทีทุกครั้ง ความร้อนจึงไม่สะสมต่อเนื่องเหมือนตอนรันยาว 5 นาที

สำหรับคนที่จะลองทำตาม: ตอนแรกผมวางแผนใช้ --show-plimits เพื่ออ่านข้อจำกัดความเร็ว CPU โดยตรง แต่บน macOS 26.6.2 มัน ไม่เพิ่ม Key ใด ๆ ใน Output แบบ plist มีผลเฉพาะข้อความที่อ่านด้วยตาเท่านั้น ไม่มีฟิลด์ speed-limit หรือ forced-idle ให้ Parse

หลักฐานเรื่อง Thermal Throttling รอบนี้จึงมาจากความถี่ของแต่ละ Cluster, ค่า thermal_pressure และแนวโน้ม Idle ด้านบน ส่วน pmset -g therm ไม่รายงานอะไรเลยตั้งแต่ต้นจนจบ จึงยังใช้เป็นสัญญาณบนเครื่องนี้ไม่ได้

หลุมพรางที่เจอระหว่างวัด

ความคลาดเคลื่อนที่ใหญ่ที่สุดรอบนี้ไม่เกี่ยวกับความร้อน แต่เป็นเรื่องนาฬิกาครับ

ฟิลด์ timestamp ของ powermetrics ละเอียดแค่ระดับวินาที Sample ที่อยู่ติดกันจึงอาจได้ Timestamp เดียวกัน ก่อนจะกระโดดทีละ 1,000 ms ทั้งที่ elapsed_ns ใน Sample เดียวกันละเอียดถึงเศษเสี้ยวมิลลิวินาที

ถ้าเราเอาหน้าต่างโหลด 10 วินาทีไปจับคู่กับ Timestamp ที่หยาบระดับหนึ่งวินาทีตรง ๆ ซึ่งเป็นวิธีที่ดูธรรมชาติที่สุด เราอาจหยิบข้อมูลกำลังไฟผิดช่วงเข้ามาได้ถึง 1 วินาที หรือคิดเป็น 10% ของหน้าต่างเลยทีเดียว

หลังแก้ ค่าวัตต์เฉลี่ยขยับไปประมาณ 4% และ Coverage ของหน้าต่างเปลี่ยนจาก 1.009 มาเป็น 1.000 พอดี วิธีที่ใช้คือสร้างแกนเวลาใหม่จาก elapsed_ns สะสม แล้วค่อยยึดเข้ากับ Timestamp หลักด้วยวิธีกำลังสองน้อยสุด พร้อมบวก 500 ms เพื่อชดเชยการตัดเศษ

อีก 2 เรื่องที่ดูเล็ก แต่เผื่อช่วยประหยัดเวลาคนที่กำลังเขียน Parser ได้ครึ่งวัน:

  • macOS 26 ตั้งชื่อ cluster ประสิทธิภาพสูงว่า S-Cluster (“Super”) ไม่ใช่ P-Cluster โค้ดที่มองหา P จะไม่เจออะไรเลยแบบเงียบ ๆ
  • bombardier ให้ percentile ของ rps แต่ไม่ให้ของ latency — percentile ของ latency ต้องใช้ -l ซึ่ง upstream ไม่ได้ส่ง

ก่อนนำตัวเลขไปอ้าง ขอฝากไว้ 7 ข้อ

  1. นี่คือเครื่องเดียว ห้องเดียว และบ่ายเดียว การวัด 25 ครั้งบอกระดับความผันผวนของ MacBook เครื่องนี้ เท่านั้น ไม่ได้แทน M5 ทุกเครื่อง โดยเฉพาะเครื่องที่มีพัดลม
  2. ตัวยิงโหลดอยู่ในทุกตัวเลข สำหรับ Framework ที่เร็ว มันใช้เวลา CPU มากกว่าตัว Server เสียอีก
  3. พลังงานที่วัดเป็นของ SoC เท่านั้น ไม่รวม DRAM, จอ, SSD หรือการสูญเสียที่ Adapter
  4. ผลของกลุ่มที่เร็วแทบเป็นเรื่องเดียวกับ Throughput เพราะกำลังไฟของกลุ่มนี้ใกล้กัน และ mJ/request = วัตต์ ÷ req/s × 1,000 ความสัมพันธ์ระหว่างสองค่านี้มี r² = 0.971 ข้อมูลใหม่จริง ๆ จึงอยู่ที่ Express และ Route video
  5. สามในห้าตัวเป็น Pre-release เช่น Elysia 2.0.0-beta.4 และ Effect 4.0.0-beta.102 ตัวเลขจึงเป็นเพียง Snapshot ของ Software ที่ยังเปลี่ยนอยู่
  6. Handler ใน Benchmark แทบไม่มีงาน ถ้า Handler ต้องรอ Database จริง ความต่างระดับเศษมิลลิจูลตรงนี้อาจเล็กมากเมื่อเทียบกับเวลา Query
  7. เครื่องมือวัดก็ใช้ทรัพยากร การเปิด powermetrics ที่ 2 Hz ทำให้ Throughput ลดลงประมาณ 1.5–2.7% จากการทดสอบ A/B ผลกระทบใกล้กันทุก Framework จึงพอหักล้างในการเปรียบเทียบได้ แต่ตัวเลขสัมบูรณ์ยังคลาดเคลื่อนเล็กน้อย

และขอเปิดเผยเหมือนคราวก่อนครับ: ผมเคยมีส่วนร่วมกับเอกสารของ Elysia และรอบนี้ Elysia ก็อยู่ลำดับแรกในตารางอีกครั้ง ใครนำผลไปใช้ควรรับรู้ความเกี่ยวข้องส่วนนี้ไว้ด้วย

อยากลองทำซ้ำ เริ่มจากตรงนี้

git clone https://github.com/saltyaom/bun-http-framework-benchmark
cd bun-http-framework-benchmark
git checkout 383eddd
bun install

จากนั้นเปิด Terminal แรกเพื่อเก็บข้อมูลพลังงาน:

sudo powermetrics -i 500 -n 12000 -s cpu_power,thermal \
  -a 0 -f plist -b 0 -o "$OUT/power.plist" --handle-invalid-values

แล้วรัน Runner ใต้ caffeinate ใน Terminal อีกหน้าต่างหนึ่ง การกำหนด -n ให้มีจำนวน Sample ชัดเจนสำคัญมาก เพราะ Sampler จะหยุดเองเมื่อครบ หากใช้ sudo kill ทีหลัง เราอาจต้องกรอกรหัสผ่านใหม่เมื่อเวลาอนุญาตของ sudo หมดไปแล้ว

Runner, Script วิเคราะห์ และ Dataset ขนาด 112 KB อยู่ใน Repository ครบทั้งหมด ทั้ง 25 Measurements, 26 ช่วง Idle และโมเดล Regression ที่ผมลองแล้วใช้ไม่ได้ ถ้าไม่เห็นด้วยกับวิธีถ่วงน้ำหนัก Route หรือการลบ Baseline สามารถหยิบข้อมูลดิบไปคำนวณใหม่ได้เลยครับ

ส่วนที่ผมอยากให้คนอื่นช่วยตรวจมากที่สุดคือ Route video เพราะ Express ใช้พลังงานต่อ MB มากกว่าตัวอื่น 2.9 เท่า และเป็น Framework เดียวที่ดึงไฟบน Route นี้มากกว่าทุก Route ของตัวเอง

ตอนนี้ผมยังตอบไม่ได้ว่าเกิดจากอะไร และผลจากเครื่องเดียวก็ยังไม่พอจะฟันธง ถ้าใครมีเครื่อง Apple Silicon และอยากช่วยลองซ้ำ ผมยินดีดูผลไปด้วยกันครับ