Skip to content

batch/debounce websocket set_props#3783

Open
T4rk1n wants to merge 4 commits into
devfrom
batch-ws-set-props
Open

batch/debounce websocket set_props#3783
T4rk1n wants to merge 4 commits into
devfrom
batch-ws-set-props

Conversation

@T4rk1n
Copy link
Copy Markdown
Contributor

@T4rk1n T4rk1n commented May 13, 2026

When you use set_props in a loop inside a websocket callback, it would create single updates in the renderer and can result in lag.
This PR add a batching and debounce mechanism to set_props, default to 5ms/200hz configurable with

Dash(..., websocket_batch_delay=0.005)  # 5ms (default) - set to 0 to disable batching

Copy link
Copy Markdown
Contributor

@camdecoster camdecoster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a changelog entry? I'd also like to see a test related to the new batching mechanism.

* Groups set_props by renderer and forwards as a single batch message.
* @param messages Array of messages
*/
private handleBatchedMessages(messages: unknown[]): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is messages of type unknown[]? Why not WorkerMessages[]?

if ((msg as any).type === 'heartbeat_ack') {
continue;
}
if (msg.type === WorkerMessageType.SET_PROPS) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use WorkerMessage[] instead of unknown[], you can get rid of the casts here.

if (!setPropsPayloadsByRenderer.has(rendererId)) {
setPropsPayloadsByRenderer.set(rendererId, []);
}
setPropsPayloadsByRenderer.get(rendererId)!.push(setPropsMsg.payload);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non null assertions are a code smell. Could this be rewritten to avoid it?

// Forward batched set_props to each renderer
for (const [rendererId, payloads] of setPropsPayloadsByRenderer) {
const port = this.renderers.get(rendererId);
if (port) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should a message be logged if port is falsy?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the message order matter? If you batch everything, the order will change. [set_prop_1, otherMessage, set_prop_2] gets sent out in the order of [set_prop_1, set_prop_2], [otherMessage].

Comment thread dash/backends/ws.py Outdated
Args:
send_text: Async function to send text data over WebSocket
outbound_queue: janus.Queue instance for receiving messages (strings)
batch_delay: Time in seconds to wait for additional messages (default: 5ms)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OP says use 0 if you want to disable batching. Maybe that should be included here? Would None be a better option? Would it be better to call say this disables debouncing rather than disabling batching?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 matches the same pattern as the inactivity config. Debouncing is the just the mechanism by which the batching happens, so batching is more relevant than debounce imo.

Comment thread dash/backends/ws.py
# Wait indefinitely for first message, then use timeout for batching
timeout = batch_delay if messages else None
try:
msg = await asyncio.wait_for(q.get(), timeout=timeout)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be more appropriate to get a message and send it immediately with send_text if batch_delay is 0?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it saves one iteration 👍

Comment on lines +134 to +136
workerClient.onSetProps = (payload: SetPropsPayload) => {
processSetProps(payload);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this also work?

Suggested change
workerClient.onSetProps = (payload: SetPropsPayload) => {
processSetProps(payload);
};
workerClient.onSetProps = processSetProps;

Comment thread dash/backends/ws.py Outdated
batch_delay: Time in seconds to wait for additional messages (default: 5ms)
"""
q = outbound_queue.async_q
messages: list = []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could tighten this type if you want:

Suggested change
messages: list = []
messages: list[str] = []

Comment thread dash/dash.py
websocket_callbacks: Optional[bool] = False,
websocket_allowed_origins: Optional[List[str]] = None,
websocket_inactivity_timeout: Optional[int] = 300000,
websocket_batch_delay: Optional[float] = 0.005,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if a user explicitly passes in None for this value?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't pass typing? Optional[float] requires a float and has float as default.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants