From 7aed1e459c3dd1e535532b795f796ca9f4c663cc Mon Sep 17 00:00:00 2001 From: Marcel Becker <stu117960@mail.uni-kiel.de> Date: Tue, 8 Feb 2022 15:33:09 +0100 Subject: [PATCH] Finished capacity metric support for search strategies --- theodolite/crd/crd-execution.yaml | 36 ++++----- .../strategies/searchstrategy/BinarySearch.kt | 63 ++++++++++++--- .../InitialGuessSearchStrategy.kt | 80 +++++++++++++++++-- .../searchstrategy/PrevResourceMinGuess.kt | 1 + .../searchstrategy/SearchStrategy.kt | 1 - .../main/kotlin/theodolite/util/Results.kt | 45 +++++++++-- .../k8s-resource-files/test-execution.yaml | 10 ++- 7 files changed, 191 insertions(+), 45 deletions(-) diff --git a/theodolite/crd/crd-execution.yaml b/theodolite/crd/crd-execution.yaml index f2319269e..3e87df678 100644 --- a/theodolite/crd/crd-execution.yaml +++ b/theodolite/crd/crd-execution.yaml @@ -81,25 +81,25 @@ spec: description: Defines the overall parameter for the execution. type: object required: ["strategy", "duration", "repetitions", "restrictions"] - metric: - default: "demand" - type: string - oneOf: - - "demand" - - "capacity" - strategy: - description: Defines the used strategy for the execution, either 'LinearSearch', 'BinarySearch' or 'InitialGuessSearch' - type: object - name: string - properties: - restrictions: - description: List of restriction strategies used to delimit the search space. - type: array - items: - type: string - guessStrategy: string - searchStrategy: string properties: + metric: + default: "demand" + type: string + oneOf: + - "demand" + - "capacity" + strategy: + description: Defines the used strategy for the execution, either 'LinearSearch', 'BinarySearch' or 'InitialGuessSearch' + type: object + name: string + properties: + restrictions: + description: List of restriction strategies used to delimit the search space. + type: array + items: + type: string + guessStrategy: string + searchStrategy: string duration: description: Defines the duration of each experiment in seconds. type: integer diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/BinarySearch.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/BinarySearch.kt index 8aaa893b0..08934bb5d 100644 --- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/BinarySearch.kt +++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/BinarySearch.kt @@ -12,7 +12,7 @@ private val logger = KotlinLogging.logger {} */ class BinarySearch(benchmarkExecutor: BenchmarkExecutor) : SearchStrategy(benchmarkExecutor) { override fun findSuitableResource(load: Int, resources: List<Int>): Int? { - val result = binarySearch(load, resources, 0, resources.size - 1, true) + val result = binarySearchDemand(load, resources, 0, resources.size - 1) if (result == -1) { return null } @@ -20,24 +20,26 @@ class BinarySearch(benchmarkExecutor: BenchmarkExecutor) : SearchStrategy(benchm } override fun findSuitableLoad(resource: Int, loads: List<Int>): Int? { - // TODO - return null + val result = binarySearchCapacity(resource, loads, 0, loads.size - 1) + if (result == -1) { + return null + } + return loads[result] } /** - * Apply binary search. + * Apply binary search for metric demand. * * @param load the load dimension to perform experiments for * @param resources the list in which binary search is performed * @param lower lower bound for binary search (inclusive) * @param upper upper bound for binary search (inclusive) */ - private fun binarySearch(load: Int, resources: List<Int>, lower: Int, upper: Int, - demand: Boolean): Int { + private fun binarySearchDemand(load: Int, resources: List<Int>, lower: Int, upper: Int): Int { if (lower > upper) { throw IllegalArgumentException() } - // special case: length == 1 or 2 + // special case: length == 1, so lower and upper bounds are the same if (lower == upper) { val res = resources[lower] logger.info { "Running experiment with load '${load}' and resources '$res'" } @@ -48,17 +50,58 @@ class BinarySearch(benchmarkExecutor: BenchmarkExecutor) : SearchStrategy(benchm } } else { // apply binary search for a list with - // length > 2 and adjust upper and lower depending on the result for `resources[mid]` + // length >= 2 and adjust upper and lower depending on the result for `resources[mid]` val mid = (upper + lower) / 2 val res = resources[mid] logger.info { "Running experiment with load '${load}' and resources '$res'" } if (this.benchmarkExecutor.runExperiment(load, resources[mid])) { + // case length = 2 if (mid == lower) { return lower } - return binarySearch(load, resources, lower, mid - 1, demand) + return binarySearchDemand(load, resources, lower, mid - 1) + } else { + return binarySearchDemand(load, resources, mid + 1, upper) + } + } + } + + + /** + * Apply binary search for metric capacity. + * + * @param resource the load dimension to perform experiments for + * @param loads the list in which binary search is performed + * @param lower lower bound for binary search (inclusive) + * @param upper upper bound for binary search (inclusive) + */ + private fun binarySearchCapacity(resource: Int, loads: List<Int>, lower: Int, upper: Int): Int { + if (lower > upper) { + throw IllegalArgumentException() + } + // length = 1, so lower and upper bounds are the same + if (lower == upper) { + val res = loads[lower] + logger.info { "Running experiment with load '$resource' and resources '$res'" } + if (this.benchmarkExecutor.runExperiment(resource, loads[lower])) return lower + else { + if (lower + 1 == loads.size) return -1 + return lower - 1 + } + } else { + // apply binary search for a list with + // length > 2 and adjust upper and lower depending on the result for `resources[mid]` + val mid = (upper + lower + 1) / 2 //round to next int + val res = loads[mid] + logger.info { "Running experiment with load '$resource' and resources '$res'" } + if (this.benchmarkExecutor.runExperiment(resource, loads[mid])) { + // length = 2, so since we round down mid is equal to lower + if (mid == upper) { + return upper + } + return binarySearchCapacity(resource, loads, mid + 1, upper) } else { - return binarySearch(load, resources, mid + 1, upper, demand) + return binarySearchCapacity(resource, loads, lower, mid - 1) } } } diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt index 7df3f9013..d8c5c1c4e 100644 --- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt +++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt @@ -35,7 +35,6 @@ class InitialGuessSearchStrategy(benchmarkExecutor: BenchmarkExecutor, guessStra return null } - var lastLowestResource : Int? = null // Getting the lastLowestResource from results and calling firstGuess() with it @@ -50,7 +49,7 @@ class InitialGuessSearchStrategy(benchmarkExecutor: BenchmarkExecutor, guessStra val resourcesToCheck: List<Int> val startIndex: Int = resources.indexOf(lastLowestResource) - logger.info { "Running experiment with load '${load}' and resources '$lastLowestResource'" } + logger.info { "Running experiment with load '$load' and resources '$lastLowestResource'" } // If the first experiment passes, starting downward linear search // otherwise starting upward linear search @@ -62,7 +61,7 @@ class InitialGuessSearchStrategy(benchmarkExecutor: BenchmarkExecutor, guessStra var currentMin: Int = lastLowestResource for (res in resourcesToCheck) { - logger.info { "Running experiment with load '${load}' and resources '$res'" } + logger.info { "Running experiment with load '$load' and resources '$res'" } if (this.benchmarkExecutor.runExperiment(load, res)) { currentMin = res } @@ -78,19 +77,86 @@ class InitialGuessSearchStrategy(benchmarkExecutor: BenchmarkExecutor, guessStra for (res in resourcesToCheck) { - logger.info { "Running experiment with load '${load}' and resources '$res'" } + logger.info { "Running experiment with load '$load' and resources '$res'" } if (this.benchmarkExecutor.runExperiment(load, res)) return res } } } else { - logger.info { "InitialGuessSearchStrategy called without lastLowestResource value, which is needed as a " + - "starting point!" } + logger.info { "lastLowestResource was null." } } return null } override fun findSuitableLoad(resource: Int, loads: List<Int>): Int? { - TODO("Not yet implemented") + + if(loads.isEmpty()) { + logger.info { "You need to specify resources to be checked for the InitialGuessSearchStrategy to work." } + return null + } + + if(guessStrategy == null){ + logger.info { "Your InitialGuessSearchStrategy doesn't have a GuessStrategy. This is not supported." } + return null + } + + if(results == null){ + logger.info { "The results need to be initialized." } + return null + } + + var lastMaxLoad : Int? = null + + // Getting the lastLowestLoad from results and calling firstGuess() with it + if (!results.isEmpty()) { + val maxResource: Int? = this.results.getMaxBenchmarkedXDimensionValue(resource) + lastMaxLoad = this.results.getMaxRequiredYDimensionValue(maxResource) + if (lastMaxLoad == Int.MIN_VALUE) lastMaxLoad = null + } + lastMaxLoad = this.guessStrategy.firstGuess(loads, lastMaxLoad) + + if (lastMaxLoad != null) { + val loadsToCheck: List<Int> + val startIndex: Int = loads.indexOf(lastMaxLoad) + + logger.info { "Running experiment with resource '$resource' and load '$lastMaxLoad'" } + + // If the first experiment passes, starting upwards linear search + // otherwise starting downward linear search + if (!this.benchmarkExecutor.runExperiment(resource, lastMaxLoad)) { + // downward search + + loadsToCheck = loads.subList(0, startIndex).reversed() + if (loadsToCheck.isNotEmpty()) { + for (load in loadsToCheck) { + + logger.info { "Running experiment with resource '$resource' and load '$load'" } + if (this.benchmarkExecutor.runExperiment(resource, load)) { + return load + } + } + } + } + else { + // upward search + if (loads.size <= startIndex + 1) { + return lastMaxLoad + } + loadsToCheck = loads.subList(startIndex + 1, loads.size) + + var currentMax: Int = lastMaxLoad + for (load in loadsToCheck) { + logger.info { "Running experiment with resource '$resource' and load '$load'" } + if (this.benchmarkExecutor.runExperiment(resource, load)) { + currentMax = load + } + } + return currentMax + } + } + else { + logger.info { "lastMaxLoad was null." } + } + return null } } \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/PrevResourceMinGuess.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/PrevResourceMinGuess.kt index bc18982f1..04d68c373 100644 --- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/PrevResourceMinGuess.kt +++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/PrevResourceMinGuess.kt @@ -14,6 +14,7 @@ class PrevResourceMinGuess() : GuessStrategy(){ * * @return the value of lastLowestResource if given otherwise the first element of the resource list or null */ + // TODO verallgemeinern für loads override fun firstGuess(resources: List<Int>, lastLowestResource: Int?): Int? { if (lastLowestResource != null) return lastLowestResource diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt index d870d38ca..bbd0f0b51 100644 --- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt +++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt @@ -46,7 +46,6 @@ abstract class SearchStrategy(val benchmarkExecutor: BenchmarkExecutor, val gues */ abstract fun findSuitableResource(load: Int, resources: List<Int>): Int? - // TODO findSuitableLoad und findSuitableResource zusammenfuehren? /** * Find biggest suitable load from the specified load list for the given resource amount. * diff --git a/theodolite/src/main/kotlin/theodolite/util/Results.kt b/theodolite/src/main/kotlin/theodolite/util/Results.kt index eca33eaab..9feee0643 100644 --- a/theodolite/src/main/kotlin/theodolite/util/Results.kt +++ b/theodolite/src/main/kotlin/theodolite/util/Results.kt @@ -14,6 +14,8 @@ class Results (val metric: Metric) { //TODO: enum statt Boolean private val results: MutableMap<Pair<Int, Int>, Boolean> = mutableMapOf() + //TODO: min instance (or max respectively) also as fields so we do not loop over results, speichert alle results für alle load/resource pairs + /** * Set the result for an experiment. * @@ -39,12 +41,12 @@ class Results (val metric: Metric) { /** * Get the smallest suitable number of instances for a specified LoadDimension. * - * @param load the LoadDimension + * @param xValue the Value of the x-dimension of the current metric * - * @return the smallest suitable number of resources. If the experiment was not executed yet, - * a @see Resource with the constant Int.MAX_VALUE as value is returned. - * If no experiments have been marked as either successful or unsuccessful - * yet, a Resource with the constant value Int.MIN_VALUE is returned. + * @return the smallest suitable number of resources/loads (depending on metric). + * If there is no experiment that has been executed yet, Int.MIN_VALUE is returned. + * If there is no experiment for the given [xValue] or there is none marked successful yet, + * Int.MAX_VALUE is returned. */ fun getMinRequiredYDimensionValue(xValue: Int?): Int { if (this.results.isEmpty()) { //should add || xValue == null @@ -53,7 +55,7 @@ class Results (val metric: Metric) { var minRequiredYValue = Int.MAX_VALUE for (experiment in results) { - // Get all successful experiments for requested load + // Get all successful experiments for requested xValue if (getXDimensionValue(experiment.key) == xValue && experiment.value) { val experimentYValue = getYDimensionValue(experiment.key) if (experimentYValue < minRequiredYValue) { @@ -65,6 +67,37 @@ class Results (val metric: Metric) { return minRequiredYValue } + + /** + * Get the largest y-Value for which the given x-Value has a positive experiment outcome. + * x- and y-values depend on the metric in use. + * + * @param xValue the Value of the x-dimension of the current metric + * + * @return the largest suitable number of resources/loads (depending on metric). + * If there wasn't any experiment executed yet, Int.MAX_VALUE is returned. + * If the experiments for the specified [xValue] wasn't executed yet or the experiments were not successful + * Int.MIN_VALUE is returned. + */ + fun getMaxRequiredYDimensionValue(xValue: Int?): Int { + if (this.results.isEmpty()) { //should add || xValue == null + return Int.MAX_VALUE + } + + var maxRequiredYValue = Int.MIN_VALUE + for (experiment in results) { + // Get all successful experiments for requested xValue + if (getXDimensionValue(experiment.key) == xValue && experiment.value) { + val experimentYValue = getYDimensionValue(experiment.key) + if (experimentYValue > maxRequiredYValue) { + // Found new largest value + maxRequiredYValue = experimentYValue + } + } + } + return maxRequiredYValue + } + // TODO: SÖREN FRAGEN WARUM WIR DAS BRAUCHEN UND NICHT EINFACH PREV, WEIL NICHT DURCHGELAUFEN? // TODO Kommentar zu XDimension und YDimension /** diff --git a/theodolite/src/test/resources/k8s-resource-files/test-execution.yaml b/theodolite/src/test/resources/k8s-resource-files/test-execution.yaml index e12c851da..ee7a901c8 100644 --- a/theodolite/src/test/resources/k8s-resource-files/test-execution.yaml +++ b/theodolite/src/test/resources/k8s-resource-files/test-execution.yaml @@ -20,10 +20,14 @@ spec: externalSloUrl: "http://localhost:80/evaluate-slope" warmup: 60 # in seconds execution: - strategy: "LinearSearch" + metric: "demand" + strategy: "RestrictionSearch" + properties: + restrictions: + - "LowerBound" + searchStrategy: "LinearSearch" duration: 300 # in seconds repetitions: 1 loadGenerationDelay: 30 # in seconds - restrictions: - - "LowerBound" + configOverrides: [] -- GitLab