perf(group): reduce websocket fanout serialization

This commit is contained in:
richjohnrambo
2026-10-04 10:15:18 +08:00
parent c46209f4e7
commit b05e8f60dd
10 changed files with 458 additions and 2 deletions
+94 -1
View File
@@ -650,12 +650,20 @@ func (t *Topic) runLocal(hub *Hub) {
// handleClientMsg is the top-level handler of messages received by the topic from sessions.
func (t *Topic) handleClientMsg(msg *ClientComMessage) {
started := time.Now()
kind := "other"
if t.cat == types.TopicCatP2P {
kind = "p2p"
} else if t.cat == types.TopicCatGrp {
kind = "group"
}
operation := "other"
if msg.Pub != nil {
operation = "publish"
} else if msg.Note != nil {
operation = "note"
}
defer immetrics.ObserveTopicProcess(kind, t.metricRole(), operation, started)
immetrics.ObserveTopicQueueWait(kind, msg.queuedAt)
msg.queuedAt = time.Time{}
if msg.TenantID.IsZero() {
@@ -1958,9 +1966,18 @@ func (t *Topic) handlePresence(msg *ServerComMessage) {
// broadcastToSessions writes message to attached sessions.
func (t *Topic) broadcastToSessions(msg *ServerComMessage) {
started := time.Now()
kind := t.metricKind()
role := t.metricRole()
websocketTargets, multiplexTargets, otherTargets := 0, 0, 0
defer func() {
immetrics.ObserveTopicBroadcast(kind, role, started, websocketTargets, multiplexTargets, otherTargets)
}()
msg.TenantID = t.tenantID
// List of sessions to be dropped.
var dropSessions []*Session
var sharedPayload []byte
sharedPayloadReady := false
// Broadcast the message. Only {data}, {pres}, {info} are broadcastable.
// {meta} and {ctrl} are sent to the session only
for sess, pssd := range t.sessions {
@@ -2027,7 +2044,41 @@ func (t *Topic) broadcastToSessions(msg *ServerComMessage) {
}
}
// Make a copy of msg since messages sent to sessions differ.
if sess.proto == WEBSOCK {
websocketTargets++
} else if sess.isMultiplex() {
multiplexTargets++
} else {
otherTargets++
}
// Ordinary group messages are identical for every local WebSocket subscriber.
// Serialize once and reuse immutable bytes to avoid O(recipients) JSON work.
if t.canShareSerializedBroadcast(msg, sess, pssd) {
if !sharedPayloadReady {
var err error
sharedPayload, err = json.Marshal(msg)
if err != nil {
immetrics.IncTopicSharedSerialization(kind, "error")
logs.Warn.Printf("topic[%s]: shared broadcast serialization failed: %v", t.name, err)
sharedPayload = nil
} else {
immetrics.IncTopicSharedSerialization(kind, "serialized")
}
sharedPayloadReady = true
}
if sharedPayload != nil {
if sess.queueOutSharedBytes(sharedPayload) {
continue
}
immetrics.IncTopicSharedSerialization(kind, "full")
logs.Warn.Printf("topic[%s]: connection stuck, detaching - %s", t.name, sess.sid)
dropSessions = append(dropSessions, sess)
continue
}
}
// Make a copy of msg since messages sent to sessions may differ.
msgCopy := msg.copy()
// Topic name may be different depending on the user to which the `sess` belongs.
t.prepareBroadcastableMessage(msgCopy, pssd.uid, pssd.isChanSub)
@@ -2046,6 +2097,48 @@ func (t *Topic) broadcastToSessions(msg *ServerComMessage) {
}
}
func (t *Topic) metricKind() string {
switch t.cat {
case types.TopicCatGrp:
return "group"
case types.TopicCatP2P:
return "p2p"
default:
return "other"
}
}
func (t *Topic) metricRole() string {
if t.isProxy {
return "proxy"
}
return "master"
}
func serverMessageType(msg *ServerComMessage) string {
switch {
case msg == nil:
return "unknown"
case msg.Data != nil:
return "data"
case msg.Ctrl != nil:
return "ctrl"
case msg.Pres != nil:
return "pres"
case msg.Info != nil:
return "info"
case msg.Meta != nil:
return "meta"
default:
return "unknown"
}
}
func (t *Topic) canShareSerializedBroadcast(msg *ServerComMessage, sess *Session, pssd perSessionData) bool {
return t.cat == types.TopicCatGrp && !t.isChan && sess.proto == WEBSOCK && !pssd.isChanSub &&
(msg.Data != nil || msg.Pres != nil || msg.Info != nil)
}
// subscriptionReply generates a response to a subscription request
func (t *Topic) subscriptionReply(asChan bool, msg *ClientComMessage) error {
// The topic is already initialized by the Hub