JDK-8211059 : JavaFX AnimationTimer running at way more than 60 fps
  • Type: Bug
  • Component: javafx
  • Sub-Component: graphics
  • Affected Version: 8u181
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • OS: linux
  • CPU: x86_64
  • Submitted: 2018-09-23
  • Updated: 2018-09-24
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdUnresolved
Description
ADDITIONAL SYSTEM INFORMATION :
Running fedora 27. Video is handled by whatever is provided by fedora (that is, I am not using proprietary video drivers). PC has 16GiB memory; CPU is an Athlon FX-8320 Black Edition 8-core processor. JRE is the one supplied with the JDK.

A DESCRIPTION OF THE PROBLEM :
While proofreading a new version of an online Java textbook I ran into a problem running a sample program from the book that illustrates JavaFX AnimationTimer. The program source is SimpleAnimationTimer.java. See below. It is a very simple program that simply displays the current frame number and the elapsed time in seconds. The expected output is to see "60 frames per second"; that is, the displayed frame number divided by the displayed elapsed time should be 60.

When compiled and run on my PC it floods the application window so much that I am almost unable to move my mouse to another window so that I can kill the running program. When I notified the book's author, he responded that the program runs fine on all his PCs (Ubuntu, Linux Mint, Mac, Windows). My PC is running Fedora 27.

I downloaded and ran the latest Linux Mint from a live USB, then tested the program there using the same Java as is used for Fedora 27. The program runs without error there.

Next I rebooted an older Fedora 24 and tested the program there. It ran without error.

I downloaded and ran Fedora 28 from a live USB. The program has the problem there.

Summary of my testing environment and results.

Java version is 1.8.0_181. It is the one from Oracle.

Linux Mint 19 kernel 4.15.0-20-generic. No problems.

Fedora 24 kernel 4.11.12. No problems.

Fedora 27 kernel 4.17.17. Has the problem.

Fedora 28 kernel 4.16.3. Has the problem.

It appears that something changed in Linux between kernel versions 4.15 and 4.16 that is causing the problem.

The original SimpleAnimationStarter.java source was modified to stop after 60 frames. The Test02SimpleAnimationStarter.java source, shown below, does this. When this program was run, the displayed frame number was 60, as expected, and the displayed elapsed time was 0.3 seconds, not the expected 1 second.

More recently, thanks to a suggestion from the author of the book, I modified the original source to stop the animation after fixed number of frames were displayed. I then started top in one terminal window then started the newer test program in another terminal window. While it was running, top showed %CPU values of 109.2 (once) with other values above 50 until the program stopped the animation. This is surely a clue.

I am no expert, but it appears to me that something changed in the linux kernel between 4.15 and 4.16 that, together with JavaFX, is causing the problem.


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Download the 2 source modules, SimpleAnimationStarter.java and Test02SimpleAnimationStarter.java.
2. Compile them using "javac <name>" where <name> is one of the 2 names.
3. WARNING: Since running SimpleAnimationStarter causes the problem, run Test02SimpleAnimationStarter first! Continue to next step.
4. From terminal window: "java Test02SimpleAnimationStarter". See expected and actual results below.
5. From terminal window: "java SimpleAnimationStarter". See expected and actual results below.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Expected result when doing Step 4:
1. Displayed "Frame number" value increases from 1 to 60
2. Displayed "Elapsed Time:" value increases from 0 to 1 seconds.

Expected result when doing Step 5:
1. Displayed "Frame number" value increases from 1 and keeps increasing
2. Displayed "Elapsed Time:" value increases from 0 and keeps increasing; manually computing fps (frames per second) yields approximately 60 fps.
3. While program is running, are able to easily move to other windows, use mouse, run other programs, etc.

Note: as reported to me by the author of the Java book, his actual results when running on Windows 10, Ubuntu, Linux Mint, and Mac are the expected results.

ACTUAL -
Actual result when doing Step 4:
1. Displayed "Frame number" value increases from 1 to 60
2. Displayed "Elapsed Time:" value increases from 0 to 0.3 seconds. (Final value higher for first run. 0.3 for subsequent runs.)

