2 คะแนน โดย GN⁺ 3 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Bun ซึ่งเริ่มต้นด้วย Zig เติบโตเป็น runtime ที่มีการดาวน์โหลดมากกว่า 22 ล้านครั้งต่อเดือน แต่ปัญหาความเสถียรที่เกิดซ้ำจากการผสานกันระหว่างเอนจิน JavaScript แบบ GC กับการจัดการหน่วยความจำด้วยตนเอง กลายเป็นเหตุผลให้เปลี่ยนไปใช้ Rust
  • แทนที่จะให้คนย้ายโค้ด Zig จำนวน 535,496 บรรทัดตลอด 1 ปี ทีมรัน dynamic workflow ของ Claude Code ประมาณ 50 รายการ และ Claude instance สูงสุด 64 ตัวแบบขนานเป็นเวลา 11 วัน
  • การ port ถูกตรวจสอบด้วย PORTING.md, LIFETIMES.tsv, ผู้ implement 1 คนกับ reviewer เชิงปฏิปักษ์ อย่างน้อย 2 คน และชุดทดสอบ TypeScript เดิม โดยผ่าน CI 6 แพลตฟอร์ม 100%
  • หลังเปลี่ยนเป็น Rust, Bun v1.4.0 แก้ บั๊ก 128 รายการ ที่ทำซ้ำได้ใน v1.3.14 แก้ memory leak ที่ instrument ได้ทั้งหมด และลดขนาด binary บน Linux·Windows ลงประมาณ 20%
  • Bun v1.3.14 เป็นเวอร์ชัน Zig สุดท้าย และ v1.4.0 เป็นเวอร์ชัน Rust แรกที่ให้ใช้ใน canary โดยทีมใช้ borrow checker, Miri, LeakSanitizer และ fuzzing แบบ 24/7 บนฐาน coverage เป็นเครื่องมือปรับปรุงความเสถียร

Bun ที่เริ่มต้นด้วย Zig และปัญหาความเสถียร

  • Bun เริ่มต้นจากโปรเจกต์ที่ port แบบทีละบรรทัด transpiler สำหรับ JavaScript·TypeScript ของ esbuild จาก Go ไปเป็น Zig
  • โค้ด Zig ชุดแรกเขียนขึ้นเมื่อวันที่ 16 เมษายน 2021 และการควบคุมระดับต่ำกับการออกแบบที่เน้นประสิทธิภาพของ Zig ทำให้การ implement Bun ช่วงแรกเป็นไปได้
  • Bun ช่วงแรกถูกเขียนด้วย Zig โดยคนคนเดียวตลอด 1 ปี และมีขอบเขตกว้างมาก
    • transpiler·minifier·bundler สำหรับ JavaScript, TypeScript, CSS
    • package manager ที่เข้ากันได้กับ npm
    • test runner คล้าย Jest
    • การ resolve module ที่เข้ากันได้กับ Node.js·TypeScript
    • client สำหรับ HTTP/1.1·WebSocket
    • การ implement Node.js API เช่น fs, net, tls
  • ปัจจุบัน Bun CLI ถูกดาวน์โหลดมากกว่า 22 ล้านครั้งต่อเดือน ถูกใช้เป็น runtime โดย Claude Code และ OpenCode และมี Vercel, Railway, DigitalOcean เป็นต้นที่ให้การรองรับแบบ 1st-party

บั๊กด้านความปลอดภัยหน่วยความจำที่เกิดซ้ำ

  • ตัวอย่างบั๊กที่แก้ใน Bun v1.3.14 รวมถึง use-after-free, double-free, memory leak, การเข้าถึง out-of-bounds และ race condition
    • heap-use-after-free จากการเรียก .reset() ระหว่าง async .write() ของ node:zlib
    • use-after-free ที่ JS callback แบบ reentrant ของ node:http2 ทำให้เกิด hashmap rehash จน pointer ของ stream ภายในเป็นโมฆะ
    • ปัญหาที่ callback valueOf() หรือ toString() detach ArrayBuffer ใน UDPSocket.send() และ sendMany()
    • crash และ out-of-bounds read จากการ detach หรือ resize ArrayBuffer ระหว่างการ coercion argument ใน Buffer#copy, Buffer#fill
    • memory leak ที่เกี่ยวข้องกับ crypto.scrypt, tlsSocket.setSession(), fs.watch()
    • double-free ระหว่างการจัดการ vendor prefix และ multi-layer background ของ CSS parser
    • crash จาก race condition ของ MessageEvent ระหว่างการเข้าถึงพร้อมกันของ BroadcastChannel หรือ MessagePort
  • ก่อนหน้านี้ก็ใช้กลไกหลายอย่างเพื่อเสริมความเสถียร
    • patch ให้ Zig compiler รองรับ Address Sanitizer และรันชุดทดสอบ ASAN ในทุก commit
    • บน Windows แจกจ่าย build แบบ Zig safety-checked ReleaseSafe
    • fuzz API ของ Bun runtime แบบ 24/7 ด้วย Fuzzilli
    • ดำเนินการทดสอบ memory leak แบบ end-to-end จำนวนมาก
  • ไม่ได้มีจุดยืนว่า Zig เองเป็นปัญหา แต่ข้อกำหนดที่ต้องจัดการค่าของ GC ร่วมกับหน่วยความจำที่จัดการเอง เป็นแหล่งหลักของปัญหาความเสถียร

