Updating DataTable data using WebSocket

Hi All,

I’m a n00b with Dash and I’m trying to update a DashTable from websocket feeds. The code appears to work when there aren’t too many feeds, but once there are, Chrome starts spamming my server with fetch requests (from dash_update_component)

Is there any way to make this more performant ?

Thanks in advance

import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
import json
import pandas as pd

from dash import callback, Dash, dash_table
from dash.dependencies import Input, Output, State
from dash_extensions import WebSocket

symbols = ["BTCUSDT"]
columns = ["symbol", "bid_volume", "bid_price", "ask_volume", "ask_price"]


def create_data():
    data = {}
    for col in columns:
        if col == "symbol":
            data[col] = symbols
        else:
            data[col] = [None] * len(symbols)
    return data


df = pd.DataFrame(data=create_data())

# Create example app.
app = Dash(prevent_initial_callbacks=True)
app.layout = html.Div([
    dash_table.DataTable(df.to_dict('records'), [{"name": i, "id": i} for i in df.columns], id='tbl', editable=True),
    dcc.Input(id="input", autoComplete="off"), html.Div(id="message"),
    WebSocket(url="wss://fstream.binance.com/ws/", id="ws")
])

# Write to websocket.
@app.callback(Output("ws", "send"), [Input("input", "value")])
def send(value):
    sub_msg = {
        "method": "SUBSCRIBE",
        "params": [],
        "id": 1
    }
    for ins in symbols:
        sub_msg["params"] += ([f"{ins.lower()}@bookTicker"])
    return json.dumps(sub_msg, indent=0)


# Read from websocket.
@app.callback(Output('tbl', 'data'), [Input("ws", "message")])
def on_feed(message):
    if "data" not in message:
        return dash.no_update
    else:
        data = json.loads(message["data"])
        print(data)
        symbol = data["s"]
        row_idx = df.index[df['symbol'] == symbol].tolist()[0]
        df.loc[row_idx, columns] = [symbol, data["B"], data["b"], data["a"], data["A"]]
        return df.to_dict('records')


if __name__ == '__main__':
    app.run_server(debug=True)

As I read your log, you are invoking a lot (!) of callbacks (each callbacks sends a request to the server, hence the “spamming”). If you want to decrease the load on the browser, there are generally (at least) two options,

  • Improve the performance of each callback. That could be e.g. by moving it client side
  • Reduce the number of callbacks. That could be e.g. by slowing down the response rate from the websocket
2 Likes

Thanks for the reply. Yep that’s what I ended up doing. I ended up implementing throttling via Intervals and then moved the processing of data client side via client side callbacks.
I also noted that the latency of websockets in dash-extensions quite high so I ended up using just normal python websockets in a separate thread. This seems to have helped significantly

That sounds like a good solution. Generally, I wouldn’t recommend writing to websockets the way you are doing it in the original example. Rather, a “real” websocket endpoint should be used to emit the data whenever possible (to ensure proper performance, as you already noted). Hence either via a separate uwsgi sever (that seems to be what you are doing now?), or using single uwsgi server via async dash,

import asyncio
import random

from async_dash import Dash
from dash import html, Output, Input, dcc
from dash_extensions import WebSocket
from quart import websocket, json

app = Dash(__name__)

app.layout = html.Div([WebSocket(id="ws"), dcc.Graph(id="graph")])

app.clientside_callback(
    """
function(msg) {
    if (msg) {
        const data = JSON.parse(msg.data);
        return {data: [{y: data, type: "scatter"}]};
    } else {
        return {};
    }
}""",
    Output("graph", "figure"),
    [Input("ws", "message")],
)


@app.server.websocket("/ws")
async def ws():
    while True:
        output = json.dumps([random.randint(200, 1000) for _ in range(6)])
        await websocket.send(output)
        await asyncio.sleep(1)

if __name__ == "__main__":
    app.run_server()

The async dash project is realatively new, but I really like the idea :slight_smile:

1 Like

That’s awesome ! I was wondering if dash supported async. This looks like the solution I was looking for.
One other idea I had was to use python bindings and mmap. So the idea is to have a C++ backend (on the same box) that would deal with all the web socket processing and all the dash back end would do is read the mmap for the latest update of the data. This way, we don’t even need to deal with the python websockets

I haven’t played around with this yet, but I assume it would be much faster.