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ộ bản dựng, các thao tác này có thể dễ dàng chiếm O(N^2) thời gian hoặc không gian. Để tránh điều này, bạn cần phải hiểu 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. 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 phần phụ thuộc
Bất cứ khi nào 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 hoặc lệnh nhập thuần tuý để phát hành thông tin trên máy 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 đây:
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 điều này:
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ỉ trở nên tệ 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 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 về 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ề phần phụ thuộc để biết thêm thông tin.
Tránh gọi 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 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 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ỉ làm phẳng ở mục tiêu cấp cao nhất, chẳng hạn như quy tắc <xx>_binary
, vì khi đó chi phí không được tích luỹ trên từng cấp của biểu đồ bản dựng. Nhưng đây là vẫn 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 chồng chéo. Điều này xảy ra khi 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 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ó lồng ghép 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])
Mã này có thể được thay thế dễ dàng. Trước tiên, hãy thu thập các phần phụ thuộc bắc cầu rồi 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 bớt bằng cách xem toàn bộ danh sách:
x = depset(transitive = [i.deps for i in inputs])
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 trì hoãn việc mở rộng mọi phần phụ thuộc trong giai đoạn thực thi.
Ngoài việc hoàn toàn nhanh hơn, điều này còn làm giảm mức sử dụng bộ nhớ của các quy tắc – đôi khi xuống tới 90% hoặc nhiều hơ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 các phần tử đó. Các danh sách này sẽ được
ctx.actions.args()
mở rộng cho bạn. Nếu bạn cần biến đổi nội dung của phần phụ thuộc, hãy truy cập vào ctx.actions.args#add để xem có nội dung nào 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 các chuỗi này với nhau. Đối số chuỗi tốt nhất là một hằng số vì bộ nhớ của hằng 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 tham số bằngctx.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 tham số, bạn có thể viết tệp đó theo cách thủ công bằngctx.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 bắc cầu 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 một 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 chuyể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 Ctrl-\ hoặc gửi tín hiệu SIGQUIT
cho Bazel (kill -3 $(bazel info server_pid)
) để lấy một tệp 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.
Phân tích hiệu suất
Hồ sơ dấu vết JSON có thể rất hữu ích trong việc nhanh chóng hiểu được Bazel đã dành thời gian gì trong lệnh gọi.
Bạn có thể dùng cờ --experimental_command_profile
để ghi lại nhiều loại hồ sơ Trình ghi chuyến bay trong Java (thời gian CPU, thời gian lưu trữ, mức phân bổ bộ nhớ và tranh chấp khoá).
Bạn có thể dùng cờ --starlark_cpu_profile
để viết hồ sơ pprof về mức sử dụng CPU theo tất cả các luồng Starlark.
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ó 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
Các nút 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, dù 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 hoạt động như thế nào. Để 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 toàn bộ thực thể Bazel tiêu thụ bao nhiêu bộ nhớ:
$ bazel $(STARTUP_FLAGS) info used-heap-size-after-gc
> 2594MB
Hãy phân tích 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í của bộ nhớ 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
Dùng công cụ pprof
để kiểm tra vùng nhớ khối xếp. Bạn nên bắt đầu với 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.
Tải tệp kết xuất văn bản về 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)