เหตุผลที่เลือก Rust

  • JavaScript เป็นภาษาแบบ GC และเอนจินอย่าง JavaScriptCore กับ V8 มีกฎที่เข้มงวดเกี่ยวกับ exception handling และ GC
  • Zig ไม่ได้จัดการหน่วยความจำอัตโนมัติเหมือน C ไม่มี constructor·destructor และส่วนใหญ่ต้องระบุ cleanup ด้วย defer ที่แต่ละ call site
  • ใน Bun การจัดการ lifetime ของค่า GC และค่าที่จัดการด้วยตนเองให้ถูกต้อง เป็นแหล่งใหญ่ของปัญหาความเสถียร
    • ต้องตรวจสอบว่า byte ที่ allocate ถูก free ที่ไหน
    • ต้องรับประกันว่า free เพียงครั้งเดียว
    • ต้องตรวจสอบการจัดการ JavaScript exception ให้ถูกต้อง
    • ต้องตรวจสอบว่า GC pointer มองเห็นได้จาก conservative stack scanner หรือไม่
  • วิธี cleanup ของ Zig คือ defer, errdefer แบบชัดเจน ส่วน C++ ใช้ destructor และ move ขณะที่ Rust ใช้ Drop
  • ในโค้ด Zig เดิมของ Bun มีการผสมกันระหว่าง arena lifetime, reference counting และการ review อย่างละเอียด
  • แม้จะบังคับกฎ ownership ด้วย style guide และ code review ได้ แต่ในโค้ดที่ปลอดภัยของ Rust, use-after-free, double-free และ free ที่ตกหล่นใน error path จะกลายเป็น compiler error
  • โค้ดของ Bun ประมาณ 20% เป็น C++ และฝังไลบรารี C/C++ หลายตัว
    • JavaScriptCore
    • uWebSockets และ usockets
    • lshpack และ lsquic
    • BoringSSL
    • SQLite
  • C++ ก็เป็นตัวเลือกได้ แต่ยังคงต้องพึ่ง style guide และ code review และแม้มี ASAN ก็ยังอาจเกิด memory corruption และ leak ได้

กลยุทธ์การเขียนใหม่: ทำทีเดียวแบบเชิงกล

  • โค้ด Zig เดิมของ Bun มี 535,496 บรรทัด ไม่รวมคอมเมนต์ และการเขียนใหม่แบบดั้งเดิมถูกประเมินว่าเป็นงานที่ทีมวิศวกรขนาดเล็กต้องใช้เวลาประมาณ 1 ปี
  • เพราะไม่สามารถหยุดแก้บั๊ก แก้ความปลอดภัย และพัฒนาฟีเจอร์เป็นเวลา 1 ปีได้ จึงเลือก การ port แบบเชิงกล ที่ลดการเปลี่ยนแปลงพฤติกรรมผู้ใช้ให้เหลือน้อยที่สุดเป็นแนวทางที่เสี่ยงต่ำที่สุด
  • ชุดทดสอบของ Bun เขียนด้วย TypeScript จึงไม่ขึ้นกับภาษาที่ใช้ implement runtime
  • การเขียนใหม่แบบ incremental ถูกมองว่าจะเจ็บปวดในระยะสั้นและกลาง เพราะต้องสร้างโค้ดชั่วคราวและหวังว่าจะลบทิ้งได้ภายหลัง จึงย้ายทั้งหมดในครั้งเดียว
  • โค้ด Rust ถูกเขียนให้ดูเหมือน transpile มาจากโค้ด Zig และเลือกแนวทางค่อย ๆ ลด unsafe หลัง Bun v1.4 พร้อม refactor ไปเป็น Rust ที่ idiomatic มากขึ้น

Dynamic workflow ของ Claude Code

  • ในการเขียนใหม่ด้วย Rust มี dynamic workflow ประมาณ 50 รายการใน Claude Code ที่รันต่อเนื่องเป็นเวลา 11 วัน
  • workflow ไล่ตั้งแต่การเขียนคู่มือการ port, แปลงไฟล์, แก้ compiler error, กู้คืน subcommand, ทำให้ชุดทดสอบทั้งหมดผ่าน ไปจนถึง cleanup ขนาดใหญ่
    • สร้างคู่มือการ port ที่ map pattern และ type ของ Zig ไปเป็น pattern และ type ของ Rust
    • port ไฟล์ .zig ทั้งหมดเป็นไฟล์ .rs แบบเชิงกลตาม PORTING.md และ LIFETIMES.tsv
    • แก้ compiler error แยกตาม crate
    • กู้คืนการทำงานของ subcommand เช่น bun test, bun build
    • ทำให้ชุดทดสอบทั้งหมดผ่าน
    • refactor และ cleanup ขนาดใหญ่
  • ตลอดเวลาส่วนใหญ่ คนอ่าน output ของ workflow เพื่อตรวจสอบปัญหาและบั๊ก พร้อมปรับ prompt เพื่อให้ Claude แก้ loop
  • เป็นงานเตรียมการ มีการหารือกับ Claude ประมาณ 3 ชั่วโมงเกี่ยวกับวิธี map pattern ใน codebase Zig ไปเป็น Rust และผลลัพธ์นี้ถูก serialize เป็น PORTING.md
  • เพื่อเพิ่ม lifetime ของ Rust ให้โค้ดจัดการหน่วยความจำด้วยตนเอง ได้รัน workflow ที่วิเคราะห์ lifetime ของทุก struct field
    • ค้นหา field ที่มี lifetime ซับซ้อน
    • เสนอ lifetime
    • agent reviewer เชิงปฏิปักษ์ 2 ตัวตรวจสอบ
    • นำ feedback มาปรับและบันทึกเป็น LIFETIMES.tsv

