Autorenarchiv

Zeitmessung mit Spring AOP

Donnerstag, 24. Juni 2010

Trifft man bei der Entwicklung einer Anwendung auf Performanceprobleme, reicht es meist aus, die verantwortlichen Codebereiche aufzuspüren, um dann den Fehler/das Problem zu beheben. Jedoch gestaltet sich das Auffinden der betroffenen Codestellen meist als schwierig oder langwierig. In jede verdächtige Methode Log-Ausgaben mit der Zeitmessung zu schreiben, wäre unsauber. Mit Spring ist die einfachste Lösung, einen Interceptor zu konfigurieren, der die Methodenaufrufe unterbricht und die Zeitmessung ins Log schreibt.

Als erstes definieren wir einen Advice (Interceptor-Bean), die Implementierung bringt Spring schon mit.

<bean id="timingLogger" 
class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor" />

Als zweiten Schritt können wir mit der AOP Konfiguration festlegen, welche Methodenaufrufe protokolliert werden sollen, damit die Log-Datei übersichtlich bleibt. Folgend zum Beispiel alle public-Methoden der Spring-Beans im Paket service und allen Unterpaketen:

<aop:config>
   <aop:pointcut id="services" expression="execution(* de.myApp.service..*.*(..))" />
   <aop:advisor advice-ref="timingLogger" pointcut-ref="services" />
</aop:config>

Für das Definieren der korrekten pointcut-expression hilft die ausführliche Spring Dokumentation (7.2.3 Declaring a pointcut, Version 3).
Damit das Ergebnis in den Logs auftaucht, sollte für diesen Interceptor der Log-Level auf TRACE stehen:

log4j.logger.org.springframework.aop.interceptor.PerformanceMonitorInterceptor=TRACE

Die Ausgabe sieht dann zum Beispiel so aus:

2010-06-24 09:06:53,109 TRACE PerformanceMonitorInterceptor:64 - StopWatch 'de.myApp.service.MyService.methodName': running time (millis) = 1813

Für eine erste Analyse ist diesen Vorgehen hilfreich, will man jedoch aggregierte Werte über einen Zeitraum bekommen, bietet sich zum Beispiel Perf4J an. Mit diesem Tool lassen sich aggregierte Messwerte (Min, Max, Durchschnitt, Anzahl) in eine extra Logdatei schreiben.

Es besteht aber auch die Möglichkeit einer einfachen eigenen Implementierung:

public class TimingLoggingInterceptor implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        StopWatch sw = new StopWatch();
        sw.start();
        Object returnValue = invocation.proceed();
        sw.stop();
        System.out.println(
            invocation.getClass().getSimpleName() + "#" + 
            invocation.getMethod().getName() + ": " +
            sw.getTotalTimeMillis());
        return returnValue;
    }
}

Nun muss nur noch der Wert der Spring-StopWatch gespeichert werden. Diese eigene Lösung ist besonders interessant, wenn nur unter bestimmten Bedingungen die Werte geloggt werden sollen, zum Beispiel wenn die Laufzeit eine definierte Obergrenze überschreitet.


Jan Kuenstler


Datum in Hibernate: Der Wolf(Timestamp) im Schafspelz(Date)

Freitag, 22. Januar 2010

Benutzt man Hibernate als OR-Mapper und will ein java.util.Date (Datum und Uhrzeit) speichern, ist Vorsicht geboten. Da java.sql.Timestamp von java.util.Date erbt, und der Typ in der Datenbank i.d.R. timestamp ist, bekommt man von Hibernate gegebenenfalls ein Timestamp-Objekt, und nicht ein Date-Objekt. Eigentlich kein Problem, doch die Klasse Timestamp ist …kaputt! Auszug aus dem Javadoc von Timestamp:

The Timestamp.equals(Object) method never returns true when passed a value of type java.util.Date because the nanos component of a date is unknown.

Will man zwei Dates vergleichen und befindet sich hinter der ersten Referenz ein Timestamp-Objekt, schlägt equals fehl.

Date d = new Date();
Date ts = new Timestamp(d.getTime());
System.out.println(ts.equals(d));    //returns false

Wie soll ich also zwei Dates miteinander vergleichen? Es wird noch schlimmer:

System.out.println(ts.compareTo(d)); // return 0
System.out.println(d.compareTo(ts)); // returns 1

Auch die compareTo-Methode ist fehlerhaft liefert ein anderes Ergebnis als erwartet. Schaut man sich den Konstruktor von Timstamp an, kommt man aus dem Staunen Entsetzen nicht mehr heraus. Die Klasse Date speichert das Datum millisekundengenau in dem Feld fastTime. Die Klasse Timestamp hingegen speichert das Datum sekundengenau in dem Feld fastTime und die Milli- und Nanosekunden im Feld nanos. Warum dies so ist, und nicht nur die Nanosekunden im Feld nanos gespeichert werden, ist mir ein Rätsel.

Will man nun ein Date und einen Timestamp miteinander vergleichen (z.B. auch mit before() und after()) schlägt dies fehl. Die compareTo-Methode von Date vergleicht die Millisekunden aus dem Feld fastTime, beim Timestamp-Objekt ist dies nur sekundengenau, die Millisekunden sind im Feld nanos gespeichert, und werden nicht berücksichtigt.

