- แนวคิดของ การค้นหาแบบทวิภาค (binary search) ไม่ได้ใช้แค่ในโจทย์สัมภาษณ์ แต่ยังถูกนำไปใช้ใน Git ซึ่งเป็นเครื่องมือสำหรับการพัฒนาจริงด้วย
- ในสภาพแวดล้อม monorepo ขนาดใหญ่ เมื่อการทดสอบล้มเหลวขึ้นมาอย่างกะทันหัน ก็มักเกิดสถานการณ์ที่ยากต่อการตามหาสาเหตุจาก log เพียงอย่างเดียว
- เพื่อนร่วมงานคนหนึ่งได้ ระบุ good commit และ bad commit แล้วใช้
git bisect ค้นหาแบบอัตโนมัติ จนสามารถหาคอมมิตต้นตอที่เริ่มทำให้เกิดบั๊กได้อย่างแม่นยำ
- ในแต่ละขั้นจะรันสคริปต์แล้วจัดหมวดหมู่คอมมิตอัตโนมัติตามผลการทดสอบ เพื่อระบุ คอมมิตแรกที่เริ่มล้มเหลว
git bisect ที่อาศัยหลักการของการค้นหาแบบทวิภาคเป็น เครื่องมือทรงพลังสำหรับตามหาสาเหตุของบั๊กอย่างรวดเร็วใน codebase ขนาดใหญ่
อัลกอริทึมและกรณีใช้งานจริง
- อัลกอริทึม การค้นหาแบบทวิภาค (binary search) ไม่ได้เป็นแค่โจทย์สัมภาษณ์ง่าย ๆ แต่ยังเป็น หลักการสำคัญที่ทำงานอยู่เบื้องหลังเครื่องมือดีบักในงานจริง
git bisect เป็นเครื่องมือที่ใช้การค้นหาแบบทวิภาคเพื่อหา “คอมมิตแรกที่นำบั๊กเข้ามา (first bad commit)”
- ทำงานด้วยหลักการคล้ายกับโจทย์ “First Bad Version” ของ Leetcode
สถานการณ์ปัญหาในสภาพแวดล้อมการทำงานจริง
- ในสภาพแวดล้อมที่ใช้ monorepo ขนาดใหญ่ อาจมีคอมมิตเกิดขึ้นวันละหลายร้อยถึงหลายพันรายการ
- สาเหตุของการทดสอบที่ล้มเหลวบางอย่าง ยากจะตามรอยได้จาก log เพียงอย่างเดียว
- ต้นเหตุของความล้มเหลวมาจาก การแก้ไขสตริงในไฟล์ตั้งค่าที่ใช้ดึงโทเค็นสำหรับ remote call ทำให้ไปอ้างอิงบัญชีอื่นและส่งผลให้การทดสอบล้มเหลว
- การเปลี่ยนแปลงนี้ผ่าน integration test ได้ แต่ในความเป็นจริงกลับก่อปัญหา และยากจะระบุว่าเกิดขึ้นตั้งแต่ช่วงไหนท่ามกลางคอมมิตจำนวนมาก
การแก้ปัญหาด้วย git bisect
- เพื่อนร่วมงานจากอีกทีมหนึ่งใช้ คำสั่ง
git bisect เพื่อระบุคอมมิตปัญหาได้อย่างรวดเร็ว
- หลังจากกำหนด good commit และ bad commit แล้ว ระบบจะ checkout คอมมิตกึ่งกลางโดยอัตโนมัติ ทำการทดสอบ และ ค่อย ๆ บีบช่วงของสาเหตุให้แคบลง
- แม้การรันทดสอบแต่ละครั้งจะใช้เวลา แต่สุดท้ายก็ ค้นพบคอมมิตที่เป็นต้นเหตุของปัญหาได้อย่างแม่นยำ
- เมื่อนำคอมมิตนั้นย้อนกลับ การทดสอบทั้งหมดก็กลับมาเป็นปกติ
ขั้นตอนการทำงานของ git bisect
บทสรุป
git bisect เป็น เครื่องมือเชิงปฏิบัติที่นำหลักการของการค้นหาแบบทวิภาคมาใช้กับการไล่ดูประวัติของโค้ด
- แม้จะเป็น repository ขนาดใหญ่หรือมีประวัติการเปลี่ยนแปลงซับซ้อน ก็ยังช่วย ตามหาช่วงเวลาที่บั๊กถูกนำเข้ามาได้อย่างรวดเร็ว
- หาก ผสานเข้ากับการทดสอบอัตโนมัติ ก็จะช่วยให้ดีบักได้อย่างมีเสถียรภาพแม้ใน codebase ขนาดใหญ่
2 ความคิดเห็น
นี่คือเหตุผลที่เราใช้ TBD (trunk-based development)
ความคิดเห็นจาก Hacker News
ตอนที่เคยทำงานกับโค้ดเบสขนาดมหึมาที่แทบไม่มี test coverage และ abstraction ก็เละเทะ
git bisectแทบจะเป็นเครื่องมือเดียวที่พอใช้งานได้โค้ดซับซ้อนเกินกว่าจะไล่บั๊กด้วยเหตุผลเชิงตรรกะได้ จึงง่ายกว่ามากที่จะหาว่าปัญหาเริ่มจาก commit ไหน
แต่ในโค้ดเบสคุณภาพสูงกลับแทบไม่จำเป็นต้องใช้ bisect เลย แต่ละคอมโพเนนต์ทดสอบแยกอิสระได้ และมี observability ที่ดี ทำให้ชัดเจนว่าควรดูตรงไหน
git bisectไม่มีทางเป็นของที่ไม่จำเป็นเด็ดขาด มันไม่ได้ช่วยแค่หาบั๊ก แต่ยังช่วยให้เข้าใจด้วยว่า ทำไม บั๊กนั้นถึงเกิดขึ้นถ้าเป็นโปรเจกต์ที่มี commit message เขียนไว้อย่างดี เราสามารถใช้ bisect เพื่อเข้าใจบริบทของ commit เก่า ๆ แล้วสะท้อนสิ่งนั้นลงไปใน commit ที่แก้บั๊กได้ วงจรแบบนี้ช่วยเสริมวัฒนธรรมการเขียน commit ให้แข็งแรงขึ้นเอง
ถ้าไล่เองแทบเป็นไปไม่ได้ แต่พอเขียน bisect script แล้วปล่อยรันประมาณ 30 นาที ก็หาตัว commit ต้นเหตุได้แม่นยำ
git bisectเดิมทีเป็นเครื่องมือที่ถูกนำมาใช้เพื่อหา regression ของ Linux kernelแม้ในกรณีที่ทดสอบไม่ได้ เช่น ฮาร์ดแวร์ไดรเวอร์ ผู้ใช้ทั่วไปก็สามารถ bisect kernel ด้วยตัวเองเพื่อชี้ไปยัง commit ที่มีปัญหาได้
เมื่อก่อนต้องส่งอีเมลขอความช่วยเหลือจากนักพัฒนา แต่ตอนนี้ผู้ใช้ค่อย ๆ ตีกรอบปัญหาด้วยตัวเองได้แล้ว
เช่น ใช้ตามขอบเขตของข้อมูลที่ถูกประมวลผลผิด หรือใช้ตัดสินว่า “นี่คือบั๊กหรือฟีเจอร์”
ตัวอย่างเช่น เมื่อลูกค้ายังเจอปัญหาอยู่บนเวอร์ชันเมื่อ 6 ปีก่อน เราสามารถเช็กได้ว่าถ้าอัปเกรดไปเวอร์ชันเมื่อ 4 ปีก่อนแล้วปัญหาจะหายไหม
หรือถ้ามีการ refactor โค้ดครั้งใหญ่ ก็ยังช่วยดูได้ว่าการแก้บั๊กนั้นตั้งใจทำหรือเป็นผลพลอยได้โดยบังเอิญ
git bisectยอดเยี่ยมมากเวลาที่มันทำงานได้ดี แต่ก็ไม่ได้หมายความว่าจะหาบั๊กได้ทุกแบบบางบั๊กตอนถูกใส่เข้ามาอาจยังไม่แสดงอาการ และเพิ่งมาเผยตัวภายหลังเพราะการเปลี่ยนแปลงอื่น
ในกรณีแบบนี้ สมมติฐานของ bisect จะพังลง เพราะมันตั้งอยู่บนเงื่อนไขว่าบั๊กจะปรากฏเพียงครั้งเดียวระหว่าง commit ที่ดีและ commit ที่แย่
commit ที่ทดสอบไม่ได้สามารถ
skipได้ แต่ถ้า commit นั้นเองคือจุดต้นเหตุ ผลลัพธ์ก็จะกำกวมไม่นานมานี้ผมเพิ่งได้ลองใช้
git bisectแบบจริงจังเป็นครั้งแรก และมันเกือบจะ เหมือนเวทมนตร์มีฟังก์ชันสองตัวที่ชื่อเหมือนกัน และระหว่างงานจัด format โค้ด import ของฟังก์ชันที่ถูกต้องถูกลบออกไปจนเกิดปัญหา
ผมทบทวนโค้ดอยู่หลายรอบ แต่ก็ไม่รู้ต้นตอสักที จน bisect ชี้ไปที่ commit ที่มีปัญหา
ปกติผมมักรู้อยู่แล้วว่าบั๊กน่าจะอยู่แถวไฟล์หรือฟังก์ชันไหน เลยไม่ได้ใช้ bisect บ่อยนัก
ผมใช้คำสั่ง
git log -L :func_name:path/to/file.cเพื่อติดตามประวัติการเปลี่ยนแปลงของฟังก์ชันเฉพาะแทนต้องมีการตั้งค่า
.gitattributes.gitattributesว่าต้องใส่อะไรบ้างgit log -Lจะไม่ค่อยเก่ง เพราะยากที่จะตามเวอร์ชันที่ต้องการของฟังก์ชัน overload ที่ชื่อเดียวกัน.gitattributesก็ใช้git log -Sเพื่อหา commit ที่มีสตริงบางตัวรวมอยู่ด้วยได้เช่นกันใน test script ควรรู้จัก exit code 125 ไว้
ถ้าเป็นกรณีอย่าง build ล้มเหลวที่ไม่สามารถตัดสินผลการทดสอบได้ ให้คืนค่า 125 แล้ว bisect จะข้าม commit นั้นไป
ผมเคยสรุปเรื่องนี้ไว้ในบล็อกโพสต์ของผม
git bisect --first-parentจะมีประโยชน์มากวิธีนี้ช่วยให้หาได้เร็วว่า “PR ไหนเป็นตัวนำบั๊กเข้ามา” แล้วค่อยไล่ bisect ละเอียดอีกรอบบน branch นั้นภายหลัง
ตอนเกิด flaky test ขึ้นมา bisect จะโดดเด่นมาก
ถ้าเป็น race condition ที่ต้องรันเทสต์นับแสนครั้งกว่าจะมั่นใจ การปล่อย bisect script รันไว้เบื้องหลังก็ทำให้แก้ปัญหาได้จริงในทางปฏิบัติ
ไม่นานมานี้ผมใช้ bisect หาเหตุของบั๊กในโปรเจกต์ music player ที่ทำด้วย Svelte (lets-make-sweet-music.com)
ไม่มีทั้งเทสต์และไม่มี error log แถม commit ก็เยอะขึ้นเพราะ
dependabotอัปเดต เลยตามรอยยากมากสุดท้าย bisect ก็พาไปเจอ commit ต้นเหตุ และสาเหตุคือไฟล์ที่ผมสลับเข้าไปใหม่ไม่ได้ implement ฟีเจอร์การ bind event หลายตัว
ถ้าเรารักษาให้ commit มีขนาดเล็ก ก็จะช่วยให้ตีกรอบสาเหตุของปัญหาที่หาเจอด้วย bisect ได้เร็วขึ้น
มีคนบอกว่า “การบังคับให้เรียน binary search ไปใช้ในสัมภาษณ์งานนั้นฝืนเกินไป” แต่
git bisectเป็นตัวอย่างที่ดีว่าคอนเซปต์นี้ถูกใช้จริงอย่างไรอย่างไรก็ดี ไม่จำเป็นต้องเขียนมันขึ้นมาเอง เพราะภาษาส่วนใหญ่มีให้ใน standard library อยู่แล้ว
เพราะถ้าคำนวณดัชนีกลางด้วย
(low + high) / 2ก็อาจเกิด overflow ได้มันเป็นแบบฝึกหัดที่ยอดเยี่ยมมากในการฝึกคิดบนพื้นฐานของ invariant
นอกจาก bisect แล้ว Git ยังมีเครื่องมือสำรวจโค้ดที่ยอดเยี่ยมอย่าง
log -L,log -S,blameก่อนหน้านี้ผมเคยเขียนบล็อกโพสต์ เกี่ยวกับหัวข้อนี้ไว้ด้วย