© Excellent backgrounds/Shutterstock.com
Time-Travelling-Debugger als Alternative zu klassischen Debuggern

Back to the Future


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:

  • Im Log Hinweise suchen, wie man den Fehler reproduzieren kann – falls vorhanden.

  • Versuchen, diesen Fehler in einem engen Rahmen nachzubilden.

  • 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 Class Loader geladen wird. Dies kann man zum Beispiel mit Frameworks wie Javassist [1] oder ASM [2] erreichen, die auch Grundlage für AOP-Frameworks wie AspectJ [3] oder Spring [4] sind.

schutzbach_debugger_1.tif_fmt1.jpgAbb. 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 Col­lector 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 ma...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang