ความท้าทายของกฎการเขียน

หน้านี้จะแสดงภาพรวมระดับสูงของปัญหาและความท้าทายที่เฉพาะเจาะจง ในการเขียนกฎ Bazel ที่มีประสิทธิภาพ

ข้อกำหนดของข้อมูลสรุป

  • สมมติฐาน: มุ่งเน้นความถูกต้อง อัตราการส่งข้อมูล ความสะดวกในการใช้งาน และเวลาในการตอบสนอง
  • สมมติฐาน: ที่เก็บข้อมูลขนาดใหญ่
  • สมมติฐาน: ภาษาคำอธิบายที่คล้ายกับ BUILD
  • ประวัติ: การแยกส่วนอย่างชัดเจนระหว่างการโหลด การวิเคราะห์ และการดำเนินการ ล้าสมัยแล้ว แต่ยังคงส่งผลต่อ API
  • โดยธรรมชาติ: การดำเนินการและการแคชจากระยะไกลเป็นเรื่องยาก
  • Intrinsic: Using Change Information for Correct and Fast Incremental Builds requires Unusual Coding Patterns
  • โดยธรรมชาติ: การหลีกเลี่ยงการใช้เวลาและหน่วยความจำแบบกำลังสองเป็นเรื่องยาก

สมมติฐาน

ต่อไปนี้คือสมมติฐานบางอย่างเกี่ยวกับระบบบิลด์ เช่น ความจำเป็นในการความถูกต้อง ความง่ายในการใช้งาน อัตราการส่งข้อมูล และที่เก็บข้อมูลขนาดใหญ่ ส่วนต่อไปนี้จะกล่าวถึงสมมติฐานเหล่านี้และเสนอหลักเกณฑ์เพื่อให้มั่นใจว่าจะมีการเขียนกฎอย่างมีประสิทธิภาพ

มุ่งเน้นความถูกต้อง อัตราการส่งข้อมูล ความสะดวกในการใช้งาน และเวลาในการตอบสนอง

เราถือว่าระบบบิลด์ต้องถูกต้องเป็นอันดับแรกและสำคัญที่สุด เมื่อพิจารณาถึงบิลด์ที่เพิ่มขึ้น สำหรับโครงสร้างแหล่งที่มาที่กำหนด เอาต์พุตของบิลด์เดียวกันควรเหมือนกันเสมอ ไม่ว่าโครงสร้างเอาต์พุตจะเป็นอย่างไร ในค่าประมาณแรก หมายความว่า Bazel ต้องทราบอินพุตทุกรายการที่เข้าสู่ขั้นตอนการบิลด์ที่กำหนด เพื่อให้สามารถเรียกใช้ขั้นตอนนั้นอีกครั้งได้หากอินพุตใดๆ มีการเปลี่ยนแปลง Bazel มีข้อจำกัดในเรื่องความถูกต้อง เนื่องจากจะแสดงข้อมูลบางอย่าง เช่น วันที่ / เวลาของการบิลด์ และไม่สนใจการเปลี่ยนแปลงบางประเภท เช่น การเปลี่ยนแปลงแอตทริบิวต์ของไฟล์ แซนด์บ็อกซ์ ช่วยให้มั่นใจในความถูกต้องโดยป้องกันการอ่านไฟล์อินพุตที่ไม่ได้ประกาศ นอกเหนือจาก ขีดจำกัดโดยธรรมชาติของระบบแล้ว ยังมีปัญหาด้านความถูกต้องที่ทราบอยู่ 2-3 อย่าง ซึ่งส่วนใหญ่เกี่ยวข้องกับ Fileset หรือกฎ C++ ซึ่งทั้ง 2 อย่างนี้เป็นปัญหาที่แก้ไขได้ยาก เรามีแผนระยะยาวในการแก้ไขปัญหาเหล่านี้

เป้าหมายที่ 2 ของระบบบิลด์คือการมีปริมาณงานสูง เรากำลังขยายขีดจำกัดของสิ่งที่ทำได้ภายในการจัดสรรเครื่องปัจจุบันสำหรับบริการการดำเนินการจากระยะไกลอย่างต่อเนื่อง หากบริการการดำเนินการจากระยะไกล มีการใช้งานมากเกินไป ก็จะไม่มีใครทำงานได้

