JDK-8298870 : Frame rate drops as number with number of Stages with animation
  • Type: Bug
  • Component: javafx
  • Sub-Component: graphics
  • Affected Version: jfx17
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • OS: linux_ubuntu
  • CPU: x86_64
  • Submitted: 2022-12-14
  • Updated: 2023-09-13
  • Resolved: 2023-01-03
Related Reports
Duplicate :  
Relates :  
Description
ADDITIONAL SYSTEM INFORMATION :
Tested on Ubuntu 20.04. Works fine on Windows 11.

A DESCRIPTION OF THE PROBLEM :
It appears that on Ubuntu Linux, the frame rate is equal to 60 divided by the number of Stages containing an active animation, e.g. some graphics continuously being updated with an AnimationTimer. If only 1 Stage with animation is active, the frame rate is about 60FPS; for 2 Stages with both animations it is goes down to 30FPS; for 3 Stages with active animations: 20FPS; for 4 Stages with active animations: 15FPS; etc. etc.
The frame rate clearly changes when enabling/disabling all the animations in a Stage.
It is worth noting that the number of animated graphics in a Stage does not seem to affect the frame rate. The issue appears as soon as there is one animation.
The issue does not appear on Windows, tested on Windows 11 and Windows 10.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
See attached source code. Run it to create simple Stages with animations. Observe that when creating an additional Stage, the frame rate does not drop until adding and animating the first widget.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The expecting result can be observed when running on Windows, the creation of graphic object has an initial on the frame rate but quickly the frame rate goes back to 60FPS.
ACTUAL -
On Ubuntu Linux, the frame rate is equal to: 60 / N, where N is the number of Stages containing at least one active animation.

---------- BEGIN SOURCE ----------
package us.ihmc.example;

import java.util.concurrent.TimeUnit;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.value.ObservableBooleanValue;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;

public class MultiWindowPerformanceDrop extends Application
{
   @Override
   public void start(Stage primaryStage) throws Exception
   {
      primaryStage.setScene(new Scene(newAnimationPane(), 720, 480));
      primaryStage.show();
   }

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

   /**
    * Creates a new Stage with all the controls populated.
    */
   public static void newStage()
   {
      Stage stage = new Stage();
      Pane animationPane = newAnimationPane();
      stage.setScene(new Scene(animationPane, 720, 480));
      stage.setOnCloseRequest(e -> animationPane.getChildren().clear());
      stage.show();
   }

   /**
    * Creates a new root pane with controls and animations used for the test.
    */
   public static Pane newAnimationPane()
   {
      FlowPane pane = new FlowPane();

      // Button to add an animate-able widget to the pane
      Button add = new Button("Add gizmo");
      // Button to remove the last widget added.
      Button remove = new Button("Remove gizmo");
      // Toggle the animation state for all the widgets contained in the pane.
      ToggleButton animate = new ToggleButton("Animate");
      // Button for creating another stage with a separate animation pane.
      Button createStage = new Button("Create Stage");

      pane.getChildren().addAll(add, remove, animate, new FPSDisplay(), createStage);

      add.setOnAction(e ->
      {
         pane.getChildren().add(new AnimatedGizmo(animate.selectedProperty()));
      });

      remove.setOnAction(e ->
      {
         int lastChildIndex = pane.getChildren().size() - 1;
         Node lastNode = pane.getChildren().get(lastChildIndex);

         if (lastNode instanceof AnimatedGizmo)
            pane.getChildren().remove(lastChildIndex);
      });

      createStage.setOnAction(e ->
      {
         newStage();
      });

      return pane;
   }

   /**
    * A little widget for keeping track of the current frame rate.
    */
   private static class FPSDisplay extends HBox
   {
      private final Label fpsLabel = new Label();

      /** For filtering the frame rate update to 2Hz. */
      private long timeIntervalBetweenUpdates = TimeUnit.MILLISECONDS.toNanos(500);

      /** To keep track of the last time the frame rate was computed. */
      private long timeLast = -1;
      /** Number of rendered frames since last time we computed frame rate. */
      private int frameCounter = 0;

