test(gateway): retain raw wire qualification evidence
Verify Data Plane / gateway (push) Successful in 4m40s
Verify Data Plane / gateway (push) Successful in 4m40s
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"encoding/csv"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -38,7 +39,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
qualificationToolVersion = "versevdi-gateway-qualification/v9"
|
||||
qualificationToolVersion = "versevdi-gateway-qualification/v10"
|
||||
qualificationImpairmentQueuePackets = nativeApolloVideoQueuePackets
|
||||
qualificationImpairmentMaxPackets = 100_000
|
||||
qualificationImpairmentPacketCount = 10_000
|
||||
@@ -50,6 +51,7 @@ const (
|
||||
qualificationApolloVideoRateBitsPerSecond = 1_000_000_000 * 80 / 100
|
||||
qualificationApolloVideoBatchBytes = 64 * 1024
|
||||
qualificationApolloVideoBatchPackets = 64
|
||||
qualificationWireTimebase = "monotonic offsets from constrained run start"
|
||||
)
|
||||
|
||||
type qualificationMediaProfile struct {
|
||||
@@ -204,13 +206,22 @@ type qualificationImpairmentObservation struct {
|
||||
RawSamples string `json:"raw_samples"`
|
||||
RawSamplesSHA256 string `json:"raw_samples_sha256"`
|
||||
RawSamplesBytes int64 `json:"raw_samples_bytes"`
|
||||
RawWireSamples string `json:"raw_wire_samples,omitempty"`
|
||||
RawWireSamplesSHA256 string `json:"raw_wire_samples_sha256,omitempty"`
|
||||
RawWireSamplesBytes int64 `json:"raw_wire_samples_bytes,omitempty"`
|
||||
RawWireRows int `json:"raw_wire_rows,omitempty"`
|
||||
RawWireDeliveryRows int `json:"raw_wire_delivery_rows,omitempty"`
|
||||
RawWireTransitionRows int `json:"raw_wire_transition_rows,omitempty"`
|
||||
RawWireTimebase string `json:"raw_wire_timebase,omitempty"`
|
||||
}
|
||||
|
||||
type qualificationCapacityStep struct {
|
||||
ReductionPercent int `json:"reduction_percent"`
|
||||
Convergence time.Duration `json:"convergence_ns"`
|
||||
MaximumFiveSecond int64 `json:"maximum_five_second_bytes"`
|
||||
FiveSecondCap int64 `json:"five_second_cap_bytes"`
|
||||
ReductionPercent int `json:"reduction_percent"`
|
||||
TransitionAfter time.Duration `json:"transition_after_ns"`
|
||||
Convergence time.Duration `json:"convergence_ns"`
|
||||
MaximumFiveSecond int64 `json:"maximum_five_second_bytes"`
|
||||
FiveSecondCap int64 `json:"five_second_cap_bytes"`
|
||||
RecomputationSource string `json:"recomputation_source"`
|
||||
}
|
||||
|
||||
type qualificationResourceSample struct {
|
||||
@@ -227,6 +238,15 @@ type qualificationDeliverySample struct {
|
||||
Bytes int64
|
||||
}
|
||||
|
||||
type qualificationWireFileEvidence struct {
|
||||
Name string
|
||||
SHA256 string
|
||||
Bytes int64
|
||||
Rows int
|
||||
DeliveryRows int
|
||||
TransitionRows int
|
||||
}
|
||||
|
||||
type qualificationFairnessEvidence struct {
|
||||
Evaluation time.Duration `json:"evaluation_ns"`
|
||||
PerFlowBytes map[string]int64 `json:"per_flow_bytes"`
|
||||
@@ -1507,7 +1527,10 @@ func runQualificationImpairment(t *testing.T, profile qualificationImpairmentPro
|
||||
beforeProviderDrops := path.session.mediaDrops.Load()
|
||||
beforeSourceUDP := path.sourceUDP.Load()
|
||||
beforePacer := path.server.pacer.reservations.Load()
|
||||
wireDeliveries := make([]qualificationDeliverySample, 0, len(jobs)*2)
|
||||
wireFragmentsPerUnit := (media.PacketBytes + frameV2PayloadSize - 1) / frameV2PayloadSize
|
||||
wireDeliveryLimit := len(jobs) * wireFragmentsPerUnit
|
||||
wireDeliveries := make([]qualificationDeliverySample, 0, wireDeliveryLimit)
|
||||
wireOverflow := false
|
||||
grace := max(2*profile.RTT+2*profile.Jitter, 2*time.Second)
|
||||
lastTarget := time.Duration(packetCount) * spacing
|
||||
if len(jobs) > 0 {
|
||||
@@ -1528,6 +1551,10 @@ func runQualificationImpairment(t *testing.T, profile qualificationImpairmentPro
|
||||
seen := make([]bool, packetCount)
|
||||
for len(result.packets) < len(jobs) {
|
||||
recovered, receiveErr := path.receivePayloadObserved(receiveCtx, func(receivedAt time.Time, bytes int) {
|
||||
if len(wireDeliveries) >= wireDeliveryLimit {
|
||||
wireOverflow = true
|
||||
return
|
||||
}
|
||||
wireDeliveries = append(wireDeliveries, qualificationDeliverySample{At: receivedAt, Bytes: int64(bytes)})
|
||||
})
|
||||
if receiveErr != nil {
|
||||
@@ -1601,6 +1628,9 @@ func runQualificationImpairment(t *testing.T, profile qualificationImpairmentPro
|
||||
if received.err != nil {
|
||||
return qualificationImpairmentObservation{}, received.err
|
||||
}
|
||||
if wireOverflow {
|
||||
return qualificationImpairmentObservation{}, errors.New("qualification public-wire observation bound exceeded")
|
||||
}
|
||||
|
||||
var totalLatency, totalJitter, previousLatency time.Duration
|
||||
previousDelivered := -1
|
||||
@@ -1713,12 +1743,33 @@ func runQualificationImpairment(t *testing.T, profile qualificationImpairmentPro
|
||||
if err != nil {
|
||||
return qualificationImpairmentObservation{}, err
|
||||
}
|
||||
wirePath := strings.TrimSuffix(rawPath, ".csv.gz") + "-wire.csv.gz"
|
||||
for _, reduction := range profile.CapacitySteps {
|
||||
step := qualificationCapacityStepObservation(wireDeliveries, stepAt[reduction], media, reduction)
|
||||
step.TransitionAfter = stepAt[reduction].Sub(started)
|
||||
step.RecomputationSource = filepath.Base(wirePath)
|
||||
observation.CapacityStepObservations = append(observation.CapacityStepObservations, step)
|
||||
}
|
||||
if len(profile.CapacitySteps) > 0 {
|
||||
wireEvidence, writeErr := writeQualificationWireSamples(
|
||||
wirePath, started, profile.CapacitySteps, stepAt, wireDeliveries,
|
||||
wireDeliveryLimit+len(profile.CapacitySteps),
|
||||
)
|
||||
if writeErr != nil {
|
||||
return qualificationImpairmentObservation{}, writeErr
|
||||
}
|
||||
observation.RawWireSamples = wireEvidence.Name
|
||||
observation.RawWireSamplesSHA256 = wireEvidence.SHA256
|
||||
observation.RawWireSamplesBytes = wireEvidence.Bytes
|
||||
observation.RawWireRows = wireEvidence.Rows
|
||||
observation.RawWireDeliveryRows = wireEvidence.DeliveryRows
|
||||
observation.RawWireTransitionRows = wireEvidence.TransitionRows
|
||||
observation.RawWireTimebase = qualificationWireTimebase
|
||||
}
|
||||
for _, step := range observation.CapacityStepObservations {
|
||||
if packetCount >= qualificationImpairmentPacketCount &&
|
||||
(step.Convergence > 10*time.Second || step.MaximumFiveSecond > step.FiveSecondCap*105/100) {
|
||||
return qualificationImpairmentObservation{}, fmt.Errorf("capacity step %d failed measured convergence=%s five-second=%d", reduction, step.Convergence, step.MaximumFiveSecond)
|
||||
return qualificationImpairmentObservation{}, fmt.Errorf("capacity step %d failed measured convergence=%s five-second=%d", step.ReductionPercent, step.Convergence, step.MaximumFiveSecond)
|
||||
}
|
||||
}
|
||||
if observation.Delivered+observation.Dropped != observation.Sent ||
|
||||
@@ -2128,6 +2179,100 @@ func qualificationFileSHA256(path string) (string, int64, error) {
|
||||
return hex.EncodeToString(hash.Sum(nil)), size, nil
|
||||
}
|
||||
|
||||
func writeQualificationWireSamples(
|
||||
path string,
|
||||
epoch time.Time,
|
||||
reductions []int,
|
||||
transitions map[int]time.Time,
|
||||
deliveries []qualificationDeliverySample,
|
||||
maximumRows int,
|
||||
) (qualificationWireFileEvidence, error) {
|
||||
type wireRecord struct {
|
||||
kind string
|
||||
reduction int
|
||||
after time.Duration
|
||||
bytes int64
|
||||
}
|
||||
if maximumRows < len(reductions)+len(deliveries) {
|
||||
return qualificationWireFileEvidence{}, errors.New("qualification public-wire row bound exceeded")
|
||||
}
|
||||
records := make([]wireRecord, 0, len(reductions)+len(deliveries))
|
||||
for _, reduction := range reductions {
|
||||
at := transitions[reduction]
|
||||
if at.IsZero() || at.Before(epoch) {
|
||||
return qualificationWireFileEvidence{}, fmt.Errorf("qualification capacity transition %d missing or before epoch", reduction)
|
||||
}
|
||||
records = append(records, wireRecord{kind: "transition", reduction: reduction, after: at.Sub(epoch)})
|
||||
}
|
||||
for _, delivery := range deliveries {
|
||||
if delivery.At.Before(epoch) || delivery.Bytes <= 0 {
|
||||
return qualificationWireFileEvidence{}, errors.New("qualification public-wire delivery invalid")
|
||||
}
|
||||
records = append(records, wireRecord{kind: "delivery", after: delivery.At.Sub(epoch), bytes: delivery.Bytes})
|
||||
}
|
||||
sort.SliceStable(records, func(first, second int) bool {
|
||||
if records[first].after != records[second].after {
|
||||
return records[first].after < records[second].after
|
||||
}
|
||||
return records[first].kind == "transition" && records[second].kind != "transition"
|
||||
})
|
||||
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o640)
|
||||
if err != nil {
|
||||
return qualificationWireFileEvidence{}, err
|
||||
}
|
||||
compressed := gzip.NewWriter(file)
|
||||
buffered := bufio.NewWriter(compressed)
|
||||
writer := csv.NewWriter(buffered)
|
||||
closeAll := func() error {
|
||||
writer.Flush()
|
||||
if err := writer.Error(); err != nil {
|
||||
_ = compressed.Close()
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
if err := buffered.Flush(); err != nil {
|
||||
_ = compressed.Close()
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
if err := compressed.Close(); err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
}
|
||||
if err := writer.Write([]string{"record_type", "reduction_percent", "transition_after_ns", "received_after_ns", "encoded_bytes"}); err != nil {
|
||||
_ = closeAll()
|
||||
return qualificationWireFileEvidence{}, err
|
||||
}
|
||||
evidence := qualificationWireFileEvidence{Name: filepath.Base(path)}
|
||||
for _, record := range records {
|
||||
var row []string
|
||||
switch record.kind {
|
||||
case "transition":
|
||||
row = []string{"transition", strconv.Itoa(record.reduction), strconv.FormatInt(record.after.Nanoseconds(), 10), "", ""}
|
||||
evidence.TransitionRows++
|
||||
case "delivery":
|
||||
row = []string{"delivery", "", "", strconv.FormatInt(record.after.Nanoseconds(), 10), strconv.FormatInt(record.bytes, 10)}
|
||||
evidence.DeliveryRows++
|
||||
default:
|
||||
_ = closeAll()
|
||||
return qualificationWireFileEvidence{}, errors.New("qualification public-wire record type invalid")
|
||||
}
|
||||
if err := writer.Write(row); err != nil {
|
||||
_ = closeAll()
|
||||
return qualificationWireFileEvidence{}, err
|
||||
}
|
||||
}
|
||||
if err := closeAll(); err != nil {
|
||||
return qualificationWireFileEvidence{}, err
|
||||
}
|
||||
evidence.Rows = evidence.TransitionRows + evidence.DeliveryRows
|
||||
evidence.SHA256, evidence.Bytes, err = qualificationFileSHA256(path)
|
||||
return evidence, err
|
||||
}
|
||||
|
||||
type qualificationFlowDelivery struct {
|
||||
at time.Time
|
||||
flow string
|
||||
@@ -2189,7 +2334,7 @@ func runQualificationFleetStage(t *testing.T, fleet *qualificationFleet, profile
|
||||
|
||||
func qualificationPacerEvidence(t *testing.T, rawPath string, baselineDuration, stepDuration time.Duration) (qualificationFairnessEvidence, error) {
|
||||
t.Helper()
|
||||
flows := []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}
|
||||
flows := qualificationFairnessFlows()
|
||||
profile := qualificationMediaProfile{Name: "fairness-h264", Codec: "h264", BitrateKbps: 8000, PacketBytes: 1000}
|
||||
fleet := newQualificationFleet(t, len(flows), profile, 8000)
|
||||
defer fleet.Close()
|
||||
@@ -2246,8 +2391,8 @@ func qualificationPacerEvidence(t *testing.T, rawPath string, baselineDuration,
|
||||
return qualificationFairnessEvidence{}, fmt.Errorf("capacity step %d failed convergence=%s five-second=%d", step.reduction, convergence, maximum)
|
||||
}
|
||||
evidence.CapacitySteps = append(evidence.CapacitySteps, qualificationCapacityStep{
|
||||
ReductionPercent: step.reduction, Convergence: convergence,
|
||||
MaximumFiveSecond: maximum, FiveSecondCap: step.cap * 5,
|
||||
ReductionPercent: step.reduction, TransitionAfter: stepStart.Sub(start), Convergence: convergence,
|
||||
MaximumFiveSecond: maximum, FiveSecondCap: step.cap * 5, RecomputationSource: filepath.Base(rawPath),
|
||||
})
|
||||
}
|
||||
end := start.Add(baselineDuration + 2*stepDuration)
|
||||
@@ -2263,6 +2408,10 @@ func qualificationPacerEvidence(t *testing.T, rawPath string, baselineDuration,
|
||||
return evidence, nil
|
||||
}
|
||||
|
||||
func qualificationFairnessFlows() []string {
|
||||
return []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}
|
||||
}
|
||||
|
||||
func qualificationPacerConvergence(deliveries []qualificationFlowDelivery, start time.Time, flows []string, targetBytesPerSecond int64) time.Duration {
|
||||
consecutive := 0
|
||||
for second := time.Duration(0); second < 10*time.Second; second += time.Second {
|
||||
|
||||
Reference in New Issue
Block a user