# Phase 19: Memory Leak Audit Report

**Audit Date:** 2026-02-08
**Auditor:** Claude Code
**Target:** Session screen memory leak causing ~10-minute freeze
**Methodology:** Static code analysis with line-by-line verification

## Executive Summary

This audit systematically examined the jam-ui session screen components, hooks, and Redux slices to identify memory leak patterns. Each file was analyzed for:
- Timer/interval creation without cleanup
- Event listener registration without removal
- Unbounded state growth patterns
- Callback registration without unregistration

Findings are categorized by severity:
- **HIGH**: Likely contributor to the 10-minute freeze
- **MEDIUM**: May contribute to memory degradation over time
- **LOW**: Minor or properly handled, included for completeness

---

## VU Meter Audit

**Files Audited:**
- `jam-ui/src/components/client/SessionTrackVU.js`
- `jam-ui/src/hooks/useMixerStore.js`
- `jam-ui/src/hooks/useVuHelpers.js`
- `jam-ui/src/hooks/useMixerHelper.js`
- `jam-ui/src/context/VuContext.js`
- `jam-ui/src/context/MixersContext.js`

**Requirements Coverage:** VUMTR-01, VUMTR-02, VUMTR-03

### SessionTrackVU.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 9-30 | useEffect | Mixer registration and VU state updates | YES | LOW | Cleanup resets VU state to 0 and nulls mixerIdRef on unmount |

**Analysis:** This component has proper cleanup. The useEffect at line 9 returns a cleanup function at lines 23-28 that resets the VU state.

### useMixerStore.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 74-94 | useEffect | Sets `window.JK.HandleBridgeCallback2` global callback | YES | LOW | Properly deletes callback on cleanup (line 87) |
| 55 | useRef | `recheckTimeoutRef` for timeout tracking | YES | LOW | Cleared at lines 91 and 205 |
| 135-191 | handleBridgeCallback | VU data processing callback | N/A | MEDIUM | Callback uses ref to avoid stale closure, but could accumulate calls if not throttled |

**Analysis:** The VU callback registration pattern is sound. The `window.JK.HandleBridgeCallback2` is set at line 79 and deleted at line 87 in the cleanup function. The `recheckTimeoutRef` is properly cleared on cleanup.

**Potential Issue (VUMTR-01):** The `handleBridgeCallback` (lines 135-191) processes VU data but has commented-out throttling logic (lines 141-152). Without throttling, this callback may fire at very high rates (native client sends VU data frequently), potentially causing performance degradation over time.

### useVuHelpers.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 6 | useState | `vuStates` object tracking VU levels per mixer | PARTIAL | HIGH | Cleared on unmount but entries never removed during session |
| 99-104 | updateVuState | Updates vuStates object | N/A | HIGH | Adds new keys but never removes old ones |
| 117-122 | useEffect | Cleanup effect | YES | PARTIAL | Only clears entire object on unmount, not per-mixer cleanup |

**Analysis:**

**VUMTR-02 - CRITICAL FINDING:** The `vuStates` object at line 6 accumulates entries over time:
```javascript
const [vuStates, setVuStates] = useState({});  // Line 6

const updateVuState = useCallback((mixerId, level, clipping = false) => {
  setVuStates(prev => ({
    ...prev,
    [mixerId]: { level, clipping }  // Adds new keys, never removes
  }));
}, []);  // Lines 99-104
```

When tracks are added/removed during a session, the `vuStates` object grows but entries for removed tracks are never deleted. Over a 10-minute session with multiple track changes, this could accumulate hundreds of stale entries.

**Evidence:**
- Line 100: `setVuStates(prev => ({ ...prev, [mixerId]: { level, clipping } }))` - spreads existing state and adds new entry
- Line 120: `setVuStates({})` - only clears on full unmount
- No mechanism to remove individual mixer entries when tracks leave

### useMixerHelper.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 67 | useRef | `allMixersRef` for mixer tracking | N/A | LOW | Ref, not timer |
| 68 | useRef | `previousMyTracksRef` for track caching | N/A | LOW | Prevents unnecessary re-renders |
| 120 | useRef | `isReady` flag | N/A | LOW | Simple flag ref |

**Analysis:** No timer/interval patterns found. Uses refs appropriately for state that shouldn't trigger re-renders.

### VuContext.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 6-14 | VuProvider | Context provider using useVuHelpers | N/A | N/A | Delegates to useVuHelpers |