วิธีรีวิวแบบปฏิปักษ์

  • มี Claude ผู้รีวิวแบบปฏิปักษ์ แยกอยู่คนละ context window กับ Claude ที่ทำ implementation แต่ละตัว โดยผู้รีวิวได้รับคำสั่งให้ดูเฉพาะ diff และสมมติว่าโค้ดผิดเพื่อค้นหา bug
  • โครงสร้างพื้นฐานประกอบด้วยผู้ implement 1 คน, ผู้รีวิวแบบปฏิปักษ์อย่างน้อย 2 คน และ fixer 1 คน
  • bug ที่ผู้รีวิวจับได้จริงทั้งหมดเป็นโค้ดที่ compile ผ่าน แต่มีปัญหาด้านพฤติกรรม
    • uv_close เป็น asynchronous แต่ Box<uv::Pipe> ถูก drop ที่ท้าย match arm ทำให้ libuv ถือ freed memory อยู่ จนเกิด use-after-free และ double-free
    • ข้อผิดพลาดของ timespec ที่ทำให้เกิด nsec ติดลบ เมื่อใช้ trunc() กับ file time ค่าลบที่ไม่ใช่จำนวนเต็ม
    • ข้อผิดพลาดที่ unwrap_or ประเมิน argument แบบ eager ทำให้เกิด panic ในกรณีละเว้น percentage ของ color-mix()
  • เช่นเดียวกับการรีวิวโดยมนุษย์ มีการแยก context ของผู้เขียนกับผู้รีวิว เพื่อลด bias ที่อาจเกิดจากการที่ผู้ implement อยากให้ merge

การทำ port ขนาดใหญ่และ parallelization

  • ก่อนย้ายไฟล์ .zig ทั้งหมด 1,448 ไฟล์ ได้ตรวจสอบขั้นตอนกับไฟล์ 3 ไฟล์ก่อน
    • ผู้ implement 1 คนเขียนไฟล์ .rs
    • ผู้รีวิว 2 คนตรวจสอบว่าพฤติกรรมตรงกับ .zig และทำตาม PORTING.md, LIFETIMES.tsv หรือไม่
    • fixer 1 คนนำข้อเสนอไปปรับใช้
  • ช่วงต้นของการ port ไฟล์ทั้งหมด Claude หลายตัวรัน git stash, git stash pop, git reset HEAD --hard จนชนกันเอง
  • หลังจากนั้นจึงเพิ่มกฎใน workflow เพื่อห้าม git stash, git reset, คำสั่ง git ที่ไม่ใช่การ commit ไฟล์เฉพาะ และคำสั่งช้าอย่าง cargo
  • สุดท้ายใช้ workflow shard 4 ชุดและ worktree 4 ชุด โดยในแต่ละ shard มี Claude 16 ตัว commit และ push ไฟล์
  • ด้วย parallelization และการเตรียมการล่วงหน้า ช่วง peak Claude เขียนโค้ดได้ประมาณ 1,300 บรรทัดต่อนาที
  • commit ใน port branch ที่ไม่รวม merge มี 6,502 commit, ชั่วโมง peak มี 695 commits และ landed diff สุดท้ายคือ +1,009,272 บรรทัด
  • ยังมีปัญหาที่ไม่ได้เพิ่ม IOPS พื้นฐานของ EC2 instance ทำให้เพียง grep ช้า ๆ ครั้งเดียวก็ทำให้การอ่าน/เขียนดิสก์หยุดไปหลายนาที

ข้อผิดพลาด compile และการแยก crate

  • หลังจากเขียนโค้ดทั้งหมดแล้ว Claude workflow จึงแก้ compiler error
  • codebase Zig โดยพื้นฐานแล้วเป็น compilation unit เดียว ขณะที่โค้ด Rust ตั้งใจจะแบ่งเป็นประมาณ 100 crate เพื่อให้ compile เร็วขึ้น
  • ประเภทข้อผิดพลาดที่ยากที่สุดคือ circular dependency
    • แค่ PR แยก crate ก่อนการ rewrite เป็น Rust ยังไม่เพียงพอ
    • workflow แยกต่างหากทำหน้าที่จัดประเภทและบันทึกว่าโค้ดที่มี circular dependency ควรอยู่ที่ไหน
    • workflow อีกชุดทำ refactoring ดังกล่าว
  • หลังแก้ circular dependency แล้ว ก็เผยให้เห็น compiler error ประมาณ 16,000 รายการ
  • ข้อผิดพลาดเหล่านี้ถูกประมวลผลแบบขนานแยกตาม crate
    • รัน cargo check ในแต่ละ crate
    • จัดกลุ่ม output ตามไฟล์แล้วบันทึกไว้
    • แก้ compiler error ของ crate นั้น
    • ผู้รีวิวแบบปฏิปักษ์ 2 คนตรวจสอบการเปลี่ยนแปลง
    • fixer 1 คนนำการแก้ไขไปปรับใช้
  • เคยมี false start ที่ Claude ตีความ “ทำให้ทุก crate compile ได้” เป็นการสร้าง function stub
  • เมื่อเกิด pattern ที่พยายามใช้ comment อธิบายยาว ๆ เพื่อ justify workaround จึงเพิ่มกฎรีวิวว่า “ถ้าต้องใช้ comment ยาวระดับย่อหน้า แปลว่าโค้ดผิดและต้องแก้โค้ด”

