VB6 hat keinen Profiler? Dann bau ich mir eben einen

Kurz gesagt: Mein Profiler arbeitet mit einer temporären Kopie des VB6-Projekts, fügt dort automatisch Messpunkte ein und zeigt dir live, wo Laufzeit und Windows-Ressourcen hängen bleiben. Dein Originalcode bleibt dabei unangetastet.

Das Problem

Visual Basic 6 ist über 25 Jahre alt — und hat bis heute keinen eingebauten Profiler. Wer wissen will, welche Methode einer VB6-Anwendung die meiste Zeit frisst oder welche einzelne Codezeile plötzlich lahmt, landet ziemlich schnell bei der Steinzeit-Lösung: Debug.Print Timer vor und nach dem verdächtigen Codeblock, von Hand eingefügt, wieder auskommentiert, wieder eingebaut, sobald der nächste Verdacht aufkommt. Für ein einzelnes, klar eingegrenztes Problem geht das noch — für eine ganze Anwendung mit hunderten Methoden über mehrere Module hinweg ist es schlicht keine Option.

Moderne Sprachen bringen ihre Profiler mit, oft direkt in die IDE integriert. VB6 bringt gar nichts mit, und die IDE selbst wird seit Jahren nicht mehr weiterentwickelt. Also musste ein eigenes, externes Werkzeug her — eines, das sich in ein bestehendes, gewachsenes VB6-Projekt einklinkt, ohne es zu gefährden.

Die Idee: Automatisch instrumentieren, ohne das Original anzufassen

Der VB6 Profiler ist eine eigenständige VB.NET-Anwendung, die ein bestehendes VB6-Projekt (oder eine ganze Projektgruppe) einliest, versteht, welche Methoden und Zeilen es enthält — und bei Bedarf eine temporäre Kopie davon anlegt, in die automatisch Zeitmess-Code eingefügt wird. Das Original-Projekt wird zu keinem Zeitpunkt verändert; alle Eingriffe passieren ausschließlich in einer Wegwerf-Kopie im Temp-Verzeichnis. Man wählt im Konfigurator einfach aus, was überwacht werden soll — ein ganzes Projekt, eine einzelne Datei, eine einzelne Methode oder sogar einzelne Zeilen darin — und der Profiler kümmert sich um den Rest: Kopie anlegen, Profiler-Code einfügen, Zeilen instrumentieren, starten.

Was der Profiler kann

Ganze Projekte und Projektgruppen einlesen

.vbp– und .vbg-Dateien werden vollständig geparst — inklusive aller Module, Formulare und Klassen, deren Methoden (Sub/Function/Property Get/Set/Let) und sogar der einzelnen Quellcodezeilen darin. Der Projektbaum gruppiert nach Dateityp (Formulare, Module, Klassenmodule, UserControls), damit man in großen Projekten nicht in einer flachen Liste versinkt. Gesucht wird über eine UND-verknüpfte Mehrzeilen-Suche mit Spaltenfiltern: jede Zeile ein Kriterium (Projekt, Datei, Methode oder direkt Quellcode-Inhalt), der Suchbereich wächst automatisch mit, und auf Wunsch werden nur bereits überwachte Elemente angezeigt.

Feingranulare Überwachungskonfiguration

Überwachung lässt sich exakt dosieren: ein komplettes Projekt, eine einzelne Datei, eine einzelne Methode — oder, wenn’s genau sein muss, zeilenbasiert bis auf einzelne Codezeilen herunter. Jeder Knoten im Baum hat eine eigene Checkbox. Wer eine ganze Verdächtigen-Liste hat (zum Beispiel aus einer vorherigen Analyse), fügt sie einfach als Text ins Kontextmenü ein — alle genannten Methoden werden auf einen Schlag angehakt, nicht gefundene Namen werden gemeldet.

Zusätzlich wählt man vor dem Start, welche Kennzahlen überhaupt erfasst werden: Die Dauer ist immer dabei, GDI-Handles, USER-Objekte und deren Peak-Werte sind einzeln zuschaltbar. Das ist keine kosmetische Einstellung — eine abgewählte Kennzahl wird beim Generieren des Messcodes gar nicht erst eingebaut, verursacht also auch keinen einzigen API-Aufruf Overhead.

Die komplette Konfiguration wird pro Projekt in schlichten JSON-Dateien gespeichert — diffbar, von Hand reparierbar, beim nächsten Start ist alles wieder genau so eingestellt wie zuletzt.

Automatische Instrumentierung im Detail