**Analysis:** Simple context wrapper. All VU logic is in useVuHelpers.

### MixersContext.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 6-14 | MixersProvider | Context provider using useMixerHelper | N/A | N/A | Delegates to useMixerHelper |

**Analysis:** Simple context wrapper. All mixer logic is in useMixerHelper.

### VU Meter Findings Summary

| ID | Finding | File | Lines | Severity | Fix Recommendation |
|----|---------|------|-------|----------|-------------------|
| VUMTR-01 | VU callback throttling disabled | useMixerStore.js | 141-152 | MEDIUM | Re-enable or implement proper throttling |
| VUMTR-02 | vuStates object grows unbounded | useVuHelpers.js | 6, 99-104 | HIGH | Add cleanup function to remove stale mixer entries |
| VUMTR-03 | No per-mixer cleanup on track leave | useVuHelpers.js | 117-122 | HIGH | Expose removeVuState function for track cleanup |

---

## Chat Window Audit

**Files Audited:**
- `jam-ui/src/components/client/JKSessionChatWindow.js`
- `jam-ui/src/components/client/chat/JKChatMessageList.js`
- `jam-ui/src/store/features/sessionChatSlice.js`
- `jam-ui/src/hooks/useSessionWebSocket.js`

**Requirements Coverage:** CHAT-01, CHAT-02, CHAT-03

### JKSessionChatWindow.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 59-67 | useEffect + addEventListener | Keyboard listener for Escape key | YES | LOW | Properly removes listener on cleanup (line 66) |
| 70-79 | useEffect + setTimeout | Focus textarea on window open | NO | LOW | Short 100ms timeout, not a significant leak |

**Analysis:** The event listener pattern is properly implemented with cleanup. The setTimeout at line 73 for focus delay is a one-time short delay and does not require explicit cleanup.

### JKChatMessageList.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 44 | useRef | `scrollTimeoutRef` for debounce tracking | YES | LOW | Proper ref usage |
| 86-93 | setTimeout | Scroll debounce timeout | YES | LOW | Cleared on next scroll (line 79) |
| 112-113 | setTimeout | Initial scroll delay | NO | LOW | One-time 100ms delay, no cleanup needed |
| 120-126 | useEffect | Cleanup effect for scrollTimeoutRef | YES | LOW | Properly clears timeout on unmount |

**Analysis:** The scroll debounce pattern is well implemented. The `scrollTimeoutRef` is cleared both when a new scroll occurs (line 79) and on unmount (lines 122-124). The initial scroll timeout at line 112 is a one-time delay that doesn't require cleanup.

### sessionChatSlice.js (CRITICAL)

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 120 | initialState | `messagesByChannel: {}` - message storage | N/A | HIGH | Messages accumulate without limit |
| 202 | push | `state.messagesByChannel[channel].push(message)` | N/A | HIGH | Unbounded array growth |
| 366 | spread + concat | `[...newMessages, ...state.messagesByChannel[channel]]` | N/A | HIGH | Merges without limiting |
| 418 | push | Optimistic message push | N/A | MEDIUM | Part of the same pattern |
| 512 | push | Attachment message push | N/A | MEDIUM | Part of the same pattern |

**Analysis:**

**CHAT-01 - CRITICAL FINDING:** The `messagesByChannel` Redux state has **NO MAX MESSAGE LIMIT**:

```javascript
// Line 120: Initial state
messagesByChannel: {},

// Line 202: WebSocket messages pushed without limit
state.messagesByChannel[channel].push(message);

// Line 366: Pagination merges without limit
state.messagesByChannel[channel] = [...newMessages, ...state.messagesByChannel[channel]];
```

During a 10-minute session with active chat:
- At ~5 messages/minute = 50+ messages
- Each message object includes: id, senderId, senderName, message, createdAt, channel, attachments
- With file attachments, message objects can be substantial in size
- **No mechanism exists to limit or purge old messages**

**Evidence of unbounded growth:**
- Line 202: `push(message)` - adds every incoming WebSocket message
- Line 366: Spread operator concatenates all fetched history
- Line 512: Attachment messages add to the same arrays
- **No slice(), splice(), or max-length check anywhere in the reducer**

