perf: performance improvements 1
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+6
@@ -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;
|
||||
|
||||
+44
-1
@@ -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;
|
||||
|
||||
+4
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user