กระบวนการจนทดสอบผ่าน

  • หลัง cargo check ผ่านแล้ว จึงแก้ link error, panic ทันทีหลังเริ่มต้น, การรัน bun --version, bun test <file> ตามลำดับ
  • ใช้ workflow ที่บันทึก stacktrace ความล้มเหลวของแต่ละ CLI subcommand เป็นไฟล์ แล้วแก้ผ่าน loop ผู้ implement·ผู้รีวิว·fixer
  • workflow สำหรับไฟล์ทดสอบ shard ไฟล์ทดสอบแบบสุ่มประมาณ 100 ไฟล์ไปยัง worktree 4 ชุด และบันทึก stacktrace กับ error ของแต่ละ failure เพื่อแก้ไข
  • test suite มีการทดสอบ memory leak และ integration test ที่อาจ timeout ใน debug build
    • การทดสอบที่รัน next dev และตรวจว่า hot module reloading ตรวจพบการเปลี่ยนแปลง 100 ครั้ง
    • stress test ที่ใช้จำนวน TCP socket จนถึงขีดจำกัดสูงสุด
    • การทดสอบอ่าน/เขียนดิสก์ระดับกิกะไบต์
    • การทดสอบที่ spawn process ประมาณ 10,000 process
  • เพื่อ isolation จึงใช้ systemd-run และ cgroups จำกัดการใช้ memory/CPU และแยก pid namespace
  • ถึงอย่างนั้นเครื่องก็ crash หลายครั้งเพราะพื้นที่ดิสก์ไม่พอ
  • สองวันหลัง CI ครั้งแรก จำนวนไฟล์ทดสอบที่ล้มเหลวลดจาก 972 เหลือ 23 และอีกหนึ่งวันครึ่งหลังจากนั้น Linux ก็ green ทั้งหมด
  • สุดท้าย CI test ทั้งหมดผ่านครบ 6 แพลตฟอร์ม
    • macOS x64
    • macOS arm64
    • Linux x64
    • Linux arm64
    • Windows x64
    • Windows arm64
  • หลังทดสอบผ่าน 100% มนุษย์ตรวจด้วยตนเองว่าการทดสอบถูกรันจริงและไม่ได้ถูก skip แล้วจึง merge
  • จุดที่ merge เข้า main ยังไม่ใช่ versioned release และยังไม่มั่นใจพอที่จะ release แต่เป็นระดับความมั่นใจที่พอจะทุ่มเทกับ rewrite ต่อได้

ขนาดและต้นทุนการทดสอบ

  • ในช่วง 11 วัน ตั้งแต่ May 3 ถึง merge วันที่ May 14 มีการสร้าง 6,778 commits
  • test ไม่ได้ถูกลบหรือ skip
  • ขนาดการทดสอบแยกตามแพลตฟอร์มมีดังนี้
    • Debian 13 x64: expect() 1,386,826 ครั้ง, test 60,624 รายการ, ไฟล์ 4,174 ไฟล์
    • macOS 14 arm64: expect() 1,259,953 ครั้ง, test 58,850 รายการ, ไฟล์ 4,175 ไฟล์
    • Windows 2019 x64: expect() 1,007,544 ครั้ง, test 57,337 รายการ, ไฟล์ 4,173 ไฟล์
  • งานก่อน merge ใช้ uncached input token 5.9 พันล้าน, output token 690 ล้าน และ cached input token read 72 พันล้าน
  • คิดตามราคา API มีค่าใช้จ่ายประมาณ 165,000 ดอลลาร์
  • ประเมินว่าถ้ามนุษย์ทำเอง วิศวกร 3 คนที่มี context ของ codebase ทั้งหมดน่าจะใช้เวลาประมาณ 1 ปี
  • model ที่ใช้คือ pre-release Claude Fable 5 และมี disclosure ว่า Bun ถูก Anthropic เข้าซื้อในเดือนธันวาคม 2025

รีวิวความปลอดภัย, fuzzing และสถานะของ unsafe

  • หลัง merge port Rust แล้ว ได้ทำ security review 11 รอบ ด้วย Claude Code Security และจัดการ findings แล้ว
  • เพิ่ม fuzzing แบบ coverage-based ตลอด 24/7 ให้กับ parser ทั้งหมดของ Bun
    • JavaScript
    • TypeScript
    • JSX
    • CSS
    • JSON5
    • JSONC
    • TOML
    • YAML
    • Markdown
    • INI
    • Bun Shell scripts
    • semver ranges
    • ไฟล์ .patch
    • CSS colors
  • fuzzer ส่ง bug ที่พบไปให้ Claude เพื่อส่ง PR ที่รวมการ reproduce และการแก้ไข ส่วนมนุษย์รีวิว PR
  • จนถึงตอนนี้ parser ถูก execute ไปแล้ว 100,000 ล้านครั้ง และนำไปสู่ PR ประมาณ 15 รายการ
  • ณ เวลาที่เขียน โค้ด Rust ประมาณ 4% อยู่ใน unsafe block
    • keyword unsafe ประมาณ 13,000 รายการ
    • ประมาณ 27,000 บรรทัด / จากทั้งหมดประมาณ 780,000 บรรทัด
    • 78% ของ unsafe block เป็นแบบบรรทัดเดียว และเป็น pointer ที่มาจาก C++ หรือการเรียก C library
  • ระบุว่าเพราะยังใช้ library C/C++ อย่าง JavaScriptCore ต่อไป unsafe จึงจะมากกว่าโปรเจกต์ Rust ล้วนเสมอ

