หน้านี้จะแสดงภาพรวมระดับสูงของปัญหาและความท้าทายที่เฉพาะเจาะจง ในการเขียนกฎ 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 จะใช้กลุ่มเธรดที่มีขนาดคงที่แทน อย่างไรก็ตาม นั่นหมายความว่าหากโหนดประกาศการขึ้นต่อกันที่ยังไม่พร้อมใช้งาน เราอาจต้องยกเลิกการประเมินนั้นและรีสตาร์ท (อาจอยู่ในเธรดอื่น) เมื่อการขึ้นต่อกันพร้อมใช้งาน ซึ่งหมายความว่าโหนดไม่ควรทำเช่นนี้มากเกินไป โหนดที่ประกาศการขึ้นต่อกัน 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 รูปแบบ ซึ่งทำให้เกิดการใช้หน่วยความจำแบบกำลังสอง (และทำให้เกิด การใช้เวลาแบบกำลังสอง)
เชนของกฎไลบรารี - พิจารณากรณีของเชนของกฎไลบรารี A ขึ้นอยู่กับ B ขึ้นอยู่กับ C และ อื่นๆ จากนั้นเราต้องการคำนวณพร็อพเพอร์ตี้บางอย่างผ่านการปิดทรานซิทีฟของกฎเหล่านี้ เช่น คลาสพาธรันไทม์ของ Java หรือคำสั่งลิงก์เกอร์ C++ สำหรับแต่ละไลบรารี ในขั้นต้น เราอาจใช้การติดตั้งใช้งานรายการมาตรฐาน อย่างไรก็ตาม การดำเนินการนี้จะทำให้ใช้หน่วยความจำแบบกำลังสองอยู่แล้ว กล่าวคือ ไลบรารีแรก มีรายการเดียวใน classpath, ไลบรารีที่สองมี 2 รายการ, ไลบรารีที่สามมี 3 รายการ และอื่นๆ รวมเป็น 1+2+3+...+N = O(N^2) รายการ
กฎไบนารีที่ขึ้นอยู่กับกฎไลบรารีเดียวกัน - พิจารณากรณีที่ชุดไบนารีขึ้นอยู่กับกฎไลบรารีเดียวกัน เช่น หากคุณมีกฎการทดสอบหลายรายการที่ทดสอบโค้ดไลบรารีเดียวกัน สมมติว่าจากกฎ N ข้อ มีกฎแบบไบนารีครึ่งหนึ่งและกฎไลบรารีอีกครึ่งหนึ่ง ตอนนี้ลองพิจารณาว่าไบนารีแต่ละรายการจะทำสำเนาของ พร็อพเพอร์ตี้บางอย่างที่คำนวณจากการปิดทรานซิทีฟของกฎไลบรารี เช่น Classpath ของรันไทม์ Java หรือบรรทัดคำสั่งของ Linker C++ เช่น อาจขยายการแสดงสตริงบรรทัดคำสั่งของการดำเนินการลิงก์ C++ การคัดลอกองค์ประกอบ N/2 จำนวน N/2 รายการต้องใช้หน่วยความจำ O(N^2)
คลาสคอลเล็กชันที่กำหนดเองเพื่อหลีกเลี่ยงความซับซ้อนแบบกำลังสอง
ทั้ง 2 สถานการณ์นี้ส่งผลกระทบอย่างมากต่อ Bazel เราจึงได้เปิดตัวชุดคลาสคอลเล็กชันที่กำหนดเองซึ่งบีบอัดข้อมูลในหน่วยความจำได้อย่างมีประสิทธิภาพโดยหลีกเลี่ยงการคัดลอกในแต่ละขั้นตอน โครงสร้างข้อมูลเหล่านี้เกือบทั้งหมดมีซีแมนทิกส์ของชุดข้อมูล ดังนั้นเราจึงเรียกโครงสร้างข้อมูลนี้ว่า depset
(หรือที่เรียกว่า NestedSet
ในการใช้งานภายใน) การเปลี่ยนแปลงส่วนใหญ่
เพื่อลดการใช้หน่วยความจำของ Bazel ในช่วงหลายปีที่ผ่านมาคือ
การเปลี่ยนแปลงเพื่อใช้ Depset แทนสิ่งที่เคยใช้ก่อนหน้านี้
แต่การใช้ Depset ไม่ได้แก้ปัญหาทั้งหมดโดยอัตโนมัติ โดยเฉพาะอย่างยิ่ง แม้เพียงการวนซ้ำผ่าน Depset ในแต่ละกฎก็ทำให้เกิด การใช้เวลาแบบกำลังสอง ภายใน NestedSets ยังมีเมธอดตัวช่วยบางอย่าง เพื่ออำนวยความสะดวกในการทำงานร่วมกันกับคลาสคอลเล็กชันปกติ แต่ การส่ง NestedSet ไปยังเมธอดเหล่านี้โดยไม่ตั้งใจจะทำให้เกิดการคัดลอก และทำให้การใช้หน่วยความจำแบบกำลังสองกลับมาอีกครั้ง