Tối ưu hoá hiệu suất

Khi viết các quy tắc, sai lầm phổ biến nhất về hiệu suất là truyền tải hoặc sao chép dữ liệu được tích luỹ từ các phần phụ thuộc. Khi được tổng hợp trên toàn bộ bản dựng, các thao tác này có thể dễ dàng tiêu tốn thời gian hoặc dung lượng O(N^2). Để tránh tình trạng này, bạn cần hiểu rõ cách sử dụng phần phụ thuộc một cách hiệu quả.

Điều này có thể khó chính xác, vì vậy, Bazel cũng cung cấp một trình phân tích bộ nhớ để hỗ trợ bạn tìm những điểm mà bạn có thể đã mắc lỗi. Cảnh báo: Chi phí viết một quy tắc không hiệu quả có thể không rõ ràng cho đến khi quy tắc đó được sử dụng rộng rãi.

Sử dụng phần phụ thuộc

Bất cứ khi nào bạn tổng hợp thông tin từ các phần phụ thuộc quy tắc, bạn nên sử dụng phần phụ thuộc. Chỉ sử dụng danh sách thuần tuý hoặc lệnh chính tả để xuất bản thông tin cục bộ cho quy tắc hiện tại.

Phần phụ thuộc biểu thị thông tin dưới dạng biểu đồ lồng nhau cho phép chia sẻ.

Hãy xem xét biểu đồ sau:

C -> B -> A
D ---^

Mỗi nút phát hành một chuỗi duy nhất. Với phần phụ thuộc, dữ liệu sẽ có dạng như sau:

a = depset(direct=['a'])
b = depset(direct=['b'], transitive=[a])
c = depset(direct=['c'], transitive=[b])
d = depset(direct=['d'], transitive=[b])

Xin lưu ý rằng mỗi mục chỉ được đề cập một lần. Với danh sách, bạn sẽ nhận được:

a = ['a']
b = ['b', 'a']
c = ['c', 'b', 'a']
d = ['d', 'b', 'a']

Xin lưu ý rằng trong trường hợp này, 'a' được đề cập 4 lần! Với biểu đồ lớn hơn, vấn đề này sẽ chỉ ngày càng trầm trọng hơn.

Dưới đây là ví dụ về cách triển khai quy tắc sử dụng các phần phụ thuộc một cách chính xác để xuất bản thông tin bắc cầu. Lưu ý rằng bạn có thể phát hành thông tin cục bộ về quy tắc bằng cách sử dụng danh sách nếu muốn vì đây không phải là O(N^2).

MyProvider = provider()

def _impl(ctx):
  my_things = ctx.attr.things
  all_things = depset(
      direct=my_things,
      transitive=[dep[MyProvider].all_things for dep in ctx.attr.deps]
  )
  ...
  return [MyProvider(
    my_things=my_things,  # OK, a flat list of rule-local things only
    all_things=all_things,  # OK, a depset containing dependencies
  )]

Xem trang tổng quan về phần khởi động để biết thêm thông tin.

Tránh gọi cho depset.to_list()

Bạn có thể chuyển đổi một phần phụ thuộc thành một danh sách phẳng bằng cách sử dụng to_list(). Tuy nhiên, việc này thường dẫn đến chi phí O(N^2). Nếu có thể, hãy tránh làm phẳng các phần phụ thuộc ngoại trừ mục đích gỡ lỗi.

Một quan niệm sai lầm phổ biến là bạn có thể tự do làm phẳng các phần phụ thuộc nếu chỉ thực hiện việc này ở các mục tiêu cấp cao nhất (chẳng hạn như quy tắc <xx>_binary), vì khi đó chi phí sẽ không được tích luỹ trên mỗi cấp của biểu đồ bản dựng. Tuy nhiên, đây vẫn là O(N^2) khi bạn xây dựng một tập hợp mục tiêu có các phần phụ thuộc trùng lặp. Điều này xảy ra khi xây dựng chương trình kiểm thử //foo/tests/... hoặc khi nhập một dự án IDE.

Giảm số lượng cuộc gọi xuống depset

Việc gọi depset trong một vòng lặp thường là một lỗi. Điều này có thể dẫn đến các phần phụ thuộc có sự lồng ghép rất sâu, hoạt động kém hiệu quả. Ví dụ:

x = depset()
for i in inputs:
    # Do not do that.
    x = depset(transitive = [x, i.deps])

