| Topic 20 | การดีบัก |
|---|
เป็นเรื่องเจ็บปวด
ที่ต้องมองความทุกข์ของตัวเองแล้วรู้
ว่าตัวเราเองนี่แหละ ไม่ใช่ใครอื่น ที่สร้างมันขึ้นมา
Sophocles , Ajax
คำว่า bug ถูกใช้เพื่ออธิบาย “สิ่งที่น่าสะพรึงกลัว” มาตั้งแต่ศตวรรษที่ 14 แล้ว พลเรือตรี ดร. เกรซ ฮอปเปอร์ (Grace Hopper) ผู้คิดค้นภาษา COBOL ได้รับการยกย่องว่าเป็นผู้พบ computer bug ตัวแรก—ซึ่งก็คือผีเสื้อกลางคืนตัวหนึ่งที่ติดอยู่ในรีเลย์ของคอมพิวเตอร์ยุคแรกๆ นั่นเอง เมื่อถูกถามว่าทำไมเครื่องถึงทำงานผิดปกติ ช่างเทคนิคคนหนึ่งรายงานว่า “มีบั๊กอยู่ในระบบ” และก็เก็บมันไว้อย่างเรียบร้อย—ทั้งปีกและตัว—แปะติดไว้ในสมุดบันทึกครับ
น่าเสียดายที่เรายังคงมีบั๊กอยู่ในระบบ แม้จะไม่ใช่ชนิดที่มีปีกบินได้แล้วก็ตาม แต่ความหมายในศตวรรษที่ 14—ผีร้ายที่คอยหลอกหลอน—กลับยิ่งใช้ได้ผลในปัจจุบันมากกว่าในอดีตเสียอีก ข้อบกพร่องของซอฟต์แวร์แสดงตัวออกมาได้หลายรูปแบบ ตั้งแต่ความเข้าใจ requirement ที่ผิดพลาดไปจนถึงข้อผิดพลาดในการเขียนโค้ด น่าเสียดายที่ระบบคอมพิวเตอร์สมัยใหม่ยังคงทำได้แค่สิ่งที่คุณ บอก ให้มันทำ ไม่ใช่สิ่งที่คุณ ต้องการ ให้มันทำครับ
ไม่มีใครเขียนซอฟต์แวร์ได้สมบูรณ์แบบ ดังนั้นจึงเป็นเรื่องปกติที่การดีบักจะกินเวลาไปมากในแต่ละวันของคุณ มาดูกันว่ามีประเด็นอะไรบ้างที่เกี่ยวข้องกับการดีบัก และกลยุทธ์ทั่วไปสำหรับการตามหาบั๊กที่ซ่อนตัวเก่งๆ กันครับ
Psychology of Debugging (จิตวิทยาของการดีบัก)
การดีบักเป็นเรื่องที่ละเอียดอ่อนและกระทบความรู้สึกของนักพัฒนาหลายคน แทนที่จะมองมันเป็นปริศนาที่ต้องแก้ไข บางคนกลับเลือกที่จะปฏิเสธ โยนความผิดให้คนอื่น หาข้ออ้างแย่ๆ หรือไม่ก็เฉยเมยไปเลยครับ
จงยอมรับความจริงที่ว่าการดีบักก็คือการ แก้ปัญหา (problem solving) และจัดการกับมันในแบบนั้นครับ
เมื่อเจอบั๊กที่คนอื่นสร้างไว้ คุณอาจเสียเวลาและพลังงานไปกับการตำหนิผู้ต้องสงสัยที่แสนน่ารังเกียจคนนั้น ในบางที่ทำงานเรื่องนี้ถือเป็นส่วนหนึ่งของวัฒนธรรมองค์กร และอาจช่วยระบายความอัดอั้นได้ แต่ในเชิงเทคนิคแล้ว คุณควรมุ่งความสนใจไปที่การแก้ ปัญหา ไม่ใช่การหาคนผิดครับ
| Tip 29 | แก้ที่ปัญหา ไม่ใช่แก้ที่คนผิด |
|---|
ไม่ว่าบั๊กนั้นจะเป็นความผิดของคุณหรือของคนอื่นก็ไม่สำคัญเลย มันก็ยังคงเป็นปัญหาของคุณอยู่ดีครับ
A Debugging Mindset (แนวคิดสำหรับการดีบัก)
ก่อนเริ่มดีบัก สิ่งสำคัญคือต้องปรับ mindset ให้ถูกต้องเสียก่อน คุณต้องปิดกลไกป้องกันตัวเองที่ใช้ปกป้องอีโก้อยู่ทุกวัน ตัดความกดดันจากโปรเจกต์ออกไป แล้วทำตัวให้สบายๆ และเหนือสิ่งอื่นใด จงจำกฎข้อแรกของการดีบักไว้ให้ขึ้นใจ:
| Tip 30 | อย่าตื่นตระหนก |
|---|
มันง่ายมากที่จะตื่นตระหนก โดยเฉพาะเมื่อคุณกำลังเผชิญกับ deadline หรือมีหัวหน้าหรือลูกค้าที่กระวนกระวายคอยจี้อยู่หลังบ่าในขณะที่คุณพยายามหาสาเหตุของบั๊ก แต่สิ่งสำคัญมากคือต้องถอยกลับมาตั้งหลักหนึ่งก้าว แล้ว คิด ให้จริงจังว่าอะไรอาจเป็นสาเหตุของอาการที่คุณเชื่อว่าเป็นสัญญาณของบั๊กครับ
ถ้าปฏิกิริยาแรกของคุณเมื่อเจอบั๊กหรือเห็นรายงานบั๊กคือ “เป็นไปไม่ได้หรอก” คุณคิดผิดอย่างแน่นอน อย่าเสียเวลาแม้แต่นิดเดียวไปกับความคิดที่ขึ้นต้นว่า “แต่มันเกิดขึ้นไม่ได้” เพราะเห็นได้ชัดว่ามัน เกิดขึ้นได้ และก็เกิดขึ้นมาแล้วจริงๆ ครับ
ระวังอย่ามองแค่สิ่งที่อยู่ตรงหน้าเวลาดีบัก อย่าใจร้อนแก้แค่อาการที่คุณเห็น เพราะสาเหตุที่แท้จริงมักจะอยู่ห่างจากสิ่งที่คุณสังเกตเห็นไปหลายขั้น และอาจเกี่ยวข้องกับสิ่งอื่นๆ อีกหลายอย่าง พยายามค้นหา root cause ของปัญหาเสมอ ไม่ใช่แค่แก้อาการที่ปรากฏให้เห็นในครั้งนี้ครับ
Where to Start (จะเริ่มจากตรงไหน)
ก่อนที่คุณจะ เริ่ม มองหาบั๊ก ต้องแน่ใจก่อนว่าคุณกำลังทำงานกับโค้ดที่ build ผ่านอย่างสะอาด—ไม่มี warning เราตั้งระดับ compiler warning ไว้สูงที่สุดเท่าที่จะทำได้เป็นประจำ เพราะไม่มีเหตุผลเลยที่จะเสียเวลาไปกับการหาปัญหาที่คอมพิวเตอร์สามารถหาให้คุณได้อยู่แล้ว! เราต้องเอาสมาธิไปทุ่มกับปัญหาที่ยากกว่านั้นต่างหากครับ
เมื่อพยายามแก้ปัญหาใดๆ ก็ตาม คุณต้องรวบรวมข้อมูลที่เกี่ยวข้องทั้งหมดให้ครบ น่าเสียดายที่การรายงานบั๊กไม่ใช่ศาสตร์ที่แม่นยำนัก มันง่ายมากที่จะถูกความบังเอิญหลอก และคุณก็ไม่มีเวลาเหลือพอที่จะไปดีบักความบังเอิญ สิ่งแรกที่ต้องทำคือทำให้การสังเกตของคุณแม่นยำครับ
ความแม่นยำของรายงานบั๊กจะยิ่งลดลงไปอีกเมื่อมันถูกส่งต่อผ่านบุคคลที่สาม—บางครั้งคุณอาจต้อง เฝ้าดู ผู้ใช้ที่รายงานบั๊กลงมือทำงานจริงๆ เพื่อให้ได้รายละเอียดที่เพียงพอครับ
ครั้งหนึ่งแอนดี้เคยทำงานกับแอปพลิเคชันด้านกราฟิกขนาดใหญ่ตัวหนึ่ง ใกล้ถึงวันรีลีส ทีมทดสอบรายงานว่าแอปพลิเคชันจะแครชทุกครั้งที่วาดเส้นด้วยแปรงตัวหนึ่งโดยเฉพาะ โปรแกรมเมอร์ที่รับผิดชอบยืนยันว่ามันไม่มีอะไรผิดปกติ เขาลองวาดด้วยแปรงนั้นแล้วมันทำงานได้ดีทุกอย่าง บทสนทนานี้วนไปวนมาอยู่หลายวัน จนอารมณ์ของทั้งสองฝ่ายเริ่มร้อนขึ้นเรื่อยๆ ครับ
ในที่สุดเราก็จับทั้งสองคนมานั่งอยู่ในห้องเดียวกัน ผู้ทดสอบเลือกเครื่องมือแปรง แล้ววาดเส้นจากมุมบนขวาไปยังมุมล่างซ้าย แอปพลิเคชันก็ระเบิดทันที “อ้าว” โปรแกรมเมอร์พูดออกมาเบาๆ ก่อนจะยอมรับอย่างเขินๆ ว่าตัวเองทดสอบวาดเส้นแค่จากมุมล่างซ้ายไปมุมบนขวาเท่านั้น ซึ่งไม่ทำให้บั๊กปรากฏออกมาครับ
เรื่องนี้มีประเด็นสำคัญอยู่สองข้อ:
-
คุณอาจต้องสัมภาษณ์ผู้ใช้ที่รายงานบั๊ก เพื่อรวบรวมข้อมูลเพิ่มเติมมากกว่าที่ได้รับในตอนแรก
-
การทดสอบแบบจำลองขึ้นมาเอง (เช่น การวาดเส้นแปรงเส้นเดียวจากล่างขึ้นบนของโปรแกรมเมอร์) ยังไม่ครอบคลุมแอปพลิเคชันมากพอ คุณต้องทดสอบทั้ง boundary condition และรูปแบบการใช้งานจริงของผู้ใช้ปลายทางอย่างเข้มข้น และต้องทำอย่างเป็นระบบด้วย (ดู Ruthless and Continuous Testing (การทดสอบอย่างต่อเนื่องและไม่ยอมปรานี) )
Debugging Strategies (กลยุทธ์การดีบัก)
เมื่อ คุณ คิดว่ารู้แล้วว่าเกิดอะไรขึ้น ก็ถึงเวลาไปดูว่า โปรแกรม คิดว่าเกิดอะไรขึ้นบ้างครับ
Reproducing Bugs (การทำให้บั๊กเกิดซ้ำ)
ไม่ใช่หรอกครับ บั๊กของเราไม่ได้แพร่พันธุ์จริงๆ หรอก (แม้ว่าบางตัวอาจจะแก่พอที่จะทำแบบนั้นได้อย่างถูกกฎหมายแล้วก็ตาม) เรากำลังพูดถึงการทำสำเนาอีกแบบหนึ่งต่างหาก
วิธีที่ดีที่สุดในการเริ่มแก้บั๊กคือทำให้มันเกิดซ้ำได้ (reproducible) เสียก่อน เพราะถ้าคุณทำให้มันเกิดซ้ำไม่ได้ แล้วคุณจะรู้ได้อย่างไรว่ามันถูกแก้ไขแล้วจริงๆ?
แต่เราต้องการมากกว่าบั๊กที่เกิดซ้ำได้ด้วยขั้นตอนยาวเหยียด เราต้องการบั๊กที่เกิดซ้ำได้ด้วย คำสั่งเดียว การแก้บั๊กจะยากขึ้นมากถ้าคุณต้องทำ 15 ขั้นตอนกว่าจะไปถึงจุดที่บั๊กปรากฏตัวครับ
ดังนั้นนี่คือกฎที่สำคัญที่สุดของการดีบัก:
| Tip 31 | เขียนเทสต์ที่ล้มเหลวก่อนแก้โค้ด |
|---|
บางครั้งการบังคับตัวเองให้แยกแยะสถานการณ์ที่ทำให้บั๊กปรากฏออกมาได้ ยังช่วยให้คุณมองเห็นแนวทางแก้ไขมันด้วยซ้ำ การเขียนเทสต์นั่นแหละที่ช่วยชี้ทางไปสู่คำตอบครับ
Coder in a Strange Land (โค้ดเดอร์ในดินแดนแปลกหน้า)
พูดเรื่องการแยกบั๊กมาตั้งมากก็ดีอยู่หรอก แต่เมื่อต้องเผชิญกับโค้ด 50,000 บรรทัดและนาฬิกาที่เดินไม่หยุด โค้ดเดอร์ผู้น่าสงสารควรทำอย่างไรดี?
อันดับแรก ให้ดูที่ตัวปัญหาก่อน มันเป็นการแครชหรือเปล่า? เวลาเราสอนคอร์สที่เกี่ยวกับการเขียนโปรแกรม เรามักแปลกใจเสมอว่ามีนักพัฒนามากมายแค่ไหนที่เห็น exception เด้งขึ้นมาเป็นสีแดง แล้วก็รีบสลับไปที่โค้ดทันทีครับ
| Tip 32 | อ่าน Error Message ให้ดีๆ เถอะน่า |
|---|
แค่นี้ก็คงเข้าใจแล้วครับ
Bad Results (ผลลัพธ์ที่ผิดพลาด)
แล้วถ้ามันไม่ใช่การแครชล่ะ? ถ้ามันแค่ให้ผลลัพธ์ที่ผิดพลาดล่ะ?
ลงมือใช้ debugger เข้าไปดู แล้วใช้เทสต์ที่ล้มเหลวของคุณเพื่อกระตุ้นให้ปัญหาเกิดขึ้นครับ
ก่อนอื่นเลย ต้องแน่ใจว่าคุณเห็นค่าที่ผิดพลาดนั้นใน debugger ด้วยเช่นกัน เราทั้งคู่เคยเสียเวลาไปหลายชั่วโมงในการไล่ตามหาบั๊ก แต่สุดท้ายก็มาพบว่าการรันโค้ดครั้งนั้นๆ ทำงานได้ปกติดีครับ
บางครั้งปัญหาก็ชัดเจนอยู่แล้ว เช่น interest_rate มีค่าเป็น 4.5 ทั้งที่ควรจะเป็น 0.045 แต่ส่วนใหญ่แล้วคุณต้องเจาะลึกลงไปมากกว่านั้นเพื่อหาว่าทำไมค่านั้นถึงผิดตั้งแต่แรก ต้องแน่ใจว่าคุณรู้วิธีเลื่อนขึ้นลงใน call stack และตรวจสอบ local stack environment ได้ครับ
เรามักพบว่าการมีปากกาและกระดาษไว้ใกล้ตัวเพื่อจดบันทึกช่วยได้มาก โดยเฉพาะเวลาที่เราเจอเบาะแสแล้วไล่ตามมันไป แต่สุดท้ายกลับพบว่ามันไม่ใช่คำตอบ ถ้าเราไม่ได้จดไว้ว่าเราอยู่ตรงไหนตอนเริ่มไล่ตาม เราอาจเสียเวลาไปมากกว่าจะกลับมาที่จุดเดิมได้ครับ
บางครั้งคุณอาจเจอ stack trace ที่ยาวเหยียดราวกับไม่มีที่สิ้นสุด ในกรณีนี้มักมีวิธีที่เร็วกว่าการไล่ดูทุกๆ stack frame นั่นคือการใช้ binary chop แต่ก่อนที่เราจะพูดถึงเรื่องนั้น มาดูสถานการณ์บั๊กที่พบบ่อยอีกสองแบบกันก่อนครับ
Sensitivity to Input Values (ความอ่อนไหวต่อค่าอินพุต)
คุณคงเคยเจอสถานการณ์นี้มาแล้ว โปรแกรมของคุณทำงานได้ดีกับข้อมูลทดสอบทั้งหมด และผ่านสัปดาห์แรกใน production มาได้อย่างสง่างาม แล้วจู่ๆ มันก็แครชเมื่อได้รับชุดข้อมูลชุดหนึ่งเข้าไปครับ
คุณอาจลองดูจุดที่มันแครชแล้วไล่ย้อนกลับไป แต่บางครั้งการเริ่มจากตัวข้อมูลก็ง่ายกว่า ให้คัดลอกชุดข้อมูลนั้นมา แล้วป้อนเข้าไปในแอปที่รันอยู่บนเครื่องของคุณเอง ตรวจให้แน่ใจว่ามันยังแครชอยู่ จากนั้นก็ binary chop ข้อมูลไปเรื่อยๆ จนกว่าจะแยกได้ว่าค่าอินพุตตัวไหนกันแน่ที่ทำให้เกิดการแครชครับ
Regressions Across Releases (การถดถอยข้ามรีลีส)
สมมติว่าคุณอยู่ในทีมที่เก่ง และปล่อยซอฟต์แวร์ของคุณเข้าสู่ production ต่อมาจู่ๆ ก็มีบั๊กโผล่ขึ้นมาในโค้ดที่เมื่อสัปดาห์ก่อนยังทำงานได้ปกติดีอยู่เลย คงจะดีไม่น้อยถ้าคุณสามารถระบุได้ว่าการเปลี่ยนแปลงจุดไหนที่เป็นต้นเหตุ เดาสิว่าต้องทำยังไง? ได้เวลา binary chop แล้วครับ
The Binary Chop (การหั่นครึ่งแบบไบนารี)
นักศึกษาสาขาวิทยาการคอมพิวเตอร์แทบทุกคนต้องเคยถูกบังคับให้เขียนโค้ด binary chop (บางทีก็เรียกว่า binary search) มาแล้ว แนวคิดนั้นง่ายมาก คุณกำลังมองหาค่าหนึ่งในอาร์เรย์ที่เรียงลำดับไว้แล้ว คุณอาจจะไล่ดูทีละค่าก็ได้ แต่โดยเฉลี่ยแล้วคุณจะต้องดูไปเกือบครึ่งหนึ่งของทั้งหมด กว่าจะเจอค่าที่ต้องการ หรือเจอค่าที่มากกว่าค่านั้น ซึ่งหมายความว่าค่าที่ต้องการไม่มีอยู่ในอาร์เรย์เลยครับ
แต่การใช้แนวทาง divide and conquer จะเร็วกว่า เลือกค่ากลางของอาร์เรย์ขึ้นมาก่อน ถ้าใช่ค่าที่คุณต้องการก็หยุดได้เลย ถ้าไม่ใช่ก็หั่นอาร์เรย์ออกเป็นสองส่วน ถ้าค่าที่เจอมากกว่าค่าเป้าหมาย คุณก็รู้ว่าค่าที่ต้องการต้องอยู่ในครึ่งแรกของอาร์เรย์ ไม่งั้นก็อยู่ในครึ่งหลัง ทำแบบนี้ซ้ำไปเรื่อยๆ ในอาร์เรย์ย่อยที่ถูกต้อง แล้วคุณจะได้คำตอบในเวลาไม่นานเลย (ดังที่เราจะเห็นเมื่อพูดถึงเรื่อง Big-O Notation (สัญกรณ์บิ๊กโอ) การค้นหาแบบ linear search จะมีความซับซ้อนเป็น
ส่วน binary chop จะมีความซับซ้อนเป็น
)
ดังนั้น binary chop จึงเร็วกว่ามากๆ สำหรับปัญหาขนาดใหญ่พอสมควร มาดูกันว่าจะนำมันไปใช้กับการดีบักได้อย่างไรครับ
เมื่อคุณเผชิญกับ stacktrace ขนาดมหึมาและพยายามหาว่าฟังก์ชันไหนกันแน่ที่ทำให้ค่าผิดเพี้ยนไป คุณสามารถหั่นมันได้ด้วยการเลือก stack frame ตรงกลางสักตัวหนึ่ง แล้วดูว่า error ปรากฏอยู่ตรงนั้นหรือเปล่า ถ้าใช่ก็รู้เลยว่าต้องโฟกัสไปที่ frame ก่อนหน้า ถ้าไม่ใช่ปัญหาก็อยู่ใน frame หลังจากนั้น หั่นซ้ำไปเรื่อยๆ ถึงแม้ stacktrace จะมีถึง 64 frame วิธีนี้ก็จะให้คำตอบคุณได้ภายในไม่เกินหกครั้งครับ
ถ้าคุณเจอบั๊กที่เกิดขึ้นกับชุดข้อมูลบางชุด คุณก็อาจทำแบบเดียวกันนี้ได้ แบ่งชุดข้อมูลออกเป็นสองส่วน แล้วดูว่าปัญหาเกิดขึ้นหรือไม่เมื่อป้อนส่วนใดส่วนหนึ่งเข้าไปในแอป แบ่งข้อมูลต่อไปเรื่อยๆ จนกว่าจะได้ชุดค่าที่น้อยที่สุดที่ยังคงทำให้ปัญหาเกิดขึ้นครับ
ถ้าทีมของคุณสร้างบั๊กขึ้นมาระหว่างการรีลีสหลายๆ ครั้ง คุณก็สามารถใช้เทคนิคแบบเดียวกันนี้ได้ สร้างเทสต์ที่ทำให้รีลีสปัจจุบันล้มเหลว จากนั้นเลือกรีลีสที่อยู่กึ่งกลางระหว่างตอนนี้กับเวอร์ชันล่าสุดที่รู้ว่ายังทำงานได้ดี รันเทสต์อีกครั้งแล้วตัดสินใจว่าจะย่อขอบเขตการค้นหาอย่างไรต่อ การทำแบบนี้ได้ถือเป็นหนึ่งในประโยชน์มากมายของการมี version control ที่ดีในโปรเจกต์ของคุณ อันที่จริงระบบ version control หลายตัวยังไปไกลกว่านั้นอีก โดยจะทำขั้นตอนนี้ให้อัตโนมัติ เลือกรีลีสให้คุณเองตามผลลัพธ์ของเทสต์ครับ
Logging and/or Tracing (การบันทึกล็อกและ/หรือการเทรซ)
โดยทั่วไป debugger จะโฟกัสที่สถานะของโปรแกรม ณ ตอน นี้ แต่บางครั้งคุณต้องการมากกว่านั้น—คุณต้องเฝ้าดูสถานะของโปรแกรมหรือโครงสร้างข้อมูลไปตามช่วงเวลา การดู stack trace บอกคุณได้แค่ว่าคุณมาถึงจุดนี้โดยตรงได้อย่างไร แต่มักบอกไม่ได้ว่าคุณกำลังทำอะไรอยู่ก่อนหน้า call chain นี้ โดยเฉพาะในระบบที่ขับเคลื่อนด้วยอีเวนต์ [25]
Tracing statements คือข้อความวินิจฉัยเล็กๆ ที่คุณพิมพ์ออกมาที่หน้าจอหรือไฟล์ เช่น “got here” หรือ “value of x = 2” มันเป็นเทคนิคดั้งเดิมเมื่อเทียบกับ debugger สไตล์ IDE แต่กลับมีประสิทธิภาพอย่างน่าประหลาดใจในการวินิจฉัยข้อผิดพลาดบางประเภทที่ debugger ทำไม่ได้ การ tracing มีค่าอย่างยิ่งในระบบใดๆ ที่เวลาเป็นปัจจัยสำคัญ เช่น concurrent process, real-time system และแอปพลิเคชันที่ขับเคลื่อนด้วยอีเวนต์ครับ
คุณสามารถใช้ tracing statement เพื่อเจาะลึกลงไปในโค้ดได้ กล่าวคือ คุณสามารถเพิ่ม tracing statement ไปเรื่อยๆ ขณะที่ไล่ลงไปตาม call tree ครับ
ข้อความ trace ควรอยู่ในรูปแบบที่สม่ำเสมอและคงเส้นคงวา เพราะคุณอาจต้องการ parse มันแบบอัตโนมัติในภายหลัง ตัวอย่างเช่น ถ้าคุณต้องตามหา resource leak (เช่นการ open/close ไฟล์ที่ไม่สมดุลกัน) คุณก็สามารถ trace การ open และการ close แต่ละครั้งลงในไฟล์ log ได้ จากนั้นเมื่อประมวลผลไฟล์ log ด้วยเครื่องมือประมวลผลข้อความหรือคำสั่ง shell คุณก็จะระบุได้อย่างง่ายดายว่าการ open ตัวที่เป็นต้นเหตุเกิดขึ้นที่ไหนครับ
Rubber Ducking (เทคนิคเป็ดยาง)
เทคนิคที่ง่ายมากแต่มีประโยชน์อย่างยิ่งในการหาสาเหตุของปัญหาก็คือการอธิบายมันให้คนอื่นฟัง คนที่ฟังควรมองข้ามไหล่คุณไปที่หน้าจอ แล้วพยักหน้าไปเรื่อยๆ (เหมือนเป็ดยางที่ลอยขึ้นลงอยู่ในอ่างอาบน้ำ) พวกเขาไม่จำเป็นต้องพูดอะไรเลยสักคำ แค่การอธิบายทีละขั้นตอนว่าโค้ดควรทำอะไรก็มักจะทำให้ปัญหากระโดดออกมาจากหน้าจอและประกาศตัวมันเองออกมาครับ [26]
ฟังดูง่าย แต่การอธิบายปัญหาให้อีกคนฟังนั้น คุณต้องพูดสิ่งต่างๆ ที่คุณอาจมองข้ามไปตอนไล่ดูโค้ดคนเดียวออกมาให้ชัดเจน การต้องพูดสมมติฐานเหล่านี้ออกมาเป็นคำพูด อาจทำให้คุณเกิดมุมมองใหม่ต่อปัญหาขึ้นมาทันที และถ้าคุณไม่มีคนให้พูดด้วย เป็ดยาง ตุ๊กตาหมี หรือแม้แต่ต้นไม้กระถางก็ใช้แทนได้ครับ [27]
Process of Elimination (การคัดออกทีละขั้น)
ในโปรเจกต์ส่วนใหญ่ โค้ดที่คุณกำลังดีบักอยู่อาจเป็นส่วนผสมของโค้ดแอปพลิเคชันที่เขียนโดยคุณและคนอื่นๆ ในทีม, ผลิตภัณฑ์จากบุคคลที่สาม (เช่น database, connectivity, web framework, การสื่อสารเฉพาะทางหรืออัลกอริทึมต่างๆ) และสภาพแวดล้อมของแพลตฟอร์ม (ระบบปฏิบัติการ, system library และ compiler) ครับ
มันเป็นไปได้ที่บั๊กจะอยู่ใน OS, compiler หรือผลิตภัณฑ์จากบุคคลที่สาม—แต่นี่ไม่ควรเป็นความคิดแรกของคุณ มันมีโอกาสสูงกว่ามากที่บั๊กจะอยู่ในโค้ดแอปพลิเคชันที่กำลังพัฒนาอยู่ โดยทั่วไปแล้วการสมมติว่าโค้ดแอปพลิเคชันเรียกใช้ไลบรารีผิดวิธีจะเป็นประโยชน์มากกว่าการสมมติว่าไลบรารีนั้นเสียเอง ถึงแม้ปัญหาจะ อยู่ ที่บุคคลที่สามจริงๆ คุณก็ยังต้องตัดโค้ดของตัวเองออกไปก่อนที่จะส่งรายงานบั๊กอยู่ดีครับ
เราเคยทำโปรเจกต์หนึ่งที่มีวิศวกรอาวุโสคนหนึ่งเชื่อสนิทใจว่า system call ที่ชื่อ select เสียบนระบบ Unix ไม่ว่าจะโน้มน้าวหรือใช้เหตุผลอะไรก็ไม่สามารถเปลี่ยนใจเขาได้ (แม้แต่ความจริงที่ว่าแอปพลิเคชันเครือข่ายอื่นๆ ทุกตัวบนเครื่องนั้นทำงานได้ดีก็ไม่มีความหมายสำหรับเขา) เขาใช้เวลาหลายสัปดาห์เขียน workaround ซึ่งด้วยเหตุผลแปลกๆ บางอย่าง กลับไม่ช่วยแก้ปัญหาได้เลย เมื่อในที่สุดเขาถูกบังคับให้นั่งอ่าน documentation ของ select เขาก็พบปัญหาและแก้ไขมันได้ภายในไม่กี่นาที ตอนนี้เราใช้ประโยค “select is broken” เป็นการเตือนสติกันเบาๆ ทุกครั้งที่ใครสักคนในพวกเราเริ่มโทษระบบสำหรับความผิดที่น่าจะเป็นของเราเองครับ
| Tip 33 | “select” ไม่ได้พัง |
|---|
จำไว้ว่า ถ้าเห็นรอยกีบเท้า ให้คิดถึงม้า—ไม่ใช่ม้าลาย OS ก็คงไม่ได้เสียหรอก และ select ก็คงไม่มีอะไรผิดปกติครับ
ถ้าคุณ “เปลี่ยนแค่อย่างเดียว” แล้วระบบหยุดทำงาน สิ่งนั้นแหละที่มักจะเป็นต้นเหตุ ไม่ทางตรงก็ทางอ้อม ไม่ว่าจะฟังดูเป็นไปไม่ได้แค่ไหนก็ตาม บางครั้งสิ่งที่เปลี่ยนไปก็อยู่นอกเหนือการควบคุมของคุณ เช่น OS, compiler, database หรือซอฟต์แวร์บุคคลที่สามเวอร์ชันใหม่ อาจสร้างความปั่นป่วนให้กับโค้ดที่เคยทำงานถูกต้องมาก่อนได้ บั๊กใหม่ๆ อาจโผล่ขึ้นมา บั๊กที่คุณเคยมี workaround อยู่แล้วอาจถูกแก้ไป จนทำให้ workaround นั้นพังไปด้วย API เปลี่ยน functionality เปลี่ยน พูดง่ายๆ คือมันกลายเป็นเกมใหม่ทั้งหมด และคุณต้องทดสอบระบบใหม่ภายใต้เงื่อนไขใหม่เหล่านี้ ดังนั้นควรจับตาดูตารางเวลาให้ดีเมื่อพิจารณาจะอัปเกรด คุณอาจอยากรอจนกว่าจะ หลังจาก รีลีสถัดไปก็ได้ครับ
The Element of Surprise (องค์ประกอบของความประหลาดใจ)
เมื่อคุณพบว่าตัวเองประหลาดใจกับบั๊กตัวหนึ่ง (อาจจะถึงขั้นพึมพำว่า “เป็นไปไม่ได้” เบาๆ ในที่ที่เราไม่ได้ยิน) คุณต้องกลับมาทบทวนความเชื่อที่คุณยึดถืออยู่ อัลกอริทึมคำนวณส่วนลดตัวนั้น—ตัวที่คุณมั่นใจว่าไร้ที่ติและไม่มีทางเป็นสาเหตุของบั๊กนี้ได้—คุณทดสอบ boundary condition ครบ ทุก แบบแล้วหรือยัง? โค้ดชิ้นอื่นที่คุณใช้มานานหลายปีนั้น—มันคงไม่มีทางยังมีบั๊กอยู่หรอกใช่ไหม? จะเป็นไปได้หรือ?
แน่นอนว่ามันเป็นไปได้ ระดับความประหลาดใจที่คุณรู้สึกเมื่อเกิดข้อผิดพลาด แปรผันตรงกับระดับความไว้ใจและศรัทธาที่คุณมีต่อโค้ดที่กำลังรันอยู่ นั่นเป็นเหตุผลว่าทำไมเมื่อเจอกับความล้มเหลวที่ “น่าประหลาดใจ” คุณต้องยอมรับว่าสมมติฐานอย่างน้อยหนึ่งข้อของคุณผิดพลาด อย่ามองข้ามรูทีนหรือส่วนของโค้ดที่เกี่ยวข้องกับบั๊กเพียงเพราะคุณ “รู้” ว่ามันทำงานถูกต้อง จงพิสูจน์มัน พิสูจน์มันใน บริบทนี้ ด้วย ข้อมูลนี้ ด้วย boundary condition เหล่านี้ ครับ
| Tip 34 | อย่าสมมติ—พิสูจน์มันซะ |
|---|
เมื่อคุณเจอบั๊กที่น่าประหลาดใจ นอกจากแค่แก้มันแล้ว คุณยังต้องหาสาเหตุว่าทำไมความล้มเหลวนี้ถึงไม่ถูกจับได้ตั้งแต่แรก ลองพิจารณาดูว่าคุณต้องปรับปรุง unit test หรือเทสต์อื่นๆ เพื่อให้มันสามารถจับบั๊กแบบนี้ได้หรือไม่ครับ
นอกจากนี้ ถ้าบั๊กเกิดจากข้อมูลที่ผิดพลาดถูกส่งผ่านไปหลายชั้นก่อนที่จะระเบิดออกมา ลองดูว่าการตรวจสอบพารามิเตอร์ที่ดีขึ้นในรูทีนเหล่านั้นจะช่วยแยกปัญหาออกมาได้เร็วกว่านี้หรือไม่ (ดูการพูดคุยเรื่องการแครชแต่เนิ่นๆ และ assertion ได้ที่ ที่นี่ และ ที่นี่ ตามลำดับ)
ในขณะที่คุณกำลังจัดการอยู่นั้น ลองดูด้วยว่ามีจุดอื่นๆ ในโค้ดที่อาจเสี่ยงต่อบั๊กแบบเดียวกันนี้อีกหรือไม่ นี่คือเวลาที่ควรหามันให้เจอแล้วแก้ไขซะ ต้องแน่ใจว่า ไม่ว่าจะเกิดอะไรขึ้น คุณจะรู้ได้ทันทีถ้ามันเกิดขึ้นอีกครั้งครับ
ถ้าใช้เวลานานกว่าจะแก้บั๊กตัวนี้ได้ ลองถามตัวเองดูว่าทำไม มีอะไรที่คุณทำได้เพื่อให้การแก้บั๊กแบบนี้ง่ายขึ้นในครั้งต่อไปหรือไม่? บางทีคุณอาจสร้าง testing hook ที่ดีกว่าเดิม หรือเขียนตัววิเคราะห์ไฟล์ log ขึ้นมาก็ได้ครับ
สุดท้าย ถ้าบั๊กเกิดจากสมมติฐานที่ผิดของใครสักคน ให้เอาปัญหานี้ไปพูดคุยกับทั้งทีม เพราะถ้าคนหนึ่งเข้าใจผิด ก็มีโอกาสที่คนอื่นๆ อีกหลายคนจะเข้าใจผิดเหมือนกันครับ
ทำแบบนี้ทั้งหมด แล้วหวังว่าครั้งหน้าคุณจะไม่ต้องประหลาดใจอีกครับ
Debugging Checklist (เช็คลิสต์การดีบัก)
-
ปัญหาที่ถูกรายงานเข้ามาเป็นผลโดยตรงจากบั๊กที่แท้จริง หรือเป็นแค่อาการเท่านั้น?
-
บั๊กนี้อยู่ใน framework ที่คุณใช้ จริงๆ หรือเปล่า? อยู่ใน OS หรือเปล่า? หรืออยู่ในโค้ดของคุณเองกันแน่?
-
ถ้าคุณต้องอธิบายปัญหานี้อย่างละเอียดให้เพื่อนร่วมงานฟัง คุณจะพูดว่าอย่างไร?
-
ถ้าโค้ดที่ต้องสงสัยผ่าน unit test ทั้งหมด เทสต์เหล่านั้นครอบคลุมมากพอหรือยัง? จะเกิดอะไรขึ้นถ้าคุณรันเทสต์ด้วย ข้อมูลชุดนี้ ?
-
เงื่อนไขที่ทำให้เกิดบั๊กนี้มีอยู่ที่อื่นในระบบอีกหรือไม่? ยังมีบั๊กตัวอื่นที่อยู่ในระยะตัวอ่อน รอวันฟักออกมาอีกไหม?
Related Sections Include (หัวข้อที่เกี่ยวข้อง)
- Topic 24, Dead Programs Tell No Lies (โปรแกรมที่ตายแล้วไม่โกหก)
Challenges (แบบฝึกหัด)
- แค่การดีบักก็ถือเป็นความท้าทายมากพอแล้วครับ