简体   繁体   中英

Java Swing Timer slower than expected

I'm writing a swing program that has to repaint at intervals. For some reason, javax.swing.Timer seems to only be repainting once every 800-900ms even when I specify lower delays. (eg 100ms) I thought the delay might be due to the repaint() method taking 800ms to run, but I timed it and it only takes .3ms. I tried using Thread.sleep() and it repaints at the desired intervals. Can anyone help explain why this might be?

What I'm trying to do seems like the intended purpose of javax.swing.Timer and so I'm confused as to why it would slow down the code so much. Main driver class:

import java.awt.Dimension;

import javax.swing.JFrame;


public class GUIDriver{

public AnimationPanel animationPanel;

public static void main(String[] args){
    GUIDriver gui = new GUIDriver();
    gui.createAndShowGUI();

}

public void createAndShowGUI(){
    animationPanel = AnimationPanel(50);
    JFrame frame = new JFrame("Animations");

    frame.setContentPane(animationPanel);
    frame.setPreferredSize(new Dimension(1015, 840));

    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    frame.pack();
    frame.validate();
    frame.doLayout();
    frame.setVisible(true);
    animationPanel.draw(); //only here when using Thread.sleep() method
}


}

Custom JPanel extension:

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Point2D;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Scanner;
import java.text.SimpleDateFormat;

import javax.imageio.ImageIO;
import javax.swing.JPanel;
import javax.swing.Timer;

/**
 * This extension of the JPanel overrides the paintComponent method to provide a     framework for
 * creating 2-D animations.
 */
@SuppressWarnings("serial")
public class AnimationPanel extends JPanel implements ActionListener{

public ArrayList<LeakInstance> data;
public Timer timer;
public SimpleDateFormat dateOutput = new SimpleDateFormat("MM/dd/yyyy");
BufferedImage background;


public Color lineColor = Color.black;
public Color under = Color.blue;
public Color over = Color.red;
public Font defaultFont = new Font("default", Font.BOLD, 14);

public float cutoff = 50;

public int timeStep = 0;
public long startTime = 0;

public Graphics2D screen;

public AnimationPanel(float cutoff) {
    super();
    setLayout(null);
    read("Data.txt");
    this.cutoff = cutoff;
    timer = new Timer(100, this); //commented out when using Thread.sleep()
    try {
        background = ImageIO.read(new File("img.png"));
    } catch (IOException e) {
        e.printStackTrace();
    }
    repaint();
    timer.start(); //commented out when using Thread.sleep()
}


/**
 * This method overrides JPanel's paintComponent method in order to customize the rendering of 2-D graphics
 * and thus make animation possible. It is not called directly; it is called by repaint() which fully refreshes
 * the screen.
 */
@Override
public void paintComponent(Graphics g) {
    ArrayList<String> drawn = new ArrayList<String>();
    screen = (Graphics2D)g;
    RenderingHints renderHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
                             RenderingHints.VALUE_ANTIALIAS_ON);
    renderHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    screen.setRenderingHints(renderHints);


    screen.drawImage(background.getScaledInstance(1000, 800, Image.SCALE_SMOOTH), 0, 0, this);

    g.setFont(defaultFont);

    screen.setColor(Color.orange);
    screen.fillRect(485, 735, 100, 20); //cover old date
    screen.setColor(lineColor);
    screen.drawString(dateOutput.format(data.get(timeStep).getDate()), 500, 750);
    screen.drawString(Integer.toString(timeStep), 300, 750);
    System.out.println((System.nanoTime() - startTime)/1e9f);
    startTime = System.nanoTime();

    float x, y;
    float z;
    int xoffset, yoffset;
    for(int i = 0; drawn.size() < 24 && (timeStep-i) > -1; i++){
        if(!drawn.contains(data.get(timeStep-i).getName())){
            xoffset = 0;
            yoffset = 15;
            String name = data.get(timeStep-i).getName();
            drawn.add(name);
            x = data.get(timeStep-i).getLocation().x;
            y = data.get(timeStep-i).getLocation().y;
            z = data.get(timeStep-i).getZ();

            if(z > cutoff)
                screen.setColor(over);
            else
                screen.setColor(under);
            switch(name){
                            //various cases to change x or y offset
            }
            screen.drawString(Float.toString(z), x+xoffset, y+yoffset);
            screen.setColor(lineColor);
            screen.drawLine((int)x-2, (int)y, (int)x+2,(int) y);
            screen.drawLine((int)x, (int)y-2, (int)x, (int)y+2);
        }
    }
}

