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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Severity: medium · Type: performance · Confidence: high
Location:
src/lotus/lotusDenoise.ts:100-107,129-141Problem
apply()guards withif (mic && !mic.getProcessor()), butgetProcessor()only becomes non-undefined aftersetProcessor()'s promise resolves — which is after the whole wasm/model load.applyruns on the subscription emit and on everyLocalTrackPublished, 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. TwoLotusDenoiseProcessorinstances are constructed, each creating its ownAudioContextand loading the model; LiveKit serializes them ontrackChangeLockand then destroys the first. Result: doubled init cost, doubled lock hold time (see the mute-freeze finding), and a transient extraAudioContextagainst the browser's hard per-document cap.How to trigger
Join a call with
lotusDenoiseSource=1and the camera enabled; two[lotus] denoise processor activelines are logged.Suggested fix
Track an in-flight
Promise/boolean per room in the closure and skipapply()while one is pending, clearing it infinally.Filed from the September 2026 audit (branch
lotus).Fixed in
59e0c852, shipped in0.25.0-lotus.1(published by CI from tag v0.25.0-lotus.1) and consumed by cinny at 9a85a487.