3

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.

mranvick
  • 339
  • 1
  • 2
  • 10

1 Answers1

2

For this kind of use case, I would generally prefer an architecture where a separate process collects the data and inserts in into a cache (e.g. Redis) from which the UI (in your case Dash) reads the data. This solution is much more robust than fetching the data directly from the UI.

If you insist on fetching the data directly from UI, I would suggest using a Websocket component instead of the Interval component. While I haven't tested extensively, it is my impression that the connection will stay alive on most platforms.

emher
  • 5,634
  • 1
  • 21
  • 32
  • Thank you for your answer, I updated my question accordingly. I did not post this complement as an answer though, because it did not solve the many questions I had in my first post (using a chainsaw does not fix one's axe). – mranvick Apr 13 '21 at 07:15
  • I am not sure about your analogy; are the three services or the websocket the chainsaw? – emher Apr 13 '21 at 07:33
  • Yes, sort of, or even the solution I finally adopted. My point was: I know that the Redis / Websocket or the multi-threaded solutions are intrinsically better than trying to "force" Dash to do it anyway (which is quite a poor design), but it does not answer my initial interrogations. Sorry about the confusion. – mranvick Apr 13 '21 at 09:08
  • 1
    Ah, I understand. Running a few services doesn't have to be that complicated though. [Here](https://github.com/thedirtyfew/dash-redis-mwe) is a small example, which in my opinion is simpler/easier to understand than e.g. the audio file hack :) – emher Apr 13 '21 at 09:16