[SOLVED] Java Swing – Timer can't run faster than 15ms

Issue

I’m making a game in Java Swing. In it I have fairly simple graphics and not that many logic operations currently so I was confused why my frameTimes never went below 15ms, even I ran it at bare minimum.

Here’s the declaration of one of my Timers as well as the bare minimum call I mentioned before.

logicTimer = new Timer(1, new TickTimer());
logicTimer.setInitialDelay(0);
...
class TickTimer implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
       System.out.println("Logic processing took "+ (System.currentTimeMillis()-previousTime)+" ms");
       tick();
       previousTime = System.currentTimeMillis();
    }
}
 
public void tick() {}

I have an essentially identical Timer for rendering as well. I render into a JPanel which is contained in a JFrame

For some additional background I’m having these results on a gaming laptop (Lenovo Y720) with a GTX 1060 MaxQ, an i7-7700HQ and 16GB RAM. I also have a similarly specced PC (1070, 7700K) that didn’t have this issue if my memory serves right, but I can’t test it right now. The laptop does have 60Hz screen, but that shouldn’t matter as far as I know.

So I tried running my Timers with the bare minimum calls, but the frameTime was still either 15ms, 16ms, 30ms or 31ms, even though I set the delay to the minimum 1ms with no initial delay. I tried reducing the game’s window size but that also had no effect. I tried unplugging my monitor that is connected to the laptop but the issue persisted. I tried closing any background windows and setting the JDK executable to high and realtime priorities in Windows to no avail.
You might say "15ms? THats still 60 fps, what is there to complain about?" but the problem is that I will have to present this game in a few months, likely on this laptop, and while the performance is just OK now, if I add complicated/lots of logic or visuals then a spike would be immediately noticable. This is assuming that this 15-30ms is a baseline and that if I had 1ms of processing that would result in 1ms higher base latency/frametime too.
After reading some previous questions regarding this issue lots of them came to the conclusion that this is an inherent and unfixable issue, but more modern threads talked about how this was solved with newer version of Swing where better precision Timers were used, so the source of the issue on modern hardware is the use a 32-bit JVM, but I’m fairly sure that I’m running 64-bit, as checked with System.getProperty("sun.arch.data.model"). Not sure if relevant but my JDK version is "17.0.2".

Solution

Bare bones example, running at 1 millisecond

enter image description here

Top text is the time between timer ticks and the middle is the time between repaints.

This demonstrates that there is a 1 millisecond delay between repaints (it does bounce a bit, so it might 1-3 milliseconds). I prefer to use a 5 millisecond delay as there are overheads with the thread scheduling which might make it … "difficult" to achieve 1 millisecond precision

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;

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

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private Instant lastRepaintTick;
        private Instant lastTick;

        private JLabel label = new JLabel();

        public TestPane() {
            setLayout(new BorderLayout());
            label.setHorizontalAlignment(JLabel.CENTER);
            add(label, BorderLayout.NORTH);
            Timer timer = new Timer(1, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (lastTick != null) {
                        label.setText(timeBetween(lastTick, Instant.now()));
                    }
                    repaint();
                    lastTick = Instant.now();
                }
            });
            timer.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 400);
        }

        protected String timeBetween(Instant then, Instant now) {
            Duration duration = Duration.between(then, now);
            String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
            return text;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (lastRepaintTick != null) {
                Graphics2D g2d = (Graphics2D) g.create();
                FontMetrics fm = g2d.getFontMetrics();
                String text = timeBetween(lastRepaintTick, Instant.now());
                int x = (getWidth() - fm.stringWidth(text)) / 2;
                int y = (getHeight() - fm.getHeight()) / 2;
                g2d.drawString(text, x, y + fm.getAscent());
                g2d.dispose();
            }
            lastRepaintTick = Instant.now();
        }

    }
}

Note, this is not the "simplest" workflow, it’s doing a lot work, creating a bunch of short lived objects, which, in of itself, could cause a drag on the frame rate. This leads me to think that your frame calculations are off.

I also tried…

System.out.println("Logic processing took " + (System.currentTimeMillis() - previousTime) + " ms");
if (lastTick != null) {
    label.setText(timeBetween(lastTick, Instant.now()));
}
repaint();
lastTick = Instant.now();
previousTime = System.currentTimeMillis();

which prints 1-2 milliseconds. But please note System.out.println adds additional overhead and might slow down your updates

