2

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. (e.g. 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();
    }
}
 }
River
  • 8,585
  • 14
  • 54
  • 67
  • May you please publish a full runnable code ? – Yves Martin Jun 16 '14 at 19:30
  • The `javax.swing.timer` function is designed to perform a specific action whenever the timer reaches a certain time from when it was started whereas the sleep function just pauses the program for a certain ammount of time so they aren't exactly designed to be able to replace eachother. – Tim.DeVries Jun 16 '14 at 19:32
  • Have you tried using a `ScheduledExecutorService.scheduleAtFixedRate`? http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ScheduledExecutorService.html#scheduleAtFixedRate(java.lang.Runnable,%20long,%20long,%20java.util.concurrent.TimeUnit) – Brett Okken Jun 16 '14 at 19:35
  • @YvesMartin I'll try to include a bit more, but it seems that since both methods use the same `repaint()` method, its contents shouldn't matter. – River Jun 16 '14 at 19:39
  • You seem to be confused about the `repaint()` method. It doesn't do a paint, it marks the GUI as needing a repaint. (That's why it executes so fast.) Swing then does the repaint when it does it. Expecting one paint to happen for every time you call repaint is completely mistaken. Apologies if I am misreading you, here. – David Conrad Jun 16 '14 at 19:41
  • @River it may... Swing stacks events for processing by a single thread - and you may have heard about invokeLater mechanism. So you should track timestamps when the "actionPerformed" is really invoked, I guess time has been lost in Swing event queue. – Yves Martin Jun 16 '14 at 19:43
  • Is it possible that, without the sleep, you are keeping the machine so busy and/or blocking the event dispatch thread, so that it can't do a paint in a timely manner? If so, that would explain why the painting is more responsive when you put in `Thread.sleep` calls. – David Conrad Jun 16 '14 at 19:44
  • Kind to add the paint method as well? – MadProgrammer Jun 16 '14 at 19:47
  • @DavidConrad I know I'm pretty much using `repaint()` as a direct call to `paintComponent()` when it really isn't, but shouldn't marking it be as good as repainting it within 100ms? Surely Java responds to a mark faster than that. There is nothing happening between `repaint()` calls, the only purpose is so that the user can see it being rendered over time. – River Jun 16 '14 at 19:51
  • If there's nothing happening between `repaint()` calls, what is there for the user to see? Something ("it") is being rendered, yes? – David Conrad Jun 16 '14 at 19:52
  • @DavidConrad ok you got me. A single integer (`timeStep`) is being incremented between each call. An array of data is having a it's contents at index `timeStep` painted at each timestep – River Jun 16 '14 at 19:55
  • Hmm. That definitely doesn't look like it should prevent Swing from painting. Sorry, I don't know what the problem could be, then. – David Conrad Jun 16 '14 at 19:58
  • @MadProgrammer Added the paint method – River Jun 16 '14 at 20:03
  • Just for comparison, [this example](http://stackoverflow.com/questions/24131513/java-what-is-heavier-canvas-or-paintcomponent/24131544#24131544) animates 10, 000 images at 25 fps (40 ms) – MadProgrammer Jun 16 '14 at 20:09
  • Beware, getScaledInstance could be quite time consuming – MadProgrammer Jun 16 '14 at 20:11
  • @MadProgrammer there's nothing wrong with how the `paintComponent()` runs, I timed it from several places and it only takes .3ms to run. But adding the timer somehow causes an additional 800ms delay, which is unacceptable. – River Jun 16 '14 at 20:20
  • The amount of time paintComponent runs and the time it takes to push it to the screen, two different things ;) – MadProgrammer Jun 16 '14 at 20:25
  • @YvesMartin added the paint method – River Jun 16 '14 at 20:29
  • @MadProgrammer regardless, it pushes to the screen in substantially less than 100ms using `Thread.sleep()` but takes almost a full second with `javax.swing.Timer`. Finding out why is my goal here – River Jun 16 '14 at 20:31
  • 1
    Try to avoid to do "new Font" each time... maybe it does some scan in graphical resources. – Yves Martin Jun 16 '14 at 20:32
  • 2
    Based on your disconnected code snippets, your Thread.sleep example shouldn't work at all, as it would be blocking the EDT. I can't make heads or tails of the drawing loop. Consider providing a runnable example which demonstrates your problem – MadProgrammer Jun 16 '14 at 20:38
  • @MadProgrammer there it is. None of what I added is really relevant to the problem I'm having though.... – River Jun 16 '14 at 20:56
  • I'm still testing your code, your call to getSacaledInstane is causing a repaint to be re-scheduled, even then, I still get about 0.25 milliseconds per update (roughly) – MadProgrammer Jun 16 '14 at 21:32
  • @MadProgrammer that's what I was getting for repaint time(~.3ms). But the time _between_ repaint calls was 800ms for a reason I still can't determine. – River Jun 16 '14 at 22:33

1 Answers1

4

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);)

Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • That does speed up the paint method a bunch, thanks. Using `javax.swing.Timer` I can now iterate through frames fast enough for my uses. 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()`. – River Jun 16 '14 at 22:34
  • Well thanks. I'm not sure I totally understand what you're saying ("`getScaledInstance` is slow, 100's milliseconds slow" seems untrue as using the unsafe `Thread` method rendered it much faster than that) but the idea that `Timer` is slower because too many things getting stuck in the Event Queue seems likely. This fixed my problem and gave me some insight into it so I'll accept it as an answer until I find a better one, if there is one (which I suspect not). – River Jun 16 '14 at 23:53
  • Even using a `Thread` instead of `Timer`, `getScaledInstance` took roughly 200ms – MadProgrammer Jun 17 '14 at 00:00
  • you really should use `System.nanoTime()` if you want accurate readings for those shorter times: [Timing](http://stackoverflow.com/questions/180158/how-do-i-time-a-methods-execution-in-java). However, I agree that `getScaledInstance` accounts for my problem. I re-timed it and it does take up almost all the time between timesteps. I think I got .3ms for my `paintComponent` originally by stupidly timing only the `repaint` method, which was being called much more often. So your original answer was right, `getScaledInstance` slowed down both methods, I was just timing incorrectly. – River Jun 17 '14 at 15:09
  • Personally, I don't think I need nano-second accuracy for 100s of milliseconds – MadProgrammer Jun 17 '14 at 20:30
  • I did say for the _shorter_ times. E.g. the rest of the paint process (10ms) and the update time (0.1ms). These would benefit from said accuracy. I know you didn't show yourself using `System.currentTimeMillis` for these measurements, but even if you didn't, in the interest of not having to switch back and forth based on what you are timing `System.nanoTime` would be useful. – River Jun 17 '14 at 23:12
  • Unless you're going to take over 20, 000 samples, discarding the first 10, 000 for JIT overhead, add erroring for thread overheads and OS injections, the timings are not much more than wild averages. If we were talking about real systems, then I can see a point for that level of precision, if we were attempting to super optimise the process, maybe. But as even you state, anything less the 10ms is wildly inaccurate, not just for the timings, but also for the `Timer` itself ;). I was working on identifying the bottle neck, not super optimising the code ;) – MadProgrammer Jun 17 '14 at 23:28
  • Fair point, just thought I'd mention `System.nanoTime` as using `System.currentTimeMillis` is not a good habit to get into just in case you happened to be in a situation where the accuracy mattered. Here though the difference is indeed moot. – River Jun 17 '14 at 23:33
  • The right tool for the right situation. If I was seeing a time < 10ms (even < 100ms) I might consider using `nanoTime` depending on what I was trying to achieve ;) – MadProgrammer Jun 17 '14 at 23:37