什么是获取JavaFX应用程序帧率的首选方法?

10
这是一个非常简单的问题:
什么是获取JavaFX应用程序帧速率的首选方法?
谷歌搜索结果来自2009年,但该示例涉及JavaFX 1.x,并以一种奇怪的方式(某种外部计量器)运行。找不到更好的示例,因此在此发布。
我希望能够查询我的JavaFX应用程序(或必要时当前场景)并获得当前FPS。
更新:2015年2月8日
以下是针对该问题的各种解决方案作为答案发布。我还发现这个问题被以下博客文章引用:http://tomasmikula.github.io/blog/2015/02/08/measuring-fps-with-reactfx.html 该文章指出(由于下面解决方案的冗长),测量FPS已添加到ReactFX 2.0里程碑2中。很酷的是事情如何传播。

有一个名为com.sun.javafx.perf.PerformanceTracker的类,但我找不到它的javadoc。我之前下载了一个使用它的弹跳球程序。 - brian
1
不会有Javadoc,因为它不是公共API的一部分。 - James_D
不会有任何Javadoc,因为它不是公共API的一部分。这是获得FPS的唯一可靠方法。在Java9中完全删除了它,而且它确实起作用。这里给出的天真应用程序无法接近正确的FPS。动画调用返回并不意味着显卡能够提供所要求的内容。通过在Java游戏上更改屏幕分辨率,我可以通过NVidia软件运行,并通过其他性能监视器报告130-300FPS之间的任何位置。但是天真的实现始终显示250FPS-无论实际情况如何。 - wax_lyrical
@wax_lyrical 我认为你正在寻找与OP所问的完全不同的东西。 OP,AIUI,正在询问JavaFX重新渲染场景图的频率(因此,例如,您可以监视您的渲染代码是否防止JavaFX工具包以其名义目标的60fps进行渲染)。 您的外部性能监视器将测量您的显卡的性能,这是完全不同的。 - James_D
嗨詹姆斯。他问如何测量FPS。FPS的正常解释是GPU每秒渲染多少帧到屏幕上。你肯定不会对此有异议吧?而不是循环调用绘制的次数。旧的PerformanceTracker曾经(据我所知)与我的外部监视器达成一致。如果PRISM在60FPS时脉动,那就是OP应该看到的。但如果他使用了-Djavafx.animation.fullspeed=true,那么我们可以期望他在内部和外部指标之间得到一些一致性。你所有的指标报告的只是我运行循环的频率? - wax_lyrical
问题的意图确实是想检查JavaFX重新绘制帧的频率。这与GPU实际正在执行的任务有何关联,在我看来,超出了问题的范围。 - brcolow
3个回答

14

您可以使用AnimationTimer

AnimationTimer的handle方法在每帧调用一次,传入的值是当前时间(最佳近似值)的纳秒数。因此,您可以跟踪自上一帧以来的时间。

以下是一个实现,它跟踪最后100个帧的时间,并使用它们计算帧速率:

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class SimpleFrameRateMeter extends Application {

    private final long[] frameTimes = new long[100];
    private int frameTimeIndex = 0 ;
    private boolean arrayFilled = false ;

    @Override
    public void start(Stage primaryStage) {

        Label label = new Label();
        AnimationTimer frameRateMeter = new AnimationTimer() {

            @Override
            public void handle(long now) {
                long oldFrameTime = frameTimes[frameTimeIndex] ;
                frameTimes[frameTimeIndex] = now ;
                frameTimeIndex = (frameTimeIndex + 1) % frameTimes.length ;
                if (frameTimeIndex == 0) {
                    arrayFilled = true ;
                }
                if (arrayFilled) {
                    long elapsedNanos = now - oldFrameTime ;
                    long elapsedNanosPerFrame = elapsedNanos / frameTimes.length ;
                    double frameRate = 1_000_000_000.0 / elapsedNanosPerFrame ;
                    label.setText(String.format("Current frame rate: %.3f", frameRate));
                }
            }
        };

        frameRateMeter.start();

        primaryStage.setScene(new Scene(new StackPane(label), 250, 150));
        primaryStage.show();
    }

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

1
我得到稳定的约60个值……我没有发现任何错误,但可能存在一个。请注意,动画定时器每帧运行一次:JavaFX旨在以60fps运行,但不能保证帧速率。有趣的是,无论我尝试什么方法,当我调整框架大小时,帧速率都会增加,这似乎很奇怪(但我想不是不可能的)。 - James_D
4
我使用了ReactFX来改编这个答案:http://tomasmikula.github.io/blog/2015/02/08/measuring-fps-with-reactfx.html。 - Tomas Mikula
错误:arrayFilled 从未重置为 false,因此在第一次为 true 后,它将在每个帧中都为 true,而不是在数组填充时为 true。 - Yan.F
@Yan.F 这是预期的行为。该标志应指示数组是否在某个时刻被填充(这是所需的全部内容)。 - James_D
@Yan.F 但我不想只使用一次平均值。我希望它在填充数组后立即开始计算平均值,然后每次读取单个时间戳时更新该值。逻辑正是我想要的。 - James_D
显示剩余2条评论

3

我刚刚复制了James_D的程序,并改用PerformanceTracker。我从之前下载的名为JavaFXBalls3的程序中复制了选项。这些选项似乎没有什么区别。

按任意键查看标签显示的内容。我的始终接近60。也许更复杂的场景会更低。据我所知,60是动画的最大值。

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

import com.sun.javafx.perf.PerformanceTracker;
import java.security.AccessControlException;
import javafx.animation.AnimationTimer;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;


public class FPS extends Application {
    public static void main(String[] args) { launch(args); }

    private static PerformanceTracker tracker;

    @Override
    public void start(Stage stage) {
        VBox root = new VBox(20);
        Label label1 = new Label();
        Label label2 = new Label();
        root.getChildren().addAll(label1, label2);

        Scene scene = new Scene(root, 200, 100);

        try {
            System.setProperty("prism.verbose", "true");
            System.setProperty("prism.dirtyopts", "false");
            //System.setProperty("javafx.animation.fullspeed", "true");
            System.setProperty("javafx.animation.pulse", "10");
        } catch (AccessControlException e) {}

        scene.setOnKeyPressed((e)->{
            label2.setText(label1.getText());
        });
        stage.setScene(scene);
        stage.show();


        tracker = PerformanceTracker.getSceneTracker(scene);
        AnimationTimer frameRateMeter = new AnimationTimer() {

            @Override
            public void handle(long now) {
                label1.setText(String.format("Current frame rate: %.3f fps", getFPS()));
            }
        };

        frameRateMeter.start();
    }

    private float getFPS () {
        float fps = tracker.getAverageFPS();
        tracker.resetAverageFPS();
        return fps;
    }

}

1
谢谢您的回复,虽然我不太喜欢在私有API中使用类,但知道这个类的存在还是很好的! - brcolow
我认为该API仅用于测试,我不确定它是否提供更有用的内容。我可以看到源代码但看不到Javadoc。我认为它只有一些辅助方法,但没有真正关键的东西。 - brian
尝试编译非常相似的代码时出现以下错误:Error:(X, Y) java: package com.sun.javafx.perf is not visible (package com.sun.javafx.perf is declared in module javafx.graphics, which does not export it to the unnamed module)。因此,我猜测这个答案不再适用。 - nhooyr
实际上,如果使用带有jigsaw(JPMS)的Java 9+ JDK,则此私有API类将不可见。您可以添加一些命令行技巧将其导出到未命名模块,但最终这将不可能,因此,在我看来,最好不要使用私有API类。 - brcolow

3

James_D提供了一个简单的实现,可以给出瞬时FPS,并建议采用更复杂的方法。我尝试的方法如下:

public class FXUtils
{
    private static long lastUpdate = 0;
    private static int index = 0;
    private static double[] frameRates = new double[100];

    static
    {
        AnimationTimer frameRateMeter = new AnimationTimer()
        {
            @Override
            public void handle(long now)
            {
                if (lastUpdate > 0)
                {
                    long nanosElapsed = now - lastUpdate;
                    double frameRate = 1000000000.0 / nanosElapsed;
                    index %= frameRates.length;
                    frameRates[index++] = frameRate;
                }

                lastUpdate = now;
            }
        };

        frameRateMeter.start();
    }

    /**
     * Returns the instantaneous FPS for the last frame rendered.
     *
     * @return
     */
    public static double getInstantFPS()
    {
        return frameRates[index % frameRates.length];
    }

    /**
     * Returns the average FPS for the last 100 frames rendered.
     * @return
     */
    public static double getAverageFPS()
    {
        double total = 0.0d;

        for (int i = 0; i < frameRates.length; i++)
        {
            total += frameRates[i];
        }

        return total / frameRates.length;
    }
}

一种更高效的方法是将实际时间值(now)存储在long[]中。然后(一旦数组被填满),只需获取旧值并从新值中减去它,然后替换它。两者之间的差异是最后n帧的时间。我会更新我的答案... - James_D
在模运算之后增加索引会导致数组越界异常。 - Roland
我没有收到异常。这是因为我使用的是先获取再增加,而不是先增加再获取(即index++和++index的区别)。 - brcolow
我已经找到了问题。在你将值加到100和进行下一个模操作之间的短暂事件中,通过调用getInstantFPS()来获取它。它绝不能是数组的大小,即100。 - Roland
将getInstantFPS()更改为:return frameRates[index%frameRates.length];应该可以解决问题。感谢您的发现!我现在会更新我的答案。 - brcolow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接