Mã này có thể dễ dàng được thay thế. Trước tiên, hãy thu thập các phần phụ thuộc bắc cầu và hợp nhất tất cả cùng một lúc:

transitive = []

for i in inputs:
    transitive.append(i.deps)

x = depset(transitive = transitive)

Đôi khi, bạn có thể giảm thiểu vấn đề này bằng cách đọc hiểu danh sách:

x = depset(transitive = [i.deps for i in inputs])

Sử dụng ctx.actions.args() cho các dòng lệnh

Khi tạo các dòng lệnh, bạn nên sử dụng ctx.actions.args(). Điều này trì hoãn việc mở rộng mọi phần phụ thuộc vào giai đoạn thực thi.

Ngoài việc nhanh hơn hoàn toàn, điều này còn giúp giảm mức sử dụng bộ nhớ cho các quy tắc của bạn – đôi khi từ 90% trở lên.

Sau đây là một số thủ thuật:

  • Truyền trực tiếp các phần phụ thuộc và danh sách dưới dạng đối số, thay vì tự làm phẳng chúng. Các miền này sẽ được mở rộng thêm cho bạn muộn nhất vào ctx.actions.args(). Nếu bạn cần bất kỳ hành động biến đổi nào trên nội dung phụ thuộc, hãy xem ctx.actions.args#add để xem có điều gì phù hợp với hoá đơn hay không.

  • Bạn có đang truyền File#path dưới dạng đối số không? Không cần. Mọi Tệp sẽ tự động được chuyển thành đường dẫn, được trì hoãn theo thời gian mở rộng.

  • Tránh tạo chuỗi bằng cách nối chúng với nhau. Đối số chuỗi tốt nhất là một hằng số vì bộ nhớ của nó sẽ được chia sẻ giữa tất cả các thực thể của quy tắc.

  • Nếu đối số quá dài đối với dòng lệnh, thì đối tượng ctx.actions.args() có thể được ghi theo điều kiện hoặc vô điều kiện vào tệp tham số bằng ctx.actions.args#use_param_file. Việc này được thực hiện ở hậu trường khi thực thi hành động. Nếu cần kiểm soát rõ ràng tệp tham số, bạn có thể viết tệp này theo cách thủ công bằng cách sử dụng ctx.actions.write.

Ví dụ:

def _impl(ctx):
  ...
  args = ctx.actions.args()
  file = ctx.declare_file(...)
  files = depset(...)

  # Bad, constructs a full string "--foo=<file path>" for each rule instance
  args.add("--foo=" + file.path)

  # Good, shares "--foo" among all rule instances, and defers file.path to later
  # It will however pass ["--foo", <file path>] to the action command line,
  # instead of ["--foo=<file_path>"]
  args.add("--foo", file)

  # Use format if you prefer ["--foo=<file path>"] to ["--foo", <file path>]
  args.add(format="--foo=%s", value=file)

  # Bad, makes a giant string of a whole depset
  args.add(" ".join(["-I%s" % file.short_path for file in files])

  # Good, only stores a reference to the depset
  args.add_all(files, format_each="-I%s", map_each=_to_short_path)

# Function passed to map_each above
def _to_short_path(f):
  return f.short_path

Dữ liệu đầu vào của hành động bắc cầu phải được đặt vào

Khi tạo một hành động bằng ctx.actions.run, đừng quên rằng trường inputs chấp nhận phần phụ thuộc. Sử dụng thuộc tính này bất cứ khi nào dữ liệu đầu vào được thu thập từ các phần phụ thuộc theo cách bắc cầu.

inputs = depset(...)
ctx.actions.run(
  inputs = inputs,  # Do *not* turn inputs into a list
  ...
)

Treo

Nếu Bazel có vẻ bị treo, bạn có thể nhấn Ctrl-\ hoặc gửi tín hiệu SIGQUIT cho Bazel (kill -3 $(bazel info server_pid)) để lấy luồng kết xuất trong tệp $(bazel info output_base)/server/jvm.out.

Vì có thể bạn sẽ không chạy được bazel info nếu bazel bị treo, nên thư mục output_base thường là thư mục mẹ của đường liên kết tượng trưng bazel-<workspace> trong thư mục không gian làm việc.

Phân tích hiệu suất

Theo mặc định, Bazel ghi một hồ sơ JSON vào command.profile.gz trong cơ sở đầu ra. Bạn có thể định cấu hình vị trí bằng cờ --profile, ví dụ: --profile=/tmp/profile.gz. Vị trí kết thúc bằng .gz được nén bằng GZIP.

Để xem kết quả, hãy mở chrome://tracing trong thẻ trình duyệt Chrome, nhấp vào "Tải" và chọn tệp cấu hình (có thể được nén). Để có kết quả chi tiết hơn, hãy nhấp vào các hộp ở góc dưới bên trái.

Bạn có thể sử dụng các chế độ điều khiển sau bằng bàn phím để di chuyển:

  • Nhấn 1 để xem chế độ "chọn". Trong chế độ này, bạn có thể chọn các hộp cụ thể để kiểm tra chi tiết sự kiện (xem ở góc dưới bên trái). Chọn nhiều sự kiện để nhận bản tóm tắt và số liệu thống kê tổng hợp.
  • Nhấn 2 để xem chế độ "kéo". Sau đó, kéo chuột để di chuyển khung hiển thị. Bạn cũng có thể dùng a/d để di chuyển sang trái/phải.
  • Nhấn 3 để dùng chế độ "thu phóng". Sau đó, kéo chuột để thu phóng. Bạn cũng có thể sử dụng w/s để phóng to/thu nhỏ.
  • Nhấn 4 để dùng chế độ "đặt thời gian". Chế độ này có thể đo khoảng cách giữa hai sự kiện.
  • Nhấn ? để tìm hiểu về tất cả các chế độ điều khiển.

Thông tin hồ sơ

Hồ sơ mẫu:

Hồ sơ mẫu

Hình 1. Hồ sơ mẫu.

Có một số hàng đặc biệt:

  • action counters: Hiển thị số lượng hành động đồng thời đang diễn ra. Hãy nhấp vào để xem giá trị thực tế. Sẽ tăng lên giá trị của --jobs trong các bản dựng sạch.
  • cpu counters: Đối với mỗi giây của bản dựng, hãy hiển thị lượng CPU mà Bazel sử dụng (giá trị 1 tương đương với một lõi đang 100% bận).
  • Critical Path: Hiển thị một khối cho mỗi hành động trên đường dẫn quan trọng.
  • grpc-command-1: Luồng chính của Bazel. Hữu ích để có được thông tin tổng quan về những việc Bazel đang làm, ví dụ: "Launch Bazel", "productTargetPatterns" và "runAnalysisGiai.
  • Service Thread: Hiển thị các điểm tạm dừng nhỏ và lớn đối với việc thu gom rác (GC).

Các hàng khác đại diện cho luồng Bazel và hiển thị tất cả sự kiện trong chuỗi đó.

Các vấn đề thường gặp về hiệu suất

Khi phân tích hồ sơ hiệu suất, hãy tìm hiểu những thông tin sau:

  • Giai đoạn phân tích chậm hơn dự kiến (runAnalysisPhase), đặc biệt là trên các bản dựng tăng dần. Đây có thể là dấu hiệu của việc triển khai quy tắc kém, ví dụ: làm phẳng các phần phụ thuộc. Quá trình tải gói có thể bị chậm do số lượng mục tiêu quá mức, macro phức tạp hoặc khối cầu đệ quy.
  • Từng thao tác chậm riêng lẻ, đặc biệt là những thao tác trên lộ trình quan trọng. Bạn có thể chia các hành động lớn thành nhiều hành động nhỏ hơn hoặc giảm tập hợp các phần phụ thuộc (bắc cầu) để tăng tốc độ. Ngoài ra, hãy kiểm tra xem có giá trị cao bất thường nào không phải là PROCESS_TIME (chẳng hạn như REMOTE_SETUP hoặc FETCH).
  • Điểm tắc nghẽn, tức là một số lượng nhỏ luồng bận trong khi tất cả những luồng khác đang rảnh / chờ kết quả (xem khoảng 15 giây đến 30 giây trong ảnh chụp màn hình bên trên). Để tối ưu hoá việc này, rất có thể bạn sẽ phải triển khai các quy tắc hoặc chính Bazel để tạo ra tính song song nhiều hơn. Điều này cũng có thể xảy ra khi có một lượng GC bất thường.

Định dạng tệp cấu hình

Đối tượng cấp cao nhất chứa siêu dữ liệu (otherData) và dữ liệu theo dõi thực tế (traceEvents). Siêu dữ liệu chứa thông tin bổ sung, ví dụ như mã nhận dạng lệnh gọi và ngày thực hiện lệnh gọi Bazel.

Ví dụ:

{
  "otherData": {
    "build_id": "101bff9a-7243-4c1a-8503-9dc6ae4c3b05",
    "date": "Tue Jun 16 08:30:21 CEST 2020",
    "output_base": "/usr/local/google/_bazel_johndoe/573d4be77eaa72b91a3dfaa497bf8cd0"
  },
  "traceEvents": [
    {"name":"thread_name","ph":"M","pid":1,"tid":0,"args":{"name":"Critical Path"}},
    {"cat":"build phase marker","name":"Launch Bazel","ph":"X","ts":-1824000,"dur":1824000,"pid":1,"tid":60},
    ...
    {"cat":"general information","name":"NoSpawnCacheModule.beforeCommand","ph":"X","ts":116461,"dur":419,"pid":1,"tid":60},
    ...
    {"cat":"package creation","name":"src","ph":"X","ts":279844,"dur":15479,"pid":1,"tid":838},
    ...
    {"name":"thread_name","ph":"M","pid":1,"tid":11,"args":{"name":"Service Thread"}},
    {"cat":"gc notification","name":"minor GC","ph":"X","ts":334626,"dur":13000,"pid":1,"tid":11},

    ...
    {"cat":"action processing","name":"Compiling third_party/grpc/src/core/lib/transport/status_conversion.cc","ph":"X","ts":12630845,"dur":136644,"pid":1,"tid":1546}
 ]
}

Dấu thời gian (ts) và thời lượng (dur) trong các sự kiện theo dõi được cung cấp tính bằng micrô giây. Danh mục (cat) là một trong các giá trị enum của ProfilerTask. Xin lưu ý rằng một số sự kiện được hợp nhất với nhau nếu chúng rất ngắn và gần nhau; hãy truyền --noslim_json_profile nếu bạn muốn ngăn việc hợp nhất sự kiện.

Xem thêm Thông số kỹ thuật về định dạng sự kiện của Chrome Trace.

Phân tích - Hồ sơ

Phương thức lập hồ sơ này bao gồm 2 bước, trước tiên, bạn phải thực thi bản dựng/kiểm thử bằng cờ --profile, ví dụ:

$ bazel build --profile=/tmp/prof //path/to:target

Tệp được tạo (trong trường hợp này là /tmp/prof) là một tệp nhị phân có thể được xử lý hậu kỳ và phân tích bằng lệnh analyze-profile:

$ bazel analyze-profile /tmp/prof

Theo mặc định, lệnh này sẽ in thông tin phân tích tóm tắt cho tệp dữ liệu hồ sơ được chỉ định. Dữ liệu này bao gồm số liệu thống kê tích luỹ cho các loại tác vụ khác nhau cho từng giai đoạn xây dựng và bản phân tích đường dẫn quan trọng.

Phần đầu tiên của kết quả mặc định là thông tin tổng quan về thời gian dành cho các giai đoạn xây dựng:

INFO: Profile created on Tue Jun 16 08:59:40 CEST 2020, build ID: 0589419c-738b-4676-a374-18f7bbc7ac23, output base: /home/johndoe/.cache/bazel/_bazel_johndoe/d8eb7a85967b22409442664d380222c0

=== PHASE SUMMARY INFORMATION ===

Total launch phase time         1.070 s   12.95%
Total init phase time           0.299 s    3.62%
Total loading phase time        0.878 s   10.64%
Total analysis phase time       1.319 s   15.98%
Total preparation phase time    0.047 s    0.57%
Total execution phase time      4.629 s   56.05%
Total finish phase time         0.014 s    0.18%
------------------------------------------------
Total run time                  8.260 s  100.00%

Critical path (4.245 s):
       Time Percentage   Description
    8.85 ms    0.21%   _Ccompiler_Udeps for @local_config_cc// compiler_deps
    3.839 s   90.44%   action 'Compiling external/com_google_protobuf/src/google/protobuf/compiler/php/php_generator.cc [for host]'
     270 ms    6.36%   action 'Linking external/com_google_protobuf/protoc [for host]'
    0.25 ms    0.01%   runfiles for @com_google_protobuf// protoc
     126 ms    2.97%   action 'ProtoCompile external/com_google_protobuf/python/google/protobuf/compiler/plugin_pb2.py'
    0.96 ms    0.02%   runfiles for //tools/aquery_differ aquery_differ

Lập hồ sơ bộ nhớ

Bazel đi kèm với một trình phân tích bộ nhớ tích hợp sẵn có thể giúp bạn kiểm tra mức sử dụng bộ nhớ của quy tắc. Nếu có sự cố, bạn có thể kết xuất vùng nhớ khối xếp để tìm chính xác dòng mã gây ra sự cố.

Bật tính năng theo dõi bộ nhớ

Bạn phải chuyển hai cờ khởi động này tới mọi lệnh gọi Bazel:

  STARTUP_FLAGS=\
  --host_jvm_args=-javaagent:$(BAZEL)/third_party/allocation_instrumenter/java-allocation-instrumenter-3.3.0.jar \
  --host_jvm_args=-DRULE_MEMORY_TRACKER=1

Các thao tác này sẽ khởi động máy chủ ở chế độ theo dõi bộ nhớ. Nếu bạn quên những lệnh gọi này, thậm chí chỉ một lệnh gọi Bazel, máy chủ sẽ khởi động lại và bạn sẽ phải bắt đầu lại.

Sử dụng Công cụ theo dõi bộ nhớ

Ví dụ: nhìn vào foo mục tiêu và xem mục tiêu này có chức năng gì. Để chỉ chạy bản phân tích chứ không chạy giai đoạn thực thi bản dựng, hãy thêm cờ --nobuild.

$ bazel $(STARTUP_FLAGS) build --nobuild //foo:foo

Tiếp theo, hãy xem mức tiêu thụ bộ nhớ mà toàn bộ thực thể Bazel tiêu thụ:

$ bazel $(STARTUP_FLAGS) info used-heap-size-after-gc
> 2594MB

Chia nhỏ quy tắc này theo lớp quy tắc bằng cách sử dụng bazel dump --rules:

$ bazel $(STARTUP_FLAGS) dump --rules
>

RULE                                 COUNT     ACTIONS          BYTES         EACH
genrule                             33,762      33,801    291,538,824        8,635
config_setting                      25,374           0     24,897,336          981
filegroup                           25,369      25,369     97,496,272        3,843
cc_library                           5,372      73,235    182,214,456       33,919
proto_library                        4,140     110,409    186,776,864       45,115
android_library                      2,621      36,921    218,504,848       83,366
java_library                         2,371      12,459     38,841,000       16,381
_gen_source                            719       2,157      9,195,312       12,789
_check_proto_library_deps              719         668      1,835,288        2,552
... (more output)

Hãy xem bộ nhớ đang ở đâu bằng cách tạo tệp pprof thông qua bazel dump --skylark_memory:

$ bazel $(STARTUP_FLAGS) dump --skylark_memory=$HOME/prof.gz
> Dumping Starlark heap to: /usr/local/google/home/$USER/prof.gz

Dùng công cụ pprof để kiểm tra vùng nhớ khối xếp. Bạn có thể bắt đầu bằng cách sử dụng pprof -flame $HOME/prof.gz để tạo biểu đồ hình ngọn lửa.

Lấy pprof từ https://github.com/google/pprof.

Nhận tệp kết xuất văn bản gồm các trang web cuộc gọi hấp dẫn nhất với chú thích bằng các dòng:

$ pprof -text -lines $HOME/prof.gz
>
      flat  flat%   sum%        cum   cum%
  146.11MB 19.64% 19.64%   146.11MB 19.64%  android_library <native>:-1
  113.02MB 15.19% 34.83%   113.02MB 15.19%  genrule <native>:-1
   74.11MB  9.96% 44.80%    74.11MB  9.96%  glob <native>:-1
   55.98MB  7.53% 52.32%    55.98MB  7.53%  filegroup <native>:-1
   53.44MB  7.18% 59.51%    53.44MB  7.18%  sh_test <native>:-1
   26.55MB  3.57% 63.07%    26.55MB  3.57%  _generate_foo_files /foo/tc/tc.bzl:491
   26.01MB  3.50% 66.57%    26.01MB  3.50%  _build_foo_impl /foo/build_test.bzl:78
   22.01MB  2.96% 69.53%    22.01MB  2.96%  _build_foo_impl /foo/build_test.bzl:73
   ... (more output)