ความสะดวกในการใช้งานมาเป็นอันดับถัดไป ในบรรดาวิธีการที่ถูกต้องหลายวิธีซึ่งมีร่องรอยของบริการการดำเนินการจากระยะไกลเหมือนกัน (หรือคล้ายกัน) เราเลือกวิธีที่ใช้งานง่ายกว่า

เวลาในการตอบสนองหมายถึงเวลาที่ใช้ตั้งแต่เริ่มบิลด์จนถึงได้รับผลลัพธ์ที่ต้องการ ไม่ว่าจะเป็นบันทึกการทดสอบจากการทดสอบที่ผ่านหรือไม่ผ่าน หรือข้อความแสดงข้อผิดพลาด ที่ระบุว่าBUILDไฟล์มีการสะกดผิด

โปรดทราบว่าเป้าหมายเหล่านี้มักจะทับซ้อนกัน โดยเวลาในการตอบสนองเป็นฟังก์ชันของปริมาณงานของบริการการดำเนินการระยะไกล เช่นเดียวกับความถูกต้องที่เกี่ยวข้องกับความสะดวกในการใช้งาน

ที่เก็บขนาดใหญ่

ระบบบิลด์ต้องทำงานในระดับของที่เก็บข้อมูลขนาดใหญ่ ซึ่งหมายความว่าที่เก็บข้อมูลมีขนาดใหญ่เกินกว่าจะจัดเก็บไว้ในฮาร์ดไดรฟ์เพียงเครื่องเดียว จึงเป็นไปไม่ได้ที่จะทำการเช็คเอาต์แบบเต็มในคอมพิวเตอร์สำหรับการพัฒนาซอฟต์แวร์แทบทุกเครื่อง บิลด์ขนาดกลาง จะต้องอ่านและแยกวิเคราะห์ไฟล์ BUILD หลายหมื่นไฟล์ และประเมิน รูปแบบไฟล์หลายแสนรายการ แม้ว่าในทางทฤษฎีแล้วจะสามารถอ่านไฟล์ BUILD ทั้งหมดในเครื่องเดียวได้ แต่เรายังไม่สามารถทำเช่นนั้นได้ภายในระยะเวลาและหน่วยความจำที่สมเหตุสมผล ดังนั้น BUILD ไฟล์ จึงต้องโหลดและแยกวิเคราะห์ได้อย่างอิสระ

ภาษาคำอธิบายที่คล้ายกับ BUILD

ในบริบทนี้ เราจะถือว่าภาษาการกำหนดค่ามีความคล้ายคลึงกับไฟล์ BUILD ในการประกาศกฎของไลบรารีและไบนารี และขึ้นอยู่กับกันและกัน BUILD อ่านและแยกวิเคราะห์ไฟล์ได้โดยอิสระ และเราจะหลีกเลี่ยงการดูไฟล์ต้นฉบับทุกครั้งที่ทำได้ (ยกเว้น การตรวจสอบว่ามีอยู่จริง)

ประวัติศาสตร์

เวอร์ชัน Bazel มีความแตกต่างกันซึ่งทำให้เกิดความท้าทาย และความแตกต่างบางส่วนจะอธิบายไว้ในส่วนต่อไปนี้

การแยกการโหลด การวิเคราะห์ และการดำเนินการอย่างชัดเจนเป็นแนวทางที่ล้าสมัย แต่ก็ยังส่งผลต่อ API

ในทางเทคนิคแล้ว กฎเพียงแค่ต้องทราบไฟล์อินพุตและเอาต์พุตของ การดำเนินการก่อนที่จะส่งการดำเนินการไปยังการดำเนินการจากระยะไกล อย่างไรก็ตาม ฐานของโค้ด Bazel เดิมมีการแยกส่วนอย่างเข้มงวดในการโหลดแพ็กเกจ จากนั้น วิเคราะห์กฎโดยใช้การกำหนดค่า (โดยพื้นฐานแล้วคือแฟล็กบรรทัดคำสั่ง) และ จากนั้นจึงเรียกใช้การดำเนินการใดๆ ความแตกต่างนี้ยังคงเป็นส่วนหนึ่งของกฎ API ในปัจจุบัน แม้ว่าแกนหลักของ Bazel จะไม่จำเป็นต้องใช้แล้วก็ตาม (ดูรายละเอียดเพิ่มเติมด้านล่าง)

ซึ่งหมายความว่า Rules API ต้องมีคำอธิบายแบบประกาศของอินเทอร์เฟซกฎ (แอตทริบิวต์ที่มี ประเภทของแอตทริบิวต์) มี ข้อยกเว้นบางกรณีที่ API อนุญาตให้โค้ดที่กำหนดเองทำงานในระหว่างระยะการโหลดเพื่อ คำนวณชื่อโดยนัยของไฟล์เอาต์พุตและค่าโดยนัยของแอตทริบิวต์ ตัวอย่างเช่น กฎ java_library ที่ชื่อ "foo" จะสร้างเอาต์พุตที่ชื่อ "libfoo.jar" โดยนัย ซึ่งอ้างอิงได้จากกฎอื่นๆ ในกราฟการสร้าง

นอกจากนี้ การวิเคราะห์กฎยังอ่านไฟล์ต้นฉบับหรือตรวจสอบเอาต์พุตของการดำเนินการไม่ได้ แต่ต้องสร้างกราฟแบบสองส่วนแบบมีทิศทางบางส่วนของขั้นตอนการสร้างและชื่อไฟล์เอาต์พุตที่กำหนดจากกฎเองและการขึ้นต่อกันเท่านั้น

Intrinsic

การเขียนกฎเป็นเรื่องที่ท้าทายเนื่องจากมีคุณสมบัติโดยธรรมชาติบางอย่าง และคุณสมบัติที่พบบ่อยที่สุดบางส่วนจะอธิบายไว้ในส่วนต่อไปนี้

การดำเนินการและการแคชจากระยะไกลเป็นเรื่องยาก

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

ในขณะนี้ โปรโตคอลกำหนดให้ระบบบิลด์ต้องทราบอินพุตทั้งหมดของการดำเนินการที่กำหนดล่วงหน้า จากนั้นระบบบิลด์จะคำนวณลายนิ้วมือการดำเนินการที่ไม่ซ้ำกัน แล้วขอให้ตัวกำหนดเวลาพบแคช หากพบแคชฮิต ตัวกำหนดเวลาก็จะตอบกลับด้วยไดเจสต์ของไฟล์เอาต์พุต ส่วนไฟล์เองจะระบุด้วยไดเจสต์ในภายหลัง อย่างไรก็ตาม การดำเนินการนี้จะกำหนดข้อจำกัดเกี่ยวกับกฎของ Bazel ซึ่งต้องประกาศไฟล์อินพุตทั้งหมดล่วงหน้า

การใช้ข้อมูลการเปลี่ยนแปลงสำหรับการสร้างแบบเพิ่มทีละรายการที่ถูกต้องและรวดเร็วต้องใช้รูปแบบการเขียนโค้ดที่ผิดปกติ

ในส่วนด้านบน เราได้อธิบายว่า Bazel ต้องทราบไฟล์อินพุตทั้งหมดที่เข้าสู่ขั้นตอนการบิลด์เพื่อตรวจหาว่าขั้นตอนการบิลด์นั้นยังเป็นข้อมูลล่าสุดหรือไม่ การโหลดแพ็กเกจและการวิเคราะห์กฎก็เช่นกัน และเราได้ออกแบบ Skyframe เพื่อจัดการเรื่องนี้โดยทั่วไป Skyframe เป็นไลบรารีกราฟและเฟรมเวิร์กการประเมินที่ใช้โหนดเป้าหมาย (เช่น 'สร้าง //foo ด้วยตัวเลือกเหล่านี้') และแยกย่อยเป็นส่วนประกอบต่างๆ ซึ่งจะได้รับการประเมินและรวมเข้าด้วยกันเพื่อให้ได้ผลลัพธ์นี้ ในกระบวนการนี้ Skyframe จะอ่านแพ็กเกจ วิเคราะห์กฎ และ ดำเนินการ

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

