G4P GTextArea & GLabel object update across windows cause app crash

HI - I have been experiencing random an intermittent lock ups of my application. I am using G4P 4.1.5 on processing 3.3.7 so all upto date.

I have worked out that my problem is caused by using multiple windows with a slider control that updates a text area/label displayed on another window.

Does anyone know if there is a better way to achieve this or is this simply a not possible with processing?

The following code demonstrates the problem – it uses a knob control to update a GTextArea object.
When the slider and text area are instantiated on different windows (as in the demo code) the problem happens randomly before lockup, from almost immediately to up to a few minutes.

When the slider and text area are on the same window the problem does not occur.

import g4p_controls.*;

GWindow window_setup;
GKnob setup_knob; 
GTextArea text;
GLabel label;
boolean cntdwn=false;

void setup() {
  size(250, 200, JAVA2D);
  
  //window setup
  window_setup = GWindow.getWindow(this, "settings", 70, 160, 250, 200, JAVA2D);
  window_setup.addDrawHandler(this, "window_setup_draw");
  
  //GTextArea / GLabel- cause java.util.ConcurrentModificationException 
  //                    or java.lang.NullPointerException amongst others?
  //                    when accessed from separate window
  text = new GTextArea(this, 20, 20, 192, 45, G4P.SCROLLBARS_NONE);
  //text = new GTextArea(window_setup, 20, 20, 192, 45, G4P.SCROLLBARS_NONE);
  
  label = new GLabel(this, 25, 80, 192, 45);
  
  //setup knob
  setup_knob = new GKnob(window_setup, 20, 80, 209, 203, 0.8);
  //setup_knob = new GKnob(this, 20, 80, 209, 203, 0.8);
  setup_knob.setTurnRange(180, 0);
  setup_knob.setTurnMode(GKnob.CTRL_HORIZONTAL);
  setup_knob.setSensitivity(1);
  setup_knob.setShowArcOnly(true);
  setup_knob.setOverArcOnly(true);
  setup_knob.setIncludeOverBezel(false);
  setup_knob.setShowTrack(true);
  setup_knob.setLimits(90.0, 0.0, 180.0);
  setup_knob.setShowTicks(true);
  setup_knob.setOpaque(false);
  setup_knob.addEventHandler(this, "setup_knob_turn1");
}

void move_knob() {
  if(setup_knob.getValueF()>=180)
    cntdwn=true;
  else if(setup_knob.getValueF()<=0)
    cntdwn=false;
  
  if(cntdwn)
    setup_knob.setValue(setup_knob.getValueF()-1);
  else
    setup_knob.setValue(setup_knob.getValueF()+1);
}

void draw() {
  background(230);
  
  move_knob();
}

synchronized public void window_setup_draw(PApplet appc, GWinData data) { //_CODE_:window_setup:219612:
  appc.background(230);
}

//pass in a millis() duration value
//returns a string with hh:mm:ss.milli format
String getElapsedTime(long duration) {
  long millis=(duration%1000);
  long secs=(duration/1000)%60;
  long mins=(duration/(1000*60))%60;
  long hours=(duration/(1000*60*60))%24;
  //formating / padding with zeros
  String[] time=new String[4];
  time[0] = (hours<10) ? "0"+hours : ""+hours;
  time[1] = (mins<10) ? "0"+mins : ""+mins;
  time[2] = (secs<10) ? "0"+secs : ""+secs;
  time[3] = (millis<10) ? "00"+millis : (millis<99) ? "0"+millis : ""+millis;
  return time[0] + ":" + time[1] + ":" + time[2] + "."+time[3];
}

public void setup_knob_turn1(GKnob source, GEvent event) { 
  println("setup_knob - GKnob >> GEvent." + event + " @ " + millis()+" "+ getElapsedTime(millis()));
  text.setText(setup_knob.getValueF()+"\nTest @ "+millis()+" "+ getElapsedTime(millis()));
  //label.setText(setup_knob.getValueF()+"\nTest @ "+millis() +" "+ getElapsedTime(millis()));
}

On crash I get exceptions of one of either “java.lang.NullPointerException, java.util.ConcurrentModificationException, or java.lang.IllegalArgumentException: Invalid substring range”
e.g.
java.lang.NullPointerException
at java.util.LinkedList$ListItr.next(LinkedList.java:893)
at g4p_controls.GTextArea.updateBuffer(Unknown Source)
at g4p_controls.GTextArea.draw(Unknown Source)
at g4p_controls.GWindowImpl.draw(Unknown Source)
at sun.reflect.GeneratedMethodAccessor6.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at processing.core.PApplet$RegisteredMethods.handle(PApplet.java:1408)
at processing.core.PApplet$RegisteredMethods.handle(PApplet.java:1401)
at processing.core.PApplet.handleMethods(PApplet.java:1600)
at processing.core.PApplet.handleDraw(PApplet.java:2439)
at processing.awt.PSurfaceAWT$12.callDraw(PSurfaceAWT.java:1557)
at processing.core.PSurfaceNone$AnimationThread.run(PSurfaceNone.java:316)

Thanks any thoughts or help appreciated!

Each window is operating in its own thread. The error occurs when the secondary thread is updating the textarea display and the main window (the knob) attempts to change it at the same time. Hence the concurrent error message.

In this case both controls are being processed in the same thread - so no problem.

Unfortunately there is no work around to avoid this problem.

BTW nice that you have tried to solve the problem yourself and posted a simple sketch to demo the problem. Made my life easier.

1 Like

Thanks for the quick response and clear explanation.

If it’s because of the separate thread for the additional window and timing issues, is there no way to have both windows managed by one thread?

In my use case its convenient to reduce main window clutter by putting some less used controls on a separate window.

You could put all those controls in one floating panel. Then you can show/hide them by changing the visibility only of the panel. I use this for advanced options in my programs.

2 Likes

No. The OS manages ALL windows as separate threads.

@quark - Ah ok fair enough - no way round the OS! I was thinking about implementing a simple queue/deque messaging between the windows to avoid the timing issues - I think that will would work and be thread safe.

@MxFxM thanks for the idea. I should have gone this route from the start! The GPanel approach seems the simple way to do this! Unfortunately I will need to unpick quite alot of my code to do this for my app and I prefer the window being free from my main application window I dont think is possible with GPanel.

To be pedantic, it’s Processing that goes out of its way to do that, not the OS (and certainly not Java2D which wants to use one thread for these!)

Each PApplet instance got a Thread associated w/ it. :thread:
I don’t see anything wrong w/ it. :confused:

Pros and cons - I didn’t actually say specifically there was something wrong with it. However, the OS and Java would generally try to run all windows of one application from a single event thread. Like I said, it’s Processing not the OS behaviour involved here. And a con is the situation being described!

For completeness and reference for anyone else in the future I updated my demo code to implement a simple queue to work around the lock up problem.
The following code works nicely for the text update in the separate window thread and also shows the GPanel suggestion in action.

import g4p_controls.*;

GPanel panel_setup;//suggestion from MxFxM works nicely
GWindow window_setup;
GKnob setup_knob; 
GTextArea text,text_panel;
GLabel label;
boolean cntdwn=false;

//workaround is to create a msg queue between the windows 
StringMsgQueue msgQ4text=new StringMsgQueue(10);//see simple class def below

void setup() {
  size(300, 400, JAVA2D);
  
  //window setup
  window_setup = GWindow.getWindow(this, "settings", 70, 160, 250, 200, JAVA2D);
  window_setup.addDrawHandler(this, "window_setup_draw");
  
  //GTextArea / GLabel- cause java.util.ConcurrentModificationException 
  //                    or java.lang.NullPointerException amongst others?
  //                    when accessed from separate window
  text_panel = new GTextArea(this, 20, 20, 192, 45, G4P.SCROLLBARS_NONE);//for the panel approach
  text = new GTextArea(window_setup, 20, 20, 192, 45, G4P.SCROLLBARS_NONE);
  
  label = new GLabel(this, 25, 80, 192, 45);
  
  //setup knob
  setup_knob = new GKnob(this, 20, 10, 209, 203, 0.8);
  //setup_knob = new GKnob(this, 20, 80, 209, 203, 0.8);
  setup_knob.setTurnRange(180, 0);
  setup_knob.setTurnMode(GKnob.CTRL_HORIZONTAL);
  setup_knob.setSensitivity(1);
  setup_knob.setShowArcOnly(true);
  setup_knob.setOverArcOnly(true);
  setup_knob.setIncludeOverBezel(false);
  setup_knob.setShowTrack(true);
  setup_knob.setLimits(90.0, 0.0, 180.0);
  setup_knob.setShowTicks(true);
  setup_knob.setOpaque(false);
  setup_knob.addEventHandler(this, "setup_knob_turn1");
  
  //BTW the panel method is simpler and works quite nicely within the main window thread
  panel_setup =new GPanel(this,20, 150, 250, 200, "setup");
  panel_setup.addControl(text_panel);
}

void move_knob() {
  if(setup_knob.getValueF()>=180)
    cntdwn=true;
  else if(setup_knob.getValueF()<=0)
    cntdwn=false;
  
  if(cntdwn)
    setup_knob.setValue(setup_knob.getValueF()-1);
  else
    setup_knob.setValue(setup_knob.getValueF()+1);
}

void draw() {
  background(230);
  move_knob();
}

synchronized public void window_setup_draw(PApplet appc, GWinData data) { //_CODE_:window_setup:219612:
  appc.background(230);
  //THIS dequeue MUST go into the draw method of the window that owns the GTextArea/ GLabel!!
  if(msgQ4text.queueLength()>0) {
    text.setText(msgQ4text.dequeue());//WARNING the setText MUST ONLY be called in the draw method of the window that owns the control
  }
}

//pass in a millis() duration value
//returns a string with hh:mm:ss.milli format
String getElapsedTime(long duration) {
  long millis=(duration%1000);
  long secs=(duration/1000)%60;
  long mins=(duration/(1000*60))%60;
  long hours=(duration/(1000*60*60))%24;
  //formating / padding with zeros
  String[] time=new String[4];
  time[0] = (hours<10) ? "0"+hours : ""+hours;
  time[1] = (mins<10) ? "0"+mins : ""+mins;
  time[2] = (secs<10) ? "0"+secs : ""+secs;
  time[3] = (millis<10) ? "00"+millis : (millis<99) ? "0"+millis : ""+millis;
  return time[0] + ":" + time[1] + ":" + time[2] + "."+time[3];
}

public void setup_knob_turn1(GKnob source, GEvent event) { 
  println("setup_knob - GKnob >> GEvent." + event + " @ " + millis()+" "+ getElapsedTime(millis()));
  //update on other window thread via msg Q
  msgQ4text.enqueue(setup_knob.getValueF()+"\nTest @ "+millis()+" "+getElapsedTime(millis()));
  //the panel is on the main window so setText can be used directly
  text_panel.setText(setup_knob.getValueF()+"\nTest @ "+millis()+" "+getElapsedTime(millis()));
}

//class implements a simple circular buffer
class StringMsgQueue {
  public
    StringMsgQueue(int sz){
      msgQueue=new String[sz];
    }
    //enqueue adds message strings to end of the buffer if it is not full
    //returns true if message added. false if the buffer is full.
    boolean enqueue(String str) {
      boolean bStatus=false;
      if(isFull()==false) {
        msgQueue[msg_write_idx]=str.toUpperCase().trim();
        msg_write_idx = (msg_write_idx + 1) % msgQueue.length;
        msg_cnt++;
        bStatus=true;
      }
      return bStatus;
    }
    //peek returns the top queue item without removing it
    //if the queue is empty returns null
    String peek() {
      return (msg_cnt>0) ? msgQueue[msg_read_idx] : null;
    }
    //dequeue removes the top item in the buffer and returns it
    String dequeue() {
      String cmd=peek();//try to read the top buffer msg
      if (cmd!=null) {
        msgQueue[msg_read_idx]="";
        msg_read_idx = (msg_read_idx + 1) % msgQueue.length;
        msg_cnt--;
      }
      return cmd;
    }
    //return the number of messages in the queue
    int queueLength() {
      return msg_cnt;
    }
    //return the total size of the message buffer
    int size() {
      return msgQueue.length;
    }
    //return true if the buffer is full
    boolean isFull() {
      return (msg_cnt<size()-1) ? false : true;
    }
    //clear the buffer by resetting the idx and counters
    void clear() {
      msg_read_idx = 0; 
      msg_write_idx = 0;
      msg_cnt = 0;
    }
    
  private 
    String [] msgQueue;//circular buffer
    int msg_read_idx = 0; // buffer read position 
    int msg_write_idx = 0; // buffer write position 
    int msg_cnt = 0; // count of commands in the queue 
}

@neilcsmith & @GoToLoop - I found this behaviour quite confusing and not obvious for a beginner.
As another con the error messages are not helpful and crashes unpredictable.
I would think for simplicity an option for a single thread or someway way of warning the beginer and would be helpful and avoid many head scratching hours!

Yes, I know! It’s one of the reasons PraxisLIVE exists actually. Threading in Processing is a pain, and it’s missing anything that might make it easier.

Now, what you probably don’t want to hear is that your queue is still broken. You’d be better using something like ConcurrentLinkedQueue (Java Platform SE 7 ) for this.

1 Like

Processing does not support multiple windows, so multiple event threads are not considered!. G4P gets round this by creating new PApplet instances

1 Like

er … yeah! :smile: And you can’t even do that reliably in one VM using OpenGL without crashing. This could all easily be fixed, but despite Processing shipping with a multiple windows example, it’s considered unsupported. And none of those Processing threads are the OS event thread!

1 Like

I didn’t know that unfortunately I can’t find it can you give me a pointer to where it is. :smile:

Does the ‘you’ refer to me? If so what is the ‘that’?

It’s under Demos / Tests which I don’t think means it’s meant to be supported! :smile:

No. I mean “it is not possible” to use multiple windows both using the OpenGL renderer reliably - there is static state in the renderer which causes crashes.

Actually thats great I do want to know this! Looking at the link you provided its really easy to use the ConcurrentLinkedQueue in place of my simple class. Thanks!