github.com/eth-easl/loader@v0.0.0-20230908084258-8a37e1d94279/pkg/generator/specification.go (about)

     1  /*
     2   * MIT License
     3   *
     4   * Copyright (c) 2023 EASL and the vHive community
     5   *
     6   * Permission is hereby granted, free of charge, to any person obtaining a copy
     7   * of this software and associated documentation files (the "Software"), to deal
     8   * in the Software without restriction, including without limitation the rights
     9   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    10   * copies of the Software, and to permit persons to whom the Software is
    11   * furnished to do so, subject to the following conditions:
    12   *
    13   * The above copyright notice and this permission notice shall be included in all
    14   * copies or substantial portions of the Software.
    15   *
    16   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    17   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    18   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    19   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    20   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    21   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    22   * SOFTWARE.
    23   */
    24  
    25  package generator
    26  
    27  import (
    28  	"math/rand"
    29  
    30  	"github.com/eth-easl/loader/pkg/common"
    31  	log "github.com/sirupsen/logrus"
    32  )
    33  
    34  type SpecificationGenerator struct {
    35  	iatRand  *rand.Rand
    36  	specRand *rand.Rand
    37  }
    38  
    39  func NewSpecificationGenerator(seed int64) *SpecificationGenerator {
    40  	return &SpecificationGenerator{
    41  		iatRand:  rand.New(rand.NewSource(seed)),
    42  		specRand: rand.New(rand.NewSource(seed)),
    43  	}
    44  }
    45  
    46  //////////////////////////////////////////////////
    47  // IAT GENERATION
    48  //////////////////////////////////////////////////
    49  
    50  // generateIATPerGranularity generates IAT for one minute based on given number of invocations and the given distribution
    51  func (s *SpecificationGenerator) generateIATPerGranularity(numberOfInvocations int, iatDistribution common.IatDistribution, shiftIAT bool, granularity common.TraceGranularity) ([]float64, float64) {
    52  	if numberOfInvocations == 0 {
    53  		return []float64{}, 0.0
    54  	}
    55  
    56  	var iatResult []float64
    57  	totalDuration := 0.0 // total non-scaled duration
    58  
    59  	for i := 0; i < numberOfInvocations; i++ {
    60  		var iat float64
    61  
    62  		switch iatDistribution {
    63  		case common.Exponential:
    64  			// NOTE: Serverless in the Wild - pg. 6, paragraph 1
    65  			iat = s.iatRand.ExpFloat64()
    66  		case common.Uniform:
    67  			iat = s.iatRand.Float64()
    68  		case common.Equidistant:
    69  			equalDistance := common.OneSecondInMicroseconds / float64(numberOfInvocations)
    70  			if granularity == common.MinuteGranularity {
    71  				equalDistance *= 60.0
    72  			}
    73  
    74  			iat = equalDistance
    75  		default:
    76  			log.Fatal("Unsupported IAT distribution.")
    77  		}
    78  
    79  		if iat == 0 {
    80  			// No nanoseconds-level granularity, only microsecond
    81  			log.Fatal("Generated IAT is equal to zero (unsupported). Consider increasing the clock precision.")
    82  		}
    83  
    84  		iatResult = append(iatResult, iat)
    85  		totalDuration += iat
    86  	}
    87  
    88  	if iatDistribution == common.Uniform || iatDistribution == common.Exponential {
    89  		// Uniform: 		we need to scale IAT from [0, 1) to [0, 60 seconds)
    90  		// Exponential: 	we need to scale IAT from [0, +MaxFloat64) to [0, 60 seconds)
    91  		for i := 0; i < len(iatResult); i++ {
    92  			// how much does the IAT contributes to the total IAT sum
    93  			iatResult[i] = iatResult[i] / totalDuration
    94  			// convert relative contribution to absolute on 60 second interval
    95  			iatResult[i] = iatResult[i] * common.OneSecondInMicroseconds
    96  
    97  			if granularity == common.MinuteGranularity {
    98  				iatResult[i] *= 60.0
    99  			}
   100  		}
   101  	}
   102  
   103  	if shiftIAT {
   104  		// Cut the IAT array at random place to move the first invocation from the beginning of the minute
   105  		split := s.iatRand.Float64() * common.OneSecondInMicroseconds
   106  		if granularity == common.MinuteGranularity {
   107  			split *= 60.0
   108  		}
   109  		sum, i := 0.0, 0
   110  		for ; i < len(iatResult); i++ {
   111  			sum += iatResult[i]
   112  			if sum > split {
   113  				break
   114  			}
   115  		}
   116  		beginningIAT := sum - split
   117  		endIAT := iatResult[i] - beginningIAT
   118  		finalIAT := append([]float64{beginningIAT}, iatResult[i+1:]...)
   119  		finalIAT = append(finalIAT, iatResult[:i]...)
   120  		iatResult = append(finalIAT, endIAT)
   121  	} else {
   122  		iatResult = append([]float64{0.0}, iatResult...)
   123  	}
   124  
   125  	return iatResult, totalDuration
   126  }
   127  
   128  // GenerateIAT generates IAT according to the given distribution. Number of minutes is the length of invocationsPerMinute array
   129  func (s *SpecificationGenerator) generateIAT(invocationsPerMinute []int, iatDistribution common.IatDistribution, shiftIAT bool, granularity common.TraceGranularity) (common.IATMatrix, common.ProbabilisticDuration) {
   130  	var IAT [][]float64
   131  	var nonScaledDuration []float64
   132  
   133  	numberOfMinutes := len(invocationsPerMinute)
   134  	for i := 0; i < numberOfMinutes; i++ {
   135  		minuteIAT, duration := s.generateIATPerGranularity(invocationsPerMinute[i], iatDistribution, shiftIAT, granularity)
   136  
   137  		IAT = append(IAT, minuteIAT)
   138  		nonScaledDuration = append(nonScaledDuration, duration)
   139  	}
   140  
   141  	return IAT, nonScaledDuration
   142  }
   143  
   144  func (s *SpecificationGenerator) GenerateInvocationData(function *common.Function, iatDistribution common.IatDistribution, shiftIAT bool, granularity common.TraceGranularity) *common.FunctionSpecification {
   145  	invocationsPerMinute := function.InvocationStats.Invocations
   146  
   147  	// Generating IAT
   148  	iat, rawDuration := s.generateIAT(invocationsPerMinute, iatDistribution, shiftIAT, granularity)
   149  
   150  	// Generating runtime specifications
   151  	var runtimeMatrix common.RuntimeSpecificationMatrix
   152  	for i := 0; i < len(invocationsPerMinute); i++ {
   153  		var row []common.RuntimeSpecification
   154  
   155  		for j := 0; j < invocationsPerMinute[i]; j++ {
   156  			row = append(row, s.generateExecutionSpecs(function))
   157  		}
   158  
   159  		runtimeMatrix = append(runtimeMatrix, row)
   160  	}
   161  
   162  	return &common.FunctionSpecification{
   163  		IAT:                  iat,
   164  		RawDuration:          rawDuration,
   165  		RuntimeSpecification: runtimeMatrix,
   166  	}
   167  }
   168  
   169  //////////////////////////////////////////////////
   170  // RUNTIME AND MEMORY GENERATION
   171  //////////////////////////////////////////////////
   172  
   173  // Choose a random number in between. Not thread safe.
   174  func (s *SpecificationGenerator) randIntBetween(min, max float64) int {
   175  	intMin, intMax := int(min), int(max)
   176  
   177  	if intMax < intMin {
   178  		log.Fatal("Invalid runtime/memory specification.")
   179  	}
   180  
   181  	if intMax == intMin {
   182  		return intMin
   183  	} else {
   184  		return s.specRand.Intn(intMax-intMin) + intMin
   185  	}
   186  }
   187  
   188  // Should be called only when specRand is locked with its mutex
   189  func (s *SpecificationGenerator) determineExecutionSpecSeedQuantiles() (float64, float64) {
   190  	//* Generate uniform quantiles in [0, 1).
   191  	runQtl := s.specRand.Float64()
   192  	memQtl := s.specRand.Float64()
   193  
   194  	return runQtl, memQtl
   195  }
   196  
   197  // Should be called only when specRand is locked with its mutex
   198  func (s *SpecificationGenerator) generateExecuteSpec(runQtl float64, runStats *common.FunctionRuntimeStats) (runtime int) {
   199  	switch {
   200  	case runQtl == 0:
   201  		runtime = int(runStats.Percentile0)
   202  	case runQtl <= 0.01:
   203  		runtime = s.randIntBetween(runStats.Percentile0, runStats.Percentile1)
   204  	case runQtl <= 0.25:
   205  		runtime = s.randIntBetween(runStats.Percentile1, runStats.Percentile25)
   206  	case runQtl <= 0.50:
   207  		runtime = s.randIntBetween(runStats.Percentile25, runStats.Percentile50)
   208  	case runQtl <= 0.75:
   209  		runtime = s.randIntBetween(runStats.Percentile50, runStats.Percentile75)
   210  	case runQtl <= 0.99:
   211  		runtime = s.randIntBetween(runStats.Percentile75, runStats.Percentile99)
   212  	case runQtl < 1:
   213  		runtime = s.randIntBetween(runStats.Percentile99, runStats.Percentile100)
   214  	}
   215  
   216  	return runtime
   217  }
   218  
   219  // Should be called only when specRand is locked with its mutex
   220  func (s *SpecificationGenerator) generateMemorySpec(memQtl float64, memStats *common.FunctionMemoryStats) (memory int) {
   221  	switch {
   222  	case memQtl <= 0.01:
   223  		memory = int(memStats.Percentile1)
   224  	case memQtl <= 0.05:
   225  		memory = s.randIntBetween(memStats.Percentile1, memStats.Percentile5)
   226  	case memQtl <= 0.25:
   227  		memory = s.randIntBetween(memStats.Percentile5, memStats.Percentile25)
   228  	case memQtl <= 0.50:
   229  		memory = s.randIntBetween(memStats.Percentile25, memStats.Percentile50)
   230  	case memQtl <= 0.75:
   231  		memory = s.randIntBetween(memStats.Percentile50, memStats.Percentile75)
   232  	case memQtl <= 0.95:
   233  		memory = s.randIntBetween(memStats.Percentile75, memStats.Percentile95)
   234  	case memQtl <= 0.99:
   235  		memory = s.randIntBetween(memStats.Percentile95, memStats.Percentile99)
   236  	case memQtl < 1:
   237  		memory = s.randIntBetween(memStats.Percentile99, memStats.Percentile100)
   238  	}
   239  
   240  	return memory
   241  }
   242  
   243  func (s *SpecificationGenerator) generateExecutionSpecs(function *common.Function) common.RuntimeSpecification {
   244  	runStats, memStats := function.RuntimeStats, function.MemoryStats
   245  	if runStats.Count <= 0 || memStats.Count <= 0 {
   246  		log.Fatal("Invalid duration or memory specification of the function '" + function.Name + "'.")
   247  	}
   248  
   249  	runQtl, memQtl := s.determineExecutionSpecSeedQuantiles()
   250  	runtime := common.MinOf(common.MaxExecTimeMilli, common.MaxOf(common.MinExecTimeMilli, s.generateExecuteSpec(runQtl, runStats)))
   251  	memory := common.MinOf(common.MaxMemQuotaMib, common.MaxOf(common.MinMemQuotaMib, s.generateMemorySpec(memQtl, memStats)))
   252  
   253  	return common.RuntimeSpecification{
   254  		Runtime: runtime,
   255  		Memory:  memory,
   256  	}
   257  }