Java is going to be limited by the underlying hardware/software/driver/JVM implementation. Swing has been using DirectX or OpenGL pipelines for years (at least +5). If you’re experiencing issues on one machine and not another, I’d be trying to explorer the differences between those two machines. I’ve had (lots) of issues with integrated video cards over the years and this tends to come down to the driver on the OS.

15ms is roughly 66fps, so it "could" be a delay in the rendering pipeline limited by the screen/driver/video hardware (guessing), but all my screens are running at 60hz and I don’t have issues (I’m running 3 off my MacBook Pro), although I am running Java 16.0.2.

I’ve also been playing around with this example over the past few weeks. It’s a simple concept of using "line boundary checking for hit detection". The idea was to use a "line of sight" projected through a field of animated objects to determine which ones could be "seen". The original question was suggesting that their algorithm has issues (frame drops) with < 100 entities.

The test I have has 15, 000 animated entities (and 18 fixed entities) and tends to run between 130-170fps (7.69-5.88 milliseconds) with a Timer set to a 5 millisecond interval. This example is NOT optimised and I found that drawing the "hits" was causing most of the slow down, but I don’t honestly know exactly why.

My point been, that a relatively efficient model shouldn’t see a large "degradation" of frame rate and a relatively efficient model should also be able to compensate for it.

If frame rate is really super important to you, then you’re going to need to explore using a "active rendering" model (as apposed to the passive rendering model use by Swing), see BufferStrategy and BufferCapabilities for more details

I just tried your barebones 1ms example and its showing essentially the same results: 0s.01X where the X is 5, 6 or 7. This virtually guarantees that my problem doesn’t have to do with my code, what other fixes can I try?

Well, nothing really. This seems to be a limitation of the hardware/drivers/OS/rendering pipeline.

The question, though, why is it so important? Generally speaking, 15ms is roughly 60fps. Why would you need more?

If you absolutely, without question, must have, the fastest rendering through put, then I’d suggest having a look at using a BufferStrategy. This takes you out of the overhead of the Swing rendering workflows, for example…

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics2D;
import java.awt.image.BufferStrategy;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class Main {

    public static void main(String[] args) {
        Frame frame = new Frame();
        frame.setTitle("Make it so");

        Canvas canvas = new Canvas() {
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(400, 400);
            }
        };
        frame.add(canvas);

        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
        canvas.createBufferStrategy(3);

        Instant lastRepaintTick = null;
        Instant lastTick = null;

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSSS");

        do {
            BufferStrategy bs = canvas.getBufferStrategy();
            while (bs == null) {
                System.out.println("buffer");
                bs = canvas.getBufferStrategy();
            }
            do {
                // The following loop ensures that the contents of the drawing buffer
                // are consistent in case the underlying surface was recreated
                do {
                    // Get a new graphics context every time through the loop
                    // to make sure the strategy is validated
                    System.out.println("draw");
                    Graphics2D g2d = (Graphics2D) bs.getDrawGraphics();

                    // Render to graphics
                    // ...
                    g2d.setColor(Color.LIGHT_GRAY);
                    g2d.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
                    if (lastRepaintTick != null) {
                        FontMetrics fm = g2d.getFontMetrics();
                        String text = timeBetween(lastRepaintTick, Instant.now());
                        int x = (canvas.getWidth() - fm.stringWidth(text)) / 2;
                        int y = (canvas.getHeight() - fm.getHeight()) / 2;
                        g2d.setColor(Color.BLACK);
                        g2d.drawString(text, x, y + fm.getAscent());

                        text = LocalTime.now().format(formatter);
                        x = (canvas.getWidth() - fm.stringWidth(text)) / 2;
                        y += fm.getHeight();
                        g2d.drawString(text, x, y + fm.getAscent());
                    }
                    lastRepaintTick = Instant.now();
                    g2d.dispose();

                    // Repeat the rendering if the drawing buffer contents
                    // were restored
                } while (bs.contentsRestored());

                System.out.println("show");
                // Display the buffer
                bs.show();

                // Repeat the rendering if the drawing buffer was lost
            } while (bs.contentsLost());
            System.out.println("done");
            try {
                Thread.sleep(5);
            } catch (InterruptedException ex) {
            }
        } while (true);
    }

    protected static String timeBetween(Instant then, Instant now) {
        Duration duration = Duration.between(then, now);
        String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
        return text;
    }
}

I did have this running without a loop, but in general, "wild loops" are a bad idea, as they tend to starve other threads.

