11

I wrote two simple programs, both draw the same Sierpinski Triangle: enter image description here
One program was implemented using swing, and one using javafx. There is a very significant performance difference, swing implementation being consistently much faster:

enter image description here


(In this test case : Swing over 1 sec. Javafx over 12 seconds)
Is it to be expected or is there something very wrong with my javafx implementation ?

Swing implementation

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class SimpleSrpnskTriSw {

    private Triangles triPanel;

    SimpleSrpnskTriSw(int numberOfLevels){

        JFrame frame = new JFrame("Sierpinski Triangles (swing)");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        triPanel = new Triangles();
        frame.add(triPanel, BorderLayout.CENTER);
        frame.pack();

        frame.setResizable(false);
        frame.setVisible(true);

        triPanel.draw(numberOfLevels);
    }

    class Triangles extends JPanel{

        private static final int PANEL_WIDTH =600, PANEL_HEIGHT = 600;
        private static final int TRI_WIDTH= 500, TRI_HEIGHT= 500;
        private static final int SIDE_GAP = (PANEL_WIDTH - TRI_WIDTH)/2;
        private static final int TOP_GAP = (PANEL_HEIGHT - TRI_HEIGHT)/2;

        private int countTriangles;
        private long startTime;
        boolean working;
        private int numberOfLevels = 0;

        Triangles() {

            setPreferredSize(new Dimension(PANEL_WIDTH, PANEL_HEIGHT));
            startTime = System.currentTimeMillis();
            countTriangles = 0;
            working = true;
            draw();
        }

        void draw(int numLevels) {

            numberOfLevels = numLevels;
            working = true;
            draw();
        }

        void draw() {

            startTime = System.currentTimeMillis();
            countTriangles = 0;

            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {

                    repaint();
                }
            });
        }

        @Override
        public void paintComponent(Graphics g) {

            super.paintComponent(g);
            g.setFont(new Font("Ariel", Font.PLAIN, 14));

            if(working) {

                g.setColor(getBackground());
                g.fillRect(0,0,PANEL_WIDTH,PANEL_HEIGHT);
                g.setColor(getForeground());

                g.drawString("Working.........", 15, 15);
                working = false;
                return;
            }

            if(numberOfLevels <= 0 ) {
                return;
            }
            Point top = new Point(PANEL_WIDTH/2, TOP_GAP);
            Point left = new Point(SIDE_GAP, TOP_GAP+ TRI_HEIGHT);
            Point right = new Point(SIDE_GAP + TRI_WIDTH, TOP_GAP+ TRI_HEIGHT);

            BufferedImage bi = getBufferedImage(top, left, right);
            Graphics2D g2d = (Graphics2D) g;
            g2d.drawImage(bi,0,0, this);

            g.drawString("Number of triangles: "+ countTriangles, 15, 15);
            g.drawString("Time : "+ (System.currentTimeMillis()- startTime)+ " mili seconds", 15, 35);
            g.drawString("Levels: "+ numberOfLevels, 15, 50);
        }

        private BufferedImage getBufferedImage(Point top, Point left, Point right) {

            BufferedImage bi = new BufferedImage(PANEL_WIDTH,PANEL_HEIGHT,
                                                            BufferedImage.TYPE_INT_ARGB);
            drawTriangle(bi, numberOfLevels, top, left, right);
            return bi;
        }

        private void drawTriangle(BufferedImage bi, int levels, Point top, Point left, Point right) {

            if(levels < 0) {
                return ;
            }

            countTriangles++;
            Graphics g = bi.getGraphics();
            g.setColor(Color.RED);

            Polygon tri = new Polygon();
            tri.addPoint(top.x, top.y);  //use top,left right rather than fixed points
            tri.addPoint(left.x, left.y);
            tri.addPoint(right.x, right.y);
            g.drawPolygon(tri);

            // Get the midpoint on each edge in the triangle
            Point p12 = midpoint(top, left);
            Point p23 = midpoint(left, right);
            Point p31 = midpoint(right, top);

            // recurse on 3 triangular areas
            drawTriangle(bi, levels - 1, top, p12, p31);
            drawTriangle(bi, levels - 1, p12, left, p23);
            drawTriangle(bi, levels - 1, p31, p23, right);
        }

        private Point midpoint(Point p1, Point p2) {

            return new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
        }
    }

    public static void main(String[] args) {

        new SimpleSrpnskTriSw(13);
    }
}

JavaFx implementation

import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class SimpleSrpnskTriFx extends Application {

    private final int PADDING = 5;
    private static int numberOfLevels;

    public static void launch(String... args){

        numberOfLevels = 8;

        if((args != null) && (args.length > 0)) {

            try {
                int num = Integer.parseInt(args[0]);
                numberOfLevels =  num ;
            } catch (NumberFormatException ex) {
                ex.printStackTrace();
                return;
            }
        }

        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {

        stage.setOnCloseRequest((ae) -> {
            Platform.exit();
            System.exit(0);
        });

        stage.setTitle("Sierpinski Triangles (fx)");

        BorderPane mainPane = new BorderPane();
        mainPane.setPadding(new Insets(PADDING));

        Pane triPanel = new Triangles();

        BorderPane.setAlignment(triPanel, Pos.CENTER);
        mainPane.setCenter(triPanel);

        Scene scene = new Scene(mainPane);

        stage.setScene(scene);
        stage.centerOnScreen();

        stage.setResizable(false);
        stage.show();
    }

    class Triangles extends AnchorPane{

        private static final int PANEL_WIDTH =600, PANEL_HEIGHT = 600;
        private static final int TRI_WIDTH= 500, TRI_HEIGHT= 500;
        private static final int SIDE_GAP = (PANEL_WIDTH - TRI_WIDTH)/2;
        private static final int TOP_GAP = (PANEL_HEIGHT - TRI_HEIGHT)/2;
        private int countTriangles;
        private long startTime;
        private Point2D top, left, right;

        private Canvas canvas;
        private Canvas backgroundCanvas;
        private GraphicsContext gc;

        Triangles(){

            setPrefSize(PANEL_WIDTH, PANEL_HEIGHT);

            canvas = getCanvas();
            backgroundCanvas = getCanvas();
            gc = backgroundCanvas.getGraphicsContext2D();
            getChildren().add(canvas);
            draw(numberOfLevels);
        }

        void draw(int numberLevels) {

            Platform.runLater(new Runnable() {

                @Override
                public void run() {

                    canvas.getGraphicsContext2D().fillText("Working....",5,15);
                    setStartPoints();

                    startTime = System.currentTimeMillis();
                    countTriangles = 0;

                    RunTask task = new RunTask(numberLevels, top, left, right);
                    Thread thread = new Thread(task);
                    thread.setDaemon(true);
                    thread.start();
                }
            });
        }

        private void drawTriangle( int levels, Point2D top, Point2D left, Point2D right) {

            if(levels < 0) {//add stop criteria
                return ;
            }

            gc.strokePolygon( //implementing with strokeLine did not make much difference
                    new double[]{
                            top.getX(),left.getX(),right.getX()
                    },
                    new double[]{
                            top.getY(),left.getY(), right.getY()
                    },
                    3);

            countTriangles++;

            //Get the midpoint on each edge in the triangle
            Point2D p12 = midpoint(top, left);
            Point2D p23 = midpoint(left, right);
            Point2D p31 = midpoint(right, top);

            // recurse on 3 triangular areas
            drawTriangle(levels - 1, top, p12, p31);
            drawTriangle(levels - 1, p12, left, p23);
            drawTriangle(levels - 1, p31, p23, right);
        }

        private void setStartPoints() {

            top = new Point2D(getPrefWidth()/2, TOP_GAP);
            left = new Point2D(SIDE_GAP, TOP_GAP + TRI_HEIGHT);
            right = new Point2D(SIDE_GAP + TRI_WIDTH, TOP_GAP + TRI_WIDTH);
        }

        private Point2D midpoint(Point2D p1, Point2D p2) {

            return new Point2D((p1.getX() + p2.getX()) /
                    2, (p1.getY() + p2.getY()) / 2);
        }

        private void updateGraphics(boolean success){

            if(success) {

                copyCanvas();

                GraphicsContext gc = canvas.getGraphicsContext2D();
                gc.fillText("Number of triangles: "+ countTriangles,5,15);
                gc.fillText("Time : "+ (System.currentTimeMillis()- startTime )+ " mili seconds", 5,35);
                gc.fillText("Levels: "+ numberOfLevels,5,55);
            }

        }

        private Canvas getCanvas() {

            Canvas canvas = new Canvas();
            canvas.widthProperty().bind(widthProperty());
            canvas.heightProperty().bind(heightProperty());
            canvas.getGraphicsContext2D().setStroke(Color.RED);
            canvas.getGraphicsContext2D().setLineWidth(0.3f);

            return canvas;
        }

        private void copyCanvas() {

            WritableImage image = backgroundCanvas.snapshot(null, null);
            canvas.getGraphicsContext2D().drawImage(image, 0, 0);
        }

        /**
         */
        class RunTask extends Task<Void>{

            private int levels;
            private Point2D top, left;
            private Point2D right;

            RunTask(int levels, Point2D top, Point2D left, Point2D right){

                this.levels = levels;
                this.top = top;
                this.left = left;
                this.right = right;

                startTime = System.currentTimeMillis();
                countTriangles = 0;
            }

            @Override public Void call() {

                drawTriangle(levels,top, left, right);
                return null;
            }

            @Override
            protected void succeeded() {

                updateGraphics(true);
                super.succeeded();
            }

            @Override
            protected void failed() {

                updateGraphics(false);
            }
        }
    }

    public static void main(String[] args) {
        launch("13");
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
c0der
  • 18,467
  • 6
  • 33
  • 65
  • 5
    The difference in performance (beyond any small difference) is almost invariably due to inexperience in the toolkit that is slower. But since I do Swing rather than Java-FX, I'll leave it to the Java-FX gurus to spot the inefficiencies in that code. – Andrew Thompson May 23 '17 at 13:28
  • This may be completely off, but in attempt to rule out any unrelated issues it may be interesting to use `Point2D.Double` for the Swing implementation. My guess is that it won't make too much of a difference, but one can't help but wonder how much of an effect using `double` instead of `int` has on the JavaFX version's performance... – Itai May 23 '17 at 13:53
  • @sillyfly Thanks for the suggestion. I just ran a test using `java.awt.Point` in the javafx implementation, to see the impact of the reduced accuracy. It made a small change reducing the overall time from ~12 sec to ~11. – c0der May 23 '17 at 14:09
  • 1
    @AndrewThompson’s assessment is correct. Your inexperience with JavaFX, much like in your Swing code, needs to be corrected before performance issues can be attributed to the toolkit. – VGR May 23 '17 at 14:35
  • 3
    The Swing example _flattens_ the image to 360,000 pixels; the JavaFX example strokes almost 2.4 million polygons. Have you tried [this example](https://stackoverflow.com/a/44056730/230513) with the triangles? – trashgod May 23 '17 at 16:11
  • I did not try to convert the fractal trees to triangles, but I will. Can you explain where do you see a difference between the two implementation ? The number of triangles counted is the same in both. – c0der May 23 '17 at 16:22

1 Answers1

25

The Swing example flattens the image to 6002 = 360,000 pixels. In contrast, the JavaFX example strokes almost 2.4 million overlapping polygons when finally rendered. Note that your JavaFX example measures both the time to compose the fractal and the time to render it in the scene graph.

If you want to preserve the strokes comprising the fractal, compose the result in a Canvas, as shown here.

If a flat Image is sufficient, compose the result in a BufferedImage, convert it to a JavaFX Image, and display it in an ImageView, as shown below. The JavaFX result is over a second faster than the Swing example on my hardware.

Because SwingFXUtils.toFXImage makes a copy, a background Task<Image> could continue to update a single BufferedImage while publishing interim Image results via updateValue(). Alternatively, this MandelbrotSet features a Task that updates a WritableImage via its PixelWriter.

image

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.image.BufferedImage;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

/**
 * @see https://stackoverflow.com/q/44136040/230513
 */
public class BufferedImageTest extends Application {

    private static final int PANEL_WIDTH = 600, PANEL_HEIGHT = 600;
    private static final int TRI_WIDTH = 500, TRI_HEIGHT = 500;
    private static final int SIDE_GAP = (PANEL_WIDTH - TRI_WIDTH) / 2;
    private static final int TOP_GAP = (PANEL_HEIGHT - TRI_HEIGHT) / 2;
    private final int numberOfLevels = 13;
    private int countTriangles;

    @Override
    public void start(Stage stage) {
        stage.setTitle("BufferedImageTest");
        StackPane root = new StackPane();
        Scene scene = new Scene(root);
        root.getChildren().add(new ImageView(createImage()));
        stage.setScene(scene);
        stage.show();
    }

    private Image createImage() {
        BufferedImage bi = new BufferedImage(
            PANEL_WIDTH, PANEL_HEIGHT, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = bi.createGraphics();
        g.setPaint(Color.white);
        g.fillRect(0, 0, PANEL_WIDTH, PANEL_HEIGHT);
        Point top = new Point(PANEL_WIDTH / 2, TOP_GAP);
        Point left = new Point(SIDE_GAP, TOP_GAP + TRI_HEIGHT);
        Point right = new Point(SIDE_GAP + TRI_WIDTH, TOP_GAP + TRI_HEIGHT);
        g.setColor(Color.red);
        long startTime = System.currentTimeMillis();
        drawTriangle(g, numberOfLevels, top, left, right);
        g.setPaint(Color.black);
        g.drawString("Number of triangles: " + countTriangles, 15, 15);
        g.drawString("Time : " + (System.currentTimeMillis() - startTime) + " ms", 15, 35);
        g.drawString("Levels: " + numberOfLevels, 15, 50);
        WritableImage image = SwingFXUtils.toFXImage(bi, null);
        g.dispose();
        return image;
    }

    private void drawTriangle(Graphics2D g, int levels, Point top, Point left, Point right) {
        if (levels < 0) {
            return;
        }
        countTriangles++;
        Polygon tri = new Polygon();
        tri.addPoint(top.x, top.y);
        tri.addPoint(left.x, left.y);
        tri.addPoint(right.x, right.y);
        g.drawPolygon(tri);

        // Get the midpoint on each edge in the triangle
        Point p12 = midpoint(top, left);
        Point p23 = midpoint(left, right);
        Point p31 = midpoint(right, top);

        // recurse on 3 triangular areas
        drawTriangle(g, levels - 1, top, p12, p31);
        drawTriangle(g, levels - 1, p12, left, p23);
        drawTriangle(g, levels - 1, p31, p23, right);
    }

    private Point midpoint(Point p1, Point p2) {
        return new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
    }

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

}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • 1
    Fast indeed: 900ms on my machine. The code does most of the time consuming work using `java.awt` technology (Polygon, BufferedImage, Graphics). The use of `javafx` technology is limited to displaying the final result. A code that uses 'javafx' technology all across, like the one I posed , or [this code](https://ideone.com/hI8drx) which is actually your the [fractal trees](https://stackoverflow.com/questions/44050344/timing-javafx-canvas-application/44056730#44056730) converted to triangles : runs much slower than the comparable `swing` version. So my basic dilemma still remains open. – c0der May 25 '17 at 09:28
  • 2
    I would't reject `java.awt` any more than I'd reject `java.util`. I don't understand the dilemma: a `Task` will let you keep adding new detail to the `BufferedImage` for each new level; in contrast, the `Task` example starts over for each level, reconstructing all previous levels.. – trashgod May 25 '17 at 10:53
  • 1
    On reflection, the dilemma revolves around [bitmap vs. vector representations](https://en.wikipedia.org/wiki/Vector_graphics_editor#Vector_editors_versus_bitmap_editors). – trashgod May 25 '17 at 22:38
  • My problem with all these is the fact that AWT still had to be used for jfx to get good speed, but Once came across an Oracle article stating end of life support for AWT and Swing in 2025, maybe by then jfx replacements would have gained, or a better technique would have been formed – Ezekiel Nov 08 '21 at 08:46
  • 1
    @Ezekiel: `WritableImage` support, for example, may improve; see also the JDK [*Client Libraries Group*](https://openjdk.java.net/groups/client-libs/index.html) regarding 2D. – trashgod Nov 08 '21 at 14:39