Skip to Main Content

Java SE (Java Platform, Standard Edition)

Announcement

For appeals, questions and feedback about Oracle Forums, please email oracle-forums-moderators_us@oracle.com. Technical questions should be asked in the appropriate category. Thank you!

Interested in getting your voice heard by members of the Developer Marketing team at Oracle? Check out this post for AppDev or this post for AI focus group information.

Why is the JavaFX canvas so slow in rendering text (compared to Swing)

Ralph.KarApr 22 2014 — edited Apr 24 2014

Hi all,

sorry, I am not sure if this is the correct forum for the question I have.

We are developing synthetic radar displays for ATC purposes. So far we have been using Swing for that. For the last year I am trying to make a push towards JavaFX (especially now with Java 8 on stage).

I came up with several example applications that try to imitate existing systems. So far so good, but the case I am trying right now is basically a canvas with some map-like data and some text. In fact we have quite a bit of text (e.g. radar target labels).

I wrote a little JavaFX program that renders some targets with text on the canvas. The canvas also supports mouse wheel zooming and panning. This is working ok with e.g. 50 targets but not with 500 targets (and we need to display even more then that). The performance is completely gone. It is getting a lot better when I omit the label text rendering.

For comparison purposes I wrote the same thing using Swing and it performs very well even with much more targets.

Why is it that JavaFX canvas is so slow in rendering text? Is there anything I can do?

Please try it out of yourself. I tried to make the two examples as small as possible. Here is the JavaFX example:

import java.util.HashMap;

import java.util.Map;

import java.util.Random;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.ArcType;
import javafx.stage.Stage;

public final class JavaFXCanvasDemo extends Application
{
   private static final Rectangle2D MAP_RECT = new Rectangle2D( -50.0, -50.0, 100.0, 100.0 );
   private static final int OBJECT_COUNT = 500;
   private static boolean PAINT_LABELS = true;  // Set this to 'false' for real speedup but then ... no labels :-(

   private Map< String, Point2D > points_;
   private Canvas canvas_;
   private double xOffset_;
   private double yOffset_;
   private double scale_ = 1.0;
   private double dragStartX_;
   private double dragStartY_;
   private Color backgroundColor_;
   private Color foregroundColor_;

   @Override
   public void init() throws Exception
   {
     backgroundColor_ = Color.grayRgb( 0xc8 );
     foregroundColor_ = Color.rgb( 0x04, 0x87, 0xf9 );

     points_ = new HashMap<>();
     Random random = new Random();
     for ( int i = 0; i < OBJECT_COUNT; ++i )
     {
       double x = random.nextDouble() * MAP_RECT.getWidth() + MAP_RECT.getMinX();
       double y = random.nextDouble() * MAP_RECT.getHeight() + MAP_RECT.getMinY();
       points_.put( String.format( "Point%04d", i ), new Point2D( x, y ) );
     }
   }

   @Override
   public void start( Stage stage ) throws Exception
   {
     Group root = new Group();
     canvas_ = new Canvas( 800, 800 );
     root.getChildren().add( canvas_ );

     stage.setTitle( "JavaFX Canvas Demo" );
     stage.setScene( new Scene( root, backgroundColor_ ) );
     stage.show();

     canvas_.widthProperty().bind( stage.widthProperty() );
     canvas_.heightProperty().bind( stage.heightProperty() );

     ChangeListener< Number > dimensionChangeListener = new ChangeListener< Number >()
     {
       @Override
       public void changed( ObservableValue< ? extends Number > arg0, Number arg1, Number arg2 )
       {
         drawObjects( canvas_.getGraphicsContext2D() );
       }
     };
     canvas_.widthProperty().addListener( dimensionChangeListener );
     canvas_.heightProperty().addListener( dimensionChangeListener );

     canvas_.setOnScroll( new EventHandler< ScrollEvent >()
     {
       @Override
       public void handle( ScrollEvent ev )
       {
         Point2D focalPoint = canvas2Physical( ev.getX(), ev.getY() );
         if ( ev.getDeltaY() > 0.0 )
         {
           scale_ *= 1.1;
         }
         else
         {
           scale_ *= 0.9;
         }
         Point2D resetPoint = physical2Canvas( focalPoint.getX(), focalPoint.getY() );
         xOffset_ -= ( resetPoint.getX() - ev.getX() );
         yOffset_ -= ( resetPoint.getY() - ev.getY() );
         drawObjects( canvas_.getGraphicsContext2D() );
       }
     } );

     canvas_.setOnMousePressed( new EventHandler< MouseEvent >()
     {
       @Override
       public void handle( MouseEvent ev )
       {
         dragStartX_ = ev.getX();
         dragStartY_ = ev.getY();
       }
     } );
     canvas_.setOnMouseDragged( new EventHandler< MouseEvent >()
     {
       @Override
       public void handle( MouseEvent ev )
       {
         xOffset_ += ev.getX() - dragStartX_;
         dragStartX_ = ev.getX();
         yOffset_ += ev.getY() - dragStartY_;
         dragStartY_ = ev.getY();
         drawObjects( canvas_.getGraphicsContext2D() );
       }
     } );

     fullScaleAndCenter( canvas_ );
     drawObjects( canvas_.getGraphicsContext2D() );
   }

   private void drawObjects( GraphicsContext gc )
   {
     gc.clearRect( 0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight() );
     gc.setStroke( foregroundColor_ );

     for ( String id : points_.keySet() )
     {
       Point2D point = points_.get( id );
       Point2D canvasPos = physical2Canvas( point.getX(), point.getY() );
       gc.strokeArc( canvasPos.getX(), canvasPos.getY(), 10.0, 10.0, 0.0, 360.0, ArcType.OPEN );
       gc.strokeLine( canvasPos.getX(), canvasPos.getY() + 5.0, canvasPos.getX() + 10.0, canvasPos.getY() + 5.0 );
       gc.strokeLine( canvasPos.getX() + 5.0, canvasPos.getY(), canvasPos.getX() + 5.0, canvasPos.getY() + 10.0 );
       if ( PAINT_LABELS )
       {
         gc.strokeText( id, canvasPos.getX() + 10.0, canvasPos.getY() - 10.0 );
       }
     }
   }

   private void fullScaleAndCenter( Canvas canvas )
   {
     double width = MAP_RECT.getWidth();
     double height = MAP_RECT.getHeight();

     double scaleX = canvas.getWidth() / width;
     double scaleY = canvas.getHeight() / height;

     scale_ = Math.min( scaleX, scaleY );

     xOffset_ = canvas.getWidth() / 2.0;
     yOffset_ = canvas.getHeight() / 2.0;
   }

   private Point2D physical2Canvas( double x, double y )
   {
     return new Point2D( ( x * scale_ + xOffset_ ), ( y * ( -scale_ ) + yOffset_ ) );
   }

   private Point2D canvas2Physical( double i, double j )
   {
     return new Point2D( ( i - xOffset_ ) / scale_, ( j - yOffset_ ) / ( -scale_ ) );
   }

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

And here is the same thing using Swing:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;

@SuppressWarnings( "serial" )
public final class SwingPanelDemo extends JPanel
{
   private static final Rectangle2D MAP_RECT =
     new Rectangle2D.Double( -50.0, -50.0, 100.0, 100.0 );
   private static final int OBJECT_COUNT = 500;

   private final Map< String, Point2D > points_;
   private double xOffset_;
   private double yOffset_;
   private double scale_ = 1.0;
   private double dragStartX_;
   private double dragStartY_;
   private final Color backgroundColor_;
   private final Color foregroundColor_;

