knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/test/vegeta/pacers/steady_up_pacer.go (about)

     1  /*
     2  Copyright 2019 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package pacers
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"math"
    23  	"time"
    24  
    25  	vegeta "github.com/tsenart/vegeta/v12/lib"
    26  )
    27  
    28  // steadyUpPacer is a Pacer that describes attack request rates that increases in the beginning then becomes steady.
    29  //
    30  //	Max  |     ,----------------
    31  //	     |    /
    32  //	     |   /
    33  //	     |  /
    34  //	     | /
    35  //	Min -+------------------------------> t
    36  //	     |<-Up->|
    37  type steadyUpPacer struct {
    38  	// upDuration is the duration that attack request rates increase from Min to Max.
    39  	// MUST be larger than 0.
    40  	upDuration time.Duration
    41  	// min is the attack request rates from the beginning.
    42  	// MUST be larger than 0.
    43  	min vegeta.Rate
    44  	// max is the maximum and final steady attack request rates.
    45  	// MUST be larger than Min.
    46  	max vegeta.Rate
    47  
    48  	slope        float64
    49  	minHitsPerNs float64
    50  	maxHitsPerNs float64
    51  }
    52  
    53  // NewSteadyUp returns a new SteadyUpPacer with the given config.
    54  func NewSteadyUp(min, max vegeta.Rate, upDuration time.Duration) (vegeta.Pacer, error) {
    55  	if upDuration <= 0 || min.Freq <= 0 || min.Per <= 0 || max.Freq <= 0 || max.Per <= 0 {
    56  		return nil, errors.New("configuration for this SteadyUpPacer is invalid")
    57  	}
    58  	minHitsPerNs := hitsPerNs(min)
    59  	maxHitsPerNs := hitsPerNs(max)
    60  	if minHitsPerNs >= maxHitsPerNs {
    61  		return nil, errors.New("min rate must be smaller than max rate for SteadyUpPacer")
    62  	}
    63  
    64  	pacer := &steadyUpPacer{
    65  		min:          min,
    66  		max:          max,
    67  		upDuration:   upDuration,
    68  		slope:        (maxHitsPerNs - minHitsPerNs) / float64(upDuration),
    69  		minHitsPerNs: minHitsPerNs,
    70  		maxHitsPerNs: maxHitsPerNs,
    71  	}
    72  	return pacer, nil
    73  }
    74  
    75  // steadyUpPacer satisfies the Pacer interface.
    76  var _ vegeta.Pacer = &steadyUpPacer{}
    77  
    78  // String returns a pretty-printed description of the steadyUpPacer's behaviour.
    79  func (sup *steadyUpPacer) String() string {
    80  	return fmt.Sprintf("Up{%s + %s / %s}, then Steady{%s}", sup.min, sup.max, sup.upDuration, sup.max)
    81  }
    82  
    83  // Pace determines the length of time to sleep until the next hit is sent.
    84  func (sup *steadyUpPacer) Pace(elapsedTime time.Duration, elapsedHits uint64) (time.Duration, bool) {
    85  	expectedHits := sup.hits(elapsedTime)
    86  	if elapsedHits < uint64(expectedHits) {
    87  		// Running behind, send next hit immediately.
    88  		return 0, false
    89  	}
    90  
    91  	// Re-arranging our hits equation to provide a duration given the number of
    92  	// requests sent is non-trivial, so we must solve for the duration numerically.
    93  	nsPerHit := 1 / sup.hitsPerNs(elapsedTime)
    94  	hitsToWait := float64(elapsedHits+1) - expectedHits
    95  	nextHitIn := time.Duration(nsPerHit * hitsToWait)
    96  
    97  	// If we can't converge to an error of <1e-3 within 10 iterations, bail.
    98  	// This rarely even loops for any large Period if hitsToWait is small.
    99  	for range 10 {
   100  		hitsAtGuess := sup.hits(elapsedTime + nextHitIn)
   101  		err := float64(elapsedHits+1) - hitsAtGuess
   102  		if math.Abs(err) < 1e-3 {
   103  			return nextHitIn, false
   104  		}
   105  		nextHitIn = time.Duration(float64(nextHitIn) / (hitsAtGuess - float64(elapsedHits)))
   106  	}
   107  
   108  	return nextHitIn, false
   109  }
   110  
   111  // Rate returns a Pacer's instantaneous hit rate (per seconds) at the given elapsed
   112  // duration of an attack.
   113  func (sup *steadyUpPacer) Rate(elapsedTime time.Duration) float64 {
   114  	return sup.hitsPerNs(elapsedTime) * 1e9
   115  }
   116  
   117  // hits returns the number of expected hits for this pacer during the given time.
   118  func (sup *steadyUpPacer) hits(t time.Duration) float64 {
   119  	// If t is smaller than the upDuration, calculate the hits as a trapezoid.
   120  	if t <= sup.upDuration {
   121  		curtHitsPerNs := sup.hitsPerNs(t)
   122  		return (curtHitsPerNs + sup.minHitsPerNs) / 2.0 * float64(t)
   123  	}
   124  
   125  	// If t is larger than the upDuration, calculate the hits as a trapezoid + a rectangle.
   126  	upHits := (sup.maxHitsPerNs + sup.minHitsPerNs) / 2.0 * float64(sup.upDuration)
   127  	steadyHits := sup.maxHitsPerNs * float64(t-sup.upDuration)
   128  	return upHits + steadyHits
   129  }
   130  
   131  // hitsPerNs returns the attack rate for this pacer at a given time.
   132  func (sup *steadyUpPacer) hitsPerNs(t time.Duration) float64 {
   133  	if t <= sup.upDuration {
   134  		return sup.minHitsPerNs + float64(t)*sup.slope
   135  	}
   136  
   137  	return sup.maxHitsPerNs
   138  }
   139  
   140  // hitsPerNs returns the attack rate this ConstantPacer represents, in
   141  // fractional hits per nanosecond.
   142  func hitsPerNs(cp vegeta.ConstantPacer) float64 {
   143  	return float64(cp.Freq) / float64(cp.Per)
   144  }