규칙을 작성할 때 가장 일반적인 성능 문제점은 종속 항목에서 누적된 데이터를 순회하거나 복사하는 것입니다. 전체 빌드에서 집계하면 이러한 작업은 O(N^2) 시간 또는 공간을 쉽게 차지할 수 있습니다. 이를 방지하려면 depsets를 효과적으로 사용하는 방법을 이해하는 것이 중요합니다.
이 작업은 올바르게 수행하기 어려울 수 있으므로 Bazel은 실수를 했을 수 있는 지점을 찾는 데 도움이 되는 메모리 프로파일러도 제공합니다. 주의: 비효율적인 규칙을 작성하는 데 드는 비용은 널리 사용될 때까지 명확하지 않을 수 있습니다.
depsets 사용
규칙 종속 항목에서 정보를 롤업할 때는 항상 depsets를 사용해야 합니다. 일반 목록 또는 dicts만 사용하여 현재 규칙에 로컬 정보 를 게시합니다.
depset은 정보를 공유할 수 있는 중첩 그래프로 나타냅니다.
다음 그래프를 살펴보세요.
C -> B -> A
D ---^
각 노드는 단일 문자열을 게시합니다. depsets를 사용하면 데이터가 다음과 같이 표시됩니다.
a = depset(direct=['a'])
b = depset(direct=['b'], transitive=[a])
c = depset(direct=['c'], transitive=[b])
d = depset(direct=['d'], transitive=[b])
각 항목은 한 번만 언급됩니다. 목록을 사용하면 다음과 같이 표시됩니다.
a = ['a']
b = ['b', 'a']
c = ['c', 'b', 'a']
d = ['d', 'b', 'a']
이 경우 'a'가 네 번 언급됩니다. 그래프가 클수록 이
문제는 더 악화됩니다.
다음은 depsets를 올바르게 사용하여 전이 정보를 게시하는 규칙 구현의 예입니다. 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
)]
자세한 내용은 depset 개요 페이지를 참고하세요.
depset.to_list() 호출 방지
`
to_list()`를 사용하여 depset을 평면 목록으로 강제 변환할 수 있지만 이렇게 하면 일반적으로 O(N^2)
비용이 발생합니다. 가능한 경우 디버깅
목적을 제외하고는 depsets를 평면화하지 마세요.
일반적인 오해는
규칙과 같은 최상위 타겟에서만 평면화하면 빌드 그래프의 각 수준에서 비용이 누적되지 않으므로 depsets를 자유롭게 평면화할 수 있다는 것입니다.<xx>_binary 하지만 종속 항목이 겹치는 타겟 집합을 빌드할 때는 여전히 O(N^2)입니다. 이는 테스트 //foo/tests/...를 빌드하거나 IDE 프로젝트를 가져올 때 발생합니다.
depset 호출 수 줄이기
루프 내에서 depset을 호출하는 것은 실수인 경우가 많습니다. 성능이 저하되는
매우 깊은 중첩이 있는 depsets로 이어질 수 있습니다. 예를 들면 다음과 같습니다.
x = depset()
for i in inputs:
# Do not do that.
x = depset(transitive = [x, i.deps])
이 코드는 쉽게 대체할 수 있습니다. 먼저 전이 depsets를 수집하고 한 번에 모두 병합합니다.
transitive = []
for i in inputs:
transitive.append(i.deps)
x = depset(transitive = transitive)
목록 이해를 사용하여 줄일 수 있는 경우도 있습니다.
x = depset(transitive = [i.deps for i in inputs])
명령줄에 ctx.actions.args() 사용
명령줄을 빌드할 때는 ctx.actions.args()를 사용해야 합니다. 이렇게 하면 depsets의 확장이 실행 단계로 지연됩니다.
엄격하게 더 빠른 것 외에도 이렇게 하면 규칙의 메모리 소비가 줄어듭니다(경우에 따라 90% 이상).
다음은 몇 가지 팁입니다.
depsets 및 목록을 직접 평면화하는 대신 인수로 직접 전달합니다.
ctx.actions.args()에 의해 확장됩니다. depset 콘텐츠에 변환이 필요한 경우 ctx.actions.args#add를 살펴보고 적합한 항목이 있는지 확인하세요.File#path를 인수로 전달하고 있나요? 그럴 필요가 없습니다. 모든 파일은 자동으로 경로로 변환되어 확장 시간으로 지연됩니다.문자열을 연결하여 생성하지 마세요. 최고의 문자열 인수는 상수이며 메모리는 규칙의 모든 인스턴스 간에 공유됩니다.
명령줄에 인수가 너무 긴 경우
ctx.actions.args()객체 를 매개변수 파일에 조건부 또는 무조건적으로 쓸 수 있습니다.ctx.actions.args#use_param_file작업이 실행될 때 백그라운드에서 실행됩니다. 매개변수 파일을 명시적으로 제어해야 하는 경우 `ctx.actions.write`를 사용하여 수동으로 작성할 수 있습니다.ctx.actions.write
예:
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
전이 작업 입력은 depsets여야 함
ctx.actions.run을 사용하여 작업을 빌드할 때는
inputs 필드가 depset을 허용한다는 점을 잊지 마세요. 입력이 종속 항목에서 전이적으로 수집될 때마다 이 방법을 사용합니다.
inputs = depset(...)
ctx.actions.run(
inputs = inputs, # Do *not* turn inputs into a list
...
)
걸이식
Bazel이 중단된 것으로 보이면 Ctrl-\를 누르거나
Bazel에 SIGQUIT 신호 (kill -3 $(bazel info server_pid))를 보내 스레드
덤프를 $(bazel info output_base)/server/jvm.out 파일에서 가져올 수 있습니다.
Bazel이 중단된 경우 bazel info을 실행할 수 없으므로
output_base 디렉터리는 일반적으로 작업공간 디렉터리의 bazel-<workspace>
심볼릭 링크의 상위 디렉터리입니다.
성능 프로파일링
Bazel은 기본적으로 출력 기본의 command.profile.gz에 JSON 프로필을 씁니다.
--profile 플래그(예:
--profile=/tmp/profile.gz)로 위치를 구성할 수 있습니다. .gz로 끝나는 위치는
GZIP으로 압축됩니다.
결과를 보려면 Chrome 브라우저 탭에서 chrome://tracing을 열고
'로드'를 클릭한 후 (압축된) 프로필 파일을 선택합니다. 자세한
결과를 보려면 왼쪽 하단에 있는 상자를 클릭합니다.
다음 키보드 컨트롤을 사용하여 탐색할 수 있습니다.
- '선택' 모드의 경우
1을 누릅니다. 이 모드에서는 특정 상자를 선택하여 이벤트 세부정보를 검사할 수 있습니다 (왼쪽 하단 참고). 여러 이벤트를 선택하여 요약 및 집계된 통계를 가져옵니다. - '이동' 모드의 경우
2를 누릅니다. 그런 다음 마우스를 드래그하여 뷰를 이동합니다. 를 사용하여 왼쪽/오른쪽으로 이동할 수도 있습니다.ad - '확대/축소' 모드의 경우
3을 누릅니다. 그런 다음 마우스를 드래그하여 확대/축소합니다.w/s를 사용하여 확대/축소할 수도 있습니다. - 두 이벤트 간의 거리를 측정할 수 있는 '타이밍' 모드의 경우
4를 누릅니다. - 모든 컨트롤에 관해 알아보려면
?을 누릅니다.
프로필 정보
프로필 예:

그림 1. 프로필 예
다음과 같은 특수 행이 있습니다.
action counters: 진행 중인 동시 작업 수를 표시합니다. 실제 값을 보려면 클릭 합니다. 클린 빌드에서--jobs값까지 올라가야 합니다.cpu counters: 빌드의 각 초에 대해 Bazel에서 사용하는 CPU 양을 표시합니다 (값 1은 하나의 코어가 100% 사용 중임을 의미).Critical Path: 핵심 경로의 각 작업에 대해 하나의 블록을 표시합니다.grpc-command-1: Bazel의 기본 스레드입니다. 'Bazel 실행', 'evaluateTargetPatterns', 'runAnalysisPhase'와 같이 Bazel이 실행 중인 작업을 대략적으로 파악하는 데 유용합니다.Service Thread: 부 가비지 컬렉션 (GC) 일시중지를 표시합니다.
다른 행은 Bazel 스레드를 나타내고 해당 스레드의 모든 이벤트를 표시합니다.
일반적인 성능 문제
성능 프로필을 분석할 때는 다음을 확인하세요.
- 예상보다 느린 분석 단계 (
runAnalysisPhase), 특히 증분 빌드에서. 이는 depsets를 평면화하는 것과 같이 규칙 구현이 잘못되었음을 나타낼 수 있습니다. 패키지 로드는 과도한 타겟 수, 복잡한 매크로 또는 재귀적 글로브로 인해 느릴 수 있습니다. - 개별 느린 작업, 특히 핵심 경로의 작업. 대규모 작업을 여러 개의 작은 작업으로 분할하거나 (전이) 종속 항목 집합을 줄여 속도를 높일 수 있습니다. 또한 비정상적으로
높은 비
PROCESS_TIME(예:REMOTE_SETUP또는FETCH)이 있는지 확인합니다. - 병목 현상, 즉 다른 모든 스레드가 유휴 상태이거나 결과를 기다리는 동안 소수의 스레드가 사용 중입니다 (위 스크린샷의 15~30초 참고). 이를 최적화하려면 규칙 구현 또는 병렬 처리를 도입하기 위해 Bazel 자체를 수정해야 할 가능성이 높습니다. GC가 비정상적으로 많은 경우에도 발생할 수 있습니다.
프로필 파일 형식
최상위 객체에는 메타데이터 (otherData)와 실제 추적 데이터
(traceEvents)가 포함되어 있습니다. 메타데이터에는 Bazel 호출의 호출 ID
및 날짜와 같은 추가 정보가 포함되어 있습니다.
예:
{
"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}
]
}
추적 이벤트의 타임스탬프 (ts)와 기간 (dur)은
마이크로초 단위로 제공됩니다. 카테고리 (cat)는 ProfilerTask의 열거형 값 중 하나입니다.
일부 이벤트는 매우 짧고 서로 가까운 경우 병합됩니다. 이벤트 병합을 방지하려면 --noslim_json_profile을 전달하세요.
analyze-profile
이 프로파일링 방법은 두 단계로 구성됩니다. 먼저
빌드/테스트를 --profile 플래그를 사용하여 실행해야 합니다. 예를 들면 다음과 같습니다.
$ bazel build --profile=/tmp/prof //path/to:target
생성된 파일 (이 경우 /tmp/prof)은
후처리 및 분석할 수 있는 analyze-profile 명령어로 바이너리 파일입니다.
$ bazel analyze-profile /tmp/prof
기본적으로 지정된 프로필 데이터 파일의 요약 분석 정보를 출력합니다. 여기에는 각 빌드 단계의 다양한 작업 유형에 대한 누적 통계와 핵심 경로 분석이 포함됩니다.
기본 출력의 첫 번째 섹션은 다양한 빌드 단계에 소요된 시간의 개요입니다.
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
메모리 프로파일링
Bazel에는 규칙의 메모리 사용량을 확인하는 데 도움이 되는 기본 제공 메모리 프로파일러가 함께 제공됩니다. 문제가 있는 경우 힙을 덤프하여 문제를 일으키는 정확한 코드 줄을 찾을 수 있습니다.
메모리 추적 사용 설정
_모든_ 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
이렇게 하면 메모리 추적 모드에서 서버가 시작됩니다. Bazel 호출을 한 번이라도 잊으면 서버가 다시 시작되고 처음부터 다시 시작해야 합니다.
메모리 추적기 사용
예를 들어 타겟 foo를 살펴보고 어떤 작업을 실행하는지 확인합니다. 빌드 실행 단계를 실행하지 않고 분석만 실행하려면
플래그를 추가합니다.--nobuild
$ bazel $(STARTUP_FLAGS) build --nobuild //foo:foo
다음으로 전체 Bazel 인스턴스가 사용하는 메모리 양을 확인합니다.
$ bazel $(STARTUP_FLAGS) info used-heap-size-after-gc
> 2594MB
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)
pprof 파일을 사용하여 메모리가 사용되는 위치를 확인합니다.bazel dump --skylark_memory
$ bazel $(STARTUP_FLAGS) dump --skylark_memory=$HOME/prof.gz
> Dumping Starlark heap to: /usr/local/google/home/$USER/prof.gz
pprof 도구를 사용하여 힙을 조사합니다. 좋은 시작점은
pprof -flame $HOME/prof.gz를 사용하여 Flame 그래프를 가져오는 것입니다.
https://github.com/google/pprof에서 pprof를 가져옵니다.
줄로 주석 처리된 가장 인기 있는 호출 사이트의 텍스트 덤프를 가져옵니다.
$ 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)