تحسين الأداء

عند كتابة القواعد، تقع مشكلة الأداء الأكثر شيوعًا في اجتياز البيانات التي يتم تجميعها من التبعيات أو نسخها. وعند التجميع عند المبنى بأكمله، يمكن أن تستغرق هذه العمليات وقتًا أو مساحة عمل O(N^2) بسهولة. ولتجنب ذلك، من الضروري فهم كيفية استخدام نقاط النهاية بشكل فعّال.

قد يكون ذلك بصعب، لذلك تقدّم Bazel أداة تحليل الذاكرة التي تساعدك في العثور على المواقع التي قد تكون ارتكبت فيها خطأ. تحذير: قد لا تكون تكلفة كتابة قاعدة غير فعّالة واضحة حتى يتم استخدامها على نطاق واسع.

استخدام نقاط النهاية

عند استخدام المعلومات المرتبطة باعتماديات القواعد، عليك استخدام نقاط. لا تستخدم سوى القوائم العادية أو الإملاء لنشر المعلومات المحلية على القاعدة الحالية.

تمثّل تفاصيل المعلومات المعلومات كرسم بياني مدمج يتيح المشاركة.

جرِّب استخدام الرسم البياني التالي:

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' أربع مرات. وباستخدام الرسوم البيانية الأكبر، ستزداد هذه المشكلة سوءًا.

في ما يلي مثال على تنفيذ قاعدة تستخدم عمليات تصحيح الأخطاء بشكل صحيح لنشر المعلومات المباشرة. يُرجى العِلم أنه لا بأس من نشر معلومات القواعد المحلية باستخدام القوائم إذا أردت ذلك وليس 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.to_list().

يمكنك فرض حذف جزء من قائمة مسطّحة باستخدام to_list()، ولكن عادةً ما يؤدي ذلك إلى تكلفة 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])

يمكن استبدال هذا الرمز بسهولة. أولاً، اجمع الخطوات الانتقالية وادمجها كلها في الوقت نفسه:

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% أو أكثر.

فيما يلي بعض الحيل:

  • انتقل إلى القوائم والقوائم مباشرةً كوسيطات، بدلاً من دمجها بنفسك. وسيتم توسيعها بنسبة ctx.actions.args() لك. إذا كنت بحاجة إلى أي إحالات ناجحة في محتوى التفاصيل، يمكنك الاطّلاع على ctx.actions.args#add لمعرفة ما إذا كان هناك أي شيء يناسب الفاتورة.

  • هل تضبط File#path كوسيطات؟ لا حاجة لذلك. يتم تحويل أي ملف تلقائيًا إلى مساره، ويتم تأجيله إلى وقت التوسيع.

  • تجنَّب إنشاء سلاسل من خلال ربطها معًا. أفضل وسيطة للسلسلة هي ثابتة حيث ستتم مشاركة ذاكرتها بين جميع مثيلات القاعدة.

  • إذا كانت الوسيطات طويلة جدًا لسطر الأوامر، يمكن كتابة الكائن ctx.actions.args() بشكل مشروط أو غير مشروط إلى ملف معلمة باستخدام ctx.actions.args#use_param_file. ويتم تنفيذ هذا الإجراء خلف الكواليس عندما يتم تنفيذ الإجراء. إذا كنت تحتاج إلى التحكّم بشكل صريح في ملف المعلمات، يمكنك كتابته يدويًا باستخدام 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

يجب أن يتم إرسال ملاحظات بشأن الإجراء الانتقالي.

عند إنشاء إجراء باستخدام ctx.actions.run، لا تنسَ أن الحقل inputs يقبل قيمة معيّنة. واستخدِم هذه الأداة عندما يتم جمع المدخلات من الاعتماديات بشكل مؤقت.

inputs = depset(...)
ctx.actions.run(
  inputs = inputs,  # Do *not* turn inputs into a list
  ...
)

مُعلّقة

إذا بدا أن برنامج Bazel مُعلَّق، يمكنك النقر على المفتاح Ctrl-\ أو إرسال إشارة SIGQUIT للمؤشر (kill -3 $(bazel info server_pid)) للحصول على سلسلة محادثات في الملف $(bazel info output_base)/server/jvm.out.

وبما أنك قد لا تتمكّن من تشغيل bazel info في حال تعليق البازار، عادةً ما يكون الدليل output_base هو أحد أصل الرمز bazel-<workspace> في دليل مساحة العمل.

تحديد مواصفات الأداء

يكتب Bazel ملف JSON شخصيًا إلى command.profile.gz في قاعدة الإخراج تلقائيًا. يمكنك ضبط الموقع الجغرافي باستخدام العلامة --profile، مثل --profile=/tmp/profile.gz. يتم ضغط الموقع الذي ينتهي بـ .gz باستخدام GZIP.

للاطّلاع على النتائج، افتح chrome://tracing في علامة تبويب لمتصفّح Chrome، وانقر على "Load";ثم اختيار ملف الملف الشخصي (الذي يُحتمل أن يكون مضغوطًا). للحصول على نتائج أكثر تفصيلاً، انقر على المربعات في أسفل يمين الشاشة.

يمكنك استخدام عناصر التحكّم في لوحة المفاتيح للتنقّل:

  • اضغط على 1 لاختيار الوضع "select". في هذا الوضع، يمكنك اختيار مربعات معينة لفحص تفاصيل الحدث (انظر أسفل اليمين). اختَر أحداث متعددة للحصول على ملخّص وإحصاءات مجمّعة.
  • اضغط على 2 للانتقال إلى وضع &اقتباس؛ ثم اسحب الماوس لتحريك العرض. يمكنك أيضًا استخدام a/d للتحرك إلى اليسار/اليمين.
  • اضغط على 3 للانتقال إلى وضع الاحتضان. ثم اسحب الماوس للتكبير أو التصغير. يمكنك أيضًا استخدام w/s للتكبير/التصغير.
  • اضغط على 4 للدخول إلى وضع "&&بدء"&quot؛ حيث يمكنك قياس المسافة بين الحدثين.
  • اضغط على ? للتعرف على جميع عناصر التحكم.

معلومات الملف الشخصي

مثال على ملف شخصي:

مثال على ملف شخصي

الشكل 1. مثال على ملف شخصي.

هناك بعض الصفوف الخاصة:

  • action counters: لعرض عدد الإجراءات المتزامنة في رحلة طيران. انقر عليها لعرض القيمة الفعلية. ينبغي أن يرتفع قيمة --jobs إلى تصميمات نظيفة.
  • cpu counters: لكل ثانية من التصميم، يتم عرض مقدار وحدة المعالجة المركزية (CPU) التي تستخدمها Bazel (القيمة 1 تساوي نواة واحدة مشغولة بنسبة 100%).
  • Critical Path: يعرض جزءًا واحدًا لكل إجراء في المسار الحرج.
  • grpc-command-1: سلسلة محادثات Bazel' الرئيسية. يفيد ذلك في الحصول على صورة عالية المستوى حول أداء Bazel، مثل "Launch & Bazel","assessTargetPatterns",&&&;;AnAnlysisStage".
  • Service Thread: يتم عرض عمليات إيقاف جمع القمامة (GC) الصغيرة والكبيرة مؤقتًا.

وتمثّل الصفوف الأخرى سلاسل محادثات Bazel وتعرض جميع الأحداث في سلسلة المحادثات هذه.

مشاكل الأداء الشائعة

عند تحليل الملفات الشخصية للأداء، ابحث عن:

  • مرحلة التحليل أبطأ من المتوقّعة (runAnalysisPhase)، لا سيما بالنسبة إلى الإصدارات المتزايدة. وقد يشير ذلك إلى ضعف تنفيذ القاعدة، على سبيل المثال، طبق مسطّح. ويمكن أن يكون تحميل الحزمة بطيئًا جدًا بعدد كبير من الاستهدافات أو وحدات الماكرو المعقّدة أو الكرات المتكررة.
  • الإجراءات البطيئة الفردية، خاصةً في المسار الحرج. قد يكون من الممكن تقسيم الإجراءات الكبيرة إلى إجراءات متعددة أصغر أو تقليل مجموعة التبعيات (العابرة) لتسريعها. وتحقّق أيضًا من أنه ليس هناك PROCESS_TIME غير معتاد (مثل REMOTE_SETUP أو FETCH).
  • المؤثر السلبي نفسه، وهو عدد قليل من سلاسل المحادثات في حين أن جميع الأجهزة الأخرى تتلاءم أو تنتظر النتيجة (يُرجى الاطِّلاع على لقطة الشاشة من 15 إلى 30 ثانية في لقطة الشاشة أعلاه). من المرجح أن يتطلب تحسين ذلك لمس تنفيذ القواعد أو Bazel نفسها لتقديم المزيد من التوازي. ويمكن أن يحدث هذا أيضًا عندما تكون هناك نسبة كبيرة من البيانات المهملة.

تنسيق ملف الملف الشخصي

يحتوي كائن المستوى الأعلى على البيانات الوصفية (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) هي إحدى قيم تعداد 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 $(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)