      /** Timer used to compute the frame rate. */
      private final AnimationTimer timer = new AnimationTimer()
      {
         @Override
         public void handle(long timeNow)
         {
            if (timeLast == -1)
            { // First call ever, initialize.
               timeLast = timeNow;
               frameCounter = 0;
            }
            else
            {
               // Increment number of frame rendered since timeLast.
               frameCounter++;

               if (timeNow - timeLast >= timeIntervalBetweenUpdates)
               { // It is time to compute our new averaged frame rate.
                 // Duration since the last time we computed the frame rate.
                  double timeElapsedInSeconds = (timeNow - timeLast) * 1.0e-9;
                  // Our averaged frame rate.
                  double framesPerSecond = frameCounter / timeElapsedInSeconds;
                  fpsLabel.setText(String.format("%6.2f FPS", framesPerSecond));

                  timeLast = timeNow;
                  frameCounter = 0;
               }
            }
         }
      };

      public FPSDisplay()
      {
         setPrefSize(50, 50);
         setAlignment(Pos.CENTER);
         fpsLabel.setMinWidth(70);
         getChildren().add(fpsLabel);

         parentProperty().addListener((o, oldValue, newValue) ->
         {
            if (newValue != null)
               timer.start();
            else
               timer.stop();
         });
      }
   }

   /**
    * A little widget to simulate some graphic animation happening.
    */
   private static class AnimatedGizmo extends AnchorPane
   {
      /** We use a triangle that will rotate to simulate an animation. */
      private final Polygon triangle = new Polygon();
      /** The transform used to rotate the triangle. */
      private final Rotate rotate = new Rotate();
      /** The animation. */
      private final AnimationTimer timer = new AnimationTimer()
      {
         @Override
         public void handle(long now)
         {
            rotate.setAngle(rotate.getAngle() + 1.0);
         }
      };

      public AnimatedGizmo(ObservableBooleanValue animate)
      {
         setPrefSize(50, 50);
         double radius = 0.5;
         double cos60 = Math.sqrt(3) / 2.0;
         double sin60 = 0.5;
         // Drawing a triangle
         triangle.getPoints().addAll(0.0, -radius, cos60 * radius, sin60 * radius, -cos60 * radius, sin60 * radius);
         triangle.setStroke(null);
         triangle.setFill(Color.BLUE);
         // Position the triangle in the center of the pane
         DoubleBinding halfWidth = widthProperty().divide(2.0);
         DoubleBinding halfHeight = heightProperty().divide(2.0);
         Translate translate = new Translate();
         translate.xProperty().bind(halfWidth);
         translate.yProperty().bind(halfHeight);
         // Scale the triangle to fill the pane
         Scale scale = new Scale();
         NumberBinding scaleFactor = Bindings.min(halfWidth, halfHeight).divide(radius);
         scale.xProperty().bind(scaleFactor);
         scale.yProperty().bind(scaleFactor);

         triangle.getTransforms().addAll(translate, scale, rotate);

         animate.addListener((o, oldValue, newValue) ->
         {
            rotate.setAngle(0.0);
            if (newValue && getParent() != null)
               timer.start();
            else
               timer.stop();
         });

         parentProperty().addListener((o, oldValue, newValue) ->
         {
            rotate.setAngle(0.0);
            if (newValue != null && animate.get())
               timer.start();
            else
               timer.stop();
         });

         getChildren().add(triangle);
      }
   }
}

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

CUSTOMER SUBMITTED WORKAROUND :
I have not found any workaround at this moment. This issue makes any multi-window JavaFX application really slow on Linux.

FREQUENCY : always



Comments
Additional information from submitter: =========================== Attached clipping from submitter
03-01-2023

This is a possible duplicate of JDK-8291958
16-12-2022

Checked with attached testcase in Ubuntu 20.04, issue could not be reproduced <attached short clipping> Test Result ========= openjfx11: Pass openjfx17: Pass openjfx19: Pass openjfx20ea11: Pass Mail to submitter ============== Please share a short clipping of the Issue
15-12-2022