BazelCon 2022 findet vom 16. bis 17. November in New York und online statt.
Jetzt anmelden

Leistung optimieren

Mit Sammlungen den Überblick behalten Sie können Inhalte basierend auf Ihren Einstellungen speichern und kategorisieren.

Der häufigste Fehler beim Schreiben von Regeln ist das Durchqueren oder Kopieren von Daten, die sich aus Abhängigkeiten ergeben. Wenn diese Vorgänge über den gesamten Build aggregiert werden, können diese Vorgänge schnell O(N^2)-Zeit oder Platz in Anspruch nehmen. Um dies zu vermeiden, ist es wichtig, zu verstehen, wie Depsets effektiv sind.

Dieser Vorgang kann kompliziert sein. Deshalb bietet Istio auch einen Speicherprofiler, der Sie beim Finden von Fehlern unterstützt. Achtung: Die Kosten für eine ineffiziente Regel werden möglicherweise erst klar, wenn sie weit verbreitet sind.

Desetsets verwenden

Wenn Sie Informationen aus Regelabhängigkeiten zusammenführen, sollten Sie depsets verwenden. Verwenden Sie nur einfache Listen oder Wörterbücher, um Informationen lokal für die aktuelle Regel zu veröffentlichen.

Ein Depset stellt Informationen als verschachteltes Diagramm dar, das die Freigabe ermöglicht.

Sehen Sie sich dazu folgende Grafik an:

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

Jeder Knoten veröffentlicht einen einzelnen String. Mit diesem Parameter sehen die Daten so aus:

a = depset(direct=['a'])
b = depset(direct=['b'], transitive=[a])
c = depset(direct=['c'], transitive=[b])
d = depset(direct=['d'], transitive=[b])

Beachten Sie, dass jeder Artikel nur einmal erwähnt wird. Bei Listen würden Sie Folgendes sehen:

a = ['a']
b = ['b', 'a']
c = ['c', 'b', 'a']
d = ['d', 'b', 'a']

In diesem Fall wird 'a' viermal erwähnt. Bei größeren Grafiken wird dieses Problem aber noch schlimmer.

Im Folgenden finden Sie ein Beispiel für eine Regelimplementierung, bei der Depsets korrekt verwendet werden, um vorübergehende Informationen zu veröffentlichen. Wenn Sie möchten, können Sie lokale Regelinformationen mithilfe von Listen veröffentlichen, da dies nicht O(N^2) ist.

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

Weitere Informationen finden Sie auf der Seite Depset Overview (Übersicht).

depset.to_list() nicht anrufen

Mit to_list() können Sie eine Dezentierung in eine einfache Liste erzwingen, aber das führt in der Regel zu O(N^2)-Kosten. Vermeiden Sie nach Möglichkeit eine Flatten der Debitsets mit Ausnahme von Debugging-Zwecken.

Es ist eine weitverbreitete Missverständnis, dass sich Depsets auch einfach flach erstellen lassen, wenn sie nur auf Zielen der obersten Ebene ausgeführt werden, beispielsweise einer <xx>_binary-Regel. Dies ist jedoch noch O(N^2), wenn Sie einen Satz von Zielen mit sich überschneidenden Abhängigkeiten erstellen. Dies geschieht beim Erstellen deiner Tests //foo/tests/... oder beim Importieren eines IDE-Projekts.

Anzahl der Anrufe auf depset reduzieren

Einen depset in einer Schleife aufzurufen, ist oft ein Fehler. Dies kann zu Problemen mit sehr tiefen Verschachtelungen führen, die schlechter abschneiden. Beispiel:

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

Er kann einfach ersetzt werden. Ziehe zuerst die Übergangsdesets und füge sie alle zusammen:

transitive = []

for i in inputs:
    transitive.append(i.deps)

x = depset(transitive = transitive)

Dieser Wert kann gelegentlich mit dem Verständnis einer Liste reduziert werden:

x = depset(transitive = [i.deps for i in inputs])

ctx.actions.args() für Befehlszeile verwenden

Beim Erstellen von Befehlszeilen sollten Sie ctx.actions.args() verwenden. Dadurch wird die Erweiterung aller Abhängigkeiten in die Ausführungsphase verschoben.

Dies reduziert nicht nur den schnelleren Betrieb, sondern reduziert auch den Speicherverbrauch Ihrer Regeln – manchmal um 90% oder mehr.

Hier einige Tricks:

  • Depsets und Listen direkt als Argumente übergeben, anstatt sie selbst aufzuschlüsseln Sie werden um ctx.actions.args() für Sie erweitert. Wenn Sie Transformationen für den Depset-Inhalt benötigen, sehen Sie sich ctx.actions.args#add an, um zu prüfen, ob etwas passt.

  • Wird File#path als Argument übergeben? Keine Sorge. Jede Datei wird automatisch in den Pfad umgewandelt und auf die Maximierungszeit verschoben.

  • Vermeiden Sie die Konstruktion von Strings, indem Sie sie miteinander verketten. Das beste Stringargument ist eine Konstante, da der Arbeitsspeicher von allen Instanzen der Regel gemeinsam genutzt wird.

  • Wenn die Argumente zu lang für die Befehlszeile sind, kann ein ctx.actions.args()-Objekt mit ctx.actions.args#use_param_file bedingt oder bedingungslos in eine Parameterdatei geschrieben werden. Dies geschieht im Hintergrund, wenn die Aktion ausgeführt wird. Wenn Sie die Parameterdatei explizit steuern möchten, können Sie sie manuell mit ctx.actions.write schreiben.

Beispiel:

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

Eingaben für Übergangsaktionen sollten depsets sein

Denken Sie beim Erstellen einer Aktion mit ctx.actions.run daran, dass das Feld inputs eine Verzögerung akzeptiert. Verwenden Sie diese Option, wenn Eingaben vorübergehend aus Abhängigkeiten erfasst werden.

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

Zum Hängen

Wenn {5/} hängen bleibt, kannst du Strg-\ drücken oder Bazel ein SIGQUITSignal senden (kill -3 $(bazel info server_pid)), um einen Thread-Dump in der Datei $(bazel info output_base)/server/jvm.out zu erhalten.

Da du bazel info möglicherweise nicht ausführen kannst, wenn Bazel beendet wird, ist das Verzeichnis output_base in der Regel das übergeordnete Element bazel-<workspace> des Symlinks in deinem Arbeitsbereichverzeichnis.

Leistungsprofilerstellung

Istio schreibt standardmäßig ein JSON-Profil in die Ausgabebasis command.profile.gz. Sie können den Standort mit dem Flag --profile konfigurieren, z. B. --profile=/tmp/profile.gz. Der Standort mit der Endung .gz wird mit GZIP komprimiert.

Wenn Sie die Ergebnisse sehen möchten, öffnen Sie chrome://tracing in einem Tab des Chrome-Browsers, klicken Sie auf „Laden“ und wählen Sie die (potenziell komprimierte) Profildatei aus. Wenn Sie detailliertere Ergebnisse erhalten möchten, klicken Sie auf die Kästchen links unten.

Mit den folgenden Tastatursteuerelementen können Sie navigieren:

  • Drücken Sie 1 für den Modus „auswählen“. In diesem Modus kannst du bestimmte Felder auswählen, um die Ereignisdetails zu prüfen (siehe linke untere Ecke). Wählen Sie mehrere Ereignisse aus, um eine Zusammenfassung und zusammengefasste Statistiken zu erhalten.
  • Drücken Sie 2 für den Modus Schwenken. Ziehen Sie die Ansicht dann mit der Maus. Du kannst auch a/d verwenden, um nach links/rechts zu verschieben.
  • Drücke 3 für den Modus „Zoom“. Ziehen Sie dann die Maus, um zu zoomen. Du kannst auch w/s verwenden, um heran- oder herauszuzoomen.
  • Drücken Sie 4 für den Modus „Timing“. Hier können Sie die Entfernung zwischen zwei Ereignissen messen.
  • Drücke ?, um mehr über alle Steuerelemente zu erfahren.

Profilinformationen

Beispielprofil:

Beispielprofil

Abbildung 1. Beispielprofil.

Dies sind einige spezielle Zeilen:

  • action counters: Zeigt an, wie viele gleichzeitige Aktionen ausgeführt werden. Klicken Sie darauf, um den tatsächlichen Wert zu sehen. Sollte bis auf den Wert von --jobs bei sauberen Builds erhöhen.
  • cpu counters: Zeigt für jede Sekunde des Builds die von Istio verwendete CPU-Menge an. Ein Wert von 1 entspricht einem Kern, der zu 100% ausgelastet ist.
  • Critical Path: Zeigt für jede Aktion auf dem kritischen Pfad einen Block an.
  • grpc-command-1: Memcache-Hauptthread. Das ist nützlich, um einen allgemeinen Überblick über die Aktionen von Istio zu erhalten, z. B. „Launch Memcache&&t“, „&TargetTargetPatterns" und „quot;runAnalysisPhase"“.
  • Service Thread: Zeigt kleinere und schwerwiegende Pausen für die automatische Speicherbereinigung an.

Andere Zeilen stellen Istio-Threads dar und zeigen alle Ereignisse in diesem Thread an.

Häufige Leistungsprobleme

Achten Sie bei der Analyse von Leistungsprofilen auf Folgendes:

  • Langsamer als erwartete Analysephase (runAnalysisPhase), insbesondere bei inkrementellen Builds. Dies kann ein Zeichen für eine fehlerhafte Implementierung von Regeln sein, z. B. eine, die Abstände vereinfacht. Das Laden des Pakets kann durch übermäßig viele Ziele, komplexe Makros oder rekursive Globs langsam sein.
  • Einzelne langsame Aktionen, insbesondere auf dem kritischen Pfad Sie können große Aktionen in mehrere kleinere Aktionen aufteilen oder die Anzahl der (temporär) abhängigen Abhängigkeiten reduzieren, um sie zu beschleunigen. Prüfe außerdem, ob ein ungewöhnlicher hoher Wert (nicht PROCESS_TIME) ist (z. B. REMOTE_SETUP oder FETCH).
  • Engpässe: Eine kleine Anzahl von Threads ist beschäftigt, während alle anderen inaktiv sind / auf das Ergebnis warten (siehe Screenshot oben 15–30 Sekunden). Die Optimierung erfordert mit hoher Wahrscheinlichkeit eine Anpassung der Regelimplementierungen oder von Istio selbst, um die Parallelität zu erhöhen. Das kann auch passieren, wenn eine ungewöhnliche Anzahl von GC vorhanden ist.

Dateiformat

Das Objekt der obersten Ebene enthält Metadaten (otherData) und die tatsächlichen Tracing-Daten (traceEvents). Die Metadaten enthalten zusätzliche Informationen, z. B. die Aufruf-ID und das Datum des Istio-Aufrufs.

Beispiel:

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

Zeitstempel (ts) und Dauer (dur) in den Trace-Ereignissen werden in Mikrosekunden angegeben. Die Kategorie (cat) ist einer von Enum-Werten von ProfilerTask. Beachten Sie, dass einige Ereignisse zusammengeführt werden, wenn sie sehr kurz und nahe beieinander liegen. Übergeben Sie --noslim_json_profile, wenn Sie das Zusammenführen von Ereignissen verhindern möchten.

Weitere Informationen finden Sie in der Spezifikation für das Trace-Ereignis von Chrome.

Profil analysieren

Diese Methode besteht aus zwei Schritten. Zuerst musst du deinen Build/Test mit dem Flag --profile ausführen.

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

Die generierte Datei (in diesem Fall /tmp/prof) ist eine Binärdatei, die mit dem Befehl analyze-profile verarbeitet und analysiert werden kann:

$ bazel analyze-profile /tmp/prof

Standardmäßig wird eine Zusammenfassungsanalyse für die angegebene Profildatendatei ausgegeben. Hierzu gehören kumulative Statistiken für verschiedene Aufgabentypen für jede Build-Phase und eine Analyse des kritischen Pfads.

Der erste Abschnitt der Standardausgabe enthält eine Übersicht über die Zeit, die in den verschiedenen Build-Phasen verbracht wurde:

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

Speicherprofil

Memcache verfügt über einen integrierten Speicherprofiler, mit dem sich die Speicherauslastung Ihrer Regeln prüfen lässt. Bei einem Problem können Sie den Heap entpacken, um die genaue Codezeile zu ermitteln, die das Problem verursacht.

Arbeitsspeicher-Tracking aktivieren

Sie müssen diese beiden Start-Flags an jeden Istio-Aufruf übergeben:

  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

Dadurch wird der Server im Arbeitsspeicher-Tracking-Modus gestartet. Wenn du diese Schritte für einen einzelnen Filestore-Aufruf vergisst, wird der Server neu gestartet und du musst von vorn beginnen.

Den Memory Tracker verwenden

Sehen Sie sich beispielsweise das Ziel foo an und sehen Sie sich an, was es bewirkt. Fügen Sie das Flag --nobuild hinzu, um nur die Analyse und nicht die Build-Ausführungsphase auszuführen.

$ bazel $(STARTUP_FLAGS) build --nobuild //foo:foo

Prüfen Sie als Nächstes, wie viel Arbeitsspeicher die gesamte Istio-Instanz verbraucht:

$ bazel $(STARTUP_FLAGS) info used-heap-size-after-gc
> 2594MB

Mit bazel dump --rules können Sie sie nach Regelklasse aufschlüsseln:

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

Erstelle eine pprof-Datei mit bazel dump --skylark_memory und sieh dir an, wohin der Arbeitsspeicher geht:

$ bazel $(STARTUP_FLAGS) dump --skylark_memory=$HOME/prof.gz
> Dumping Starlark heap to: /usr/local/google/home/$USER/prof.gz

Verwenden Sie das pprof-Tool, um den Heap zu untersuchen. Ein guter Ausgangspunkt ist die Verwendung eines Flame-Diagramms mit pprof -flame $HOME/prof.gz.

Laden Sie pprof von https://github.com/google/pprof herunter.

Erhalte eine Dump-Datei der heißesten Anruf-Websites, die mit Zeilen versehen sind:

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