Time-Travelling-Debugger als Alternative zu klassischen Debuggern

Back to the Future: Zeitreisen durch die Codebasis – mit Time-Travelling-Debugger

Daniel Schutzbach, Fatih Uzdilli, Dr. Mark Cieliebak

© Shutterstock / Erin Cadigan

Time-Travelling-Debugger versprechen das Paradies für Softwareentwickler: frei im Code vorwärts und rückwärts navigieren, nachträglich Logstatements einfügen, an beliebige Zeitpunkte in der Codeausführung springen. Wir nehmen diese vielversprechenden Tools genauer unter die Lupe und beantworten folgende Fragen: Wie funktioniert die Technologie? Wo kann man einen Time-Travelling-Debugger am besten einsetzen? Und welche Einschränkungen gibt es in der Praxis?

Die Grundidee für einen Time-Travelling-Debugger (TTD) ist einfach und bestechend: Der Entwickler soll sich im Debugger durch den Code nicht nur vorwärts, sondern auch rückwärts bewegen können. Darüber hinaus soll es möglich sein, zu einem beliebigen Zeitpunkt der Codeausführung zu springen.

Use Cases

Erweiterung des traditionellen Code-Debuggings: Wer kennt dieses Szenario nicht? Step over → step over → step into → step over → „Neiinn, schon wieder zu weit! Also wieder von vorne“. Wie schön wäre doch ein Step-Back-Button! Genau dieses Bedürfnis beim traditionellen Debuggen sollen TTDs stillen: Man kann sich vorwärts und rückwärts bewegen. Dadurch verliert man weniger Zeit, um die kritischen Codestellen zu finden.

Reproduktion von Ereignissen in produktiven Umgebungen: Die technischen Eigenheiten eines TTD (Details im nächsten Abschnitt) erlauben es, den üblichen Workflow des Debuggens auf ein höheres Level zu heben. Tritt zum Beispiel im Betrieb einer produktiven Software ein Fehler auf, sind die nächsten Schritte üblicherweise wie folgt:

  1. Im Log Hinweise suchen, wie man den Fehler reproduzieren kann – falls vorhanden.
  2. Versuchen, diesen Fehler in einem engen Rahmen nachzubilden.
  3. Debuggen, um den Fehler zu finden.

Mit einem TTD wird der gesamte Run in einer produktiven Umgebung aufgenommen. Dadurch kann man die ersten zwei Schritte auslassen und direkt mit der Fehlersuche beginnen. Ein Nebeneffekt ist, dass man sich auch die Zeit sparen kann, Logstatements zu schreiben.

Trace Recording als Basistechnologie

Für die Umsetzung von TTDs verwendet man eine simple und naheliegende Idee: Jede ausgeführte Codezeile wird mit all ihren Aktoren und Parameterwerten chronologisch gespeichert (Trace Recording). Beim Debuggen wird der Code dann nicht tatsächlich ausgeführt, es sieht nur so aus. In Wirklichkeit wird der echte Ablauf einfach wiedergegeben, ähnlich wie bei einem Videorekorder. Dass man in diesem Set-up beliebig vorwärts und rückwärts springen kann, ist eine logische Konsequenz.

Mit aktivem Trace Recording werden also alle Ereignisse im Programmablauf aufgezeichnet. Möchte man später ein unvorhergesehenes Ereignis überprüfen, einen Bug zum Beispiel, so muss man diesen nicht reproduzieren, sondern kann ganz einfach die Aufzeichnungen laden und an die gewünschte „zeitliche Stelle“ springen.

Bytecode-Manipulation

So simpel die Idee des Trace Recordings auch sein mag, die einwandfreie Umsetzung ist nicht trivial. Eine Möglichkeit besteht darin, dass der Trace Recorder für die Aufzeichnung des Programmablaufs den Bytecode des Java-Programms manipuliert, sodass stets das aktuelle Statement abgespeichert wird (Abb. 1). Hierzu wird der Bytecode der Klasse geändert, wenn die Klasse vom Classloader geladen wird. Dies kann man zum Beispiel mit Frameworks wie Javassist oder ASM erreichen, die auch Grundlage für AOP-Frameworks wie AspectJ oder Spring sind.

straube_angularjs_1

Abb. 1 Programmverlauf mittels Bytecode-Manipulation aufnehmen

State Recording

Die große Herausforderung besteht darin, das Trace Recording so effizient zu realisieren, dass es die Programmausführung nicht zu sehr verzögert und das entstehende Datenvolumen nicht zu groß wird. Dazu kann man z. B. einen hybriden Ansatz verwenden. Hierbei protokolliert der Recorder nicht alle Ausführungen von einzelnen Codezeilen, sondern erzeugt zwischendurch immer wieder einen Snapshot der JVM. Daraus können später Objekte und Status der JVM für beliebige Zeitpunkte wiederhergestellt werden. Dieses Vorgehen verringert die anfallende Datenmenge massiv, bewirkt aber, dass die Navigation im Debugger langsamer wird.

Bei den Snapshots kann am einfachsten der gesamte Adressraum der JVM gespeichert werden. Dies beinhaltet aber auch viel unbenutzten Speicher, sodass es effizienter ist, nur den echten Programmzustand mit seinen Liveobjekten zu speichern. Dabei sollte natürlich darauf geachtet werden, dass dies zu einem günstigen Zeitpunkt geschieht, z. B. nachdem der Garbage Collector aufgeräumt hat. So werden keine unreferenzierten Objekte gespeichert.

Exkurs: Automatisch Tests generieren

Trace Recordings enthalten wertvolle Informationen über den Programmablauf eines Produktivsystems. Diese helfen nicht nur beim Debuggen: Die Zürcher Hochschule für Angewandte Wissenschaften (ZHAW) arbeitet zurzeit an einer Technologie, um aus den Recordings automatisch Regressionstests zu erzeugen. Diese kann man z. B. einsetzen, um Refactorings bei Legacy-Systemen sicherer zu machen.

Toollandschaft

In der wissenschaftlichen Literatur werden Time-Traveling-Debugger schon seit über zwei Jahrzehnten diskutiert [1]. Der erste uns bekannte praxistaugliche Time-Travelling-Debugger für Java wurde im Jahre 2003 von Lambda Computer Science vorgestellt, der Omniscient Debugger. Zwei Jahre später wurde seine Präsentation an der Java One mit dem „Outstanding Talk“ Award ausgezeichnet. Trotzdem wurde die Weiterentwicklung ab 2007 eingestellt.

Es gab zwischenzeitlich noch einige weitere Tools wie z. B. JIVE, Diver und TOD, die sich aber in der Java-Welt nicht durchsetzen konnten. Der Platzhirsch stammt heute von Chronon Systems, die seit 2010 einen kostenpflichtigen TTD entwickeln und heute verschiedene Produktvarianten für das Recording anbieten. Wir haben darum dieses System für unseren Praxistest verwendet.

Chronons Time-Travelling-Debugger im Praxistest

Sind TTD wirklich das Paradies für Entwickler, und können sie halten, was sie versprechen? Um diese Fragen zu beantworten, wollten wir einen Debugger auf Herz und Nieren prüfen. Wir verwenden den Chronon-Debugger von Chronon Systems, da er unsere oben erwähnten Use Cases abdeckt und praktisch keine Konkurrenz in der Java-Welt hat. Das Debuggen mit Chronon besteht im Wesentlichen aus vier Schritten:

  • Auswählen der Klassen, deren Ausführung aufgezeichnet werden soll
  • Ausführen der Applikation mit aktiviertem Chronon Recorder
  • Entpacken der Aufnahmedatei
  • Debuggen in der Chronon-Debug-Ansicht

Für das Recording bietet Chronon verschiedene Varianten: Für die Aufnahme von Produktivsystemen dient der Standalone Recorder und der Recorder-Server, mit dem man den Aufnahmevorgang auch von der Ferne aktivieren und die Aufnahmen zentral speichern kann. Eine Embedded-Variante stellt ein API zur Verfügung, um programmiertechnisch das Recording zu steuern.

Für die Unterstützung im Entwicklungsprozess bietet Chronon Plug-ins für Eclipse und IntelliJ an. Diese Plug-ins erweitern den gewohnten Debugger der IDE, wobei der Recorder transparent eingebettet ist. Für unsere Tests verwenden wird das IntelliJ-Plug-in (Version 3.7).

Die zu debuggende Applikation muss mit aktiviertem Chronon Recorder gestartet werden. In den IDE-Plug-ins geschieht dies unauffällig mit dem Starten der Applikation im Debug-Modus. Auf einem produktiven Server muss man beim Ausführen der Applikation den Chronon Recorder über VM-Argumente mitgeben. Chronon bietet an, Klassen über ihren exakten Namen oder Namensmuster für das Recording auszuwählen oder auszuschließen, um unnötiges Protokollieren zu vermeiden. Im IntelliJ-Plug-in geschieht das einfach über ein GUI (Abb. 2). Auf einem produktiven Server erstellt man eine Konfigurationsdatei, die via VM-Argument mitgegeben wird. Die Verwendung des Chronon Recorders ist sehr einfach, da keine Sourcecode-Änderungen nötig sind.

straube_angularjs_2

Abb. 2: Laufzeitkonfiguration von Chronon in IntelliJ

Verwendung des Debuggers

Sobald das Recording durchgeführt wurde, liegt eine Aufnahmedatei auf der Festplatte. Diese ist stark komprimiert und wird automatisch entpackt, wenn sie das erste Mal im Debugger verwendet wird. Das Entpacken kann einige Zeit in Anspruch nehmen (Details siehe unten), entfällt aber beim wiederholten Laden.

Der Debugger bietet dann verschiedene Möglichkeiten, um den Code zu untersuchen: Man kann Schritt für Schritt vor und zurücklaufen, in Methoden rein-, raus- und zurückspringen sowie zum Cursor vorwärts und rückwärts springen. Außerdem kann zwischen den verschiedenen Threads hin und hergewechselt werden. Auch kann zu den Ausführungspunkten von geworfenen Exceptions gesprungen werden. Sobald man sich in einer Methode befindet, wird eine Übersicht aller Aufrufe in zeitlicher Abfolge bereitgestellt. In dieser sind auch die dazugehörigen Aufrufparameter ersichtlich. Es besteht zudem die Möglichkeit, Logstatements einzuführen. Auf Variablen kann zugegriffen werden. So kann im Nachhinein ein Log generiert werden.

Beim Debuggen wird der Code nicht ausgeführt, sondern nur die Aufnahme „abgespielt“. Ein wesentlicher Vorteil ist, dass keine echten Zugriffe auf externe Ressourcen (DB, Filesystem) stattfinden. Man muss also keine DB in einen Ursprungszustand bringen, und versehentliche Schreibzugriffe können nicht stattfinden.

Dies sind alles Vorteile gegenüber herkömmlichem Debuggen. Jedoch muss man auch gewillt sein, auf gewisse Features zu verzichten: Während beim herkömmlichen Debuggen eine Codezeile verändert und im Idealfall weiter ausgeführt werden kann, ist dies bei Chronon nicht möglich. Auch das dynamische Verändern von Variablenwerten ist nicht mehr möglich. Dafür müsste der Code geändert und die ganze Applikation erneut im Recording-Modus ausgeführt werden.

Zeitreisen mit Chronon können dauern

Beim Trace Recording werden alle Ereignisse in einem Programmlauf aufgezeichnet. Es ist offensichtlich, dass dies Einfluss auf die Performance des Programms hat. Kann man also mit einer Applikation noch arbeiten, wenn das Trace Recording aktiviert ist? Oder wird sie so langsam, dass das System nicht mehr brauchbar ist? Dies sind kritische Fragen, die geklärt sein müssen, bevor man Chronon auf einem produktiven System einsetzt.

In unseren Experimenten haben wir verschiedene typische Applikationsarten untersucht, und die kurze Antwort auf die Fragen oben lautet: Es hängt von der Art der Software ab, wie groß die Performanceeinbußen sind.

Konkret haben wir für unsere Untersuchung eine Webapplikation, einen Web-Crawler, eine Dokument-Indexing-Applikation sowie verschiedene Sortier- und Suchalgorithmen (z. B. Mergesort) verwendet. Alle Experimente wurden mit dem IntelliJ-Plug-in (Version 3.7) gemacht, und wir haben alle externen Bibliotheksklassen vom Recording-Prozess ausgeschlossen.

Kaum Performanceeinfluss bei I/O-intensiven Applikationen

