Telegram-iOS/tools/go_sfu/network_sim.go
Isaac 5962a563e4 feat: tgcalls CLI test tool with group SFU, video, and adaptation
Squashed buildout of the tgcalls testbench:

- CLI test tool with --mode p2p/reflector/group/group-churn,
  cross-version interop (--version, --version2), and quiet/summary output
- Linux toolchain + Docker multi-stage build, AWS Fargate mass test harness,
  local parallel mass test harness with signaling loss simulation
- SCTP writable gate, retransmission timer tuning, role-based handshake
- InstanceV2CompatImpl (PeerConnection backend with V2Impl signaling) and
  SignalingTranslator for v14.0.0 interop
- In-process Go/Pion SFU (ICE+DTLS+SRTP+SCTP per participant) with audio
  RTP forwarding, ActiveAudio/VideoSsrcs data channel broadcast, RTCP
  feedback path, and CGo c-archive integration
- GroupInstanceReferenceImpl (PeerConnection group-call) and mixed-impl
  group mode (--reference-participants), with SDP munging for simulcast
- H264 simulcast group video (FakeVideoTrackSource pattern generator,
  FakeVideoSink frame counting, --video flag, two-pass channel setup,
  reactive video setup from ActiveVideoSsrcs)
- Group churn stress mode (--mode group-churn, --churn-cycles)
- SFU stream-quality adaptation: BandwidthEstimator, LayerSelector
  state machine, RtxRingBuffer, simulcast SSRC rewrite
- Transport-cc feedback generation, NetworkSimulator (delay/jitter/loss/
  token-bucket bandwidth), --network-scenario step-down-up
- CLAUDE.md updates throughout

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 18:28:43 +02:00

128 lines
3 KiB
Go

package main
import (
"math/rand"
"sync"
"time"
)
// NetworkSimulator models a uni-directional network pipe with delay, jitter,
// packet loss, and bandwidth cap (token bucket).
type NetworkSimulator struct {
mu sync.Mutex
delayMs int
jitterMs int
dropRate float64
bandwidthBps int64
// Token bucket for bandwidth cap.
tokens float64 // available tokens (bits)
maxTokens float64 // max tokens = 200ms worth of bandwidth
lastRefill time.Time
rng *rand.Rand
closed bool
}
// NewNetworkSimulator creates a simulator with no simulation (passthrough).
func NewNetworkSimulator() *NetworkSimulator {
return &NetworkSimulator{
lastRefill: time.Now(),
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
// SetParams reconfigures the simulator at runtime. Thread-safe.
func (ns *NetworkSimulator) SetParams(delayMs, jitterMs int, dropRate float64, bandwidthBps int64) {
ns.mu.Lock()
defer ns.mu.Unlock()
ns.delayMs = delayMs
ns.jitterMs = jitterMs
ns.dropRate = dropRate
ns.bandwidthBps = bandwidthBps
if bandwidthBps > 0 {
ns.maxTokens = float64(bandwidthBps) * 0.2 // 200ms buffer
if ns.tokens > ns.maxTokens {
ns.tokens = ns.maxTokens
}
} else {
ns.maxTokens = 0
ns.tokens = 0
}
}
// Send processes a packet through the simulator. deliverFn is called
// (possibly asynchronously) after simulation. The packet bytes are copied
// if delivery is deferred.
func (ns *NetworkSimulator) Send(pkt []byte, deliverFn func([]byte)) {
ns.mu.Lock()
if ns.closed {
ns.mu.Unlock()
return
}
// Drop check.
if ns.dropRate > 0 && ns.rng.Float64() < ns.dropRate {
ns.mu.Unlock()
return
}
// Bandwidth cap: token bucket.
if ns.bandwidthBps > 0 {
ns.refillTokens()
cost := float64(len(pkt)) * 8
if ns.tokens < cost {
// Queue full / no tokens — tail drop.
ns.mu.Unlock()
return
}
ns.tokens -= cost
}
// Calculate delay.
delayMs := ns.delayMs
if ns.jitterMs > 0 {
delayMs += ns.rng.Intn(2*ns.jitterMs+1) - ns.jitterMs
if delayMs < 0 {
delayMs = 0
}
}
ns.mu.Unlock()
if delayMs == 0 {
deliverFn(pkt)
return
}
// Copy packet for deferred delivery.
pktCopy := make([]byte, len(pkt))
copy(pktCopy, pkt)
time.AfterFunc(time.Duration(delayMs)*time.Millisecond, func() {
deliverFn(pktCopy)
})
}
// Close stops the simulator. Pending delayed packets may still fire.
func (ns *NetworkSimulator) Close() {
ns.mu.Lock()
ns.closed = true
ns.mu.Unlock()
}
// refillTokens adds tokens based on elapsed time. Must be called with mu held.
func (ns *NetworkSimulator) refillTokens() {
now := time.Now()
elapsed := now.Sub(ns.lastRefill).Seconds()
ns.lastRefill = now
ns.tokens += float64(ns.bandwidthBps) * elapsed
if ns.tokens > ns.maxTokens {
ns.tokens = ns.maxTokens
}
}
// IsPassthrough returns true if no simulation is configured.
func (ns *NetworkSimulator) IsPassthrough() bool {
ns.mu.Lock()
defer ns.mu.Unlock()
return ns.delayMs == 0 && ns.jitterMs == 0 && ns.dropRate == 0 && ns.bandwidthBps == 0
}