3

My application generates heatmap images as fast as the CPU can (around 30-60 per second) and I want to display them in a single "live heatmap". In AWT/Swing, I could just paint them into a JPanel which worked like a charm. Recently, I switched to JavaFX and want to achieve the same here; at first, I tried with a Canvas, which was slow but okay-ish but had a severe memory leak problem, causing the application to crash. Now, I tried the ImageView component - which apparently is way too slow as the image gets quite laggy (using ImageView.setImage on every new iteration). As far as I understand, setImage does not guarantee that the image is actually displayed when the function finishes.

I am getting the impression that I am on the wrong track, using those components in a manner they are not made to. How can I display my 30-60 Images per second?

EDIT: A very simple test application. You will need the JHeatChart library. Note that on a desktop machine, I get around 70-80 FPS and the visualization is okay and fluid, but on a smaller raspberry pi (my target machine), I get around 30 FPS but an extremly stucking visualization.

package sample;

import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.tc33.jheatchart.HeatChart;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.LinkedList;

public class Main extends Application {
ImageView imageView = new ImageView();
final int scale = 15;

@Override
public void start(Stage primaryStage) {
    Thread generator = new Thread(() -> {
        int col = 0;
        LinkedList<Long> fps = new LinkedList<>();
        while (true) {
            fps.add(System.currentTimeMillis());
            double[][] matrix = new double[48][128];
            for (int i = 0; i < 48; i++) {
                for (int j = 0; j < 128; j++) {
                    matrix[i][j] = col == j ? Math.random() : 0;
                }
            }
            col = (col + 1) % 128;

            HeatChart heatChart = new HeatChart(matrix, 0, 1);
            heatChart.setShowXAxisValues(false);
            heatChart.setShowYAxisValues(false);
            heatChart.setLowValueColour(java.awt.Color.black);
            heatChart.setHighValueColour(java.awt.Color.white);
            heatChart.setAxisThickness(0);
            heatChart.setChartMargin(0);
            heatChart.setCellSize(new Dimension(1, 1));

            long currentTime = System.currentTimeMillis();
            fps.removeIf(elem -> currentTime - elem > 1000);
            System.out.println(fps.size());

            imageView.setImage(SwingFXUtils.toFXImage((BufferedImage) scale(heatChart.getChartImage(), scale), null));
        }
    });

    VBox box = new VBox();
    box.getChildren().add(imageView);

    Scene scene = new Scene(box, 1920, 720);
    primaryStage.setScene(scene);
    primaryStage.show();

    generator.start();
}


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

private static Image scale(Image image, int scale) {
    BufferedImage res = new BufferedImage(image.getWidth(null) * scale, image.getHeight(null) * scale,
            BufferedImage.TYPE_INT_ARGB);
    AffineTransform at = new AffineTransform();
    at.scale(scale, scale);
    AffineTransformOp scaleOp =
            new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);

    return scaleOp.filter((BufferedImage) image, res);
}

}