   private SwingPanelDemo()
   {
     super( null );

     backgroundColor_ = new Color( 0xc8c8c8 );
     foregroundColor_ = new Color( 0x0487f9 );

     points_ = new HashMap<>();
     Random random = new Random();
     for ( int i = 0; i < OBJECT_COUNT; ++i )
     {
       double x = random.nextDouble() * MAP_RECT.getWidth() + MAP_RECT.getMinX();
       double y = random.nextDouble() * MAP_RECT.getHeight() + MAP_RECT.getMinY();
       points_.put( String.format( "Point%04d", i ), new Point2D.Double( x, y ) );
     }

     MouseInputAdapter mouseListener = new MouseInputAdapter()
     {
       @Override
       public void mousePressed( MouseEvent ev )
       {
         dragStartX_ = ev.getX();
         dragStartY_ = ev.getY();
       }

       @Override
       public void mouseDragged( MouseEvent ev )
       {
         xOffset_ += ev.getX() - dragStartX_;
         dragStartX_ = ev.getX();
         yOffset_ += ev.getY() - dragStartY_;
         dragStartY_ = ev.getY();
         repaint();
       }

       @Override
       public void mouseWheelMoved( MouseWheelEvent ev )
       {
         Point2D focalPoint = canvas2Physical( ev.getX(), ev.getY() );
         if ( ev.getWheelRotation() < 0 )
         {
           scale_ *= 1.1;
         }
         else
         {
           scale_ *= 0.9;
         }
         Point2D resetPoint = physical2Canvas( focalPoint.getX(), focalPoint.getY() );
         xOffset_ -= ( resetPoint.getX() - ev.getX() );
         yOffset_ -= ( resetPoint.getY() - ev.getY() );
         repaint();
       }
     };
     addMouseListener( mouseListener );
     addMouseMotionListener( mouseListener );
     addMouseWheelListener( mouseListener );
   }

   @Override
   protected void paintComponent( Graphics g )
   {
     Graphics2D g2d = (Graphics2D) g;
     g2d.setBackground( backgroundColor_ );
     g2d.clearRect( 0, 0, getWidth(), getHeight() );
     g2d.setColor( foregroundColor_ );

     for ( String id : points_.keySet() )
     {
       Point2D point = points_.get( id );
       Point2D canvasPos = physical2Canvas( point.getX(), point.getY() );
       g2d.drawArc( (int) canvasPos.getX(), (int) canvasPos.getY(), 10, 10, 0, 360 );
       g2d.drawLine( (int) canvasPos.getX(), (int) canvasPos.getY() + 5, (int) canvasPos.getX() + 10, (int) canvasPos.getY() + 5 );
       g2d.drawLine( (int) canvasPos.getX() + 5, (int) canvasPos.getY(), (int) canvasPos.getX() + 5, (int) canvasPos.getY() + 10 );
       g2d.drawString( id, (int) canvasPos.getX() + 10, (int) canvasPos.getY() - 10 );
     }
   }

   private void fullScaleAndCenter()
   {
     double width = MAP_RECT.getWidth();
     double height = MAP_RECT.getHeight();

     double scaleX = getWidth() / width;
     double scaleY = getHeight() / height;

     scale_ = Math.min( scaleX, scaleY );

     xOffset_ = getWidth() / 2.0;
     yOffset_ = getHeight() / 2.0;
   }

   private Point2D physical2Canvas( double x, double y )
   {
     return new Point2D.Double( ( x * scale_ + xOffset_ ), ( y * ( -scale_ ) + yOffset_ ) );
   }

   private Point2D canvas2Physical( double i, double j )
   {
     return new Point2D.Double( ( i - xOffset_ ) / scale_, ( j - yOffset_ ) / ( -scale_ ) );
   }

   public static void main( String[] args )
   {
     SwingPanelDemo panel = new SwingPanelDemo();
     JFrame frame = new JFrame( "Swing Panel Demo" );
     frame.setContentPane( panel );
     frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
     frame.setSize( 800, 800 );
     frame.setVisible( true );
     panel.fullScaleAndCenter();
     panel.repaint();
   }
}

Thanks for your support.
Greeting,
Ralph

Comments

Jeff Martin

I think you want to use GraphicsContext.fillText() instead of strokeText(). I made this change and your demo runs at least 10x faster.

Ralph.Kar

Thanks Jeff,

that really makes a big difference.

Ralph

jsmith

You may be interested in this presentation on JavaFX performance optimizations

The diamond benchmark used in the presentation looks a bit like your sample application output.

1 - 3
Locked Post
New comments cannot be posted to this locked post.

Post Details

Locked on May 22 2014
Added on Apr 22 2014
3 comments
3,464 views