GitHub Copilot เผยเบื้องหลังวิธีเรนเดอร์ PR ขนาดล้านบรรทัด

· By: NatapolK

GitHub Copilot เผยเบื้องหลังวิธีเรนเดอร์ PR ขนาดล้านบรรทัด

การทำ refactor หรือการย้ายระบบครั้งใหญ่มักหลีกเลี่ยงไม่ได้ที่ต้องรวมความเปลี่ยนแปลงไว้ในชุดเดียว

Stacked pull requests เป็นวิธีที่ยอดเยี่ยมในการย่อยงานให้เล็กลงเพื่อให้รีวิวง่ายขึ้นและลดความเสี่ยง แต่การเปลี่ยนแปลงบางประเภทไม่สามารถแยกส่วนได้ชัดเจน ส่งผลให้เกิด Pull Request (PR) ขนาดมหึมาที่เต็มไปด้วยบทสนทนาการรีวิวที่ทำให้ไฟล์บวมขึ้นไปอีก

ประสบการณ์การรีวิวต้องรวดเร็วและราบรื่นแม้ diff และบทสนทนาจะใหญ่โตมโหฬาร ใน GitHub Copilot app เราจึงสร้างมุมมอง PR ขึ้นมาใหม่โดยเน้นประสิทธิภาพสูงสุดเป็นหัวใจสำคัญ เพื่อทดสอบขีดจำกัด เราได้ลองเปิด PR ที่ใหญ่ที่สุดเท่าที่จะหาได้ ซึ่งเป็นโปรเจกต์โอเพนซอร์สที่มี 2,200 ไฟล์ เปลี่ยนแปลงโค้ดกว่าหนึ่งล้านบรรทัด และมีคอมเมนต์รีวิวแบบ inline มากกว่า 400 รายการ นี่คือวิธีที่เราทำให้ PR ระดับสุดโต่งนี้ยังทำงานได้อย่างมีประสิทธิภาพ

GitHub Copilot เผยเบื้องหลังวิธีเรนเดอร์ PR ขนาดล้านบรรทัด

ขอบเขตของปัญหา

การเรนเดอร์ diff ขนาดใหญ่ด้วยความเร็วไม่ใช่เรื่องใหม่ เราใช้ virtualization สำหรับแถว (rows) เพื่อรักษาจำนวน DOM ที่ถูก mount ให้ต่ำที่สุด โดยอาศัยหลักการว่าแถวโค้ดทุกแถวมีความสูงคงที่

แต่ส่วนที่ท้าทายจริงๆ คือ "ความคิดเห็น" (Comments) เพราะความสูงของมันไม่คงที่ ขึ้นอยู่กับการตัดบรรทัดของ Markdown, ส่วนที่ขยายได้, กล่องตอบกลับ หรือแม้แต่รูปภาพที่กำลังโหลด ข้อมูลเหล่านี้จะทราบได้ก็ต่อเมื่อทำการเรนเดอร์แล้วเท่านั้น ซึ่งขัดกับสถาปัตยกรรมแบบเดิม

ปัญหาหลักมี 3 ประการคือ:

  1. การวัดผล (Measurement): เราไม่รู้ความสูงของคอมเมนต์จนกว่าจะเรนเดอร์ ซึ่งทำลายระบบเลื่อนหน้าจอที่ลื่นไหล
  2. ช่องทางข้อมูล (Data pipeline): ความเร็วของ diff จะไร้ความหมายหากการส่งข้อมูลหยุดชะงักหรือต้องเริ่มงานใหม่ซ้ำๆ
  3. การพบ Bug จริง: ปัญหาเหล่านี้มักเกิดเฉพาะตอนโหลดหนักๆ หรือในตำแหน่งการเลื่อนที่เฉพาะเจาะจง เราจึงต้องสร้างระบบวัดผลอัตโนมัติเพื่อวิเคราะห์และปรับปรุงอย่างต่อเนื่อง

ส่วนที่ 1: Virtualization และสาเหตุที่คอมเมนต์ทำให้ระบบพัง

ขั้นตอนแรกคือการทำความเข้าใจโครงสร้าง (geometry) ที่ทำให้ diff แบบโค้ดล้วนทำงานได้เร็ว แต่เมื่อมีคอมเมนต์เข้ามาแทรก โครงสร้างแบบเดิมก็ใช้ไม่ได้อีกต่อไป

สิ่งที่ทำให้ diff ขนาดใหญ่รวดเร็ว

เราไม่สามารถวาง DOM nodes ล้านโหนดลงในหน้าเดียวได้ คำตอบคือ virtualization หรือการ mount เฉพาะแถวที่ปรากฏบนหน้าจอและนำ DOM เดิมมาใช้ซ้ำ (recycle) ขณะเลื่อน ทำให้หน้าเว็บดูเหมือนมีครบทุกแถวแต่ใช้ทรัพยากรจริงเพียงประมาณ 100 แถวเท่านั้น เพื่อให้ระบบนี้ทำงานได้ ต้องมีการคำนวณความสูงรวมของแถบเลื่อนและตำแหน่งของแต่ละแถวไว้ล่วงหน้า หากทุกแถวคือบรรทัดโค้ดที่มีขนาดฟอนต์แน่นอน เราสามารถคำนวณตารางความสูงทั้งหมดได้ทันทีโดยไม่ต้องแก้ไขภายหลัง

เราเรียกสิ่งนี้ว่าสัญญา "ทราบความสูงทั้งหมดก่อนการ paint" (all heights known before paint) ซึ่งเป็นรากฐานของส่วนแสดงผล diff ของเรา ทั้งการเรนเดอร์แถวแบบ imperative, การใช้ Typed-array คำนวณตำแหน่ง และการส่งข้อมูลแบบ stream จาก backend

ความคิดเห็นเปลี่ยนสัญญาอย่างไร

เมื่อลองแทรก thread การรีวิวไว้กลาง diff คำถามคือมันจะสูงเท่าไหร่? เราไม่มีทางรู้ล่วงหน้าเพราะความสูงเปลี่ยนได้ตลอดเวลาจาก Markdown ที่ตัดบรรทัดต่างกัน, บล็อก <details> ที่พับขยายได้ หรือรูปภาพที่โหลดเสร็จทีหลัง

การจองพื้นที่แบบคงที่ (fixed height) มักล้มเหลวใน PR ขนาดใหญ่ เพราะถ้าประมาณการผิดจะเกิดช่องว่างสีขาวหรือเนื้อหาถูกตัด และหากวัดความสูงจริงแล้วไปอัปเดตตารางตำแหน่งขณะที่ผู้ใช้กำลังเลื่อน จะเกิดอาการ "หน้าจอกระตุก" (scroll jump) ที่รุนแรงมาก

ดังนั้น ความคิดเห็นจึงต้องการสัญญาใหม่ คือความสูงต้องมีขอบเขต วัดผลแบบ lazy และการแก้ไขตำแหน่งต้องเกิดขึ้นน้อยที่สุดโดยยึดโยงกับสิ่งที่ผู้ใช้กำลังดูอยู่

แยกรูปทรงเป็นสองส่วน

เราแก้ปัญหานี้ด้วยการแยกความสูงของเอกสารออกเป็นสองโดเมนอิสระต่อกัน:

total height = deterministic code height          (แม่นยำ, ทราบล่วงหน้า)
             + Σ dynamic block effective heights   (ประมาณค่า, แล้วจึงวัดผล)
             + scroll padding

รูปทรงของโค้ด (Code geometry) ยังคงความแม่นยำและไม่เปลี่ยนแปลงเมื่อคอมเมนต์เปลี่ยนขนาด ส่วน รูปทรงของบล็อกแบบไดนามิก (Dynamic block geometry) จะดูแลส่วนที่ไม่แน่นอนอย่างคอมเมนต์ โดยใช้คีย์ที่เสถียรยึดกับไฟล์และบรรทัดแทนพิกเซลพิกัด ทำให้การวัดผลใหม่ไม่กระทบโครงสร้างหลัก

การจัดการเวลาการวัดผล

เราเลิกใช้ ResizeObserver หนึ่งตัวต่อหนึ่งบล็อกเพราะทำให้เกิด loop การทำงานที่ซ้ำซ้อนและกินทรัพยากร แต่เปลี่ยนมาใช้การวัดผลรอบเดียวที่ควบคุมด้วยสถานะว่าง (idle) และการเลื่อน:

  • หลีกเลี่ยงช่วงเลื่อนหน้าจอ: จะไม่วัดผลขณะเลื่อนเพื่อป้องกันอาการกระตุก แต่จะทำงานเมื่อหน้าจอนิ่งแล้ว
  • จำกัดใน Viewport: วัดผลเฉพาะบล็อกที่อยู่ใกล้หน้าจอ (ประมาณ 2400px) เพื่อให้งานเป็นแบบ O(viewport)
  • ลำดับความสำคัญ: บล็อกบนหน้าจอต้องถูกวัดค่าจริงและแม่นยำที่สุด ส่วนบล็อกนอกหน้าจอจะใช้การเรนเดอร์ล่วงหน้าอย่างจำกัด
  • ข้อยกเว้น: หากผู้ใช้เป็นคนสั่งขยายเนื้อหาเอง (เช่น กดดูคำตอบ) ระบบจะวัดผลและขยับโค้ดด้านล่างทันทีในเฟรมเดียวกันเพื่อให้ความรู้สึกเป็นธรรมชาติ

การยึดตำแหน่งการเลื่อน (Scroll anchoring)

เมื่อความสูงเปลี่ยน ตำแหน่งหน้าจออาจกระโดด เราจึงใช้การยึดโยงด้วยตัวตน (identity) แทนพิกเซล โดยจับภาพว่าผู้ใช้กำลังดูแถวหรือบล็อกไหนอยู่ เมื่อความสูงอัปเดต ระบบจะคำนวณพิกเซลใหม่และเลื่อนหน้าจอให้จุดเดิมยังคงอยู่ที่เดิมในสายตาผู้ใช้

ส่วนที่ 2: Pipeline ข้อมูลเบื้องหลัง

UI จะเร็วได้เท่ากับข้อมูลที่ส่งมา เราจึงเน้น 3 นิสัยหลัก: อย่างแรกคือส่งโครงสร้างไฟล์มาก่อนเนื้อหาเพื่อให้หน้าเว็บปรากฏทันที อย่างที่สองคือเลื่อนงานที่ไม่จำเป็นออกไป เช่น การเน้นสีไวยากรณ์ (Syntax highlighting) จะทำนอก main thread และแสดงผลเมื่อพร้อม และสุดท้ายคือการใช้ระบบแคชที่ชาญฉลาด เก็บข้อมูล PR ล่าสุดไว้บางส่วนเพื่อให้สลับหน้าไปมาได้อย่างรวดเร็ว

ส่วนที่ 3: การตรวจจับ Bug ด้วยระบบอัตโนมัติ

Bug ในระบบซับซ้อนแบบนี้มักมองไม่เห็นด้วยตาเปล่า เราจึงติดตั้ง "ตัวตรวจสอบ" (probes) ไว้ในแอปเพื่อตอบคำถามเชิงประสิทธิภาพในทุกการเรนเดอร์ เช่น มีกี่แถวที่ถูก mount อยู่ หรือการเลื่อนกระตุกหรือไม่

เรายังใช้ระบบ Autopilot เพื่อทดสอบแอปจริงๆ บนเดสก์ท็อปแบบวนลูป ทั้งการเปิด PR ขนาดใหญ่ เลื่อนหน้าจอ และโต้ตอบกับคอมเมนต์ โดยมีเอเจนต์วิเคราะห์ log และสัญญาณสุขภาพของแอปโดยอัตโนมัติ ช่วยให้เราพบจุดบกพร่องและแก้ไขได้ทันทีโดยไม่ต้องมีคนนั่งเฝ้า

บทสรุป

การรีวิว PR ขนาดใหญ่จะไม่ใช่เรื่องน่าเบื่ออีกต่อไป ผลลัพธ์ที่ได้คือมุมมอง PR ที่สามารถจัดการโค้ดล้านบรรทัดและคอมเมนต์หลายร้อยรายการได้ลื่นไหลเหมือน PR ขนาดปกติ คอมเมนต์จะแสดงผลครบถ้วน การขยายเนื้อหาทำได้ราบรื่น และตำแหน่งการอ่านจะถูกรักษาไว้อย่างแม่นยำ

หากคุณต้องรีวิวโค้ดเป็นประจำ ลองสัมผัสประสบการณ์ใหม่นี้ด้วยการเปิด PR ที่ใหญ่ที่สุดที่คุณมีได้เลย

ลองใช้แอป GitHub Copilot >

Source: GitHub Blog
ดูแลงานแปลและเรียบเรียงโดย NatapolK

ความคิดเห็น (0)

เข้าสู่ระบบเพื่อร่วมแสดงความเห็น

สมัครสมาชิก

มาเป็นคนแรกที่แสดงความเห็นกันเลยโบร