当浏览器选项卡位于绘图破折号应用程序的后台时,保持dcc.interval启动

2024-04-26 03:21:58 发布

您现在位置:Python中文网/ 问答频道 /正文

发行

我正在使用Dash制作一个web应用的原型,Dash通过串行外围设备定期执行一些测量。至于现在,我正在使用dcc.Interval组件定期从我的传感器中获取测量值,然后绘制并存储它们

但是,无论是使用Firefox还是Chrome,当选项卡不在前台时,web浏览器的性能调节机制都会大大降低dcc.Interval组件的触发频率。在某些情况下,后台计时器甚至完全停止

该问题记录在案herethere

我能够创建以下最低限度的工作示例,它仅在控制台输出中起作用:

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)

当选项卡处于活动状态时,这种方法可以很好地工作,每秒一个接一个地计数(两次触发dcc.Interval组件之间的默认延迟),但当web浏览器切换到其他选项卡时,这种方法不再起作用。特别是,一段时间后,两次点火之间的间隔增加

对于这个非常简单的示例,根据您的浏览器和机器,情况并不总是如此。但对于一个更复杂的应用程序,在后台进行两次回调之间的时间可以达到几秒,甚至几十秒

问题:

你知道一种解决方法,还是迫使浏览器把应用程序看作“<>强><强> >的方式,因此不节制它?主要目标是,在任何时候,应用程序中嵌入的dcc.Interval组件都会以相同的速度继续启动

变通办法

到目前为止,我测试了三种解决方案,但收效甚微:

  • 使用dash_devices
  • 播放音频文件以防止选项卡转到后台
  • dcc.Interval放在层次结构的更高位置

使用dash_devices

我偶然发现了this thread^{}似乎是个好主意,因为它使用WebSocket而不是HTTP请求进行更新。我设法使它在一个基本的例子上工作,但它需要在更大的范围内进行测试

播放音频文件

播放音频(如果您不想听,可以低音量播放或禁用制表符声音播放)是一个很好的解决方案to keep the tab in the foreground from the browser point of view

但是,如果我能够使用以下代码段:

html.Audio(autoPlay=True, src='http://www.hochmuth.com/mp3/Haydn_Cello_Concerto_D-1.mp3', loop=True)

使用路径而不是URL的相同代码,即src='/path/to/my_audio_file.mp3'src='file:///path/to/my_audio_file.mp3'似乎不起作用(没有播放音频),我不知道为什么

根据我读到的herethere,我还尝试了一些base64元素,但只有第一个元素起作用:

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)

在显示的四个音频播放器中,只有第一个能够播放某些内容(另一个呈灰色,好像没有指定音频文件)

注意事项:

  • 对于涉及base64的解决方案,我只是根据我所看到的进行了调整,我可能在某个时候犯了一个错误
  • 我知道这是一个肮脏的解决方案,但至少它部分地起作用

dcc.Interval放在层次结构的更高位置

here所述,在某些情况下,将dcc.Interval组件定位在层次结构的更高位置似乎是可行的。然而,我尝试了它却没有成功

结论与展望

因此,我非常感谢任何能在这方面帮助我的人。通过以下方式之一:

  • 找到一种为背景选项卡激发dcc.Interval元素的方法
  • 找到使用html.Audio组件播放本地存储的音频文件的方法
  • 找到另一种方法,一方面使用Dash应用程序,另一方面使用一些定期触发的事件,同时将两者链接起来

最初于2021年4月9日询问



更新日期为2021年4月13日

根据emher's answer,我重新定向到一个具有两个不同线程的独立体系结构:

  • 一个线程定期(使用while True / time.sleep()例程)使用纯Python编写的串行外围设备池,并存储结果它在一个全局变量中的池
  • 另一个线程运行Dash应用程序并定期(使用dcc.Interval)读取上述全局变量

下面是一个简单的工作示例:

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()

请注意第行末尾注释掉的debug=True参数:

app.run_server(dev_tools_silence_routes_logging=True)  # , debug=True)

这是因为Dash调用Flask的方式不允许在Dash应用程序从非主线程启动时启用调试模式。完整的问题记录在案here

无论Dash应用程序是在浏览器中加载还是在前台加载,此脚本每秒只计算一次。脚本仍在运行时,计数器也会运行,在选项卡中打开Dash应用程序或将其置于前台只会更新计数器的显示


免责声明:与最初提出问题时的dcc.Interval方法相反,上面的代码将逐步取消同步。实际上,使用dcc.Interval,关联的callback每1.0秒调用一次,而不管之前调用的callback是否已完成运行

因此,如果我们在t=0s运行程序,并假设选项卡位于前台,我们将看到以下调用:

t=0s : callback()
t=1s : callback()
t=2s : callback()
t=3s : callback()
[...]

相反,使用多线程方法,如果要运行的代码(上面:counter += 1 ; print(counter))需要执行时间dt,我们将看到以下调用:

t=0s        : callback()
t=1s + dt   : callback()
t=2s + 2*dt : callback()
t=3s + 3*dt : callback()
[...]

也就是说,执行链正在逐步从预期的“每秒一次回调”行为中取消同步。在某些情况下,这可能很棘手,在这种情况下,请参阅here了解解决方法


Tags: 方法nameimporttrueapp应用程序myhtml
1条回答
网友
1楼 · 发布于 2024-04-26 03:21:58

对于这种用例,我通常更喜欢一种架构,其中一个单独的进程收集数据并插入缓存(例如Redis),UI(在您的例子中是Dash)从中读取数据。此解决方案比直接从UI获取数据要健壮得多

如果您坚持直接从UI获取数据,我建议使用^{}组件而不是Interval组件。虽然我还没有进行广泛的测试,但我的印象是连接将在most platforms上保持活动状态

相关问题 更多 >