### useSessionWebSocket.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 74-308 | useEffect | WebSocket callback registration | YES | LOW | Properly unregisters all callbacks on cleanup (lines 299-306) |
| 289-296 | forEach | Callback registration loop | YES | LOW | Each callback stored for later unregistration |
| 300-306 | forEach | Cleanup unregistration loop | YES | LOW | Iterates same callbacks object |

**Analysis:** The WebSocket callback lifecycle is properly managed:

```javascript
// Lines 289-296: Registration
Object.entries(callbacks).forEach(([event, callback]) => {
  if (jamServer.on) {
    jamServer.on(event, callback);
  } else if (jamServer.registerMessageCallback) {
    jamServer.registerMessageCallback(event, callback);
  }
});

// Lines 299-306: Cleanup
return () => {
  Object.entries(callbacks).forEach(([event, callback]) => {
    if (jamServer.off) {
      jamServer.off(event, callback);
    } else if (jamServer.unregisterMessageCallback) {
      jamServer.unregisterMessageCallback(event, callback);
    }
  });
};
```

The `callbacks` object is defined within the useEffect, so the cleanup function correctly references all registered callbacks.

**CHAT-02 - POTENTIAL ISSUE:** The `chatState` selector at line 72 is included in the dependency array at line 308. Since `chatState` includes `isWindowOpen` and `activeChannel`, this effect could re-run more often than necessary, potentially causing multiple callback registrations/unregistrations. However, this is a performance issue, not a memory leak.

### Chat Window Findings Summary

| ID | Finding | File | Lines | Severity | Fix Recommendation |
|----|---------|------|-------|----------|-------------------|
| CHAT-01 | Messages accumulate without limit | sessionChatSlice.js | 202, 366, 512 | HIGH | Implement MAX_MESSAGES limit (e.g., 500 messages per channel) |
| CHAT-02 | chatState in useEffect dependencies | useSessionWebSocket.js | 308 | LOW | Extract only needed values from chatState, or use useRef |
| CHAT-03 | No message cleanup on session leave | sessionChatSlice.js | N/A | MEDIUM | Add clearChannel action called when leaving session |

---

## Session Screen Audit

**Files Audited:**
- `jam-ui/src/components/client/JKSessionScreen.js`
- `jam-ui/src/hooks/useJamServer.js`
- `jam-ui/src/hooks/useSessionStats.js`
- `jam-ui/src/hooks/useSessionModel.js`
- `jam-ui/src/components/client/JKSessionBackingTrackPlayer.js`
- `jam-ui/src/components/client/JKSessionJamTrackPlayer.js`

**Requirements Coverage:** SESS-01, SESS-02, SESS-03

### JKSessionScreen.js

**useEffect Hook Count:** 14 total hooks

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 454-472 | setTimeout x3 | Track sync timers (timer1, timer2, timer3) | YES | LOW | Properly cleared at lines 470-472 |
| 616-629 | Callback registration | WebSocket message callbacks stored in state | YES | LOW | Unregistered via unregisterMessageCallbacks on leave |
| 985 | setTimeout | Video loading button timeout | NO | LOW | Short one-time delay in finally block, not a leak |
| 1032-1040 | useEffect | Session state sync | YES | LOW | Cleanup return function present |
| 1180-1195 | window.JK.HandleAlertCallback | Global alert callback registration | YES | LOW | Deleted on cleanup |

**Analysis:** JKSessionScreen.js is a large component (1740 lines) with 14 useEffect hooks. The majority have proper cleanup functions. The track sync timers at lines 454-472 are well-managed with refs and cleanup. The WebSocket callback registration pattern stores callbacks in state and unregisters them on leave.

**SESS-01 - POTENTIAL ISSUE:** The callback registration pattern (lines 616-629) stores callbacks in React state. If the leave handler doesn't execute (e.g., browser crash, navigation away), callbacks may remain registered. However, this is mitigated by the jamServer cleanup on connection close.

### useJamServer.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 270-272 | setTimeout | `reconnectTimeoutRef` for reconnection delay | YES | LOW | Cleared in disconnect() and on unmount |
| 311-320 | setTimeout | Request timeout in sendRequest | YES | LOW | Cleared via clearTimeout |
| 485-493 | setInterval x3 | `heartbeatInterval`, `heartbeatAckCheckInterval`, `startHeartbeatTimeout` | YES | LOW | All cleared via stopHeartbeat() |
| 608-634 | setTimeout | `activityTimeout` for user activity tracking | YES | LOW | Cleared in markActive() and cleanup |

