diff --git a/gateway/qualification_contract_test.go b/gateway/qualification_contract_test.go index b23b5ab..b97efab 100644 --- a/gateway/qualification_contract_test.go +++ b/gateway/qualification_contract_test.go @@ -75,9 +75,15 @@ func TestQualificationApolloFixturePacesSourceShapedVideo(t *testing.T) { 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) + packetsPerMillisecond, batchSize := qualificationApolloVideoPacing(apolloVideoRawPacketSize) + if apolloVideoRawPacketSize != 1040 || packetsPerMillisecond != 96 || batchSize != 63 { + t.Fatalf("Apollo raw pacing vector = %d bytes, %d packets/ms, batch %d; want 1040, 96, and 63", apolloVideoRawPacketSize, packetsPerMillisecond, batchSize) + } + wantOffsets := []time.Duration{0, 656250 * time.Nanosecond, 1312500 * time.Nanosecond, 10416666 * time.Nanosecond} + for index, sent := range []int{0, 63, 126, 1000} { + if got := qualificationApolloVideoOffset(sent, packetsPerMillisecond); got != wantOffsets[index] { + t.Fatalf("Apollo pacing offset after %d packets = %s, want %s", sent, got, wantOffsets[index]) + } } receiver, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")}) @@ -113,7 +119,7 @@ func TestQualificationApolloFixturePacesSourceShapedVideo(t *testing.T) { t.Fatal(err) } elapsed := time.Since(started) - wantCarry := 10 * time.Millisecond // floor(1000 / 93) ms at Apollo's pinned 80%-of-1-Gbps rate. + wantCarry := qualificationApolloVideoOffset(len(packets), packetsPerMillisecond) if elapsed < wantCarry { t.Fatalf("source fixture sent the next frame after %s, before Apollo pacing carry %s", elapsed, wantCarry) } diff --git a/gateway/qualification_harness_test.go b/gateway/qualification_harness_test.go index f89b9a0..6f630ef 100644 --- a/gateway/qualification_harness_test.go +++ b/gateway/qualification_harness_test.go @@ -762,7 +762,7 @@ func (f *qualificationApolloFixture) sendVideo(ctx context.Context, packets [][] if len(packets) == 0 { return nil } - packetsPerMillisecond, batchSize := qualificationApolloVideoPacing(len(packets[0])) + packetsPerMillisecond, batchSize := qualificationApolloVideoPacing(apolloVideoRawPacketSize) if packetsPerMillisecond == 0 || batchSize == 0 { return ErrProviderMalformed } @@ -773,14 +773,11 @@ func (f *qualificationApolloFixture) sendVideo(ctx context.Context, packets [][] if f.videoNext.After(frameStart) { frameStart = f.videoNext } - framePackets, groupPackets := 0, 0 + framePackets := 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 + due := frameStart.Add(qualificationApolloVideoOffset(framePackets, packetsPerMillisecond)) + if err := qualificationWaitContext(ctx, due); err != nil { + return err } batchEnd := min(batchStart+batchSize, len(packets)) for _, packet := range packets[batchStart:batchEnd] { @@ -794,9 +791,8 @@ func (f *qualificationApolloFixture) sendVideo(ctx context.Context, packets [][] } currentBatch := batchEnd - batchStart framePackets += currentBatch - groupPackets += currentBatch } - f.videoNext = frameStart.Add(time.Millisecond * time.Duration(framePackets) / time.Duration(packetsPerMillisecond)) + f.videoNext = frameStart.Add(qualificationApolloVideoOffset(framePackets, packetsPerMillisecond)) return nil } @@ -809,6 +805,10 @@ func qualificationApolloVideoPacing(packetBytes int) (packetsPerMillisecond, bat return packetsPerMillisecond, batchSize } +func qualificationApolloVideoOffset(packets, packetsPerMillisecond int) time.Duration { + return time.Millisecond * time.Duration(packets) / time.Duration(packetsPerMillisecond) +} + func qualificationWaitContext(ctx context.Context, due time.Time) error { delay := time.Until(due) if delay <= 0 { diff --git a/openspec/changes/truthful-fixed-frame-qualification/design.md b/openspec/changes/truthful-fixed-frame-qualification/design.md index 5ed2c92..009a809 100644 --- a/openspec/changes/truthful-fixed-frame-qualification/design.md +++ b/openspec/changes/truthful-fixed-frame-qualification/design.md @@ -4,6 +4,8 @@ The current harness sends one fixed 1,179-byte payload per logical sample. It re The complete-frame fixture also must preserve the pinned Apollo source schedule. For each frame it derives packets per millisecond from the raw UDP block size at 80% of 1 Gbps, limits source batches to both 64 KiB and 64 packets, and carries the next-send time into the following frame. Waiting is context-cancellable. This is qualification-fixture behavior only; production transport and queue behavior remain unchanged. +Because the bounded fixture uses loopback rather than a physical 1 Gbps link, each batch begins at its cumulative wire-rate offset. This preserves Apollo's raw-block rate and batch ceilings without collapsing multiple batches into an instantaneous loopback burst. + ## Goals / Non-Goals **Goals:** diff --git a/openspec/changes/truthful-fixed-frame-qualification/specs/gateway-qualification/spec.md b/openspec/changes/truthful-fixed-frame-qualification/specs/gateway-qualification/spec.md index 83617ae..32b9c39 100644 --- a/openspec/changes/truthful-fixed-frame-qualification/specs/gateway-qualification/spec.md +++ b/openspec/changes/truthful-fixed-frame-qualification/specs/gateway-qualification/spec.md @@ -14,5 +14,5 @@ Within each complete frame the source fixture SHALL reproduce pinned Apollo's so - **THEN** the qualification command exits unsuccessfully without recording a passing candidate #### Scenario: Source-shaped Apollo pacing is preserved -- **WHEN** the fixture emits 1,072-byte encrypted video shards for consecutive complete frames -- **THEN** it uses 93 packets per millisecond, batches at most 61 shards, carries the integer next-send offset into the following frame, and emits no shard after a cancelled pacing wait +- **WHEN** the fixture emits 1,072-byte encrypted video shards with 1,040-byte raw blocks for consecutive complete frames +- **THEN** it uses 96 packets per millisecond, batches at most 63 shards at offsets derived from cumulative packet count, carries the next-send offset into the following frame, and emits no shard after a cancelled pacing wait