diff --git a/lib/execution_segment.go b/lib/execution_segment.go index 5404133c491..f774147033e 100644 --- a/lib/execution_segment.go +++ b/lib/execution_segment.go @@ -623,6 +623,17 @@ func (essw *ExecutionSegmentSequenceWrapper) GetStripedOffsets(segmentIndex int) return jumps[0], offsets, essw.lcd } +// GetStripedJumps returns the stripped jumps for the given segment +// the returned values are as follows in order: +// - jumps: a list of jumps from the beginning value for the segment. This are only the jumps +// to from the start to the next start if we chunk the elements we are going to strip +// into lcd sized chunks +// - lcd: the LCD of the lengths of all segments in the sequence. This is also the number of +// elements after which the algorithm starts to loop and give the same values +func (essw *ExecutionSegmentSequenceWrapper) GetStripedJumps(segmentIndex int) ([]int64, int64) { + return essw.jumps[segmentIndex], essw.lcd +} + // GetTuple returns an ExecutionTuple for the specified segment index. func (essw *ExecutionSegmentSequenceWrapper) GetTuple(segmentIndex int) *ExecutionTuple { return &ExecutionTuple{ @@ -723,6 +734,11 @@ func (et *ExecutionTuple) GetStripedOffsets() (int64, []int64, int64) { return et.Sequence.GetStripedOffsets(et.SegmentIndex) } +// GetStripedJumps returns the striped jumps for our execution segment. +func (et *ExecutionTuple) GetStripedJumps() ([]int64, int64) { + return et.Sequence.GetStripedJumps(et.SegmentIndex) +} + // GetNewExecutionTupleFromValue re-segments the sequence, based on the given // value (see GetNewExecutionSegmentSequenceFromValue() above), and either // returns the new tuple, or an error if the current segment isn't present in diff --git a/lib/executor/variable_looping_vus.go b/lib/executor/variable_looping_vus.go index 9166132af8d..aeb0c2bd312 100644 --- a/lib/executor/variable_looping_vus.go +++ b/lib/executor/variable_looping_vus.go @@ -186,8 +186,9 @@ func (vlvc VariableLoopingVUsConfig) getRawExecutionSteps(et *lib.ExecutionTuple timeTillEnd time.Duration fromVUs = vlvc.StartVUs.Int64 start, offsets, lcd = et.GetStripedOffsets() + jumps, _ = et.GetStripedJumps() steps = make([]lib.ExecutionStep, 0, vlvc.precalculateTheRequiredSteps(et, zeroEnd)) - index = segmentedIndex{start: start, lcd: lcd, offsets: offsets} + index = segmentedIndex{start: start, lcd: lcd, offsets: offsets, jumps: jumps} ) // Reserve the scaled StartVUs at the beginning @@ -248,6 +249,7 @@ func (vlvc VariableLoopingVUsConfig) getRawExecutionSteps(et *lib.ExecutionTuple type segmentedIndex struct { // TODO: rename ... although this is probably the best name so far :D start, lcd int64 offsets []int64 + jumps []int64 scaled, unscaled int64 // for both the first element(vu) is 1 not 0 } @@ -275,7 +277,6 @@ func (s *segmentedIndex) prev() { // goTo sets the scaled index to it's biggest value for which the corresponding unscaled index is // is smaller or equal to value func (s *segmentedIndex) goTo(value int64) int64 { // TODO optimize - var gi int64 // Because of the cyclical nature of the striping algorithm (with a cycle // length of LCD, the least common denominator), when scaling large values // (i.e. many multiples of the LCD), we can quickly calculate how many times @@ -283,28 +284,34 @@ func (s *segmentedIndex) goTo(value int64) int64 { // TODO optimize wholeCycles := (value / s.lcd) // So we can set some approximate initial values quickly, since we also know // precisely how many scaled values there are per cycle length. - s.scaled = wholeCycles * int64(len(s.offsets)) - s.unscaled = wholeCycles*s.lcd + s.start + 1 // our indexes are from 1 the start is from 0 + s.scaled = wholeCycles * int64(len(s.jumps)) + s.unscaled = wholeCycles * s.lcd // our indexes are from 1 the start is from 0 // Approach the final value using the slow algorithm with the step by step loop // TODO: this can be optimized by another array with size offsets that instead of the offsets // from the previous is the offset from either 0 or start - i := s.start - for ; i < value%s.lcd; gi, i = gi+1, i+s.offsets[gi] { - s.scaled++ - s.unscaled += s.offsets[gi] - } - if gi > 0 { // there were more values after the wholecycles - // the last offset actually shouldn't have been added - s.unscaled -= s.offsets[gi-1] - } else if s.scaled > 0 { // we didn't actually have more values after the wholecycles but we still had some + remaining := value % s.lcd + switch { + case s.jumps[0]+1 > remaining: + // we didn't actually have more values after the wholecycles but we still had some // in this case the unscaled value needs to move back by the last offset as it would've been // the one to get it from the value it needs to be to it's current one - s.unscaled -= s.offsets[len(s.offsets)-1] - } - - if s.scaled == 0 { - s.unscaled = 0 // we would've added the start and 1 + if wholeCycles > 0 { + s.unscaled -= s.lcd - s.jumps[len(s.jumps)-1] - 1 + } + default: + i, j := 0, len(s.jumps) + for i < j { + h := int(uint(i+j) >> 1) // avoid overflow when computing h + // i ≤ h < j + if s.jumps[h] < remaining { + i = h + 1 // preserves f(i-1) == false + } else { + j = h // preserves f(j) == true + } + } + s.scaled += int64(i) + s.unscaled += s.jumps[i-1] + 1 } return s.scaled diff --git a/lib/executor/variable_looping_vus_test.go b/lib/executor/variable_looping_vus_test.go index c67794b6fbf..fc2463167e3 100644 --- a/lib/executor/variable_looping_vus_test.go +++ b/lib/executor/variable_looping_vus_test.go @@ -771,6 +771,26 @@ func BenchmarkVariableLoopingVUsGetRawExecutionSteps(b *testing.B) { { name: "normal", stages: `[{"duration":"5m", "target":5000},{"duration":"5m", "target":5000},{"duration":"5m", "target":10000},{"duration":"5m", "target":10000}]`, + }, { + name: "jumpy", + stages: `[{"duration":"5m", "target":5000},{"duration":"0s", "target":0}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":5432}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":0}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":5432}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":0}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":5432}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":0}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":5432}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":0}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":5432}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":0}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":5432}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":0}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":5432}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":0}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":5432}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":0}, + {"duration":"5m", "target":5000},{"duration":"0s", "target":5432}]`, }, { name: "rollercoaster", stages: `[{"duration":"5m", "target":5000},{"duration":"5m", "target":0}, @@ -933,7 +953,7 @@ func TestSegmentedIndex(t *testing.T) { }) t.Run("strange", func(t *testing.T) { - s := segmentedIndex{start: 1, lcd: 7, offsets: []int64{4, 3}} + s := segmentedIndex{start: 1, lcd: 7, offsets: []int64{4, 3}, jumps: []int64{1, 5}} s.next() assert.EqualValues(t, 2, s.unscaled)