Regression ที่พบหลังเปลี่ยนมาใช้ Rust

  • Rust rewrite เป็นการเปลี่ยนแปลงครั้งใหญ่ จึงทำให้เกิด regression ที่ทราบแล้ว 19 รายการ และทั้งหมดได้รับการแก้ไขแล้ว
  • ส่วนใหญ่เกิดจากโค้ดที่ไวยากรณ์ของทั้งสองภาษาคล้ายกัน แต่ความหมายต่างกัน
  • side effect ภายใน debug_assert!

    • assert ของ Zig เป็นฟังก์ชัน ดังนั้น argument จะถูกเรียกใช้ในทุก build
    • debug_assert! ของ Rust เป็น macro ดังนั้นใน release build นิพจน์ทั้งหมดจะถูกลบออก
    • การเรียก insert_stale หายไปใน release build ทำให้เคส HMR บางกรณีของโปรเจกต์ HTML route ที่ใช้ React พัง
    • issue ที่เกี่ยวข้อง: #30678
  • slice ที่มีความยาวเป็นเลขคี่

    • Zig helper reinterpretSlice(u16, bytes) ของ Bun ใช้ @divTrunc จึงละเว้น trailing odd byte
    • bytemuck::cast_slice ของ Rust จะ panic เมื่อความยาวเป็นเลขคี่
    • มี regression ที่ Blob.text() ซึ่งมีไบต์เลขคี่ตามหลัง UTF-16 BOM ไม่คืนค่า string แต่ทำให้ process panic
    • การแก้ไขคือกลับไปละเว้น odd byte ด้วย &buf[..buf.len() & !1]
    • issue ที่เกี่ยวข้อง: #31188
  • Bounds checks

    • โค้ด Zig บน macOS และ Linux ถูกคอมไพล์ด้วย ReleaseFast ทำให้ bounds check ถูกลบออก ส่วน Rust release build ยังคง bounds check ไว้
    • ขนาด overflow block ของ Bun module resolver ยังเป็น placeholder 64 อยู่ ทำให้ ceiling ลดจาก 8.4 ล้าน interned filenames เหลือ 270,272
    • ptrs[4095] ที่ port มาเกิด off-by-one และกลายเป็นเคสที่โปรเจกต์จริงเข้าถึงได้ โดย Rust จะ panic แทนการเขียนออกนอกขอบเขต
    • issue ที่เกี่ยวข้อง: #31503
  • format strings แบบ comptime

    • Output.pretty ของ Zig มี fmt เป็น comptime ดังนั้น color marker อย่าง <r>, <d> จะถูกแปลงเป็น ANSI escape ก่อนแทนที่ argument
    • ฟังก์ชัน Rust ไม่มี comptime parameter จึงประมวลผล marker ใน string ที่เสร็จแล้ว และ rewrite ไปถึง argument อย่างผิดพลาด
    • ใน bun update -i การจบ OSC 8 hyperlink ชนกับ trailing <r> marker ทำให้ r ถูกพิมพ์ออกมาเป็นข้อความ
    • ใน Rust จำเป็นต้องใช้ macro bun_core::pretty!("<r>{}<r>", hyperlink)
    • issue ที่เกี่ยวข้อง: #30693

บั๊กและ memory leak ที่แก้ไขแล้ว

  • Bun v1.4.0 แก้ไข 128 บั๊ก ที่ reproduce ได้ใน v1.3.14
  • ครอบคลุมตั้งแต่ memory leak, crash ไปจนถึง help text ที่ลงสีผิด
  • Drop ของ Rust จะเรียกฟังก์ชัน drop โดยอัตโนมัติเมื่อค่าออกนอก scope
  • ใน Zig ต้องเพิ่ม defer ในแต่ละ call site จึงเกิดการลืม cleanup หรือ cleanup ซ้ำได้ง่าย
  • Drop ของ Rust เป็นการเลือกยอมรับ hidden control flow เพื่อแลกกับการลด footgun ที่พบบ่อย
  • Drop แก้ไข memory leak ที่เกี่ยวกับ file path ใน error handling code ได้หลายจุด
  • การผสานรวม LeakSanitizer ของ Bun ได้รับการปรับปรุง ทำให้ติดตาม native code memory allocations ทั้งหมดได้
  • instrumentable memory leak ทั้งหมดได้รับการแก้ไขแล้ว
  • ปรับปรุง leak ใน Bun.build()

    • ใน Bun v1.3.14 เดิม การเรียก Bun.build() แบบ in-process แต่ละครั้งทำให้ parsed source text และ AST symbol table มีอายุอยู่นานกว่าอายุของ build และ leak ทีละหลาย MB
    • ในการทดสอบที่ bundle โปรเจกต์ 60-module เดิม 2,000 ครั้งใน process เดียว v1.3.14 leak ต่อเนื่องประมาณ 3MB ต่อ build
    • ใน Bun v1.4.0 การใช้หน่วยความจำทรงตัว
    • | Builds | Bun v1.3.14 | Bun v1.4.0 |
    • | --- | ---: | ---: |
    • | 500 | 1,914 MB | 526 MB |
    • | 1,000 | 3,506 MB | 586 MB |
    • | 1,500 | 5,097 MB | 608 MB |
    • | 2,000 | 6,745 MB | 609 MB |

