call_state can emit up to 8 messages/second; dedupe runs after the throttle #20

Closed
opened 2026-09-12 02:13:07 -04:00 by jared · 0 comments
Owner

Severity: medium · Type: performance · Confidence: high

Location: src/lotus/lotusCallState.ts:38-70, src/state/media/UserMediaViewModel.ts:112-123

Problem

speaking$ here is raw LiveKit IsSpeakingChanged (not the 1 s/60 s debounced observeSpeaker$ used for the auto-spotlight), so it flips constantly in an active conversation. throttleTime(250, undefined, {leading:true, trailing:true}) therefore allows both a leading and a trailing emission per window — up to ~8 postMessage round trips per second, each one a full re-serialisation of every participant, and each one waking every host subscriber (CallEmbed.lotusCallStateListeners, i.e. useCallSpeakers + useAfkAutoMute). distinctUntilChanged(JSON.stringify) is placed after the throttle, so the (cheap) dedupe never gets the chance to prevent the throttle window from being spent, and the stringify of the whole array runs on every throttled emission (O(participants) at 8 Hz in a 30-person call).

Cross-reference: the host-side re-render cost is LotusGuild/cinny#32.

How to trigger

15+ person call with several people talking; instrument lotusSendToHost or the host's listenAction.

Suggested fix

move distinctUntilChanged before throttleTime (and compare field-wise rather than via JSON.stringify), and consider dropping the trailing emission or raising the interval for the speaking field specifically.


Filed from the September 2026 audit (branch lotus).

**Severity:** medium · **Type:** performance · **Confidence:** high **Location:** `src/lotus/lotusCallState.ts:38-70`, `src/state/media/UserMediaViewModel.ts:112-123` ### Problem `speaking$` here is raw LiveKit `IsSpeakingChanged` (not the 1 s/60 s debounced `observeSpeaker$` used for the auto-spotlight), so it flips constantly in an active conversation. `throttleTime(250, undefined, {leading:true, trailing:true})` therefore allows both a leading and a trailing emission per window — up to ~8 postMessage round trips per second, each one a full re-serialisation of *every* participant, and each one waking every host subscriber (`CallEmbed.lotusCallStateListeners`, i.e. `useCallSpeakers` + `useAfkAutoMute`). `distinctUntilChanged(JSON.stringify)` is placed *after* the throttle, so the (cheap) dedupe never gets the chance to prevent the throttle window from being spent, and the stringify of the whole array runs on every throttled emission (O(participants) at 8 Hz in a 30-person call). **Cross-reference:** the host-side re-render cost is LotusGuild/cinny#32. ### How to trigger 15+ person call with several people talking; instrument `lotusSendToHost` or the host's `listenAction`. ### Suggested fix move `distinctUntilChanged` *before* `throttleTime` (and compare field-wise rather than via `JSON.stringify`), and consider dropping the `trailing` emission or raising the interval for the `speaking` field specifically. --- _Filed from the September 2026 audit (branch `lotus`)._
jared added this to the EC fork audit 2026-09 · Medium & Low milestone 2026-09-12 02:13:07 -04:00
jared added the bugpriority: mediumperformancearea: widget-actions labels 2026-09-12 02:13:07 -04:00
jared self-assigned this 2026-09-12 02:13:07 -04:00
jared closed this issue 2026-09-13 01:22:55 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
2026-11-14
Dependencies

No dependencies set.

Reference: LotusGuild/element-call#20