
Heute teilen wir Ihnen eine Übersetzung eines Artikels des Erstellers von FunctionTrace mit, einem Python-Profiler mit einer intuitiven grafischen Oberfläche, der Multiprozessor- und Multithread-Anwendungen profilieren kann und eine Größenordnung weniger Ressourcen verbraucht als andere Python-Profiler. Es spielt keine Rolle, ob Sie gerade Webentwicklung in Python lernen oder es schon lange verwenden - es ist immer gut zu verstehen, was Ihr Code tut. Über das Aussehen dieses Projekts, über die Details seiner Entwicklung - weiter unter dem Schnitt.
Einführung
Der Firefox Profiler war der Eckpfeiler von Firefox während der Project Quantum- Ära . Wenn Sie den Beispieleintrag öffnen , wird eine leistungsstarke webbasierte Benutzeroberfläche für die Leistungsanalyse angezeigt, die Aufrufbäume, Stapeldiagramme, Branddiagramme und mehr enthält. Alle Aktionen zum Filtern, Skalieren, Schneiden und Transformieren von Daten werden in einer URL gespeichert, die gemeinsam genutzt werden kann. Die Ergebnisse können in einem Fehlerbericht geteilt, Ihre Ergebnisse dokumentiert, mit anderen Aufzeichnungen verglichen oder Informationen für weitere Untersuchungen weitergegeben werden. Firefox DevEdition verfügt über einen integrierten Profiling-Thread. Dieser Ablauf erleichtert die Kommunikation. Unser Ziel ist es, allen Entwicklern, auch außerhalb von Firefox, die Möglichkeit zu geben, produktiv zusammenzuarbeiten.
Zuvor importierte Firefox Profiler andere Formate, beginnend mit Linux Perf- und Chrome-Profilen . Im Laufe der Zeit haben Entwickler weitere Formate hinzugefügt. Heute entstehen die ersten Projekte zur Anpassung von Firefox an Analysetools. FunctionTrace ist ein solches Projekt. Matt erzählt die Geschichte, wie das Instrument hergestellt wurde.
FunctionTrace
Ich habe kürzlich ein Tool erstellt, mit dem Entwickler besser verstehen können, was in ihrem Python-Code vor sich geht. FunctionTrace ist ein No-Sampling-Profiler für Python, der auf unveränderten Anwendungen mit sehr geringem Overhead ausgeführt wird - weniger als 5%. Es ist wichtig zu beachten, dass es in Firefox Profiler integriert ist. Auf diese Weise können Sie grafisch mit Profilen interagieren und so Muster leichter erkennen und Änderungen an Ihrer Codebasis vornehmen.
Ich werde die Entwicklungsziele von FunctionTrace durchgehen und die technischen Implementierungsdetails teilen. Am Ende werden wir mit einer kleinen Demo spielen .

Ein Beispiel für ein FunctionTrace-Profil, das in Firefox Profiler geöffnet wurde.
Technische Schulden als Motivation
Codebasen werden mit der Zeit immer größer. Besonders wenn Sie an komplexen Projekten mit vielen Menschen arbeiten. Einige Sprachen behandeln dieses Problem besser. Zum Beispiel gibt es die Funktionen der Java-IDE seit Jahrzehnten. Oder Rust und seine starke Typisierung, die das Refactoring sehr einfach macht. Manchmal scheint es schwieriger zu werden, Codebasen in anderen Sprachen zu pflegen. Dies gilt insbesondere für älteren Python-Code. Zumindest sind wir jetzt alle Python 3, oder?
Es kann äußerst schwierig sein, umfangreiche Änderungen vorzunehmen oder unbekannten Code umzugestalten. Es ist viel einfacher für mich, den Code richtig zu ändern, wenn ich alle Interaktionen des Programms und dessen Funktionsweise sehe. Oft schreibe ich sogar Codeteile neu, die ich nie berühren wollte: Die Ineffizienz ist offensichtlich, wenn ich sie in der Visualisierung sehe.
Ich wollte verstehen, was im Code vor sich geht, ohne Hunderte von Dateien lesen zu müssen. Aber ich habe nicht die Werkzeuge gefunden, die meinen Bedürfnissen entsprechen. Außerdem habe ich das Interesse daran verloren, ein solches Tool selbst zu erstellen, da viel UI-Arbeit erforderlich ist. Und die Schnittstelle war notwendig. Meine Hoffnungen auf ein schnelles Verständnis der Programmausführung wurden neu entfacht, als ich auf den Firefox-Profiler stieß.
Der Profiler stellte alle schwer zu implementierenden Elemente bereit - eine intuitive Open-Source-Benutzeroberfläche, die Stack-Plots, zeitgebundene Protokollmarkierungen und Branddiagramme anzeigt und Stabilität verleiht, deren Art an einen bekannten Webbrowser gebunden ist. Jedes Tool, das ein ordnungsgemäß formatiertes JSON-Profil schreiben kann, kann alle zuvor genannten grafischen Analysefunktionen wiederverwenden.
FunctionTrace-Design
Zum Glück hatte ich bereits eine Woche Urlaub geplant, nachdem ich den Firefox-Profiler entdeckt hatte. Und ich hatte einen Freund, der mit mir ein Instrument entwickeln wollte. In dieser Woche nahm er sich auch einen Tag frei.
Ziele
Als wir mit der Entwicklung von FunctionTrace begannen, hatten wir mehrere Ziele:
- Die Fähigkeit, alles zu sehen , was im Programm passiert.
- .
- , .
Das erste Ziel hatte einen erheblichen Einfluss auf das Design. Die letzten beiden haben die technische Komplexität erhöht. Wir wussten beide aus früheren Erfahrungen mit ähnlichen Tools, dass die Frustration darin besteht, dass wir keine zu kurzen Funktionsaufrufe sehen. Wenn Sie einen 1-ms-Tracking-Datensatz aufnehmen, aber wichtige und schnellere Funktionen haben, verpassen Sie viel von dem, was in Ihrem Programm passiert.
Wir wussten auch, dass wir alle Funktionsaufrufe verfolgen mussten. Daher konnten wir den Stichprobenprofiler nicht verwenden. Außerdem habe ich kürzlich einige Zeit mit Code verbracht, in dem Python-Funktionen anderen Python-Code ausführen, häufig über ein Shell-Mittelsmann-Skript. Auf dieser Grundlage wollten wir in der Lage sein, die untergeordneten Prozesse zu verfolgen.
Erstimplementierung
Um mehrere Prozesse und Nachkommen zu unterstützen, haben wir uns für ein Client-Server-Modell entschieden. Python-Clients senden Trace-Daten an den Rust-Server. Der Server aggregiert und komprimiert die Daten, bevor er ein Profil generiert, das vom Firefox-Profiler verwendet werden kann. Wir haben uns aus mehreren Gründen für Rust entschieden, darunter starkes Tippen, das Streben nach konsistenter Leistung und vorhersehbarer Speichernutzung sowie die einfache Erstellung von Prototypen und Refactoring.
Wir haben den Client als Python-Modul mit dem Namen prototypisiert
python -m functiontrace code.py. Dies machte es einfach, die integrierten Trace-Hooks zum Protokollieren der Ausführung zu verwenden. Die ursprüngliche Implementierung sah folgendermaßen aus:
def profile_func(frame, event, arg):
if event == "call" or event == "return" or event == "c_call" or event == "c_return":
data = (event, time.time())
server.sendall(json.dumps(data))
sys.setprofile(profile_func)
Der Server überwacht einen Unix-Domain-Socket . Die Daten werden dann vom Client gelesen und vom Firefox-Profiler in JSON konvertiert .
Der Profiler unterstützt verschiedene Arten von Profilen, z. B. Perf-Protokolle . Wir haben uns jedoch entschlossen, JSON im internen Profiler-Format zu generieren. Es erfordert weniger Speicherplatz und Wartung als das Hinzufügen eines neuen unterstützten Formats. Es ist wichtig zu beachten, dass der Profiler die Abwärtskompatibilität zwischen Profilversionen beibehält. Dies bedeutet, dass jedes Profil, das für die aktuelle Version des Formats entwickelt wurde, beim zukünftigen Download automatisch in die neueste Version konvertiert wird. Der Profiler bezieht sich auch auf Zeichenfolgen mit ganzzahligen Bezeichnern. Dies ermöglicht eine erhebliche Platzersparnis durch Deduplizierung (während die Verwendung trivial istIndexkarte ).
Mehrere Optimierungen
Meistens funktionierte der ursprüngliche Code. Bei jedem Funktionsaufruf und jeder Rückkehr rief Python den Hook auf. Der Hook hat über den Socket eine JSON-Nachricht an den Server gesendet, um sie in das gewünschte Format zu konvertieren. Aber es war unglaublich langsam. Selbst nach dem Stapeln der Socket-Aufrufe haben wir den achtfachen Aufwand einiger Testprogramme festgestellt.
Nachdem wir solche Kosten gesehen hatten, gingen wir mit der C-API für Python auf die C-Ebene . Und sie haben für dieselben Programme einen Overhead-Koeffizienten von 1,1 erhalten. Danach konnten wir eine weitere Schlüsseloptimierung durchführen und Aufrufe
time.time()an rdtsc- Operationen über ersetzenclock_gettime()... Wir haben den Leistungsaufwand beim Aufrufen von Funktionen auf mehrere Anweisungen reduziert und 64 Bit Daten generiert. Es ist viel effizienter als das Verketten von Python-Aufrufen und komplexer Arithmetik auf einem geschäftskritischen Pfad.
Ich erwähnte, dass die Ablaufverfolgung mehrerer Threads und untergeordneter Prozesse unterstützt wird. Dies ist einer der schwierigsten Teile des Kunden, daher lohnt es sich, einige Details auf niedrigerer Ebene zu besprechen.
Unterstützung für mehrere Streams
Der Handler für alle Threads wird über installiert
threading.setprofile(). Wir registrieren uns über einen solchen Handler, wenn wir den Thread-Status einrichten. Dies stellt sicher, dass Python ausgeführt wird und die GIL gehalten wird. Dies vereinfacht einige der Annahmen:
// This is installed as the setprofile() handler for new threads by
// threading.setprofile(). On its first execution, it initializes tracing for
// the thread, including creating the thread state, before replacing itself with
// the normal Fprofile_FunctionTrace handler.
static PyObject* Fprofile_ThreadFunctionTrace(..args..) {
Fprofile_CreateThreadState();
// Replace our setprofile() handler with the real one, then manually call
// it to ensure this call is recorded.
PyEval_SetProfile(Fprofile_FunctionTrace);
Fprofile_FunctionTrace(..args..);
Py_RETURN_NONE;
}
Wenn der Hook aufgerufen wird
Fprofile_ThreadFunctionTrace(), ordnet er die Struktur zu ThreadState. Diese Struktur enthält Informationen, die der Thread benötigt, um Ereignisse zu protokollieren und mit dem Server zu kommunizieren. Wir senden dann eine Init-Nachricht an den Profilserver. Hier benachrichtigen wir den Server, um einen neuen Stream zu starten und einige erste Informationen bereitzustellen: Zeit, PID usw. Nach der Initialisierung ersetzen wir den Hook durch Fprofile_FunctionTrace()einen, der die eigentliche Ablaufverfolgung durchführt.
Unterstützung für untergeordnete Prozesse
Bei der Arbeit mit mehreren Prozessen wird davon ausgegangen, dass die untergeordneten Prozesse über den Python-Interpreter gestartet werden. Leider werden untergeordnete Prozesse nicht aufgerufen
-m functiontrace, sodass wir nicht wissen, wie wir sie aufspüren können. Um sicherzustellen, dass untergeordnete Prozesse überwacht werden, wird die Umgebungsvariable $ PATH beim Start geändert . Dadurch wird sichergestellt, dass Python auf eine ausführbare Datei verweist, die Folgendes weiß functiontrace:
# Generate a temp directory to store our wrappers in. We'll temporarily
# add this directory to our path.
tempdir = tempfile.mkdtemp(prefix="py-functiontrace")
os.environ["PATH"] = tempdir + os.pathsep + os.environ["PATH"]
# Generate wrappers for the various Python versions we support to ensure
# they're included in our PATH.
wrap_pythons = ["python", "python3", "python3.6", "python3.7", "python3.8"]
for python in wrap_pythons:
with open(os.path.join(tempdir, python), "w") as f:
f.write(PYTHON_TEMPLATE.format(python=python))
os.chmod(f.name, 0o755)
Ein Interpreter mit einem Argument
-m functiontracewird im Wrapper aufgerufen. Schließlich wird beim Start eine Umgebungsvariable hinzugefügt. Die Variable gibt an, welcher Socket für die Kommunikation mit dem Profilserver verwendet wird. Wenn der Client eine bereits festgelegte Umgebungsvariable initialisiert und sieht, erkennt er den untergeordneten Prozess. Anschließend wird eine Verbindung zur vorhandenen Serverinstanz hergestellt, sodass die Ablaufverfolgung mit der des ursprünglichen Clients korreliert werden kann.
FunctionTrace jetzt
Die heutige Implementierung von FunctionTrace hat viel mit der oben beschriebenen Implementierung gemeinsam. Auf hoher Ebene wird der Kunde über FunctionTrace einen Anruf wie folgt verfolgt :
python -m functiontrace code.py. Diese Zeile lädt ein Python-Modul für einige Anpassungen und ruft dann das C-Modul auf, um verschiedene Trace-Hooks festzulegen. Diese Hooks umfassen die oben genannten sys.setprofileSpeicherzuweisungs-Hooks sowie benutzerdefinierte Hooks mit interessanten Funktionen wie builtins.printoder builtins.__import__. Außerdem wird eine Instanz erzeugt functiontrace-server, ein Socket für die Kommunikation mit ihr eingerichtet und es wird garantiert, dass zukünftige Threads und untergeordnete Prozesse mit demselben Server kommunizieren.
Bei jedem Trace-Ereignis sendet der Python-Client einen MessagePack- Eintrag... Es enthält minimale Ereignisinformationen und einen Zeitstempel im Stream-Speicherpuffer. Wenn der Puffer voll ist (alle 128 KB), wird er über den gemeinsam genutzten Socket auf den Server übertragen, und der Client erledigt seine Arbeit weiterhin. Der Server lauscht asynchron auf jeden Client und verbraucht schnell Traces in einem separaten Puffer, um deren Blockierung zu vermeiden. Der jedem Client entsprechende Thread kann dann jedes Trace-Ereignis analysieren und in das entsprechende endgültige Format konvertieren. Nach dem Beenden aller verbundenen Clients werden die Protokolle für jedes Thema zu einem vollständigen Profilprotokoll zusammengefasst. Schließlich wird das Ganze an eine Datei gesendet, die dann mit dem Firefox-Profiler verwendet werden kann.
Gewonnene Erkenntnisse
Ein Python C-Modul bietet deutlich mehr Leistung und Leistung, ist jedoch gleichzeitig mit hohen Kosten verbunden. Es ist mehr Code erforderlich, eine gute Dokumentation ist schwerer zu finden, nur wenige Funktionen sind verfügbar. C-Module scheinen ein nicht ausreichend genutztes Werkzeug zum Schreiben von Hochleistungs-Python-Modulen zu sein. Ich sage dies basierend auf einigen der FunctionTrace-Profile, die ich gesehen habe. Wir empfehlen einen Ausgleich. Schreiben Sie den größten Teil des nicht leistungsfähigen, geschäftskritischen Codes in Python und rufen Sie innere Schleifen oder C-Setup-Code für Teile Ihres Programms auf, in denen Python nicht glänzt.
Das Codieren und Decodieren von JSON kann unglaublich langsam sein, wenn keine Lesbarkeit erforderlich ist. Wir wechselten zu
MessagePackfür die Client-Server-Kommunikation und fand es genauso einfach, damit zu arbeiten, während einige Benchmark-Zeiten halbiert wurden!
Die Unterstützung von Multithread-Profilen in Python ist recht schwierig. Es ist verständlich, warum dies in früheren Python-Profilern keine Schlüsselfunktion war. Es dauerte verschiedene Ansätze und viele Segmentierungsfehler, bis wir eine gute Vorstellung davon hatten, wie man mit der GIL arbeitet und dabei eine hohe Leistung beibehält.
Sie können einen begehrten Beruf von Grund auf neu erwerben oder Ihre Fähigkeiten und Ihr Gehalt verbessern, indem Sie Online-SkillFactory-Kurse belegen:
- Python für Webentwicklungskurs (9 Monate)
- Beruf Webentwickler (8 Monate)
- Ausbildung für den Data Science-Beruf von Grund auf neu (12 Monate)
- - Data Science (14 )
- - Data Analytics (5 )
- (18 )
E
- Machine Learning (12 )
- « Machine Learning Data Science» (20 )
- «Machine Learning Pro + Deep Learning» (20 )
- (6 )
- DevOps (12 )
- iOS- (12 )
- Android- (18 )
- Java- (18 )
- JavaScript (12 )
- UX- (9 )
- Web- (7 )
