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