fortuneNext
  • 109
  • 1
  • 14
  • 3
    You may find [this](https://stackoverflow.com/questions/44136040/performance-of-javafx-gui-vs-swing) Q&A interesting – c0der Mar 13 '20 at 10:58
  • How are the images generated? – Slaw Mar 13 '20 at 11:02
  • @Slaw It shouldn't really matter, right? Anyhow, I'm using the jheatchart library (and convert to JavaFX images). – fortuneNext Mar 13 '20 at 11:17
  • 2
    Presumably you're generating the images in a background thread somewhere. I would: 1. Generate them as `int[]` data and publish them (carefully ensuring you do this in a way that maintains liveness and data integrity across multiple threads). 2. In the UI application, create a `WritableImage` and display it in an `ImageView`, and 3. Use an `AnimationTimer` which retrieves the latest image data and passes them to the `setPixels(...)` method on the images `PixelWriter`. If you can throw together a simple test application, it may be easier to help... – James_D Mar 13 '20 at 11:41
  • 1
    It could matter. For instance, JavaFX 13 added [`PixelBuffer`](https://openjfx.io/javadoc/13/javafx.graphics/javafx/scene/image/PixelBuffer.html) which, as I understand it, dramatically increased the performance of [vlcj](https://github.com/caprica/vlcj) when used with JavaFX (though that library does most of the work in native code). – Slaw Mar 13 '20 at 12:04
  • @James_D I made a quick demo program! Mind that the problem seems not to occur on fast machines - so maybe this is just a problem of general JavaFX performance? – fortuneNext Mar 13 '20 at 12:50
  • Also, a quick video to show the result: https://streamable.com/y65m1 – fortuneNext Mar 13 '20 at 12:58
  • Hmm. So one thing you are doing that you shouldn't do is to update the `WritableImage` from a background thread. I doubt that's causing the issue, though. My guess is that you're creating and discarding a lot of objects and causing the GC to kick in frequently on a low-memory machine. Try running it on your desktop with the heap memory limited (-Xmx512m for example) and see if it replicates the issues on the pi. – James_D Mar 13 '20 at 13:12
  • 3
    Your code is highly inefficient. Have a look at this example here https://github.com/mipastgt/JFXToolsAndDemos#awtimage . You could render your heatmap directly into this image and avoid all the performance killing image generations/conversions/copies ... – mipa Mar 13 '20 at 13:16
  • 1
    Yes: even using the heatmap library (which I'm not really convinced is giving you much), the `HeatChart` class is mutable. So you could simply create a single matrix, a single `HeatChart`, then update the matrix and call `setZValues()` each time. Then just publish the new image. But, as stated above, it's probably better just to directly write to the image. – James_D Mar 13 '20 at 13:19
  • 1
    whatever you do, you _must not_ update a node off the fx application thread (as you seem to do in the example above) – kleopatra Mar 13 '20 at 13:42
  • While I totally appreciate the performance tipps you are giving, am I mistaken that those will only make my heatmap generation even faster, which will be an even larger problem for the ImageView? (Will try the WritableImage solution though!) Btw, letting the setImage done in a Platform.runlater() lets the screen stay blank (even on Desktop), but that would be how to use the JavaFX Thread? – fortuneNext Mar 13 '20 at 13:42
  • If you simply add `Platform.runLater(...)` in the thread, you'll flood the FX Application Thread with too many requests, and it won't be able to process them all. You need to throttle those so they're only running once per frame render (the easiest way is with an `AnimationTimer`). – James_D Mar 13 '20 at 14:37
  • 1
    "_While I totally appreciate the performance tipps you are giving, am I mistaken that those will only make my heatmap generation even faster, which will be an even larger problem for the ImageView?_" – JavaFX typically targets 60fps for rendering speed which you can hook into using, as suggested, an `AnimationTimer` (it's invoked once per frame). Doing that will allow you to update the UI at exactly the rate the UI can handle. The other performance tips are about creating less short-lived objects to avoid too many GC cycles, including so-called "stop-the-world" pauses. – Slaw Mar 13 '20 at 15:41

1 Answers1

4

Your code updates the UI from a background thread, which is definitely not allowed. You need to ensure you update from the FX Application Thread. You also want to try to "throttle" the actual UI updates to occur no more than once per JavaFX frame rendering. The easiest way to do this is with an AnimationTimer, whose handle() method is invoked each time a frame is rendered.

Here's a version of your code which does that:

import java.awt.Dimension;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicReference;

import org.tc33.jheatchart.HeatChart;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {
    ImageView imageView = new ImageView();
    final int scale = 15;

    @Override
    public void start(Stage primaryStage) {

        AtomicReference<BufferedImage> image = new AtomicReference<>();

        Thread generator = new Thread(() -> {
            int col = 0;
            LinkedList<Long> fps = new LinkedList<>();
            while (true) {
                fps.add(System.currentTimeMillis());
                double[][] matrix = new double[48][128];
                for (int i = 0; i < 48; i++) {
                    for (int j = 0; j < 128; j++) {
                        matrix[i][j] = col == j ? Math.random() : 0;
                    }
                }
                col = (col + 1) % 128;

                HeatChart heatChart = new HeatChart(matrix, 0, 1);
                heatChart.setShowXAxisValues(false);
                heatChart.setShowYAxisValues(false);
                heatChart.setLowValueColour(java.awt.Color.black);
                heatChart.setHighValueColour(java.awt.Color.white);
                heatChart.setAxisThickness(0);
                heatChart.setChartMargin(0);
                heatChart.setCellSize(new Dimension(1, 1));

                long currentTime = System.currentTimeMillis();
                fps.removeIf(elem -> currentTime - elem > 1000);
                System.out.println(fps.size());

                image.set((BufferedImage) scale(heatChart.getChartImage(), scale));

            }
        });

        VBox box = new VBox();
        box.getChildren().add(imageView);

        Scene scene = new Scene(box, 1920, 720);
        primaryStage.setScene(scene);
        primaryStage.show();

        generator.setDaemon(true);
        generator.start();

        AnimationTimer animation = new AnimationTimer() {

            @Override
            public void handle(long now) {
                BufferedImage img = image.getAndSet(null);
                if (img != null) {
                    imageView.setImage(SwingFXUtils.toFXImage(img, null));
                }
            }

        };

        animation.start();
    }

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

    private static Image scale(Image image, int scale) {
        BufferedImage res = new BufferedImage(image.getWidth(null) * scale, image.getHeight(null) * scale,
                BufferedImage.TYPE_INT_ARGB);
        AffineTransform at = new AffineTransform();
        at.scale(scale, scale);
        AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);

        return scaleOp.filter((BufferedImage) image, res);
    }
}

Using the AtomicReference to wrap the buffered image ensures that it is safely shared between the two threads.

On my machine, this generates about 130 images per second; note that not all are displayed, as only the latest one is shown each time the JavaFX graphics framework displays a frame (which is typically throttled at 60fps).

If you want to ensure you show all images that are generated, i.e. you throttle the image generation by the JavaFX framerate, then you can use a BlockingQueue to store the images:

    // AtomicReference<BufferedImage> image = new AtomicReference<>();

    // Size of the queue is a trade-off between memory consumption
    // and smoothness (essentially works as a buffer size)
    BlockingQueue<BufferedImage> image = new ArrayBlockingQueue<>(5);

    // ...

    // image.set((BufferedImage) scale(heatChart.getChartImage(), scale));
    try {
        image.put((BufferedImage) scale(heatChart.getChartImage(), scale));
    } catch (InterruptedException exc) {
        Thread.currentThread.interrupt();
    }

and

        @Override
        public void handle(long now) {
            BufferedImage img = image.poll();
            if (img != null) {
                imageView.setImage(SwingFXUtils.toFXImage(img, null));
            }
        }

The code is pretty inefficient, as you generate a new matrix, new HeatChart, etc, on every iteration. This causes many objects to be created on the heap and quickly discarded, which can cause the GC to be run too often, particularly on a small-memory machine. That said, I ran this with the maximum heap size set at 64MB, (-Xmx64m), and it still performed fine. You may be able to optimize the code, but using the AnimationTimer as shown above, generating images more quickly will not cause any additional stress on the JavaFX framework. I would recommend investigating using the mutability of HeatChart (i.e. setZValues()) to avoid creating too many objects, and/or using PixelBuffer to directly write data to the image view (this would need to be done on the FX Application Thread).

Here's a different example, which (almost) completely minimizes object creation, using one off-screen int[] array to compute data, and one on-screen int[] array to display it. There's a little low-level threading details to ensure the on-screen array is only seen in a consistent state. The on-screen array is used to underly a PixelBuffer, which in turn is used for a WritableImage.

This class generates the image data:

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;

public class ImageGenerator {

    private final int width;
    private final int height;


    // Keep two copies of the data: one which is not exposed
    // that we modify on the fly during computation;
    // another which we expose publicly. 
    // The publicly exposed one can be viewed only in a complete 
    // state if operations on it are synchronized on this object.
    private final int[] privateData ;
    private final int[] publicData ;

    private final long[] frameTimes ;
    private int currentFrameIndex ;
    private final AtomicLong averageGenerationTime ;

    private final ReentrantLock lock ;


    private static final double TWO_PI = 2 * Math.PI;
    private static final double PI_BY_TWELVE = Math.PI / 12; // 15 degrees

    public ImageGenerator(int width, int height) {
        super();
        this.width = width;
        this.height = height;
        privateData = new int[width * height];
        publicData = new int[width * height];

        lock = new ReentrantLock();

        this.frameTimes = new long[100];
        this.averageGenerationTime = new AtomicLong();
    }

    public void generateImage(double angle) {

        // compute in private data copy:

        int minDim = Math.min(width, height);
        int minR2 = minDim * minDim / 4;
        for (int x = 0; x < width; x++) {
            int xOff = x - width / 2;
            int xOff2 = xOff * xOff;
            for (int y = 0; y < height; y++) {

                int index = x + y * width;

                int yOff = y - height / 2;
                int yOff2 = yOff * yOff;
                int r2 = xOff2 + yOff2;
                if (r2 > minR2) {
                    privateData[index] = 0xffffffff; // white
                } else {
                    double theta = Math.atan2(yOff, xOff);
                    double delta = Math.abs(theta - angle);
                    if (delta > TWO_PI - PI_BY_TWELVE) {
                        delta = TWO_PI - delta;
                    }
                    if (delta < PI_BY_TWELVE) {
                        int green = (int) (255 * (1 - delta / PI_BY_TWELVE));
                        privateData[index] = (0xff << 24) | (green << 8); // green, fading away from center
                    } else {
                        privateData[index] = 0xff << 24; // black
                    }
                }
            }
        }

        // copy computed data to public data copy:
        lock.lock(); 
        try {
            System.arraycopy(privateData, 0, publicData, 0, privateData.length);
        } finally {
            lock.unlock();
        }

        frameTimes[currentFrameIndex] = System.nanoTime() ;
        int nextIndex = (currentFrameIndex + 1) % frameTimes.length ;
        if (frameTimes[nextIndex] > 0) {
            averageGenerationTime.set((frameTimes[currentFrameIndex] - frameTimes[nextIndex]) / frameTimes.length);
        }
        currentFrameIndex = nextIndex ;
    }


    public void consumeData(Consumer<int[]> consumer) {
        lock.lock();
        try {
            consumer.accept(publicData);
        } finally {
            lock.unlock();
        }
    }

    public long getAverageGenerationTime() {
        return averageGenerationTime.get() ;
    }

}

And here's the UI:

import java.nio.IntBuffer;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class AnimationApp extends Application {


    private final int size = 400 ;
    private IntBuffer buffer ;

    @Override
    public void start(Stage primaryStage) throws Exception {

        // background image data generation:

        ImageGenerator generator = new ImageGenerator(size, size);

        // Generate new image data as fast as possible:
        Thread thread = new Thread(() ->  {
            while( true ) {
                long now = System.currentTimeMillis() ;
                double angle = 2 * Math.PI * (now % 10000) / 10000  - Math.PI;
                generator.generateImage(angle);
            }
        });
        thread.setDaemon(true);
        thread.start();


        generator.consumeData(data -> buffer = IntBuffer.wrap(data));
        PixelFormat<IntBuffer> format = PixelFormat.getIntArgbPreInstance() ;
        PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(size, size, buffer, format);
        WritableImage image = new WritableImage(pixelBuffer);

        BorderPane root = new BorderPane(new ImageView(image));

        Label fps = new Label("FPS: ");
        root.setTop(fps);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Give me a ping, Vasili. ");
        primaryStage.show();

        AnimationTimer animation = new AnimationTimer() {

            @Override
            public void handle(long now) { 
                // Update image, ensuring we only see the underlying
                // data in a consistent state:
                generator.consumeData(data ->  {
                    pixelBuffer.updateBuffer(pb -> null); 
                });
                long aveGenTime = generator.getAverageGenerationTime() ;
                if (aveGenTime > 0) {
                    double aveFPS = 1.0 / (aveGenTime / 1_000_000_000.0);
                    fps.setText(String.format("FPS: %.2f", aveFPS));
                }
            }

        };

        animation.start();

    }



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

For a version that doesn't rely on the JavaFX 13 PixelBuffer, you can just modify this class to use a PixelWriter (AIUI this won't be quite as efficient, but works just as smoothly in this example):

//      generator.consumeData(data -> buffer = IntBuffer.wrap(data));
        PixelFormat<IntBuffer> format = PixelFormat.getIntArgbPreInstance() ;
//      PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(size, size, buffer, format);
//      WritableImage image = new WritableImage(pixelBuffer);

        WritableImage image = new WritableImage(size, size);
        PixelWriter pixelWriter = image.getPixelWriter() ;

and

        AnimationTimer animation = new AnimationTimer() {

            @Override
            public void handle(long now) { 
                // Update image, ensuring we only see the underlying
                // data in a consistent state:
                generator.consumeData(data ->  {
//                  pixelBuffer.updateBuffer(pb -> null); 
                    pixelWriter.setPixels(0, 0, size, size, format, data, 0, size);
                });
                long aveGenTime = generator.getAverageGenerationTime() ;
                if (aveGenTime > 0) {
                    double aveFPS = 1.0 / (aveGenTime / 1_000_000_000.0);
                    fps.setText(String.format("FPS: %.2f", aveFPS));
                }
            }

        };
James_D
  • 201,275
  • 16
  • 291
  • 322
  • While this answer contains a lot of good hints, the original problem remains: the image still has jumps, pauses and lags (on raspberry pi, at least) which the Swing-Code didn't. 30-40 Images are generated, but the ImageView seemingly can't keep up. A side problem would be: Is it at least possible to wait for a occured update of ImageView so that I can just sequentially "generate-paint-generate-paint", lowering framerate but guaranteeing a smooth (altough slow) "video"? – fortuneNext Mar 13 '20 at 17:08
  • @fortuneNext Yes; see update. Ultimately, though, I don't think the heatmap library you found is doing you many favors; I'd just implement that from scratch if I were you. I'd be interested to know if my other example runs nicely on the raspberry pi... that would at least indicate that using those techniques would help. – James_D Mar 13 '20 at 17:17
  • 1
    @fortuneNext Can you use a profiler (e.g. _VisualVM_) while running your application on the raspberry pi to find out where the bottlenecks are? – Slaw Mar 13 '20 at 20:31
  • @James_D Your solution with the BlockingQueue brings a smooth altough slow replay (which makes me accept your answer :) Sadly I can't try the code below because PixelBuffer is JavaFX 13 which I couldn't get working on ARM yet... – fortuneNext Mar 16 '20 at 12:20
  • @fortuneNext I added a pre-JavaFX 13 version. – James_D Mar 16 '20 at 12:33
  • Your example runs very smooth at around 41 FPS on raspberry pi! – fortuneNext Mar 16 '20 at 15:06
  • 1
    @fortuneNext In that case, I'd recommend trying to use that approach; i.e. generate your heatmaps as raw pixel data. The library you're using is designed for use in AWT/Swing, as it generates `BufferedImage`s; you're likely spending a lot of CPU time converting these to FX images. A basic heatmap is pretty simple; really just a bunch of rectangular blocks, so it should be easy enough to represent it as an `int[ ]` of color data. – James_D Mar 16 '20 at 15:16
  • So for future readers: I implemented as suggested and had good results! A problem that arised though was that while the image was high FPS and smooth, it had a brutal delay (always showing the data thats 10 seconds old). Letting the Animationtimer rewrite the int[] only every few ms solved this though. – fortuneNext Mar 18 '20 at 16:47
  • @fortuneNext The `AnimationTimer` is supposed to run only once per frame, and the frame rate is supposed to be throttled at 60fps. Some linux implementations had a bug where the frame rate throttling was turned off by default; that may be happening on the Raspberry Pi. You might find running with the JVM option `-Djavafx.animation.fullspeed=false` fixes the problem immediately. – James_D Mar 18 '20 at 17:00
  • @James_D sadly, not fixes the problem – fortuneNext Mar 19 '20 at 14:20