**Analysis:** useJamServer.js has comprehensive cleanup for all timers:
- `stopHeartbeat()` (lines 485-493) clears all three heartbeat-related timers
- `disconnect()` clears reconnect timeout
- Activity timeout is cleared when marking active and on cleanup

All timer patterns follow best practices with refs and explicit cleanup.

### useSessionStats.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 151-153 | setInterval | Stats refresh every 2000ms | YES | LOW | Cleared via return () => clearInterval(intervalId) |

**Analysis:** Clean implementation. The stats polling interval is stored in a local variable and cleared in the useEffect return function.

```javascript
// Lines 151-153
const intervalId = setInterval(() => {
  fetchStats();
}, 2000);

return () => clearInterval(intervalId);
```

### useSessionModel.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 93 | useRef | `backendMixerAlertThrottleTimerRef` for throttling | YES | LOW | Cleared at lines 171-178 |
| 171-178 | useEffect | Cleanup effect for throttle timer | YES | LOW | Properly clears timeout on unmount |

**Analysis:** The throttle timer for backend mixer alerts is properly managed:

```javascript
// Lines 171-178
useEffect(() => {
  return () => {
    if (backendMixerAlertThrottleTimerRef.current) {
      clearTimeout(backendMixerAlertThrottleTimerRef.current);
    }
  };
}, []);
```

### JKSessionBackingTrackPlayer.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 155-218 | setInterval | Position/duration polling | YES | LOW | Cleared at lines 222-226 |
| 230-239 | useEffect | Unmount cleanup for playback | YES | LOW | Stops playback on unmount |
| 301 | setTimeout | Async Promise delay | N/A | LOW | One-time delay in async function |

**Analysis:** The position polling interval is properly cleaned up:

```javascript
// Lines 222-226
return () => {
  if (intervalId) {
    clearInterval(intervalId);
  }
};
```

The polling interval also adjusts based on visibility state (500ms when visible, 2000ms when hidden), reducing CPU usage when tab is backgrounded.

### JKSessionJamTrackPlayer.js

| Line | Pattern | Description | Has Cleanup | Severity | Notes |
|------|---------|-------------|-------------|----------|-------|
| 415-461 | setInterval | Position/duration polling | YES | LOW | Cleared via return clearInterval(intervalId) |
| 465-473 | addEventListener | visibilitychange listener | YES | LOW | Removed on cleanup |

**Analysis:** Both the polling interval and visibility listener are properly cleaned up. The pattern matches JKSessionBackingTrackPlayer.js with visibility-aware polling rates.

### Session Screen Findings Summary

| ID | Finding | File | Lines | Severity | Fix Recommendation |
|----|---------|------|-------|----------|-------------------|
| SESS-01 | Callback unregistration relies on leave handler | JKSessionScreen.js | 616-629 | LOW | Add backup cleanup in useEffect return |
| SESS-02 | No issues found | useJamServer.js | N/A | N/A | All timers properly cleaned up |
| SESS-03 | No issues found | useSessionStats.js, useSessionModel.js | N/A | N/A | All timers properly cleaned up |

**Overall Assessment:** The session screen and supporting hooks have well-implemented cleanup patterns. No critical memory leaks were identified in this area. The primary leak sources are in the VU meter (vuStates accumulation) and chat (unbounded message growth) systems.

---

## Priority Summary

### HIGH Severity Findings (Likely Causes of 10-Minute Freeze)

These findings represent unbounded state growth that accumulates over time and are the most likely causes of the session freeze:

| Priority | ID | Finding | Impact | Estimated Fix Effort |
|----------|----|---------|---------|--------------------|
| 1 | VUMTR-02 | vuStates object grows unbounded | State object grows with each track change, never pruned | Low - Add removeVuState function |
| 2 | VUMTR-03 | No per-mixer cleanup on track leave | Stale entries accumulate throughout session | Low - Call removeVuState on track leave |
| 3 | CHAT-01 | Messages accumulate without limit | Memory grows linearly with chat activity | Medium - Add MAX_MESSAGES constant and slice() |

### MEDIUM Severity Findings (May Contribute to Degradation)

These findings affect performance but are less likely to cause the freeze:

| Priority | ID | Finding | Impact | Estimated Fix Effort |
|----------|----|---------|---------|--------------------|
| 4 | VUMTR-01 | VU callback throttling disabled | High-frequency callbacks may cause render thrashing | Low - Re-enable throttling |
| 5 | CHAT-03 | No message cleanup on session leave | Memory not released when leaving session | Low - Add clearAllMessages action |

### LOW Severity Findings (Minor or Properly Handled)

These findings are included for completeness but do not require immediate action:

| Priority | ID | Finding | Impact | Estimated Fix Effort |
|----------|----|---------|---------|--------------------|
| 6 | CHAT-02 | chatState in useEffect dependencies | Performance, not memory | Low - Extract needed values |
| 7 | SESS-01 | Callback unregistration relies on leave handler | Edge case only | Low - Add backup cleanup |

---

## Implementation Recommendations

### Phase 20: VU Meter Fixes

**Objective:** Fix unbounded vuStates growth pattern

**Tasks:**
1. **Add removeVuState function to useVuHelpers.js**
   - Add `removeVuState(mixerId)` callback that deletes specific mixer entries
   - Export from VuContext for consumers

2. **Integrate cleanup with track lifecycle**
   - Call `removeVuState(mixerId)` when a track leaves the session
   - Hook into existing track leave detection in useMixerHelper.js

3. **Re-enable VU callback throttling (optional)**
   - Uncomment or re-implement throttling in useMixerStore.js lines 141-152
   - Set reasonable throttle interval (e.g., 50-100ms)

**Files to modify:**
- `jam-ui/src/hooks/useVuHelpers.js`
- `jam-ui/src/context/VuContext.js`
- `jam-ui/src/hooks/useMixerHelper.js` or `jam-ui/src/hooks/useMixerStore.js`

**Verification:**
- Join session, add/remove tracks multiple times
- Monitor vuStates object size - should stay bounded
- Memory should not grow after tracks removed

### Phase 21: Chat Window Fixes

**Objective:** Implement message limit and cleanup

**Tasks:**
1. **Add MAX_MESSAGES constant to sessionChatSlice.js**
   - Define `const MAX_MESSAGES = 500` (or appropriate limit)
   - Apply limit in `receiveMessage` reducer with `slice(-MAX_MESSAGES)`
   - Apply limit in `fetchMessagesSuccess` reducer

2. **Add clearAllMessages action**
   - Create `clearAllMessages` reducer that resets `messagesByChannel` to `{}`
   - Dispatch on session leave from JKSessionScreen.js

3. **Optimize chatState dependencies (optional)**
   - Extract only needed values from chatState selector in useSessionWebSocket.js
   - Reduce unnecessary effect re-runs

**Files to modify:**
- `jam-ui/src/store/features/sessionChatSlice.js`
- `jam-ui/src/components/client/JKSessionScreen.js` (for cleanup dispatch)

**Verification:**
- Send 600+ messages in a session
- Verify only latest 500 are retained
- Memory should plateau after limit reached

### Phase 22: Session Screen Hardening

**Objective:** Add defensive cleanup patterns

**Tasks:**
1. **Add backup callback cleanup in JKSessionScreen.js**
   - Add useEffect return function that unregisters callbacks
   - Ensures cleanup even if leave handler doesn't execute

2. **Verify all exit paths trigger cleanup**
   - Trace error, timeout, and navigation exit paths
   - Ensure cleanup runs in all cases

3. **Add memory monitoring for regression prevention**
   - Document manual testing procedure
   - Consider adding performance markers for automated detection

**Files to modify:**
- `jam-ui/src/components/client/JKSessionScreen.js`

**Verification:**
- Test various exit scenarios (leave, error, timeout, navigation)
- Confirm cleanup runs in all cases
- Session stable after 15+ minutes

---

## Conclusion

The audit identified **3 HIGH severity** findings that are the most likely causes of the ~10-minute session freeze:

1. **VUMTR-02 & VUMTR-03:** VU meter state accumulates without cleanup - affects every session with track changes
2. **CHAT-01:** Chat messages grow unbounded - affects sessions with active chat

The session screen base components (JKSessionScreen.js, useJamServer.js, player components) have **well-implemented cleanup patterns** and are not primary contributors to the leak.

**Recommended fix order:**
1. Phase 20: VU meter fixes (highest impact, lowest effort)
2. Phase 21: Chat fixes (high impact, medium effort)
3. Phase 22: Session hardening (defensive, low priority)

Total estimated effort: 2-3 hours for Phases 20-21, 1 hour for Phase 22.

