14 คะแนน โดย GN⁺ 2025-11-04 | 2 ความคิดเห็น | แชร์ทาง WhatsApp
  • แนวคิดของ การค้นหาแบบทวิภาค (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

  • ตัวอย่างประวัติของคอมมิต
    • Commit 1: คอมมิตเริ่มต้น (ปกติ)
    • Commit 2: รีแฟกเตอร์ (ปกติ)
    • Commit 3: นำบั๊กเข้ามา (เกิดข้อผิดพลาด)
    • Commit 4~10: การเปลี่ยนแปลงที่ไม่กระทบฟังก์ชันการทำงาน (ข้อผิดพลาดยังคงอยู่)
  • ตัวอย่างคำสั่งที่ใช้
    git bisect start  
    git bisect bad HEAD  
    git bisect good HEAD~9  
    git bisect run ./test_script.sh  
    
  • สคริปต์ทดสอบ (test_script.sh) จะ คืนค่า 0 เมื่อสำเร็จ และคืนค่า error code เมื่อทดสอบล้มเหลว
  • git bisect จะ checkout คอมมิตกึ่งกลางโดยอัตโนมัติและรันสคริปต์ทดสอบ
    โดยอ้างอิงจากจุดที่การทดสอบเริ่มล้มเหลวเพื่อระบุ คอมมิต bad ตัวแรก
  • จากผลลัพธ์พบว่าคอมมิต b982ed9373fe235fe61c74b15faf264bc7142398 คือ คอมมิตแรกที่ก่อให้เกิดบั๊ก

บทสรุป

  • git bisect เป็น เครื่องมือเชิงปฏิบัติที่นำหลักการของการค้นหาแบบทวิภาคมาใช้กับการไล่ดูประวัติของโค้ด
  • แม้จะเป็น repository ขนาดใหญ่หรือมีประวัติการเปลี่ยนแปลงซับซ้อน ก็ยังช่วย ตามหาช่วงเวลาที่บั๊กถูกนำเข้ามาได้อย่างรวดเร็ว
  • หาก ผสานเข้ากับการทดสอบอัตโนมัติ ก็จะช่วยให้ดีบักได้อย่างมีเสถียรภาพแม้ใน codebase ขนาดใหญ่

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

 
kandk 2025-11-04

นี่คือเหตุผลที่เราใช้ TBD (trunk-based development)

 
GN⁺ 2025-11-04
ความคิดเห็นจาก Hacker News
  • ตอนที่เคยทำงานกับโค้ดเบสขนาดมหึมาที่แทบไม่มี test coverage และ abstraction ก็เละเทะ git bisect แทบจะเป็นเครื่องมือเดียวที่พอใช้งานได้
    โค้ดซับซ้อนเกินกว่าจะไล่บั๊กด้วยเหตุผลเชิงตรรกะได้ จึงง่ายกว่ามากที่จะหาว่าปัญหาเริ่มจาก commit ไหน
    แต่ในโค้ดเบสคุณภาพสูงกลับแทบไม่จำเป็นต้องใช้ bisect เลย แต่ละคอมโพเนนต์ทดสอบแยกอิสระได้ และมี observability ที่ดี ทำให้ชัดเจนว่าควรดูตรงไหน

    • ผมคิดว่า git bisect ไม่มีทางเป็นของที่ไม่จำเป็นเด็ดขาด มันไม่ได้ช่วยแค่หาบั๊ก แต่ยังช่วยให้เข้าใจด้วยว่า ทำไม บั๊กนั้นถึงเกิดขึ้น
      ถ้าเป็นโปรเจกต์ที่มี commit message เขียนไว้อย่างดี เราสามารถใช้ bisect เพื่อเข้าใจบริบทของ commit เก่า ๆ แล้วสะท้อนสิ่งนั้นลงไปใน commit ที่แก้บั๊กได้ วงจรแบบนี้ช่วยเสริมวัฒนธรรมการเขียน commit ให้แข็งแรงขึ้นเอง
    • ผมเคยหาบั๊กจากสตริงแปลก ๆ ที่โผล่ในโปรแกรม OSS มาก่อน เป็นโค้ด C และสาเหตุมาจากตัวแปรที่ไม่ได้ initialize
      ถ้าไล่เองแทบเป็นไปไม่ได้ แต่พอเขียน bisect script แล้วปล่อยรันประมาณ 30 นาที ก็หาตัว commit ต้นเหตุได้แม่นยำ
    • git bisect เดิมทีเป็นเครื่องมือที่ถูกนำมาใช้เพื่อหา regression ของ Linux kernel
      แม้ในกรณีที่ทดสอบไม่ได้ เช่น ฮาร์ดแวร์ไดรเวอร์ ผู้ใช้ทั่วไปก็สามารถ bisect kernel ด้วยตัวเองเพื่อชี้ไปยัง commit ที่มีปัญหาได้
      เมื่อก่อนต้องส่งอีเมลขอความช่วยเหลือจากนักพัฒนา แต่ตอนนี้ผู้ใช้ค่อย ๆ ตีกรอบปัญหาด้วยตัวเองได้แล้ว
    • ถ้าเป้าหมายมีแค่แก้บั๊กอย่างเดียว อาจไม่จำเป็นต้องใช้ bisect ก็ได้ แต่บางครั้งเราจำเป็นต้องรู้ว่า บั๊กนี้ มีมาตั้งแต่เมื่อไร
      เช่น ใช้ตามขอบเขตของข้อมูลที่ถูกประมวลผลผิด หรือใช้ตัดสินว่า “นี่คือบั๊กหรือฟีเจอร์”
    • บางครั้งก็ต้องรู้ด้วยว่า บั๊กนี้ ถูกแก้ไปตั้งแต่เมื่อไร
      ตัวอย่างเช่น เมื่อลูกค้ายังเจอปัญหาอยู่บนเวอร์ชันเมื่อ 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 ว่าต้องใส่อะไรบ้าง
    • มีคนบอกว่าตัวเองใช้ bisect ทุกวัน แปลว่า workflow ต่างกันไปคนละแบบเลย
    • ถ้าเป็นกรณีที่มี polymorphic function อย่าง C++ นั้น git log -L จะไม่ค่อยเก่ง เพราะยากที่จะตามเวอร์ชันที่ต้องการของฟังก์ชัน overload ที่ชื่อเดียวกัน
    • ถ้าไม่มี .gitattributes ก็ใช้ git log -S เพื่อหา commit ที่มีสตริงบางตัวรวมอยู่ด้วยได้เช่นกัน
  • ใน test script ควรรู้จัก exit code 125 ไว้
    ถ้าเป็นกรณีอย่าง build ล้มเหลวที่ไม่สามารถตัดสินผลการทดสอบได้ ให้คืนค่า 125 แล้ว bisect จะข้าม commit นั้นไป
    ผมเคยสรุปเรื่องนี้ไว้ในบล็อกโพสต์ของผม

    • ถ้าเป็น repository ที่ merge commit หมายถึงจุดที่ผ่าน CI แล้ว การใช้ git bisect --first-parent จะมีประโยชน์มาก
      วิธีนี้ช่วยให้หาได้เร็วว่า “PR ไหนเป็นตัวนำบั๊กเข้ามา” แล้วค่อยไล่ bisect ละเอียดอีกรอบบน branch นั้นภายหลัง
  • ตอนเกิด flaky test ขึ้นมา bisect จะโดดเด่นมาก
    ถ้าเป็น race condition ที่ต้องรันเทสต์นับแสนครั้งกว่าจะมั่นใจ การปล่อย bisect script รันไว้เบื้องหลังก็ทำให้แก้ปัญหาได้จริงในทางปฏิบัติ

    • ในกรณีแบบนี้ ดูเหมือนว่าการใช้ Bayesian binary search น่าจะช่วยลดจำนวนครั้งที่ต้องรันเทสต์ลงได้มาก
  • ไม่นานมานี้ผมใช้ 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 อยู่แล้ว

    • น่าสนใจว่าแม้ binary search จะถูกเสนอขึ้นมาตั้งแต่ช่วงทศวรรษ 1940 แต่ก็มีเรื่องเล่าว่า implementation ที่ ไม่มีบั๊ก เพิ่งมาปรากฏในช่วงทศวรรษ 1960
      เพราะถ้าคำนวณดัชนีกลางด้วย (low + high) / 2 ก็อาจเกิด overflow ได้
    • โดยส่วนตัวผมคิดว่านักพัฒนาทุกคนควรลองเขียน binary search เองสักครั้งด้วย ภาษาที่มีจำนวนเต็มความแม่นยำไม่จำกัด เช่น Python
      มันเป็นแบบฝึกหัดที่ยอดเยี่ยมมากในการฝึกคิดบนพื้นฐานของ invariant
  • นอกจาก bisect แล้ว Git ยังมีเครื่องมือสำรวจโค้ดที่ยอดเยี่ยมอย่าง log -L, log -S, blame
    ก่อนหน้านี้ผมเคยเขียนบล็อกโพสต์ เกี่ยวกับหัวข้อนี้ไว้ด้วย