Auf Trab

Wer die Engpässe in seinem Java-Programm finden will, kann sich unter anderem einer kostenlosen Variante von JProbe bedienen.

vorlesen Druckansicht 33 Kommentare lesen
Lesezeit: 5 Min.
Von
  • Michael K. Klemke
Inhaltsverzeichnis

Nach längerer Entwicklungsphase ist es endlich soweit: Das Java-Programm kann in Test gehen. Leider fördert der zutage, dass einige Funktionen zu langsam sind oder träge reagieren. Da heutzutage mehrere Entwickler an einem Projekt arbeiten, lässt sich schwer vorhersagen, wo genau der gefürchtete Flaschenhals sitzt. JProbe hilft dabei, indem es die Ausführungszeiten einzelner Methoden misst und dem Entwickler hierüber detailliert Auskunft gibt.

JProbe gibt es in zwei Versionen: eine kostenlose, abgespeckte und die komplette kommerzielle. Diese bietet neben dem reinen Profiler der Gratisvariante einen Speicher-Debugger zum Auffinden von Lecks und einen Coverage-Debugger, der prĂĽft, welche Codezeilen in Tests geprĂĽft wurden. Im Folgenden geht es lediglich um den Profiler, der ausschlieĂźlich fĂĽr Linux und Windows verfĂĽgbar ist. Er steht samt LizenzschlĂĽssel unter www. quest.com/jprobe/profiler-freeware.asp zum Download bereit. Der SchlĂĽssel gilt leider nur drei bis sechs Monate. Danach muss man einen neuen herunterladen.

Mehr Infos

iX-Wertung

[+] einfach zu benutzen
[+] kostenlos
[-] läuft nicht mit Java 5
[-] keine automatisierten Messungen möglich

Dass es den kostenlosen Profiler nur für diese beiden Plattformen gibt, lässt sich verschmerzen, wenn man die zu untersuchende Applikation unter Windows oder Linux zum Laufen bekommt. Die Details könnten bei denselben Tests unter anderen Betriebssystemen oder Prozessoren vielleicht etwas anders aussehen, dennoch geben die Testergebnisse Aufschluss über die kritischen Teile.

Zur Performance-Messung simuliert JProbes „JPLauncher“ gegenüber der zu untersuchenden Anwendung eine Java Virtual Machine (JVM). Alle API-Aufrufe reicht er an die tatsächliche JVM weiter, wodurch er die Ausführungszeiten für jede Methode messen kann. Die Ergebnisse stellt er per TCP/IP auf Port 52991 dem Auswertungsprogramm zur Verfügung.

Zur Veranschaulichung dient ein Java-Programm, dessen Auszüge Listing 1 zeigt. Es fragt während des Fensteraufbaus die Liste von Personalnummern aus einer Datenbank ab und zeigt sie in einer Combobox an. Nach Auswahl einer Nummer und Klick auf „Abfrage starten“ holt es die Personendaten aus der Datenbank. Zum Test diente die HSQLDB, die genutzten Daten stammen aus dessen Testdatensatz.

Drei Funktionen bilden im Wesentlichen das Programm:

  • Aufbau des JFrames und FĂĽllen mit Inhalt;
  • Abfrage der Datenbank nach den Personalnummern;
  • Holen der Detaildaten in actionPerformed() nach Klick auf den einzigen Knopf.

JProbes Callgraph zeigt die AusfĂĽhrungszeitenaller Methoden an.

Damit man dieses Programm analysieren kann, legt man nach dem Start von JProbe mit „Session/New J2SE Settings“ eine neue Session an. Der sich nach „Manage Configurations“ und „Add“ öffnende Dialog nimmt die nötigen Parameter auf: zu startende Klasse, Arbeitsverzeichnis des Programms, Klassenpfad und Pfad zur JVM. Die freie Version von JProbe unterstützt noch kein Java 5, man muss also mit dem JDK 1.4 arbeiten.

Nun kann man verschiedene Testszenarien durchspielen, während JProbe mit dem beschriebenen Verfahren die Ausführungszeiten aller Methoden misst. Nach dessen Ende stellt es sie in einem Graphen dar, der für das Beispiel etwa so aussieht wie die Abbildung oben.

In den Knoten des Graphen steht der Name der ausgeführten Methode und die Summe der Zeiten, die das Programm in ihr und in den von ihr aufgerufenen Methoden verbrachte. So brauchte main() im Beispiel 2210 ms, von denen buildFrontend mit 2193 ms den Löwenanteil verbriet. Allerdings kostet nicht nur der Fensteraufbau Zeit: Mit 963 ms dauerte der Verbindungsaufbau zur Datenbank mit DriverManager.getConnection() fast ebenso lange.

Ein Doppelklick auf einen der Knoten im Graphen liefert detaillierte Informationen ĂĽber die jeweilige Methode: Von welchen sie wie oft aufgerufen wurde und welche sie selbst wiederum verwendete. Eine Liste unter dem Call-Graphen fĂĽhrt nochmals alle AusfĂĽhrungszeiten tabellarisch auf (s. Aufmacher). Hier kann man nach verschiedenen Kriterien suchen, wie Anzahl der Aufrufe oder durchschnittliche AusfĂĽhrungszeit der Methoden.

