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

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

ข้อกำหนดเกี่ยวกับข้อมูลสรุป

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

สมมติฐาน

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

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

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

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

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

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

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

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