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

Báo cáo sự cố Xem nguồn

Khi viết quy tắc, vấn đề 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 mất O(N^2) thời gian hoặc không gian. Để tránh tình trạng này, bạn cần phải 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 điểm mà bạn có thể đã phạm 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 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 các 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 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 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ề một phương thức 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. Xin lưu ý rằng bạn có thể xuất bản thông tin 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
  )]

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 chi phí O(N^2). Nếu có thể, hãy tránh làm phẳng các phần tách rời, 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ể thoải mái làm phẳng các phần phụ thuộc nếu chỉ thực hiện ở 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ỹ trên từng cấp của biểu đồ bản dựng. Tuy nhiên, đây vẫn là ngoài O(N^2) khi bạn tạo một tập hợp mục tiêu có các phần phụ thuộc chồng chéo. Điều này xảy ra khi bạn tạo bản 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 vòng lặp thường là một lỗi. Điều này có thể dẫn đến việc gỡ bỏ với mức 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ác phần đó 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(). Việc này trì hoãn việc mở rộng bất kỳ phần phụ thuộc nào sang giai đoạn thực thi.

Ngoài việc nhanh hơn, điều này còn giúp giảm mức tiêu thụ bộ nhớ cho các quy tắc – đôi khi tới 90% hoặc hơ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ì tự làm phẳng chúng. 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 bất kỳ phép biến đổi nào đối với nội dung phân tách, hãy truy cập ctx.actions.args#add để xem có phép biến đổi nào phù hợp với dự án này 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ủa tệp, đượ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 mọi 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. Việc này được thực hiện ở hậu trường khi hành động được thực thi. Nếu cần kiểm soát rõ ràng tệp thông số, bạn có thể ghi tệp đó 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

Đầ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 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 dữ liệu đầu vào được thu thập từ các phần phụ thuộc bắc cầu.

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

Treo

Nếu có vẻ như Bazel bị treo, bạn có thể nhấn Ctrl-\ hoặc gửi tín hiệu Bazel SIGQUIT (kill -3 $(bazel info server_pid)) để 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 đường liên kết tượng trưng 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

Hồ sơ theo dõi JSON có thể rất hữu ích trong việc nhanh chóng hiểu được Bazel đã dành thời gian để làm gì trong lệnh gọi.

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 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 truyền hai cờ khởi động này đến mọi lệnh gọi Bazel:

  STARTUP_FLAGS=\
  --host_jvm_args=-javaagent:<path to 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 thông tin này cho dù chỉ với 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)

Hãy 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. Bạn nên bắt đầu bằng cách sử dụng pprof -flame $HOME/prof.gz để tạo biểu đồ ngọn lửa.

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)