perf: performance improvements 1

This commit is contained in:
2026-07-14 01:34:33 +02:00
parent edc4d935d8
commit 59dfd2de85
27 changed files with 762 additions and 94 deletions
@@ -0,0 +1,30 @@
import {
clearDebugNetworkPeerMetrics,
getDebugNetworkMetricSnapshot,
MAX_TRACKED_DEBUG_NETWORK_PEERS,
recordDebugNetworkPing
} from './debug-network-metrics';
describe('debug network metrics retention', () => {
it('releases a peer metric entry when the peer is cleared', () => {
recordDebugNetworkPing('peer-cleared', 42);
expect(getDebugNetworkMetricSnapshot('peer-cleared')).not.toBeNull();
clearDebugNetworkPeerMetrics('peer-cleared');
expect(getDebugNetworkMetricSnapshot('peer-cleared')).toBeNull();
});
it('evicts the oldest peer entries once the tracked-peer cap is exceeded', () => {
recordDebugNetworkPing('peer-oldest', 10);
for (let index = 0; index < MAX_TRACKED_DEBUG_NETWORK_PEERS; index++) {
recordDebugNetworkPing(`peer-fill-${index}`, index);
}
expect(getDebugNetworkMetricSnapshot('peer-oldest')).toBeNull();
expect(
getDebugNetworkMetricSnapshot(`peer-fill-${MAX_TRACKED_DEBUG_NETWORK_PEERS - 1}`)
).not.toBeNull();
});
});
@@ -3,6 +3,13 @@ type DebugNetworkHandshakeType = 'answer' | 'ice_candidate' | 'offer';
const FILE_RATE_WINDOW_MS = 6_000;
/**
* Peer metric entries are tiny, but the store is keyed by every peer id ever
* seen in the session. Long sessions with peer churn would otherwise grow the
* map for the app lifetime, so the oldest entries are evicted past this cap.
*/
export const MAX_TRACKED_DEBUG_NETWORK_PEERS = 200;
export interface DebugNetworkMetricHandshakeCounts {
answersReceived: number;
answersSent: number;
@@ -236,12 +243,25 @@ class DebugNetworkMetricsStore {
};
}
clearPeer(peerId: string): void {
this.metrics.delete(peerId);
}
private ensure(peerId: string): InternalDebugNetworkMetricState {
const existing = this.metrics.get(peerId);
if (existing)
return existing;
while (this.metrics.size >= MAX_TRACKED_DEBUG_NETWORK_PEERS) {
const oldestPeerId = this.metrics.keys().next().value;
if (oldestPeerId === undefined)
break;
this.metrics.delete(oldestPeerId);
}
const created: InternalDebugNetworkMetricState = {
connectionDrops: 0,
downloads: createDownloadRates(),
@@ -384,3 +404,8 @@ export function recordDebugNetworkFileChunk(
export function getDebugNetworkMetricSnapshot(peerId: string): DebugNetworkMetricSnapshot | null {
return debugNetworkMetricsStore.getSnapshot(peerId);
}
/** Drop the metric entry for a peer that fully left (no reconnect pending). */
export function clearDebugNetworkPeerMetrics(peerId: string): void {
debugNetworkMetricsStore.clearPeer(peerId);
}
@@ -340,6 +340,12 @@ export class PeerConnectionManager {
return false;
try {
// Release the failed channel before adopting the replacement; otherwise
// its SCTP resources and handlers stay alive for the connection lifetime.
try {
expectedChannel.close();
} catch { /* channel may already be closed */ }
const replacement = peerData.connection.createDataChannel(DATA_CHANNEL_LABEL, { ordered: true });
peerData.dataChannel = replacement;
@@ -5,7 +5,11 @@ import {
PeerConnectionManagerContext,
RecoveryHandlers
} from '../shared';
import { scheduleDataChannelRecovery } from './peer-recovery';
import {
closeAllPeers,
removePeer,
scheduleDataChannelRecovery
} from './peer-recovery';
describe('peer recovery', () => {
afterEach(() => {
@@ -13,6 +17,40 @@ describe('peer recovery', () => {
vi.useRealTimers();
});
it('removes every remote stream map entry for a removed peer, including camera streams', () => {
const channel = createDataChannel(DATA_CHANNEL_STATE_OPEN);
const context = createContext('alice');
context.state.activePeerConnections.set('bob', createPeerData(channel, 'connected'));
context.state.remotePeerStreams.set('bob', createMediaStream());
context.state.remotePeerVoiceStreams.set('bob', createMediaStream());
context.state.remotePeerScreenShareStreams.set('bob', createMediaStream());
context.state.remotePeerCameraStreams.set('bob', createMediaStream());
removePeer(context, 'bob');
expect(context.state.remotePeerStreams.has('bob')).toBe(false);
expect(context.state.remotePeerVoiceStreams.has('bob')).toBe(false);
expect(context.state.remotePeerScreenShareStreams.has('bob')).toBe(false);
expect(context.state.remotePeerCameraStreams.has('bob')).toBe(false);
});
it('clears every remote stream map when all peers close, including camera streams', () => {
const context = createContext('alice');
context.state.remotePeerStreams.set('bob', createMediaStream());
context.state.remotePeerVoiceStreams.set('bob', createMediaStream());
context.state.remotePeerScreenShareStreams.set('bob', createMediaStream());
context.state.remotePeerCameraStreams.set('bob', createMediaStream());
closeAllPeers(context.state);
expect(context.state.remotePeerStreams.size).toBe(0);
expect(context.state.remotePeerVoiceStreams.size).toBe(0);
expect(context.state.remotePeerScreenShareStreams.size).toBe(0);
expect(context.state.remotePeerCameraStreams.size).toBe(0);
});
it('recreates a peer immediately when the data channel is already closed', () => {
vi.useFakeTimers();
@@ -205,9 +243,14 @@ function createPeerData(
};
}
function createMediaStream(): MediaStream {
return { getTracks: () => [] } as unknown as MediaStream;
}
function createDataChannel(readyState: RTCDataChannelState): RTCDataChannel {
return {
bufferedAmount: 0,
close: vi.fn(),
label: 'chat',
readyState
} as unknown as RTCDataChannel;
@@ -14,6 +14,7 @@ import {
RemovePeerOptions
} from '../shared';
import { clearAllPingTimers, stopPingInterval } from '../messaging/ping';
import { clearDebugNetworkPeerMetrics } from '../../logging/debug-network-metrics';
/**
* Close and remove a peer connection, data channel, and emit a disconnect event.
@@ -33,11 +34,13 @@ export function removePeer(
if (!preserveReconnectState) {
clearPeerReconnectTimer(state, peerId);
state.disconnectedPeerTracker.delete(peerId);
clearDebugNetworkPeerMetrics(peerId);
}
state.remotePeerStreams.delete(peerId);
state.remotePeerVoiceStreams.delete(peerId);
state.remotePeerScreenShareStreams.delete(peerId);
state.remotePeerCameraStreams.delete(peerId);
if (peerData) {
if (peerData.dataChannel)
@@ -72,6 +75,7 @@ export function closeAllPeers(state: PeerConnectionManagerState): void {
state.remotePeerStreams.clear();
state.remotePeerVoiceStreams.clear();
state.remotePeerScreenShareStreams.clear();
state.remotePeerCameraStreams.clear();
state.peerNegotiationQueue.clear();
state.peerLatencies.clear();
state.pendingPings.clear();