A more precise timer for Java

Posted by – August 2, 2011

I was trying to benchmark a Swing app I’d written, but found I was reaching a ceiling at 1000 frames per second. The source of this ceiling turns out to be the fact that, on Windows at least, there’s no way to set up a timer for less than 1 millisecond. I tried the 2-argument version of Thread.sleep(), I tried Object.wait(), I tried java.util.concurrent.locks.LockSupport.parkNanos(), I tried constructing a Timer with 0 initial delay… still no joy. There’s no problem timing things with System.nanoTime(), but there’s no way to control things… until now!

I knocked together the following extremely dirty solution in about 10 minutes:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.Timer;

public class NanoTimer {

    public final int TARGET_DELAY_NS = 200;
    public final int POLL_FREQ_MS = 1000;
    private int updates = 0;
    private int cycles = 500 * TARGET_DELAY_NS;

    private Timer rateChecker = new Timer(POLL_FREQ_MS, new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {

            double delay = POLL_FREQ_MS * 1000.0 / updates;
            double ratio = delay / TARGET_DELAY_NS;
            cycles /= ratio;
            updates = 0;
        }
    });

    private class QuickTimer implements Runnable {

        @Override
        public void run() {
            while (true) {
                updates++;
                doWork();
                for (int i = 0; i < cycles; i++) {
                    double x = i/7;
                }
            }
        }
    }

    public void go(){
        new Thread(new QuickTimer()).start();
        rateChecker.start();
    }

    public void doWork(){
        // Add your work here
    }

    public static void main(String[] args) {
        new NanoTimer().go();
    }
}

Can you see how it works? If you’re old enough to remember the 8-bit computers of the early ’80s, you may know that a common technique for pausing in languages (i.e. BASIC) with no PAUSE command, was to loop a large number of times (how many exactly was left to trial and error). And that’s what we do here, except the computer does the trial and error for us and dynamically updates the number of cycles in the loop.

It’s not quite as bad as it sounds, because on a multi-core processor this will only take up one core, leaving your actual application run in the other(s). My test application running on full burn slowed down by only about 18%. Here it is repainting a GUI at over 5000 frames per second:Adding some bell and whistles to allow things like stopping it, changing the delay and poll freqency, adding constructors, getting it to behave more like the standard Swing Timer (i.e. throwing out ActionEvents) complicates things a bit, but here goes anyway:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.Timer;

public class NanoTimer {

    private double TARGET_DELAY_NS;
    private int POLL_FREQ_MS;
    private long updates = 0;
    private int cycles;
    private int cyclesLast = 0;
    private boolean isRunning = false;
    private boolean stopping = false;
    private boolean doneInitializing = false;
    private double dummy;
    private final PublicTimer PUBLIC_TIMER = new PublicTimer();

    // Swing's Timer class's method to fire events is protected, so we need to extend it
    // to fire off events manually
    private class PublicTimer extends Timer {

        private PublicTimer(){
            super(Integer.MAX_VALUE, null);
        }

        public void fireActionPerformedPublic () {
            fireActionPerformed(new ActionEvent(NanoTimer.this, 0, ""));
        }
    }

    private class QuickTimer implements Runnable {

        @Override
        public void run() {
            while (!stopping) {
                updates++;
                if(doneInitializing) {
                    PUBLIC_TIMER.fireActionPerformedPublic();
                }
                for (int i = 0; i < cycles; i++) {
                    dummy = i/7;
                }
            }
            stopping = false;
            isRunning = false;
            doneInitializing = false;
        }
    }

    private Timer rateChecker = new Timer(POLL_FREQ_MS, new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
            cyclesLast = cycles;
            double delay = POLL_FREQ_MS * 1000.0 / updates;
            double ratio = delay / TARGET_DELAY_NS;
            cycles /= ratio;

            float cyclesRatio = ((float) cyclesLast) / cycles;
            if(cyclesRatio < 1.1 && cyclesRatio > 0.9)
                doneInitializing = true;
            updates = 0;
        }
    });

    public NanoTimer(int delayNanos) {
        this(delayNanos, null);
    }

    public NanoTimer(int delayNanos, ActionListener al) {
        this(delayNanos, 100, al);
    }

    public NanoTimer(int delayNanos, int pollFreqMillis, ActionListener al){
        if(pollFreqMillis * 1000 / delayNanos < 10) {
            throw new IllegalArgumentException("Poll frequency must be 10 times or greater than the delay");
        }
        TARGET_DELAY_NS = delayNanos;
        POLL_FREQ_MS = pollFreqMillis;
        rateChecker.setDelay(POLL_FREQ_MS);
        cycles = (int)(500 * TARGET_DELAY_NS);
         addActionListener(al);
    }

    public void start(){
        if(!isRunning) {
            Thread nanoThread = new Thread(new QuickTimer());
            nanoThread.start();
            rateChecker.start();
            isRunning = true;
        }
    }

    public void stop(){
        if(isRunning) {
            stopping = true;
            rateChecker.stop();
        }
    }

    public int getPollMs() { return POLL_FREQ_MS; }
    public void setPollMs(int POLL_FREQ_MS) {
        if(POLL_FREQ_MS * 1000 / TARGET_DELAY_NS < 10) {
            throw new IllegalArgumentException("Poll frequency must be 10 times or greater than the delay");
        }
        doneInitializing = false;
        this.POLL_FREQ_MS = POLL_FREQ_MS;
        rateChecker.setDelay(POLL_FREQ_MS);
     }
    public double getDelayNs() { return TARGET_DELAY_NS; }
    public void setDelayNs(double TARGET_DELAY_NS) { this.TARGET_DELAY_NS = TARGET_DELAY_NS; }
    public void addActionListener(ActionListener al) {PUBLIC_TIMER.addActionListener(al); }
    public void removeActionListener(ActionListener al) {PUBLIC_TIMER.removeActionListener(al); }
} 

It’s far from perfect: in particular:
– changing poll frequency while running sometimes stops events altogether
– it’s not thread-safe: strange things might happen due to methods accessing the instance variables as they’re changed by the setter methods
– the initialization period before events are fired could be made a lot shorter, perhaps using System.nanoTime

However it does a pretty good job under a normal conditions. Firing a million events per second? No problem. Here’s a test class which you can play around with:

class NanoTimerTest {

    public static void main(String[] args) {
        NanoTimer n = new NanoTimer(200);
        final TestListener tl = new TestListener();

        Timer t = new Timer(1000, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                tl.getUpdate();
            }
        });
        n.addActionListener(tl);
        t.start();
        n.start();
        sleep();
        System.out.println("Change poll");
        n.setPollMs(50);
        sleep();
        sleep();
        System.out.println("Change delay");
        n.setDelayNs(2);
        sleep();
        System.out.println("stopping");
        n.stop();
        sleep();
        System.out.println("starting");
        n.start();
        sleep();
        System.out.println("removing ActionListener");
        n.removeActionListener(tl);
        sleep();
        System.out.println("adding ActionListener");
        n.addActionListener(tl);
    }

    static void sleep() {try{Thread.sleep(3000);}catch(Exception e){}}

}

class TestListener implements ActionListener {

    private int count = 0;
    private long t0 = System.currentTimeMillis();

    @Override
    public void actionPerformed(ActionEvent e) {
        count++;
    }

    public void getUpdate() {
        long t1 = System.currentTimeMillis();
        System.out.printf("TestListener received %d events in %.2f seconds%n", count, (t1 - t0) / 1e3);
        count = 0;
        t0 = t1;
    }
}

Let me know if you find this useful for anything!

Leave a Reply

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