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