Search code examples
javaswinggraphics2d

Why is java.awt.Graphics.drawLine exceptionally slow?


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! :)


Solution

  • 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.