Why is java.awt.Graphics.drawLine exceptionally slow?
Asked Answered
W

2

13

I am trying to achieve the following 'grid' layout.

CompleteImage

The class is extending java.awt.Canvas, and drawing these shapes (or lines) in the paint function. Why Canvas? Check here, trying to do something similar inititally.

Updated MCVE Code for getting the above 'layout':

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

@SuppressWarnings("serial")
public class SO_MCVE extends JPanel {

    private DrawingCanvas _drawingCanvas = null;

    private JButton repaintBtn;

    public SO_MCVE() {
        super(new BorderLayout());

        _drawingCanvas = new DrawingCanvas();
        _drawingCanvas.setSize(new Dimension(600, 600));

        JLabel repaintLabel = new JLabel(
                "<html><div style=\"text-align: center;\">" +
                "REPAINT</html>");
        repaintLabel.setHorizontalAlignment(
                SwingConstants.CENTER);

        repaintBtn = new JButton();
        repaintBtn.add(repaintLabel);
        repaintBtn.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {

                _drawingCanvas.triggerRepaint();
            }
        });

        add(_drawingCanvas, BorderLayout.CENTER);
        add(repaintBtn, BorderLayout.PAGE_END);
    }

    private static void createAndShowGUI() {
        JFrame frame = new JFrame("StackOverflow MCVE for drawLine");

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new SO_MCVE());
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                UIManager.put("swing.boldMetal", Boolean.FALSE);
                createAndShowGUI();
            }
        });
    }
}

@SuppressWarnings("serial")
class DrawingCanvas extends Canvas {

    public static final Color lightGreen = new Color(0, 255, 0, 180);
    public static final BasicStroke STROKE1PX = new BasicStroke(1.0f);
    public static final BasicStroke STROKE3PX = new BasicStroke(3.0f);

    private static final int LEFT = 50;
    private static final int RIGHT = 550;
    private static final int TOP = 50;
    private static final int BOTTOM = 550;

    private static final double WIDTH = 500.00d;
    private static final double HEIGHT = 500.00d;

    public DrawingCanvas() {

        setBackground(Color.BLACK);
    }

    public void paint(Graphics g) {
        update(g);
    }

    public void triggerRepaint() {
        repaint();
    }

    public void update(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;
        Dimension dim = getSize();
        int w = (int) dim.getWidth();
        int h = (int) dim.getHeight();

        // Clears the rectangle that was previously drawn
        g2.setPaint(Color.BLACK);
        g2.fillRect(0, 0, w, h);

        drawLines(g2, w, h);
    }

    /** Draw the lines marking the x-y limits **/
    private void drawLines(Graphics2D g2, int w, int h) {

        long start = System.nanoTime();
        System.out.println("Start of drawLines(): " + start);

        // Thick lines
        g2.setPaint(Color.GREEN);
        g2.setStroke(STROKE3PX);
        g2.drawLine(LEFT, 0, LEFT, h);
        g2.drawLine(RIGHT, 0, RIGHT, h);
        g2.drawLine(0, TOP, w, TOP);
        g2.drawLine(0, BOTTOM, w, BOTTOM);

        System.out.println("Done drawing thick lines!");
        long end = System.nanoTime();
        System.out.println("Time taken (ns): " +
                (end - start) + ", Time taken(ms): " +
                ((end - start)/1000/1000));
        start = end;

        // Thin vertical lines
        g2.setPaint(lightGreen);
        g2.setStroke(STROKE1PX);
        int wInc = ((int) WIDTH) / 50;
        for(int i = LEFT; i <= RIGHT; i += wInc) {
            g2.drawLine(i, TOP, i, BOTTOM);
        }

        System.out.println("Done drawing vertical lines!");
        end = System.nanoTime();
        System.out.println("Time taken (ns): " +
                (end - start) + ", Time taken(ms): " +
                ((end - start)/1000/1000));
        start = end;

        // Thin horizontal lines
        g2.setPaint(lightGreen);
        g2.setStroke(STROKE1PX);
        int hInc = ((int) HEIGHT) / 50;
        for(int i = TOP; i <= BOTTOM; i += hInc) {
            g2.drawLine(LEFT, i, RIGHT, i);
        }

        System.out.println("Done drawing horizontal lines!");
        end = System.nanoTime();
        System.out.println("Time taken (ns): " +
                (end - start) + ", Time taken(ms): " +
                ((end - start)/1000/1000));

        System.out.println();
    }
}

The problem with the code shown above is that, it is taking awhile (around 3 seconds) to render these lines, whenever I call repaint().
Press the "Repaint" button to trigger a repaint in the MCVE.
The lines will get drawn slowly one by one, as shown in the image below:

Halfway

So the question is:
Is there any reason why drawLine is so slow? I have tried drawing just as many (if not more) ellipses using g2.draw(some Ellipse2D.Double..) in a similar for loop and there was no issue.

Note: Using jre1.7.0_25, Windows 7, Eclipse
Simple benchmarking using System.nanoTime():

Done drawing thick lines!
Time taken (ns): 8858966, Time taken(ms): 8
Done drawing vertical lines!
Time taken (ns): 3649188968, Time taken(ms): 3649
Done drawing horizontal lines!
Time taken (ns): 106730282, Time taken(ms): 106

Note: Drawing the 'thin vertical lines' is taking forever!

UPDATE:
Note: Using jre1.8.0_11, Windows 7, Eclipse
Simple benchmarking using System.nanoTime():

Done drawing thick lines!
Time taken (ns): 110027, Time taken(ms): 0
Done drawing vertical lines!
Time taken (ns): 185567, Time taken(ms): 0
Done drawing horizontal lines!
Time taken (ns): 195419, Time taken(ms): 0

Note: Using jre1.8.0_45, Windows 7, Eclipse
Simple benchmarking using System.nanoTime():

Done drawing thick lines!
Time taken (ns): 6716121, Time taken(ms): 6
Done drawing vertical lines!
Time taken (ns): 2427676380, Time taken(ms): 2427
Done drawing horizontal lines!
Time taken (ns): 83030042, Time taken(ms): 83

Apparently, jre1.8.0_11 works really well?

How I run w different jre versions (Not sure if I am doing it correctly!):

VersionSwitch

Thanks! :)

Woolfolk answered 16/6, 2015 at 7:21 Comment(0)
M
4

Do as Michael Dibbets suggested and use buffering.

I replaced the SO_MCVE.update(Graphics g) method with one that draws to an offscreen buffer and then draws the buffer:

public void update(Graphics g) {

    Graphics2D g2 = (Graphics2D) g;
    Dimension dim = getSize();
    int w = (int) dim.getWidth();
    int h = (int) dim.getHeight();

    // Create the buffer
    BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics2D ig2 = image.createGraphics();

    // Paint everythign on the buffer
    // Clears the rectangle that was previously drawn
    ig2.setPaint(Color.BLACK);
    ig2.fillRect(0, 0, w, h);

    drawLines(ig2, w, h);

    // Paint the buffer
    g2.drawImage(image, 0, 0, null);
}

Start of drawLines(): 1832687816359773
Done drawing thick lines!
Time taken (ns): 2212913, Time taken(ms): 2
Done drawing vertical lines!
Time taken (ns): 37676442, Time taken(ms): 37
Done drawing horizontal lines!
Time taken (ns): 6453455, Time taken(ms): 6

If you want to be even more efficient you can store the buffered image as an attribute and replace it if the dimensions change.

That said, it is very interesting that drawing the vertical lines takes longer time that the horizontal ones. After digging through the internals of SunGraphics2D with a debugger and a profiler I still can't explain it.

Micrography answered 16/6, 2015 at 10:3 Comment(6)
Hi, thanks for taking the time to answer my question! I will try it soon. :)Woolfolk
Thanks, the solution is good. I am still curious about why 'drawing the vertical lines' take so much longer though, but your answer did fix my problem, so I have accepted it. Thanks! :)Woolfolk
Probably a bug in our version of java2d on our machines... (I used JDK1.7.0_60 on Windows 8). When I use JVisualVM CPU profiler, I find that for drawing only the vertical lines there are 25050 calls to sun.java2d.loops.Blit$GeneralMaskBlit.Blit(...) that take up almost all the drawing time. I can't find anything about this specific issue with a google search.Exultation
Tried 1.7.0_25, 1.8.0_11, 1.8.0_45, and 1.8.0_11 was the one that gave me 0ms, 0ms, 0ms. I am using Windows 7 and Eclipse btw.Woolfolk
Note that Swing Controls are double-buffered by default so alternatively you could extend JPanel and do the drawing from an overridden paintComponent method. Be sure to call super.paintComponent and restore the initial color and stroke. When I do this drawing time for the vertical lines is about 16ms. Perhaps this could explain why I couldn't find this bug in Oracle's database; I guess most people nowadays are using Swing instead of AWT directly.Exultation
I suspect horizontal lines are contiguous in memory. You don't often care about cache misses in Java, but perhaps this time you do.Quasimodo
C
4

Seems it's the alpha in Color lightGreen = new Color(0, 255, 0, 180); that causes this behavior.

Removing the alpha will render the lines instantly. If you need the alpha then one solution is to tweak the rendering hints.

Example

public void update(Graphics g) {

  Graphics2D g2 = (Graphics2D) g;
  g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,  RenderingHints.VALUE_ANTIALIAS_ON);

// ...
Construction answered 16/6, 2015 at 19:24 Comment(4)
Hi, that is an interesting observation. However, why is the alpha only affecting vertical lines? Does it have to do with how the graphics are being rendered?Woolfolk
+1 when removing the alpha it takes 0ms... I think @Marco13's comment on the question above is also correct that this is slow due to software rendering. When I keep the alpha and add the VM argument -Dsun.java2d.opengl=true it also becomes 0ms. Question remains why software rendering with alpha takes ages, still seems like a bug in the software renderer to me.Exultation
yeah there is definitely a bug regarding translucency in the renderer.Construction
Looks like we have some pretty good explanations for whatever is happening here, hopefully it will help others who encounter similar problems in the future :) Thanks for all the suggestions and ideas! :)Woolfolk

© 2022 - 2024 — McMap. All rights reserved.