실적 최적화

문제 신고 소스 보기

규칙을 작성할 때 가장 일반적인 성능 문제는 종속 항목에서 누적된 데이터를 순회하거나 복사하는 것입니다. 이러한 작업은 전체 빌드를 대상으로 집계할 때 쉽게 O(N^2) 시간이나 공간이 걸릴 수 있습니다. 이를 방지하려면 depset을 효과적으로 사용하는 방법을 이해하는 것이 중요합니다.

이 모든 것을 올바르게 수행하기가 어려울 수 있으므로 Bazel은 실수했을 수 있는 곳을 찾도록 도와주는 메모리 프로파일러도 제공합니다. 주의: 비효율적인 규칙을 작성하는 데 드는 비용은 광범위하게 사용될 때까지 분명하게 드러나지 않을 수 있습니다.

depset 사용

규칙 종속 항목에서 정보를 롤업할 때마다 depsets를 사용해야 합니다. 현재 규칙에 로컬인 정보를 게시할 때는 일반 목록 또는 사전만 사용하세요.

depset은 공유를 사용 설정하는 중첩 그래프로 정보를 나타냅니다.

다음 그래프를 살펴보세요.

C -> B -> A
D ---^

각 노드는 하나의 문자열을 게시합니다. depset을 사용하면 데이터는 다음과 같이 표시됩니다.

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'가 4번 언급됩니다. 그래프가 클수록 이 문제는 더 악화될 뿐입니다.

다음은 depset을 올바르게 사용하여 전이 정보를 게시하는 규칙 구현의 예입니다. 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) 비용이 발생합니다. 가능하면 디버깅 목적을 제외하고 depset의 평면화를 피합니다.

일반적으로 <xx>_binary 규칙과 같은 최상위 대상에서만 depset을 평탄화하면 비용이 빌드 그래프의 각 수준에 걸쳐 누적되지 않으므로 평탄화가 가능하다는 것이 가장 일반적인 오해입니다. 그러나 종속 항목이 중복되는 타겟 세트를 빌드하는 경우 여전히 O(N^2)입니다. 이는 //foo/tests/... 테스트를 빌드하거나 IDE 프로젝트를 가져올 때 발생합니다.

depset 통화 수를 줄입니다.

루프 내에서 depset를 호출하는 것은 종종 실수입니다. 매우 깊은 중첩이 있는 depset을 초래할 수 있고, 이는 성능이 떨어질 수 있습니다. 예를 들면 다음과 같습니다.

x = depset()
for i in inputs:
    # Do not do that.
    x = depset(transitive = [x, i.deps])

이 코드는 쉽게 교체할 수 있습니다. 먼저 전이적 depset을 수집하고 모두 한 번에 병합합니다.

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()를 사용해야 합니다. 그러면 종속 항목의 확장이 실행 단계로 지연됩니다.

이렇게 하면 속도가 엄격하게 적용되는 것 외에도 규칙의 메모리 소비를 90% 이상 줄일 수 있습니다.

다음은 몇 가지 유용한 정보입니다.

  • depset과 목록을 평면화하는 대신 인수로 직접 전달합니다. ctx.actions.args() 단위로 확장됩니다. depset 콘텐츠를 변환해야 하는 경우 ctx.actions.args#add를 확인하여 청구서에 맞는 항목이 있는지 확인하세요.

  • File#path를 인수로 전달하나요? 필요하지 않습니다. 모든 파일은 자동으로 파일의 경로로 전환되며 확장 시간에 따라 결정됩니다.

  • 문자열을 함께 연결하여 구성하지 마세요. 최상의 문자열 인수는 메모리가 규칙의 모든 인스턴스 간에 공유되므로 상수입니다.

  • 명령줄에서 인수가 너무 긴 경우 ctx.actions.args#use_param_file를 사용하여 ctx.actions.args() 객체를 조건부 또는 무조건적으로 매개변수 파일에 작성할 수 있습니다. 이 작업은 작업이 실행될 때 백그라운드에서 실행됩니다. 매개변수 파일을 명시적으로 제어해야 하는 경우 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

전환 작업 입력은 depset이어야 합니다.

ctx.actions.run을 사용하여 작업을 빌드할 때는 inputs 필드가 종속 항목을 허용한다는 점을 잊지 마세요. 종속 항목에서 입력이 전이적으로 수집될 때마다 이 메서드를 사용합니다.

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> 심볼릭 링크의 상위 요소입니다.

성능 프로파일링

JSON 트레이스 프로필은 Bazel이 호출 중에 시간을 소비한 부분을 빠르게 파악하는 데 매우 유용할 수 있습니다.

메모리 프로파일링

Bazel은 규칙의 메모리 사용을 확인하는 데 도움이 될 수 있는 메모리 프로파일러가 내장되어 있습니다. 문제가 있는 경우 힙을 덤프하여 문제를 일으키는 정확한 코드 줄을 찾을 수 있습니다.

메모리 추적 사용 설정

다음 두 시작 플래그를 모든 Bazel 호출에 전달해야 합니다.

  STARTUP_FLAGS=\
  --host_jvm_args=-javaagent:<path to java-allocation-instrumenter-3.3.0.jar> \
  --host_jvm_args=-DRULE_MEMORY_TRACKER=1

메모리 추적 모드에서 서버를 시작합니다. Bazel 호출 한 번이라도 이를 잊어버리면 서버가 다시 시작되므로 다시 시작해야 합니다.

Memory Tracker 사용

예를 들어 타겟 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)

bazel dump --skylark_memorypprof 파일을 생성하여 메모리의 위치를 확인합니다.

$ 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)