5

I have a Swing JTable dynamically updated with a big amount of data—new rows are live added constantly, and some 1000-2000 rows can be added during a few minutes. I have registered a Listener to reponse for use's single row selection event to perform some staff. I have used Observer pattern for Swing data binding and table's model is backed by a WritableList implementation. So new items are added to the table from its own Realm. And Listener was added from the SWT UI thread. The problem is, that when new rows are added to table, it doesn't respond at once on user row selection event. Only when stop updating table model, table will respond on user selection- some times with delay more then 30-60 seconds. Please, help me undersand why my table model doesn't respond at once to user selection when intensively updated, and how to overcome this limitation. Any suggestions will be appreciated.

trashgod
  • 203,806
  • 29
  • 246
  • 1,045
kioria
  • 53
  • 1
  • 5

1 Answers1

12

Use SwingWorker to publish() your rows in the background and update the TableModel in your implementation of process(). SwingWorker will limit updates to a sustainable rate. Profile to ensure that you are not blocking the event dispatch thread.

Addendum: The GUI remains responsive with this 1,000,000-row variation, as tested. When profiling, note that each click of the "Go" button starts a new worker.

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;

/** @see https://stackoverflow.com/a/17415635/230513 */
public class TestTableLoad01 {

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

    public TestTableLoad01() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                final MyTableModel model = new MyTableModel();
                JTable table = new JTable(model);
                table.setDefaultRenderer(Date.class, new TimeCellRenderer());

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new JScrollPane(table));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
                frame.add(new JButton(new AbstractAction("Go") {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        TableSwingWorker worker = new TableSwingWorker(model);
                        worker.execute();
                    }
                }), BorderLayout.SOUTH);
            }
        });
    }

    public class TimeCellRenderer extends DefaultTableCellRenderer {

        private DateFormat df;

        public TimeCellRenderer() {
            df = new SimpleDateFormat("HH:mm:ss");
        }

        @Override
        public Component getTableCellRendererComponent(JTable table,
            Object value, boolean isSelected, boolean hasFocus, int row, int column) {

            if (value instanceof Date) {

                value = df.format(value);

            }

            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

            return this;

        }
    }

    public class MyTableModel extends AbstractTableModel {

        private String[] columnNames = new String[]{"Date", "Row"};
        private List<RowData> data;

        public MyTableModel() {
            data = new ArrayList<RowData>(25);
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            return columnIndex == 0 ? Date.class : Integer.class;
        }

        @Override
        public String getColumnName(int col) {
            return columnNames[col];
        }

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

        @Override
        public int getRowCount() {
            return data.size();
        }

        @Override
        public Object getValueAt(int row, int col) {
            RowData value = data.get(row);
            return col == 0 ? value.getDate() : value.getRow();
        }

        public void addRow(RowData value) {
            int rowCount = getRowCount();
            data.add(value);
            fireTableRowsInserted(rowCount, rowCount);
        }

        public void addRows(RowData... value) {
            addRows(Arrays.asList(value));
        }

        private void addRows(List<RowData> rows) {
            int rowCount = getRowCount();
            data.addAll(rows);
            fireTableRowsInserted(rowCount, getRowCount() - 1);
        }
    }

    public class RowData {

        private Date date;
        private int row;

        public RowData(int row) {
            this.date = new Date();
            this.row = row;
        }

        public Date getDate() {
            return date;
        }

        public int getRow() {
            return row;
        }
    }

    public class TableSwingWorker extends SwingWorker<MyTableModel, RowData> {

        private final MyTableModel tableModel;

        public TableSwingWorker(MyTableModel tableModel) {
            this.tableModel = tableModel;
        }

        @Override
        protected MyTableModel doInBackground() throws Exception {

            // This is a deliberate pause to allow the UI time to render
            Thread.sleep(1000);

            System.out.println("Start polulating");

            for (int index = 0; index < 1000000; index++) {

                RowData data = new RowData(index);
                publish(data);
                Thread.yield();
            }
            return tableModel;
        }

        @Override
        protected void process(List<RowData> chunks) {
            System.out.println("Adding " + chunks.size() + " rows");
            tableModel.addRows(chunks);
        }
    }
}
Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Thank you for your reply, Trashgod. I was using VisualVM - but haven't got much information whether event dispatch thread was blocked or not. From that profiler i can see a list of active threads only. How to see whether some thread is blocked by another? Thank you in advance – kioria Jun 30 '13 at 01:57
  • I don't remember the exact color scheme in `jvisualvm`; it's red in NetBeans; compare with a [thread dump](http://javatechniques.com/blog/jdb-example-generating-a-thread-dump/), for [example](http://stackoverflow.com/q/12728589/230513). – trashgod Jun 30 '13 at 11:19
  • I wonder about the amount of threads need to create and run to publish a lot of of new rows... I know that thread creation and teardown is not a chip operation. And with SwingWorker framework it seems i need every time create new Instance of it in order to call method doInBackground(). Is it efficient? – kioria Jul 02 '13 at 15:03
  • I found it instructive to profile this [example](http://stackoverflow.com/a/17415635/230513); note `Thread.yield()` in the background. – trashgod Jul 02 '13 at 15:07
  • Every time new packet from client received and parsed to POJO, I create an instance of SwingWorker class to process a collection of these POJOs. In doInBackground() method I process all collection elements - Model for UI. Then with the received ordered list of POJOs I make table input in the form of Object[][] array. And return updated table, firing model changed event. All these stuff is done in the doInBackground() method. So I didn’t override any other methods in SwingWorker class, thus not delivering any intermediate results to UI, as you suggested, in process() methods. – kioria Jul 04 '13 at 08:10
  • Please, share you experience whether is is an efficient way to populate Swing table with new data. And also explain whether it Is garantired that when Runnable exits it’s run() – it’s Thraed is teared down/ Could it be te case of any memeory leaks due to Runnale Thread not terminating properly?Admire for you answer. – kioria Jul 04 '13 at 08:12
  • I agree: do _not_ alter the `TableModel` from `doInBackground()`; only alter it in `process()`. I don't see a problem with multiple workers, but you may be able to use a single `SwingWorker` that checks a queue, for [example](http://stackoverflow.com/q/7784909/230513). – trashgod Jul 04 '13 at 08:54
  • In doInBackground() method I process all collection elements - Model for UI. Then with the received ordered list of POJOs I make table input in the form of Object[][] array. And return updated table, firing model changed event. - i mean i alter the TableModel exactly from doInBackground() - it is wrong? Why then? – kioria Jul 04 '13 at 08:57
  • Yes, that is very wrong; the [API example](http://docs.oracle.com/javase/7/docs/api/javax/swing/SwingWorker.html#publish%28V...%29) is correct. – trashgod Jul 04 '13 at 08:59
  • I am a fool - as "Swing components should be accessed on the Event Dispatch Thread only." Thank you for your helpful examples they help me to solve my issue – kioria Jul 04 '13 at 09:28
  • I was testing table responsiveness under very huge data load - 50 000 packets (50 000 new POJOs added to collection) and i got very bad results - with amount of more then 10000 items in the Model's collection, table starts froze and responce very poor and the whole application's behaviour is very bad and UI sometimes turns to be a white screen. – kioria Jul 04 '13 at 11:30
  • Note that all stuff with collection for table's model is done in background thread now. Please, help me understand what could be wrong in my table inplementation – kioria Jul 04 '13 at 11:32
  • OK,i have analyzed your code in the doInBackground() method, and note please, that all data for table input is generated very simple in your case. In my case it takes some steps - first is to parse packet, and the second step is updated Model collection for UI with new parsed POJO. – kioria Jul 04 '13 at 15:35
  • So, when simply add data to table for thousend of times - it indeed wworks perfect and no UI freezing. So from this investigation I have unnderstand the the roots of my problem now in the inefficient parsing. I wonder, as all packet processing is done in the separate thread - not UI thread. – kioria Jul 04 '13 at 15:37
  • So I have arrived at the stage when I possbly need to implement two SwingWorkers? So the result received from one's doInBackground() method will be the input for the second? – kioria Jul 04 '13 at 15:38
  • If this is correct direction, please, edvice me with reasonable implementation of so called SwingWorkers chain – kioria Jul 04 '13 at 15:39
  • 1
    I don't think you need yet another worker thread. Do any waiting and parsing in `doInBackground()` so that whatever you `publish()` can be added to the `TableModel` efficiently in `process()`. Also note that `DefaultTableModel` uses `Vector`, which is synchronized and fires updates for each addition; use `AbstractTableModel` so you can fire an update once per `chunks`. – trashgod Jul 04 '13 at 18:13