بهینه سازی عملکرد

هنگام نوشتن قوانین، رایج ترین دام عملکرد عبور یا کپی کردن داده هایی است که از وابستگی ها انباشته شده اند. هنگامی که در کل ساخت جمع شوند، این عملیات به راحتی می تواند زمان یا فضای O(N^2) را بگیرد. برای جلوگیری از این امر، درک نحوه استفاده موثر از depset بسیار مهم است.

درست کردن این کار می تواند سخت باشد، بنابراین Bazel همچنین یک نمایه کننده حافظه ارائه می دهد که به شما در یافتن نقاطی که ممکن است اشتباه کرده باشید کمک می کند. هشدار داده شود: هزینه نوشتن یک قانون ناکارآمد ممکن است تا زمانی که به طور گسترده مورد استفاده قرار نگیرد، آشکار نباشد.

از depsets استفاده کنید

هر زمان که اطلاعات مربوط به قوانین وابستگی را جمع آوری می کنید، باید از depsets استفاده کنید. فقط از فهرست ها یا دستورات ساده برای انتشار اطلاعات محلی به قانون فعلی استفاده کنید.

یک depset اطلاعات را به عنوان یک نمودار تودرتو نشان می دهد که امکان اشتراک گذاری را فراهم می کند.

نمودار زیر را در نظر بگیرید:

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

هر گره یک رشته را منتشر می کند. با دیپست ها داده ها به این شکل به نظر می رسند:

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) می‌شود. در صورت امکان، از هر گونه مسطح کردن دیپست ها به جز برای اهداف اشکال زدایی خودداری کنید.

یک تصور غلط رایج این است که اگر این کار را فقط در اهداف سطح بالا انجام دهید، مانند یک قانون <xx>_binary ، می توانید آزادانه دپست ها را صاف کنید، زیرا از آن زمان هزینه در هر سطح از نمودار ساخت انباشته نمی شود. اما وقتی مجموعه‌ای از اهداف با وابستگی‌های همپوشانی ایجاد می‌کنید، همچنان O(N^2) است. این اتفاق در هنگام ساختن تست‌های شما //foo/tests/... یا هنگام وارد کردن یک پروژه IDE رخ می‌دهد.

تعداد تماس ها را با 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() استفاده کنید. این گسترش هر گونه depset را به مرحله اجرا موکول می کند.

این کار جدا از اینکه به شدت سریع‌تر است، مصرف حافظه قوانین شما را کاهش می‌دهد - گاهی اوقات تا 90٪ یا بیشتر.

در اینجا چند ترفند وجود دارد:

  • به جای اینکه خودتان آنها را صاف کنید، دپست ها و لیست ها را مستقیماً به عنوان آرگومان ارسال کنید. آنها توسط ctx.actions.args() برای شما گسترش خواهند یافت. اگر به هر گونه تغییری در محتوای depset نیاز دارید، به ctx.actions.args#add نگاه کنید تا ببینید آیا چیزی مناسب است یا خیر.

  • آیا File#path به عنوان آرگومان ارسال می کنید؟ نیازی نیست. هر فایلی به‌طور خودکار به مسیر خود تبدیل می‌شود و به زمان گسترش موکول می‌شود.

  • از ساختن رشته ها با به هم پیوستن آنها خودداری کنید. بهترین آرگومان رشته ای یک ثابت است زیرا حافظه آن بین تمام نمونه های قانون شما به اشتراک گذاشته می شود.

  • اگر args برای خط فرمان خیلی طولانی باشد، یک شی ctx.actions.args() می تواند به صورت مشروط یا بدون قید و شرط در یک فایل param با استفاده از ctx.actions.args#use_param_file شود. این کار در پشت صحنه زمانی که اکشن اجرا می شود انجام می شود. اگر نیاز به کنترل صریح فایل params دارید، می توانید آن را به صورت دستی با استفاده از 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-\ را فشار دهید یا یک سیگنال SIGQUIT برای Bazel ارسال کنید ( kill -3 $(bazel info server_pid) ) تا یک موضوع در فایل $(bazel info output_base)/server/jvm.out .

از آنجایی که اگر bazel آویزان باشد، ممکن است نتوانید bazel info را اجرا کنید، دایرکتوری output_base معمولاً والد bazel-<workspace> در فهرست فضای کاری شما است.

پروفایل عملکرد

Bazel به طور پیش فرض یک نمایه JSON در command.profile.gz در پایه خروجی می نویسد. می توانید مکان را با پرچم --profile پیکربندی کنید، به عنوان مثال --profile=/tmp/profile.gz . مکان هایی که به .gz ختم می شوند با GZIP فشرده می شوند.

برای مشاهده نتایج، chrome://tracing را در برگه مرورگر کروم باز کنید، روی «بارگیری» کلیک کنید و فایل نمایه (احتمالاً فشرده) را انتخاب کنید. برای نتایج دقیق تر، روی کادرهای گوشه پایین سمت چپ کلیک کنید.

