การประเมินแบบคู่ขนานและโมเดลส่วนเพิ่มของ Bazel
โมเดลข้อมูล
โมเดลข้อมูลประกอบด้วยรายการต่อไปนี้
SkyValue
เรียกอีกอย่างว่าโหนดSkyValues
คือออบเจ็กต์ที่เปลี่ยนแปลงไม่ได้ซึ่งมีข้อมูลทั้งหมดที่สร้างขึ้นในช่วงบิลด์ และอินพุตของบิลด์ ตัวอย่างเช่น ไฟล์อินพุต ไฟล์เอาต์พุต เป้าหมาย และเป้าหมายที่กำหนดค่าSkyKey
ชื่อสั้นๆ ที่เปลี่ยนแปลงไม่ได้เพื่ออ้างอิงถึงSkyValue
เช่นFILECONTENTS:/tmp/foo
หรือPACKAGE://foo
SkyFunction
สร้างโหนดตามคีย์และโหนดที่อ้างอิง- กราฟโหนด โครงสร้างข้อมูลที่มีความสัมพันธ์แบบการพึ่งพาระหว่างโหนด
Skyframe
ชื่อโค้ดของเฟรมเวิร์กการประเมินที่เพิ่มขึ้น Bazel อิงตาม
การประเมิน
บิลด์ประกอบด้วยการประเมินโหนดที่แสดงถึงคำขอบิลด์ (นี่คือสถานะที่เราต้องการ แต่มีโค้ดเดิมจำนวนมากอยู่ระหว่างดำเนินการ) ก่อนอื่น ระบบพบ SkyFunction
และเรียกใช้ด้วยคีย์ของ SkyKey
ระดับบนสุด จากนั้นฟังก์ชันจะขอการประเมินโหนดที่ต้องใช้ในการประเมินโหนดระดับบนสุด ซึ่งส่งผลให้เกิดการเรียกใช้ฟังก์ชันอื่นๆ เป็นเช่นนี้ไปเรื่อยๆ จนกระทั่งถึงโหนด Leaf (ซึ่งมักเป็นโหนดที่แสดงถึงไฟล์อินพุตในระบบไฟล์) สุดท้าย เราจะจบด้วยค่าของ SkyValue
ระดับบนสุด ผลข้างเคียงบางอย่าง (เช่น ไฟล์เอาต์พุตในระบบไฟล์) และกราฟแบบวนซ้ำที่มีทิศทางของทรัพยากร Dependency ระหว่างโหนดที่เกี่ยวข้องในการสร้าง
SkyFunction
สามารถขอ SkyKeys
ในหลายบัตรได้หากบอกโหนดทั้งหมดที่จำเป็นต้องใช้ในการทำงานล่วงหน้าไม่ได้ ตัวอย่างง่ายๆ คือการประเมินโหนดไฟล์อินพุตที่กลายเป็นลิงก์สัญลักษณ์ โดยฟังก์ชันจะพยายามอ่านไฟล์แล้วรู้ว่าเป็นลิงก์สัญลักษณ์ จึงดึงโหนดระบบไฟล์ที่เป็นตัวแทนเป้าหมายของลิงก์สัญลักษณ์ แต่ผลลัพธ์นั้นอาจเป็นลิงก์สัญลักษณ์ได้ ซึ่งในกรณีนี้ฟังก์ชันต้นฉบับจะต้องดึงข้อมูลเป้าหมายด้วย
ฟังก์ชันจะแสดงเป็นโค้ดโดยอินเทอร์เฟซ SkyFunction
และบริการที่มีให้โดยอินเทอร์เฟซที่เรียกว่า SkyFunction.Environment
ฟังก์ชันต่างๆ ทำได้ดังต่อไปนี้
- ขอการประเมินโหนดอื่นโดยการเรียกใช้
env.getValue
หากโหนดพร้อมใช้งาน ระบบจะส่งคืนค่าของโหนด มิเช่นนั้นจะแสดงผลnull
และฟังก์ชันควรแสดงผลnull
ในกรณีหลัง โหนดที่อ้างอิงกันจะได้รับการประเมิน แล้วจะมีการเรียกใช้เครื่องมือสร้างโหนดเดิมอีกครั้ง แต่ครั้งนี้การเรียกใช้env.getValue
เดียวกันจะแสดงค่าที่ไม่ใช่null
- ขอการประเมินโหนดอื่นๆ หลายโหนดโดยเรียกใช้
env.getValues()
ซึ่งโดยพื้นฐานแล้วก็จะเหมือนกัน เว้นแต่ว่าโหนดที่เกี่ยวข้องจะได้รับการประเมินแบบคู่ขนาน - คำนวณในระหว่างการเรียกใช้
- มีผลข้างเคียง เช่น เขียนไฟล์ลงในระบบไฟล์ แต่ควรระวังเรื่องงาน 2 อย่างที่ว่านี้ไม่ได้เหยียบเท้ากัน โดยทั่วไปแล้ว การเขียนผลข้างเคียง (ในกรณีที่ข้อมูลไหลออกมาจาก Bazel) นั้นใช้ได้ อ่านผลข้างเคียง (ในกรณีที่ข้อมูลไหลเข้าสู่ Bazel โดยไม่ต้องมี Dependency ที่ลงทะเบียน) จะไม่มีผล เนื่องจากข้อมูลดังกล่าวเป็นทรัพยากร Dependency ที่ไม่ได้ลงทะเบียน จึงอาจทำให้เกิดการเพิ่มขึ้นที่ไม่ถูกต้องได้
การใช้งาน SkyFunction
ไม่ควรเข้าถึงข้อมูลด้วยวิธีอื่นๆ นอกเหนือจากการขอทรัพยากร Dependency (เช่น การอ่านระบบไฟล์โดยตรง) เนื่องจากจะทำให้ Bazel ไม่ลงทะเบียนการอ้างอิงข้อมูลในไฟล์ที่อ่าน ซึ่งส่งผลให้มีบิลด์ส่วนเพิ่มที่ไม่ถูกต้อง
เมื่อฟังก์ชันมีข้อมูลเพียงพอที่จะทำงานแล้ว ฟังก์ชันควรแสดงผลค่าที่ไม่ใช่ null
ซึ่งระบุถึงการทำงานเสร็จสมบูรณ์
กลยุทธ์การประเมินนี้มีประโยชน์หลายประการดังนี้
- ความสุจริต หากฟังก์ชันขอเฉพาะข้อมูลอินพุตโดยจะขึ้นอยู่กับโหนดอื่นๆ ด้วย Bazel รับประกันได้ว่าหากสถานะอินพุตเหมือนกัน ระบบจะส่งคืนข้อมูลเดียวกัน หากฟังก์ชันของ Sky ทั้งหมดเป็นแบบกำหนดทิศทาง ก็หมายความว่าสิ่งที่สร้างทั้งหมดจะกำหนดทิศทางด้วยเช่นกัน
- ส่วนเพิ่มที่ถูกต้องและสมบูรณ์แบบ หากมีการบันทึกข้อมูลอินพุตทั้งหมดของฟังก์ชันทั้งหมด Bazel จะนำเฉพาะชุดของโหนดที่ถูกต้องซึ่งกำหนดให้ไม่ถูกต้องเมื่อข้อมูลอินพุตเปลี่ยนแปลงเท่านั้น
- การทำงานพร้อมกัน เนื่องจากฟังก์ชันต่างๆ จะโต้ตอบระหว่างกันด้วยการขอทรัพยากร Dependency เท่านั้น ฟังก์ชันที่ไม่ได้พึ่งพากันและกันจึงจะทำงานพร้อมกันได้และ Bazel สามารถรับประกันได้ว่าผลลัพธ์จะเหมือนกันเสมือนว่าทำงานตามลำดับ
ส่วนเพิ่ม
เนื่องจากฟังก์ชันจะเข้าถึงได้เฉพาะข้อมูลอินพุตเท่านั้น โดยขึ้นอยู่กับโหนดอื่นๆ ทำให้ Bazel สร้างโฟลว์ข้อมูลที่สมบูรณ์จากไฟล์อินพุตไปยังไฟล์เอาต์พุต และใช้ข้อมูลนี้เพื่อสร้างโหนดใหม่เฉพาะที่ต้องสร้างใหม่เท่านั้น นั่นคือการปิดแบบย้อนกลับของชุดไฟล์อินพุตที่มีการเปลี่ยนแปลง
กล่าวอย่างเจาะจงคือ กลยุทธ์ส่วนเพิ่มที่เป็นไปได้มีอยู่ 2 กลยุทธ์ด้วยกัน ได้แก่ กลยุทธ์จากล่างขึ้นบนและกลยุทธ์จากบนลงล่าง วิธีใดจะเหมาะสมที่สุดนั้นขึ้นอยู่กับลักษณะของกราฟทรัพยากร Dependency
ในระหว่างการสร้างโมฆะแบบจากล่างขึ้นบน หลังจากที่สร้างกราฟและทราบชุดอินพุตที่มีการเปลี่ยนแปลงแล้ว โหนดทั้งหมดจะเป็นโมฆะซึ่งอาศัยไฟล์ที่มีการเปลี่ยนแปลงแบบสับเปลี่ยน ซึ่งเหมาะสมที่สุดหากเราทราบว่าจะสร้างโหนดระดับบนสุดเดียวกันอีกครั้ง โปรดทราบว่าการระบุสถานะจากล่างขึ้นบนต้องเรียกใช้
stat()
ในไฟล์อินพุตทั้งหมดของบิลด์ก่อนหน้าเพื่อพิจารณาว่ามีการเปลี่ยนแปลงหรือไม่ ซึ่งคุณจะปรับปรุงได้โดยใช้inotify
หรือกลไกที่คล้ายกันเพื่อเรียนรู้เกี่ยวกับไฟล์ที่มีการเปลี่ยนแปลงในระหว่างการทำให้โหนดระดับบนสุดใช้งานไม่ได้ ระบบจะตรวจสอบการปิดแบบทางอ้อมของโหนดระดับบนสุดและเก็บเฉพาะโหนดเหล่านั้นที่การปิดแบบทรานซิชันไม่มีความเรียบร้อย วิธีนี้จะดีกว่าหากเราทราบว่ากราฟโหนดปัจจุบันมีขนาดใหญ่ แต่เราต้องการเพียงส่วนเล็กๆ ของกราฟโหนดในบิลด์ถัดไปเท่านั้น นั่นคือ การใช้ไม่ได้จากด้านล่างขึ้นบนจะทำให้กราฟที่ใหญ่กว่าของบิลด์แรกใช้งานไม่ได้ ซึ่งต่างจากการใช้จากด้านบนลงล่าง ซึ่งจะเป็นเพียงกราฟเล็กๆ ของบิลด์ที่ 2
ปัจจุบันเราทำให้ฟังก์ชันจากล่างขึ้นบนใช้ไม่ได้เท่านั้น
หากต้องการหาส่วนเพิ่มเพิ่มเติม เราใช้การตัดการเปลี่ยนแปลง: หากโหนดใช้งานไม่ได้ แต่เมื่อสร้างโหนดใหม่ พบว่าค่าใหม่เหมือนกับค่าเก่า โหนดที่ใช้งานไม่ได้เนื่องจากการเปลี่ยนแปลงในโหนดนี้จะ "กู้คืน"
ซึ่งจะเป็นประโยชน์ เช่น ในกรณีที่มีการเปลี่ยนแปลงความคิดเห็นในไฟล์ C++ ไฟล์ .o
ที่สร้างขึ้นจากไฟล์นั้นจะยังคงเดิม เราจึงไม่จำเป็นต้องเรียก Linker อีก
การลิงก์ / การคอมไพล์ส่วนเพิ่ม
ข้อจำกัดหลักของโมเดลนี้คือการทำให้โหนดเป็นโมฆะเป็นส่วนความสัมพันธ์ทั้งหมดหรือไม่เกี่ยวข้องเลย โดยเมื่อมีการเปลี่ยนแปลงทรัพยากร Dependency จะสร้างโหนดอ้างอิงใหม่ตั้งแต่ต้นเสมอ แม้ว่าจะมีอัลกอริทึมที่ดีกว่าอยู่ซึ่งจะกลายค่าเก่าของโหนดตามการเปลี่ยนแปลงดังกล่าวก็ตาม ตัวอย่างบางส่วนที่การดำเนินการนี้จะเป็นประโยชน์
- การเพิ่มลิงก์
- เมื่อไฟล์
.class
ไฟล์เดียวมีการเปลี่ยนแปลงใน.jar
ในทางทฤษฎี เราอาจแก้ไขไฟล์.jar
แทนการสร้างไฟล์ใหม่ตั้งแต่ต้นอีกครั้ง
เหตุผลที่ปัจจุบัน Bazel ยังไม่รองรับสิ่งเหล่านี้ตามหลักการ (เรามีมาตรการรองรับการลิงก์เพิ่มขึ้น แต่ไม่ได้นำไปใช้ภายใน Skyframe) มีอยู่ 2 อย่าง คือเรามีประสิทธิภาพเพิ่มขึ้นเพียงเล็กน้อยเท่านั้น และรับประกันไม่ได้ว่าผลลัพธ์ของการเปลี่ยนแปลงจะเหมือนกับการสร้างใหม่ที่ดูสะอาดตา และค่าของ Google ที่สร้างแบบบิตต่อบิตก็ทำซ้ำๆ ได้
จนถึงตอนนี้ เราคงประสิทธิภาพที่ดีเพียงพอได้เสมอโดยแยกขั้นตอนบิลด์ที่ราคาแพงและบรรลุผลการประเมินซ้ำบางส่วนด้วยวิธีดังกล่าว โดยจะแบ่งคลาสทั้งหมดในแอปออกเป็นหลายกลุ่ม และแยกคลาสต่างๆ แยกกัน ด้วยวิธีนี้ หากชั้นเรียนในกลุ่มไม่มีการเปลี่ยนแปลง ก็ไม่จำเป็นต้องทำซ้ำ
การแมปกับแนวคิดของ Bazel
นี่คือภาพรวมคร่าวๆ ของการใช้งาน SkyFunction
บางส่วนที่ Bazel ใช้ในการสร้างบิลด์
- FileStateValue ผลลัพธ์ของ
lstat()
สำหรับไฟล์ที่มีอยู่ เราจะคำนวณข้อมูลเพิ่มเติมเพื่อตรวจหาการเปลี่ยนแปลงในไฟล์ นี่เป็นโหนดระดับต่ำสุดในกราฟ Skyframe และไม่มีทรัพยากร Dependency - FileValue ใช้สำหรับอะไรก็ได้ที่ให้ความสำคัญกับเนื้อหาจริงและ/หรือเส้นทางที่แก้ไขแล้วของไฟล์ ขึ้นอยู่กับ
FileStateValue
ที่เกี่ยวข้องและลิงก์สัญลักษณ์ที่ต้องได้รับการแก้ไข (เช่นFileValue
สำหรับa/b
ต้องใช้เส้นทางที่แก้ไขแล้วของa
และเส้นทางที่แก้ไขแล้วของa/b
) ความแตกต่างระหว่างFileStateValue
มีความสำคัญเนื่องจากในบางกรณี (เช่น การประเมิน glob ของระบบไฟล์ (เช่นsrcs=glob(["*/*.java"])
) ไม่จำเป็นต้องมีการใช้เนื้อหาของไฟล์ - DirectoryListingValue ผลลัพธ์โดยประมาณของ
readdir()
ขึ้นอยู่กับFileValue
ที่เชื่อมโยงกับไดเรกทอรี - PackageValue แสดงเวอร์ชันที่แยกวิเคราะห์ของไฟล์ BUILD ขึ้นอยู่กับ
FileValue
ของไฟล์BUILD
ที่เชื่อมโยง และขึ้นอยู่กับDirectoryListingValue
ที่ใช้เพื่อแก้ไข glob ในแพ็กเกจ (โครงสร้างข้อมูลที่แสดงถึงเนื้อหาของไฟล์BUILD
ภายใน) - ConfiguredTargetValue แสดงเป้าหมายที่กำหนดค่า ซึ่งเป็นชุดของชุดการกระทำที่สร้างขึ้นระหว่างการวิเคราะห์เป้าหมายและข้อมูลที่ระบุให้กับเป้าหมายที่กำหนดค่าไว้ซึ่งอ้างอิงเป้าหมายนี้ ขึ้นอยู่กับ
PackageValue
โดยมีเป้าหมายที่เกี่ยวข้อง,ConfiguredTargetValues
ของทรัพยากร Dependency โดยตรง และโหนดพิเศษที่แสดงถึงการกำหนดค่าบิลด์ - ArtifactValue แสดงไฟล์ในบิลด์ ไม่ว่าจะเป็นต้นฉบับหรืออาร์ติแฟกต์เอาต์พุต (อาร์ติแฟกต์แทบจะเทียบเท่ากับไฟล์และใช้เพื่ออ้างถึงไฟล์ในระหว่างดำเนินการตามขั้นตอนของบิลด์จริง) สำหรับไฟล์ต้นฉบับ จะขึ้นอยู่กับ
FileValue
ของโหนดที่เกี่ยวข้อง สำหรับอาร์ติแฟกต์เอาต์พุตจะขึ้นอยู่กับActionExecutionValue
ของการดำเนินการใดก็ตามที่สร้างอาร์ติแฟกต์ - ActionExecutionValue แสดงถึงการดำเนินการ ขึ้นอยู่กับ
ArtifactValues
ของไฟล์อินพุต การทำงานที่ดำเนินการอยู่นั้นอยู่ในคีย์ Sky ซึ่งขัดกับแนวคิดที่ว่า คีย์ Sky ควรมีขนาดเล็ก เรากำลังหาทางแก้ไขความคลาดเคลื่อนนี้ (โปรดทราบว่าระบบจะไม่ได้ใช้ActionExecutionValue
และArtifactValue
หากเราไม่ได้เรียกใช้เฟสการดำเนินการบน Skyframe)