public void draw(){
    try{
        for(; timeStep < data.size()-1; timeStep++){
            Thread.sleep(100);
            repaint();
        }
    }catch(Exception e){
        e.printStackTrace();
    }
}

public void read(String filename){
    File file = new File(filename);
    data = new ArrayList<MyType>(100);
    try(Scanner scan = new Scanner(file)){
        while(scan.hasNextLine())
            data.add(new MyType(scan.next(), scan.next(), scan.nextFloat(),
                    new Point2D.Float(scan.nextFloat(), scan.nextFloat())));
    }catch (Exception e){
        e.printStackTrace();
    }
    Collections.sort(data);
}


@Override
public void actionPerformed(ActionEvent e) {
    if(e.getSource().equals(timer)){
        if(timeStep < data.size()-1){
            timeStep++;
            repaint();
        }
        else
            timer.stop();
    }
}
 }

The main problem I had was with

screen.drawImage(background.getScaledInstance(1000, 800, Image.SCALE_SMOOTH), 0, 0, this);

This was causing a repaint event continuously as well as reducing the update repaint time to rough 0.25ms

When I pre-scaled the image (I had to change it's type to Image ), for example...

try {
    background = ImageIO.read(new File("C:\\Users\\Shane Whitehead\\Dropbox\\Wallpapers\\5781217115_4182ee16da_o.jpg"));
    background = background.getScaledInstance(1000, 800, Image.SCALE_SMOOTH);
} catch (IOException e) {
    e.printStackTrace();
}

I was able to get a repaint time of 0.1ms.

I tried using 100, 1000, and 10, 000 elements in the data list without much issue (and just for fun, I tried 100, 000 and still got a repaint of 0.1ms)

You need to pay careful attention to ensuring that the paint process is well optimised

Updated based on comment

However it still doesn't explain why with a paint method that took ~.3ms was causing the timer to take ~800ms. Or why using javax.swing.Timer seems to be an order of magnitude slower than using Thread.sleep()

Actually, it does, but you need to understanding how the Event Dispatching Thread, Event Queue and RepaintManager work.

Basically...

  • paintComponent is called, you call getScaledInstance , which triggers a repaint request via the ImageObserver . getScaledInstance is slow, 100's milliseconds slow. The RepaintManager optimises the repaint requests and tries to reduce the number of paint events that get posted on the EDT, this is sometimes good, sometimes bad. These paint events are placed on the Event Queue
  • javax.swing.Timer triggers an event, this event is placed onto the Event Queue, to be processed by the EDT. Now, this is where it gets a "little weird". javax.swing.Timer is optimised in such away that "If coalesce is true (which is default), only allow one Runnable to be queued on the EventQueue and be pending" - so if there is a pre-existing "timer" event already on the event queue, no new ones are posted.
  • The actionPerformed method is (eventually) called, also within the context of the EDT, taking (even a very small amount of time) away from processing other events...it all adds up...
  • And/or paintComponent is called again, repeat...

So, what is probably happening, the repeated updates caused by getScaledInstance are just far enough apart to prevent the RepaintManager from optimising those calls, which is placing a strain on the EDT, the Timer is ticking away, but because the events may not be getting processed fast enough, some are getting dropped, which is, in the long run, causing the "paints" to be spaced further apart.

The reason the Thread approach doesn't suffer these issues it because it can just spam the EDT with new paint requests, without taking into consideration the state of the Event Queue...

Also, I could break you Thread update, meaning that until all items in the data list are processed, nothing will get painted, take a look at Initial Threads for more details

For more details about how painting works in Swing, take a look at Painting in AWT and Swing

You might also like to take a look at The Perils of Image.getScaledInstance() and Java: maintaining aspect ratio of JPanel background image for some more details about scaling...

Updated with some additional testing

So I added...

long scaleStart = System.currentTimeMillis();
screen.drawImage(background.getScaledInstance(getWidth(), getHeight(), Image.SCALE_SMOOTH), 0, 0, this);
System.out.println("Scaled in: " + ((System.currentTimeMillis()- scaleStart) / 1000f));

To test the scaling process, this generally, takes about 200-240 ms. The rest of the paint process only adds about 10ms.

I did this while using timer.setCoalesce(false); , so I got no additional benefit from turning coalescing off.

By pre-scaling the image, I got a constant 0.1ms update (with and without timer.setCoalesce(false); )

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM