Racing LocalTrackPublished events start two denoise processors (two AudioContexts, two wasm inits) #10

Closed
opened 2026-09-12 02:13:01 -04:00 by jared · 1 comment
Owner

Severity: medium · Type: performance · Confidence: high

Location: src/lotus/lotusDenoise.ts:100-107, 129-141

Problem

apply() guards with if (mic && !mic.getProcessor()), but getProcessor() only becomes non-undefined after setProcessor()'s promise resolves — which is after the whole wasm/model load. apply runs on the subscription emit and on every LocalTrackPublished, and LiveKit fires that once per local track, so a join with camera on produces mic-published → apply #1 → camera-published → apply #2, both passing the guard. Two LotusDenoiseProcessor instances are constructed, each creating its own AudioContext and loading the model; LiveKit serializes them on trackChangeLock and then destroys the first. Result: doubled init cost, doubled lock hold time (see the mute-freeze finding), and a transient extra AudioContext against the browser's hard per-document cap.

How to trigger

Join a call with lotusDenoiseSource=1 and the camera enabled; two [lotus] denoise processor active lines are logged.

Suggested fix

Track an in-flight Promise/boolean per room in the closure and skip apply() while one is pending, clearing it in finally.


Filed from the September 2026 audit (branch lotus).

**Severity:** medium · **Type:** performance · **Confidence:** high **Location:** `src/lotus/lotusDenoise.ts:100-107`, `129-141` ### Problem `apply()` guards with `if (mic && !mic.getProcessor())`, but `getProcessor()` only becomes non-undefined after `setProcessor()`'s promise resolves — which is after the whole wasm/model load. `apply` runs on the subscription emit *and* on every `LocalTrackPublished`, and LiveKit fires that once per local track, so a join with camera on produces mic-published → `apply` #1 → camera-published → `apply` #2, both passing the guard. Two `LotusDenoiseProcessor` instances are constructed, each creating its own `AudioContext` and loading the model; LiveKit serializes them on `trackChangeLock` and then destroys the first. Result: doubled init cost, doubled lock hold time (see the mute-freeze finding), and a transient extra `AudioContext` against the browser's hard per-document cap. ### How to trigger Join a call with `lotusDenoiseSource=1` and the camera enabled; two `[lotus] denoise processor active` lines are logged. ### Suggested fix Track an in-flight `Promise`/boolean per room in the closure and skip `apply()` while one is pending, clearing it in `finally`. --- _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:01 -04:00
jared added the bugpriority: mediumperformancearea: denoise labels 2026-09-12 02:13:01 -04:00
jared self-assigned this 2026-09-12 02:13:01 -04:00
jared closed this issue 2026-09-12 11:56:45 -04:00
Author
Owner

Fixed in 59e0c852, shipped in 0.25.0-lotus.1 (published by CI from tag v0.25.0-lotus.1) and consumed by cinny at 9a85a487.

Fixed in 59e0c852, shipped in `0.25.0-lotus.1` (published by CI from tag v0.25.0-lotus.1) and consumed by cinny at 9a85a487.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
2026-11-14
Dependencies

No dependencies set.

Reference: LotusGuild/element-call#10