אופטימיזציה של ביצועים

כשכותבים כללים, הירידה הכי נפוצה בביצועים היא מעבר בין נתונים או העתקה של נתונים שתלויים מתלויים. במצטבר, לאורך כל הבניין הזה, הפעולות האלה עשויות להימשך זמן (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-\ או לשלוח Bazel SIGQUIT את האות (kill -3 $(bazel info server_pid)) כדי ליצור שרשור בקובץ $(bazel info output_base)/server/jvm.out.

יכול להיות שלא תהיה לך אפשרות להפעיל את bazel info אם הקישור תלוי, לכן הספרייה output_base היא בדרך כלל ההורה של הסמל bazel-<workspace> שבספרייה שלך ב-Workspace.

יצירת פרופילים

כברירת מחדל, Bazel כותבת פרופיל JSON ב-command.profile.gz בבסיס הפלט. ניתן להגדיר את המיקום באמצעות הדגל --profile, לדוגמה --profile=/tmp/profile.gz. המיקום שמסתיים ב-.gz דחוס באמצעות GZIP.

כדי לראות את התוצאות, יש לפתוח את chrome://tracing בכרטיסייה של דפדפן Chrome, ללחוץ על "Load" ולבחור את קובץ הפרופיל (שיכול לדחוס) את הפרופיל. לקבלת תוצאות מפורטות יותר, לוחצים על התיבות שבפינה הימנית התחתונה.

אפשר להשתמש בפקדי המקלדת האלה כדי לנווט:

  • צריך להקיש על 1 למצב "select". במצב הזה אפשר לבחור תיבות ספציפיות לבדיקת פרטי האירוע (פרטים בפינה הימנית התחתונה). יש לבחור כמה אירועים כדי לקבל סיכום ונתונים סטטיסטיים נצברים.
  • צריך להקיש על 2 למצב "pan". לאחר מכן גוררים את העכבר כדי להזיז את התצוגה. אפשר גם להשתמש ב-a/d כדי לזוז שמאלה/ימינה.
  • יש להקיש על 3 למצב "Zoom" . לאחר מכן גוררים את העכבר כדי לשנות את מרחק התצוגה. אפשר גם להשתמש ב-w/s כדי להגדיל או להקטין את התצוגה.
  • יש להקיש על 4 למצב "תזמון" כדי למדוד את המרחק בין שני אירועים.
  • יש ללחוץ על ? כדי לקבל מידע על כל הפקדים.

פרטי פרופיל

פרופיל לדוגמה:

פרופיל לדוגמה

איור 1. פרופיל לדוגמה.

יש כמה שורות מיוחדות:

  • action counters: מראה כמה פעולות במקביל מוצגות בטיסה. לוחצים עליו כדי לראות את הערך בפועל. צריך להעלות את הערך של --jobs ב-builds נקיים.
  • cpu counters: עבור כל שנייה של ה-build, מוצגת כמות המעבד (CPU) שנעשה בה שימוש ב-Bazel (הערך 1 שווה ללבה אחת של 100% עומס).
  • Critical Path: יוצג בלוק אחד עבור כל פעולה בנתיב הקריטי.
  • grpc-command-1: השרשור הראשי של Bazel. כדאי להשתמש בתמונה כדי לקבל תמונה ברמה גבוהה לגבי מה ש-Bazel עושה, לדוגמה, "Launch Bazel", "EvaluateTargetpattern&&; , &"runAnalysisPhase".
  • Service Thread: מוצג השהיות של איסוף אשפה גדול וראשי (GC).

שורות אחרות מייצגות את השרשורים של Bazel ומציגות את כל האירועים בשרשור הזה.

בעיות נפוצות הקשורות לביצועים

כשמנתחים פרופילים של ביצועים, מחפשים:

  • שלב איטי יותר מהצפוי (runAnalysisPhase), במיוחד על גרסאות מצטברות. מצב כזה יכול להיות סימן לכך שההטמעה של כלל גרועה, לדוגמה דוגמה לאיחוד של ציבור. טעינת החבילה עשויה להיות איטית באמצעות כמות מוגזמת של יעדים, רכיבי מאקרו מורכבים או כדורים רקורסיביים.
  • פעולות איטיות בודדות, במיוחד אלה בנתיב קריטי. ייתכן ש יתפצלו פעולות גדולות למספר פעולות קטנות יותר או יקטינו את קבוצת התלות (עקירות) כדי לזרז אותן. כמו כן, יש לחפש ערך גבוה במיוחד שאינו PROCESS_TIME (כמו REMOTE_SETUP או FETCH).
  • צווארי בקבוק, מספר קטן של שרשורים עמוסים בזמן שכל האחרים ממתינים וממתינים לתוצאה (ראו צילום מסך בערך 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 Trace.

פרופיל ניתוח

שיטת הפרופיל הזו מורכבת משני שלבים, ראשית עליכם לבצע את ה-build/test באמצעות הסימון --profile, לדוגמה

$ bazel build --profile=/tmp/prof //path/to:target

הקובץ שנוצר (במקרה הזה /tmp/prof) הוא קובץ בינארי, שאותו אפשר לעבד לאחר ניתוח ולנתח אותו באמצעות הפקודה analyze-profile:

$ bazel analyze-profile /tmp/prof

כברירת מחדל, הוא מדפיס סיכום של ניתוח נתונים עבור קובץ הנתונים של הפרופיל שצוין. הנתונים האלה כוללים נתונים סטטיסטיים מצטברים לגבי סוגי משימות שונים לכל שלב ביצירה וניתוח של הנתיב הקריטי.

הקטע הראשון בפלט ברירת המחדל הוא סקירה כללית של הזמן שהושקע בשלבי build השונים:

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 מגיעה עם פרופיל מובנה בזיכרון שיכול לעזור לך לבדוק את השימוש שלך בכלל. אם יש בעיה, תוכלו להשתמש בערימה (heap) כדי למצוא את שורת הקוד המדויקת שגורמת לבעיה.

הפעלת מעקב אחר זיכרון

עליך להעביר את שני התרעות ההפעלה האלה לכל הפעלה של 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 וראו מה הוא עושה. כדי להריץ רק את הניתוח ולא להריץ את שלב הביצוע של ה-build, מוסיפים את הסימון --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)