Anvil Skulpt Python Javascript Worker Threads

Hi,

I’m evaluating Anvil to see it’s viable to use in a commercial product. We’ll be displaying a KPI dashboard for some PLCs. The KPIs will be uploaded to an MySQL DB by the PLCs.

I have a server microservice that obtain the latest values.

The form has code to get update the KPI elements - gauges, bullets & graphs. I want to update these every 10s.

So I first I tried to Python multiprocessing but Skulpt, the Anvil Python to Javascript transpiler does not support it.

I had a little look around and it appears Javascript has worker threads that can execute separate to the main thread. My thinking is to let the KPI 10s refresh to be run in a worker thread so the main Javascript thread can carry out the processing of the main app and not get blocked by the screen refresh task.

How would i go about utilising these ‘worker threads’ in Anvil/Skulpt?

My form code: -


def update_kpi_elements(lis_dic_kpis, obj_form, str_return_list_key):
  '''
  updates the KPI gauges, bullets, plots etc...
  
  lis_dic_kpis   is a list of dictionaries   describes the key performance indicator for the gauge/plot = [{str_kpi_type, int_person_id:,  str_subject:, str_date_time_of_test:, flo_grade:, str_plot:},]
  e.g. [{kpi_type: grade, int_person_id:1, str_subject:Physics, str_date_time_of_test:, flo_grade:, str_plot:Form1.plot_1}, {kpi_type: grade, int_person_id:2, str_subject:Music, str_date_time_of_test:latest, flo_grade:, str_plot:Form1.plot_FazLatest}]

  obj_form   Anvil form object,   the form with the KPI guages 
  '''
  write_log_msg(str_func_name=update_kpi_elements.__name__, str_state='entry')
  lis_dic_kpis = lis_dic_kpis
  obj_kpis_form = obj_form
  str_return_list_key = str_return_list_key

  #let's get the kpi_values that need to be displayed
  write_log_msg(str_func_name=update_kpi_elements.__name__, str_state='executing', str_type='DEBUG', str_message=f'Client Message - GOING TO SERVER')
  lis_dic_kpis = anvil.server.call('return_kpi_values', lis_dic_kpis, str_return_list_key)
  write_log_msg(str_func_name=update_kpi_elements.__name__, str_state='executing', str_type='DEBUG', str_message=f'Client Message - RETURNED FROM SERVER')
  write_log_msg(str_func_name=update_kpi_elements.__name__, str_state='executing', str_type='DEBUG', str_message=f'lis_dic_kpis=({lis_dic_kpis})')

  #lets build the form plots
  flo_grade = lis_dic_kpis[0][str_return_list_key]
  write_log_msg(str_func_name=update_kpi_elements.__name__, str_state='executing', str_type='DEBUG', str_message=f'Client Message: flo_grade=({flo_grade}), lis_dic_kpis[0][\'flo_grade\']=({lis_dic_kpis[0][\'flo_grade\']})')
  obj_gauge = plotly_graph.Figure(plotly_graph.Indicator(mode="gauge+number", 
                                                         value=flo_grade,
                                                         gauge={'axis': {'range': [None, 100]}, 
                                                                'bar': {'color': "black"},
                                                                'steps': [{'range': [0, 30], 'color': "red"}, {'range': [80, 100], 'color': "gold"}], 
                                                                'threshold': {'line': {'color': "black", 'width': 4}, 
                                                                'thickness': 0.75, 
                                                                'value': flo_grade}}, 
                                                          domain={
                                                                'x': [0, 0], 
                                                                'y': [0, 0]}, 
                                                          title={'text': 'Grade Latest'}))
  obj_kpis_form.plot_HaiderLatest.data = obj_gauge
  write_log_msg(str_func_name=update_kpi_elements.__name__, str_state='exit')
  
class Form1(Form1Template):
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)
    write_log_msg(str_func_name=Form1.__name__, str_state='entry')

    # Any code you write here will run before the form opens.
    
    write_log_msg(str_func_name=Form1.__name__, str_state='entry')
    lis_dic_kpis = [{'str_kpi_name': 'Form1.plot_1', 'kpi_type': 'grade', 'int_person_id':1, 'str_subject':'Physics', 'str_date_time_of_test':'latest', 'flo_grade':None}, 
            {'str_kpi_name': 'Form1.plot_FazLatest', 'kpi_type': 'grade', 'int_person_id': 2, 'str_subject': 'Music', 'str_date_time_of_test': 'latest', 'flo_grade':None}]
    str_return_list_key = 'flo_grade'
    update_kpi_elements(lis_dic_kpis=lis_dic_kpis, obj_form=self, str_return_list_key=str_return_list_key)
    write_log_msg(str_func_name=Form1.__name__, str_state='exit')