Actual result when doing Step 5:
1. Displayed "Frame number" value increases from 1 and keeps increasing
2. Displayed "Elapsed Time:" value increases from 0 and keeps increasing; manually computing fps yields approximately 200+ fps.
3. The %CPU value for the program that is shown in another terminal window running top shows excessive CPU for the program (all in excess of 50; some 80s; one 109).
4. Mouse almost unusable. Did manage to move to another terminal window, but it took awhile. Then tried to enter a kill command to end the process. Keyboard was pretty much unresponsive and pressed keys backed up, resulting in things like "killlllllllll" appearing when I'd typed "kill". Eventually was able to kill the process.

Note: as reported to me by the author of the Java book, his actual results when running on Windows 10, Ubuntu, Linux Mint, and Mac are the expected results. Also, my actual result described here is when running fedora 27 and fedora 28. When running fedora 24 or Linux Mint 19, my actual result was the same as the expected result.


---------- BEGIN SOURCE ----------
Source code follows for both source modules.

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.layout.BorderPane;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

/**
 *  This file can be used to create very simple animations.  Just fill in
 *  the definition of drawFrame with the code to draw one frame of the 
 *  animation, and possibly change a few of the values in the rest of
 *  the program as noted below.
 */
public class SimpleAnimationStarter extends Application {

    /**
     * Draws one frame of an animation. This subroutine should be called
     * about 60 times per second.  It is responsible for redrawing the
     * entire drawing area. The parameter g is used for drawing. The frameNumber 
     * starts at zero and increases by 1 each time this subroutine is called.  
     * The parameter elapsedSeconds gives the number of seconds since the animation
     * was started.  By using frameNumber and/or elapsedSeconds in the drawing
     * code, you can make a picture that changes over time.  That's an animation.
     * The parameters width and height give the size of the drawing area, in pixels.  
     */
    public void drawFrame(GraphicsContext g, int frameNumber, double elapsedSeconds, int width, int height) {

        /* NOTE:  To get a different animation, just erase the contents of this 
         * subroutine and substitute your own. 
         */

        g.setFill(Color.WHITE);
        g.fillRect(0, 0, width, height); // First, fill the entire image with a background color!

        g.setFill(Color.BLACK);
        g.fillText( "Frame number " + frameNumber, 40, 50 );
        g.fillText( String.format("Elapsed Time: %1.1f seconds", elapsedSeconds), 40, 80);

    }

    //------ Implementation details: DO NOT EXPECT TO UNDERSTAND THIS ------


    private int frameNum;
    private long startTime;

    public void start(Stage stage) {
        int width = 800;   // The width of the image.  You can modify this value!
        int height = 600;  // The height of the image. You can modify this value!
        Canvas canvas = new Canvas(width,height);
        drawFrame(canvas.getGraphicsContext2D(), 0, 0, width, height);
        BorderPane root = new BorderPane(canvas);
        root.setStyle("-fx-border-width: 4px; -fx-border-color: #444");
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setTitle("Simple Animation"); // STRING APPEARS IN WINDOW TITLEBAR!
        stage.show();
        stage.setResizable(false);
        AnimationTimer anim = new AnimationTimer() {
            public void handle(long now) {
                if (startTime < 0)
                    startTime = now;
                frameNum++;
                drawFrame(canvas.getGraphicsContext2D(), frameNum, (now-startTime)/1e9, width, height);
            }
        };
        startTime = -1;
        anim.start();
    } 

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

} // end SimpleAnimationStarter



import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.layout.BorderPane;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

/**
 *  This file can be used to create very simple animations.  Just fill in
 *  the definition of drawFrame with the code to draw one frame of the 
 *  animation, and possibly change a few of the values in the rest of
 *  the program as noted below.
 */
public class Test02SimpleAnimationStarter extends Application {

    /**
     * Draws one frame of an animation. This subroutine should be called
     * about 60 times per second.  It is responsible for redrawing the
     * entire drawing area. The parameter g is used for drawing. The frameNumber 
     * starts at zero and increases by 1 each time this subroutine is called.  
     * The parameter elapsedSeconds gives the number of seconds since the animation
     * was started.  By using frameNumber and/or elapsedSeconds in the drawing
     * code, you can make a picture that changes over time.  That's an animation.
     * The parameters width and height give the size of the drawing area, in pixels.  
     */
    public void drawFrame(GraphicsContext g, int frameNumber, double elapsedSeconds, int width, int height) {

        /* NOTE:  To get a different animation, just erase the contents of this 
         * subroutine and substitute your own. 
         */

        g.setFill(Color.WHITE);
        g.fillRect(0, 0, width, height); // First, fill the entire image with a background color!

        g.setFill(Color.BLACK);
        g.fillText( "Frame number " + frameNumber, 40, 50 );
        g.fillText( String.format("Elapsed Time: %1.1f seconds", elapsedSeconds), 40, 80);

    }

    //------ Implementation details: DO NOT EXPECT TO UNDERSTAND THIS ------


    private int frameNum;
    private long startTime;

    AnimationTimer anim = null;  // Moved outside start() so handle() can call anim.stop()

    public void start(Stage stage) {
        int width = 800;   // The width of the image.  You can modify this value!
        int height = 600;  // The height of the image. You can modify this value!
        Canvas canvas = new Canvas(width,height);
        drawFrame(canvas.getGraphicsContext2D(), 0, 0, width, height);
        BorderPane root = new BorderPane(canvas);
        root.setStyle("-fx-border-width: 4px; -fx-border-color: #444");
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setTitle("Simple Animation"); // STRING APPEARS IN WINDOW TITLEBAR!
        stage.show();
        stage.setResizable(false);
//        AnimationTimer anim = new AnimationTimer() {            
        anim = new AnimationTimer() {
            // Constants: change NUM_SECS and/or framesPerSec
            private final int NUM_SECS = 1; // Desired number of seconds.
            private final int framesPerSec = 60; // Expected frames per second.
            private final int NUM_FRAMES = NUM_SECS*framesPerSec;

            public void handle(long now) {
                if (startTime < 0)
                    startTime = now;
                frameNum++;
                if (frameNum < NUM_FRAMES+1)
                {
                    drawFrame(canvas.getGraphicsContext2D(),
                              frameNum,
                              (now-startTime)/1e9,
                              width, height);
                }
                else if (frameNum == NUM_FRAMES+1)
                {
                    anim.stop();
                    System.out.println("Animation stopped");
                }
                else if (frameNum == NUM_FRAMES+2)
                {
                    System.out.println("Animation kept going after stop ???");
                }
            }
        };
        startTime = -1;
        anim.start();
    }

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

} // end Test02SimpleAnimationStarter


---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Modifed the handle() method to "slow down to a fixed frame rate".
1. Add 2 lines just above the start() method:
    private int framesPerSec = 60; // Desired frames per second. You can modify this value!
    private long nSecPerFrame = Math.round(1.0/framesPerSec * 1e9);
2. Modify the handle() method within the AnimationTimer:
        AnimationTimer anim = new AnimationTimer() {
            private long lastUpdate = 0;
            public void handle(long now) {
                if (now - lastUpdate > nSecPerFrame) {
                    if (startTime < 0)
                        startTime = now;
                    frameNum++;
                    drawFrame(canvas.getGraphicsContext2D(),
                              frameNum,
                              (now-startTime)/1e9,
                              width, height);
                    lastUpdate = now;
                }
            }
        };

With this modification, SimpleAnimationStarter works as expected. In addition, the Java book has many other examples that use AnimationStarter that have the same problem. I modified them using the same technique; they now all work. Note: the book is being updated from using Swing to using JavaFX; the sample animation programs, Swing version, and the sample animation programs JavaFX version (with the workaround) all work as expected.

FREQUENCY : always