forked from grafana/k6
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathexecution_segment.go
732 lines (652 loc) · 26.4 KB
/
execution_segment.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
/*
*
* k6 - a next-generation load testing tool
* Copyright (C) 2019 Load Impact
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package lib
import (
"encoding"
"fmt"
"math/big"
"sort"
"strings"
)
// ExecutionSegment represents a (start, end] partition of the total execution
// work for a specific test. For example, if we want the split the execution of a
// test in 2 different parts, we can split it in two segments (0, 0.5] and (0,5, 1].
//
// We use rational numbers so it's easier to verify the correctness and easier to
// reason about portions of indivisible things, like VUs. This way, we can easily
// split a test in thirds (i.e. (0, 1/3], (1/3, 2/3], (2/3, 1]), without fearing
// that we'll lose a VU along the way...
//
// The most important part is that if work is split between multiple k6 instances,
// each k6 instance can precisely and reproducibly calculate its share of the work,
// just by knowing its own segment. There won't be a need to schedule the
// execution from a master node, or to even know how many other k6 instances are
// running!
type ExecutionSegment struct {
// 0 <= from < to <= 1
from *big.Rat
to *big.Rat
// derived, equals to-from, but pre-calculated here for speed
length *big.Rat
}
// Ensure we implement those interfaces
var (
_ encoding.TextUnmarshaler = &ExecutionSegment{}
_ fmt.Stringer = &ExecutionSegment{}
)
// Helpful "constants" so we don't initialize them in every function call
var (
zeroRat, oneRat = big.NewRat(0, 1), big.NewRat(1, 1) //nolint:gochecknoglobals
oneBigInt, twoBigInt = big.NewInt(1), big.NewInt(2) //nolint:gochecknoglobals
)
// NewExecutionSegment validates the supplied arguments (basically, that 0 <=
// from < to <= 1) and either returns an error, or it returns a
// fully-initialized and usable execution segment.
func NewExecutionSegment(from, to *big.Rat) (*ExecutionSegment, error) {
if from.Cmp(zeroRat) < 0 {
return nil, fmt.Errorf("segment start value should be at least 0 but was %s", from.FloatString(2))
}
if from.Cmp(to) >= 0 {
return nil, fmt.Errorf("segment start(%s) should be less than its end(%s)", from.FloatString(2), to.FloatString(2))
}
if to.Cmp(oneRat) > 0 {
return nil, fmt.Errorf("segment end value shouldn't be more than 1 but was %s", to.FloatString(2))
}
return newExecutionSegment(from, to), nil
}
// newExecutionSegment just creates an ExecutionSegment without validating the arguments
func newExecutionSegment(from, to *big.Rat) *ExecutionSegment {
return &ExecutionSegment{
from: from,
to: to,
length: new(big.Rat).Sub(to, from),
}
}
// stringToRat is a helper function that tries to convert a string to a rational
// number while allowing percentage, decimal, and fraction values.
func stringToRat(s string) (*big.Rat, error) {
if strings.HasSuffix(s, "%") {
num, ok := new(big.Int).SetString(strings.TrimSuffix(s, "%"), 10)
if !ok {
return nil, fmt.Errorf("'%s' is not a valid percentage", s)
}
return new(big.Rat).SetFrac(num, big.NewInt(100)), nil
}
rat, ok := new(big.Rat).SetString(s)
if !ok {
return nil, fmt.Errorf("'%s' is not a valid percentage, decimal, fraction or interval value", s)
}
return rat, nil
}
// NewExecutionSegmentFromString validates the supplied string value and returns
// the newly created ExecutionSegment or and error from it.
//
// We are able to parse both single percentage/float/fraction values, and actual
// (from: to] segments. For the single values, we just treat them as the
// beginning segment - thus the execution segment can be used as a shortcut for
// quickly running an arbitrarily scaled-down version of a test.
//
// The parsing logic is that values with a colon, i.e. ':', are full segments:
// `1/2:3/4`, `0.5:0.75`, `50%:75%`, and even `2/4:75%` should be (1/2, 3/4]
// And values without a colon are the end of a first segment:
// `20%`, `0.2`, and `1/5` should be converted to (0, 1/5]
// empty values should probably be treated as "1", i.e. the whole execution
func NewExecutionSegmentFromString(toStr string) (result *ExecutionSegment, err error) {
from := zeroRat
if toStr == "" {
toStr = "1" // an empty string means a full 0:1 execution segment
}
if strings.ContainsRune(toStr, ':') {
fromToStr := strings.SplitN(toStr, ":", 2)
toStr = fromToStr[1]
if from, err = stringToRat(fromToStr[0]); err != nil {
return nil, err
}
}
to, err := stringToRat(toStr)
if err != nil {
return nil, err
}
return NewExecutionSegment(from, to)
}
// UnmarshalText implements the encoding.TextUnmarshaler interface, so that
// execution segments can be specified as CLI flags, environment variables, and
// JSON strings. It is a wrapper for the NewExecutionFromString() constructor.
func (es *ExecutionSegment) UnmarshalText(text []byte) (err error) {
segment, err := NewExecutionSegmentFromString(string(text))
if err != nil {
return err
}
*es = *segment
return nil
}
func (es *ExecutionSegment) String() string {
if es == nil {
return "0:1"
}
return es.from.RatString() + ":" + es.to.RatString()
}
// MarshalText implements the encoding.TextMarshaler interface, so is used for
// text and JSON encoding of the execution segment.
func (es *ExecutionSegment) MarshalText() ([]byte, error) {
if es == nil {
return nil, nil
}
return []byte(es.String()), nil
}
// FloatLength is a helper method for getting some more human-readable
// information about the execution segment.
func (es *ExecutionSegment) FloatLength() float64 {
if es == nil {
return 1.0
}
res, _ := es.length.Float64()
return res
}
// Split evenly divides the execution segment into the specified number of
// equal consecutive execution sub-segments.
func (es *ExecutionSegment) Split(numParts int64) ([]*ExecutionSegment, error) {
if numParts < 1 {
return nil, fmt.Errorf("the number of parts should be at least 1, %d received", numParts)
}
from, to := zeroRat, oneRat
if es != nil {
from, to = es.from, es.to
}
increment := new(big.Rat).Sub(to, from)
increment.Denom().Mul(increment.Denom(), big.NewInt(numParts))
results := make([]*ExecutionSegment, numParts)
for i := int64(0); i < numParts; i++ {
segmentTo := new(big.Rat).Add(from, increment)
segment, err := NewExecutionSegment(from, segmentTo)
if err != nil {
return nil, err
}
results[i] = segment
from = segmentTo
}
if from.Cmp(to) != 0 {
return nil, fmt.Errorf("expected %s and %s to be equal", from, to)
}
return results, nil
}
// Equal returns true only if the two execution segments have the same from and
// to values.
func (es *ExecutionSegment) Equal(other *ExecutionSegment) bool {
if es == other {
return true
}
thisFrom, otherFrom, thisTo, otherTo := zeroRat, zeroRat, oneRat, oneRat
if es != nil {
thisFrom, thisTo = es.from, es.to
}
if other != nil {
otherFrom, otherTo = other.from, other.to
}
return thisFrom.Cmp(otherFrom) == 0 && thisTo.Cmp(otherTo) == 0
}
// SubSegment returns a new execution sub-segment - if a is (1/2:1] and b is
// (0:1/2], then a.SubSegment(b) will return a new segment (1/2, 3/4].
//
// The basic formula for c = a.SubSegment(b) is:
// c.from = a.from + b.from * (a.to - a.from)
// c.to = c.from + (b.to - b.from) * (a.to - a.from)
func (es *ExecutionSegment) SubSegment(child *ExecutionSegment) *ExecutionSegment {
if child == nil {
return es // 100% sub-segment is the original segment
}
parentFrom, parentLength := zeroRat, oneRat
if es != nil {
parentFrom, parentLength = es.from, es.length
}
resultFrom := new(big.Rat).Mul(parentLength, child.from)
resultFrom.Add(resultFrom, parentFrom)
resultLength := new(big.Rat).Mul(parentLength, child.length)
return &ExecutionSegment{
from: resultFrom,
length: resultLength,
to: new(big.Rat).Add(resultFrom, resultLength),
}
}
// helper function for rounding (up) of rational numbers to big.Int values
func roundUp(rat *big.Rat) *big.Int {
quo, rem := new(big.Int).QuoRem(rat.Num(), rat.Denom(), new(big.Int))
if rem.Mul(rem, twoBigInt).Cmp(rat.Denom()) >= 0 {
return quo.Add(quo, oneBigInt)
}
return quo
}
// Scale proportionally scales the supplied value, according to the execution
// segment's position and size of the work.
func (es *ExecutionSegment) Scale(value int64) int64 {
if es == nil { // no execution segment, i.e. 100%
return value
}
// Instead of the first proposal that used remainders and floor:
// floor( (value * from) % 1 + value * length )
// We're using an alternative approach with rounding that (hopefully) has
// the same properties, but it's simpler and has better precision:
// round( (value * from) - round(value * from) + (value * (to - from)) )?
// which reduces to:
// round( (value * to) - round(value * from) )?
toValue := big.NewRat(value, 1)
toValue.Mul(toValue, es.to)
fromValue := big.NewRat(value, 1)
fromValue.Mul(fromValue, es.from)
toValue.Sub(toValue, new(big.Rat).SetFrac(roundUp(fromValue), oneBigInt))
return roundUp(toValue).Int64()
}
// InPlaceScaleRat scales rational numbers in-place - it changes the passed
// argument (and also returns it, to allow for chaining, like many other big.Rat
// methods).
func (es *ExecutionSegment) InPlaceScaleRat(value *big.Rat) *big.Rat {
if es == nil { // no execution segment, i.e. 100%
return value
}
return value.Mul(value, es.length)
}
// CopyScaleRat scales rational numbers without changing them - creates a new
// bit.Rat object and uses it for the calculation.
func (es *ExecutionSegment) CopyScaleRat(value *big.Rat) *big.Rat {
if es == nil { // no execution segment, i.e. 100%
return value
}
return new(big.Rat).Mul(value, es.length)
}
// ExecutionSegmentSequence represents an ordered chain of execution segments,
// where the end of one segment is the beginning of the next. It can serialized
// as a comma-separated string of rational numbers "r1,r2,r3,...,rn", which
// represents the sequence (r1, r2], (r2, r3], (r3, r4], ..., (r{n-1}, rn].
// The empty value should be treated as if there is a single (0, 1] segment.
type ExecutionSegmentSequence []*ExecutionSegment
// NewExecutionSegmentSequence validates the that the supplied execution
// segments are non-overlapping and without gaps. It will return a new execution
// segment sequence if that is true, and an error if it's not.
func NewExecutionSegmentSequence(segments ...*ExecutionSegment) (ExecutionSegmentSequence, error) {
if len(segments) > 1 {
to := segments[0].to
for i, segment := range segments[1:] {
if segment.from.Cmp(to) != 0 {
return nil, fmt.Errorf(
"the start value %s of segment #%d should be equal to the end value of the previous one, but it is %s",
segment.from, i+1, to,
)
}
to = segment.to
}
}
return ExecutionSegmentSequence(segments), nil
}
// NewExecutionSegmentSequenceFromString parses strings of the format
// "r1,r2,r3,...,rn", which represents the sequences like (r1, r2], (r2, r3],
// (r3, r4], ..., (r{n-1}, rn].
func NewExecutionSegmentSequenceFromString(strSeq string) (ExecutionSegmentSequence, error) {
if len(strSeq) == 0 {
return nil, nil
}
points := strings.Split(strSeq, ",")
if len(points) < 2 {
return nil, fmt.Errorf("at least 2 points are needed for an execution segment sequence, %d given", len(points))
}
var start *big.Rat
segments := make([]*ExecutionSegment, 0, len(points)-1)
for i, point := range points {
rat, err := stringToRat(point)
if err != nil {
return nil, err
}
if i == 0 {
start = rat
continue
}
segment, err := NewExecutionSegment(start, rat)
if err != nil {
return nil, err
}
segments = append(segments, segment)
start = rat
}
return NewExecutionSegmentSequence(segments...)
}
// UnmarshalText implements the encoding.TextUnmarshaler interface, so that
// execution segment sequences can be specified as CLI flags, environment
// variables, and JSON strings.
func (ess *ExecutionSegmentSequence) UnmarshalText(text []byte) (err error) {
seq, err := NewExecutionSegmentSequenceFromString(string(text))
if err != nil {
return err
}
*ess = seq
return nil
}
// MarshalText implements the encoding.TextMarshaler interface, so is used for
// text and JSON encoding of the execution segment sequences.
func (ess ExecutionSegmentSequence) MarshalText() ([]byte, error) {
return []byte(ess.String()), nil
}
// String just implements the fmt.Stringer interface, encoding the sequence of
// segments as "start1,end1,end2,end3,...,endn".
func (ess ExecutionSegmentSequence) String() string {
result := make([]string, 0, len(ess)+1)
for i, s := range ess {
if i == 0 {
result = append(result, s.from.RatString())
}
result = append(result, s.to.RatString())
}
return strings.Join(result, ",")
}
// LCD calculates the lowest common denominator of the sequence.
// https://en.wikipedia.org/wiki/Least_common_multiple#Using_the_greatest_common_divisor
func (ess ExecutionSegmentSequence) LCD() int64 {
acc := ess[0].length.Denom().Int64()
var n int64
for _, seg := range ess[1:] {
n = seg.length.Denom().Int64()
if acc == n || acc%n == 0 { // short circuit
continue
}
acc *= (n / gcd(acc, n))
}
return acc
}
// Greatest common divisor
// https://en.wikipedia.org/wiki/Euclidean_algorithm
func gcd(a, b int64) int64 {
for a != b {
if a > b {
a -= b
} else {
b -= a
}
}
return a
}
// IsFull returns whether the sequences is full, that is, whether it starts at 0
// and ends at 1. Use GetFilledExecutionSegmentSequence() to get a full sequence.
func (ess ExecutionSegmentSequence) IsFull() bool {
return ess != nil && len(ess) != 0 && ess[0].from.Cmp(zeroRat) == 0 && ess[len(ess)-1].to.Cmp(oneRat) == 0
}
// FindSegmentPosition returns the index of the supplied execution segment in
// the sequence, or an error if the segment isn't present. This shouldn't be
// used on a nil or empty sequence, it's best to use this method on the result
// of GetFilledExecutionSegmentSequence().
func (ess ExecutionSegmentSequence) FindSegmentPosition(segment *ExecutionSegment) (int, error) {
from := zeroRat
if segment != nil {
from = segment.from
}
index := sort.Search(len(ess), func(i int) bool {
return ess[i].from.Cmp(from) >= 0
})
if index < 0 || index >= len(ess) || !ess[index].Equal(segment) {
return -1, fmt.Errorf("couldn't find segment %s in sequence %s", segment, ess)
}
return index, nil
}
// GetFilledExecutionSegmentSequence makes sure we don't have any gaps in the
// given execution segment sequence, or a nil one. It makes sure that the whole
// 0-1 range is filled.
func GetFilledExecutionSegmentSequence(
sequence *ExecutionSegmentSequence, fallback *ExecutionSegment,
) (result ExecutionSegmentSequence) {
if sequence == nil || len(*sequence) == 0 {
if fallback == nil || fallback.length.Cmp(oneRat) == 0 {
// There is no sequence or a segment, so it means the whole test run
// is being planned/executed. So we make sure not to have a nil
// sequence, returning a full; "0,1" sequence instead, otherwise we
// will need to check for nil everywhere...
return ExecutionSegmentSequence{newExecutionSegment(zeroRat, oneRat)}
}
// We don't have a sequence, but we have a defined segment, so we
// fill around it with the missing pieces for a full sequence.
result = ExecutionSegmentSequence{fallback}
} else {
result = *sequence
}
if result[0].from.Cmp(zeroRat) != 0 {
es := newExecutionSegment(zeroRat, result[0].from)
result = append(ExecutionSegmentSequence{es}, result...)
}
if result[len(result)-1].to.Cmp(oneRat) != 0 {
es := newExecutionSegment(result[len(result)-1].to, oneRat)
result = append(result, es)
}
return result
}
// ExecutionSegmentSequenceWrapper is a caching layer on top of the execution
// segment sequence that allows us to make fast and useful calculations, after
// a somewhat slow initialization.
type ExecutionSegmentSequenceWrapper struct {
ExecutionSegmentSequence // a filled-out segment sequence
lcd int64 // pre-calculated least common denominator
// The striped offsets, i.e. the repeating indexes that "belong" to each
// execution segment in the sequence.
offsets [][]int64
}
// NewExecutionSegmentSequenceWrapper expects a filled-out execution segment
// sequence. It pre-calculates the initial caches of and returns a new
// ExecutionSegmentSequenceWrapper, but doesn't calculate the striped offsets.
func NewExecutionSegmentSequenceWrapper(ess ExecutionSegmentSequence) *ExecutionSegmentSequenceWrapper {
if !ess.IsFull() {
panic(fmt.Sprintf("Cannot wrap around a non-full execution segment sequence '%s'", ess))
}
sequenceLength := len(ess)
offsets := make([][]int64, sequenceLength)
lcd := ess.LCD()
// This will contain the normalized numerator values (i.e. what they would have
// been if all denominators were equal to the LCD), sorted in descending
// order (i.e. biggest segments are first), with references to their actual
// indexes in the execution segment sequence (i.e. `seq` above).
sortedNormalizedIndexes := make([]struct {
normNumerator int64
originalIndex int
}, sequenceLength)
for i := range ess {
normalizedNumerator := ess[i].length.Num().Int64() * (lcd / ess[i].length.Denom().Int64())
sortedNormalizedIndexes[i].normNumerator = normalizedNumerator
sortedNormalizedIndexes[i].originalIndex = i
offsets[i] = make([]int64, 0, normalizedNumerator+1)
}
sort.SliceStable(sortedNormalizedIndexes, func(i, j int) bool {
return sortedNormalizedIndexes[i].normNumerator > sortedNormalizedIndexes[j].normNumerator
})
// This is the striping algorithm. Imagine you have a number of rational
// numbers which all add up to 1 (or less), and call them segments. If you
// want each to get proportional amount of anything, you need to give them
// their numerator count of elements for each denominator amount from the
// original elements. So, for 1/3, you give 1 element for each 3 elements.
// For 3/5 - 3 elements for each 5. If you have, for example, a sequence
// with elements with length 3/5 and 1/3, in order to know how to distribute
// it accurately, you need to get the LCD(lowest common denominitor). In
// this case, between 3 and 5, the LCD is 15. Then to transform the numbers
// to have the same, LCD equal, denominator. So 3/5 becomes 9/15 and 1/3
// becomes 5/15. So now for each 15 elements 9 need to go to the 3/5, and 5
// need to go to 1/3. This is what we did above in sortedNormalizedIndexes.
//
// We use the algorithm below to split elements between ExecutionSegments by
// using their length as the rational number. As we would like to get
// non-sequential elements, we try to get the maximum distance between them.
// That is the number of elements divided by the number of elements for any
// given segment, which concidently is the length of the segment reversed.
// The algorithm below does the following:
// 1. Goes through the elements from 0 to the lcd-1
// 2. For each of element, it goes through the segments and looks if the
// amount of already taken elements by the given segment, multiplied by
// that segment's length inverted, is equal to or less to the current
// element index. If it is, give that element to that segment. If not,
// continue with the next element.
// The code below specifically avoids using big.Rat, for performance
// reasons, which complicates the code somewhat. As additional note, the
// sorting of the segments from biggest to smallest helps with the fact that
// the biggest elements will need to take the most elements, and for them it
// will be the hardest to not get sequential elements.
prev := make([]int64, sequenceLength)
chosenCounts := make([]int64, sequenceLength)
saveIndex := func(iteration int64, index int, numerator int64) {
offsets[index] = append(offsets[index], iteration-prev[index])
prev[index] = iteration
if int64(len(offsets[index])) == numerator {
offsets[index] = append(offsets[index], offsets[index][0]+lcd-iteration)
}
}
for i := int64(0); i < lcd; i++ {
for sortedIndex, chosenCount := range chosenCounts {
num := chosenCount * lcd
denom := sortedNormalizedIndexes[sortedIndex].normNumerator
if i > num/denom || (i == num/denom && num%denom == 0) {
chosenCounts[sortedIndex]++
saveIndex(i, sortedNormalizedIndexes[sortedIndex].originalIndex, denom)
break
}
}
}
return &ExecutionSegmentSequenceWrapper{ExecutionSegmentSequence: ess, lcd: lcd, offsets: offsets}
}
// LCD returns the (cached) least common denominator of the sequence - no need
// to calculate it again, since we did it in the constructor.
func (essw *ExecutionSegmentSequenceWrapper) LCD() int64 {
return essw.lcd
}
// ScaleInt64 scales the provided value for the given segment.
func (essw *ExecutionSegmentSequenceWrapper) ScaleInt64(segmentIndex int, value int64) int64 {
start := essw.offsets[segmentIndex][0]
offsets := essw.offsets[segmentIndex][1:]
result := (value / essw.lcd) * int64(len(offsets))
for gi, i := 0, start; i < value%essw.lcd; gi, i = gi+1, i+offsets[gi] {
result++
}
return result
}
// GetStripedOffsets returns the stripped offsets for the given segment
// the returned values are as follows in order:
// - start: the first value that is for the segment
// - offsets: a list of offsets from the previous value for the segment. This are only the offsets
// 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) GetStripedOffsets(segmentIndex int) (int64, []int64, int64) {
offsets := essw.offsets[segmentIndex]
return offsets[0], offsets[1:], essw.lcd
}
// GetTuple returns an ExecutionTuple for the specified segment index.
func (essw *ExecutionSegmentSequenceWrapper) GetTuple(segmentIndex int) *ExecutionTuple {
return &ExecutionTuple{
Sequence: essw,
Segment: essw.ExecutionSegmentSequence[segmentIndex],
SegmentIndex: segmentIndex,
}
}
// GetNewExecutionSegmentSequenceFromValue uses the value provided, splits it
// between all the segments, using the striping offsets in the sequence,
// generating a new segment sequence. It then returns a new
// ExecutionSegmentSequenceWrapper, with the new sequence and segments, such
// that each new segment in the new sequence has length `Scale(value)/value`
// while keeping the order.
//
// Additionally, the position of a given segment index can be tracked (since
// empty segments are removed), so that you can reconstruct an ExecutionTuple,
// if required. If the segment with the trackedIndex is not part of the new
// sequence, or if a new sequence cannot be generated (for example, for 0
// values), an error will be returned.
func (essw *ExecutionSegmentSequenceWrapper) GetNewExecutionSegmentSequenceFromValue(value int64, trackedIndex int) (
newSequence *ExecutionSegmentSequenceWrapper, newIndex int, err error,
) {
if value < 1 {
return nil, -1, fmt.Errorf("cannot generate new sequence for value %d", value)
}
if value%essw.lcd == 0 { // the value is perfectly divisible so we will get the same tuple
return essw, trackedIndex, nil
}
newIndex = -1
newESS := make(ExecutionSegmentSequence, 0, len(essw.ExecutionSegmentSequence)) // this can be smaller
prev := int64(0)
for i := range essw.ExecutionSegmentSequence {
newValue := essw.ScaleInt64(i, value)
if newValue == 0 {
continue
}
currentES := newExecutionSegment(big.NewRat(prev, value), big.NewRat(prev+newValue, value))
prev += newValue
if i == trackedIndex {
newIndex = len(newESS)
}
newESS = append(newESS, currentES)
}
if newIndex == -1 {
return nil, -1, fmt.Errorf(
"segment %d (%s) isn't present in the new sequence",
trackedIndex, essw.ExecutionSegmentSequence[trackedIndex],
)
}
return NewExecutionSegmentSequenceWrapper(newESS), newIndex, nil
}
// ExecutionTuple is the combination of an ExecutionSegmentSequence(Wrapper) and
// a specific ExecutionSegment from it. It gives easy access to the efficient
// scaling and striping algorithms for that specific segment, since the results
// are cached in the sequence wrapper.
type ExecutionTuple struct { // TODO rename? make fields private and have getter methods?
Sequence *ExecutionSegmentSequenceWrapper
Segment *ExecutionSegment
SegmentIndex int
}
func (et *ExecutionTuple) String() string {
return fmt.Sprintf("%s in %s", et.Segment, et.Sequence)
}
// NewExecutionTuple returns a new ExecutionTuple for the provided segment and
// sequence.
//
// TODO: don't return a pointer?
func NewExecutionTuple(segment *ExecutionSegment, sequence *ExecutionSegmentSequence) (*ExecutionTuple, error) {
filledSeq := GetFilledExecutionSegmentSequence(sequence, segment)
wrapper := NewExecutionSegmentSequenceWrapper(filledSeq)
index, err := wrapper.FindSegmentPosition(segment)
if err != nil {
return nil, err
}
return &ExecutionTuple{Sequence: wrapper, Segment: segment, SegmentIndex: index}, nil
}
// ScaleInt64 scales the provided value for our execution segment.
func (et *ExecutionTuple) ScaleInt64(value int64) int64 {
if len(et.Sequence.ExecutionSegmentSequence) == 1 {
return value // if we don't have any segmentation, just return the original value
}
return et.Sequence.ScaleInt64(et.SegmentIndex, value)
}
// GetStripedOffsets returns the striped offsets for our execution segment.
func (et *ExecutionTuple) GetStripedOffsets() (int64, []int64, int64) {
return et.Sequence.GetStripedOffsets(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
// the new sequence.
func (et *ExecutionTuple) GetNewExecutionTupleFromValue(value int64) (*ExecutionTuple, error) {
newSequenceWrapper, newIndex, err := et.Sequence.GetNewExecutionSegmentSequenceFromValue(value, et.SegmentIndex)
if err != nil {
return nil, err
}
return &ExecutionTuple{
Sequence: newSequenceWrapper,
Segment: newSequenceWrapper.ExecutionSegmentSequence[newIndex],
SegmentIndex: newIndex,
}, nil
}