ตัวรายงานแครชตัวใหม่ของ Bun
(bun.sh)- Bun v1.1.5 เพิ่มตัวรายงานแครช
bun.reportที่ส่งข้อมูลสแตกของ Zig/C++ ผ่าน URL ขนาดราว 150 ไบต์ที่ไม่มีข้อมูลส่วนตัว แม้จะเกิดแครชหรือแพนิก - ตัวรายงานแครชของ OS แบบเดิมและคอร์ดัมป์มีภาระด้าน debug symbols, ประสิทธิภาพ, ความเป็นส่วนตัว, ขนาดไฟล์รันได้ สูง จึงนำมาใช้กับเครื่องมือ CLI อย่าง Bun ได้ยาก
- วิธีใหม่นี้จะแปลงแอดเดรสที่ความหมายไม่ชัดเจนเพราะ ASLR ให้เป็น แอดเดรสสัมพัทธ์อิงตามโมดูล แล้วให้เซิร์ฟเวอร์กู้คืนชื่อฟังก์ชันด้วย debug symbols ที่ตรงกับ commit SHA และแพลตฟอร์ม
- ใน URL จะมีแพลตฟอร์ม ซับคอมมานด์ commit SHA ฟีเจอร์แฟลก แอดเดรสสแตก ประเภทแครช และข้อความแครช โดยแอดเดรสสแตกจะถูกเข้ารหัสให้สั้นด้วย base64 VLQ
- จะไม่มีการส่งซอร์สโค้ด JavaScript/TypeScript หรือ environment variables แต่ส่งเฉพาะ ข้อมูลสแตกของ Zig/C++ และเมทาดาทาบางส่วน ที่ทีม Bun ต้องใช้ในการวิเคราะห์
เหตุผลที่ Bun สร้างตัวรายงานแครชของตัวเอง
- ณ เวลาที่เขียน Bun มี GitHub issues ที่ยังเปิดอยู่มากกว่า 2,600 รายการ และบาง issue ก็ยากเป็นพิเศษทั้งในการทำซ้ำและดีบัก
- บริการรายงานแครชอย่าง Sentry เหมาะกับแอปและผลิตภัณฑ์ SaaS แต่ถ้าให้อัปโหลดคอร์ดัมป์จากเครื่องมือ CLI อย่าง Bun จะเกิดปัญหาเรื่องความเป็นส่วนตัว ประสิทธิภาพ และขนาดไฟล์รันได้มากขึ้น
- Bun v1.1.5 จึงนำเสนอ ฟอร์แมตใหม่ขนาดเล็ก สำหรับรายงานแครชของ Zig และ C++
- รายงานแครชเก็บอยู่ใน URL ขนาดประมาณ 150 ไบต์
- ไม่มีข้อมูลส่วนตัวรวมอยู่ด้วย
ข้อจำกัดของการใช้ตัวรายงานแครชของ OS เพียงอย่างเดียว
- ระบบปฏิบัติการบางตัวอย่าง macOS มีตัวรายงานแครชในตัว แต่ถ้าจะใช้งานได้อย่างเหมาะสม โดยทั่วไปต้องแจกจ่าย debug symbols ไปพร้อมกับแอปพลิเคชัน
- debug symbols ทำให้ขนาดไฟล์แจกจ่ายของ Bun เพิ่มขึ้นมาก
- debug symbols บน Linux มีขนาดประมาณ 30MB
- debug symbols บน macOS มีขนาดประมาณ 9MB
- ไฟล์
.pdbบน Windows มีขนาด มากกว่า 250MB
- ตัวอย่างไฟล์รันได้ของ Bun ลดจาก
60Mเหลือ51Mหลังใช้llvm-strip - หากเกิดแครชโดยไม่มี debug symbols สแตกเทรซจะเหลือเพียง
???กับแอดเดรส ทำให้ใช้งานได้ไม่มาก - เนื่องจาก ASLR(Address space layout randomization) แอดเดรสของฟังก์ชันจะมีออฟเซ็ตแบบสุ่มปะปนอยู่ จึงไม่สามารถกู้คืนชื่อฟังก์ชันได้โดยตรง
วิธีทำงานของ bun.report
- เมื่อเกิดแครชหรือแพนิกใน Bun v1.1.5, Bun จะพิมพ์ลิงก์
bun.reportออกมาพร้อมเวอร์ชัน แพลตฟอร์ม อาร์กิวเมนต์ที่ใช้รัน การใช้หน่วยความจำ และข้อความแครช - เมื่อผู้ใช้เปิดลิงก์ จะถูกรีไดเร็กต์ไปยัง ฟอร์ม issue ของ GitHub ที่กรอกไว้ล่วงหน้า
- ภายใน URL มีการเข้ารหัสสแตกเทรซที่ถูกรีแมปแล้วไว้
- เซิร์ฟเวอร์จะกู้คืนแอดเดรสสแตกจากข้อมูลใน URL และแปลงให้เป็นรายงานแครชที่ทีม Bun อ่านได้
ขั้นตอนการแปลงแอดเดรสให้เป็นสแตกเทรซที่อ่านได้
- แอดเดรสของฟังก์ชันคือพอยน์เตอร์ที่ชี้ไปยังตำแหน่งที่โค้ดของแอปพลิเคชันถูกโหลดเข้าไปในหน่วยความจำ และมีออฟเซ็ตแบบสุ่มเพื่อความปลอดภัยรวมอยู่
- แนวคิดพื้นฐานคือ นำ base address ของไบนารีมาลบออกจากแอดเดรสดิบเพื่อให้ได้แอดเดรสสัมพัทธ์
- การติดตั้งจริงซับซ้อนกว่านั้นเพราะ API ของแต่ละแพลตฟอร์มต่างกัน
- Windows ใช้แฟลก
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESSกับGetModuleHandleExWแล้วใช้พอยน์เตอร์ของโมดูลเป็น base address - Linux ใช้
dl_iterate_phdrวนดูโมดูลที่ถูกโหลด และใช้dl_phdr_info.dlpi_addrของโมดูลที่มีแอดเดรสนั้นเป็น base address - macOS ใช้
_dyld_image_count,_dyld_get_image_headerเพื่อวนดูโมดูล และใช้_dyld_get_image_vmaddr_slideเพื่อดึง ASLR slide- แอดเดรสผลลัพธ์บน macOS จะยังมี image offset ติดอยู่ ซึ่งสำหรับ Bun คือ
0x100000000 - เพื่อให้ URL สั้นลง จะตัดออฟเซ็ตนี้ออก แต่ต้องบวกกลับเข้าไปก่อนรีแมปด้วย
llvm-symbolizer
- แอดเดรสผลลัพธ์บน macOS จะยังมี image offset ติดอยู่ ซึ่งสำหรับ Bun คือ
- Windows ใช้แฟลก
- บน Linux และ macOS โมดูลตัวแรกจะชี้ไปยังไบนารีหลักของแอปพลิเคชัน
- บน Windows สามารถตรวจได้ว่าเป็นไบนารีหลักหรือไม่โดยเทียบชื่อโมดูลกับ
peb.ProcessParameters.ImagePathName - Bun ไม่ดาวน์โหลดและพาร์ส debug symbols บนเครื่อง แต่ มอบหมายการ demangle ให้เซิร์ฟเวอร์
- เซิร์ฟเวอร์สามารถแคช debug symbols ได้
- สามารถ demangle สแตกเทรซได้ภายในไม่กี่วินาที
- และทำหน้าที่เป็นลิงก์สำหรับเปิด GitHub issue ใหม่ไปพร้อมกัน
โครงสร้าง URL ของ bun.report
- URL ของ
bun.reportเข้ารหัสข้อมูลต่อไปนี้ไว้- Platform: อักขระหนึ่งตัวที่บอกแพลตฟอร์ม เช่น
wคือ x86_64 Windows และMคือ aarch64 macOS - Subcommand: อักขระหนึ่งตัวที่บอกซับคอมมานด์ เช่น
bun test,bun install,bun run - Commit SHA: commit SHA ของ Bun เวอร์ชันปัจจุบัน ใช้สำหรับดึง debug symbols ภายหลัง
- Feature Flags: ตัวบ่งชี้ API และฟีเจอร์ที่ถูกใช้ก่อนเกิดแครช
- Stack Trace Addresses: แอดเดรสที่คำนวณได้จากขั้นตอนก่อนหน้า
- Crash Type: อักขระหนึ่งตัวที่บอกประเภทของแครช
- Crash Message: ข้อความที่มีรูปแบบต่างกันตามประเภทแครช
- Platform: อักขระหนึ่งตัวที่บอกแพลตฟอร์ม เช่น
- หมายเลขเวอร์ชันใน URL มีไว้ให้มนุษย์อ่านเป็นหลัก มากกว่าจะใช้ในการประมวลผลจริง
- ข้อมูลเท่านี้ก็ช่วยให้พอมองลักษณะของแครชบางส่วนได้ด้วยตาเปล่า
- หากเห็นตัวระบุ
wก็รู้ได้เร็วว่าเป็นแครชบน Windows - หากเห็น
A2ตอนท้ายสตริง ก็ระบุได้ว่าเป็น segmentation fault
- หากเห็นตัวระบุ
การเข้ารหัส VLQ เพื่อให้ URL สั้น
- แอดเดรสในสแตกเทรซถูกเข้ารหัสเป็นตัวเลขแบบ base64 Variable Length Quantity(VLQ) เพื่อให้ URL สั้น
- VLQ ทำให้ตัวเลขขนาดเล็กใช้ตัวอักษรน้อยลง แต่ก็ยังเข้ารหัสตัวเลขขนาดใหญ่ได้
- เทคนิคเดียวกันนี้ถูกใช้ใน JavaScript source maps เพื่อเก็บหมายเลขบรรทัด
- เซิร์ฟเวอร์จะถอดรหัสค่า VLQ กลับมาเป็นแอดเดรสสัมพัทธ์ แล้วใช้ commit hash และแพลตฟอร์มเพื่อดาวน์โหลด debug symbols ก่อน demangle ชื่อฟังก์ชันด้วย
llvm-symbolizer - ในตัวอย่างแครชพบว่า assertion ล้มเหลวใน
dirInfoCachedMaybeLogซึ่งเป็นส่วนหนึ่งของโค้ด module resolver บน Windows
การเข้ารหัสฟีเจอร์แฟลก
- URL ยังเข้ารหัส จำนวนเต็ม 64 บิต ด้วย โดยแต่ละบิตสอดคล้องกับการใช้งานฟีเจอร์เฉพาะของ Bun
- แฟลกนี้ให้เบาะแสว่า API และระบบใดอาจมีส่วนต่อการเกิดแครช
- หากมีการโหลดไฟล์
.envอัตโนมัติ จะตั้งค่าฟีเจอร์dotenv - หากใช้
fetch()จะตั้งค่าฟีเจอร์fetch
- หากมีการโหลดไฟล์
- Bun ติดตามการใช้ฟีเจอร์ผ่านคอนเทนเนอร์ตัวแปรโกลบอล และภายในแต่ละ API จะเพิ่มค่าตัวเลขที่เกี่ยวข้องเพื่อระบุว่าได้ใช้งานแล้ว
- ใช้เมตาโปรแกรมมิงแบบ compile time ของ Zig เพื่อวนผ่านรายการฟีเจอร์และสร้าง packed struct แบบไดนามิกที่ใช้ 1 บิตต่อฟีเจอร์
- การใช้
inline forทำให้สามารถวนผ่านรายการฟีเจอร์ในช่วง compile time แต่ตั้งค่าบิตจริงในช่วง runtime ได้ - หากเพิ่มฟีเจอร์ใหม่เข้าไปใน
Featuresstruct เดิม ตัวรายงานแครชก็จะรองรับได้โดยไม่ต้องเขียนซ้ำ - วิธีเดียวกันนี้ทำได้ด้วยแมโครใน C หรือ Rust เช่นกัน แต่ในการติดตั้งของ Bun นั้น Zig
comptimeทำให้โค้ดเรียบง่ายและอ่านง่ายกว่า
ความต่างจากคอร์ดัมป์
- คอร์ดัมป์มีข้อมูลมากกว่ามาก แต่ก็มีขนาดใหญ่ ต้องมี debug symbols จึงจะมีประโยชน์ และอาจมีข้อมูลอ่อนไหวหรือข้อมูลลับรวมอยู่จำนวนมาก
- วิธีรายงานแบบใหม่ของ Bun หลีกเลี่ยงการส่งซอร์สโค้ด JavaScript/TypeScript, environment variables และข้อมูลอ่อนไหวอื่น ๆ
- แทนที่จะส่งทุกอย่างไปโดยปริยาย จะส่งเฉพาะ สแตกเทรซของ Zig/C++ และรายละเอียดบางอย่างที่มีแนวโน้มจำเป็นต่อการวิเคราะห์ปัญหา
- หากต้องการข้อมูลเพิ่ม ก็สามารถขอจากผู้ใช้แยกต่างหากได้
- ทำให้ทีม Bun วินิจฉัยแครชได้ง่ายกว่าสถานการณ์แบบเดิมที่เหลือเพียงแอดเดรสที่ยังไม่ถูกแมป
เดโม
- มีเว็บแอปขนาดเล็กสำหรับทดสอบตัวรายงานแครชที่ bun.report
- หากเติม
/viewต่อท้าย URL ของรายงานแครชใด ๆ ก็จะเปิดหน้าเว็บแอปนี้ได้
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ถ้าเหตุผลที่ใช้วิธีนี้แทน stack trace แบบทั่วไปคือเพื่อหลีกเลี่ยงการแจกจ่าย debug symbols ขนาดหลาย MB ก็ดูเหมือนว่าพวกเขาจะมองข้ามทางเลือกที่ดีกว่าอย่างการใส่เฉพาะชื่อฟังก์ชันไว้ใน debug table
เป็นวิธีที่ดีกว่ามากเมื่อเทียบกับการต้องใช้เว็บเซอร์วิสเพื่อดู stack trace และนี่ไม่ใช่แค่ทฤษฎี แต่มีการ implement ไว้แล้วใน LLVM: https://clang.llvm.org/docs/UsersManual.html#cmdoption-gline...
ถ้าใช้ URL เดียวแล้วช่วยกรอกสิ่งที่จำเป็นให้เกือบอัตโนมัติ มันก็ง่ายพอ และนั่นแหละที่จะทำให้นักพัฒนาโพสต์ crash report จริง ๆ ขนาดก็สำคัญ เพื่อไม่ให้เกิดข้อเสียกับผู้ใช้ แต่ประเด็นหลักคือทำให้ทั้งกระบวนการง่ายมาก ๆ
ใน use case นี้ การต้องดู stack trace ผ่านเว็บเซอร์วิสไม่ใช่ข้อเสียใหญ่ คล้ายกับการ obfuscate/minify บันเดิล JavaScript ฝั่ง frontend แล้วอัปโหลด source map ไปยัง Sentry จากนั้นจึงกู้คืน stack trace ที่มาจากเบราว์เซอร์ของผู้ใช้ผ่าน Sentry ผู้ใช้ก็ไม่ได้จะดู stack trace นั้นอยู่แล้ว และผมก็ไม่ได้รู้สึกไม่สะดวกที่ต้องใช้ Sentry ดู เพราะถ้าไม่ทำแบบนั้นก็คงไม่ได้เห็นมันเลย
การเสนอทางเลือกทำได้หลายแบบ
บนแพลตฟอร์มเหล่านี้ นี่คือวิธีที่ symbolication ค้นหาชื่อฟังก์ชันของ system libraries ได้โดยไม่ต้องมี debug symbols แบบเต็ม
ยอดเยี่ยมและสร้างสรรค์มาก อยากให้หลายโปรเจกต์ทำตามวิธีนี้ แก่นของมันคือการบันทึก stack trace ด้วย relative program counter อ้างอิงตาม executable/shared object
เท่าที่รู้ Bun ถูกลิงก์แบบ static แต่ถ้าเป็นระบบลิงก์แบบ dynamic ก็น่าจะต้องใส่ shared object ID เป็นเลขขนาดเล็กนำหน้า program counter ที่ normalize แล้วแต่ละตัว
ตัวอย่างเช่น Unreal Engine crash reporter ส่งรูปแบบเรียบง่ายแบบนี้ได้มาหลายปีแล้ว และสามารถกู้คืนฟังก์ชัน/หมายเลขบรรทัดของแต่ละ stack frame ได้ค่อนข้างแม่นยำ แต่โดยทั่วไปยังนิยม minidump มากกว่า เพราะถ้ามี stack variables ด้วยก็มักได้เบาะแสเพิ่มเติมว่าเกิดอะไรขึ้น
Microsoft ทำเรื่องแบบนี้ได้ดีจริง ๆ ใน SQL Server ใช้ minidump ที่ลบข้อมูลส่วนบุคคลออกแล้ว ซึ่งเล็กมากและมีประโยชน์มาก
แม้ในตอนนั้น ซึ่งก็คือ 15 ปีก่อน full dump ของ SQL Server ใน production ก็เป็นไฟล์ขนาดมหึมาจนเคลื่อนย้ายลำบากแล้ว
ผมติดตาม Bun มาหลายปีตั้งแต่เห็นทวีตแรกเกี่ยวกับ Zig และเพิ่งเริ่มใช้เมื่อไม่นานนี้ มันก็แค่ทำงานได้ดีโดยไม่ต้องวุ่นวายอะไรเป็นพิเศษ
Bun ค่อนข้างน่าสนใจ ผมลองใช้กับโปรเจกต์ตัวอย่างเล็ก ๆ ไม่กี่อันแล้ว ความเร็วดี และชอบที่รวม package management กับ JavaScript runtime ไว้ด้วยกัน
แต่ในโปรเจกต์จริงจังส่วนใหญ่ผมใช้ Dependabot อยู่ เท่าที่รู้ การรองรับ Bun ของ Dependabot กำลังอยู่ระหว่างทำ หรืออย่างน้อยก็มีการพูดคุยกันในบาง issue ของ repository ดังนั้นผมจึงชะลอการใช้ไว้จนกว่าการรองรับจะถูกปล่อยออกมา
ไม่เสียใจเลย ส่วนที่เร็วขึ้นซึ่งสะสมกันเป็นการประหยัดเวลา และการปรับปรุง developer experience ครั้งใหญ่ คุ้มค่าตามที่คาดไว้
คงมีคนไม่มากที่จะสังเกตว่าใส่ใจรายละเอียดกับเรื่องแบบนี้มากแค่ไหน ดีใจที่ได้เห็นว่าทีม Bun ใส่ใจ craft ของตัวเองมากเพียงใด
Bun น่าทึ่ง แต่เมื่อเร็ว ๆ นี้ผมพยายามสร้าง HTTP/2 server ด้วย Fastify แล้วทำไม่ได้
มี error ว่า
node:http2 createServer is not yet implemented in Bunและ issue ที่ข้อความชี้ไปจริง ๆ แล้วเกี่ยวกับการรองรับ HTTP/2 client ส่วนการรองรับ client ถูกปล่อยแล้วใน v1.0.13: https://bun.sh/blog/bun-v1.0.13#http2-client-supportข้อความ
NotImplementedErrorควรถูกเปลี่ยนให้ชี้ไปยัง issue ฝั่ง server: https://github.com/oven-sh/bun/issues/8823การรองรับ HTTP/2 server เป็นหนึ่งใน feature request อันดับต้น ๆ: https://github.com/oven-sh/bun/issues?q=is%3Aissue+is%3Aopen...
ถ้าฟีเจอร์นี้ออกมา น่าจะทำให้คนย้ายมา Bun ได้มากขึ้นมาก
Bun ยังอยู่ในช่วงต้นของวงจรชีวิตมากเกินไป แต่ถึงอย่างนั้นก็ยังคาดหวังกับโปรเจกต์นี้มาก
สงสัยว่ามีคนใช้ Bun จริง ๆ ไหม ดีอย่างที่คาดหวังหรือเปล่า?
การตั้งค่าสภาพแวดล้อม TypeScript Node อย่าง ts-node, ts-jest, การรองรับ ESM, top-level await ฯลฯ ยุ่งยากเกินจำเป็น แม้ Node รุ่นล่าสุดจะลดความไม่สะดวกบางอย่างลงแล้ว แต่ก็ยังไม่ง่ายเท่า
bun initผมยังใช้ bun shell API อย่างสนุกด้วย: https://bun.sh/blog/the-bun-shellerror message ก็แย่กว่า Node ค่อนข้างมาก ผมลองใช้อยู่พักหนึ่ง แต่ช่วงนี้ใช้ Node กับ
—loader tsxก็ทำทุกอย่างที่ต้องการได้หมดและไม่มีข้อเสีย ถ้าเป็น server ง่าย ๆ เช่นใช้ WebSocket และมั่นใจว่าไม่ต้องใช้ native modules ก็พิจารณา Bun ได้ จริง ๆ ผมก็รันบริการแบบนั้นอยู่สองสามตัวความเร็วในการเริ่มทำงานทันทีนั้นยังน่าทึ่งอยู่
ยังมีบางอย่างที่ขาดอยู่ แต่สำหรับผมตอนนี้ดีกว่า Node แล้ว
บทความนี้ยังรู้สึกว่าเป็น กรณีศึกษา Zig ที่ยอดเยี่ยมด้วย น่าสนใจมาก
Bun ต้องดาวน์โหลดแพ็กเกจ 37 ตัวก่อนจะใช้ REPL ได้ ถ้าไม่มีอินเทอร์เน็ตก็ใช้ REPL ไม่ได้
เมื่อรัน
bun replจะขึ้น error ว่าดาวน์โหลด package manifest ของbun-replไม่สำเร็จ ไม่ใช่ปัญหาใหญ่ แต่ผมคาดหวังและตั้งตารอไว้มากว่าถ้าแค่ใส่ executable ไฟล์เดียวไว้ใน PATH ก็ควรทำงานได้ทันทีโดยไม่ต้องติดตั้งอะไรภายใน
bun replทำสิ่งเดียวกับbunx bun-repl