fix(huddle): stop 20 Hz speaker-level churn from re-rendering the whole app (#5825)

## Problem

With a huddle open, Buzz Desktop becomes extremely slow and laggy
(Tyler, live report, 2026-08-14). Root-caused and runtime-convicted on
the instrumented rig in #buzz-conversational-agents:

- The Rust playout loop emits `huddle-speaker-levels` over Tauri IPC
every 50 ms, unconditionally, for the whole life of a huddle
(`playout.rs` `SPEAKER_LEVEL_TICK_MS = 50`).
- Each event deserializes to a fresh object, so `setRemoteSpeakerLevels`
updates state at 20 Hz even in silence.
- `HuddleProvider` wraps the entire main app and its context value was
an inline object literal — never memoized. Every level tick minted a new
context identity, re-rendering **every** `useHuddle()` consumer,
including `ChannelScreen` and message rows.

**Measured (A/B, silent one-participant huddle, same channel/state):**
~41 sustained ChannelScreen renders/sec unsuppressed vs ~4/sec with only
the speaker-level setState suppressed — the 20 Hz path is ~90% of the
load. Receipts: `driver-render-counter-unsuppressed.jsonl` /
`-suppressed.jsonl` on the rig, verified independently. The same
main-thread churn starves the relay client's 16 ms event-flush timer,
which is the delayed/bursty message hydration and thread-panel stalls
seen alongside the lag.

## Fix (minimal, no behavior change for meters)

1. **Split the high-frequency fields** (`micLevel`, `activeSpeakers`,
`speakerLevels`) out of `HuddleContextValue` into a new
`HuddleLevelsContext`, consumed via `useHuddleLevels()` only by the
three meter components (`HuddleBar`, `HuddleRoomHeader`,
`HuddleProfileControl`).
2. **Memoize the main context value** so provider re-renders no longer
mint a new identity for the ~everything that consumes `useHuddle()`.
3. **Extract the mic-level analyser** into `useMicLevelAnalyser` — the
level pipeline now lives in one place, and `HuddleContext.tsx` stays
under the file-size ratchet (977 lines).

Level meters keep their 20-30 Hz updates. Everything else re-renders
only when a value it actually consumes changes.

## Acceptance bar

With this fix, a silent open huddle should hold `ChannelScreen` at idle
render rates (single digits/sec), and message hydration should stay live
during huddles. The rig's render-counter + four-clock instrumentation
can verify on this branch.

## Validation

- `pnpm typecheck` clean
- `biome check` clean (repo leftovers in sidebar tests are preexisting
on main)
- full desktop suite: **4,775 passed, 0 failed** at the final tree
- file-size ratchet passes (was the reason for the analyser extraction)
- lefthook pre-commit (desktop-fix + signoff) passed on commit

Not yet done: live-local A/B rerun on this branch — the rig (Wren/Max)
has the instrumentation ready and can convict/acquit the fix with the
same probe that convicted the bug.

Base: `068a83b0` (main). Co-developed with runtime evidence from Wren
and instrumentation by Max.

Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
This commit is contained in:
Tyler
2026-08-13 22:26:59 -04:00
committed by GitHub
co-authored by Eva
parent 0f61f24ad6
commit 5743562896
7 changed files with 223 additions and 141 deletions
+100 -122
View File
@@ -11,8 +11,12 @@ import {
useHuddlePttState,
} from "./lib/useHuddlePttState";
import { useHuddleSpeakerActivity } from "./lib/useHuddleSpeakerActivity";
import { useMicLevelAnalyser } from "./lib/useMicLevelAnalyser";
import { useTtsSubscription } from "./lib/useTtsSubscription";
import type { HuddleContextValue } from "./HuddleContext.types";
import type {
HuddleContextValue,
HuddleLevelsValue,
} from "./HuddleContext.types";
/**
* Huddle lifecycle (React context):
@@ -47,29 +51,16 @@ const HUDDLE_AUDIO_COMMAND_EVENT = "huddle-audio-command";
const HUDDLE_AUDIO_STATE_EVENT = "huddle-audio-state";
const HUDDLE_AUDIO_LEVEL_EVENT = "huddle-audio-level";
const MIC_ANALYSER_UPDATE_INTERVAL_MS = 33;
const MIC_INITIAL_NOISE_FLOOR = 0.01;
const MIC_VOICE_GATE_ON_RMS = 0.018;
const MIC_VOICE_GATE_OFF_RMS = 0.012;
const MIC_VOICE_GATE_MARGIN_RMS = 0.012;
const MIC_LEVEL_ACTIVE_RANGE_RMS = 0.11;
const MIC_MIN_ACTIVE_LEVEL = 0.18;
const MIC_LEVEL_ATTACK = 0.58;
const MIC_ACTIVE_NOISE_FLOOR_RISE = 0.006;
function isRedundantHuddlePhaseError(message: string): boolean {
return /^cannot (?:start|join) huddle: already in phase /i.test(message);
}
function clamp01(value: number): number {
return Math.min(1, Math.max(0, value));
}
function interruptAgentSpeech(agentPubkey: string) {
return invoke<void>("interrupt_huddle_speech", { agentPubkey });
}
const HuddleContext = React.createContext<HuddleContextValue | null>(null);
const HuddleLevelsContext = React.createContext<HuddleLevelsValue | null>(null);
export function HuddleProvider({
children,
@@ -110,7 +101,6 @@ export function HuddleProvider({
const [mirroredAudioState, setMirroredAudioState] =
React.useState<HuddleAudioMirrorState | null>(null);
const [mirroredMicLevel, setMirroredMicLevel] = React.useState(0);
const [micLevel, setMicLevel] = React.useState(0);
const {
getVoiceInputMode,
pttActive,
@@ -790,77 +780,7 @@ export function HuddleProvider({
usePipelineHotstart(ephemeralChannelId);
// Mic level analyser — drives the voice activity indicator
React.useEffect(() => {
if (!localAudioTrack || !micConnected) {
setMicLevel(0);
return;
}
const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
analyser.fftSize = 512;
const source = ctx.createMediaStreamSource(
new MediaStream([localAudioTrack]),
);
source.connect(analyser);
const buf = new Float32Array(analyser.fftSize);
let raf = 0;
let lastUpdate = 0;
let voiceActive = false;
let noiseFloor = MIC_INITIAL_NOISE_FLOOR;
let smoothedLevel = 0;
function tick(now: number) {
raf = requestAnimationFrame(tick);
if (now - lastUpdate < MIC_ANALYSER_UPDATE_INTERVAL_MS) return;
lastUpdate = now;
analyser.getFloatTimeDomainData(buf);
let sumSquares = 0;
for (let i = 0; i < buf.length; i += 1) {
sumSquares += buf[i] * buf[i];
}
const rms = Math.sqrt(sumSquares / buf.length);
const activeThreshold = Math.max(
MIC_VOICE_GATE_ON_RMS,
noiseFloor + MIC_VOICE_GATE_MARGIN_RMS,
);
const idleThreshold = Math.max(
MIC_VOICE_GATE_OFF_RMS,
noiseFloor + MIC_VOICE_GATE_MARGIN_RMS * 0.55,
);
voiceActive = voiceActive ? rms > idleThreshold : rms > activeThreshold;
const floorRate =
rms < noiseFloor
? 0.18
: voiceActive
? MIC_ACTIVE_NOISE_FLOOR_RISE
: 0.025;
noiseFloor += (rms - noiseFloor) * floorRate;
if (!voiceActive) {
smoothedLevel = 0;
setMicLevel(0);
return;
}
const normalized = clamp01(
(rms - noiseFloor) / MIC_LEVEL_ACTIVE_RANGE_RMS,
);
const targetLevel = Math.max(normalized, MIC_MIN_ACTIVE_LEVEL);
smoothedLevel += (targetLevel - smoothedLevel) * MIC_LEVEL_ATTACK;
setMicLevel(smoothedLevel);
}
raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
source.disconnect();
void ctx.close();
};
}, [localAudioTrack, micConnected]);
const micLevel = useMicLevelAnalyser(localAudioTrack, micConnected);
React.useEffect(() => {
if (ownsAudioSession) {
@@ -950,42 +870,87 @@ export function HuddleProvider({
};
}, [ownsAudioSession]);
// High-frequency (20-30 Hz) audio levels live in their own context so their
// churn re-renders only the meter components, not every useHuddle consumer.
const levelsValue = React.useMemo<HuddleLevelsValue>(
() => ({
micLevel: ownsAudioSession ? micLevel : mirroredMicLevel,
activeSpeakers,
speakerLevels,
}),
[
activeSpeakers,
micLevel,
mirroredMicLevel,
ownsAudioSession,
speakerLevels,
],
);
const effectiveMicConnected = ownsAudioSession
? micConnected
: (mirroredAudioState?.micConnected ?? false);
const contextValue = React.useMemo<HuddleContextValue>(
() => ({
localAudioTrack,
isStarting,
huddleError,
clearHuddleError,
micConnected: effectiveMicConnected,
isMuted: effectiveIsMuted,
toggleMute,
interruptAgentSpeech,
pttActive,
voiceInputMode: effectiveVoiceInputMode,
setVoiceInputMode,
audioDevices,
selectedDeviceId,
setSelectedDeviceId,
micGain,
setMicGain,
outputDevices,
selectedOutputDevice,
setSelectedOutputDevice,
activeEphemeralChannelId: ephemeralChannelId,
showHuddleInMainApp,
viewHuddleChannel,
startHuddle,
joinHuddle,
leaveHuddle,
}),
[
audioDevices,
clearHuddleError,
effectiveIsMuted,
effectiveMicConnected,
effectiveVoiceInputMode,
ephemeralChannelId,
huddleError,
isStarting,
joinHuddle,
leaveHuddle,
localAudioTrack,
micGain,
outputDevices,
pttActive,
selectedDeviceId,
selectedOutputDevice,
setMicGain,
setSelectedDeviceId,
setSelectedOutputDevice,
setVoiceInputMode,
showHuddleInMainApp,
startHuddle,
toggleMute,
viewHuddleChannel,
],
);
return (
<HuddleContext.Provider
value={{
localAudioTrack,
isStarting,
huddleError,
clearHuddleError,
micConnected: ownsAudioSession
? micConnected
: (mirroredAudioState?.micConnected ?? false),
isMuted: effectiveIsMuted,
toggleMute,
interruptAgentSpeech,
micLevel: ownsAudioSession ? micLevel : mirroredMicLevel,
pttActive,
voiceInputMode: effectiveVoiceInputMode,
setVoiceInputMode,
activeSpeakers,
speakerLevels,
audioDevices,
selectedDeviceId,
setSelectedDeviceId,
micGain,
setMicGain,
outputDevices,
selectedOutputDevice,
setSelectedOutputDevice,
activeEphemeralChannelId: ephemeralChannelId,
showHuddleInMainApp,
viewHuddleChannel,
startHuddle,
joinHuddle,
leaveHuddle,
}}
>
{children}
<HuddleContext.Provider value={contextValue}>
<HuddleLevelsContext.Provider value={levelsValue}>
{children}
</HuddleLevelsContext.Provider>
</HuddleContext.Provider>
);
}
@@ -997,3 +962,16 @@ export function useHuddle(): HuddleContextValue {
}
return ctx;
}
/**
* High-frequency (20-30 Hz) mic/speaker levels. Consume only from components
* that render audio meters; everything else should use {@link useHuddle} so it
* is insulated from level churn.
*/
export function useHuddleLevels(): HuddleLevelsValue {
const ctx = React.useContext(HuddleLevelsContext);
if (!ctx) {
throw new Error("useHuddleLevels must be used within a HuddleProvider");
}
return ctx;
}
@@ -1,6 +1,17 @@
import type { AudioInputDevice } from "./lib/useAudioDevices";
import type { VoiceInputMode } from "./lib/useHuddlePttState";
/**
* High-frequency audio-level fields, split from {@link HuddleContextValue} so
* their 20-30 Hz updates only re-render the meter components that consume
* them — not every `useHuddle()` consumer across the app.
*/
export interface HuddleLevelsValue {
micLevel: number;
activeSpeakers: string[];
speakerLevels: Record<string, number>;
}
export interface HuddleContextValue {
localAudioTrack: MediaStreamTrack | null;
isStarting: boolean;
@@ -11,12 +22,9 @@ export interface HuddleContextValue {
toggleMute: () => void;
/** Interrupt this agent only if it still owns the active utterance. */
interruptAgentSpeech: (agentPubkey: string) => Promise<void>;
micLevel: number;
pttActive: boolean;
voiceInputMode: VoiceInputMode;
setVoiceInputMode: (mode: VoiceInputMode) => Promise<void>;
activeSpeakers: string[];
speakerLevels: Record<string, number>;
audioDevices: AudioInputDevice[];
selectedDeviceId: string;
setSelectedDeviceId: (id: string) => void;
@@ -27,7 +27,7 @@ import { Button } from "@/shared/ui/button";
import { useEmojiBurst } from "@/shared/ui/EmojiBurstProvider";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { useHuddle } from "../HuddleContext";
import { useHuddle, useHuddleLevels } from "../HuddleContext";
import { AddAgentDialog, type AgentAddResult } from "./AddAgentDialog";
import type { HuddleAgentVoiceSettings } from "./AgentVoiceMenu";
import { MicControls, SpeakerControls } from "./MicControls";
@@ -157,11 +157,8 @@ export function HuddleBar({
micConnected,
isMuted,
toggleMute,
micLevel,
voiceInputMode,
setVoiceInputMode,
activeSpeakers,
speakerLevels,
huddleError,
clearHuddleError,
audioDevices,
@@ -173,6 +170,7 @@ export function HuddleBar({
selectedOutputDevice,
setSelectedOutputDevice,
} = useHuddle();
const { activeSpeakers, micLevel, speakerLevels } = useHuddleLevels();
const customEmoji = useCustomEmoji();
const identityQuery = useIdentityQuery();
const profileQuery = useProfileQuery();
@@ -5,7 +5,7 @@ import * as React from "react";
import type { Channel } from "@/shared/api/types";
import { Button } from "@/shared/ui/button";
import { useHuddle } from "../HuddleContext";
import { useHuddle, useHuddleLevels } from "../HuddleContext";
import { MicControls } from "./MicControls";
type HuddleProfileState = {
@@ -42,7 +42,6 @@ export function HuddleProfileControl({
leaveHuddle,
micConnected,
micGain,
micLevel,
selectedDeviceId,
setMicGain,
setSelectedDeviceId,
@@ -50,6 +49,7 @@ export function HuddleProfileControl({
toggleMute,
voiceInputMode,
} = useHuddle();
const { micLevel } = useHuddleLevels();
const [isLeaving, setIsLeaving] = React.useState(false);
const [state, setState] = React.useState<HuddleProfileState | null>(null);
const lastHuddleChannelIdRef = React.useRef<string | null>(null);
@@ -4,7 +4,7 @@ import * as React from "react";
import { useProfileQuery, useSelfProfileCache } from "@/features/profile/hooks";
import { useIdentityQuery } from "@/shared/api/hooks";
import { useHuddle } from "../HuddleContext";
import { useHuddle, useHuddleLevels } from "../HuddleContext";
import type { HuddleAgentVoiceSettings } from "./AgentVoiceMenu";
import { HuddleParticipantsControl } from "./ParticipantList";
@@ -28,14 +28,8 @@ function isVisible(state: HuddleRosterState | null) {
/** Larger, persistent roster for the companion huddle room window. */
export function HuddleRoomHeader() {
const {
activeSpeakers,
interruptAgentSpeech,
isMuted,
micConnected,
micLevel,
speakerLevels,
} = useHuddle();
const { interruptAgentSpeech, isMuted, micConnected } = useHuddle();
const { activeSpeakers, micLevel, speakerLevels } = useHuddleLevels();
const identityQuery = useIdentityQuery();
const profileQuery = useProfileQuery();
const selfProfileCache = useSelfProfileCache();
+5 -1
View File
@@ -1,4 +1,8 @@
export { HuddleProvider, useHuddle } from "./HuddleContext";
export {
HuddleProvider,
useHuddle,
useHuddleLevels,
} from "./HuddleContext";
export { setupAudioWorklet } from "./lib/audioWorklet";
export { HuddleBar } from "./components/HuddleBar";
export { HuddleProfileControl } from "./components/HuddleProfileControl";
@@ -0,0 +1,100 @@
import * as React from "react";
const MIC_ANALYSER_UPDATE_INTERVAL_MS = 33;
const MIC_INITIAL_NOISE_FLOOR = 0.01;
const MIC_VOICE_GATE_ON_RMS = 0.018;
const MIC_VOICE_GATE_OFF_RMS = 0.012;
const MIC_VOICE_GATE_MARGIN_RMS = 0.012;
const MIC_LEVEL_ACTIVE_RANGE_RMS = 0.11;
const MIC_MIN_ACTIVE_LEVEL = 0.18;
const MIC_LEVEL_ATTACK = 0.58;
const MIC_ACTIVE_NOISE_FLOOR_RISE = 0.006;
function clamp01(value: number): number {
return Math.min(1, Math.max(0, value));
}
/**
* Mic level analyser — drives the voice activity indicator. Emits a smoothed
* 0..1 level at up to ~30 Hz while the local track is live; 0 when idle.
*/
export function useMicLevelAnalyser(
localAudioTrack: MediaStreamTrack | null,
micConnected: boolean,
): number {
const [micLevel, setMicLevel] = React.useState(0);
React.useEffect(() => {
if (!localAudioTrack || !micConnected) {
setMicLevel(0);
return;
}
const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
analyser.fftSize = 512;
const source = ctx.createMediaStreamSource(
new MediaStream([localAudioTrack]),
);
source.connect(analyser);
const buf = new Float32Array(analyser.fftSize);
let raf = 0;
let lastUpdate = 0;
let voiceActive = false;
let noiseFloor = MIC_INITIAL_NOISE_FLOOR;
let smoothedLevel = 0;
function tick(now: number) {
raf = requestAnimationFrame(tick);
if (now - lastUpdate < MIC_ANALYSER_UPDATE_INTERVAL_MS) return;
lastUpdate = now;
analyser.getFloatTimeDomainData(buf);
let sumSquares = 0;
for (let i = 0; i < buf.length; i += 1) {
sumSquares += buf[i] * buf[i];
}
const rms = Math.sqrt(sumSquares / buf.length);
const activeThreshold = Math.max(
MIC_VOICE_GATE_ON_RMS,
noiseFloor + MIC_VOICE_GATE_MARGIN_RMS,
);
const idleThreshold = Math.max(
MIC_VOICE_GATE_OFF_RMS,
noiseFloor + MIC_VOICE_GATE_MARGIN_RMS * 0.55,
);
voiceActive = voiceActive ? rms > idleThreshold : rms > activeThreshold;
const floorRate =
rms < noiseFloor
? 0.18
: voiceActive
? MIC_ACTIVE_NOISE_FLOOR_RISE
: 0.025;
noiseFloor += (rms - noiseFloor) * floorRate;
if (!voiceActive) {
smoothedLevel = 0;
setMicLevel(0);
return;
}
const normalized = clamp01(
(rms - noiseFloor) / MIC_LEVEL_ACTIVE_RANGE_RMS,
);
const targetLevel = Math.max(normalized, MIC_MIN_ACTIVE_LEVEL);
smoothedLevel += (targetLevel - smoothedLevel) * MIC_LEVEL_ATTACK;
setMicLevel(smoothedLevel);
}
raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
source.disconnect();
void ctx.close();
};
}, [localAudioTrack, micConnected]);
return micLevel;
}