ขนาด binary, การใช้ stack และประสิทธิภาพ

  • แค่การเปลี่ยนแปลงช่วงต้นของ Rust rewrite ก็ทำให้ขนาด binary ลดลง
    • Windows: ลดลง 3.8 MB
    • macOS: ลดลง 5.5 MB
    • Linux: ลดลง 6.8 MB
  • สาเหตุหลักคือการใช้ comptime มากเกินไปในโค้ด Zig
  • หลังจากนั้นยังมีการใช้ Identical Code Folding, ลบ unused data ของ ICU และทำให้บางส่วนของ libicu ถูกคลายการบีบอัดแบบ lazy ด้วย zstd dictionary
  • เมื่อนำ Rust rewrite, การเปลี่ยนแปลง ICU และ identical code folding มารวมกัน ขนาด binary ของ Bun บน Linux และ Windows ลดลงประมาณ 20%
Version Platform Size
Bun v1.4.0 canary Windows 76 MB
Bun v1.3.14 Windows 94 MB
Bun v1.4.0 canary Linux 70 MB
Bun v1.3.14 Linux 88 MB
  • TOML parser และ recursive-descent parser ของ Bun ใช้ stack space น้อยลง
  • LLVM IR codegen ของ Rust ปล่อย intrinsic llvm.lifetime.start และ llvm.lifetime.end ให้กับ stack variable ทำให้ LLVM สามารถนำ stack slot กลับมาใช้ซ้ำได้
  • ก่อนหน้านี้ต้อง refactor ฟังก์ชันขนาดใหญ่เป็นฟังก์ชันเล็กหลายตัวด้วยมือ โดยเฉพาะเพื่อเลี่ยง open issue ของ Zig
  • Rust รองรับ cross-language link-time optimization ระหว่าง C/C++ กับ Rust ทำให้สามารถ inline ข้ามภาษาได้
  • benchmark บน Linux x64

    • เปรียบเทียบ Bun v1.3.14 กับ Bun v1.4.0 บน Linux x64 EC2 Xeon Platinum 8488C
    • วัด HTTP throughput ด้วย oha และ app workload ด้วย hyperfine
    • | server | Bun v1.3.14 | Bun v1.4.0 | Δ |
    • | --- | ---: | ---: | ---: |
    • | Bun.serve | 169.6k req/s | 177.7k req/s | +4.8% |
    • | node:http | 103.8k req/s | 108.5k req/s | +4.5% |
    • | Elysia | 158.9k req/s | 163.3k req/s | +2.8% |
    • | express | 64.5k req/s | 66.6k req/s | +3.2% |
    • | fastify | 91.5k req/s | 95.9k req/s | +4.8% |
    • | workload | Bun v1.3.14 | Bun v1.4.0 | Δ |
    • | --- | ---: | ---: | ---: |
    • | next build | 13.62 s | 13.03 s | +4.5% |
    • | vite build | 1.69 s | 1.65 s | +2.2% |
    • | tsc -b --force | 0.94 s | 0.89 s | +4.7% |

กรณีการใช้งานจริงและสถานะการเผยแพร่

  • Prisma เปิดตัว public beta ของ Prisma Compute บน Rust rewrite ของ Bun
  • ฝั่ง Prisma ระบุว่าได้ทดสอบ failure mode อย่าง connection pool ที่ไม่สามารถกู้คืนได้หลัง VM pause/resume และ memory leak บน Rust rewrite แล้ว และสามารถจัดการ failure mode ดังกล่าวได้ดี
  • Claude Code v2.1.181 และเวอร์ชันหลังจากรีลีสวันที่ 17 มิถุนายน ใช้ Bun ที่พอร์ตเป็น Rust
  • Linux startup ของ Claude Code เร็วขึ้น 10% และนอกเหนือจากนั้น ผู้ใช้ส่วนใหญ่แทบไม่สังเกตเห็นความแตกต่าง
  • Bun v1.3.14 เป็น Bun เวอร์ชันสุดท้ายที่เขียนด้วย Zig
  • Bun v1.4.0 เป็น Bun เวอร์ชันแรกที่เขียนด้วย Rust และให้ใช้งานในรูปแบบ canary

เครื่องมือที่ทีมได้รับและงานที่ยังเหลือ

  • codebase Rust ใหม่ยังคงมีรูปแบบใกล้เคียงกับ codebase Zig เดิมมาก
  • เขียนขึ้นเพื่อให้คนที่เข้าใจโค้ด Zig เดิมสามารถเข้าใจโค้ด Rust ที่แปลแบบเชิงกลได้เช่นกัน
  • การรีวิว PR ของ Rust rewrite ดำเนินไปโดยตรวจสอบว่า adversarial review agent สามารถจับความไม่สอดคล้องระหว่าง Zig กับ Rust การปฏิบัติตาม porting guide และ lifetime guide ได้อย่างถูกต้องหรือไม่ และให้คนอ่านโค้ดจำนวนมากแบบ side-by-side
  • Bun v1.4 ทำให้ Bun เร็วขึ้น เล็กลง ใช้หน่วยความจำน้อยลง และมอบเครื่องมือสำหรับการปรับปรุงเสถียรภาพ
    • Rust borrow checker
    • Miri
    • LeakSanitizer
    • coverage-guided fuzzing ตลอด 24/7 สำหรับ parser
  • ยังมีส่วนที่ต้อง refactor เหลืออยู่ และมีการเชื่อมต่อ bun-unsafe-audit
  • วิศวกรหนึ่งคนเฝ้าติดตาม Fable และ Claude Code อย่างใกล้ชิด จนไปถึงสถานะที่ test suite ทั้งหมดผ่านบนทุกแพลตฟอร์มได้ภายในเวลาเพียง 11 วัน