ในส่วนนี้ โหนดแต่ละโหนดจะดำเนินการค้นหากระบวนการขึ้นต่อกัน แต่ละโหนดสามารถประกาศการอ้างอิง แล้วใช้เนื้อหาของการอ้างอิงเหล่านั้นเพื่อประกาศการอ้างอิงเพิ่มเติมได้ ในทางทฤษฎีแล้ว การดำเนินการนี้จะสอดคล้องกับโมเดล เธรดต่อโหนด อย่างไรก็ตาม บิลด์ขนาดกลางมีโหนด Skyframe หลายแสนรายการ ซึ่งเทคโนโลยี Java ปัจจุบันทำได้ยาก (และด้วยเหตุผลทางประวัติศาสตร์ ปัจจุบันเราจึงต้องใช้ Java ดังนั้นจึงไม่มีเธรดน้ำหนักเบาและไม่มีการดำเนินการต่อ)

แต่ Bazel จะใช้กลุ่มเธรดที่มีขนาดคงที่แทน อย่างไรก็ตาม นั่นหมายความว่าหากโหนดประกาศทรัพยากร Dependency ที่ยังไม่พร้อมใช้งาน เราอาจต้องยกเลิกการประเมินนั้นและรีสตาร์ท (อาจอยู่ในเทรดอื่น) เมื่อทรัพยากร Dependency พร้อมใช้งาน ซึ่งหมายความว่าโหนดไม่ควรทำเช่นนี้มากเกินไป โหนดที่ประกาศการขึ้นต่อกัน N รายการแบบอนุกรมอาจรีสตาร์ทได้ N ครั้ง ซึ่งใช้เวลา O(N^2) แต่เรามุ่งเน้นการประกาศการขึ้นต่อกันแบบกลุ่มล่วงหน้า ซึ่งบางครั้งอาจต้องจัดระเบียบโค้ดใหม่ หรือแม้กระทั่งแยก โหนดออกเป็นหลายโหนดเพื่อจำกัดจำนวนการรีสตาร์ท

โปรดทราบว่าขณะนี้เทคโนโลยีนี้ยังไม่พร้อมใช้งานใน Rules API แต่ Rules API ยังคงกำหนดโดยใช้แนวคิดเดิมของเฟสการโหลด การวิเคราะห์ และการดำเนินการ อย่างไรก็ตาม ข้อจำกัดพื้นฐานคือการเข้าถึงโหนดอื่นๆ ทั้งหมดต้องผ่านเฟรมเวิร์กเพื่อให้เฟรมเวิร์กติดตามการอ้างอิงที่เกี่ยวข้องได้ ไม่ว่าระบบบิลด์จะได้รับการติดตั้งใช้งานในภาษาใดหรือเขียนกฎในภาษาใด (ไม่จำเป็นต้องเป็นภาษาเดียวกัน) ผู้เขียนกฎต้องไม่ใช้ไลบรารีหรือรูปแบบมาตรฐานที่ข้าม Skyframe สำหรับ Java นั่นหมายถึงการหลีกเลี่ยง java.io.File รวมถึงรูปแบบใดก็ตามของ การสะท้อน และไลบรารีใดก็ตามที่ทำอย่างใดอย่างหนึ่ง ไลบรารีที่รองรับการแทรก Dependency ของอินเทอร์เฟซระดับล่างเหล่านี้ยังคงต้องได้รับการตั้งค่าอย่างถูกต้องสำหรับ Skyframe

ซึ่งแสดงให้เห็นอย่างชัดเจนว่าควรหลีกเลี่ยงการให้ผู้เขียนกฎเข้าถึงรันไทม์ของภาษาแบบเต็ม ตั้งแต่แรก อันตรายจากการใช้ API ดังกล่าวโดยไม่ตั้งใจนั้นมีมากเกินไป ข้อบกพร่องหลายอย่างของ Bazel ในอดีตเกิดจากกฎที่ใช้ API ที่ไม่ปลอดภัย แม้ว่า กฎเหล่านั้นจะเขียนโดยทีม Bazel หรือผู้เชี่ยวชาญด้านอื่นๆ ก็ตาม