برای پیمایش می توانید از این کنترل های صفحه کلید استفاده کنید:

  • 1 را برای حالت "انتخاب" فشار دهید. در این حالت، می توانید کادرهای خاصی را برای بررسی جزئیات رویداد انتخاب کنید (به گوشه پایین سمت چپ مراجعه کنید). چندین رویداد را برای دریافت خلاصه و آمار انبوه انتخاب کنید.
  • 2 را برای حالت "pan" فشار دهید. سپس ماوس را بکشید تا نما حرکت کند. همچنین می توانید از a / d برای حرکت به چپ/راست استفاده کنید.
  • 3 را برای حالت "zoom" فشار دهید. سپس ماوس را بکشید تا زوم کنید. همچنین می توانید از w / s برای بزرگنمایی/کوچکنمایی استفاده کنید.
  • 4 را برای حالت "زمان بندی" فشار دهید، جایی که می توانید فاصله بین دو رویداد را اندازه گیری کنید.
  • فشار ? برای یادگیری در مورد تمام کنترل ها

اطلاعات پروفایل

نمایه نمونه:

Example profile

شکل 1. نمایه نمونه.

چند ردیف خاص وجود دارد:

  • action counters : تعداد اقدامات همزمان در حال پرواز را نشان می دهد. برای دیدن مقدار واقعی روی آن کلیک کنید. باید تا ارزش --jobs در ساخت های تمیز بالا رود.
  • cpu counters : برای هر ثانیه از ساخت، مقدار CPU استفاده شده توسط Bazel را نمایش می‌دهد (مقدار 1 برابر است با 100٪ مشغول بودن یک هسته).
  • Critical Path : یک بلوک را برای هر عمل در مسیر بحرانی نمایش می دهد.
  • grpc-command-1 : موضوع اصلی Bazel. برای دریافت تصویری در سطح بالا از کارهایی که Bazel انجام می دهد مفید است، برای مثال "Launch Bazel"، "evaluateTargetPatterns"، و "runAnalysisPhase".
  • Service Thread : مکث های جزئی و عمده جمع آوری زباله (GC) را نمایش می دهد.

سطرهای دیگر نشان دهنده رشته های Bazel هستند و همه رویدادهای آن رشته را نشان می دهند.

مشکلات رایج عملکرد

هنگام تجزیه و تحلیل پروفایل های عملکرد، به دنبال موارد زیر باشید:

  • مرحله تحلیل کندتر از حد انتظار ( runAnalysisPhase )، به ویژه در ساخت های افزایشی. این می تواند نشانه ای از اجرای ضعیف قانون باشد، به عنوان مثال یکی که depsets را صاف می کند. بارگیری بسته می تواند با مقدار بیش از حد اهداف، ماکروهای پیچیده یا گلوب های بازگشتی کند شود.
  • اقدامات آهسته فردی، به ویژه آنهایی که در مسیر بحرانی هستند. ممکن است بتوان اقدامات بزرگ را به چندین اقدام کوچکتر تقسیم کرد یا مجموعه ای از وابستگی های (گذرا) را برای سرعت بخشیدن به آنها کاهش داد. همچنین بالا بودن غیر معمول غیر PROCESS_TIME (مانند REMOTE_SETUP یا FETCH ) را بررسی کنید.
  • Bottlenecks، یعنی تعداد کمی از رشته ها مشغول هستند در حالی که بقیه در حال بیکاری هستند / منتظر نتیجه هستند (حدود 15-30 ثانیه را در تصویر بالا ببینید). بهینه‌سازی این امر به احتمال زیاد مستلزم لمس پیاده‌سازی قوانین یا خود Bazel برای معرفی موازی‌سازی بیشتر است. این همچنین می تواند زمانی اتفاق بیفتد که مقدار غیرعادی GC وجود داشته باشد.

فرمت فایل پروفایل

شی سطح بالا حاوی ابرداده ( otherData ) و داده ردیابی واقعی ( traceEvents ) است. فراداده حاوی اطلاعات اضافی است، برای مثال شناسه فراخوانی و تاریخ فراخوانی Bazel.

مثال:

{
  "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}
 ]
}

مهرهای زمانی ( ts ) و مدت‌ها ( dur ) در رویدادهای ردیابی در میکروثانیه داده می‌شوند. دسته ( cat ) یکی از مقادیر enum ProfilerTask است. توجه داشته باشید که برخی از رویدادها اگر بسیار کوتاه و نزدیک به یکدیگر باشند با هم ادغام می شوند. اگر می‌خواهید از ادغام رویداد جلوگیری کنید، --noslim_json_profile را پاس کنید.

همچنین به مشخصات قالب رویداد ردیابی Chrome مراجعه کنید.

تجزیه و تحلیل-پروفایل

این روش پروفایل شامل دو مرحله است، ابتدا باید ساخت/تست خود را برای مثال با پرچم --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 است.

pprof از https://github.com/google/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)