Thanks
Haider

Have you tried using a Timer component to do the polling?

That’s what timers are for, as @jshaffstall says.

It’s not the timer element I’m worried about, it’s more if Anvil is processing the refreshing of the KPI gauges and graphs, will it be able to process a user has clicked a button to say stop the production line and process that request at the same time? We don’t want the system to not be processing user inputs while KPIs are being refreshed.

The PLC alters the manufacturing process by getting a ‘callback’ from the MySQL DB if certain DB fields are changed by the Anvil App. TBH that the whole point of the system is you have a central place to control all the different PLCs and view their KPIs without having to walk over to each one…

The Timer’s tick event handler does not block user input from being processed.

Are you talking about a button on an Anvil form or a physical button controlled by a plc?

If I understand you have a plc controlling buttons and collecting data and you want an Anvil app to show that data.

If that is the case then you have a timer calling a server function every 10 seconds, the server function collects all the info and returns it to the form, the form renders all the charts and whatever you have on the ui.

If that is not the case, then please explain your requirements.

Yes, I’m talking about a button in the Anvil web-app. My question, to phrase it another way is, while the form is processing the KPI screen refresh can it handle another event in tandem e.g. a user clicks a button for some other function.

From Jay’s post above it seems yes Anvil should handle the KPI refresh and user input at the same time.

The tick event triggered by the timer every 10 seconds will call the server function, wait for the data to come back from the server, update the UI and give the control back to the form.

The click events are triggered when the buttons are clicked by the user, and they will immediately do their job, unless another event handler is already running.

So, the buttons will not be responsive while the tick event is calling the server and updating the UI and will be responsive during the 9.5 seconds between tick events.

If I remember Anvil Extras (or Anvil Labs?) has a library that manages the javascript worker threads. If you really want to try, you can have a look at it. I have similar applications and I’m very happy to live with that half second delay. But I also don’t know your requirements.

I think you’re clearly at a point where you need to write a proof of concept using the Timer and test out for yourself whether it’s responsive enough for your needs.

That’s fine guys, To be honest the whole point of the evaluation is gaining knowledge about Anvil; I can then steer the requirements and design around that. For instance we could increase the delays between KPI updates and the no. of KPIs per screen to ensure the site feels responsive; get the balance right…I will play around with the timer. I’m waiting for the PLC setup to be completed so I can get a data feed into the DB…

This would not make the UI more responsive. Well, it depends on how you measure the responsiveness of the UI.

By decreasing the polling time you decrease the chances that the user clicks the button while the form is handling the tick event, but you are not going to change the maximum delay.

For example, if it takes 1 second to call the server and update the form and the timer has 10 second interval, then you have 90% chances of zero delay and 10% chances of 0 to 1 second delay.

If you decrease the timer to 2 seconds, then you have 50% chances of zero delay and 50% chances of 0 to 1 second delay.

So, the average delay increases, but the maximum delay stays the same.

Calling a server function should have no noticeable impact on interactivity. While it is blocking in the sense that the next line of code won’t execute until the server call returns, it is not blocking in the sense that the page remains interactive. This is how the browsers work and it differs from executing python natively.

A simple test can reveal this.


def button_click(self, **event_args):
    print("clicked")
    anvil.server.call_s("slow", 1)


@anvil.server.callable
def slow(n=1):
    from time import sleep
    print("slow")
    sleep(n)
    print("done")

Observe you can click the button multiple times despite the button event handler being in the middle of a server call.
(This can be a gotcha if you have a submit button that inserts a row to a database. You’d want to disable the button inside the event handler and enable it once it’s finished to avoid inserting a row twice)


The only way to prevent interactivity through blocking code is to do something expensive, that’s also synchronous (no server calls or sleeps).


def button_click(self, **event_args):
    print("clicked")
    for _ in range(2**25):
        pass
    print("done")

Observe that you can’t click the button again because the browser is effectively locked up.


In terms of a WebWorker, this can help for expensive calculations, but if you’re not doing expensive calculations you won’t find benefit from using one.
A WebWorker is also limited in scope. It can’t access elements on the page, so it would be unable to update the DOM. You’re basically limited to working with JSONable types.

To me at least it doesn’t look like you’re doing anything particularly expensive on the client.
And so a Timer seems like the way to go (as others have said)

That said, if your server call is particularly slow, you may find benefit in moving that code to a WebWorker. Anvil server calls won’t happen in parallel and the next call to the server is queued until the current call has completed. This can make the page feel less responsive.

If you do decide to explore that option you might want to create a discussion over at anvil-labs to get some help on the API where we have an implementation of WebWorkers in python: anvilistas/anvil-labs · Discussions · GitHub

2 Likes