การหลีกเลี่ยงการใช้เวลาและหน่วยความจำแบบกำลังสองเป็นเรื่องยาก

นอกจากข้อกำหนดที่ Skyframe กำหนด ข้อจำกัดในอดีตของการใช้ Java และความล้าสมัยของ Rules API แล้ว การใช้เวลาหรือหน่วยความจำแบบกำลังสองโดยไม่ตั้งใจยังเป็นปัญหาพื้นฐานในระบบบิลด์ใดๆ ที่อิงตามกฎของไลบรารีและไบนารี มีรูปแบบที่พบบ่อย 2 รูปแบบ ซึ่งทำให้เกิดการใช้หน่วยความจำแบบกำลังสอง (และทำให้ ใช้เวลาแบบกำลังสองด้วย)

  1. เชนของกฎไลบรารี - พิจารณากรณีของเชนของกฎไลบรารี A ขึ้นอยู่กับ B ขึ้นอยู่กับ C และ อื่นๆ จากนั้นเราต้องการคำนวณพร็อพเพอร์ตี้บางอย่างผ่านการปิดทรานซิทีฟของกฎเหล่านี้ เช่น คลาสพาธรันไทม์ของ Java หรือคำสั่งลิงก์เกอร์ C++ สำหรับแต่ละไลบรารี ในขั้นต้น เราอาจใช้การติดตั้งใช้งานรายการมาตรฐาน อย่างไรก็ตาม การดำเนินการนี้จะทำให้ใช้หน่วยความจำแบบกำลังสองอยู่แล้ว กล่าวคือ ไลบรารีแรก มีรายการเดียวใน classpath, ไลบรารีที่สองมี 2 รายการ, ไลบรารีที่สามมี 3 รายการ และอื่นๆ รวมเป็น 1+2+3+...+N = O(N^2) รายการ

  2. กฎไบนารีที่ขึ้นอยู่กับกฎไลบรารีเดียวกัน - พิจารณากรณีที่มีชุดไบนารีที่ขึ้นอยู่กับกฎไลบรารีเดียวกัน เช่น หากคุณมีกฎการทดสอบหลายรายการที่ทดสอบโค้ดไลบรารีเดียวกัน สมมติว่าจากกฎ N ข้อ มีกฎแบบไบนารีครึ่งหนึ่งและกฎไลบรารีอีกครึ่งหนึ่ง ตอนนี้ลองพิจารณาว่าไบนารีแต่ละรายการจะทำสำเนาของ พร็อพเพอร์ตี้บางอย่างที่คำนวณจาก Closure แบบทรานซิทีฟของกฎไลบรารี เช่น Classpath ของรันไทม์ Java หรือบรรทัดคำสั่งของ Linker C++ เช่น อาจขยายการแสดงสตริงบรรทัดคำสั่งของการดำเนินการลิงก์ C++ การคัดลอกองค์ประกอบ N/2 จำนวน N/2 รายการต้องใช้หน่วยความจำ O(N^2)

คลาสคอลเล็กชันที่กำหนดเองเพื่อหลีกเลี่ยงความซับซ้อนแบบกำลังสอง

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

แต่การใช้ Depset ไม่ได้แก้ปัญหาทั้งหมดโดยอัตโนมัติ โดยเฉพาะอย่างยิ่ง แม้เพียงการวนซ้ำใน Depset ในแต่ละกฎก็ทำให้เกิด การใช้เวลาแบบกำลังสอง ภายใน NestedSets ยังมีเมธอดตัวช่วยบางอย่าง เพื่ออำนวยความสะดวกในความสามารถในการทำงานร่วมกันกับคลาสคอลเล็กชันปกติ แต่การส่ง NestedSet ไปยังเมธอดเหล่านี้โดยไม่ตั้งใจจะทำให้เกิดลักษณะการทำงานของการคัดลอก และทำให้เกิดการใช้หน่วยความจำแบบกำลังสองอีกครั้ง