Khi viết quy tắc, cạm bẫy hiệu suất phổ biến nhất là duyệt qua hoặc sao chép dữ liệu 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 hiểu cách sử dụng depset một cách hiệu quả.
Việc này có thể khó thực hiện đúng cách, vì vậy, Bazel cũng cung cấp một trình phân tích bộ nhớ giúp bạn tìm ra những điểm có thể đã xảy ra 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 depset
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 phát hành một chuỗi duy nhất. Với depset, 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 kết quả như sau:
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 bốn lần! Với các 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 depset một cách chính xác để phát hành thông tin bắc cầu. Xin lưu ý rằng bạn có thể phát hành thông tin cục bộ theo 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ề bộ lập trình để 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 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 các nhóm 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 nhóm phần phụ thuộc nếu 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ỹ trên từng cấp của biểu đồ bản dựng. Tuy nhiên, đây 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 trùng lặp. Điều này xảy ra khi bạn tạo //foo/tests/...
kiểm thử hoặc khi nhập dự án IDE.
Giảm số lượng lệnh gọi đến 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 các depset có
nội dung lồng nhau rất sâu, khiế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ể thay thế mã này một cách dễ dàng. 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, bạn có thể giảm thiểu việc này bằng cách sử dụng cú pháp liệt kê:
x = depset(transitive = [i.deps for i in inputs])
Sử dụng ctx.actions.args() cho 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 đáng kể, việc này còn giúp giảm mức sử dụng bộ nhớ của các quy tắc – đôi khi giảm tới 90% trở lên.
Sau đây là một số mẹo:
Truyền trực tiếp các nhóm phần phụ thuộc và danh sách dưới dạng đối số, thay vì tự làm phẳng các nhóm phần phụ thuộc và danh sách đó. Các giá trị này sẽ được mở rộng theo
ctx.actions.args()
cho bạn. Nếu bạn cần bất kỳ phép biến đổi nào trên nội dung depset, hãy xem ctx.actions.args#add để xem có gì phù hợp không.Bạn có đang truyền
File#path
làm đố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 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 args 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 không có điều kiện vào tệp tham số bằng cách sử dụ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 params, bạn có thể ghi 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 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 một cách 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.
Phân tích hiệu suất
Theo mặc định, Bazel sẽ ghi 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í có đuôi .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" rồi chọn tệp hồ sơ (có thể được nén). Để biết thêm kết quả chi tiết, 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 nút đ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ế độ "di chuyển". Sau đó, hãy kéo chuột để di chuyển thành phần hiển thị. Bạn cũng có thể sử dụnga
/d
để di chuyển sang trái/phải. - Nhấn
3
để chuyển sang chế độ "thu phóng". Sau đó, hãy kéo chuột để thu phóng. Bạn cũng có thể dùngw
/s
để phóng to/thu nhỏ. - Nhấn
4
để chuyển sang chế độ "timing" (đo thời gian), trong đó bạn 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ình 1. Hồ sơ mẫu.
Có một số hàng đặc biệt:
action counters
: Cho thấy số lượng hành động đồng thời đang diễn ra. Nhấp vào giá trị đó để xem giá trị thực tế. Phải tăng lên giá trị--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, 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 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 gì Bazel đang làm, ví dụ: "Launch Bazel" (Khởi chạy Bazel), "evaluateTargetPatterns" (đánh giá TargetPatterns) và "runAnalysisPhase" (chạy AnalysisPhase).Service Thread
: Hiển thị các điểm tạm dừng thu gom rác (GC) nhỏ và lớn.
Các hàng khác đại diện cho luồng Bazel và hiển thị tất 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:
- 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 gia tăng. Đây có thể là dấu hiệu cho thấy bạn đã triển khai quy tắc kém hiệu quả, chẳng hạn như quy tắc làm phẳng phần phân tách. Quá nhiều mục tiêu, macro phức tạp hoặc glob đệ quy có thể khiến quá trình tải gói bị chậm. - Từng hành động chậm, đặc biệt là những hành động trên lộ trình quan trọng. Bạn có thể phân tách các thao tác lớn thành nhiều thao tác nhỏ hơn hoặc giảm bộ phần phụ thuộc (chuyển đổi) để tăng tốc các thao tác đó. Ngoài ra, hãy kiểm tra xem có giá trị cao bất thường không phải
PROCESS_TIME
(chẳng hạn nhưREMOTE_SETUP
hoặcFETCH
) hay không. - Điểm tắc nghẽn, tức là một số ít luồng đang bận trong khi tất cả các luồng khác đang ở trạng thái rảnh / chờ kết quả (xem khoảng 15 giây đến 30 giây trong ảnh chụp màn hình ở trên). Để tối ưu hoá điều 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 để 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êm thông tin, chẳng hạn như mã gọi và ngày 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 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 sẽ đượ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 chặ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 theo dõi của Chrome.
analyze-profile
Phương thức phân tích tài nguyên 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ý sau 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ơ đã 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 trong 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
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ó vấn đề, bạn có thể kết xuất vùng nhớ khối xếp để tìm dòng mã chính xác đang 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
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ỉ 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 quy trình 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 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
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)
Xem bộ nhớ đang đi đâ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 vùng nhớ khối xếp. Bạn có thể bắt đầu bằng cách tạo biểu đồ hình ngọn lửa bằng pprof -flame $HOME/prof.gz
.
Tải pprof
qua https://github.com/google/pprof.
Nhận tệp kết xuất văn bản của các vị trí 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)