What the Fork - เครื่องมือแสดงภาพการบิลด์แบบเรียลไทม์สำหรับ C/C++/Rust
(danielchasehooper.com)- What the Fork เป็นเครื่องมือข้ามแพลตฟอร์มที่ใช้ แสดงภาพกระบวนการบิลด์แบบเรียลไทม์สำหรับ C/C++/Rust และอื่น ๆ
- ช่วยให้มองเห็นปัญหาเชิงโครงสร้างของระบบบิลด์เดิมได้ง่าย เช่น การประมวลผลแบบขนานที่ไม่เพียงพอ และกระบวนการที่ไม่มีประสิทธิภาพ
- ทำงานได้กับทุกระบบบิลด์และทุกภาษาโปรแกรม และรองรับ เครื่องมือบิลด์หลากหลาย เช่น make, ninja, gradle, zig, cargo
- ใช้ การมอนิเตอร์ system call เพื่อแสดงภาพเวลาในการทำงาน คำสั่ง และความสัมพันธ์ของการพึ่งพาระหว่างแต่ละโปรเซสในรูปแบบกล่อง
- เป็นเครื่องมือที่มีประโยชน์มากสำหรับการปรับแต่งบิลด์ การวิเคราะห์คอขวด และการปรับปรุงประสิทธิภาพ CI
บทนำและที่มา
- What the Fork เป็นเครื่องมือแสดงภาพการบิลด์แบบเรียลไทม์ที่พัฒนาขึ้นเพื่อวินิจฉัยสาเหตุที่ทำให้การบิลด์ช้าผ่านภาพที่เข้าใจง่าย
- บางโปรเจ็กต์อย่าง LLVM อาจคอมไพล์ช้าเพราะปริมาณโค้ดมีมากจริง แต่การบิลด์ส่วนใหญ่มักใช้เวลานานเกินจำเป็นเพราะการตั้งค่าที่ไม่มีประสิทธิภาพ
- ที่ผ่านมา การตรวจสอบปัญหาของการบิลด์โดยตรงหรือมองเห็นปัญหาเชิงโครงสร้างได้ในภาพรวมทำได้ยาก จึงเกิดความต้องการเครื่องมือนี้
- เครื่องมือนี้ถูกออกแบบมาให้เป็นแบบข้ามแพลตฟอร์ม และใช้ได้กับทุกระบบบิลด์และทุกภาษา
ฟีเจอร์หลักและวิธีใช้งาน
- What the Fork ไม่ใช่เพียง system profiler ทั่วไป แต่เป็นเครื่องมือสำหรับวินิจฉัยปัญหาที่เฉพาะกับงานบิลด์
- ตัวอย่างเช่น การไม่ใช้แฟล็ก
-jเมื่อใช้ make, การกระจุกตัวของเวลาในไฟล์หรือขั้นตอนคอมไพล์บางจุด, และการตรวจจับคำสั่งที่ทำงานแบบลำดับทั้งที่สามารถทำแบบขนานได้ - มีประสิทธิภาพอย่างมากโดยเฉพาะกับการวิเคราะห์และปรับแต่งประสิทธิภาพของ clean build ใน สภาพแวดล้อม CI
- วิธีใช้คือใส่คำสั่ง
wtfนำหน้าคำสั่งบิลด์ (เช่นwtf make,wtf cargo build,wtf npm run build) - เมื่อเริ่มบิลด์แล้ว UI จะทำงานและอัปเดตสถานะความคืบหน้าของแต่ละโปรเซสแบบเรียลไทม์
UI และวิธีการแสดงภาพ
- แต่ละโปรเซสของการบิลด์จะแสดงบนไทม์ไลน์ในรูปแบบกล่อง และแยกประเภทด้วยสี
- ความสัมพันธ์แบบพ่อ-ลูกของโปรเซสจะแสดงเป็นโครงสร้างซ้อนกัน
- แผงด้านล่างจะแสดงเวลาในการรันของโปรเซสที่เลือก ไดเรกทอรีทำงาน และข้อมูลอาร์กิวเมนต์ของคำสั่งทั้งหมด
หลักการทำงาน
- การบิลด์คือการทำงานร่วมกันของหลายโปรเซส (เช่น
bash,clang,ld) - การบิลด์ขนาดใหญ่ใช้เครื่องมือบิลด์หลากหลาย เช่น
cargo,make,bazel,gradle,xcodebuildซึ่งเบื้องหลังทำงานกับคำสั่งจำนวนมาก การพึ่งพา แคช และการจัดตารางงาน - การดูแค่เอาต์พุตในเทอร์มินัลไม่สามารถทำให้เข้าใจโปรเซสที่ซ้อนกันอยู่ (เช่น
ldที่clangเรียกภายใน) และโครงสร้างเวลาอย่างละเอียดได้ - เพื่อแก้ปัญหานี้ เครื่องมือจึงใช้ system call ที่ตรวจจับการเริ่มต้นและสิ้นสุดของโปรเซส ตามแต่ละระบบปฏิบัติการ (macOS: Endpoint Security API, Linux:
ptrace(), Windows: Event Tracing for Windows) - ด้วยวิธีนี้จึงสามารถกู้คืนลำดับเวลาของกระบวนการบิลด์ทั้งหมด และระบุเส้นทางการทำงานกับเวลาที่ใช้ในแต่ละขั้นตอนได้
- นอกจากงานบิลด์แล้ว ยังสามารถนำไปใช้ติดตาม subprocess รูปแบบต่าง ๆ ได้ด้วย
กรณีใช้งานจริงและสิ่งที่ค้นพบ
- วิศวกรหลายคน (จาก Delta, Mozilla, Apple) ได้นำไปใช้กับโปรเจ็กต์จริงและพบปัญหาที่ไม่คาดคิด
- ตัวอย่าง 1: ในโปรเจ็กต์โอเพนซอร์สที่ใช้ Cargo พบว่าไฟล์ต่าง ๆ ถูกคอมไพล์แบบลำดับ ทำให้เห็นชัดว่าขาดการทำงานแบบขนาน (บน CPU 10 คอร์มีศักยภาพในการเร่งความเร็วได้มากกว่า 10 เท่า)
- ตัวอย่าง 2: ในการบิลด์ LLVM ด้วย Ninja ทุกคอร์ของ CPU ทำงานแบบขนานได้อย่างมีประสิทธิภาพ จนได้ประสิทธิภาพการบิลด์ที่ใกล้อุดมคติ
- ตัวอย่าง 3: ในโปรเจ็กต์ที่ใช้ CMake พบโครงสร้างที่ไม่มีประสิทธิภาพ โดยมีการรันซ้อนของ cmake/make/clang และมีการตรวจสอบเวอร์ชัน Xcode/OS ซ้ำถึง 85 ครั้ง ทั้งที่งานจริงมีเพียงเล็กน้อย
- ตัวอย่าง 4: ในโปรเจ็กต์ Objective-C ขนาดใหญ่ที่ใช้ xcodebuild พบว่าช่วงท้ายของการบิลด์มีการประมวลผลแบบขนานไม่เพียงพอ และก่อนเริ่มบิลด์ยังมีช่วงที่ไม่มีการทำงานยาว 6 วินาที (ขณะที่ ninja เริ่มคอมไพล์ได้ภายใน 0.4 วินาที)
- ตัวอย่าง 5: เมื่อ Zig คอมไพล์ Orca Project ลำดับการบิลด์ของ dependency ถูกกำหนดแบบสุ่ม ทำให้ประสิทธิภาพของการประมวลผลแบบขนานเปลี่ยนไปตามดวง โดยมีบาง dependency ถูกเลื่อนไปทำช่วงท้ายจนลดความขนานลง
- ตัวอย่าง 6: ในโปรเจ็กต์ GitHub CLI ที่ใช้ make/go พบว่าเวลาสำหรับดาวน์โหลด dependency สูงมาก จึงคาดว่าจะปรับความเร็วการบิลด์ได้หากลด dependency ลง
ผลลัพธ์จากการใช้งานและข้อจำกัด
- การวิเคราะห์ไทม์ไลน์แบบภาพช่วยให้ค้นพบคอขวดที่ไม่คาดคิด การพึ่งพาที่ซ้ำซ้อนโดยไม่จำเป็น และจุดที่ขาดความขนานได้
- สามารถระบุจุดปรับปรุงเชิงโครงสร้างได้อย่างรวดเร็ว เช่น ปัญหา dependency การทำงานซ้ำโดยไม่จำเป็น หรือความไม่มีประสิทธิภาพของเครื่องมือบางชนิด เพื่อนำไปใช้ปรับแต่งประสิทธิภาพการบิลด์ได้โดยตรง
- การดูคำสั่งทั้งหมดของแต่ละโปรเซสยังช่วยให้วิเคราะห์ได้ละเอียดมากขึ้น
โปรแกรมเบต้า
- What the Fork ทำงานได้บน Windows, Linux และ macOS
- บุคคลหรือทีมที่ต้องการให้ฟีดแบ็กสามารถสมัครเข้าร่วม private beta ได้ (มีลิงก์ Google Form ให้)
3 ความคิดเห็น
คำสั่งมันตลกเกินไปนะครับ 555 ชื่อว่า wtf ซะด้วย…
ถ้าออกมาเป็นโอเพนซอร์สก็คงดีนะ
ความคิดเห็นจาก Hacker News
ตอนนี้ติดอยู่ในสภาพแวดล้อมที่ใช้ได้แค่ CMake, GCC และ Unix Make เลยแทบเป็นไปไม่ได้ที่จะได้ข้อมูลละเอียดว่าทำไมบิลด์ถึงช้า บิลด์ก็ยุ่งเหยิงมาก มีขั้นตอนบิลด์ซับซ้อนอย่างการคัดลอกไฟล์จากซอร์สไปยังโฟลเดอร์บิลด์ มีหลายภาษา (C, C++, Fortran, Python) มีสเต็ป CMake แบบกำหนดเอง ฯลฯ ถ้าเครื่องมือนี้ทำงานได้ดีแม้ในสภาพแวดล้อมที่วุ่นวายแบบนี้ ก็น่าจะเรียนรู้อะไรได้อีกมากจริงๆ
tsoding ได้เขียนไลบรารี C แบบ single-header สำหรับการบิลด์ข้ามแพลตฟอร์มไว้ที่ https://github.com/tsoding/nob.h โดยต้องใช้แค่ cc เท่านั้น และยังใช้เครื่องมือโปรไฟล์ของ GDB เพื่อดูขั้นตอนการบิลด์ได้ด้วย คิดว่าเป็นไอเดียที่เจ๋งมาก แม้อาจจะไม่เหมาะกับผู้เขียนโพสต์นี้ แต่ถ้าต้องจัดการหลายภาษา Nix ก็เป็นเครื่องมือบิลด์ที่ยอดเยี่ยม
ผมเคยทำเครื่องมือ trace/profile เวลาคอมไพล์เป็น GCC plugin ขึ้นมาเอง ถ้าสนใจก็ดูได้ที่: https://github.com/royjacobson/externis
ตอนที่พยายามลดเวลาคอมไพล์ของเกมเอนจินตัวเอง ผมเคยใช้ขนาดของผลลัพธ์ที่คอมไพล์แล้วเป็นตัวชี้วัดแทน เพราะเวลาแบบ wall-clock ไม่นิ่งมาก ขนาดไบนารีซึ่งให้ผลเหมือนกันในแต่ละบิลด์ แม้กระทั่งบนคนละเครื่อง กลับจัดการได้ง่ายกว่า ถึงจะไม่ตรง 100% แต่ในทางปฏิบัติก็ช่วยได้มาก
กำลังเจอปัญหาคล้ายกันอยู่ และบ่อยครั้งก็เห็น CMake คอมไพล์ไฟล์ที่ไม่ได้แก้ใหม่ด้วย เช่น แก้ .cpp นิดเดียวโดยไม่มีการเปลี่ยน interface เลย แต่ object ที่ไม่เกี่ยวกันเลยก็ยังถูกคอมไพล์ใหม่ บางทีก็สงสัยว่า CMake สร้าง dependency ที่เข้มกว่าความเป็นจริงของไฟล์หรือเปล่า เลยทำให้เวลา build ยาวโดยใช่เหตุ
อยากเสนอให้ผู้เขียนบล็อกเอา gif ที่อัดการบิลด์แอป macOS ไปไว้ด้านบนของหน้า ใต้ header ลงมาทันที การแสดงสิ่งที่สร้างได้ก่อนแล้วค่อยอธิบายทีหลังน่าจะดูดีมาก
ชอบโปรเจกต์นี้มาก ผมเคยลองแนวทางคล้ายกันในปี 2018 โดยใช้ strace, dtruss และ https://buildinfer.loopperfect.com/ เพื่อทำอะไรประมาณการสร้างไฟล์ BUCK อัตโนมัติ แล้วก็ใช้ graphviz, perfetto.dev ฯลฯ เพื่อทำ visualization เสียดายที่ไม่ได้แพ็กเกจมันเป็นผลิตภัณฑ์จริงจัง แต่ในงานคอนซัลต์มันช่วยมากทั้งด้านวิเคราะห์สาเหตุและการย้ายไป BUCK/Bazel ช่วงหลังก็กลับมาดูอีกครั้งเพื่อคิดเรื่องการประยุกต์ใช้ให้กว้างขึ้น วิธีนี้มีความท้าทายทางเทคนิคโดยเนื้อแท้อยู่เหมือนกัน เช่น ถ้าบันทึก system call log ลงดิสก์ ขนาดจะพุ่งไปเป็นหลักหลายสิบถึงหลายร้อย GB (เช่น llvm 50GB และบางกรณีเกิน 100GB) แถมยังต้องจัดการให้ครอบคลุมขั้นตอนบิลด์ผ่าน https, IPC ฯลฯ ด้วย (เมื่อก่อนเคยมีลูกค้ารายหนึ่งดึงโค้ดจาก Firebird DB ด้วย Perl ทุกครั้งที่บิลด์) และเพราะมันเป็นการวิเคราะห์ตอนรันจริง จึงต้องวิเคราะห์ซ้ำสำหรับแต่ละคอนฟิกการบิลด์ด้วย
คิดว่าเจ๋งมาก รู้สึกว่ามีปัญหาหลายอย่างที่เราพลาดไปเพราะไม่มี visualization แบบนี้ เมื่อ 10 ปีก่อนตอน optimize ระบบ build ของ Mozilla ถ้ามีเครื่องมือนี้คงช่วยได้มากเหมือนกัน ในบทความนี้ถ้าเล่าเพิ่มอีกหน่อยว่าจับปัญหาอะไรได้จริงบ้างก็น่าจะดี
เคยใช้ ninjatracing กับ
-ftime-traceของ Clang ในโปรเจกต์ C++ ที่จัดการด้วย CMake เพื่อทำภาพรวมประสิทธิภาพการบิลด์ได้สำเร็จ ถ้าใช้ ClangBuildAnalyzer เพิ่ม ก็จะวิเคราะห์ได้ละเอียดขึ้นว่า compiler ใช้เวลาไปกับตรงไหนคิดว่าเจ๋งมาก อยากรู้ว่ามีแผนจะโอเพนซอร์สไหม เพราะผมก็กำลังทำอะไรคล้ายๆ กันอยู่และอยากร่วมมือด้วย
ถ้าใช้ Visual C++ compiler บน Windows ก็ขอแนะนำ vcperf ด้วย มันมีมากับ VS2022 อยู่แล้ว หรือจะ build จาก GitHub มาใช้เองก็ได้ ผมเคยใช้กับโปรเจกต์ที่สร้างจาก UBT หรือ CMake เหมือนกัน จำไม่ได้ว่ามันช่วยตัดสินคุณภาพของการ parallelize build ได้โดยตรงหรือไม่ แต่ดูข้อมูลฝั่ง compiler frontend ได้ง่ายมาก โดยเฉพาะการหา header ที่ถูก include บ่อยหรือหนักโดยเนื้อแท้
โดยทั่วไปคนมักมองข้ามประเด็นสำคัญว่า build logic ที่ “baked in” (คำนวณไว้ล่วงหน้า) อยู่ในระบบบิลด์นั้น ไม่ได้ติดตามอย่างละเอียดจริงๆ ว่าจะได้รับผลกระทบจากการเปลี่ยนแปลงใดบ้าง ตัวอย่างเช่นใน ninja มี build logic บางส่วนที่ใส่ไว้ล่วงหน้า จึงทำงานได้เร็ว ผมเคย benchmark การ full build ของ Xerces-C++ ทั้งบน ninja (ตั้งค่าด้วย CMake) และ build2 (เครื่องมือที่จัดการทั้งการตั้งค่าและการติดตามการเปลี่ยนแปลงระหว่างบิลด์) โดย ninja ใช้เวลา 3.23 วินาที ส่วน build2 ใช้ 3.54 วินาที ถ้าคงไฟล์บางส่วนที่ CMake สร้างไว้แล้ว build ซ้ำ เวลาจะลดลงเป็น 3.28 วินาที อนึ่ง ขั้นตอน configure ของ CMake อย่างเดียวใช้ 4.83 วินาที ดังนั้นการ build แบบ full stack ด้วย CMake+ninja จริงๆ แล้วใช้เวลาประมาณ 8 วินาที ซึ่งมักเป็นเวลาที่ต้องเจอจริงเมื่อใช้มันในฐานะไลบรารี
เคยลองแนวทางคล้ายกันโดยเปิด Instruments ระหว่าง build เพื่อดูว่าโปรเซสไหนทำอะไรเมื่อไร ข้อเสียคือถ้า build นาน Instruments จะอืด และมันกรองตาม process tree ไม่ได้เลยทำให้ใช้งานไม่ค่อยสะดวก แต่ตอนช่วยลดเวลา build โค้ด iOS ของ Twitter ลงได้มาก มันมีประโยชน์มาก ช่วงหลังมานี้ tracing แบบ “All Processes” ของ Instruments พังไปแล้ว เลยใช้วิธีนี้ต่อไม่ได้
รู้สึกว่าเจ๋งมาก อยากรู้ว่าตอนนี้มีเวอร์ชัน macOS ให้ลองได้ทันทีไหม อยากเอาไปลองกับงาน Rust หรือ C++/Swift ดู
หลังจากแก้บั๊กแล้ว มีแผนจะปล่อยเวอร์ชัน macOS ให้ผู้ใช้เบต้าคนถัดไปไหม ถ้าสมัครไว้ (ที่ท้ายบทความ) แล้วพูดถึงคอมเมนต์นี้ด้วย จะใส่คุณเข้าไปในกลุ่มเบต้าแน่นอน
ดูเหมือนว่ายังไม่มี public release สำหรับ OS ไหนเลย ตอนนี้น่าจะเปิดให้สมัคร early access ได้อย่างเดียว