Eine Besonderheit stellt die Analyse einer J2EE-Applikation dar. Die sie ausführenden Applikationsserver benötigen in der Regel ein spezielles Skript. Oder sie brauchen andere, weitergehende Informationen zum Start, die sich von Hersteller zu Hersteller unterscheiden. JProbe ist auf diese speziellen Anforderungen vorbereitet und kann die meisten Applikationsserver für Performancemessungen per GUI konfigurieren.

Dort erledigt man abhängig vom Server individuelle Einstellungen. Dazu gehören der Servertyp samt Version, das Server-Verzeichnis und das Startup-Skript. Wichtig ist, nach Konfiguration des Servers die Filtereinstellungen vorzunehmen. Sie legen die zu messenden Packages fest. Dadurch lassen sich die nicht beeinflussbaren Packages des Applikationsservers von der Messung einfach ausschließen.

Michael K. Klemke
ist Diplom-Informatiker der Medizin und arbeitet als Entwickler fĂĽr Webapplikationen in einem Dienstleistungsunternehmen im Raum Rhein-Neckar.

[1] Michael Tamm; Java-Profiling; Wo es klemmt; Engpässe in Java-Programmen finden; iX 4/2004, S. 42

Mehr Infos

Listing 1: Testprogramm fĂĽr JProbe, AuszĂĽge

package de.heise.mklemke.jprobe;

import java.awt.Container;

...
import javax.swing.JButton;
...

public class DBSelect implements ActionListener {
JComboBox cbNummer = null;
JTextField tfName = null;
JTextField tfStreet = null;
JTextField tfCity = null;
private JFrame frame = null;

public final static String SQL_CONNECTION =
"jdbc:hsqldb:file:db/namen";
public final static String SQL_SELECT_BY_ID =
"SELECT id, firstname, lastname, \
street, city FROM CUSTOMER where id = ?";
public final static String SQL_COUNT_ITEMS =
"SELECT count(*) FROM CUSTOMER";
public final static String SQL_SELECT_ALL =
"SELECT id FROM CUSTOMER";

public DBSelect() {

} // DBSelect()

public void buildFrontend() {
frame = new JFrame();

Container contentPane = frame.getContentPane();
contentPane.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.LINE_START;
gbc.insets = new Insets(3, 3, 3, 3);

JLabel label = new JLabel("Personalnummer: ");
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridy = 0;
contentPane.add(label, gbc);

Object[] listOfPersonals = getPersonalNumbers();

cbNummer = new JComboBox(listOfPersonals);
cbNummer.setEditable(false);
gbc.gridx = 1;
contentPane.add(cbNummer, gbc);

JButton button = new JButton("Abfrage starten");
gbc.gridx = 2;
gbc.gridy = 0;
contentPane.add(button, gbc);
button.addActionListener(this);

label = new JLabel("Name: ");
gbc.gridx = 0;
gbc.gridy = 1;
contentPane.add(label, gbc);

tfName = new JTextField(40);
tfName.setEditable(false);
gbc.gridx = 1;
gbc.gridy = 1;
gbc.gridwidth = 2;
contentPane.add(tfName, gbc);

...

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
} // public buildFrontend()

private String[] getPersonalNumbers() {
...
Connection c = DriverManager.getConnection(SQL_CONNECTION, "sa", "");

PreparedStatement psCount = c.prepareStatement(SQL_COUNT_ITEMS);
ResultSet rs = psCount.executeQuery();
rs.next();
int numberOfRows = rs.getInt(1);

PreparedStatement ps = c.prepareStatement(SQL_SELECT_ALL);
rs = ps.executeQuery();
String s[] = new String[numberOfRows];
for (int i = 0; i < numberOfRows; i++) {
rs.next();
s[i] = rs.getString("id");
} // for (int i = 0; i < numberOfRows; i++)
c.close();
return s;
...
} // private Object[] getPersonalNumbers()

public void showFrontend() {
frame.setVisible(true);
} // public void showFrontend()

public static void main(String[] args) {
DBSelect dbSelect = new DBSelect();
dbSelect.buildFrontend();
dbSelect.showFrontend();
} // public static void main(String[])

public void actionPerformed(ActionEvent arg0) {
String item = (String) cbNummer.getSelectedItem();
...
Connection c = DriverManager.getConnection(SQL_CONNECTION, "sa", "");
PreparedStatement psCount = c.prepareStatement(SQL_SELECT_BY_ID);
psCount.setString(1, item);

ResultSet rs = psCount.executeQuery();
rs.next();
String firstname = rs.getString("FirstName");
String lastname = rs.getString("LastName");
tfName.setText(firstname + " " + lastname);
...
c.close();
...
} // public void actionPerformed(ActionEvent)

} // public class DBSelect

(ck)