mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-01 19:50:41 +02:00
perf: stop scanning every socket.io payload for binary data (#28180)
* perf: stop scanning every socket.io payload for binary data Every socket.io event the backend sends was first walked recursively to check whether any value was a bytes object needing binary attachment framing. Open WebUI never emits binary, so the walk always came back empty and the work was thrown away. It has no early exit and allocates at every level, so it scaled with the full size of the message, and the messages are the big ones: chat streaming re-emits the whole assistant message on every update, note collaboration sends document state as a JSON array with one entry per byte. With the Redis manager it ran once per instance per emit on top of that, since every instance builds its own copy of the packet. The server now installs a Packet subclass with binary events off, through python-socketio's own serializer hook, the same mechanism its msgpack serializer uses. Inbound binary attachments are decoded to int lists rather than refused, so the one frontend path that sends a raw Uint8Array keeps working and handlers can still echo client data straight back out. One scan remains in multi-instance setups: python-socketio's Redis manager calls it on the base Packet class directly, where the serializer hook cannot reach. Measured per encode: | payload | before | after | |---|---|---| | chat completion re-emit (7.5 KB JSON) | 30 us | 13 us | | collaborative document state (292 KB JSON) | 9.0 ms | 1.7 ms | With ENABLE_ORJSON=true, where the scan is nearly the whole encode cost: 20 us to 2.3 us, and 7.8 ms to 0.14 ms. Closes #28164 * fix: match the other Yjs emits and send the full state as an array Collaboration.ts sent the initial full-document state as a raw Uint8Array while the other two Yjs emit sites convert with Array.from first. socket.io framed that one as a binary attachment, so with the JSON-only packet class the server turns it into a list of ints and re-broadcasts it as JSON: a 10240-byte state update becomes 36561 JSON characters. Converting at the emit site keeps the wire form uniform across all three sites. Also trims the JSONOnlyPacket docstring, which claimed attachments already arrive as int lists when the override is what converts them, and annotates the new reconstruct_binary parameters.
This commit is contained in:
@@ -5,6 +5,7 @@ import logging
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import pycrdt as Y
|
||||
import socketio
|
||||
@@ -47,6 +48,7 @@ from open_webui.utils.redis import (
|
||||
get_redis_connection,
|
||||
get_sentinels_from_env,
|
||||
)
|
||||
from socketio.packet import Packet
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -65,6 +67,17 @@ def get_room_sid_map(manager, namespace: str, room: str):
|
||||
return manager.rooms.get(namespace, {}).get(room)
|
||||
|
||||
|
||||
class JSONOnlyPacket(Packet):
|
||||
"""Packet class for JSON-serializable payloads only, skipping python-socketio's per-emit binary scan."""
|
||||
|
||||
uses_binary_events = False
|
||||
|
||||
@classmethod
|
||||
def reconstruct_binary(cls, data: Any, attachments: list[bytes]):
|
||||
"""Normalize client attachments to int lists, the form the Yjs handlers store and apply."""
|
||||
return super().reconstruct_binary(data, [list(attachment) for attachment in attachments])
|
||||
|
||||
|
||||
if WEBSOCKET_MANAGER == 'redis':
|
||||
sentinel_hosts = WEBSOCKET_SENTINEL_HOSTS or ''
|
||||
ws_redis_url = (
|
||||
@@ -77,6 +90,7 @@ if WEBSOCKET_MANAGER == 'redis':
|
||||
cors_allowed_origins=SOCKETIO_CORS_ORIGINS,
|
||||
async_mode='asgi',
|
||||
json=SOCKETIO_JSON,
|
||||
serializer=JSONOnlyPacket,
|
||||
transports=(['websocket'] if ENABLE_WEBSOCKET_SUPPORT else ['polling']),
|
||||
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
|
||||
always_connect=True,
|
||||
@@ -91,6 +105,7 @@ else:
|
||||
cors_allowed_origins=SOCKETIO_CORS_ORIGINS,
|
||||
async_mode='asgi',
|
||||
json=SOCKETIO_JSON,
|
||||
serializer=JSONOnlyPacket,
|
||||
transports=(['websocket'] if ENABLE_WEBSOCKET_SUPPORT else ['polling']),
|
||||
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
|
||||
always_connect=True,
|
||||
|
||||
@@ -163,7 +163,7 @@ export class SocketIOCollaborationProvider {
|
||||
document_id: this.documentId,
|
||||
user_id: this.user?.id,
|
||||
socket_id: this.socket.id,
|
||||
update: Y.encodeStateAsUpdate(this.doc)
|
||||
update: Array.from(Y.encodeStateAsUpdate(this.doc))
|
||||
});
|
||||
} else {
|
||||
console.warn('Yjs document is empty, not sending state.');
|
||||
|
||||
Reference in New Issue
Block a user