Delayed response to JTable row selection event under a huge data load
Asked Answered
S

1

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.

Snort answered 29/6, 2013 at 19:8 Comment(0)
Z
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://mcmap.net/q/456469/-populate-jtable-with-large-number-of-rows */
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);
        }
    }
}
Zeph answered 29/6, 2013 at 20:4 Comment(17)
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 advanceSnort
I don't remember the exact color scheme in jvisualvm; it's red in NetBeans; compare with a thread dump, for example.Zeph
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?Snort
I found it instructive to profile this example; note Thread.yield() in the background.Zeph
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.Snort
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.Snort
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.Zeph
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?Snort
Yes, that is very wrong; the API example is correct.Zeph
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 issueSnort
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.Snort
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 inplementationSnort
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.Snort
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.Snort
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?Snort
If this is correct direction, please, edvice me with reasonable implementation of so called SwingWorkers chainSnort
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.Zeph

© 2022 - 2024 — McMap. All rights reserved.