diff --git a/theodolite-benchmarks/load-generator-commons/src/main/java/theodolite/commons/workloadgeneration/LoadGenerator.java b/theodolite-benchmarks/load-generator-commons/src/main/java/theodolite/commons/workloadgeneration/LoadGenerator.java index 73f064d1ce44ff8a613f9ce0a7b9a64d4bac6c38..3f5d14c2e7dccb94e4aacde1f531ec2e9d1fb8db 100644 --- a/theodolite-benchmarks/load-generator-commons/src/main/java/theodolite/commons/workloadgeneration/LoadGenerator.java +++ b/theodolite-benchmarks/load-generator-commons/src/main/java/theodolite/commons/workloadgeneration/LoadGenerator.java @@ -22,7 +22,7 @@ public final class LoadGenerator { private static final int THREADS_DEFAULT = 4; private static final String SCHEMA_REGISTRY_URL_DEFAULT = "http://localhost:8081"; private static final String KAFKA_TOPIC_DEFAULT = "input"; - private static final String KAFKA_BOOTSTRAP_SERVERS_DEFAULT = "localhost:19092"; // NOPMD + private static final String KAFKA_BOOTSTRAP_SERVERS_DEFAULT = "localhost:9092"; // NOPMD private ClusterConfig clusterConfig; private WorkloadDefinition loadDefinition; diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/GuessStrategy.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/GuessStrategy.kt new file mode 100644 index 0000000000000000000000000000000000000000..786a3baf159e94841c1f76c696f030718e8f768f --- /dev/null +++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/GuessStrategy.kt @@ -0,0 +1,22 @@ +package theodolite.strategies.searchstrategy + +import io.quarkus.runtime.annotations.RegisterForReflection +import theodolite.util.Resource + +/** + * Base class for the implementation of Guess strategies. Guess strategies are strategies to determine the resource + * demand we start with in our initial guess search strategy. + */ + +@RegisterForReflection +abstract class GuessStrategy { + /** + * Computing the resource demand for the initial guess search strategy to start with. + * + * @param resources List of all possible [Resource]s. + * @param lastLowestResource Previous resource demand needed for the given load. + * + * @return Returns the resource demand to start the initial guess search strategy with or null + */ + abstract fun firstGuess(resources: List<Resource>, lastLowestResource: Resource?): Resource? +} \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt new file mode 100644 index 0000000000000000000000000000000000000000..d97fb62cc9d37dd50122199e5d089c491784e511 --- /dev/null +++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt @@ -0,0 +1,93 @@ +package theodolite.strategies.searchstrategy + +import mu.KotlinLogging +import theodolite.execution.BenchmarkExecutor +import theodolite.util.LoadDimension +import theodolite.util.Resource +import theodolite.util.Results + +private val logger = KotlinLogging.logger {} + +/** + * Search strategy implementation for determining the smallest suitable resource demand. + * Starting with a resource amount provided by a guess strategy. + * + * @param benchmarkExecutor Benchmark executor which runs the individual benchmarks. + * @param guessStrategy Strategy that provides us with a guess for the first resource amount. + * @param results current results of all previously performed benchmarks. + */ +class InitialGuessSearchStrategy(benchmarkExecutor: BenchmarkExecutor, guessStrategy: GuessStrategy, results: Results) : + SearchStrategy(benchmarkExecutor, guessStrategy, results) { + + override fun findSuitableResource(load: LoadDimension, resources: List<Resource>): Resource? { + + if(resources.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 lastLowestResource : Resource? = null + + // Getting the lastLowestResource from results and calling firstGuess() with it + if (!results.isEmpty()) { + val maxLoad: LoadDimension? = this.results.getMaxBenchmarkedLoad(load) + lastLowestResource = this.results.getMinRequiredInstances(maxLoad) + if (lastLowestResource.get() == Int.MAX_VALUE) lastLowestResource = null + } + lastLowestResource = this.guessStrategy.firstGuess(resources, lastLowestResource) + + if (lastLowestResource != null) { + val resourcesToCheck: List<Resource> + val startIndex: Int = resources.indexOf(lastLowestResource) + + logger.info { "Running experiment with load '${load.get()}' and resources '${lastLowestResource.get()}'" } + + // If the first experiment passes, starting downward linear search + // otherwise starting upward linear search + if (this.benchmarkExecutor.runExperiment(load, lastLowestResource)) { + + resourcesToCheck = resources.subList(0, startIndex).reversed() + if (resourcesToCheck.isEmpty()) return lastLowestResource + + var currentMin: Resource = lastLowestResource + for (res in resourcesToCheck) { + + logger.info { "Running experiment with load '${load.get()}' and resources '${res.get()}'" } + if (this.benchmarkExecutor.runExperiment(load, res)) { + currentMin = res + } + } + return currentMin + } + else { + if (resources.size <= startIndex + 1) { + logger.info{ "No more resources left to check." } + return null + } + resourcesToCheck = resources.subList(startIndex + 1, resources.size) + + for (res in resourcesToCheck) { + + logger.info { "Running experiment with load '${load.get()}' and resources '${res.get()}'" } + if (this.benchmarkExecutor.runExperiment(load, res)) return res + } + } + } + else { + logger.info { "InitialGuessSearchStrategy called without lastLowestResource value, which is needed as a " + + "starting point!" } + } + 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 new file mode 100644 index 0000000000000000000000000000000000000000..413eecea27279cd79bad155fbb7d5d18b674a12e --- /dev/null +++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/PrevResourceMinGuess.kt @@ -0,0 +1,24 @@ +package theodolite.strategies.searchstrategy + +import theodolite.util.Resource + +/** + * This Guess strategy takes the minimal resource demand of the previous load, which is given as an argument for the + * firstGuess function. + */ + +class PrevResourceMinGuess() : GuessStrategy(){ + + /** + * @param resources List of all possible [Resource]s. + * @param lastLowestResource Previous resource demand needed for the given load. + * + * @return the value of lastLowestResource if given otherwise the first element of the resource list or null + */ + override fun firstGuess(resources: List<Resource>, lastLowestResource: Resource?): Resource? { + + if (lastLowestResource != null) return lastLowestResource + else if(resources.isNotEmpty()) return resources[0] + else return null + } +} \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt index 4e304b010d4d56f6b5fe734a6b977361f93e57a1..97c723f2cfe459081cbb327f6860e48319c8f4f1 100644 --- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt +++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt @@ -4,14 +4,18 @@ import io.quarkus.runtime.annotations.RegisterForReflection import theodolite.execution.BenchmarkExecutor import theodolite.util.LoadDimension import theodolite.util.Resource +import theodolite.util.Results /** * Base class for the implementation for SearchStrategies. SearchStrategies determine the smallest suitable number of instances. * * @param benchmarkExecutor Benchmark executor which runs the individual benchmarks. + * @param guessStrategy Guess strategy for the initial resource amount in case the InitialGuessStrategy is selected. + * @param results the [Results] object. */ @RegisterForReflection -abstract class SearchStrategy(val benchmarkExecutor: BenchmarkExecutor) { +abstract class SearchStrategy(val benchmarkExecutor: BenchmarkExecutor, val guessStrategy: GuessStrategy? = null, + val results: Results? = null) { /** * Find smallest suitable resource from the specified resource list for the given load. * diff --git a/theodolite/src/main/kotlin/theodolite/util/Results.kt b/theodolite/src/main/kotlin/theodolite/util/Results.kt index 60641ea0248435de53aaaaf362da7be995b391c5..2221c2e64f6dbc1776122f20793aa8d04d621d9d 100644 --- a/theodolite/src/main/kotlin/theodolite/util/Results.kt +++ b/theodolite/src/main/kotlin/theodolite/util/Results.kt @@ -3,7 +3,7 @@ package theodolite.util import io.quarkus.runtime.annotations.RegisterForReflection /** - * Central class that saves the state of a execution of Theodolite. For an execution, it is used to save the result of + * Central class that saves the state of an execution of Theodolite. For an execution, it is used to save the result of * individual experiments. Further, it is used by the RestrictionStrategy to * perform the [theodolite.strategies.restriction.RestrictionStrategy]. */ @@ -44,16 +44,16 @@ class Results { * If no experiments have been marked as either successful or unsuccessful * yet, a Resource with the constant value Int.MIN_VALUE is returned. */ - fun getMinRequiredInstances(load: LoadDimension?): Resource? { + fun getMinRequiredInstances(load: LoadDimension?): Resource { if (this.results.isEmpty()) { return Resource(Int.MIN_VALUE, emptyList()) } - var minRequiredInstances: Resource? = Resource(Int.MAX_VALUE, emptyList()) + var minRequiredInstances = Resource(Int.MAX_VALUE, emptyList()) for (experiment in results) { // Get all successful experiments for requested load if (experiment.key.first == load && experiment.value) { - if (minRequiredInstances == null || experiment.key.second.get() < minRequiredInstances.get()) { + if (experiment.key.second.get() < minRequiredInstances.get()) { // Found new smallest resources minRequiredInstances = experiment.key.second } @@ -83,4 +83,13 @@ class Results { } return maxBenchmarkedLoad } + + /** + * Checks whether the results are empty. + * + * @return true if [results] is empty. + */ + fun isEmpty(): Boolean{ + return results.isEmpty() + } } diff --git a/theodolite/src/test/kotlin/theodolite/InitialGuessSearchStrategyTest.kt b/theodolite/src/test/kotlin/theodolite/InitialGuessSearchStrategyTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..1af6f548b219697009c688ace712a9f7f5620bd0 --- /dev/null +++ b/theodolite/src/test/kotlin/theodolite/InitialGuessSearchStrategyTest.kt @@ -0,0 +1,133 @@ +package theodolite + +import io.quarkus.test.junit.QuarkusTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import theodolite.benchmark.BenchmarkExecution +import theodolite.strategies.searchstrategy.InitialGuessSearchStrategy +import theodolite.util.LoadDimension +import theodolite.util.Resource +import theodolite.util.Results +import mu.KotlinLogging +import theodolite.strategies.searchstrategy.PrevResourceMinGuess + +private val logger = KotlinLogging.logger {} + +@QuarkusTest +class InitialGuessSearchStrategyTest { + + @Test + fun testInitialGuessSearch() { + val mockResults = arrayOf( + arrayOf(true, true, true, true, true, true, true), + arrayOf(false, false, true, true, true, true, true), + arrayOf(false, false, true, true, true, true, true), + arrayOf(false, false, false, true, true, true, true), + arrayOf(false, false, false, false, true, true, true), + arrayOf(false, false, false, false, false, false, true), + arrayOf(false, false, false, false, false, false, false) + ) + val mockLoads: List<LoadDimension> = (0..6).map { number -> LoadDimension(number, emptyList()) } + val mockResources: List<Resource> = (0..6).map { number -> Resource(number, emptyList()) } + val results = Results() + val benchmark = TestBenchmark() + val guessStrategy = PrevResourceMinGuess() + val sloChecker: BenchmarkExecution.Slo = BenchmarkExecution.Slo() + val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results, listOf(sloChecker), 0, 0, 5) + val strategy = InitialGuessSearchStrategy(benchmarkExecutor,guessStrategy, results) + + val actual: ArrayList<Resource?> = ArrayList() + val expected: ArrayList<Resource?> = ArrayList(listOf(0, 2, 2, 3, 4, 6).map { x -> Resource(x, emptyList()) }) + expected.add(null) + + for (load in mockLoads) { + val returnVal : Resource? = strategy.findSuitableResource(load, mockResources) + if(returnVal != null) { + logger.info { "returnVal '${returnVal.get()}'" } + } + else { + logger.info { "returnVal is null." } + } + actual.add(returnVal) + } + + assertEquals(actual, expected) + } + + @Test + fun testInitialGuessSearchLowerResourceDemandHigherLoad() { + val mockResults = arrayOf( + arrayOf(true, true, true, true, true, true, true), + arrayOf(false, false, true, true, true, true, true), + arrayOf(false, false, true, true, true, true, true), + arrayOf(false, true, true, true, true, true, true), + arrayOf(false, false, false, false, true, true, true), + arrayOf(false, false, false, false, false, false, true), + arrayOf(false, false, false, false, false, false, false) + ) + val mockLoads: List<LoadDimension> = (0..6).map { number -> LoadDimension(number, emptyList()) } + val mockResources: List<Resource> = (0..6).map { number -> Resource(number, emptyList()) } + val results = Results() + val benchmark = TestBenchmark() + val guessStrategy = PrevResourceMinGuess() + val sloChecker: BenchmarkExecution.Slo = BenchmarkExecution.Slo() + val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results, listOf(sloChecker), 0, 0, 5) + val strategy = InitialGuessSearchStrategy(benchmarkExecutor,guessStrategy, results) + + val actual: ArrayList<Resource?> = ArrayList() + val expected: ArrayList<Resource?> = ArrayList(listOf(0, 2, 2, 1, 4, 6).map { x -> Resource(x, emptyList()) }) + expected.add(null) + + for (load in mockLoads) { + val returnVal : Resource? = strategy.findSuitableResource(load, mockResources) + if(returnVal != null) { + logger.info { "returnVal '${returnVal.get()}'" } + } + else { + logger.info { "returnVal is null." } + } + actual.add(returnVal) + } + + assertEquals(actual, expected) + } + + @Test + fun testInitialGuessSearchFirstNotDoable() { + val mockResults = arrayOf( + arrayOf(false, false, false, false, false, false, false), + arrayOf(false, false, true, true, true, true, true), + arrayOf(false, false, false, true, true, true, true), + arrayOf(true, true, true, true, true, true, true), + arrayOf(false, false, false, false, true, true, true), + arrayOf(false, false, false, false, false, false, true), + arrayOf(false, false, false, false, false, false, false) + ) + val mockLoads: List<LoadDimension> = (0..6).map { number -> LoadDimension(number, emptyList()) } + val mockResources: List<Resource> = (0..6).map { number -> Resource(number, emptyList()) } + val results = Results() + val benchmark = TestBenchmark() + val guessStrategy = PrevResourceMinGuess() + val sloChecker: BenchmarkExecution.Slo = BenchmarkExecution.Slo() + val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results, listOf(sloChecker), 0, 0, 5) + val strategy = InitialGuessSearchStrategy(benchmarkExecutor, guessStrategy, results) + + val actual: ArrayList<Resource?> = ArrayList() + var expected: ArrayList<Resource?> = ArrayList(listOf(2, 3, 0, 4, 6).map { x -> Resource(x, emptyList()) }) + expected.add(null) + expected = ArrayList(listOf(null) + expected) + + for (load in mockLoads) { + val returnVal : Resource? = strategy.findSuitableResource(load, mockResources) + if(returnVal != null) { + logger.info { "returnVal '${returnVal.get()}'" } + } + else { + logger.info { "returnVal is null." } + } + actual.add(returnVal) + } + + assertEquals(actual, expected) + } +} \ No newline at end of file