I am working on creating a logger to show output as part of a larger Java swing GUI. Unfortunately I was experiencing a slowdown after adding it. I have traced the problem to repeated calls of Document.insertString().
I made a test which shows this slowdown:
LogPanel.java
public class LogPanel extends JPanel{
private static final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private JEditorPane textPane;
private static int numTextRows = 50;
private SimpleAttributeSet keyWord;
private Document document;
public LogPanel() {
super(new BorderLayout());
add(makePanel(), BorderLayout.CENTER);
}
private Component makePanel() {
// Just a text area that grows and can be scrolled.
textPane = new JTextPane();
document = textPane.getDocument();
keyWord = new SimpleAttributeSet();
StyleConstants.setForeground(keyWord, Color.BLACK);
//textArea.setRows(numTextRows);
textPane.setEditable(false);
textPane.setFont(new Font("monospaced", Font.PLAIN, 12));
DefaultCaret caret = (DefaultCaret) textPane.getCaret();
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
//Wrap the textPane in a JPanel with BorderLayout so that the text does not wrap
JPanel textPaneWrapper = new JPanel();
textPaneWrapper.setLayout(new BorderLayout());
textPaneWrapper.add(textPane);
JScrollPane areaScrollPane = new JScrollPane(textPaneWrapper);
areaScrollPane.getVerticalScrollBar().setUnitIncrement(20);
//JScrollPane areaScrollPane = new JScrollPane(textPane);
areaScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
areaScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
areaScrollPane.setPreferredSize(new Dimension(250, 250));
// builder.add(areaScrollPane, cc.xywh(1, 3, 3, 1));
StyleConstants.setBackground(keyWord, areaScrollPane.getBackground());
textPane.setBackground(areaScrollPane.getBackground());
return areaScrollPane;
}
public void appendResult(final String action, final String result, final Color color) {
Date now = new Date();
String strDate = df.format(now);
String paddedAction = String.format("%-19s", action);
StyleConstants.setForeground(keyWord, color);
try {
document.insertString(document.getLength(), strDate + " " + paddedAction + " " + result + "\n", keyWord);
} catch (BadLocationException e) {
throw new RuntimeException(e);
}
if(!textPane.hasFocus()) {
textPane.setCaretPosition(document.getLength());
}
}
public void appendResult(String action, String result) {
appendResult(action, result, Color.BLACK);
}
}
LogTester.Java
public class LogTester extends JFrame{
private LogPanel logPanel;
private JButton pushMe;
public LogTester(){
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new GridBagLayout());
logPanel = new LogPanel();
this.add(logPanel);
pushMe= new JButton("Press Me");
LogTester self=this;
pushMe.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
self.addLotsOfStuff();
}
});
this.add(pushMe);
this.pack();
this.setVisible(true);
}
public void addLotsOfStuff(){
for(int i=0; i<1000; i+=1){
long start=System.currentTimeMillis();
for(int j=0; j<1000; j+=1){
String str="This is a very long peice of text designed to test the capabilites of our log panel. Move along, nothing to see here.";
logPanel.appendResult("HERP",str);
}
long end=System.currentTimeMillis();
System.out.println(end-start);
}
}
public static void main(String args[]){
LogTester test=new LogTester();
}
}
The program above attempts to write a large number of lines to a JTextPane, which utilizes Document.insertString(). The results of this program are concerning:
For some reason, each call to this function increases the runtime of the next call: It looks linear or even mildly exponential. This might imply that all the previous contents of the document are being copied on each insert, rather than the new string being appended (in some sort of linked list fashion)
Unlike Java GUI freezing because of insertString method? I am primarily concerned with the increasing runtime of the function, not the idle time of the application. Adding threading will not help if each individual call gets very slow.
Unlike Limit JTextPane space usage I am not concerned with large memory usage. If the document is large, it will use a large amount of memory. I just don't understand why that would affect the runtime of inserting more information to the Document.
Perhaps the slowdown can be attributed to this caret position memory leak?
What parts of JTextPane or Document would I have to override in order to achieve a constant time insertString()?
Growing arrays is nearly linear, but not actually linear.
Once you exceed the size of the level 1 cache line, you use another until:
There are no free cache lines, so you have to evict an existing line cache, populating it from a portion of a level 2 cache line.
There are no free level 2 cache lines, so you have to evict one of those, populating it from a (Typically) general RAM reqeust.
Your general RAM request is too large to fit into one request, and the group of requested pages can not be co-fetched due to their count or memory chip locations.
This means that, even for linear operations, really small stuff runs much, much faster than the same linear algorithm dealing with a much larger amount of data.
So when deciding on if an algorithm is O(n) or otherwise, it's important to remember that such a decision is based on a fundamental understanding of the expectations of your computing model. Big-O notation assumes that all RAM fetches are equal in time, and computations (operations) are also equal in time. Under such constraints, comparing the count of the operations to the amount of data still makes sense; but, assuming the wall clock time is exact doesn't.
Related
To start with -- I'm not sure, that I have properly formulated the question (I'm new in Java and in making programs with GUI).
It is the following thing, I'm trying to do. I have a window with several similar parameters (numbers are just for distinction between lines and it ist just very simplified example, of what should my GUI be):
Initial Window
Then, by clicking on the "+"-button I would like to add an new line, like here:
Line 35 is added
It should be also possible to delete lines, like here: Line 30 was deleted, by pressing "-"-Button.
As I wrote at the beginning, it is possible, that there was such a question, but I couldn't find anything (probably, because I do not now the keywords or I was looking with a wrong ones).
How such window can be done? The only idea I have is to draw a new window after every +/-.
Addition: Code (not working in the part of changing the number of rows).
import javax.swing.*;
import java.awt.event.*;
public class Test extends JFrame {
public Test() {
setSize(200, 600);
JButton plusButton[] = new JButton[100];
JButton minusButton[] = new JButton[100];
JTextField fields[] = new JTextField[100];
JPanel panel1 = new JPanel();
for (int i=0; i<plusButton.length; i++) {
plusButton[i]=new JButton("+");
minusButton[i]=new JButton("-");
fields[i] = new JTextField("Text "+ i);
}
for (int i=1; i<4; i++) {
panel1.add(plusButton[i*10]);
plusButton[i*10].setActionCommand("add after " +String.valueOf(i));
panel1.add(minusButton[i*10]);
minusButton[i*10].setActionCommand("remove " +String.valueOf(i));
panel1.add(fields[i*10]);
}
panel1.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
this.getContentPane().add(panel1);
setVisible(true);
setDefaultCloseOperation(EXIT_ON_CLOSE);
}
public void actionPerformed(ActionEvent e) {
for (int i=0; i<100; i++) {
String stand1 = "add after "+String.valueOf(i);
String stand2 = "remove "+String.valueOf(i);
if (stand1.equals(e.getActionCommand())) {
//add "row" of elements
panel1.add(plusButton[i]);
plusButton[i+1].setActionCommand("add");
panel1.add(minusButton[i+1]);
minusButton[i+1].setActionCommand("remove");
panel1.add(fields[i+1]);
} else if (stand2.equals(e.getActionCommand())) {
//delete "row" of elements
}
}
}
public static void main(String[] args) {
Test a = new Test();
}
}
The Problem, that is obvious -- when I want to add 2 rows (i think it is proper definition) of buttons after button 20, there will be an doubling of numbers. As a solution I see here a creation of a new panel for each new row. But it is sounds wrong for me.
P.S. Unfortunately I do not have time to end this topic or to post a working example. I actually found some kind of solution, beginning from the Question here, on Stack Overflow:
Adding JButton to JTable as cell.
So, in case somebody will be looking for such topic, it should sounds like "jButton in jTable".
There are multiple GUI frameworks for Java. First decide which one you wanna use.
As for your particular query
Add functionality to the + and - such that it will create an instance of a field object (that line with parameters as you call them) or destroy that particular instance of the object.
+ is clicked -> Create new object on consecutive line and increase the pointer-count(?) of the following fields.
- is clicked -> Call destructor for the particular object and decrease the pointer-count of the following fields.
I am using MigLayout 3.5.5, as the newer updates are not compatible with my older code.
Problem
When setting text to a JTextPane in a MigLayout, the JTextPane will take double the space (according to font size) IF the text I am setting the JTextPane contains space characters. It does not happen all the time, but in the specific program I am making, it happens frequently.
The program's goal is to present information in a letter-by-letter basis, so there is a button that updates the text to the next letter. However, the text bounces around, because the JTextPane is sometimes occupying more space than usual. I identified a certain pattern to the height differences.
Pattern
A new line indicates that I added a letter.
"|" represents a space character in the text.
"Space" means JTextPane is taking double the space.
Full String: "The quick brown fox jumps over the lazy dog."
T
Th
The
The|
The|q (Space)
The|qu
The|qui (Space)
The|quic
The|quick (Space)
The|quick|
Note: I stopped the pattern here, because from this point on (starting with The|quick|b), every single letter addition resulted in the JTextPane occupying double its height.
I've already tried printing out the letter-by-letter text to the console to see if there were any new line characters within the text being added, but to no avail. I also thought it might be a problem with the automatic wrapping of the JTextPane, but the text I inserted isn't quite long enough to wrap in the JFrame's size.
Here is a short example to reproduce the behavior:
public class MainFrame extends JFrame {
int currentLetter = 1;
final String FULL_TEXT = "The quick brown fox jumps over the lazy dog.";
JTextPane text;
JButton addLetter;
MainFrame() {
setSize(500, 500);
setLayout(new MigLayout("align center, ins 0, gap 0"));
addElements();
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
MainFrame application = new MainFrame();
}
});
}
private void addElements() {
text = new JTextPane();
text.setEditable(false);
text.setFont(new Font("Times New Roman", Font.BOLD, 19));
text.setForeground(Color.WHITE);
text.setBackground(Color.BLACK);
add(text, "alignx center, wmax 80%, gapbottom 5%");
addLetter = new JButton("Add Letter");
addLetter.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (currentLetter != FULL_TEXT.length()) {
currentLetter++;
updateText();
}
}
});
add(addLetter, "newline, alignx center");
updateText();
}
private void updateText() {
String partialText = new String();
for (int letter = 0; letter < currentLetter; letter++) {
partialText += FULL_TEXT.toCharArray()[letter];
}
text.setText(partialText);
}
}
Why am I using JTextPane?
I tried using JLabel for this task, and it worked well... until the text was long enough to wrap. Then, when I used HTML within the JLabel text to wrap it, every time I updated the text, it would take time for the HTML to render and result in some pretty nasty visual effects.
Next, I tried JTextArea to disguise it as a JLabel, since it not only has line wrapping, but word wrapping as well. It was a great solution, until I found out that I couldn't use a center paragraph alignment in a JTextArea.
So I settled for a JTextPane, which will work well if only I got rid of the extra space at the bottom of it.
Thanks in advance for your help!
The solution is to append text by using the insertString() method on the StyledDocument of the JTextPane instead of using setText() on the JTextPane itself.
For example, instead doing this every time:
JTextPane panel = new JTextPane();
panel.setText(panel.getText() + "test");
You should do this:
JTextPane panel = new JTextPane();
StyledDocument document = panel.getStyledDocument();
document.insertString(document.getLength(), "test", null);
And of course you need to catch the BadLocationException.
Then the space disappears. Here's the question where I found my answer to the rendering problem: JTextPane appending a new string
The answers to those questions don't address the problem with the space, but they do show the correct way to edit text in the JTextPane.
Here is my code:
while(monster.curHp > 0)
{
System.out.println("");
if(battle.pressedButton)
{
text = Player.name + ": " + Player.curHitPoints + " " + monster.name + ": " + monster.curHp;
battle = new GUIForBattle(text,Player,monster);
}
}
The weird thing is that if I have that println line in the while loop the code will work normally and when the button is pressed we will update text to have the current status and we will redraw the GUI using the GUIForBattle class, however if I don't have that println it wont redraw. Any advice? Thank you!
Here is the GUIForBattle for more context
public class GUIForBattle extends JFrame {
boolean pressedButton = false;
public GUIForBattle(String words, player PlayerOne, Monster monster)
{
JFrame frame = new JFrame(); //frame that holds everything
JPanel Panel = new JPanel(new GridLayout(5,5)); //panel where things get added
JLabel text = new JLabel(words); // text label
JButton attack = new JButton("Attack"); //makes a button used to attack
//adding what pressing the attack button would do
attack.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
int attackAmount = PlayerOne.weaponEquipped.att;
monster.curHp = monster.curHp - attackAmount;
pressedButton = true;
}
}
);
JButton Item = new JButton("Item"); // makes a button used to use items
Item.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
//we need to make a item interface
}
});
Panel.add(text); //adds the text to the panel
Panel.add(attack);
Panel.add(Item);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 800); //setting size of frame
frame.add(Panel); //adding the panel to frame
frame.setVisible(true); //making the frame visible
}
}
Your code is inherently multi-threaded; one thread is running through that little while loop; the other is the swing application thread that will be handling your swing event handlers.
If you use shared variables like this (both threads access pressedButton) you need to make sure that variable is synchronized between threads. There are several ways of handling this, but an easy way for this particular problem would be to make the variable volatile.
If the variable is not synchronized in any way, there is no guarantee by the JVM as to when one thread will 'see' the changes made to it by the other. And typically, if you keep one thread occupied like you're doing here (this while loop is called a busy wait) it will never take the time to synchronize, and you'll never see the updates.
The println is an IO operation, meaning at some point your thread will be waiting for IO to complete. Most likely this causes the JVM to synchronize the variables, which is why you notice this difference.
In any case, relying on this without thinking about synchronization can be considered a bug.
Java threads and memory handling are a complex subject, and not something I would advise for beginners to jump in to; it could be overwhelming. Just try to avoid sharing memory between threads for now. For the moment, just run your logic in your swing application code (it's not ideal, but for some beginner code it's probably a good starting point).
When you feel ready for it, read up on the memory model and what it implies for multi-threading.
I built a simple Java program that logs in a JTextArea component.
JTextArea _log = new JTextArea();
_log.setEditable(false);
JScrollPane scrollLog = new JScrollPane(_log);
scrollLog.setPreferredSize(getMaximumSize());
add(scrollLog);
The problem is that logging like this takes 15ms on average:
public void log(String info) {
_log.append(info + "\n");
}
This is far(!) slower than logging using System.out.println. Logging takes more time than the whole running time of the algorithm!
Why is the JTextArea is so slow? Is there a way to improve it?
EDIT 1:
I am using separate thread for the algorithm, and using SwingUtilities.invokeLater to update the log in the UI.
The algorithm tread finish his work after 130ms on average, but the JTextArea finish his appends after 6000ms on avarage.
EDIT 2:
I tried to test this by use setText of string that contains 2500 charaters. In that case the operation took 1000ms on average.
I tried to use another controller then JTextArea and I get same results.
Is it hard for Swing components to deal with large strings? What can I do about it?
EDIT 3:
I just test with this code:
public class Test extends JFrame {
public Test() {
final JTextArea log = new JTextArea();
log.setEditable(false);
log.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
JScrollPane scrollLog = new JScrollPane(log);
scrollLog.setPreferredSize(getMaximumSize());
JButton start = new JButton("Start");
start.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
long start = System.nanoTime();
for (int i = 0; i < 2500; i++) {
log.append("a\n");
}
long end = System.nanoTime();
System.out.println((end - start) / 1000000.0);
}
});
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(2, 1));
panel.add(scrollLog);
panel.add(start);
add(panel);
}
public static void main(String[] args) {
Test frame = new Test();
frame.setSize(600,500);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
The time of that for loop is 1870ms on avarage.
This is the only code that I ran (include the declaration of _log at the top of the question)
A JTextArea is not slow.
Far(!) away from System.out.println.
System.out.println() executes on a separate Thread.
The log takes more time then the hole running time of the algorithm!
So your algorithm is probably executing on the Event Dispatch Thread (EDT) which is the same Thread as the logic that appends text to the text area. So the text area can't repaint itself until the algorithm is finished.
The solution is to use a separate Thread for the long running algorithm.
Or maybe a better choice is to use a SwingWorker so you can run the algorithm and "publish" results to the text area.
Read the section from the Swing tutorial on Concurrency for more information and a working example of a SwingWorker.
Edit:
//log.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
The above line is causing the problem. I get 125 for the first test and 45 when I keep clicking the button.
That property is not needed. The text is still displayed on the left side of the text pane. If you want right aligned text then you need to use a JTextPane and set the attributes of the text pane to be right aligned.
That is why you should always post an MCVE. There is no way we could have guessed from your original question that you were using that method.
Edit2:
Use the alignment feature of a JTextPane:
SimpleAttributeSet center = new SimpleAttributeSet();
StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);
textPane.getStyledDocument().setParagraphAttributes(0, doc.getLength(), center, false);
Now any new text you add to the document should be center aligned. You can change this to right.
I'm writing a program that takes in some equations from the user. I want each constant to be entered in a JTextField, with each separated by a JTextArea (saying +x0, +x1, etc.). However, I can't quite get the formatting to work, and I'm not sure why. Here's the relevant code:
JTextField[][] dataTextFields = new JTextField[a+1][b+1];
JTextArea[][] dataLabels = new JTextArea[a][b+1];
for (int i = 0; i < a+1; i++)
{
for (int j = 0; j < b+1; j++)
{
dataTextFields[i][j] = new JTextField(10);
dataTextFields[i][j].setLocation(5+70*i, 10+30*j);
dataTextFields[i][j].setSize(40,35);
dataEntryPanel.add(dataTextFields[i][j]);
if (i < a)
{
String build = "x" + Integer.toString(i) + "+";
dataLabels[i][j] = new JTextArea(build);
dataLabels[i][j].setBackground(dataEntryPanel.getBackground());
dataLabels[i][j].setBounds(45+70*i,20+30*j,29,30);
dataEntryPanel.add(dataLabels[i][j]);
}
}
}
This creates JTextFields with JTextAreas 0f "+xi" in between them. However, when I run the applet, it looks like this:
I can click on the labels and bring them to the foreground, it seems, resulting in this:
I'd like for the labels to be visible without any effort from the user, obviously. Does JTextArea have some attribute that can be changed to bring this to the foreground? I'd really prefer not to add any more UI elements (panels, containers, etc). Thanks!
I would layout the container using GridBagLayout. GridBagLayout works a lot like HTML tables, where you have different cells, which grow in height and width to try and accommodate the content most effectively. For your particular layout, something like this would work:
public class SwingTest extends JFrame {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run () {
new SwingTest().setVisible(true);
}
});
}
public SwingTest () {
super("Swing Test");
JPanel contentPane = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridy = 0;
contentPane.add(createJTextField(), gbc.clone());
contentPane.add(new JLabel("x0+"), gbc.clone());
contentPane.add(createJTextField(), gbc.clone());
// go to next line
gbc.gridy++;
contentPane.add(createJTextField(), gbc.clone());
contentPane.add(new JLabel("x0+"), gbc.clone());
contentPane.add(createJTextField(), gbc.clone());
setContentPane(contentPane);
pack();
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setLocationRelativeTo(null);
}
private JTextField createJTextField () {
JTextField textField = new JTextField(4);
textField.setMinimumSize(textField.getPreferredSize());
return textField;
}
}
GridBagLayout is the most complicated (but flexible) of the layouts, and requires many parameters to configure. There are simpler ones, like FlowLayout, BorderLayout, GridLayout, etc, that can be used in conjunction with one another to achieve complex layouts, as well.
In the Swing Tutorial, there is a very good section on Laying Out Components. If you plan on spending any significant amount of time on building Swing GUI's, it may be worth the read.
Note, that there is one strange caveat with GridBagLayout: if you are going to use a JTextField in a GridBagLayout, there is one silly issue (described here) that causes them to render at their minimum sizes if they can't be rendered at their preferred sizes (which causes them to show up as tiny slits). To overcome this, I specify the number of columns on my JTextField constructor so that the minimum is something reasonable, and then set the minimum size to the preferred size.