7 Replies Latest reply: Mar 4, 2013 7:03 PM by 992315 RSS

    How to draw a (or two) million lines fast?

    992315
      Hello,

      I have tried various ways to draw a million lines with JavaFX with little or no success. Can someone help me? What is the best way to accomplish this in JavaFX? The performance is extremely poor (47 seconds) compared to less than 2 sec using Java2D and the memory is huge 14 billion bytes.

      I included two functions attempting this in 2 different ways (the scaling is just to write the lines over each other for a window of 1200)

      private void drawWaveform(GraphicsContext gc) {

      gc.setStroke(Color.FIREBRICK);
      byte[] signal = new byte[1000000];
           int width = 4;
           for(int i = 1; i < signal.length-1; i+=2) {
           signal[i] = 100;
           }

      int factor;
      int length = signal.length;
      int scalefactor = 1200;

      for(int j = 1; j < length; j++){
      factor = scalefactor*(j-1)/length;
      gc.strokeLine(factor, signal[j-1], factor, signal[j]);

      }
      }


      private void drawWaveform2(GraphicsContext gc) {    
      byte[] signal = new byte[1000000];
           int width = 4;
           for(int i = 1; i < signal.length-1; i+=2) {
           signal[i] = 100;
           }

      int factor;
      int length = signal.length;
      int scalefactor = 1200;
      gc.setStroke(Color.FIREBRICK);

           for(int j = 1; j < length; j++){
      factor = scalefactor*(j-1)/length;

      gc.beginPath();
      gc.moveTo(factor,signal[j-1]);
      gc.lineTo(factor, signal[j]);
      gc.stroke();
      }
      gc.closePath();
      }

      Thanks for help-- this is fundamental for me and I cannot use JavaFX until I get past it.
        • 1. Re: How to draw a (or two) million lines fast?
          MiPa
          In your case it might be a good idea to stick with Java2D and draw into a buffered image which you can then display in JavaFX. The problem with JavaFX is, that it does not provide any real immediate mode API but instead is always building up large buffers before anything is actually drawn. At first glance the canvas API looks like an immediate mode API but actually it isn't.
          • 2. Re: How to draw a (or two) million lines fast?
            992315
            MiPa,

            I have many signals with dynamic data that requires interactive editing. For example, I may have 100 signals (hence a lot of lines on the screen, hence the 1 million test) and zoom into a time slice for all the selected signals and then edit the signals. Would drawing a static image (for all the signals selected) that gets recreated again and again be efficient for this?

            Can you point me to an example of writing into an ImageBuffer and then putting it into an JavaFX Image control? Would it require using Swing utilities or is there a way to interact with Java2D directly from JavaFX app? If it requires Swing interoperability would I then have 2 threads - EDT and JavaFX app? Would I have a Swing app with JavaFX controls?


            Thanks for your help.
            • 3. Re: How to draw a (or two) million lines fast?
              jsmith
              I took the original sample and wrapped it up in an AnimationTimer to time it.
              I set up my test to split the line drawing up into chunks.

              The outcome of my tests showed:

              1. You have to execute your loops more than once if you want the JIT to optimize them.
              2. You want the JIT to optimize your loops because the speed gains are phenomenal.
              3. The very first time you run a loop to draw more than one hundred thousand objects in a canvas, that loop can take a long time to execute.
              4. Subsequent executions of a loop to draw more than one hundred thousand objects in a canvas are hundreds of times quicker.
              5. My machine can get smooth animation performance with one hundred thousand lines on a canvas.
              6. With one million lines being drawn to a canvas each frame, performance will drop to about two frames per second.
              7. Once you have run a couple of animation loops, most of the time is spent by the JavaFX system (probably rendering the stroke commands to canvas).

              What may be happening here is that there are optimizations made by Hotspot JVM JIT compiler. On first execution of the line stroking loop, the JVM does not recognize a need to optimize and just runs in a (relatively expensive) interpreted mode. On subsequent executions, the need to optimize is recognized and the JVM performs a (better late than never) just in time compilation and everything starts to run very quickly. (That's just a guess though).

              For the first test below, I draw 1 million lines in chunks of one hundred thousand over the course of 10 animation pulses (one chunk per pulse).
              I also set it up so that the first chunk of lines is plotted twice.

              The stroke time is the time our application spends issuing the stroke commands to the canvas.
              The frame time is the time elapsed between display of each fully rendered frame and determines the FPS of the application.
              When the stroke time is low and the frame time is high, most of the time elapses within the JavaFX rendering system and not our application code.

              The first chunk plotted takes 800ms, then to plot the exact same set of lines on the canvas again takes 11ms and subsequent plots of chunks take only 4ms to 5ms (i.e. ~200 times faster). Here we can see also that time to render a frame settles down to about 50ms or 20fps which is sufficient for smooth animation for most applications.

              Output of the program is:
              Stroke Time: 0 to 100000 took 800ms for 99999 strokes
                Frame Time: 822ms, 1.217fps
              Stroke Time: 0 to 100000 took 11ms for 99999 strokes
                Frame Time: 313ms, 3.195fps
              Stroke Time: 100000 to 200000 took 5ms for 99999 strokes
                Frame Time: 71ms, 14.085fps
              Stroke Time: 200000 to 300000 took 4ms for 99999 strokes
                Frame Time: 47ms, 21.277fps
              Stroke Time: 300000 to 400000 took 5ms for 99999 strokes
                Frame Time: 48ms, 20.833fps
              If I increase the chunk size to one million lines (i.e. a single chunk for all lines), then keep plotting a new chunk of one million lines on each animation frame, I get output as below.

              The first chunk plotted takes 68 seconds, then to plot the exact same set of lines on the canvas again takes 55ms and subsequent plots of chunks take only 43ms (i.e. ~1500 times faster). Note that although the time to stroke to the canvas drops to 43ms which would give a draw rate of about 22fps, the actual fps drops to about 2fps (presumably because canvas in just buffering the stroke commands and then performing the actual stroke at a later time before the canvas is rendered to the screen). So you won't get a smooth animation.
              Stroke Time: 0 to 1000000 took 68030ms for 999999 strokes
                Frame Time: 68051ms, 0.015fps
              Stroke Time: 0 to 1000000 took 55ms for 999999 strokes
                Frame Time: 721ms, 1.387fps
              Stroke Time: 0 to 1000000 took 43ms for 999999 strokes
                Frame Time: 475ms, 2.105fps
              Stroke Time: 0 to 1000000 took 43ms for 999999 strokes
                Frame Time: 456ms, 2.193fps
              Stroke Time: 0 to 1000000 took 44ms for 999999 strokes
                Frame Time: 472ms, 2.119fps
              Tests were run with jdk8b77 (64 bit), win7, ati hd4600, 2.83ghz quad core pentium Q9505.
              import javafx.animation.AnimationTimer;
              import javafx.application.Application;
              import javafx.beans.property.*;
              import javafx.scene.*;
              import javafx.scene.canvas.*;
              import javafx.scene.paint.Color;
              import javafx.stage.Stage;
              
              public class WavePlotter extends Application {
                final Canvas canvas = new Canvas(1200, 100);
                
                final int TOTAL_LENGTH = 1000000;
                final int NUM_CHUNKS   = 10;
                final int CHUNK_LENGTH = TOTAL_LENGTH / NUM_CHUNKS;
                
                private byte[] signal = new byte[TOTAL_LENGTH];
              
                private String[] colors = { 
                  "blue", "blueviolet", "brown", "burlywood", "cadetblue", 
                  "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson" 
                };
                
                @Override public void init() {
                  for(int i = 1; i < signal.length-1; i+=2) {
                    signal[i] = 100;
                  }
                }
                  
                @Override public void start(Stage stage) {
                  stage.setScene(new Scene(new Group(canvas)));
                  stage.show();
              
                  final BooleanProperty firstPulse = new SimpleBooleanProperty(true);
                  final IntegerProperty currentChunk = new SimpleIntegerProperty(0);
                  final AnimationTimer timer = new AnimationTimer() {
                    @Override public void handle(long l) {
                      if (!firstPulse.get()) {
                        drawWaveform(
                          canvas.getGraphicsContext2D(), 
                          currentChunk.get() * CHUNK_LENGTH, CHUNK_LENGTH, 
                          Color.web(colors[currentChunk.get() % colors.length])
                        );
                        if (currentChunk.get() < NUM_CHUNKS - 1) {
                          currentChunk.set((currentChunk.get() + 1) % NUM_CHUNKS);
                        } else {
                          stop();
                        }
                      } else {
                        drawWaveform(
                          canvas.getGraphicsContext2D(), 
                          0, CHUNK_LENGTH, 
                          Color.FIREBRICK
                        );
                        firstPulse.set(false);
                      }  
                    }
                  };
                  timer.start();
                }
              
                private void drawWaveform(GraphicsContext gc, int startIdx, int chunkLength, Color color) {
                  //gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
                  gc.setStroke(color);
                  int factor;
                  int totalLength = signal.length;
                  int scalefactor = 1200;
              
                  long start = getTime();
                  int nStrokes = 0;
                  for(int j = startIdx + 1; j < startIdx + chunkLength; j++) {
                    factor = scalefactor*(j-1)/totalLength;
                    gc.strokeLine(factor, signal[j-1], factor, signal[j]);
                    nStrokes++;
                  }
                  long end = getTime();
                  
                  System.out.println(
                    "Stroke Time: " + startIdx + " to " + (startIdx + chunkLength) + 
                     " took " + (end - start) + "ms for " + nStrokes + " strokes"
                  );
                }   
                      
                private long getTime() {
                  return System.nanoTime() / 1000000;
                }
              
                public static void main(String[] args) { launch(args); }
              }
              • 4. Re: How to draw a (or two) million lines fast?
                992315
                Hello,

                Thanks for the benchmarking. Even though the JIT is optimizing on the second loop or in chunks, the memory is still large. So I decided to implement it as a Java2D BufferedImage and convert that into a JavaFX image. I think I ran into a possible BUG (see below). The version that runs is now for 2 million lines, less than 5 seconds and about 1GB of memory.

                Are there any suggestions on getting it faster with less memory???


                private ImageView drawWaveform4 ( byte[] signal ) {
                BufferedImage bufferedImage = new BufferedImage(signal.length, 100, BufferedImage.TYPE_BYTE_BINARY);
                Graphics2D g2d = bufferedImage.createGraphics();
                int factor, factor2;
                int length = signal.length;
                int scalefactor = 1200;
                g2d.setColor(java.awt.Color.red);
                for(int j = 1; j < length; j++) {
                factor = scalefactor*(j-1)/length;
                if (j == length-1) { System.out.println("factor is " + factor); }
                factor2 = (scalefactor*j)/length;     
                g2d.drawLine(factor, (int)signal[j-1], factor, (int)signal[j]);
                g2d.drawLine(factor, (int)signal[j], factor2, (int)signal[j]);
                }

                /* I had to convert to JavaFX image pixel by pixel since SwingFXUtils dies with 'out of memory on the java heap'. If you comment out these lines and uncomment out the ones below you can recreate that problem. in Summary why is SwingFXUtils.toFXimage EVEN calling the constructor for bufferedImage?? and then when it does why does it ignore how it was created and make a copy filled with INTs which rightly runs out of memory??? */

                // start working block
                WritableImage wr = null;
                if (bufferedImage != null) {
                wr = new WritableImage(bufferedImage.getWidth(), bufferedImage.getHeight());
                PixelWriter pw = wr.getPixelWriter();
                for (int x = 0; x < bufferedImage.getWidth(); x++) {
                for (int y = 0; y < bufferedImage.getHeight(); y++) {
                pw.setColor(x,y, Color.BLUE);
                }
                }
                }
                // end working block

                // uncomment after here (and comment out the above that converts to JavaFX image pixel by pixel) to see the SwingFXutils blow up in size
                /* WritableImage wr = null;
                SwingFXUtils.toFXImage(bufferedImage, wr);
                // also tried
                // Image img = SwingFXUtils.toFXImage(bufferedImage, null);
                // Image img = SwingFXUtils.toFXImage(bufferedImage, wr);
                // with no success
                */

                ImageView imageView = new ImageView();
                imageView.setImage((Image)wr);
                return imageView;
                }


                Thanks for more comments.. I would like it to be in the 2-3 second range so any help getting there is much appreciated and valued...
                • 5. Re: How to draw a (or two) million lines fast?
                  MiPa
                  There are far more efficient ways to get your pixels on the screen. Maybe this thread
                  can give you an idea. With the correct pixel format this is a really fast solution.
                  Canvas performance
                  • 6. Re: How to draw a (or two) million lines fast?
                    992315
                    So I memory profiled your WavePlotter code and it is ~350M; I chunked the data differently, in 1000 byte chunks, and it is now acceptable in the 1M range but it flickers...how do I get a double buffer to draw to the canvas without flicker keeping the memory in an acceptable range (around a couple meg)

                    Thanks.
                    • 7. Re: How to draw a (or two) million lines fast?
                      992315
                      It seems to me that your Animation Timer loop executed blazingly fast (total timeis maybe < 50ms) but the memory is quite large (350Mb). I found a "sweet spot" between memory usage and timing that chunked the data in 1000 byte chunks (NUM_CHUNKS= 1000)

                      However, when I am not writing a game but an interactive desktop app with a lot of data. I would like to draw a million lines "statically" to the screen at the speed your animation does it -- <50ms but with a small amount of memory, ~1Mb or less. The I will interact with the waveform and redraw it.

                      what I tried : I put drawWaveform in a for loop 0-Num_CHUNKS and it takes 57000 ms again and the memory blows up again so it seems the handle method is doing something special to be so fast - how can iI do that without animating my scene?

                      Do you understand what I am asking?