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