javaswingswingxglazedlistsjxtable

Why does my JXTable sort so much slower when I switch to GlazedLists' EventTableModel?


Updated

I have updated this question to more accurately describe the cause of my problem and have included a simpler example that the one I originally used.

I've included a simple example below to show the performance issue I'm having. When I back my JXTable with a normal ArrayList, it performs reasonably well. However, if I switch the ArrayList for an EventList and build the table using an EventTableModel, the sorting is much slower (~10x slower in this case).

If using Maven or Gradle, here are the artifact coordinates I'm using.

apply plugin: 'java'
apply plugin: 'application'
mainClassName = "SortPerfMain"

dependencies {
    compile "net.java.dev.glazedlists:glazedlists_java15:1.8.0"
    compile "org.swinglabs.swingx:swingx-core:1.6.4"
}

And here is the example. The only reason I was trying to use an EventList is because I wanted a data structure that I could modify outside of the TableModel and have the necessary notification occur.

    import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.swing.EventTableModel;
import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.renderer.*;
import org.jdesktop.swingx.table.TableColumnExt;

import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;

import static javax.swing.WindowConstants.EXIT_ON_CLOSE;

/* This class creates a JFrame with two JXTables displayed side by side.  Both
 * tables have a single column that holds Item objects.  Each Item has one
 * property; amount.  The amount property is a BigDecimal, but the performance
 * disparity is still present when using int instead.
 *
 * The first table is backed by a simple ArrayList.  The second table is backed
 * by an EventList (GlazedLists).
 *
 * When sorting 1,000,000 rows, the first table takes about 1 second and the
 * second table takes about 10 seconds.
 */

public class SortPerfMain {
    @SuppressWarnings("FieldCanBeLocal")
    private final boolean useDebugRenderer = true;

    // The number of items that should be added to the model.
    @SuppressWarnings("FieldCanBeLocal")
    private final int itemCount = 2;

    // The number of visible rows in each table.
    @SuppressWarnings("FieldCanBeLocal")
    private final int visibleRowCount = 2;

    public static void main(String[] args) {
        new SortPerfMain();
    }

    public SortPerfMain() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                List<Item> itemList = createItemList();

                JPanel leftPanel = createTablePanel(
                        createTable(createSimpleModel(itemList)));

                JPanel rightPanel = createTablePanel(
                        createTable(createGlazedModel(itemList)));

                JPanel mainPanel = new JPanel(new GridLayout(1, 2));
                mainPanel.add(leftPanel);
                mainPanel.add(rightPanel);

                JFrame mainFrame = new JFrame("Table Sort Perf");
                mainFrame.setContentPane(mainPanel);
                mainFrame.pack();
                mainFrame.setSize(600, mainFrame.getHeight());
                mainFrame.setLocationRelativeTo(null);
                mainFrame.setDefaultCloseOperation(EXIT_ON_CLOSE);
                mainFrame.setVisible(true);
            }
        });
    }

    private List<Item> createItemList() {
        List<Item> itemList = new ArrayList<>(itemCount);
        for (int i = 0; i < itemCount; i++) {
            itemList.add(new Item(i));
        }
        return itemList;
    }

    private JXTable createTable(TableModel model) {
        JXTable table = new JXTable(model);
        table.setVisibleRowCount(visibleRowCount);
        addRenderer(table);
        return table;
    }

    private void addRenderer(JXTable table) {
        TableColumnExt column = table.getColumnExt(Columns.AMOUNT.ordinal());
        column.setCellRenderer(createCurrencyRenderer());
    }

    private JPanel createTablePanel(JXTable table) {
        JLabel panelLabel = new JLabel(table.getModel().getClass().getName());
        JPanel panel = new JPanel(new BorderLayout());

        panel.add(panelLabel, BorderLayout.NORTH);
        panel.add(new JScrollPane(table), BorderLayout.CENTER);

        return panel;
    }

    private TableModel createSimpleModel(List<Item> items) {
        return new SimpleTableModel(items);
    }

    private TableModel createGlazedModel(List<Item> items) {
        EventList<Item> itemList = new BasicEventList<>();
        itemList.addAll(items);
        return new EventTableModel<>(itemList, new EventTableModelFormat());
    }

    private TableCellRenderer createCurrencyRenderer() {
        //noinspection ConstantConditions
        if (useDebugRenderer) {
            return new DebugRenderer();
        }

        return new DefaultTableRenderer(
                new LabelProvider(new FormatStringValue(
                        NumberFormat.getCurrencyInstance())));
    }

    // Enum for managing table columns
    private static enum Columns {
        AMOUNT("Amount", BigDecimal.class);

        private final String name;
        private final Class type;

        private Columns(String name, Class type) {
            this.name = name;
            this.type = type;
        }
    }

    // Each table holds a list of items.
    private static class Item {
        private final BigDecimal amount;

        private Item(BigDecimal amount) {
            this.amount = amount;
        }

        private Item(int amount) {
            this(new BigDecimal(amount));
        }
    }

    // A simple model that doesn't perform any change notification
    private static class SimpleTableModel extends DefaultTableModel {
        private final List<Item> itemList;

        public SimpleTableModel(List<Item> items) {
            this.itemList = items;
        }

        @Override
        public int getRowCount() {
            if (itemList == null) {
                return 0;
            }

            return itemList.size();
        }

        @Override
        public int getColumnCount() {
            return Columns.values().length;
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            switch (Columns.values()[columnIndex]) {
                case AMOUNT:
                    return itemList.get(rowIndex).amount;
            }

            return null;
        }

        @Override
        public String getColumnName(int column) {
            return Columns.values()[column].name;
        }

        @Override
        public Class<?> getColumnClass(int column) {
            return Columns.values()[column].type;
        }
    }

    // Table format for use with the EventTableModel
    private static class EventTableModelFormat implements TableFormat<Item> {
        @Override
        public int getColumnCount() {
            return 1;
        }

        @Override
        public String getColumnName(int i) {
            return Columns.values()[i].name;
        }

        @Override
        public Object getColumnValue(Item item, int i) {
            return item.amount;
        }
    }

    /* The following classes are used to add println statements to the part
     * of the component hierarchy we're interested in for debugging.
     */

    private class DebugRenderer extends DefaultTableRenderer {
        private DebugRenderer() {
            super(new DebugProvider());
        }

        @Override
        public Component getTableCellRendererComponent(
                JTable table,
                Object value,
                boolean isSelected,
                boolean hasFocus,
                int row,
                int column) {
            System.out.println("Renderer requested for " + value.toString());
            return super.getTableCellRendererComponent(
                    table, value, isSelected, hasFocus, row, column);
        }
    }

    private class DebugProvider extends LabelProvider {
        private DebugProvider() {
            super(new DebugFormatter());
        }

        @Override
        public String getString(Object value) {
            System.out.println("Providing string for " + value.toString());
            return super.getString(value);
        }
    }

    private class DebugFormatter extends FormatStringValue {
        private DebugFormatter() {
            super(NumberFormat.getCurrencyInstance());
        }

        @Override
        public String getString(Object value) {
            System.out.println("Formatting object: " + value.toString());
            return super.getString(value);
        }
    }
}

I also noticed the table backed by the EventTableModel is sorting based on string values rather than numeric values, but I'm not sure why. Here are a couple of screenshots from the profiler with a million rows being sorted.

First Table

Second Table

Any ideas?


Solution

  • The problem I was having with this was a combination of the way SwingX's TableRowSorterModelWrapper works with GlazedLists' TableFormat.

    When using GlazedLists' TableFormat the class types are not supplied for table columns. When the class type is not provided, JXTable will end up sorting the column based on string values which are supplied by a ComponentProvider. If the ComponentProvider is constructed with a FormatStringValue converter, every item in the column will be formatted prior to being used for comparison during a sort. The actual call to the ComponentProvider happens in the TableRowSorterModelWrapper.

    In my case, when I added the custom renderer, I replaced the default ComponentProvider with a LabelProvider that was using a FormatStringValue that was using the formatter returned from NumberFormat.getCurrencyInstance().

    The reason the table using my SimpleTableModel didn't suffer from the same performance problems was because it supplied column class types. Since BigDecimal implements Comparable, sort operations didn't require a call to the ComponentProvider to get a (possibly formatted) string value.

    The solution is very simple; use GlazedLists' AdvancedTableFormat instead of TableFormat and supply the class types for each table column. The following will work with the example in my question.

    private static class EventTableModelFormat implements AdvancedTableFormat<Item> {
        @Override
        public int getColumnCount() {
            return 1;
        }
    
        @Override
        public String getColumnName(int i) {
            return Columns.values()[i].name;
        }
    
        @Override
        public Object getColumnValue(Item item, int i) {
            return item.amount;
        }
    
        @Override
        public Class getColumnClass(int column) {
            return Columns.values()[column].type;
        }
    
        @Override
        public Comparator getColumnComparator(int column) {
            return null;
        }
    }