Speed up listener notification

Discover the fastest way to notify event listeners defined by the JavaBeans 1.01 specification

1 2 3 Page 2
Page 2 of 3

Source2is implemented so that there is no clone in Source2.notifyModelChanged. Again, I will not repeat the source code here, since it is essentially the same as the code above in the basic technique section. The average time for 50,000 notifyModelChangedcalls was 0.753 seconds. Voilà -- Source2 is around 3.5 times faster than Source1!

Actually, the first implementation of Source2 used an Iterator to retrieve the listeners and produced an average time of 1.42 seconds. This was only about twice as fast as Source1. The slower time makes sense because each Iteratorcreated by the Vector is another short-lived object that has to be garbage collected.

Swing

After conducting the previous tests, I began thinking about Swing's event notification performance. After digging though the Swing source code for a while, I found out that Swing uses javax.swing.event.EventListenerListto manage event listeners.

Source3

I designed Source3 to use Swing's EventListenerListto manage Source3's listeners. Interestingly, there was a slight improvement over Source2. It turns out that EventListenerListuses an array to store the listeners. You can retrieve elements from arrays a little more quickly (see "Java Performance Programming, Part 2: The Cost of Casting" in Resources). Source3's average time was 0.566 seconds.

EventListenerListplaces listeners of different types into a single array. Every even element in the array is the class of the listener, and every odd element is the listener interface itself. Therefore, when notifying listeners, the notifyModelChanged()method must check each registered listener to verify that it is a ModelChangedListener before calling modelChanged(). I wanted to see what would happen if the listener list contained several listeners of the wrong type in addition to one of the correct type; thus, I added seven adversarial listeners to the list that would have to be skipped before reaching the ModelChangeListener. With the adversaries in place, Source3takes an average of 0.6186 seconds.

import javax.swing.event.*; import java.awt.event.*; import java.util.*;

/** * Use Swing utility class with an adversary */ public class Source3 implements TestSource { private EventListenerList m_listeners = new EventListenerList(); private boolean m_adversary = true; private static int m_fireCount = 0; private static int m_instCount = 0; private int m_instNumber;

public Source3() { m_instNumber = ++m_instCount; } public void addModelChangedListener(ModelChangedListener l) { if (Trace.isOn()) { Trace.out.println("addModelChangedListener l=" + l.getClass()); } m_listeners.add(ModelChangedListener.class, l); if (m_adversary) { ActionListener ladverse = new ActionListener() { public void actionPerformed(ActionEvent e) { } }; m_listeners.add(ActionListener.class, ladverse); m_listeners.add(ActionListener.class, ladverse); m_listeners.add(ActionListener.class, ladverse); m_listeners.add(ActionListener.class, ladverse); m_listeners.add(ActionListener.class, ladverse); m_listeners.add(ActionListener.class, ladverse); m_listeners.add(ActionListener.class, ladverse); } } public void removeModelChangedListener(ModelChangedListener l) { if (Trace.isOn()) { Trace.out.println("removeModelChangedListener l=" + l); } m_listeners.remove(ModelChangedListener.class, l); } public void notifyModelChanged() { if (Trace.isOn()) { Trace.out.println("notifyModelChanged size=" + m_listeners.getListenerCount()); } m_fireCount++; // Makeup a EventObject EventObject event = new EventObject(this);

Object [] listeners = m_listeners.getListenerList(); for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==ModelChangedListener.class) { if (Trace.isOn()) { Trace.out.println("fired when i="+i); } ((ModelChangedListener)listeners[i+1]) .modelChanged(event); } } } public int getFireCount() { return m_fireCount; }

public int getInstanceNumber() { return m_instNumber; } }

Source4

Determined to have the fastest listener notification on the block, I revamped Source2 to use arrays as listener storage and called this test Source4. Each time a listener is added, a new array is created. The old listeners are copied into the new array and the added listener is placed at the end of the new array. Finally, the new array is stored in the listenerListfield. Since I only needed add()for the test, I left remove() as an exercise for the reader. Source4's average time was 0.453 seconds -- smoking!

import java.util.*;

/** * Like clone vector at add/remove but uses an array * like the swing event listener list. */ public class Source4 implements TestSource { private ModelChangedListener[] m_listeners = new ModelChangedListener[0]; private static int m_fireCount = 0; private static int m_instCount = 0; private int m_instNumber;

public Source4() { m_instNumber = ++m_instCount; }

public void addModelChangedListener(ModelChangedListener l) { if (Trace.isOn()) { Trace.out.println("addModelChangedListener l=" + l.getClass()); } synchronized (this) { int length = m_listeners.length; ModelChangedListener[] els = new ModelChangedListener[length + 1]; System.arraycopy(m_listeners, 0, els, 0, length); els[length] = l; m_listeners = els; } } public void notifyModelChanged() { if (Trace.isOn()) { Trace.out.println("notifyModelChanged size=" + m_listeners.length); } m_fireCount++; // Makeup a EventObject EventObject event = new EventObject(this);

int length = m_listeners.length; for (int i = 0; i < length; i++) { m_listeners[i].modelChanged(event); } } public int getFireCount() { return m_fireCount; }

public int getInstanceNumber() { return m_instNumber; } }

InfoBus

Next, I wanted to see how well javax.infobus.DataItemChangeManagerSupportperformed. Since DataItemChangeManagerSupport manages DataItemChangeListener, I had to build an adapter to adapt from DataItemChangeListener to a ModelChangedListenerso that I could use my test harness.

Source5