Dieses Problem tritt häufig in Verbindung mit Hibernate auf, die Schuld liegt aber bei der Standard Java-API. Allerdings lässt sich das Problem umgehen, indem man Hierbernate mit einem UserType zwingt, nur Objekte vom Typ Date zu liefern.

public class DateTimeType implements UserType {
    private static final int[] SQL_TYPES = new int[] {Types.TIMESTAMP};
    public DateTimeType() {
        super();
    }
 
    @SuppressWarnings("unchecked")
    public Class returnedClass() {
        return java.util.Date.class;
    }
 
    public int[] sqlTypes() {
        return SQL_TYPES;
    }
 
    public boolean equals(final Object x, final Object y) {
        if (x == y) {
            return true;
        }
        if (x == null || y == null) {
            return false;
        }
        Date xDate = (Date) x;
        Date yDate = (Date) y;
        return xDate.equals(yDate);
    }
 
    public Object nullSafeGet(final ResultSet rs, final String[] names, final Object owner) throws SQLException {
        // extract Timestamp from the result set
        Timestamp timestamp = (Timestamp) Hibernate.TIMESTAMP.nullSafeGet(rs, names[0]);
        // return the value as a java.util.Date (dropping the nanoseconds)
        if (timestamp == null) {
            return null;
        } else {
            return new Date(timestamp.getTime());
        }
    }
 
    public void nullSafeSet(final PreparedStatement st, final Object value, final int index) throws SQLException {
        // handle the NULL special case immediately
        if (value == null) {
            st.setTimestamp(index, null);
            return;
        }
        // make sure the received value is of the right type
        if (!Date.class.isAssignableFrom(value.getClass())) {
            throw new IllegalArgumentException("Received value is not a [java.util.Date] but [" + value.getClass() + "]");
        }
        // set the value into the resultset
        Timestamp tstamp = null;
        if (value instanceof Timestamp) {
            tstamp = (Timestamp) value;
        } else {
            tstamp = new Timestamp(((Date) value).getTime());
        }
        st.setTimestamp(index, tstamp);
    }
 
    public Object deepCopy(final Object value) {
        if (value == null) {
            return null;
        } else {
            return ((Date) value).clone();
        }
    }
 
    public boolean isMutable() {
        return true;
    }
 
    @Override
    public Object assemble(final Serializable cached, final Object owner) {
        return deepCopy(cached);
    }
 
    @Override
    public Serializable disassemble(final Object value) {
        return (Date) value;
    }
 
    @Override
    public int hashCode(final Object x) {
        return x.hashCode();
    }
 
    @Override
    public Object replace(final Object original, final Object target, final Object owner) {
        return deepCopy(original);
    }
}

Möglich ist eine Konfiguration auf Packet-Ebene, indem man eine package-info.java erstellt. Folgend die Konfiguration mit Annotationen auf Klassenebene:

@TypeDefs( {@TypeDef(name = "dateTimeType", typeClass = DateTimeType.class)})
public class MyBusinessObject {
 
    @Type(type = "dateTimeType")
    @Column(name = "time")
    private Date time;
    ...
}

Eventuell wird die freie Joda Time API in Java 1.7 die alten Datum und Kalendarklassen ersetzten. Bis dahin kann man mit diesem UserType sicher sein, dass Hibernate nur Date-Objekte zur Verfügung stellt, und somit kann man wieder equals und compareTo ohne Bedenken verwenden.
Siehe auch


Jan Kuenstler


Animationen mit SWT

Mittwoch, 07. Oktober 2009

In SWT(JFace) gibt es momentan keine Hilfestellung bei der Erstellung von Animationen. Selbst die Darstellung von animierten GIF-Bildernist etwas umständlich (Beispiel). Eclipse 4 wird voraussichtlich eine eigene API für Animationen mitbringen, bis dahin lohnt ein Blick auf das SWT Animation Toolkit von Nicolas Richeton, dass Zugang zum Nebula Projekt gefunden hat.

Es bringt eine Reihe von Effekten mit, wie zum Beispiel SmooothScrolling, Animierte Größenänderung und Bewegung von SWT-Controls sowie Ein- und Ausblenden von Fenstern. Allerdings keine Animationen für Bilder (SWT-Image). Folgend nun eine Erweiterung dieser Effekte um eine Pulsieren-Animation eines Icons. Die Transparenz des Bildes wird stufenweise verringert (und wieder erhöht). Folgend die Methode zum Erzeugen der Einzelbilder für die Animation:

private Image[] createImageArray(final Image origImage) {
    Image[] images = new Image[10];
    Image previous = origImage;
    for (int i = 0; i &lt; images.length; i++) {
        ImageData data = previous.getImageData();
 
        int[] lineData = new int[data.width];
        for (int y = 0; y &lt; data.height; y++) {
            data.getPixels(0, y, data.width, lineData, 0);
            // Analyze each pixel value in the line
            for (int x = 0; x &lt; lineData.length; x++) {
                int pixelValue = lineData[x];
                if (pixelValue == data.transparentPixel) {
                    continue;
                }
                int alpha = data.getAlpha(x, y) - i * 25;
                if (alpha &lt; 0) {
                    alpha = 0;
                } else if (alpha &gt; 255) {
                    alpha = 255;
                }
                data.setAlpha(x, y, alpha);
            }
        }
        images[i] = new Image(Display.getCurrent(), data);
        previous = images[i];
    }
    return images;
}

Es muss für jedes Einzelbild die Transparenz aller Pixel verändert werden, da SWT keinen passenden vordefinierten Filter mitbringt. Hat man die Einzelbilder der Animation erstellt, müssen diese nur noch zum richtigen Zeitpunkt dargestellt werden. Folgend die Klasse Pulse die von AbstractEffect, eine Klasse aus dem Animation Toolkit, erbt:

public class Pulse extends AbstractEffect {
 
    private final RestoreImage restoreImage = new RestoreImage();
    private final ImageButton imageButton;
    private final Image origImage;
    private final Image[] images;
 
    public Pulse(final ImageButton imageButton, final long lengthMilli) {
        super(lengthMilli, new LinearInOut(), null, null);
        origImage = imageButton.getImage();
        this.imageButton = imageButton;
        images = createImageArray(origImage);
 
        easingFunction.init(0, 1, (int) lengthMilli);
    }
 
    @Override
    public void applyEffect(final long currentTime) {
        double value = easingFunction.getValue((int) currentTime);
 
        int img = 0;
        if (value &lt; 0.5) {
            img = Math.min((int) (value * 20), 9);
        } else {
            img = Math.min((int) (10 - (value * 20 - 10)), 9);
        }
 
        if (!imageButton.isDisposed()) {
            imageButton.updateImage(images[img]);
        }
    }
}

Das ganze kann wie folgt gestartet werden:

AnimationRunner runner = new AnimationRunner(10);
runner.runEffect(new Pulse(imageButton, 1000));

Die Animation wird nun mit 10 Bildern pro Sekunde (fps) ausgeführt. Die Methode applyEffect wird dabei 10 mal ausgeführt: 10fps * 1000ms. Mögliche Verbesserungen:

  • Die Anzahl der erzeugten SWT-Images automatisch an die fps anpassen.
  • Das aktuelle Bild der Animation durch eine Sinus-Funktion bestimmen. Durch ein Erhöhen der Frequenz kann das Pulsieren mehrfach innerhalb eines Aufrufes ausgeführt werden.
  • Die erzeugten Resourcen (SWT-Images) müssen am Ende wieder freigegeben werden (dispose()) werden: runnableOnStop und runnableOnCancel der Oberklasse verwenden.

Jan Kuenstler


Enspannung für die Augen

Freitag, 03. April 2009

eye_iris_small

Für die Arbeit am Bildschirm sind neben den eigenen Händen die Augen das Wichtigste. Mehr als 80 Prozent unserer Eindrücke werden über das Auge wahrgenommen. 80 Prozent der Menschen, die täglich länger als drei Stunden vor dem Rechner sitzen, klagen über Beschwerden, dabei betreffen die Hälfte der Beschwerden den Sehapparat. Beim “natürlichen Sehen” schweift das Auge, es verharrt nicht lange am selben Punkt.  Das Auge ist so angelegt, dass es schnell der Umgebung(Distanz, Kontrast und Helligkeit) angepasst werden kann. Bei der Bildschirmarbeit hingegen verharrt unser Blick immer in der gleichen Richtung und immer mit dem gleichen Abstand. Zudem Ändern sich die Kontrastverhältnisse bei der normalen Arbeit wenig. Diese Einseitige Belastung lässt das Auge schnell Ermüden, Symptome sind trockene Augen, Brennen oder allgemeine Ermüdung und nachlassende Konzentration. Um dem engegenzuwirken stelle ich nun ein paar einfache Übungen vor:

  • Aus dem Fenster schau’n. Die Weitsicht ist eine willkommende Abwechslung.
  • Den äußeren Sichtbereich der Augen trainieren. Auf verschiedenen Distanzen sollte versucht werden Objekte am äußeren Rand des Sichtfeldes wahrzunehmen.
  • Die Augen für mehrere Sekunden schliessen und langsam wieder öffnen. Idealerweise vorher ein Ziel in der Ferne fokussieren.
  • Häufiger und länger Blinzeln. Durch konzentriertes Arbeiten verringert sich die Lidschlagfreqeunz (Dank geht hier an Alexander für dieses wunderschöne Synonym) und das Auge trocknet aus.
  • Leichtes massieren des Augapfels bei geschlossenem Lid
  • Gesichtsgymnastik. Grimassen, Nase rümpfen, Augen aufreißen entspannen das gesamte Gesicht.

Generell gilt: Kurze, häufige Pausen sind besser als einige lange. Zudem sollte neben den Augen auch Rücken, Schultern und Nacken durch kurze Auflockerungen entspannt werden. Wenn es die Situation ermöglicht, wirkt ein kurzer (maximal 30 min) Mittagsschlaf Wunder.

Also, bleibt gesund!

Euer Blogfrosch


Jan Kuenstler