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

Báo cáo vấn đề Xem nguồn Nightly · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Khi viết quy tắc, sai lầm phổ biến nhất về hiệu suất là duyệt qua 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 mất thời gian hoặc không gian O(N^2). Để tránh điều này, bạn cần phải hiểu cách sử dụng depsets một cách hiệu quả.

Việc này có thể khó thực hiện đúng, 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 ra những vị trí mà bạn có thể đã mắc lỗi. Xin lưu ý: 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 depsets

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

Một depset biểu thị thông tin dưới dạng một 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 xuất bản một chuỗi duy nhất. Với depsets, 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 những thông tin sau:

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 các biểu đồ lớn hơn, vấn đề này sẽ chỉ trở nên tồi tệ hơn.

Dưới đây là ví dụ về cách triển khai quy tắc sử dụng depsets một cách chính xác để xuất bản thông tin bắc cầu. Xin lưu ý rằng bạn có thể xuất bản thông tin theo quy tắc cục bộ 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
  )]

Hãy xem trang tổng quan về depset để biết thêm thông tin.

Tránh gọi depset.to_list()

Bạn có thể ép buộc một depset thành một danh sách phẳng bằng cách sử dụng to_list(), nhưng 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 mọi depsets, 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 depsets 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. Nhưng điều này vẫn là O(N^2) khi bạn tạo một nhóm mục tiêu có các phần phụ thuộc trùng lặp. Điều này xảy ra khi bạn tạo các kiểm thử //foo/tests/... hoặc khi nhập một dự án IDE.

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

Việc gọi depset bên 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 depsets có mức lồng ghép rất sâu, dẫn đến hiệu suất kém. Ví dụ:

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

Bạn có thể dễ dàng thay thế mã này. Trước tiên, hãy thu thập các depsets 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 số lượng này bằng cách sử dụng một danh sách toàn diện:

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

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

Khi tạo dòng lệnh, bạn nên sử dụng ctx.actions.args(). Thao tác này sẽ hoãn việc mở rộng mọi depsets sang giai đoạn thực thi.

Ngoài việc nhanh hơn đáng kể, điều này sẽ làm giảm mức tiêu thụ bộ nhớ của các quy tắc – đôi khi giảm đến 90% hoặc hơn.

Sau đây là một số mẹo:

  • Truyền trực tiếp depsets và danh sách làm đối số, thay vì tự làm phẳng chúng. ctx.actions.args() sẽ mở rộng các tệp này cho bạn. Nếu bạn cần thực hiện bất kỳ hoạt động chuyển đổi nào đối với nội dung depset, hãy xem ctx.actions.args#add để xem có nội dung nào phù hợp hay không.

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

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

  • Nếu các đối số quá dài đối với dòng lệnh, thì đối tượng ctx.actions.args() có thể được ghi có điều kiện hoặc vô điều kiện vào tệp tham số bằng cách sử dụng ctx.actions.args#use_param_file. Việc này được thực hiện ngầm khi thao tác được thực thi. Nếu cần kiểm soát rõ ràng tệp params, bạn có thể tự viết tệp này 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
  # I<t will ho>wever pass ["--foo", file path] to the acti<on comman>d line,
  # instead of ["--foo=file_path"]
  args.add(&qu<ot;--foo&>quot;, file)

  <# Use for>mat 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

Đầu vào của hành động bắc cầu phải là depsets

Khi tạo một thao tác bằng cách sử dụng ctx.actions.run, đừng quên rằng trường inputs chấp nhận một depset. Hãy sử dụng phương thức này bất cứ khi nào các đầu vào được thu thập từ các phần phụ thuộc một cách gián tiếp.

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

Treo

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

Vì bạn có thể 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 symlink bazel-<workspace> trong thư mục không gian làm việc của bạn.

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 một thẻ trình duyệt Chrome, nhấp vào "Load" (Tải) rồi chọn tệp hồ sơ (có thể đã nén). Để xem kết quả chi tiết hơn, hãy nhấp vào các hộp ở góc dưới cùng bên trái.

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

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

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. Nhấp vào đó để xem giá trị thực tế. Phải tăng lên đế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 bận 100%).
  • Critical Path: Hiển thị một khối cho mỗi thao tác trên đường dẫn quan trọng.
  • grpc-command-1: Luồng chính của Bazel. Hữu ích khi bạn muốn biết thông tin tổng quan về những việc mà Bazel đang làm, ví dụ: "Launch Bazel" (Khởi chạy Bazel), "evaluateTargetPatterns" (đánh giá TargetPattern) và "runAnalysisPhase" (chạy giai đoạn phân tích).
  • Service Thread: Hiển thị các lần tạm dừng Thu gom rác (GC) nhỏ và lớn.

Các hàng khác đại diện cho các luồng Bazel và cho thấy tất cả các sự kiện trên luồng đó.

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:

  • 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 gia tăng. Đây có thể là dấu hiệu cho thấy việc triển khai quy tắc không hiệu quả, chẳng hạn như quy tắc làm phẳng depsets. Việc tải gói có thể diễn ra chậm do có quá nhiều mục tiêu, macro phức tạp hoặc glob đệ quy.
  • Các thao tác riêng lẻ có tốc độ chậm, đặc biệt là những thao tác trên đường dẫn quan trọng. Bạn có thể chia các thao tác lớn thành nhiều thao tác 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ó số lượng lớn bất thường về PROCESS_TIME không phải là PROCESS_TIME (chẳng hạn như REMOTE_SETUP hoặc FETCH) hay không.
  • Nút thắt, tức là một số ít luồng đang bận trong khi tất cả các luồng khác đều ở trạng thái rảnh / chờ kết quả (xem khoảng 15 đến 30 giây trong ảnh chụp màn hình ở trên). Việc tối ưu hoá này có thể sẽ yêu cầu bạn điều chỉnh các cách triển khai quy tắc hoặc chính Bazel để tăng tính song song. Đ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 hồ sơ

Đố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, chẳng hạn như mã nhận dạng lệnh gọi và ngày của lệnh gọi Bazel.

Ví dụ:

{
  "otherData": {
    "build_id": "101bff9a-7243-4c1a-8503-9dc6ae4c3b05",
    "date": "Tue Jun 16 08:30:21 CEST 2020",
    "profile_finish_ts": "1677666095162000",
    "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 tính bằng phần triệu 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 sẽ được hợp nhất 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 Quy cách định dạng sự kiện theo dõi của Chrome.

analyze-profile

Phương pháp 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, công cụ 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. Thông tin 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 trong từng giai đoạn xây dựng và phân tích đường dẫn quan trọng.

Phần đầu tiên của đầu ra 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 khác nhau:

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

Phân tích 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ó vấn đề, bạn có thể kết xuất heap để tìm ra chính xác dòng mã gây ra vấn đề.

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

Bạn phải truyền hai cờ khởi động này đến 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 lệnh này khởi động máy chủ ở chế độ theo dõi bộ nhớ. Nếu bạn quên những thông tin này dù chỉ trong 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 Trình theo dõi bộ nhớ

Ví dụ: hãy xem mục tiêu foo và xem mục tiêu này làm gì. Để chỉ chạy quy trình phân tích mà 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 toàn bộ phiên bản Bazel tiêu thụ bao nhiêu bộ nhớ:

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

Chia nhỏ 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)

Xem bộ nhớ đang đi đến đâu bằng cách tạo tệp pprof bằng bazel dump --skylark_memory:

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

Sử dụng công cụ pprof để điều tra heap. Một điểm khởi đầu tốt là lấy biểu đồ ngọn lửa bằng cách sử dụng pprof -flame $HOME/prof.gz.

Tải pprof từ https://github.com/google/pprof.

Nhận một tệp văn bản kết xuất của các trang web cuộc gọi phổ biến nhất được 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)