The addModelChangedListener() method creates an instance of the adapter and adds it as a listener to DataItemChangeManagerSupport. The notifyModelChanged()method simply calls DataItemChangeManagerSupport.fireItemValueChanged. Here is the source code:

import javax.infobus.*;

/** * Uses infobus DataItemChangeManagerSupport by adapting * the listeners */ public class Source5 implements TestSource { private DataItemChangeManagerSupport m_listeners = new DataItemChangeManagerSupport(this); private static int m_fireCount = 0; private static int m_instCount = 0; private int m_instNumber;

public Source5() { m_instNumber = ++m_instCount; }

class ModelChangedListenerAdapter extends DataItemChangeListenerSupport implements DataItemChangeListener { private ModelChangedListener m_l; ModelChangedListenerAdapter(ModelChangedListener l) { m_l = l; } public void dataItemValueChanged(DataItemValueChangedEvent event) { m_l.modelChanged(event); } } public void addModelChangedListener(ModelChangedListener l) { if (Trace.isOn()) { Trace.out.println("addModelChangedListener l=" + l.getClass()); } m_listeners.addDataItemChangeListener( new ModelChangedListenerAdapter(l)); } public void notifyModelChanged() { if (Trace.isOn()) { Trace.out.println("notifyModelChanged"); } m_fireCount++; m_listeners.fireItemValueChanged(null, null); } public int getFireCount() { return m_fireCount; }

public int getInstanceNumber() { return m_instNumber; } }

The average time for 50,000 Source5.notifyModelChangedcalls was 2.956 seconds. This is on the same order of magnitude as Source1. There is room for improvement; in fact, results along the lines of those in Source4 should be possible.

Source6

For the next test case, Source6, I made a DefaultDataItemChangeManagerthat implements the methods from DataItemChangeManagerSupport that I needed for Source5. Source6 is identical to Source5, except that I used an instance of DefaultDataItemChangeManagerinstead of an instance of DataItemChangeManagerSupport. The average time for Source6was 0.649 seconds -- almost five times faster than javax.infobus.DataItemChangeManagerSupport. Here's the code for DefaultDataItemChangeManager:

import javax.infobus.*; import java.util.*;

public class DefaultDataItemChangeManager { protected Object m_source; private DataItemChangeListener[] m_listeners = new DataItemChangeListener[0]; public DefaultDataItemChangeManager(Object source) { m_source = source; } public void addDataItemChangeListener(DataItemChangeListener listener) { if (Trace.isOn()) { Trace.out.println("addDataItemChangeListener l=" + listener.getClass()); } synchronized (this) { int length = m_listeners.length; DataItemChangeListener[] newListeners = new DataItemChangeListener[length + 1]; System.arraycopy(m_listeners, 0, newListeners, 0, length); newListeners[length] = listener; m_listeners = newListeners; } } public void fireItemValueChanged(Object changedItem, InfoBusPropertyMap propertyMap) { DataItemValueChangedEvent event = new DataItemValueChangedEvent(m_source, changedItem, propertyMap);

int length = m_listeners.length; for (int i = 0; i < length; i++) { m_listeners[i].dataItemValueChanged(event); } } }

Source7

Finally, I noticed that there was still one short-lived object remaining in Source6: the event object itself. I wanted to see if eliminating the creation of the event object would have any impact on the performance. Source7 thus uses DefaultDataItemChangeManagerBroken, which in turn uses a field to store a precreated DataItemValueChangedEventinstance. Source7is indeed much faster than Source6, with an average time of 0.120 seconds. Here's the code:

import javax.infobus.*; import java.util.*;

public class DefaultDataItemChangeManagerBroken { protected Object m_source; private DataItemChangeListener[] m_listeners = new DataItemChangeListener[0]; private DataItemValueChangedEvent m_event; public DefaultDataItemChangeManagerBroken(Object source) { m_source = source; m_event = new DataItemValueChangedEvent(m_source, null, null); } public void addDataItemChangeListener(DataItemChangeListener listener) { if (Trace.isOn()) { Trace.out.println("addDataItemChangeListener l=" + listener.getClass()); } synchronized (this) { int length = m_listeners.length; DataItemChangeListener[] newListeners = new DataItemChangeListener[length + 1]; System.arraycopy(m_listeners, 0, newListeners, 0, length); newListeners[length] = listener; m_listeners = newListeners; } } public void fireItemValueChanged(Object changedItem, InfoBusPropertyMap propertyMap) { int length = m_listeners.length; for (int i = 0; i < length; i++) { m_listeners[i].dataItemValueChanged(m_event); } } }

Unfortunately, it is normally not possible to use this optimization with InfoBus. In order to be useful, the event object needs to have several of its properties set to match the event being delivered. However, none of the InfoBus DataItem<*>Eventclasses define mutator methods for the event properties. Furthermore, each DataItem<*>Eventis declared final. Therefore, the only way to initialize the properties of the event is in the events constructor.

For the DataItemValueChangedEvent, which reports the changed item in the event, an event would have to be created for each item. Additionally, instead of using a single event per item, a pool of events for a single item would have to be maintained, because two or more threads may attempt to notify listeners about changes to the same item. It would be much simpler to implement a pool of events if the events could be reused for different data items. To support this, InfoBus would have to add mutator methods to the DataItem<*>Events classes or allow classes to extend the DataItem<*>Events classes.

Performance numbers

The table below shows the performance numbers for Source1through Source7, using Sun's JDK 1.1.8, IBM's JDK 1.1.8, and Sun's JDK 1.3 Beta.

Related:
1 2 3 Page 2
Page 2 of 3