Bei der Webapplikation handelte es sich um eine typische CRUD-Anwendung mit einer Datenbank im Backend. Die Performanceauswirkungen erfassten wir empirisch. Wir bemerkten keine Verschlechterung der Response Time auf der grafischen Oberfläche.

Mit dem Web-Crawler ließen wir eine Webseite komplett crawlen und verglichen die Laufzeit mit und ohne Recording. Die Crawl-Dauer war praktisch gleich. Ähnlich verhielt es sich mit dem Dokument-Indexer, der eine Menge von Webdokumenten in einen Lucene-Index speicherte. Die Laufzeit blieb mit dem Recorder praktisch gleich.

Die Gemeinsamkeit dieser drei Applikationen ist, dass die verwendetet CPU-Zeit im Vergleich zu den Wartezeiten für I/O-Operationen sehr gering ist. Die CPU-Leerläufe (Warten auf I/O) lassen dem Recorder offenbar genügend Zeit, um zwischendurch alle notwendigen Informationen zu persistieren. Solche I/O-intensiven Applikationen sind also „ideal“ für Chronon, da für den User praktisch keine merkbaren Performanceeinbußen auftreten.

Sortieralgorithmen werden deutlich langsamer

Ganz anders ist die Situation bei Programmen, die speicher- und rechenintensiv sind. In diesem Fall ist der Einfluss von Chronon auf die Laufzeit massiv. Konkret haben wir in unseren Experimenten das komplette Aufzeichnen von verschiedenen Sortier-, Such- und ähnlichen Algorithmen untersucht, die ausschließlich auf dem Arbeitsspeicher operieren (keine Zugriffe auf DB oder Festplatte). Je nach Algorithmus erhielten wir bis zu 2 000 mal längere Ausführungszeiten (vgl. Abb. 3 für Mergesort, das „nur“ einen Overhead-Faktor von 200 hatte). Für die Praxis heißt das, dass man im schlechtesten Fall statt einer Sekunde plötzlich rund eine halbe Stunde auf eine Antwort warten müsste!

schutzbach_debugger_3

Abb. 3: Unterschied der Laufzeit für einen Mergesort-Algorithmus mit und ohne Chronon

Ein großer Overhead bei der Programmausführung ist natürlich per se noch kein Ausschlusskriterium für die Nutzung. Erstens kann (und sollte!) man die aufzunehmenden Klassen einschränken, und zweitens gilt es abzuwägen, ob man den Overhead in Kauf nimmt, um einen Bug schneller zu finden.

Während der Experimente machten wir noch eine interessante Beobachtung: Die Laufzeit einer einzelnen Applikation verlängerte sich im Recording-Modus praktisch immer um einen konstanten Faktor, der vom Typ der Applikation abhing. Das heißt, wenn man eine konkrete Applikation einmal im Recording-Modus ausgeführt und den Overhead-Faktor bestimmt hat, kann man davon ausgehen, dass dieser Faktor auch bei allen zukünftigen Programmläufen dieser Applikation ähnlich sein wird. Dadurch kann man sehr gut vorhersagen, wie sich die Laufzeit durch Chronon für diese Applikation verhalten wird. Eine allgemeine Regel, welche Applikationen wie stark verlangsamt werden, konnten wir hingegen nicht finden.

Chronon braucht Speicher

Die Größe der Datei, die vom Chronon Recorder auf die Festplatte geschrieben wird, hängt von der Menge und Art der ausgeführten Operationen ab. Bei den I/O-intensiven Applikationen war dies eher wenig (40 bis 200 MB in 2 Minuten), während beim Sortieren von 300 000 Elementen (7 Sekunden mit Chronon) etwa 1 Gigabyte Daten geschrieben wurden.

Auf modernen SSDs fällt das Schreiben kaum ins Gewicht, aber bei langsameren Festplatten kann dies schon mehrere Sekunden in Anspruch nehmen. Dies führt wiederum dazu, dass die Daten länger im Arbeitsspeicher gehalten werden müssen, womit der Bedarf nach einem größeren Heap steigt.

Auch der Stack wird beim Recording mehr beansprucht: In unseren Experimenten mit rekursiven Methoden wurden etwa dreimal mehr Daten auf dem Stack abgelegt. Das bedeutet, dass das Stack Limit schon bei dreimal weniger Funktionsaufrufen erreicht werden kann. Die Default-Einstellung der Stackgröße sollte dementsprechend angepasst werden.

Aufnahmedatei öffnen und Kaffee trinken

Hat man eine Applikation mit dem Chronon Recorder aufgezeichnet, wird die erstellte Aufnahmedatei beim ersten Debuggen von Chronon entpackt. Dieser Schritt kann überraschend lange dauern. Als Beispiel: Eine Crawler-Applikation mit fünf Klassen und ca. 300 Zeilen Code (die Bibliotheksklassen werden nicht aufgezeichnet) lädt bei der Ausführung in zwei Minuten ca. 300 Webseiten herunter. Chronon generiert dafür eine komprimierte Aufnahmedatei von etwa 50 MB. Das Entpacken dauerte etwas mehr als eine halbe Minute.

Bei rechen- und speicherintensiven Applikationen werden ungleich größere Aufnahmedateien erzeugt, die auch entsprechend länger zum Entpacken benötigen: Für einen Mergesort, der 300 000 Elemente sortierte (mit Chronon), dauerte das Entpacken bereits 2 Minuten. Bei 900 000 Elementen wurde die Aufnahmedatei so groß, dass sie sich auf unserem PC nicht mehr verwenden ließ.

Bei längeren Laufzeiten einer Applikation – wie es insbesondere in einer produktiven Umgebung der Fall ist –, ist also mit sehr langen Zeiten zum Entpacken zu rechnen. Als Workaround bietet Chronon als Teil vom Recording-Server einen Standalone Unpacker, der sich eignet, Aufnahmedateien im Hintergrund über längere Zeit zu entpacken.

Fazit

Time-Travelling-Debugger bieten eine interessante Alternative zu klassischen Debuggern, indem sie erlauben, beliebig in einem Programm vorwärts und rückwärts zu navigieren und Logstatements nachträglich einzufügen. Dafür zeichnen sie den Ablauf eines Programms vollständig auf (Trace Recording) und spielen ihn im Debug-Modus ab.

TTDs sind extrem nützlich bei Bugs, die man nicht einfach reproduzieren kann. Für diese Fälle hilft es, wenn man einfach das Recording abspielt, an den Zeitpunkt springt, wo der Bug passiert und sieht, was unmittelbar vorher im Programm passiert ist. Aus unserer Sicht hilft ein TTD, einen Bug zu finden. Das Beheben der Bugs ist hingegen in einem klassischen Debugger einfacher, bei dem man Codezeilen direkt ändern und weiter ausführen kann.

Im Praxistest mit Chronon zeigte sich, dass das Trace Recording bei I/O-intensiven Applikationen kaum einen Einfluss auf die Laufzeit hat, während rechenintensive Programme massiv gebremst werden. Man muss also genau überlegen, für welche Klassen man das Recording aktiviert, insbesondere in einer Produktivumgebung.

Der Entwicklungsprozess wird durch die Plug-ins für Eclipse und IntelliJ sehr gut unterstützt. Das Recording wird automatisch gestartet, man kann einfach Klassen ein- und ausschließen, und der TTD bietet mächtige und wertvolle Analysewerkzeuge für den Umgang mit dem Code. Leider wird das flüssige Arbeiten oft durch die Zeit für das Entpacken der Recording-Datei gebremst.

Insgesamt haben wir einen sehr positiven Eindruck von Chronon. Das System arbeitet stabil und unterstützt den Entwickler sehr gut bei der Bugsuche. Der Grund, warum Chronon sich bisher noch nicht in der Entwicklercommunity durchgesetzt hat, sind vermutlich die Performanceprobleme.

In [2] wurden die Eigenschaften eines brauchbaren TTDs wie folgt definiert: maximal 25 Prozent Performanceeinbuße beim Recording und höchstens eine Sekunde Wartezeit bis zum Starten des Debuggers. Sobald Chronon diese Ziele erreicht, wird es sicher zu einem Standardtool für Java-Entwickler avancieren.

Aufmacherbild: A replica of the Back to the Future DeLorean via Shutterstock / Urheberrecht: Erin Cadigan

Verwandte Themen:

Geschrieben von
Daniel Schutzbach, Fatih Uzdilli, Dr. Mark Cieliebak
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: