hageldave / JPlotter

OpenGL based 2D Plotting Library for Java using AWT and LWJGL
https://github.com/hageldave/JPlotter/wiki
MIT License
44 stars 6 forks source link

Please help with java.util.ConcurrentModificationException #62

Closed gmaczuga closed 1 year ago

gmaczuga commented 1 year ago

Experiencing the java.util.ConcurrentModificationException:

Exception in thread "AWT-EventQueue-0" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1631) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) at hageldave.jplotter.renderables.Triangles.getIntersectingTriangles(Triangles.java:453) at hageldave.jplotter.renderers.TrianglesRenderer.renderFallback(TrianglesRenderer.java:263) at hageldave.jplotter.renderers.CoordSysRenderer.renderFallback(CoordSysRenderer.java:715) at hageldave.jplotter.canvas.BlankCanvasFallback.render(BlankCanvasFallback.java:115) at hageldave.jplotter.canvas.BlankCanvasFallback.render(BlankCanvasFallback.java:106) at hageldave.jplotter.canvas.BlankCanvasFallback.repaint(BlankCanvasFallback.java:70) at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318) at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773) at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720) at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714) at java.base/java.security.AccessController.doPrivileged(AccessController.java:399) at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86) at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742) at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203) at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124) at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113) at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109) at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101) at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

and quite dirty source code of JPanel (that draws 60 OHLC of price candles).

public class CandlesPanel extends BlankCanvasFallback /* BlankCanvas */ implements SignalSubscriber {

private static final Logger LOGGER = LoggerFactory.getLogger(CandlesPanel.class);

List<ClickSubscriber> clickSubscribers = new ArrayList<>();

CircularFifoQueue<Candle> candlesQueue = new CircularFifoQueue(60);

CircularFifoQueue<Triangles> candlesTriangles = new CircularFifoQueue(60);

Candle currentCandle;

PickingRegistry<Integer> candlePickingRegistry = new PickingRegistry<>();

long viewLow = 13_350;
long viewHigh = 13_450;

Calendar viewStart;
Calendar viewEnd;

public void addClickSubscriber(ClickSubscriber subscriber){
    clickSubscribers.add(subscriber);
}

public class Candle{

    public int id;
    public Calendar startTime = Calendar.getInstance();
    public double open;
    public double high;
    public double low;
    public double close;

    @Override
    public String toString() {
        return "Candle: id:" + id + " " + calendarToString(startTime) + " OHLC: " + open + " " + high + " " + low + " " + close;
    }

    public void recreateTrianglesForCandle(Integer position, Triangles c) {
        c.removeAllTriangles();
        // candle body
        c.addQuad(new Rectangle2D.Double(position,
                close > open ? open : close,
                0.9,
                abs(close - open)));

        // candle low and high pins
        c.addQuad(new Rectangle2D.Double(position + 0.4,
                low,
                0.12,high - low));

        c.getTriangleDetails().forEach(tri->{
            int red=0xffe41a1c, green= new Color(0, 204,0).getRGB();
            tri.setColor(close < open ? red:green);

            // set picking color for candle
            int pickingColor = candlePickingRegistry.register(position);
            tri.setPickColor(pickingColor);
        });
    }

}

TrianglesRenderer panelRenderer = new TrianglesRenderer();
Triangles priceHorizontalLineTriangles = new Triangles();
Rectangle2D.Double priceLine = new Rectangle2D.Double(0, 0, 60, 0.075);

@Override
public void newTx(long time, double price) {

    // horizontal line handling
    priceHorizontalLineTriangles.removeAllTriangles();
    priceLine.y = price;
    priceHorizontalLineTriangles.addQuad(priceLine);
    priceHorizontalLineTriangles.getTriangleDetails().forEach(triangleDetails -> triangleDetails.setColor(Color.MAGENTA));

    // candles queue handling
    if (isNewMinute(time)) {

        // new minute, new candle
        currentCandle = new Candle();
        currentCandle.open = price;
        currentCandle.high = price;
        currentCandle.low = price;
        currentCandle.close = price;
        currentCandle.startTime.setTimeInMillis(time);

        candlesQueue.add(currentCandle);

        // repaint all candles including new one
        for (int i = 0; i < numberOfCandles; i++) {
            Candle candle = candlesQueue.get(i);
            candle.recreateTrianglesForCandle(i, candlesTriangles.get(i));
        }

    } else {
        // update current candle
        currentCandle.close = price;
        if(currentCandle.high < price) currentCandle.high = price;
        if(currentCandle.low > price) currentCandle.low = price;

        // repaint only last candle
        Candle candle = candlesQueue.get(numberOfCandles - 1);
        candle.recreateTrianglesForCandle(numberOfCandles - 1, candlesTriangles.get(numberOfCandles - 1));

    }

    this.repaint();

}

Calendar calendar = Calendar.getInstance();
private boolean isNewMinute(long time) {

    // get minute
    calendar.setTimeInMillis(time);
    int minute = calendar.get(Calendar.MINUTE);

    // check if it is new minute
    if(currentCandle == null ||
            currentCandle.startTime.get(Calendar.MINUTE) != minute){
        return true;
    }
    return false;
}

int numberOfCandles = 60;

Point mousePt;

CoordSysRenderer coordsys = new CoordSysRenderer();

// We want to display Triangles, so we need the appropriate renderer.
public CandlesPanel(){

    panelRenderer.addItemToRender(priceHorizontalLineTriangles);

    // add all candles
    for (int i = 0; i < numberOfCandles; i++) {
        Triangles t = new Triangles();
        candlesTriangles.add(t);
        panelRenderer.addItemToRender(t);
        candlesQueue.add(new Candle());
    }

    // 1 hour of view, 60 minutes
    viewStart = Calendar.getInstance();
    viewStart.set(2023, Calendar.APRIL, 28, 10, 00, 00); // 20230428 10:00:00.000
    viewStart.clear(Calendar.MILLISECOND);

    viewEnd = Calendar.getInstance();
    viewEnd.set(2023, Calendar.APRIL, 28, 11, 00, 00); // 20230428 11:00:00.000
    viewEnd.clear(Calendar.MILLISECOND);

    // use a coordinate system for display
    coordsys.setCoordinateView(0, viewLow, numberOfCandles, viewHigh);

    // set the content renderer of the coordinate system
    coordsys.setContent(panelRenderer);

    this.setRenderer(coordsys);

    this.asComponent().addMouseListener(new MouseAdapter() {

        @Override
        public void mouseClicked(MouseEvent e) {
            // get candle over which mouse is hovering
            int pickingPixel = getPixel(e.getX(), e.getY(), true, 3);
            Integer index = candlePickingRegistry.lookup(pickingPixel);
            if(index != null){
                Candle candle = candlesQueue.get(index);
                System.out.println("Clicked candle: " + candle);
                candleClicked(candle);
            }
        }

        @Override
        public void mousePressed(MouseEvent e) {
            mousePt = e.getPoint();
        }

    });

    //this.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));

    this.asComponent().addMouseMotionListener(new MouseMotionAdapter() {
        @Override
        public void mouseDragged(MouseEvent e) {
            int dx = e.getX() - mousePt.x;
            int dy = e.getY() - mousePt.y;

            viewLow += dy / 5;
            viewHigh += dy / 5;

            coordsys.setCoordinateView(0, viewLow, numberOfCandles, viewHigh);
            mousePt = e.getPoint();

            repaint();
        }
    });

}

private void candleClicked(Candle candle) {

    clickSubscribers.forEach(subscriber -> subscriber.candleClicked(candle));

}

public void addHorizontalLine(double price, Color color) {

    Triangles priceHorizontalLine = new Triangles();

    priceHorizontalLine.addQuad(new Rectangle2D.Double(0, price, 60, 0.075));
    priceHorizontalLine.getTriangleDetails().forEach(triangleDetails -> triangleDetails.setColor(color));

    panelRenderer.addItemToRender(priceHorizontalLine);

    this.repaint();
}
}
hageldave commented 1 year ago

Okay so I didn't debug your code or anything, but just from the stack trace, it seems that you are accessing triangle details from different concurrent threads. This can easily happen when you do calculations on the main thread, then modify a Triangles (or Lines or Points) object accordingly from this thread. The rendering takes place on the AWT event despatch thread, and when rendering and modifications take place at the same time it can cause a concurrent modification exception.

In this case you can for example use SwingUtilities.invokeLater(...) or SwingUtilities.invokeAndWait(...) for the part that modifies the Renderable object (Triangles), which will push this action onto the event dispatch queue and will then be called on the dispatch thread (and therefore not concurrently with rendering).

Another issue could be that you are not running the rest of your AWT/Swing application on the event dispatch thread (which you should). I would recommend to call things like frame.setVisible(true) with invokeLater.

It could also be a one time problem when setting up the Renderable objects on the main thread but already displaying the window somewhere in between. In this case you can simply call the window showing code in the end, after everything is set up.

For further debugging you can use SwingUtilities.isEventDispatchThread() to check which parts are and which are not running on the dispatch thread.

Is this helping a bit?

gmaczuga commented 1 year ago

That helped! Thx