test(gateway): pace qualification video source
Verify Data Plane / gateway (push) Failing after 1m30s

This commit is contained in:
sechmachine
2026-08-09 15:46:07 +07:00
parent b3ed1db36a
commit 0b7e7b8b31
6 changed files with 163 additions and 14 deletions
+67
View File
@@ -7,6 +7,7 @@ import (
"encoding/binary"
"errors"
"io"
"net"
"os"
"path/filepath"
"reflect"
@@ -67,6 +68,72 @@ func TestQualificationRecordsLinkedToolVersions(t *testing.T) {
}
}
func TestQualificationApolloFixturePacesSourceShapedVideo(t *testing.T) {
key := bytes.Repeat([]byte{0x3c}, 16)
encoded := make([]byte, 1000*apolloVideoShardPayloadSize-8)
packets := qualificationSourceVideoPackets(t, key, 1, encoded)
if len(packets) != 1000 || len(packets[0]) != 1072 {
t.Fatalf("source vector = %d packets of %d bytes, want 1000 packets of 1072 bytes", len(packets), len(packets[0]))
}
packetsPerMillisecond, batchSize := qualificationApolloVideoPacing(len(packets[0]))
if packetsPerMillisecond != 93 || batchSize != 61 {
t.Fatalf("Apollo pacing vector = %d packets/ms, batch %d; want 93 and 61", packetsPerMillisecond, batchSize)
}
receiver, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
if err != nil {
t.Fatal(err)
}
defer receiver.Close()
sender, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
if err != nil {
t.Fatal(err)
}
defer sender.Close()
fixture := &qualificationApolloFixture{video: sender, failures: make(chan error, 1)}
remote := *receiver.LocalAddr().(*net.UDPAddr)
fixture.videoRemote.Store(&remote)
drained := make(chan struct{})
go func() {
defer close(drained)
buffer := make([]byte, 2048)
for range len(packets) + 1 {
if _, _, readErr := receiver.ReadFromUDP(buffer); readErr != nil {
return
}
}
}()
started := time.Now()
if err := fixture.sendVideo(context.Background(), packets); err != nil {
t.Fatal(err)
}
if err := fixture.sendVideo(context.Background(), packets[:1]); err != nil {
t.Fatal(err)
}
elapsed := time.Since(started)
wantCarry := 10 * time.Millisecond // floor(1000 / 93) ms at Apollo's pinned 80%-of-1-Gbps rate.
if elapsed < wantCarry {
t.Fatalf("source fixture sent the next frame after %s, before Apollo pacing carry %s", elapsed, wantCarry)
}
select {
case <-drained:
case <-time.After(time.Second):
t.Fatal("source-shaped UDP receiver did not drain the fixed vector")
}
fixture.videoNext = time.Now().Add(time.Second)
beforeCancel := fixture.sentPackets.Load()
cancelled, cancel := context.WithCancel(context.Background())
cancel()
if err := fixture.sendVideo(cancelled, packets[:1]); !errors.Is(err, context.Canceled) {
t.Fatalf("cancelled pacing wait returned %v, want context.Canceled", err)
}
if fixture.sentPackets.Load() != beforeCancel {
t.Fatal("cancelled pacing wait emitted a UDP shard")
}
}
func TestQualificationOutputAndStatisticsFailClosed(t *testing.T) {
if err := validateQualificationOutputDir("relative/evidence"); err == nil {
t.Fatal("relative evidence directory was accepted")
+80 -14
View File
@@ -39,15 +39,18 @@ import (
)
const (
qualificationToolVersion = "versevdi-gateway-qualification/v6"
qualificationImpairmentQueuePackets = nativeApolloVideoQueuePackets
qualificationImpairmentMaxPackets = 100_000
qualificationImpairmentPacketCount = 10_000
qualificationProcessingLimit = 5 * time.Millisecond
qualificationImpairmentSeed uint64 = 0x3c6a11ce
qualificationClockOverheadMethod = "median of 1000 batches of 100 monotonic time reads"
qualificationGatewayCPUScope = "isolated gateway subprocess; bounded recorder/control included, fixture and client driver excluded"
qualificationResourceMethod = "RUSAGE_SELF user+system CPU; runtime/metrics heap objects, allocated objects/bytes, and live goroutines sampled once per second"
qualificationToolVersion = "versevdi-gateway-qualification/v7"
qualificationImpairmentQueuePackets = nativeApolloVideoQueuePackets
qualificationImpairmentMaxPackets = 100_000
qualificationImpairmentPacketCount = 10_000
qualificationProcessingLimit = 5 * time.Millisecond
qualificationImpairmentSeed uint64 = 0x3c6a11ce
qualificationClockOverheadMethod = "median of 1000 batches of 100 monotonic time reads"
qualificationGatewayCPUScope = "isolated gateway subprocess; bounded recorder/control included, fixture and client driver excluded"
qualificationResourceMethod = "RUSAGE_SELF user+system CPU; runtime/metrics heap objects, allocated objects/bytes, and live goroutines sampled once per second"
qualificationApolloVideoRateBitsPerSecond = 1_000_000_000 * 80 / 100
qualificationApolloVideoBatchBytes = 64 * 1024
qualificationApolloVideoBatchPackets = 64
)
type qualificationMediaProfile struct {
@@ -425,6 +428,8 @@ type qualificationApolloFixture struct {
closeOnce sync.Once
sentPackets atomic.Uint64
work protocol.ProviderSessionWork
videoPaceMu sync.Mutex
videoNext time.Time
controlImpairmentMu sync.Mutex
controlRTT time.Duration
@@ -754,15 +759,76 @@ func (f *qualificationApolloFixture) sendVideo(ctx context.Context, packets [][]
}
}
remote := f.videoRemote.Load()
for _, packet := range packets {
if _, err := f.video.WriteToUDP(packet, remote); err != nil {
return err
}
f.sentPackets.Add(1)
if len(packets) == 0 {
return nil
}
packetsPerMillisecond, batchSize := qualificationApolloVideoPacing(len(packets[0]))
if packetsPerMillisecond == 0 || batchSize == 0 {
return ErrProviderMalformed
}
f.videoPaceMu.Lock()
defer f.videoPaceMu.Unlock()
frameStart := time.Now()
if f.videoNext.After(frameStart) {
frameStart = f.videoNext
}
framePackets, groupPackets := 0, 0
for batchStart := 0; batchStart < len(packets); batchStart += batchSize {
if framePackets == 0 || groupPackets >= packetsPerMillisecond {
due := frameStart.Add(time.Millisecond * time.Duration(framePackets) / time.Duration(packetsPerMillisecond))
if err := qualificationWaitContext(ctx, due); err != nil {
return err
}
groupPackets = 0
}
batchEnd := min(batchStart+batchSize, len(packets))
for _, packet := range packets[batchStart:batchEnd] {
if len(packet) != len(packets[0]) {
return ErrProviderMalformed
}
if _, err := f.video.WriteToUDP(packet, remote); err != nil {
return err
}
f.sentPackets.Add(1)
}
currentBatch := batchEnd - batchStart
framePackets += currentBatch
groupPackets += currentBatch
}
f.videoNext = frameStart.Add(time.Millisecond * time.Duration(framePackets) / time.Duration(packetsPerMillisecond))
return nil
}
func qualificationApolloVideoPacing(packetBytes int) (packetsPerMillisecond, batchSize int) {
if packetBytes <= 0 {
return 0, 0
}
packetsPerMillisecond = qualificationApolloVideoRateBitsPerSecond / 1000 / packetBytes / 8
batchSize = min(qualificationApolloVideoBatchBytes/packetBytes, qualificationApolloVideoBatchPackets)
return packetsPerMillisecond, batchSize
}
func qualificationWaitContext(ctx context.Context, due time.Time) error {
delay := time.Until(due)
if delay <= 0 {
select {
case <-ctx.Done():
return ctx.Err()
default:
return nil
}
}
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
func (f *qualificationApolloFixture) streamKey() ([]byte, error) {
key := f.key.Load()
if key == nil || len(*key) != 16 {