Issue
I am prototyping a web-app using Dash which - among other things - performs some measurements through a serial peripheral periodically. As for now I am using a dcc.Interval
component to periodically take a measurement from my sensors, and then plot and store them.
However, either using Firefox or Chrome, when the tab is not in the foreground, the performance throttling mechanisms of the web browsers greatly reduces the frequency at which the dcc.Interval components fires. At some points, the background timers even stop completely!
The issue is documented here and there.
I was able to create the following minimum working example which just counts in the console output:
import dash
from dash.dependencies import Input, Output
import dash_html_components as html
import dash_core_components as dcc
app = dash.Dash(__name__)
app.layout = html.Div([
html.Div("A div", id="my_div"),
dcc.Interval(id='my_interval', disabled=False, n_intervals=0),
])
@app.callback(Output('my_div', 'children'), [Input('my_interval', 'n_intervals')])
def update(n_intervals):
print(n_intervals)
return dash.no_update
if __name__ == '__main__':
app.run_server(debug=True, host='0.0.0.0', port=5000)
While this works fine when the tab is active, counting one by one each second (the default delay between two firings of the dcc.Interval
component), it does not work anymore when the web browser switches for other tab. In particular, after a while, the interval between two firings increases.
With this very simple example it is not always the case depending on your browser and machine. But for a more complex app, the time between two callbacks while in background can reach several, up to several tens of seconds.
Question
Would you know a workaround, or a way to force the browser to consider the app as "important" thus not throttling it? The main objective would be that, at all time, the dcc.Interval
components embedded in the app continue to fire at the same rate.
Workarounds
Up to now I tested three solutions, with little success:
- Use
dash_devices
- Play an audio file to prevent the tab from going background
- Place the
dcc.Interval
higher in the hierarchy
Using dash_devices
I stumbled on this thread and dash_devices
seemed like a good idea since it uses websockets instead of HTTP requests for updates. I managed to make it work on a basic example but it needs to be tested at bigger scale.
Play an audio file
Playing audio (either at low volume or by disabling the tab sound if you do not want to listen to it) is a good solution to keep the tab in the foreground from the browser point of view.
However, if I was able to use the following snippet:
html.Audio(autoPlay=True, src='http://www.hochmuth.com/mp3/Haydn_Cello_Concerto_D-1.mp3', loop=True)
the same code using a path instead of an URL, i.e. either src='/path/to/my_audio_file.mp3'
or src='file:///path/to/my_audio_file.mp3'
does not seem to work (no audio is playing) and I don't know why...
I also tried something with base64
according to what I read here and there but only the first element works:
import dash
from dash.dependencies import Input, Output
import dash_html_components as html
import dash_core_components as dcc
import base64
haydn_path = "/Users/XXX/Haydn.mp3"
encoded_haydn = base64.b64encode(open(haydn_path, 'rb').read())
app = dash.Dash(__name__)
app.layout = html.Div([
html.Div("A div", id="my_div"),
dcc.Interval(id='my_interval', disabled=False, n_intervals=0),
html.Audio(autoPlay=True, src='http://www.hochmuth.com/mp3/Haydn_Cello_Concerto_D-1.mp3', loop=True),
html.Audio(autoPlay=True, src=haydn_path, loop=True),
html.Audio(autoPlay=True, src='file://' + haydn_path, loop=True),
html.Audio(autoPlay=True, src='data:audio/mp3;base64,{}'.format(encoded_haydn), loop=True),
])
@app.callback(Output('my_div', 'children'), [Input('my_interval', 'n_intervals')])
def update(n_intervals):
print(n_intervals)
return dash.no_update
if __name__ == '__main__':
app.run_server(debug=True)
Of the four audio players displayed, only the first one is able to play something (the other are greyed as if no audio file was specified).
Notes:
- for the solution involving base64, I only adapted from what I saw, I may have made a mistake at some point...
- I know this solution is a dirty fix, but at least it partially works.
Place the dcc.Interval
higher in the hierarchy
Positioning the dcc.Interval
component higher in the hierarchy seemed to work in certain cases, as reported here. However I tried it without success.
Conclusion and Outlooks
I would thus be very thankful to anyone who could help me with this. Either by:
- finding a way to fire the
dcc.Interval
elements for background tabs, - finding a way to play locally stored audio file with an
html.Audio
component - finding another way round to have a Dash application on the one hand, and some periodically fired event on the other hand, while linking the two of them...
Originally asked 09/04/2021
Update as of 13/04/2021
According to emher's answer, I re-oriented toward a separate architecture with two different threads:
- One thread performing periodic (using a
while True / time.sleep()
routine) pooling of my serial peripheral, written in pure Python, and storing the result of its pooling inside a global variable. - Another thread running the Dash application and periodically (using
dcc.Interval
) reading inside the afore-mentioned global variable.
Below is a minimal working example:
import dash
from dash.dependencies import Input, Output
import dash_html_components as html
import dash_core_components as dcc
import threading
import time
counter = 0
app = dash.Dash(__name__)
class DashThread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
global counter
global app
app.layout = html.Div([
dcc.Interval(id='my_interval', disabled=False, n_intervals=0),
html.Div("Counter :", style={"display": "inline-block"}),
html.Div(children=None, id="cnt_val", style={"display": "inline-block", "margin-left": "15px"}),
])
@app.callback(Output('cnt_val', 'children'), [Input('my_interval', 'n_intervals')])
def update(_):
return counter
app.run_server(dev_tools_silence_routes_logging=True) # , debug=True)
class CountingThread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
global counter
while True:
counter += 1
print(counter)
time.sleep(1)
a = DashThread("The Dash Application")
b = CountingThread("An Independent Thread")
b.start()
a.start()
a.join()
b.join()
Please note the commented-out debug=True
argument at the end of line:
app.run_server(dev_tools_silence_routes_logging=True) # , debug=True)
This is because the way Dash calls Flask does not allow for debug mode to be enabled when the Dash application is launched from a thread which is not the main one. The complete issue is documented here.
This script just counts one every second, no matter if the Dash application is loaded in a browser, or in the foreground. While the script is still running, so will the counter, and opening the Dash application in a tab or bringing it to the foreground will only update the counter's display.
Disclaimer: contrary to the dcc.Interval
method presented when the question was originally asked, the code above will progressively de-sync. Indeed, using dcc.Interval
, the associated callback
is called every 1.0 s, no matter whether the previously called callback
finished running or not.
So if we run the program at t=0s and supposing that the tab is in foreground, we will see the following calls:
t=0s : callback()
t=1s : callback()
t=2s : callback()
t=3s : callback()
[...]
At the opposite, using the multithreaded approach, given the code that we want to run (above: counter += 1 ; print(counter)
) takes an execution time dt, we will see the following calls:
t=0s : callback()
t=1s + dt : callback()
t=2s + 2*dt : callback()
t=3s + 3*dt : callback()
[...]
That is, the execution chain is progressively de-syncing from the expected "one callback per second" behavior. This can be tricky in certain situation, in which case please refer here for a workaround.