1 ความคิดเห็น

 
GN⁺ 3 시간 전
ความคิดเห็นบน Lobste.rs
  • แม้จะใช้ C++ ก็น่าจะเป็นตัวเลือกที่สมเหตุสมผลสำหรับ Bun อยู่ดี น่าจะได้ประโยชน์จาก constructor และ destructor และลบโค้ด wrapper แบบ extern "C" ออกไปได้มาก แต่ก็ยังต้องพึ่ง style guide ที่บังคับผ่าน code review อยู่ดี และแม้มี ASAN ก็ยังคงเกิด memory corruption และ memory leak ต่อไป
    น่าสนใจที่ Node.js ทำงานได้ดีแม้ใช้ C++ แต่ผมไม่เคยมอง Bun เป็นโปรเจกต์จริงจัง ตอนนี้มันดูเหมือน test bench ของฝ่ายการตลาด Anthropic เลยคิดว่าจะถอยห่างต่อไป
    • Node เองก็เคยมี CVE ด้าน memory safety จากโค้ดไลบรารีของตัวเองอย่างแน่นอน แค่ค้นว่า "nodejs memory cves" ก็เจอหลายรายการทันที
    • การบอกว่า Node ทำงานได้ดีด้วย C++ ก็อยู่ในระดับเดียวกับการบอกว่า Bun ทำงานได้ดีด้วย Zig นั่นแหละ Node มีความสนใจและผู้ใช้มากกว่า ปัญหาเลยถูกพบเร็วกว่า สุดท้ายทั้งคู่ก็อยู่ในสถานการณ์แบบ “ถ้าระวังสุด ๆ และไม่มีใครทำพลาด ก็จะทำงานได้สมบูรณ์แบบ”
      สปอยล์คือ ในความเป็นจริงไม่ได้ระวังกันขนาดนั้น และก็มีการทำพลาดด้วย
  • ประโยคที่ว่า “ประมาณ 4% ของโค้ด Rust ของ Bun อยู่ในบล็อก unsafe และ 78% ในนั้นเป็นบล็อกบรรทัดเดียว” ดูเหมือนเป็นคำพูดเพื่อให้สบายใจ แต่การที่บล็อก unsafe มีบรรทัดเดียวหรือไม่ไม่ใช่ประเด็น ถ้าข้างในนั้นทำลายการรับประกันความปลอดภัย โค้ดทั้งหมดนอกบล็อกก็อาจมี soundness ที่ถูกทำลายได้ เช่นกัน
    การ merge ช่วงแรกของพอร์ต Rust ของ Bun มี unsoundness ชัดเจนแบบนี้อยู่: https://github.com/oven-sh/bun/issues/30719
    issue นั้นได้รับการรับมือโดยที่ผู้ดูแลเปิดใช้เครื่องมือ Miri ของ Rust ใน CI และในส่วน “What's Next” ของบทความก็มี Miri (which runs for a growing chunk of code in CI) รวมอยู่ด้วย ดังนั้นดูเหมือนว่ากำลังทำงานไปในทิศทางนั้น ซึ่งเป็นเรื่องดี
    ถ้าพูดอย่างเป็นธรรม Rust ที่มีการละเมิดความปลอดภัยก็ยังอาจดูแลง่ายกว่าได้ ขึ้นอยู่กับคุณภาพของโค้ด Zig ที่มันเข้ามาแทนที่ ถึงอย่างนั้น จำนวนบรรทัดโค้ดต่อบล็อก unsafe ก็ไม่ใช่ตัวชี้วัดคุณภาพ โดยเฉพาะอย่างยิ่งถ้าบล็อกเหล่านั้นไม่มีแนวปฏิบัติการเขียนโค้ดอื่น ๆ ความเชี่ยวชาญ หรือการตรวจสอบอัตโนมัติประกอบด้วย
    • ประโยคนั้นดูเหมือนไม่ได้ตั้งใจให้สบายใจ แต่ใกล้เคียงกับการบอกว่า บล็อก unsafe ไม่ได้เกิดจากกระบวนการพอร์ต แต่เกิดจาก ข้อกำหนดของโปรเจกต์ มากกว่า ถ้าเรียกใช้ไลบรารี C ก็จำเป็นต้องมีบล็อก unsafe และไม่มีทางกำจัดได้ด้วยการ refactor อย่างเดียว
      แน่นอนว่าถ้าเขียนไลบรารี C นั้นใหม่ด้วยก็เป็นไปได้ แต่นั่นอาจค่อยพิจารณาภายหลัง
  • หนึ่งในจุดที่น่าสนใจที่สุดของ Bun คือโดยพื้นฐานแล้วมันเป็นเรื่องราวของ วิศวกรคนเดียวที่ทำงานได้มากกว่าที่คาดไว้มาก ตอนแรก Jarred บอกว่าเป็นเพราะ Zig คราวนี้บอกว่าเป็นเพราะ Claude แต่ถ้าจะบอกว่าเป็นเพราะตัว Jarred เองหรือ work ethic ของเขาก็ไม่แปลก อาจเป็นพลังของทีมเล็ก ๆ หรืออาจเป็นเพราะเป็นการเขียนและ implement สิ่งที่มีอยู่แล้วขึ้นมาใหม่ก็ได้
    ในประกาศ “Bun is joining Anthropic” Jarred บอกว่าจะจ้างวิศวกรเพิ่มเพื่อทำงานกับ Bun แต่ถ้าดูแค่ GitHub ทีม Bun กลับดูเหมือนเล็กลงด้วยซ้ำ ไม่แน่ใจว่าจะสรุปอะไรได้จากตรงนี้ แค่ประมาณว่า “Bun เป็นทีมเล็ก” เท่านั้น
  • พูดว่า “การเขียนใหม่เป็นความคิดที่แย่มาก” แล้วก็เดินหน้า เขียนใหม่ ทันทีนั่นเอง
    วิธีการเองน่าสนใจ แต่บทความอ่านแล้วเหมือนบทความการตลาด ขาดการวิเคราะห์ว่าต้นทุนเท่าไร ไม่มีความเสี่ยงของการเขียนใหม่ด้วย Rust และคำอธิบายเชิงรูปธรรมว่าทำไมถึงเกิดการเขียนใหม่ตั้งแต่แรกก็อ่อน เดาว่าอาจเป็นเพราะ no ai policy ของ Zig หรืออาจมีนโยบายภายใน Anthropic ที่ต้องการโฟกัสกับ Rust
    • เรื่องต้นทุน อย่างน้อยย่อหน้านี้ก็ดูเกี่ยวข้อง: “ก่อน merge ใช้ input token ที่ไม่ถูก cache 5.9 พันล้าน token, output token 690 ล้าน token และการอ่าน input token ที่ถูก cache 72 พันล้าน token คิดตามราคา API แล้วประมาณ 165,000 ดอลลาร์
      ส่วนเหตุผลที่เขียนใหม่นั้น ผมว่าบทความเล่าเรื่องได้ค่อนข้างสอดคล้อง Bun ยังคง crash ต่อเนื่องแม้จะมีมาตรการเพื่อจับปัญหาแล้ว และนักพัฒนาก็ต้องการวิธีป้องกันปัญหาเหล่านี้อย่างเป็นระบบมากขึ้น
      แผนแรกคือบังคับใช้สไตล์การเขียนโค้ดบางแบบให้เข้มขึ้น และนำ smart pointer มาใช้ แต่ Jared มองว่า smart pointer ที่ทำเองใช้งานแย่กว่า Rust และไม่มีการรับประกันเท่า Rust จากนั้นจึงกลายเป็นว่า “ลองทดสอบสักสัปดาห์ไหมว่าโมเดลใหม่ของ Anthropic จะเขียน Bun ใหม่เป็น Rust ได้หรือเปล่า?” และเมื่ออัตราการผ่าน test suite สูงขึ้น กระแสก็ดูเหมือนเปลี่ยนจาก “น่าลอง” เป็น “จะ merge”
      กล่าวคือ ดูไม่เหมือนตัดสินใจเขียนใหม่ด้วย Rust ตั้งแต่แรก แต่ใกล้เคียงกับ “Rust ดูเหมือนให้ทางแก้ปัญหา แต่ทำไม่ได้เพราะต้นทุนการเขียนใหม่สูง ทว่าพอลองพอร์ตด้วย LLM แล้วเห็นความเป็นไปได้ งั้นก็ไปทางการเขียนใหม่ด้วย LLM กัน” มากกว่า
    • “ก่อน merge ใช้ input token ที่ไม่ถูก cache 5.9 พันล้าน token, output token 690 ล้าน token และการอ่าน input token ที่ถูก cache 72 พันล้าน token คิดตามราคา API แล้วประมาณ 165,000 ดอลลาร์
    • เหตุผลในการเขียนใหม่ดูมีความชอบธรรมเพียงพอ มีความกังวลเรื่อง ปัญหา memory safety ที่เกิดซ้ำ และความกลัวการแก้บั๊กแบบไล่ตีตัวตุ่นไม่รู้จบ วิศวกรบางคนอาจรับมือด้วยวิธีอื่น แต่การเลือกเขียนใหม่เป็น Rust ด้วย LLM ก็เป็นหนึ่งในวิธีแก้ปัญหานี้
    • แน่นอนว่าอ่านแล้วเหมือนบทความการตลาด ไม่ว่าจะตั้งใจหรือไม่ก็ตาม แต่ในทางปฏิบัติการคำนวณต้นทุนแทบเป็นไปไม่ได้ โดยเฉพาะถ้านักพัฒนาเข้าถึงโมเดล Anthropic ก่อนเปิดตัวได้ อย่างไรก็ตาม ถ้าคนทั่วไปอย่างเราจะเขียนใหม่ในสเกลนั้นด้วยโมเดล Claude ปัจจุบัน ก็พอประเมินต้นทุนได้
  • แทนที่จะคอยแก้บั๊กพวกนี้ทีละตัว เหตุผลที่ว่าควรทำให้ดีกว่านี้เพื่อผู้ใช้ที่พึ่งพาอยู่ และควรป้องกันอย่างเป็นระบบไม่ให้บั๊กแบบนี้เกิดซ้ำ ถือเป็นเหตุผลที่ยอดเยี่ยม ถึงอย่างนั้น แม้เหตุผลจะเป็นแค่ว่า “กระแส Rust กำลังดี” ผมก็คงยอมรับได้เหมือนกัน
    ขอแสดงความยินดีกับ Jarred, ทีม Bun และ Anthropic ที่ทำสิ่งนี้สำเร็จ