Khung tranh tường

Báo cáo vấn đề Xem nguồn Hằng đêm · 7,4 của Google. 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Mô hình đánh giá song song và mô hình tăng dần của Bazel.

Mô hình dữ liệu

Mô hình dữ liệu bao gồm các mục sau:

  • SkyValue. Còn được gọi là nút. SkyValues là các đối tượng không thể thay đổi chứa tất cả dữ liệu được tạo trong quá trình xây dựng và dữ liệu đầu vào của bản dựng. Ví dụ: tệp đầu vào, tệp đầu ra, mục tiêu và tệp đã định cấu hình mục tiêu.
  • SkyKey. Một tên ngắn bất biến để tham chiếu đến SkyValue, ví dụ: FILECONTENTS:/tmp/foo hoặc PACKAGE://foo.
  • SkyFunction. Tạo các nút dựa trên khoá và các nút phụ thuộc.
  • Biểu đồ nút. Cấu trúc dữ liệu chứa mối quan hệ phần phụ thuộc giữa các nút.
  • Skyframe. Tên mã cho khung đánh giá gia tăng mà Bazel dựa trên đó.

Đánh giá

Một bản dựng sẽ đánh giá nút đại diện cho yêu cầu bản dựng (đây là trạng thái chúng ta đang hướng đến, nhưng vẫn còn rất nhiều mã cũ cần được thêm vào). Trước tiên, SkyFunction của lớp đó được tìm thấy và gọi bằng khoá của SkyKey cấp cao nhất. Sau đó, hàm này sẽ yêu cầu đánh giá các nút mà nó cần để đánh giá nút cấp cao nhất, từ đó dẫn đến các lệnh gọi hàm khác, v.v. cho đến khi đạt được các nút lá (thường là các nút đại diện cho tệp đầu vào trong hệ thống tệp). Cuối cùng, chúng ta sẽ đưa ra giá trị của SkyValue cấp cao nhất, một số hiệu ứng phụ (chẳng hạn như các tệp đầu ra trong hệ thống tệp) và một đồ thị không chu trình có hướng của các phần phụ thuộc giữa các nút có liên quan đến bản dựng.

SkyFunction có thể yêu cầu SkyKeys trong nhiều lần truyền nếu không thể biết trước tất cả các nút mà nó cần để thực hiện công việc của mình. Một ví dụ đơn giản là đánh giá nút tệp đầu vào hóa ra là một đường liên kết tượng trưng: hàm này cố gắng đọc tệp, nhận ra đó là một đường liên kết tượng trưng và do đó tìm nạp nút hệ thống tệp đại diện cho mục tiêu của đường liên kết tượng trưng. Nhưng chính đường dẫn đó cũng có thể là một đường liên kết tượng trưng, trong trường hợp đó, hàm ban đầu cũng cần tìm nạp mục tiêu của nó.

Các hàm được biểu thị trong mã bằng giao diện SkyFunction còn các dịch vụ được cung cấp qua giao diện có tên là SkyFunction.Environment. Sau đây là những việc mà các hàm có thể làm:

  • Yêu cầu đánh giá một nút khác bằng cách gọi env.getValue. Nếu nút có sẵn, giá trị của nút đó sẽ được trả về, nếu không, null sẽ được trả về và hàm đó dự kiến sẽ trả về null. Trong trường hợp sau, nút phụ thuộc sẽ được đánh giá, sau đó trình tạo nút ban đầu được gọi lại, nhưng lần này lệnh gọi env.getValue tương tự sẽ trả về một giá trị không phải null.
  • Yêu cầu đánh giá nhiều nút khác bằng cách gọi env.getValues(). Về cơ bản, điều này giống nhau, ngoại trừ việc các nút phụ thuộc được đánh giá song song.
  • Thực hiện việc tính toán trong lệnh gọi
  • Có tác dụng phụ, chẳng hạn như ghi tệp vào hệ thống tệp. Bạn cần cẩn thận để hai hàm khác nhau không can thiệp vào nhau. Nói chung, bạn có thể viết các hiệu ứng phụ (trong đó dữ liệu truyền ra từ Bazel). Đọc các hiệu ứng phụ (trong đó dữ liệu truyền vào Bazel mà không có phần phụ thuộc đã đăng ký) thì không, vì chúng là phần phụ thuộc chưa đăng ký và do đó có thể gây ra các bản dựng gia tăng không chính xác.

Việc triển khai SkyFunction không được truy cập dữ liệu theo cách nào khác ngoài việc yêu cầu các phần phụ thuộc (chẳng hạn như bằng cách trực tiếp đọc hệ thống tệp), vì điều đó sẽ khiến Bazel không đăng ký phần phụ thuộc dữ liệu trên tệp đã đọc, dẫn đến các bản dựng gia tăng không chính xác.

Khi có đủ dữ liệu để thực hiện công việc, hàm đó sẽ trả về giá trị không phải null cho biết đã hoàn thành.

Chiến lược đánh giá này có một số lợi ích:

  • Tính kín. Nếu các hàm chỉ yêu cầu dữ liệu đầu vào bằng cách phụ thuộc vào các nút khác, thì Bazel có thể đảm bảo rằng nếu trạng thái đầu vào giống nhau, thì dữ liệu trả về cũng giống nhau. Nếu tất cả các hàm bầu trời đều mang tính xác định, thì toàn bộ bản dựng cũng sẽ có tính xác định.
  • Mức độ gia tăng chính xác và hoàn hảo. Nếu tất cả dữ liệu đầu vào của tất cả các hàm được ghi lại, thì Bazel chỉ có thể vô hiệu hoá chính tập hợp các nút cần được vô hiệu hoá khi dữ liệu đầu vào thay đổi.
  • Song song. Vì các hàm chỉ có thể tương tác với nhau bằng cách yêu cầu phần phụ thuộc, nên các hàm không phụ thuộc vào nhau có thể chạy song song và Bazel có thể đảm bảo rằng kết quả sẽ giống như khi chúng được chạy tuần tự.

Mức độ gia tăng

Vì các hàm chỉ có thể truy cập vào dữ liệu đầu vào bằng cách phụ thuộc vào các nút khác, nên Bazel có thể xây dựng một biểu đồ luồng dữ liệu hoàn chỉnh từ các tệp đầu vào đến các tệp đầu ra và sử dụng thông tin này để chỉ xây dựng lại các nút thực sự cần xây dựng lại: kết thúc bắc cầu ngược của tập hợp các tệp đầu vào đã thay đổi.

Cụ thể, có hai chiến lược tăng dần có thể áp dụng: chiến lược từ dưới lên và chiến lược từ trên xuống. Phương án nào tối ưu sẽ phụ thuộc vào hình thức của biểu đồ phần phụ thuộc.

  • Trong quá trình vô hiệu hoá từ dưới lên, sau khi tạo một biểu đồ và xác định được tập hợp các đầu vào đã thay đổi, tất cả các nút đều không hợp lệ và phụ thuộc bắc cầu vào các tệp đã thay đổi. Đây là phương pháp tối ưu nếu chúng ta biết rằng cùng một nút cấp cao nhất sẽ được tạo lại. Lưu ý rằng việc vô hiệu hoá từ dưới lên yêu cầu chạy stat() trên tất cả các tệp đầu vào của bản dựng trước để xác định xem chúng đã được thay đổi hay chưa. Bạn có thể cải thiện vấn đề này bằng cách sử dụng inotify hoặc một cơ chế tương tự để tìm hiểu về các tệp đã thay đổi.

  • Trong quá trình vô hiệu hoá từ trên xuống, hệ thống sẽ kiểm tra tập hợp đóng bắc cầu của nút cấp cao nhất và chỉ giữ lại những nút có tập hợp đóng bắc cầu sạch. Sẽ tốt hơn nếu chúng ta biết rằng biểu đồ nút hiện tại lớn, nhưng chúng ta chỉ cần một tập hợp con nhỏ của biểu đồ này trong bản dựng tiếp theo: việc vô hiệu hoá từ dưới lên sẽ làm mất hiệu lực biểu đồ lớn hơn của bản dựng đầu tiên, không giống như việc vô hiệu hoá từ trên xuống chỉ đi qua biểu đồ nhỏ của bản dựng thứ hai.

Hiện tại, chúng tôi chỉ thực hiện việc vô hiệu hoá từ dưới lên.

Để tăng tính gia tăng, chúng ta sử dụng tính năng loại bỏ thay đổi: nếu một nút bị vô hiệu hoá, nhưng khi tạo lại, chúng ta phát hiện thấy giá trị mới của nút đó giống với giá trị cũ, thì các nút bị vô hiệu hoá do thay đổi trong nút này sẽ được "hồi sinh".

Cách này hữu ích, chẳng hạn như khi một người thay đổi nhận xét trong tệp C++: thì tệp .o được tạo từ tệp đó sẽ giống nhau, do đó, chúng ta không cần phải gọi lại trình liên kết.

Liên kết / Biên dịch gia tăng

Hạn chế chính của mô hình này là việc vô hiệu hoá một nút là một vấn đề toàn bộ hoặc không có gì: khi một phần phụ thuộc thay đổi, nút phụ thuộc luôn được tạo lại từ đầu, ngay cả khi có một thuật toán tốt hơn sẽ thay đổi giá trị cũ của nút dựa trên các thay đổi. Sau đây là một số ví dụ về trường hợp hữu ích của tính năng này:

  • Liên kết gia tăng
  • Khi một tệp .class thay đổi trong .jar, về lý thuyết, chúng ta có thể sửa đổi tệp .jar thay vì tạo lại tệp đó từ đầu.

Lý do mà Bazel hiện không hỗ trợ những việc này theo nguyên tắc (chúng tôi có một số biện pháp hỗ trợ cho việc liên kết gia tăng, nhưng không được triển khai trong Skyframe) có hai lần: chúng tôi chỉ đạt được hiệu suất hạn chế và khó đảm bảo rằng kết quả của quá trình đột biến cũng giống như việc xây dựng lại sạch sẽ và Google đánh giá các bản dựng có thể lặp lại từng bit.

Cho đến nay, chúng ta luôn có thể đạt được hiệu suất đủ tốt bằng cách chỉ cần phân tách một bước xây dựng tốn kém và đạt được đánh giá lại một phần theo cách đó: công cụ này chia tất cả các lớp trong một ứng dụng thành nhiều nhóm và tạo tệp dex trên các lớp đó riêng biệt. Bằng cách này, nếu các lớp trong một nhóm không thay đổi, bạn không cần phải tạo lại tệp dex.

Liên kết với các khái niệm Bazel

Dưới đây là thông tin tổng quan sơ lược về một số cách triển khai SkyFunction mà Bazel sử dụng để tạo bản dựng:

  • FileStateValue. Kết quả của lstat(). Đối với các tệp hiện có, chúng tôi cũng tính toán thông tin bổ sung để phát hiện các thay đổi đối với tệp. Đây là nút cấp thấp nhất trong biểu đồ Skyframe và không có phần phụ thuộc.
  • FileValue. Được sử dụng bởi mọi thứ quan tâm đến nội dung thực tế và/hoặc đường dẫn đã phân giải của tệp. Tuỳ thuộc vào FileStateValue tương ứng và mọi đường liên kết tượng trưng cần được phân giải (chẳng hạn như FileValue cho a/b cần đường dẫn đã phân giải của a và đường dẫn đã phân giải của a/b). Sự khác biệt giữa FileStateValue là quan trọng vì trong một số trường hợp (ví dụ: đánh giá glob hệ thống tệp (chẳng hạn như srcs=glob(["*/*.java"])), nội dung của tệp thực sự không cần thiết.
  • DirectoryListingValue. Về cơ bản, đây là kết quả của readdir(). Phụ thuộc vào FileValue được liên kết với thư mục.
  • PackageValue (Giá trị gói). Đại diện cho phiên bản đã phân tích cú pháp của tệp BUILD. Tuỳ thuộc vào FileValue của tệp BUILD được liên kết, đồng thời cũng tuỳ thuộc vào mọi DirectoryListingValue được dùng để phân giải glob trong gói (cấu trúc dữ liệu đại diện cho nội dung của tệp BUILD trong nội bộ)
  • ConfiguredTargetValue. Đại diện cho mục tiêu đã định cấu hình, là một bộ gồm tập hợp các hành động được tạo trong quá trình phân tích mục tiêu và thông tin được cung cấp cho các mục tiêu đã định cấu hình phụ thuộc vào mục tiêu này. Tuỳ thuộc vào PackageValue có mục tiêu tương ứng, ConfiguredTargetValues của phần phụ thuộc trực tiếp và một nút đặc biệt đại diện cho cấu hình bản dựng.
  • ArtifactValue. Biểu thị một tệp trong bản dựng, cho dù đó là một cấu phần phần mềm nguồn hay đầu ra (cấu phần phần mềm gần như tương đương với tệp và được dùng để tham chiếu đến tệp trong quá trình thực thi thực tế của các bước bản dựng). Đối với tệp nguồn, điều này phụ thuộc vào FileValue của nút được liên kết, đối với cấu phần phần mềm đầu ra, điều này phụ thuộc vào ActionExecutionValue của bất kỳ hành động nào tạo ra cấu phần phần mềm.
  • ActionExecutionValue. Biểu thị việc thực thi một hành động. Phụ thuộc vào ArtifactValues của các tệp đầu vào. Thao tác mà nó thực thi hiện nằm trong khoá sky, điều này trái ngược với khái niệm rằng khoá sky phải nhỏ. Chúng tôi đang nỗ lực giải quyết sự không nhất quán này (lưu ý rằng ActionExecutionValueArtifactValue sẽ không được dùng nếu chúng tôi không chạy giai đoạn thực thi trên Skyframe).