BufferStrategy example does finally result in frametimes lower than 15ms, so I guess the limitation is somewhere in Timer on my hardware.

Just be careful with what’s been displayed here. The BufferStrategy example is displaying the time between each LOOP cycle, not each frame. AFAIK there’s no way to determine when a frame is actually painted on the screen (or if it is).

You could "try" doing a side by side comparison, but you might find it hard to tell the difference between the two, again, the time between the BufferStrategy rendering a page and presenting might have some drift, so the value "presented" by the BufferStrategy may be stale. Keep it in mind.

enter image description here

Canvas on top

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferStrategy;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Main {

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

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.setLayout(new GridLayout(2, 1));
                TestCanvas testCanvas = new TestCanvas();
                frame.add(testCanvas);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);

                testCanvas.start();
            }
        });
    }

    private static final long TIME_DELAY = 5;
    private static final Dimension PREFERRED_SIZE = new Dimension(200, 35);
    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.nnnnnnnnn");

    protected String timeBetween(Instant then, Instant now) {
        Duration duration = Duration.between(then, now);
        String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
        return text;
    }

    public class TestPane extends JPanel {
        private Instant lastRepaintTick;
        private Instant lastTick;

        public TestPane() {
            setLayout(new BorderLayout());
            Timer timer = new Timer((int) TIME_DELAY, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    repaint();
                    lastTick = Instant.now();
                }
            });
            timer.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return PREFERRED_SIZE;
        }

        protected String timeBetween(Instant then, Instant now) {
            Duration duration = Duration.between(then, now);
            String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
            return text;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (lastRepaintTick != null) {
                Graphics2D g2d = (Graphics2D) g.create();
                FontMetrics fm = g2d.getFontMetrics();
                String text = timeBetween(lastRepaintTick, Instant.now());
                int x = (getWidth() - fm.stringWidth(text)) / 2;
                int y = 0;
                g2d.drawString(text, x, y + fm.getAscent());

                text = LocalTime.now().format(TIME_FORMATTER);
                x = (getWidth() - fm.stringWidth(text)) / 2;
                y += fm.getHeight();
                g2d.drawString(text, x, y + fm.getAscent());
                g2d.dispose();
            }
            lastRepaintTick = Instant.now();
        }
    }

    public class TestCanvas extends Canvas {
        private Thread thread;

        public TestCanvas() {
            thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    createBufferStrategy(3);

                    Instant lastRepaintTick = null;
                    do {
                        BufferStrategy bs = getBufferStrategy();
                        while (bs == null) {
                            bs = getBufferStrategy();
                        }
                        do {
                            // The following loop ensures that the contents of the drawing buffer
                            // are consistent in case the underlying surface was recreated
                            do {
                                // Get a new graphics context every time through the loop
                                // to make sure the strategy is validated
                                Graphics2D g2d = (Graphics2D) bs.getDrawGraphics();

                                // Render to graphics
                                // ...
                                g2d.setColor(getBackground());
                                g2d.fillRect(0, 0, getWidth(), getHeight());
                                if (lastRepaintTick != null) {
                                    FontMetrics fm = g2d.getFontMetrics();
                                    String text = timeBetween(lastRepaintTick, Instant.now());
                                    int x = (getWidth() - fm.stringWidth(text)) / 2;
                                    int y = 0;
                                    g2d.setColor(Color.BLACK);
                                    g2d.drawString(text, x, y + fm.getAscent());

                                    text = LocalTime.now().format(TIME_FORMATTER);
                                    x = (getWidth() - fm.stringWidth(text)) / 2;
                                    y += fm.getHeight();
                                    g2d.drawString(text, x, y + fm.getAscent());
                                }
                                lastRepaintTick = Instant.now();
                                g2d.dispose();

                                // Repeat the rendering if the drawing buffer contents
                                // were restored
                            } while (bs.contentsRestored());

                            // Display the buffer
                            bs.show();

                            // Repeat the rendering if the drawing buffer was lost
                        } while (bs.contentsLost());
                        Instant tickTime = Instant.now();
                        try {
                            Thread.sleep(TIME_DELAY);
                        } catch (InterruptedException ex) {
                        }
                    } while (true);
                }
            });
        }

        public void start() {
            thread.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return PREFERRED_SIZE;
        }

    }
}

Answered By – MadProgrammer

Answer Checked By – Mary Flores (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *