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

Báo cáo vấn đề Xem nguồn Hằng đêm · 7,3 · 7.2 · 7.1 · 7 · 6,5

Khi viết 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ộ tạo, các thao tác này có thể dễ dàng mất O(N^2) thời gian hoặc không gian. Để tránh điều này, là rất quan trọng để hiểu cách sử dụng phần phụ thuộc hiệu quả.

Việc này có thể khó thực hiện, 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. Lưu ý: Chi phí viết một quy tắc không hiệu quả có thể chưa rõ ràng cho đến khi quy tắc đó đượ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 của quy tắc, bạn nên sử dụng phần phụ thuộc. Chỉ 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ộ thành 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 xuất bản một chuỗi duy nhất. Với việc tách rời dữ liệu, 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])

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

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

Lưu ý rằng trong trường hợp này, 'a' được đề cập đến 4 lần! Với biểu đồ lớn hơn, thì vấn đề 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 phần tách 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ể xuất bản quy tắc cục bộ bằng cách sử dụng danh sách nếu bạn 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ề bộ lập trình để biết thêm thông tin.

Tránh gọi depset.to_list()

Bạn có thể chuyển đổi phần phụ thuộc thành danh sách phẳng bằng cách sử dụng to_list(), nhưng làm như vậy thường dẫn đến O(N^2) chi phí. Nếu có thể, hãy tránh bất kỳ hoạt động làm phẳng nào của phần tách rời, ngoại trừ hoạt động gỡ lỗi .

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

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

Việc gọi depset trong vòng lặp thường là một lỗi. Điều này có thể dẫn đến phần phụ thuộc với lồng rất sâu, hoạt động 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 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, điều này có thể giảm bớt nếu bạn áp dụng mứ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, điều này còn giảm mức tiêu thụ bộ nhớ của các quy tắc của mình -- đôi khi là 90% trở lên.

Dưới đây là một số thủ thuật:

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

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

  • Tránh tạo chuỗi bằng cách nối các chuỗi lạ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ố đó 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 thông số bằng cách sử dụng ctx.actions.args#use_param_file. Đây là thực hiện trong hậu trường khi hành động đó được thực hiện. Nếu bạn cần xác định rõ ràng kiểm soát tệp thông số, bạn có thể tự viết tệp 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

Đầu vào hành động trung gian phải là phần phụ thuộc

Khi tạo một thao tác bằng ctx.actions.run, đừng tạo một thao tác quên rằng trường inputs chấp nhận phần phụ thuộc. Sử dụng phương thức này bất cứ khi nào thông tin đầ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 Bazel tín hiệu SIGQUIT (kill -3 $(bazel info server_pid)) để nhận luồng kết xuất vào tệp $(bazel info output_base)/server/jvm.out.

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

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

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

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

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

  • Nhấn 1 để "chọn" . Ở 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 để xem bản tóm tắt và số liệu thống kê tổng hợp.
  • Nhấn 2 để "di chuyển" . Sau đó, kéo chuột để di chuyển khung hiển thị. Bạn cũng có thể sử dụng a/d để di chuyển sang trái/phải.
  • Nhấn 3 để "thu phóng" . Sau đó, kéo chuột để thu phóng. Bạn có thể cũng sử dụng w/s để phóng to/thu nhỏ.
  • Nhấn 4 để "đặt thời gian" cho phép bạn đo khoảng cách giữa hai sự kiện.
  • Nhấn ? để tìm hiểu về mọi 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. Nhấp chuột để xem giá trị thực tế. Sẽ tăng lên giá trị của --jobs trong bản dựng sạch.
  • cpu counters: Hiển thị lượng CPU mỗi giây của bản dựng được sử dụng bởi Bazel (giá trị 1 tương đương với một lõi bận 100%).
  • 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 khi có được hình ảnh cấp cao của việc Bazel đang làm, chẳng hạn như "Launch Bazel", "measuringTargetPatterns", và "runAnalysisPhase".
  • Service Thread: Hiển thị các lần tạm dừng thu gom rác (GC) nhỏ và chính.

Các hàng khác đại diện cho các luồng Bazel và hiển thị tất cả sự kiện trên 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 kiếm:

  • Chậm hơn giai đoạn phân tích 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 cho thấy bạn đã triển khai quy tắc kém hiệu quả, ví dụ: làm phẳng phần phân tách. Quá trình tải gói có thể bị chậm do quá nhiều mục tiêu, macro phức tạp hoặc cụm cầu đệ quy.
  • Từng hành động chậm, đặc biệt là những hành động trên lộ trình quan trọng. Có thể là 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ăng tốc độ của chúng. Đồng thời kiểm tra xem có cao không phải PROCESS_TIME (chẳng hạn như REMOTE_SETUP hoặc FETCH).
  • Nút thắt cổ chai, tức là một số ít luồng bận rộn trong khi tất cả các luồng khác không hoạt động / đang chờ kết quả (xem khoảng 15 giây-30 giây trong ảnh chụp màn hình bên trên). Để tối ưu hoá chỉ số này, rất có thể bạn sẽ phải chạm vào các hoạt động triển khai quy tắc hoặc chính Bazel để giới thiệu chủ đề song song 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 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 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",
    "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 cung cấp 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 với với nhau; chuyển --noslim_json_profile nếu bạn muốn ngăn hợp nhất sự kiện.

Xem thêm Thông số kỹ thuật về định dạng sự kiện theo dõi trong Chrome.

analyze-profile

Phương pháp phân tích tài nguyên này bao gồm hai bước. Trước tiên, bạn phải thực thi tạo/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à tệp nhị phân, có thể sau xử lý và phân tích bằng lệnh analyze-profile:

$ bazel analyze-profile /tmp/prof

Theo mặc định, máy in này sẽ in thông tin phân tích tóm tắt cho hồ sơ được chỉ định tệp dữ liệu. Dữ liệu này bao gồm số liệu thống kê tích luỹ cho các loại việc cần làm khác nhau giai đoạn xây dựng và phân tích lộ trình 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 sử dụng trên 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 có thể giúp bạn kiểm tra sử dụng bộ nhớ. Nếu có sự cố, bạn có thể kết xuất vùng nhớ khối xếp để tìm dòng mã chính xác gây ra sự cố.

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

Thao tác này sẽ khởi động máy chủ ở chế độ theo dõi bộ nhớ. Nếu bạn quên các cài đặt này vì 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 foo mục tiêu và xem chức năng của nó. Chỉ chạy bản phân tích và 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ớ của toàn bộ thực thể Bazel:

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

Hãy chia nhỏ quy tắc 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 vị trí bộ nhớ sẽ chuyển đến bằng cách tạo một tệp pprof sử dụ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

Dùng công cụ pprof để điều tra vùng nhớ khối xếp. Một điểm khởi đầu phù hợp là để có được biểu đồ hình ngọn lửa bằng cách sử dụng pprof -flame $HOME/prof.gz.

Tải pprof qua https://github.com/google/pprof.

Nhận kết xuất văn bản 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)