How to call a function asynchronously in Perspective session

I have a system I created with two views. On the first view is a table, and when a row is selected and the Edit button is clicked, a new view pops up with the selected row data passed in as a parameter. On the second view is a form and another table. Users enter information into the form, which adds a new row to the second table. Also on the second view is a Complete button, which takes every single row in the second table and inserts it into a database through an API (to an onsite webserver). On a successful insert, the second view closes, and the items appear in a row subview (if expanded) of the first table.

The problem is, if the second table has more than about 5 entries in the table, the UI lags considerably. For instance, a user reported that when they had 10 entries, the UI didn’t respond for 15 seconds. This is very inconvenient.

I would like to insert the records asynchronously, so that the popup view closes and then the necessary data is inserted when available. I used the system.util.invokeAsynchronous and system.util.invokeLater functions in Vision projects before, but the scope for the invokeLater function is listed as Vision only. Here is the example listed in the documentation:

def longProcess(rootContainer = event.source.parent):
   import system
   results = ...( something )
   def sendBack(results = results, rootContainer = rootContainer):
      rootContainer.resultsProperty = results
   system.util.invokeLater(sendBack) # only valid for Vision scope?
    
system.util.invokeAsynchronous(longProcess) 

This example doesn’t work with a Perspective session. How would I call a function asynchronously in a Perspective session? Also, is a new thread created on the Ignition server for every individual session?

A few more things to note: the API is third-party, something I have no control over. There is no batch insert, so I iterate over every row in the second table manually. Also, when I use Postman or curl to insert, the time to insert a single record takes about ~250ms.

Every call to invokeAsynchronous creates an additional thread. Be judicious about the use of the function.

There’s no equivalent to invokeLater in Perspective, because Perspective’s single-threaded event dispatcher is all on the frontend (the browser/device/workstation app) happening in the wide, wonderful world of Javascript. All scripting exposed to users, meanwhile, is happening on the ‘backend’, so that you can continue to use all the Ignition scripting paradigms you’re used to.

What I would recommend is using invokeAsynchronous as you are, and passing a session ID and/or page ID into the asynchronous thread. Then from the async thread, you can call system.perspective.sendMessage to broadcast back to the front-end. You could even have individual progress states this way.

3 Likes

I guess I misunderstood the purpose of system.util.invokeLater. Does it just define what happens after the asynchronous function executes?

system.util.invokeLater runs a function on the Swing EDT. It’s very special purpose for when you’re running code in the Vision Client or Designer and need to interact with the UI from a background thread.

1 Like

Not being completely up to speed on Perspective yet but…
Could you create a gateway function that accepts a dataset and does the inserts?
Then you could call the function and close the popup?
Have the function write out to a status tag for success or failure and monitor that from the first view?

I do do that, sort of. The actual API insert is a gateway function that accepts a few parameters.

Regarding a more general point, even if the function is on the gateway and it is called from an onActionPerformed script, doesn’t it still execute on one thread? @PGriffith

Look at this example below I tried in the script console:

def printMessage():
    import time
    time.sleep(5)       # wait for 5 seconds
    print "first"

printMessage()
print "second"

The above code first prints "first" and then "second", as you would expect. However, it also waits for 5 seconds. It is identical to this:

import time
time.sleep(5)
print "first"
print "second"

@MMaynard you aren’t the first person who told me about this approach, though. My boss said he also uses it and I’m confused as to how that would be any different (offloading to gateway script or a function inside onActionPerformed script) from keeping all of the code in a script inline (like the second code snippet above).

Thanks in advance.

Yes, writing it in a function vs writing it ‘inline’ is exactly the same - but encapsulating it into a function encourages good practices, and makes it trivial to call asynchronously. It’s also possible to ‘test’ business logic when it’s in functions (although in practice, Ignition does nothing to make that easier for you).

1 Like

Paul,
Lets say the function is in a global script library like

def DoSomething():
     #Following code extremely simplified for examples.
     ret = (collect data from dataset tag and insert code)
     #set flag to true or false on success/failure
    if ret:
        system.tag.write('[default]Global/InsertResult',1)
    else:
        system.tag.write('[default]Global/TriggerInsert',0)
    #Reset the trigger tag
    system.tag.write('[default]Global/TriggerInsert',0)

Then using a boolean memory tag script to trigger it

def valueChanged(tagPath, previousValue, currentValue, initialChange, missedEvents):
    if not initialChange and currentValue.value==1:
        global.doSomething()

Then on the popup write the values to a dataset tag and trigger the boolean tag to kick it off.
Wouldn’t that prevent the GUI from being locked while the processing is happening?
We use something like this for large downloads to the local system backends and it keeps the GUI from being taken over.

Is thread creation and destruction done automatically (by OS, JVM or whatever it may be) or is there a command to let the same party know to close the thread? Will the thread be terminated upon the script inside it running completely or throwing an exception?

Yes, that's perfectly reasonable and is a true async invocation. There's a tradeoff; it's easier to trigger from either Vision or Perspective if you need to support both, but I would argue harder to reason about down the road - since you've got trigger logic separate from data separate from data handling code.
However, no matter what you do, there's no way to put absolutely everything together, so I don't think there's any problems with that approach.

8.0.11+ offers some visibility and ability to terminate threads: Diagnostics - Running Scripts - Ignition User Manual 8.0 - Ignition Documentation
And yes, the thread will automatically clean itself up once it doesn't have anything else to run - whether by a normal exit or an exception.

2 Likes