Beim Start des Profilers passiert Folgendes automatisch, ohne manuellen Eingriff: Das komplette Projekt (bzw. die Projektgruppe) wird in einen Temp-Ordner kopiert, ein fertiges Profiler-Modul und eine Profiler-Klasse werden in jedes betroffene Projekt eingefügt, und in jeder überwachten Methode werden Start-/Ende-Aufrufe eingebaut — bei zeilenbasierter Überwachung vor jeder einzelnen markierten Zeile. Anschließend übernimmt VB6 selbst das Kompilieren/Ausführen dieser Kopie ganz normal, nur dass sie jetzt „mitschreibt“.

Wenn der Profiler lügen würde: die Select-Case-Falle

Die wichtigste Eigenschaft eines Instrumentierers ist nicht Geschwindigkeit, sondern Unsichtbarkeit: Das vermessene Programm muss sich exakt so verhalten wie ohne Messung. Wie schnell das kippen kann, zeigt eine Falle, in die die zeilenbasierte Instrumentierung anfangs getappt ist. Case-Zeilen wurden nach diesem Muster instrumentiert:

' vorher:
Case 5
' instrumentiert:
Case Not Profiler.StartZeile(4711, True), 5

Sieht harmlos aus: Not True ergibt False, und False matcht ja nichts … außer, der Select-Ausdruck ist selbst 0, False oder ein Leerstring. Dann matcht plötzlich der eingeschleuste Vergleichswert — und die Ausführung landet im falschen Zweig. Bewiesen habe ich das mit einem Mini-Testprojekt, das die Instrumentierung von Hand nachstellt (Select Case 0, Select Case False, Select Case "" — alle drei kippen). Die Konsequenz: Case-Zeilen mit Werteliste werden bewusst nicht instrumentiert — die erste Zeile im Rumpf des Zweigs übernimmt die Messung. Der Informationsverlust ist minimal, die Semantik bleibt garantiert unangetastet.

Präzise Zeitmessung mit Windows-Bordmitteln

Statt ungenauer Sekunden-Ticks kommt QueryPerformanceCounter zum Einsatz — dieselbe High-Resolution-API, auf die auch professionelle Windows-Profiler setzen. Gemessen wird die verstrichene Zeit seit dem jeweils letzten Messpunkt, auf Methoden- wie auf Zeilenebene, in Millisekunden mit Nachkommastellen. Damit der Profiler nicht seine eigenen Kosten mitmisst, wird der Zeitzähler erst nach der kompletten Log-Verwaltung (Handle-Abfragen, Puffer, Datei-I/O) neu gesetzt — die Eigenkosten der Messung fließen in keine gemessene Dauer ein. Und Schleifen werden ehrlich behandelt: Eine Zeile, die 10.000-mal durchlaufen wird, zeigt die Summe aller Durchläufe, nicht nur die letzte Ausführung.

Live mitschauen, während das Programm läuft

Sobald der Profiler gestartet ist, laufen die instrumentierten Methodenaufrufe in Echtzeit ins Tool — mit Dauer, Aufrufzahl und einer klaren Unterscheidung zwischen „läuft noch“ und „abgeschlossen“. Über eine Schnellleiste blendet man ganze Spaltengruppen ein und aus (Dauer, Gruppierung, Aufrufe, Analyse, GDI, USER) und holt sich genau die Kennzahlen ins Bild, die gerade interessieren: prozentualer Anteil, Eigenzeit (ohne Kinder), Spannweite, Kinderzahl, Aufruftiefe, Durchschnittswerte, Peaks. Die zweizeiligen, nach Gruppen pastellig eingefärbten Spaltenköpfe halten das Ganze trotz Spaltenmenge lesbar.

Auch bei extremen Datenmengen bleibt das UI bedienbar: Große Aufruflisten werden asynchron geladen, und gerät die Auswertung bei sehr vielen Logs in Rückstand, zeigt die Statusleiste die aktuelle Auswerterate in Logs/s — man sieht, dass das Tool arbeitet, statt zu rätseln, ob es hängt.

Hotspots: Wo geht die Zeit hin?

Die wichtigste Frage beim Profiling ist selten „was ist alles passiert?“, sondern fast immer „wo steckt die Zeit?“. Die Hotspot-Ansicht beantwortet sie direkt: Gleichartige Aufrufe werden zusammengefasst und als Treemap (Fläche = Zeit) oder Balkenliste dargestellt, mit Summendauer, prozentualem Anteil und einstellbarer Gruppenanzahl von 5 bis 100. Statt sich durch einen Aufrufbaum zu hangeln, sieht man auf einen Blick, welche drei Methoden 90 % des Laufs ausmachen.

Session-Übersicht: Auswerten auch nach dem Stopp

Die Session-Übersicht (Taste F6) zeigt kumulierte Kennzahlen je Methode oder je Zeile über die gesamte Profiling-Session — Aufrufzahl, Eigenzeit, Gesamtdauer, Durchschnitt, langsamster Einzelaufruf, GDI-/USER-Delta. Sortierbar per Spaltenklick, mit Auto-Refresh während der Aufzeichnung, und ein Klick springt direkt an die passende Stelle im Quellcode-Viewer. Das Beste daran: Die Session-Daten überleben das Stoppen des Profilers — man wertet in Ruhe aus, wenn das Programm längst beendet ist. Für alles Weitere gibt es einen CSV-Export (Semikolon-getrennt, UTF-8 mit BOM — Excel öffnet das ohne Murren).

Der KI-Export: Profiling-Ergebnis als fertiger Prompt

Mein persönliches Lieblings-Feature: Die Session-Übersicht kann ihr Ergebnis als KI-Export ausgeben — ein fertiges Markdown-Dokument mit Analyseauftrag, einer Beschreibung der Messmethodik, der Kennzahlen-Übersicht, den heißesten Zeilen (alles ab 1 % Eigenzeit oder 1 ms) und dem Original-Quellcode vor der Instrumentierung. Das Ganze ist so formuliert, dass man es unverändert einer KI zur Optimierung vorlegen kann: „Hier sind die Messwerte, hier ist der Code, finde die Ursache.“ Aus einer Profiling-Session wird in Sekunden ein vollständiger, kontextreicher Optimierungsauftrag.

Eingebetteter Quellcode-Viewer

Wählt man einen Aufruf aus, zeigt der Quellcode-Viewer sofort die passende Stelle im Code — wahlweise eingebettet im Hauptfenster, in einem eigenen, immer sichtbaren Extra-Fenster, oder ganz ausgeblendet, wenn gerade nur die reinen Zahlen interessieren. So lässt sich eine auffällig lange Dauer sofort mit dem tatsächlichen Code dahinter abgleichen, ohne zwischen VB6-IDE und Profiler hin- und herzuwechseln.

Ressourcen im Blick, nicht nur Zeit

Zusätzlich zur Laufzeit erfasst der Profiler auf Wunsch GDI-Handles, USER-Objekte und deren Peak-Werte — an jedem Messpunkt, also bis auf Zeilenebene herunter. Handle-Lecks, ein klassisches und oft unsichtbares Problem älterer Windows-Anwendungen, werden damit nicht nur sichtbar, sondern zeilengenau lokalisierbar — statt erst nach Stunden im Dauerbetrieb als Rätsel aufzufallen. Wie oben beschrieben ist jede dieser Kennzahlen einzeln abschaltbar und kostet abgeschaltet exakt nichts. Das Log-Dateiformat ist dafür selbstbeschreibend: Jede Log-Datei stellt ihre Satzlänge voran, sodass ältere Aufzeichnungen auch nach künftigen Format-Erweiterungen ladbar bleiben.

Fehler nicht verpassen

Unbehandelte Exceptions der profilierten Anwendung landen automatisch in einer eigenen Fehlerliste — inzwischen inklusive ThreadException und UnobservedTaskException, mit farbig dargestellter, vollständiger Exception-Kette samt Stacktrace. Die Liste überlebt Projektwechsel und Neustart, und alles wird zusätzlich als NDJSON-Log auf Platte gesichert. Praktisch, wenn ein Absturz genau während einer Profiling-Sitzung passiert und man sonst Gefahr liefe, ihn im Log-Rauschen zu übersehen.

Persistenz & schneller Workflow

Konfiguration in JSON, Fenster- und Splitterpositionen (auch verschachtelt) werden gespeichert und wiederhergestellt — beim nächsten Start ist alles wieder genau so wie zuletzt. Dazu ein auf Tastatur-Bedienung ausgelegter Workflow für den täglichen Einsatz:

TasteAktion
Strg+FFokus ins Suchfeld (Projekt/Datei)
Strg+OVB6-Projekt/Konfiguration auswählen
F3Projekt im Konfigurator in VB6 öffnen
F4Profiler starten/neu starten
F5Instrumentierte Projektkopie in VB6 öffnen
F6Session-Übersicht öffnen
EscProfiler stoppen
EntfMethode von der Überwachung ausschließen

Qualitätssicherung: der Hindernislauf

Die Select-Case-Falle hat eine Konsequenz hinterlassen: Der Profiler hat ein eigenes Folter-Testprojekt bekommen. „Hindernislauf“ ist ein absichtlich fieses VB6-Programm, das so ziemlich alles enthält, woran eine automatische Instrumentierung scheitern kann:

  • GoTo vorwärts und rückwärts über tote Codeblöcke, GoSub/Return, On x GoSub-Verteiler
  • nummerierte Zeilen mit Erl-Auswertung und allen drei Resume-Varianten (Resume, Resume Next, Resume <Ziel>)
  • Einzeiler-Ifs mit Doppelpunkt und Else, IIf mit Seiteneffekten, Fortsetzungszeilen-Monster, nicht übersetzte #If-Zweige
  • die Select-Case-Semantik-Fallen (Case 0, Case False, Case "") als Dauer-Regression
  • enge Schleifen mit 100.000 Durchläufen (Overhead-Messung), Sleep, Busy-Wait und DoEvents-Warten — inklusive eines zuschaltbaren „Störfeuer-Timers“, der alle 250 ms mitten in laufende Messungen hineinfunkt
  • exponentielle und wechselseitige Rekursion (Stress für Aufruftiefe und Flush-Drosselung)
  • Objektketten mit eigenem Class_Initialize/Class_Terminate (kollidiert bewusst mit der Terminate-basierten Ende-Erkennung des Profilers) und CallByName-Aufrufe am Parser vorbei

Der Clou: Jeder Parcours protokolliert seine Ergebnisse, und die Soll-Werte sind dokumentiert. Läuft das instrumentierte Projekt und im Protokoll steht trotzdem Summe = 61, Runden = 3 und 0 -> null — dann hat die Instrumentierung die Semantik nicht angefasst. Ein Lackmustest für jede künftige Änderung am Instrumentierungs-Code.

Unter der Haube

Damit das Ganze auch bei intensiver Nutzung performant bleibt, steckt einiges an Feinschliff in der Umsetzung: Logs werden im Speicher gepuffert (bis zu 10.000 Einträge) und blockweise geschrieben — nur der tatsächlich befüllte Teil des Puffers landet auf der Platte, nicht ein aufgeblähtes Fixarray. Am Ende der äußersten profilierten Methode wird sofort geschrieben (Live-Ansicht!), verschachtelte Methodenenden werden dagegen zeitgedrosselt gesammelt — sonst entstünde für jeden Untermethodenaufruf eine eigene Log-Datei.

Die Verarbeitung der eingehenden Log-Daten läuft in einem mehrstufigen Thread-Modell: Ein Thread liest die Logs vom Dateisystem, ein zweiter ordnet sie den passenden Methoden und Zeilen zu und aktualisiert die laufenden Aufruflisten, ein dritter erkennt abgeschlossene Aufrufe und meldet sie ans UI — sodass die Anzeige auch bei sehr vielen Aufrufen pro Sekunde flüssig bleibt, ohne die Oberfläche zu blockieren.

Für die Oberfläche kommen ObjectListView für die performanten Baum-/Listenansichten und FastColoredTextBox für die Quellcode-Anzeige mit Syntax-Highlighting zum Einsatz. Ein eigenes VB6-Datenmodell (Projekt → Datei → Methode → Zeile) bildet die komplette Struktur eines VB6-Projekts inklusive der historischen Windows-1252-Zeichenkodierung sauber ab — Umlaute in Bezeichnern wie „Rückgabe“ bleiben dabei korrekt erhalten. Die Konfiguration liegt in schlanken JSON-Dateien, ganz ohne Datenbank-Unterbau.

Fazit

Was mit „ich will einfach mal wissen, welche Methode so lange braucht“ begann, ist inzwischen ein vollwertiges Profiling-Werkzeug für eine Sprache, die nie eines bekommen sollte — mit Live-Auswertung, zeilengenauer Zeitmessung, Hotspot-Analyse, Session-Auswertung und einem KI-Export, der Messwerte und Quellcode direkt zum Optimierungsauftrag verschnürt. Und es ist an seinem eigenen Einsatz gereift: Jede im Alltag gefundene Falle — allen voran die Select-Case-Semantik — hat einen Test im Hindernislauf hinterlassen, damit sie nie wiederkommt. Man muss Legacy-Technologie nicht ersetzen, um sie endlich verstehbar zu machen. Man muss ihr nur die richtigen Werkzeuge bauen.