diff options
Diffstat (limited to 'simulator')
142 files changed, 6135 insertions, 2417 deletions
diff --git a/simulator/buildSrc/build.gradle.kts b/simulator/buildSrc/build.gradle.kts index 7ccd920f..6ad5cfe9 100644 --- a/simulator/buildSrc/build.gradle.kts +++ b/simulator/buildSrc/build.gradle.kts @@ -40,7 +40,7 @@ repositories { } dependencies { - implementation(kotlin("gradle-plugin", version = "1.4.10")) - implementation("org.jlleitschuh.gradle:ktlint-gradle:9.4.0") + implementation(kotlin("gradle-plugin", version = "1.4.21")) + implementation("org.jlleitschuh.gradle:ktlint-gradle:9.4.1") implementation("org.jetbrains.dokka:dokka-gradle-plugin:0.10.1") } diff --git a/simulator/buildSrc/src/main/kotlin/kotlin-library-convention.gradle.kts b/simulator/buildSrc/src/main/kotlin/kotlin-library-convention.gradle.kts index bbecf346..f9d8a966 100644 --- a/simulator/buildSrc/src/main/kotlin/kotlin-library-convention.gradle.kts +++ b/simulator/buildSrc/src/main/kotlin/kotlin-library-convention.gradle.kts @@ -33,6 +33,7 @@ plugins { /* Project configuration */ repositories { + mavenCentral() jcenter() } diff --git a/simulator/buildSrc/src/main/kotlin/library.kt b/simulator/buildSrc/src/main/kotlin/library.kt index af278a07..6735c110 100644 --- a/simulator/buildSrc/src/main/kotlin/library.kt +++ b/simulator/buildSrc/src/main/kotlin/library.kt @@ -45,5 +45,5 @@ object Library { /** * Kotlin coroutines support */ - val KOTLINX_COROUTINES = "1.3.9" + val KOTLINX_COROUTINES = "1.4.2" } diff --git a/simulator/gradle/wrapper/gradle-wrapper.properties b/simulator/gradle/wrapper/gradle-wrapper.properties index be52383e..da9702f9 100644 --- a/simulator/gradle/wrapper/gradle-wrapper.properties +++ b/simulator/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/simulator/opendc-compute/opendc-compute-core/build.gradle.kts b/simulator/opendc-compute/opendc-compute-core/build.gradle.kts index 9682b50f..ac2dc78d 100644 --- a/simulator/opendc-compute/opendc-compute-core/build.gradle.kts +++ b/simulator/opendc-compute/opendc-compute-core/build.gradle.kts @@ -29,6 +29,7 @@ plugins { dependencies { api(project(":opendc-core")) + api(project(":opendc-trace:opendc-trace-core")) implementation(project(":opendc-utils")) implementation("io.github.microutils:kotlin-logging:1.7.9") diff --git a/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/driver/VirtDriver.kt b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/driver/VirtDriver.kt index 5ecfd357..68cc7b50 100644 --- a/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/driver/VirtDriver.kt +++ b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/driver/VirtDriver.kt @@ -23,6 +23,7 @@ package org.opendc.compute.core.virt.driver import kotlinx.coroutines.flow.Flow +import org.opendc.compute.core.Flavor import org.opendc.compute.core.Server import org.opendc.compute.core.image.Image import org.opendc.compute.core.virt.HypervisorEvent @@ -40,6 +41,11 @@ public interface VirtDriver { public val events: Flow<HypervisorEvent> /** + * Determine whether the specified [flavor] can still fit on this driver. + */ + public fun canFit(flavor: Flavor): Boolean + + /** * Spawn the given [Image] on the compute resource of this driver. * * @param name The name of the server to spawn. @@ -50,7 +56,7 @@ public interface VirtDriver { public suspend fun spawn( name: String, image: Image, - flavor: org.opendc.compute.core.Flavor + flavor: Flavor ): Server public companion object Key : AbstractServiceKey<VirtDriver>(UUID.randomUUID(), "virtual-driver") diff --git a/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/VirtProvisioningService.kt b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/VirtProvisioningService.kt index ab96e0a3..3d722110 100644 --- a/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/VirtProvisioningService.kt +++ b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/VirtProvisioningService.kt @@ -42,6 +42,11 @@ public interface VirtProvisioningService { public suspend fun drivers(): Set<VirtDriver> /** + * The number of hosts available in the system. + */ + public val hostCount: Int + + /** * Submit the specified [Image] to the provisioning service. * * @param name The name of the server to deploy. diff --git a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/ResourceSelectionPolicy.kt b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/HypervisorAvailableEvent.kt index 990b990a..c1802e64 100644 --- a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/ResourceSelectionPolicy.kt +++ b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/HypervisorAvailableEvent.kt @@ -20,13 +20,12 @@ * SOFTWARE. */ -package org.opendc.workflows.service.stage.resource +package org.opendc.compute.core.virt.service.events -import org.opendc.compute.core.metal.Node -import org.opendc.workflows.service.stage.StagePolicy +import org.opendc.trace.core.Event +import java.util.* /** - * This interface represents the **R5** stage of the Reference Architecture for Schedulers and matches the the selected - * task with a (set of) resource(s), using policies such as First-Fit, Worst-Fit, and Best-Fit. + * This event is emitted when a hypervisor has become available. */ -public interface ResourceSelectionPolicy : StagePolicy<Comparator<Node>> +public class HypervisorAvailableEvent(public val uid: UUID) : Event() diff --git a/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/HypervisorUnavailableEvent.kt b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/HypervisorUnavailableEvent.kt new file mode 100644 index 00000000..1fea21ea --- /dev/null +++ b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/HypervisorUnavailableEvent.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.compute.core.virt.service.events + +import org.opendc.trace.core.Event +import java.util.* + +/** + * This event is emitted when a hypervisor has become unavailable. + */ +public class HypervisorUnavailableEvent(public val uid: UUID) : Event() diff --git a/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmScheduledEvent.kt b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmScheduledEvent.kt new file mode 100644 index 00000000..662068dd --- /dev/null +++ b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmScheduledEvent.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.compute.core.virt.service.events + +import org.opendc.trace.core.Event + +/** + * This event is emitted when a virtual machine has successfully been scheduled on a hypervisor. + */ +public class VmScheduledEvent(public val name: String) : Event() diff --git a/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmStoppedEvent.kt b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmStoppedEvent.kt new file mode 100644 index 00000000..96103129 --- /dev/null +++ b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmStoppedEvent.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.compute.core.virt.service.events + +import org.opendc.trace.core.Event + +/** + * This event is emitted when a virtual machine has stopped running. + */ +public class VmStoppedEvent(public val name: String) : Event() diff --git a/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmSubmissionEvent.kt b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmSubmissionEvent.kt new file mode 100644 index 00000000..f6b71e22 --- /dev/null +++ b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmSubmissionEvent.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.compute.core.virt.service.events + +import org.opendc.compute.core.Flavor +import org.opendc.compute.core.image.Image +import org.opendc.trace.core.Event + +/** + * This event is emitted when a virtual machine is submitted to the provisioning service. + */ +public class VmSubmissionEvent(public val name: String, public val image: Image, public val flavor: Flavor) : Event() diff --git a/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmSubmissionInvalidEvent.kt b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmSubmissionInvalidEvent.kt new file mode 100644 index 00000000..d0e5c102 --- /dev/null +++ b/simulator/opendc-compute/opendc-compute-core/src/main/kotlin/org/opendc/compute/core/virt/service/events/VmSubmissionInvalidEvent.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.compute.core.virt.service.events + +import org.opendc.trace.core.Event + +/** + * An event that is emitted when the submission is deemed to be invalid. + */ +public class VmSubmissionInvalidEvent(public val name: String) : Event() diff --git a/simulator/opendc-compute/opendc-compute-simulator/build.gradle.kts b/simulator/opendc-compute/opendc-compute-simulator/build.gradle.kts index d7570e54..dc93e956 100644 --- a/simulator/opendc-compute/opendc-compute-simulator/build.gradle.kts +++ b/simulator/opendc-compute/opendc-compute-simulator/build.gradle.kts @@ -29,10 +29,10 @@ plugins { dependencies { api(project(":opendc-compute:opendc-compute-core")) + api(project(":opendc-simulator:opendc-simulator-compute")) + api(project(":opendc-simulator:opendc-simulator-failures")) implementation(project(":opendc-utils")) implementation("io.github.microutils:kotlin-logging:1.7.9") - implementation(project(":opendc-simulator:opendc-simulator-compute")) - api(project(":opendc-simulator:opendc-simulator-failures")) testImplementation(project(":opendc-simulator:opendc-simulator-core")) testRuntimeOnly("org.slf4j:slf4j-simple:${Library.SLF4J}") diff --git a/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimBareMetalDriver.kt b/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimBareMetalDriver.kt index 97f550ba..7a978a53 100644 --- a/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimBareMetalDriver.kt +++ b/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimBareMetalDriver.kt @@ -41,6 +41,7 @@ import org.opendc.core.services.ServiceRegistry import org.opendc.simulator.compute.SimBareMetalMachine import org.opendc.simulator.compute.SimExecutionContext import org.opendc.simulator.compute.SimMachineModel +import org.opendc.simulator.compute.workload.SimResourceCommand import org.opendc.simulator.compute.workload.SimWorkload import org.opendc.simulator.failures.FailureDomain import org.opendc.utils.flow.EventFlow @@ -139,15 +140,31 @@ public class SimBareMetalDriver( events ) + val delegate = (node.image as SimWorkloadImage).workload // Wrap the workload to pass in a ComputeSimExecutionContext val workload = object : SimWorkload { - override suspend fun run(ctx: SimExecutionContext) { - val wrappedCtx = object : ComputeSimExecutionContext, SimExecutionContext by ctx { + lateinit var wrappedCtx: ComputeSimExecutionContext + + override fun onStart(ctx: SimExecutionContext) { + wrappedCtx = object : ComputeSimExecutionContext, SimExecutionContext by ctx { override val server: Server get() = nodeState.value.server!! + + override fun toString(): String = "WrappedSimExecutionContext" } - (node.image as SimWorkloadImage).workload.run(wrappedCtx) + + delegate.onStart(wrappedCtx) + } + + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + return delegate.onStart(wrappedCtx, cpu) + } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + return delegate.onNext(wrappedCtx, cpu, remainingWork) } + + override fun toString(): String = "SimWorkloadWrapper(delegate=$delegate)" } job = coroutineScope.launch { diff --git a/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimVirtDriver.kt b/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimVirtDriver.kt index 09eec1ef..d7a8a8b2 100644 --- a/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimVirtDriver.kt +++ b/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimVirtDriver.kt @@ -32,30 +32,29 @@ import org.opendc.compute.core.virt.HypervisorEvent import org.opendc.compute.core.virt.driver.InsufficientMemoryOnServerException import org.opendc.compute.core.virt.driver.VirtDriver import org.opendc.core.services.ServiceRegistry -import org.opendc.simulator.compute.SimExecutionContext -import org.opendc.simulator.compute.SimHypervisor -import org.opendc.simulator.compute.SimMachine +import org.opendc.simulator.compute.* import org.opendc.simulator.compute.interference.IMAGE_PERF_INTERFERENCE_MODEL import org.opendc.simulator.compute.interference.PerformanceInterferenceModel +import org.opendc.simulator.compute.model.MemoryUnit +import org.opendc.simulator.compute.workload.SimResourceCommand import org.opendc.simulator.compute.workload.SimWorkload import org.opendc.utils.flow.EventFlow -import java.time.Clock import java.util.* /** * A [VirtDriver] that is simulates virtual machines on a physical machine using [SimHypervisor]. */ -public class SimVirtDriver( - private val coroutineScope: CoroutineScope, - clock: Clock, - private val ctx: SimExecutionContext -) : VirtDriver { +public class SimVirtDriver(private val coroutineScope: CoroutineScope, hypervisor: SimHypervisorProvider) : VirtDriver, SimWorkload { + /** + * The execution context in which the [VirtDriver] runs. + */ + private lateinit var ctx: ComputeSimExecutionContext /** * The server hosting this hypervisor. */ public val server: Server - get() = (ctx as ComputeSimExecutionContext).server + get() = ctx.server /** * The [EventFlow] to emit the events. @@ -67,35 +66,33 @@ public class SimVirtDriver( /** * Current total memory use of the images on this hypervisor. */ - private var availableMemory: Long = ctx.machine.memory.map { it.size }.sum() + private var availableMemory: Long = 0 /** * The hypervisor to run multiple workloads. */ - private val hypervisor = SimHypervisor( - coroutineScope, - clock, + private val hypervisor = hypervisor.create( object : SimHypervisor.Listener { override fun onSliceFinish( hypervisor: SimHypervisor, - requestedBurst: Long, - grantedBurst: Long, - overcommissionedBurst: Long, - interferedBurst: Long, + requestedWork: Long, + grantedWork: Long, + overcommittedWork: Long, + interferedWork: Long, cpuUsage: Double, cpuDemand: Double ) { eventFlow.emit( HypervisorEvent.SliceFinished( this@SimVirtDriver, - requestedBurst, - grantedBurst, - overcommissionedBurst, - interferedBurst, + requestedWork, + grantedWork, + overcommittedWork, + interferedWork, cpuUsage, cpuDemand, vms.size, - (ctx as ComputeSimExecutionContext).server + ctx.server ) ) } @@ -107,6 +104,14 @@ public class SimVirtDriver( */ private val vms = HashSet<VirtualMachine>() + override fun canFit(flavor: Flavor): Boolean { + val sufficientMemory = availableMemory > flavor.memorySize + val enoughCpus = ctx.machine.cpus.size >= flavor.cpuCount + val canFit = hypervisor.canFit(flavor.toMachineModel()) + + return sufficientMemory && enoughCpus && canFit + } + override suspend fun spawn(name: String, image: Image, flavor: Flavor): Server { val requiredMemory = flavor.memorySize if (availableMemory - requiredMemory < 0) { @@ -126,13 +131,26 @@ public class SimVirtDriver( events ) availableMemory -= requiredMemory - val vm = VirtualMachine(server, events, hypervisor.createMachine(ctx.machine)) + + val vm = VirtualMachine(server, events, hypervisor.createMachine(flavor.toMachineModel())) vms.add(vm) vmStarted(vm) eventFlow.emit(HypervisorEvent.VmsUpdated(this, vms.size, availableMemory)) return server } + /** + * Convert flavor to machine model. + */ + private fun Flavor.toMachineModel(): SimMachineModel { + val originalCpu = ctx.machine.cpus[0] + val processingNode = originalCpu.node.copy(coreCount = cpuCount) + val processingUnits = (0 until cpuCount).map { originalCpu.copy(id = it, node = processingNode) } + val memoryUnits = listOf(MemoryUnit("Generic", "Generic", 3200.0, memorySize)) + + return SimMachineModel(processingUnits, memoryUnits) + } + private fun vmStarted(vm: VirtualMachine) { vms.forEach { it -> vm.performanceInterferenceModel?.onStart(it.server.image.name) @@ -148,18 +166,35 @@ public class SimVirtDriver( /** * A virtual machine instance that the driver manages. */ - private inner class VirtualMachine(server: Server, val events: EventFlow<ServerEvent>, machine: SimMachine) { + private inner class VirtualMachine(server: Server, val events: EventFlow<ServerEvent>, val machine: SimMachine) { val performanceInterferenceModel: PerformanceInterferenceModel? = server.image.tags[IMAGE_PERF_INTERFERENCE_MODEL] as? PerformanceInterferenceModel? val job = coroutineScope.launch { + val delegate = (server.image as SimWorkloadImage).workload + // Wrap the workload to pass in a ComputeSimExecutionContext val workload = object : SimWorkload { - override suspend fun run(ctx: SimExecutionContext) { - val wrappedCtx = object : ComputeSimExecutionContext, SimExecutionContext by ctx { + lateinit var wrappedCtx: ComputeSimExecutionContext + + override fun onStart(ctx: SimExecutionContext) { + wrappedCtx = object : ComputeSimExecutionContext, SimExecutionContext by ctx { override val server: Server - get() = this@VirtualMachine.server + get() = server + + override fun toString(): String = "WrappedSimExecutionContext" } - (server.image as SimWorkloadImage).workload.run(wrappedCtx) + + delegate.onStart(wrappedCtx) + } + + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + return delegate.onStart(wrappedCtx, cpu) + } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + return delegate.onNext(wrappedCtx, cpu, remainingWork) } + + override fun toString(): String = "SimWorkloadWrapper(delegate=$delegate)" } delay(1) // TODO Introduce boot time @@ -169,6 +204,8 @@ public class SimVirtDriver( exit(null) } catch (cause: Throwable) { exit(cause) + } finally { + machine.close() } } @@ -200,7 +237,17 @@ public class SimVirtDriver( } } - public suspend fun run() { - hypervisor.run(ctx) + override fun onStart(ctx: SimExecutionContext) { + this.ctx = ctx as ComputeSimExecutionContext + this.availableMemory = ctx.machine.memory.map { it.size }.sum() + this.hypervisor.onStart(ctx) + } + + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + return hypervisor.onStart(ctx, cpu) + } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + return hypervisor.onNext(ctx, cpu, remainingWork) } } diff --git a/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimVirtProvisioningService.kt b/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimVirtProvisioningService.kt index e83370d7..defea888 100644 --- a/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimVirtProvisioningService.kt +++ b/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimVirtProvisioningService.kt @@ -38,7 +38,11 @@ import org.opendc.compute.core.virt.driver.InsufficientMemoryOnServerException import org.opendc.compute.core.virt.driver.VirtDriver import org.opendc.compute.core.virt.service.VirtProvisioningEvent import org.opendc.compute.core.virt.service.VirtProvisioningService +import org.opendc.compute.core.virt.service.events.* import org.opendc.compute.simulator.allocation.AllocationPolicy +import org.opendc.simulator.compute.SimHypervisorProvider +import org.opendc.trace.core.EventTracer +import org.opendc.utils.TimerScheduler import org.opendc.utils.flow.EventFlow import java.time.Clock import java.util.* @@ -51,7 +55,10 @@ public class SimVirtProvisioningService( private val coroutineScope: CoroutineScope, private val clock: Clock, private val provisioningService: ProvisioningService, - public val allocationPolicy: AllocationPolicy + public val allocationPolicy: AllocationPolicy, + private val tracer: EventTracer, + private val hypervisor: SimHypervisorProvider, + private val schedulingQuantum: Long = 300000, // 5 minutes in milliseconds ) : VirtProvisioningService { /** * The logger instance to use. @@ -71,7 +78,7 @@ public class SimVirtProvisioningService( /** * The incoming images to be processed by the provisioner. */ - private val incomingImages: MutableSet<ImageView> = mutableSetOf() + private val incomingImages: Deque<ImageView> = ArrayDeque() /** * The active images in the system. @@ -99,11 +106,16 @@ public class SimVirtProvisioningService( override val events: Flow<VirtProvisioningEvent> = eventFlow + /** + * The [TimerScheduler] to use for scheduling the scheduler cycles. + */ + private var scheduler: TimerScheduler<Unit> = TimerScheduler(coroutineScope, clock) + init { coroutineScope.launch { val provisionedNodes = provisioningService.nodes() provisionedNodes.forEach { node -> - val workload = SimVirtDriverWorkload() + val workload = SimVirtDriver(coroutineScope, hypervisor) val hypervisorImage = SimWorkloadImage(UUID.randomUUID(), "vmm", emptyMap(), workload) launch { var init = false @@ -121,7 +133,7 @@ public class SimVirtProvisioningService( }.launchIn(this) delay(1) - onHypervisorAvailable(server, workload.driver) + onHypervisorAvailable(server, workload) } } } @@ -131,11 +143,15 @@ public class SimVirtProvisioningService( return availableHypervisors.map { it.driver }.toSet() } + override val hostCount: Int = hypervisors.size + override suspend fun deploy( name: String, image: Image, flavor: Flavor ): Server { + tracer.commit(VmSubmissionEvent(name, image, flavor)) + eventFlow.emit( VirtProvisioningEvent.MetricsAvailable( this@SimVirtProvisioningService, @@ -161,36 +177,34 @@ public class SimVirtProvisioningService( provisionedNodes.forEach { node -> provisioningService.stop(node) } } - private var call: Job? = null - private fun requestCycle() { - if (call != null) { + // Bail out in case we have already requested a new cycle. + if (scheduler.isTimerActive(Unit)) { return } - val quantum = 300000 // 5 minutes in milliseconds // We assume that the provisioner runs at a fixed slot every time quantum (e.g t=0, t=60, t=120). // This is important because the slices of the VMs need to be aligned. // We calculate here the delay until the next scheduling slot. - val delay = quantum - (clock.millis() % quantum) + val delay = schedulingQuantum - (clock.millis() % schedulingQuantum) - val call = coroutineScope.launch { - delay(delay) - this@SimVirtProvisioningService.call = null - schedule() + scheduler.startSingleTimer(Unit, delay) { + coroutineScope.launch { schedule() } } - this.call = call } private suspend fun schedule() { - val imagesToBeScheduled = incomingImages.toSet() - - for (imageInstance in imagesToBeScheduled) { - val requiredMemory = imageInstance.image.tags["required-memory"] as Long + while (incomingImages.isNotEmpty()) { + val imageInstance = incomingImages.peekFirst() + val requiredMemory = imageInstance.flavor.memorySize val selectedHv = allocationLogic.select(availableHypervisors, imageInstance) - if (selectedHv == null) { + if (selectedHv == null || !selectedHv.driver.canFit(imageInstance.flavor)) { + logger.trace { "Image ${imageInstance.image} selected for scheduling but no capacity available for it." } + if (requiredMemory > maxMemory || imageInstance.flavor.cpuCount > maxCores) { + tracer.commit(VmSubmissionInvalidEvent(imageInstance.name)) + eventFlow.emit( VirtProvisioningEvent.MetricsAvailable( this@SimVirtProvisioningService, @@ -199,12 +213,13 @@ public class SimVirtProvisioningService( submittedVms, runningVms, finishedVms, - queuedVms, + --queuedVms, ++unscheduledVms ) ) - incomingImages -= imageInstance + // Remove the incoming image + incomingImages.poll() logger.warn("Failed to spawn ${imageInstance.image}: does not fit [${clock.millis()}]") continue @@ -215,7 +230,7 @@ public class SimVirtProvisioningService( try { logger.info { "[${clock.millis()}] Spawning ${imageInstance.image} on ${selectedHv.server.uid} ${selectedHv.server.name} ${selectedHv.server.flavor}" } - incomingImages -= imageInstance + incomingImages.poll() // Speculatively update the hypervisor view information to prevent other images in the queue from // deciding on stale values. @@ -231,6 +246,8 @@ public class SimVirtProvisioningService( imageInstance.server = server imageInstance.continuation.resume(server) + tracer.commit(VmScheduledEvent(imageInstance.name)) + eventFlow.emit( VirtProvisioningEvent.MetricsAvailable( this@SimVirtProvisioningService, @@ -252,6 +269,8 @@ public class SimVirtProvisioningService( if (event.server.state == ServerState.SHUTOFF) { logger.info { "[${clock.millis()}] Server ${event.server.uid} ${event.server.name} ${event.server.flavor} finished." } + tracer.commit(VmStoppedEvent(event.server.name)) + eventFlow.emit( VirtProvisioningEvent.MetricsAvailable( this@SimVirtProvisioningService, @@ -310,6 +329,8 @@ public class SimVirtProvisioningService( hypervisors[server] = hv } + tracer.commit(HypervisorAvailableEvent(server.uid)) + eventFlow.emit( VirtProvisioningEvent.MetricsAvailable( this@SimVirtProvisioningService, @@ -333,6 +354,8 @@ public class SimVirtProvisioningService( val hv = hypervisors[server] ?: return availableHypervisors -= hv + tracer.commit(HypervisorUnavailableEvent(hv.uid)) + eventFlow.emit( VirtProvisioningEvent.MetricsAvailable( this@SimVirtProvisioningService, @@ -359,6 +382,8 @@ public class SimVirtProvisioningService( hv.driver = hypervisor availableHypervisors += hv + tracer.commit(HypervisorAvailableEvent(hv.uid)) + eventFlow.emit( VirtProvisioningEvent.MetricsAvailable( this@SimVirtProvisioningService, diff --git a/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/allocation/ComparableAllocationPolicyLogic.kt b/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/allocation/ComparableAllocationPolicyLogic.kt index 8defe8b7..4470eab9 100644 --- a/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/allocation/ComparableAllocationPolicyLogic.kt +++ b/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/allocation/ComparableAllocationPolicyLogic.kt @@ -40,7 +40,7 @@ public interface ComparableAllocationPolicyLogic : AllocationPolicy.Logic { ): HypervisorView? { return hypervisors.asSequence() .filter { hv -> - val fitsMemory = hv.availableMemory >= (image.image.tags["required-memory"] as Long) + val fitsMemory = hv.availableMemory >= (image.flavor.memorySize) val fitsCpu = hv.server.flavor.cpuCount >= image.flavor.cpuCount fitsMemory && fitsCpu } diff --git a/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimBareMetalDriverTest.kt b/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimBareMetalDriverTest.kt index 0f1bd444..fb8a5f47 100644 --- a/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimBareMetalDriverTest.kt +++ b/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimBareMetalDriverTest.kt @@ -64,7 +64,7 @@ internal class SimBareMetalDriverTest { testScope.launch { val driver = SimBareMetalDriver(this, clock, UUID.randomUUID(), "test", emptyMap(), machineModel) - val image = SimWorkloadImage(UUID.randomUUID(), "<unnamed>", emptyMap(), SimFlopsWorkload(4_000, 2, utilization = 1.0)) + val image = SimWorkloadImage(UUID.randomUUID(), "<unnamed>", emptyMap(), SimFlopsWorkload(4_000, utilization = 1.0)) // Batch driver commands withContext(coroutineContext) { @@ -84,6 +84,6 @@ internal class SimBareMetalDriverTest { testScope.advanceUntilIdle() assertEquals(ServerState.SHUTOFF, finalState) - assertEquals(1001, finalTime) + assertEquals(501, finalTime) } } diff --git a/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimProvisioningServiceTest.kt b/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimProvisioningServiceTest.kt index def78ce7..a33a4e5f 100644 --- a/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimProvisioningServiceTest.kt +++ b/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimProvisioningServiceTest.kt @@ -64,7 +64,7 @@ internal class SimProvisioningServiceTest { val clock = DelayControllerClockAdapter(testScope) testScope.launch { - val image = SimWorkloadImage(UUID.randomUUID(), "<unnamed>", emptyMap(), SimFlopsWorkload(1000, 2)) + val image = SimWorkloadImage(UUID.randomUUID(), "<unnamed>", emptyMap(), SimFlopsWorkload(1000)) val driver = SimBareMetalDriver(this, clock, UUID.randomUUID(), "test", emptyMap(), machineModel) val provisioner = SimpleProvisioningService() diff --git a/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimVirtDriverTest.kt b/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimVirtDriverTest.kt index a0c61f29..1831eae0 100644 --- a/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimVirtDriverTest.kt +++ b/simulator/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimVirtDriverTest.kt @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll import org.opendc.compute.core.Flavor import org.opendc.compute.core.virt.HypervisorEvent +import org.opendc.simulator.compute.SimFairShareHypervisorProvider import org.opendc.simulator.compute.SimMachineModel import org.opendc.simulator.compute.model.MemoryUnit import org.opendc.simulator.compute.model.ProcessingNode @@ -66,17 +67,17 @@ internal class SimVirtDriverTest { } /** - * Test overcommissioning of a hypervisor. + * Test overcommitting of resources by the hypervisor. */ @Test - fun overcommission() { - var requestedBurst = 0L - var grantedBurst = 0L - var overcommissionedBurst = 0L + fun testOvercommitted() { + var requestedWork = 0L + var grantedWork = 0L + var overcommittedWork = 0L scope.launch { - val virtDriverWorkload = SimVirtDriverWorkload() - val vmm = SimWorkloadImage(UUID.randomUUID(), "vmm", emptyMap(), virtDriverWorkload) + val virtDriver = SimVirtDriver(this, SimFairShareHypervisorProvider()) + val vmm = SimWorkloadImage(UUID.randomUUID(), "vmm", emptyMap(), virtDriver) val duration = 5 * 60L val vmImageA = SimWorkloadImage( UUID.randomUUID(), @@ -84,10 +85,10 @@ internal class SimVirtDriverTest { emptyMap(), SimTraceWorkload( sequenceOf( - SimTraceWorkload.Fragment(0, 28L * duration, duration * 1000, 28.0, 2), - SimTraceWorkload.Fragment(0, 3500L * duration, duration * 1000, 3500.0, 2), - SimTraceWorkload.Fragment(0, 0, duration * 1000, 0.0, 2), - SimTraceWorkload.Fragment(0, 183L * duration, duration * 1000, 183.0, 2) + SimTraceWorkload.Fragment(duration * 1000, 28.0, 2), + SimTraceWorkload.Fragment(duration * 1000, 3500.0, 2), + SimTraceWorkload.Fragment(duration * 1000, 0.0, 2), + SimTraceWorkload.Fragment(duration * 1000, 183.0, 2) ), ) ) @@ -97,10 +98,10 @@ internal class SimVirtDriverTest { emptyMap(), SimTraceWorkload( sequenceOf( - SimTraceWorkload.Fragment(0, 28L * duration, duration * 1000, 28.0, 2), - SimTraceWorkload.Fragment(0, 3100L * duration, duration * 1000, 3100.0, 2), - SimTraceWorkload.Fragment(0, 0, duration * 1000, 0.0, 2), - SimTraceWorkload.Fragment(0, 73L * duration, duration * 1000, 73.0, 2) + SimTraceWorkload.Fragment(duration * 1000, 28.0, 2), + SimTraceWorkload.Fragment(duration * 1000, 3100.0, 2), + SimTraceWorkload.Fragment(duration * 1000, 0.0, 2), + SimTraceWorkload.Fragment(duration * 1000, 73.0, 2) ) ), ) @@ -115,31 +116,30 @@ internal class SimVirtDriverTest { delay(5) val flavor = Flavor(2, 0) - val vmDriver = virtDriverWorkload.driver - vmDriver.events + virtDriver.events .onEach { event -> when (event) { is HypervisorEvent.SliceFinished -> { - requestedBurst += event.requestedBurst - grantedBurst += event.grantedBurst - overcommissionedBurst += event.overcommissionedBurst + requestedWork += event.requestedBurst + grantedWork += event.grantedBurst + overcommittedWork += event.overcommissionedBurst } } } .launchIn(this) - vmDriver.spawn("a", vmImageA, flavor) - vmDriver.spawn("b", vmImageB, flavor) + virtDriver.spawn("a", vmImageA, flavor) + virtDriver.spawn("b", vmImageB, flavor) } scope.advanceUntilIdle() assertAll( { assertEquals(emptyList<Throwable>(), scope.uncaughtExceptions, "No errors") }, - { assertEquals(2073600, requestedBurst, "Requested Burst does not match") }, - { assertEquals(2013600, grantedBurst, "Granted Burst does not match") }, - { assertEquals(60000, overcommissionedBurst, "Overcommissioned Burst does not match") }, - { assertEquals(1200007, scope.currentTime) } + { assertEquals(4197600, requestedWork, "Requested work does not match") }, + { assertEquals(3057600, grantedWork, "Granted work does not match") }, + { assertEquals(1140000, overcommittedWork, "Overcommitted work does not match") }, + { assertEquals(1200006, scope.currentTime) } ) } } diff --git a/simulator/opendc-experiments/opendc-experiments-sc18/build.gradle.kts b/simulator/opendc-experiments/opendc-experiments-sc18/build.gradle.kts index 9cf72f18..b6b35694 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc18/build.gradle.kts +++ b/simulator/opendc-experiments/opendc-experiments-sc18/build.gradle.kts @@ -29,14 +29,16 @@ plugins { } application { - mainClassName = "org.opendc.experiments.sc18.TestExperiment" + mainClass.set("org.opendc.harness.runner.console.ConsoleRunnerKt") } dependencies { api(project(":opendc-core")) + api(project(":opendc-harness")) implementation(project(":opendc-format")) implementation(project(":opendc-workflows")) implementation(project(":opendc-simulator:opendc-simulator-core")) + implementation(project(":opendc-compute:opendc-compute-simulator")) implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8") { exclude("org.jetbrains.kotlin", module = "kotlin-reflect") } diff --git a/simulator/opendc-experiments/opendc-experiments-sc18/src/main/kotlin/org/opendc/experiments/sc18/TestExperiment.kt b/simulator/opendc-experiments/opendc-experiments-sc18/src/main/kotlin/org/opendc/experiments/sc18/TestExperiment.kt deleted file mode 100644 index 3786eebf..00000000 --- a/simulator/opendc-experiments/opendc-experiments-sc18/src/main/kotlin/org/opendc/experiments/sc18/TestExperiment.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.experiments.sc18 - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.test.TestCoroutineScope -import org.opendc.compute.core.metal.service.ProvisioningService -import org.opendc.format.environment.sc18.Sc18EnvironmentReader -import org.opendc.format.trace.gwf.GwfTraceReader -import org.opendc.simulator.utils.DelayControllerClockAdapter -import org.opendc.workflows.service.StageWorkflowService -import org.opendc.workflows.service.WorkflowEvent -import org.opendc.workflows.service.WorkflowSchedulerMode -import org.opendc.workflows.service.stage.job.NullJobAdmissionPolicy -import org.opendc.workflows.service.stage.job.SubmissionTimeJobOrderPolicy -import org.opendc.workflows.service.stage.resource.FirstFitResourceSelectionPolicy -import org.opendc.workflows.service.stage.resource.FunctionalResourceFilterPolicy -import org.opendc.workflows.service.stage.task.NullTaskEligibilityPolicy -import org.opendc.workflows.service.stage.task.SubmissionTimeTaskOrderPolicy -import java.io.File -import kotlin.math.max - -/** - * Main entry point of the experiment. - */ -@OptIn(ExperimentalCoroutinesApi::class) -public fun main(args: Array<String>) { - if (args.isEmpty()) { - println("error: Please provide path to GWF trace") - return - } - - var total = 0 - var finished = 0 - - val token = Channel<Boolean>() - val testScope = TestCoroutineScope() - val clock = DelayControllerClockAdapter(testScope) - - val schedulerAsync = testScope.async { - val environment = Sc18EnvironmentReader(object {}.javaClass.getResourceAsStream("/env/setup-test.json")) - .use { it.construct(this, clock) } - - StageWorkflowService( - this, - clock, - environment.platforms[0].zones[0].services[ProvisioningService], - mode = WorkflowSchedulerMode.Batch(100), - jobAdmissionPolicy = NullJobAdmissionPolicy, - jobOrderPolicy = SubmissionTimeJobOrderPolicy(), - taskEligibilityPolicy = NullTaskEligibilityPolicy, - taskOrderPolicy = SubmissionTimeTaskOrderPolicy(), - resourceFilterPolicy = FunctionalResourceFilterPolicy, - resourceSelectionPolicy = FirstFitResourceSelectionPolicy - ) - } - - testScope.launch { - val scheduler = schedulerAsync.await() - scheduler.events - .onEach { event -> - when (event) { - is WorkflowEvent.JobStarted -> { - println("Job ${event.job.uid} started") - } - is WorkflowEvent.JobFinished -> { - finished += 1 - println("Jobs $finished/$total finished (${event.job.tasks.size} tasks)") - - if (finished == total) { - token.send(true) - } - } - } - } - .collect() - } - - testScope.launch { - val reader = GwfTraceReader(File(args[0])) - val scheduler = schedulerAsync.await() - - while (reader.hasNext()) { - val (time, job) = reader.next() - total += 1 - delay(max(0, time * 1000 - clock.millis())) - scheduler.submit(job) - } - } - - testScope.advanceUntilIdle() -} diff --git a/simulator/opendc-experiments/opendc-experiments-sc18/src/main/kotlin/org/opendc/experiments/sc18/UnderspecificationExperiment.kt b/simulator/opendc-experiments/opendc-experiments-sc18/src/main/kotlin/org/opendc/experiments/sc18/UnderspecificationExperiment.kt new file mode 100644 index 00000000..6d2c0ec7 --- /dev/null +++ b/simulator/opendc-experiments/opendc-experiments-sc18/src/main/kotlin/org/opendc/experiments/sc18/UnderspecificationExperiment.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.experiments.sc18 + +import kotlinx.coroutines.* +import kotlinx.coroutines.test.TestCoroutineScope +import org.opendc.compute.core.metal.service.ProvisioningService +import org.opendc.compute.simulator.SimVirtProvisioningService +import org.opendc.compute.simulator.allocation.NumberOfActiveServersAllocationPolicy +import org.opendc.format.environment.sc18.Sc18EnvironmentReader +import org.opendc.format.trace.gwf.GwfTraceReader +import org.opendc.harness.dsl.Experiment +import org.opendc.harness.dsl.anyOf +import org.opendc.simulator.compute.SimSpaceSharedHypervisorProvider +import org.opendc.simulator.utils.DelayControllerClockAdapter +import org.opendc.trace.core.EventTracer +import org.opendc.trace.core.enable +import org.opendc.workflows.service.StageWorkflowService +import org.opendc.workflows.service.WorkflowEvent +import org.opendc.workflows.service.WorkflowSchedulerMode +import org.opendc.workflows.service.stage.job.NullJobAdmissionPolicy +import org.opendc.workflows.service.stage.job.SubmissionTimeJobOrderPolicy +import org.opendc.workflows.service.stage.task.NullTaskEligibilityPolicy +import org.opendc.workflows.service.stage.task.SubmissionTimeTaskOrderPolicy +import java.io.File +import java.io.FileInputStream +import kotlin.math.max + +/** + * The [UnderspecificationExperiment] investigates the impact of scheduler underspecification on performance. + * It focuses on components that must exist (that is, based on their own publications, the correct operation of the + * schedulers under study requires these components), yet have been left underspecified by their author. + */ +public class UnderspecificationExperiment : Experiment("underspecification") { + /** + * The workflow traces to test. + */ + private val trace: String by anyOf("traces/chronos_exp_noscaler_ca.gwf") + + /** + * The datacenter environments to test. + */ + private val environment: String by anyOf("environments/base.json") + + @OptIn(ExperimentalCoroutinesApi::class) + override fun doRun(repeat: Int) { + val testScope = TestCoroutineScope() + val clock = DelayControllerClockAdapter(testScope) + val tracer = EventTracer(clock) + val recording = tracer.openRecording().run { + enable<WorkflowEvent.JobSubmitted>() + enable<WorkflowEvent.JobStarted>() + enable<WorkflowEvent.JobFinished>() + enable<WorkflowEvent.TaskStarted>() + enable<WorkflowEvent.TaskFinished>() + this + } + + testScope.launch { + launch { println("MAKESPAN: ${recording.workflowRuntime()}") } + launch { println("WAIT: ${recording.workflowWaitingTime()}") } + recording.start() + } + + testScope.launch { + val environment = Sc18EnvironmentReader(FileInputStream(File(environment))) + .use { it.construct(testScope, clock) } + + val bareMetal = environment.platforms[0].zones[0].services[ProvisioningService] + + // Wait for the bare metal nodes to be spawned + delay(10) + + val provisioner = SimVirtProvisioningService( + testScope, + clock, + bareMetal, + NumberOfActiveServersAllocationPolicy(), + tracer, + SimSpaceSharedHypervisorProvider(), + schedulingQuantum = 1000 + ) + + // Wait for the hypervisors to be spawned + delay(10) + + val scheduler = StageWorkflowService( + testScope, + clock, + tracer, + provisioner, + mode = WorkflowSchedulerMode.Batch(100), + jobAdmissionPolicy = NullJobAdmissionPolicy, + jobOrderPolicy = SubmissionTimeJobOrderPolicy(), + taskEligibilityPolicy = NullTaskEligibilityPolicy, + taskOrderPolicy = SubmissionTimeTaskOrderPolicy(), + ) + + val reader = GwfTraceReader(File(trace)) + + while (reader.hasNext()) { + val (time, job) = reader.next() + delay(max(0, time * 1000 - clock.millis())) + scheduler.submit(job) + } + } + + testScope.advanceUntilIdle() + recording.close() + + // Check whether everything went okay + testScope.uncaughtExceptions.forEach { it.printStackTrace() } + assert(testScope.uncaughtExceptions.isEmpty()) { "Errors occurred during execution of the experiment" } + } +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc18/src/main/kotlin/org/opendc/experiments/sc18/WorkflowMetrics.kt b/simulator/opendc-experiments/opendc-experiments-sc18/src/main/kotlin/org/opendc/experiments/sc18/WorkflowMetrics.kt new file mode 100644 index 00000000..dbd04b87 --- /dev/null +++ b/simulator/opendc-experiments/opendc-experiments-sc18/src/main/kotlin/org/opendc/experiments/sc18/WorkflowMetrics.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.experiments.sc18 + +import org.opendc.trace.core.EventStream +import org.opendc.trace.core.onEvent +import org.opendc.workflows.service.WorkflowEvent +import java.util.* +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +/** + * This function collects the makespan of workflows that appear in the event stream. + */ +public suspend fun EventStream.workflowRuntime(): Map<UUID, Long> = suspendCoroutine { cont -> + val starts = mutableMapOf<UUID, Long>() + val results = mutableMapOf<UUID, Long>() + + onEvent<WorkflowEvent.JobStarted> { + starts[it.job.uid] = it.timestamp + } + onEvent<WorkflowEvent.JobFinished> { + val start = starts.remove(it.job.uid) ?: return@onEvent + results[it.job.uid] = it.timestamp - start + } + onClose { cont.resume(results) } +} + +/** + * This function collects the waiting time of workflows that appear in the event stream, which the duration between the + * workflow submission and the start of the first task. + */ +public suspend fun EventStream.workflowWaitingTime(): Map<UUID, Long> = suspendCoroutine { cont -> + val starts = mutableMapOf<UUID, Long>() + val results = mutableMapOf<UUID, Long>() + + onEvent<WorkflowEvent.JobStarted> { + starts[it.job.uid] = it.timestamp + } + onEvent<WorkflowEvent.TaskStarted> { + results.computeIfAbsent(it.job.uid) { _ -> + val start = starts.remove(it.job.uid)!! + it.timestamp - start + } + } + onClose { cont.resume(results) } +} + +/** + * This function collects the response time of tasks that appear in the event stream. + */ +public suspend fun EventStream.taskResponse(): Map<UUID, Long> = suspendCoroutine { cont -> + val starts = mutableMapOf<UUID, Long>() + val results = mutableMapOf<UUID, Long>() + + onEvent<WorkflowEvent.JobSubmitted> { + for (task in it.job.tasks) { + starts[task.uid] = it.timestamp + } + } + onEvent<WorkflowEvent.TaskFinished> { + val start = starts.remove(it.job.uid) ?: return@onEvent + results[it.task.uid] = it.timestamp - start + } + onClose { cont.resume(results) } +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc18/src/main/resources/env/setup-test.json b/simulator/opendc-experiments/opendc-experiments-sc18/src/main/resources/env/setup-test.json deleted file mode 100644 index 0965b250..00000000 --- a/simulator/opendc-experiments/opendc-experiments-sc18/src/main/resources/env/setup-test.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "Experimental Setup 2", - "rooms": [ - { - "type": "SERVER", - "objects": [ - { - "type": "RACK", - "machines": [ - { "cpus": [2] }, { "cpus": [2]}, - { "cpus": [2] }, { "cpus": [2]}, - { "cpus": [2] }, { "cpus": [2]}, - { "cpus": [2] }, { "cpus": [2]}, - { "cpus": [2] }, { "cpus": [2]}, - { "cpus": [2] }, { "cpus": [2]}, - { "cpus": [2] }, { "cpus": [2]}, - { "cpus": [2] }, { "cpus": [2]} - ] - }, - { - "type": "RACK", - "machines": [ - { "cpus": [1] }, { "cpus": [1]}, - { "cpus": [1] }, { "cpus": [1]}, - { "cpus": [1] }, { "cpus": [1]}, - { "cpus": [1] }, { "cpus": [1]}, - { "cpus": [1] }, { "cpus": [1]}, - { "cpus": [1] }, { "cpus": [1]}, - { "cpus": [1] }, { "cpus": [1]}, - { "cpus": [1] }, { "cpus": [1]} - ] - } - ] - } - ] -} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/build.gradle.kts b/simulator/opendc-experiments/opendc-experiments-sc20/build.gradle.kts index 3b682668..b94207ba 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/build.gradle.kts +++ b/simulator/opendc-experiments/opendc-experiments-sc20/build.gradle.kts @@ -29,21 +29,22 @@ plugins { } application { - mainClassName = "org.opendc.experiments.sc20.MainKt" + mainClass.set("org.opendc.harness.runner.console.ConsoleRunnerKt") applicationDefaultJvmArgs = listOf("-Xms2500M") } dependencies { api(project(":opendc-core")) + api(project(":opendc-harness")) implementation(project(":opendc-format")) implementation(project(":opendc-simulator:opendc-simulator-core")) implementation(project(":opendc-simulator:opendc-simulator-compute")) implementation(project(":opendc-simulator:opendc-simulator-failures")) implementation(project(":opendc-compute:opendc-compute-simulator")) - implementation("com.github.ajalt:clikt:2.6.0") - implementation("me.tongfei:progressbar:0.8.1") - implementation("io.github.microutils:kotlin-logging:1.7.9") + implementation("io.github.microutils:kotlin-logging:2.0.4") + implementation("me.tongfei:progressbar:0.9.0") + implementation("com.github.ajalt.clikt:clikt:3.1.0") implementation("org.apache.parquet:parquet-avro:1.11.0") implementation("org.apache.hadoop:hadoop-client:3.2.1") { @@ -51,8 +52,6 @@ dependencies { exclude(group = "log4j") } - runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:2.13.1") - testImplementation("org.junit.jupiter:junit-jupiter-api:${Library.JUNIT_JUPITER}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Library.JUNIT_JUPITER}") testImplementation("org.junit.platform:junit-platform-launcher:${Library.JUNIT_PLATFORM}") diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/Main.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/Main.kt deleted file mode 100644 index 8916261b..00000000 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/Main.kt +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.experiments.sc20 - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.parameters.options.convert -import com.github.ajalt.clikt.parameters.options.default -import com.github.ajalt.clikt.parameters.options.defaultLazy -import com.github.ajalt.clikt.parameters.options.multiple -import com.github.ajalt.clikt.parameters.options.option -import com.github.ajalt.clikt.parameters.options.required -import com.github.ajalt.clikt.parameters.types.choice -import com.github.ajalt.clikt.parameters.types.file -import com.github.ajalt.clikt.parameters.types.int -import mu.KotlinLogging -import org.opendc.experiments.sc20.experiment.* -import org.opendc.experiments.sc20.reporter.ConsoleExperimentReporter -import org.opendc.experiments.sc20.runner.ExperimentDescriptor -import org.opendc.experiments.sc20.runner.execution.ThreadPoolExperimentScheduler -import org.opendc.experiments.sc20.runner.internal.DefaultExperimentRunner -import org.opendc.format.trace.sc20.Sc20PerformanceInterferenceReader -import org.opendc.format.trace.sc20.Sc20VmPlacementReader -import java.io.File - -/** - * The logger for this experiment. - */ -private val logger = KotlinLogging.logger {} - -/** - * Represents the command for running the experiment. - */ -public class ExperimentCli : CliktCommand(name = "sc20-experiment") { - /** - * The path to the directory where the topology descriptions are located. - */ - private val environmentPath by option("--environment-path", help = "path to the environment directory") - .file(canBeFile = false) - .required() - - /** - * The path to the directory where the traces are located. - */ - private val tracePath by option("--trace-path", help = "path to the traces directory") - .file(canBeFile = false) - .required() - - /** - * The path to the performance interference model. - */ - private val performanceInterferenceStream by option( - "--performance-interference-model", - help = "path to the performance interference file" - ) - .file(canBeDir = false) - .convert { it.inputStream() } - - /** - * The path to the original VM placements file. - */ - private val vmPlacements by option("--vm-placements-file", help = "path to the VM placement file") - .file(canBeDir = false) - .convert { - Sc20VmPlacementReader(it.inputStream().buffered()).construct() - } - .default(emptyMap()) - - /** - * The selected portfolios to run. - */ - private val portfolios by option("--portfolio", help = "portfolio of scenarios to explore") - .choice( - "hor-ver" to { experiment: Experiment, i: Int -> HorVerPortfolio(experiment, i) } - as (Experiment, Int) -> Portfolio, - "more-velocity" to { experiment, i -> MoreVelocityPortfolio(experiment, i) }, - "composite-workload" to { experiment, i -> CompositeWorkloadPortfolio(experiment, i) }, - "operational-phenomena" to { experiment, i -> OperationalPhenomenaPortfolio(experiment, i) }, - "replay" to { experiment, i -> ReplayPortfolio(experiment, i) }, - "test" to { experiment, i -> TestPortfolio(experiment, i) }, - "more-hpc" to { experiment, i -> MoreHpcPortfolio(experiment, i) }, - ignoreCase = true - ) - .multiple(required = true) - - /** - * The maximum number of worker threads to use. - */ - private val parallelism by option("--parallelism", help = "maximum number of concurrent simulation runs") - .int() - .default(Runtime.getRuntime().availableProcessors()) - - /** - * The buffer size for writing results. - */ - private val bufferSize by option("--buffer-size") - .int() - .default(4096) - - /** - * The path to the output directory. - */ - private val output by option("-O", "--output", help = "path to the output directory") - .file(canBeFile = false) - .defaultLazy { File("data") } - - override fun run() { - logger.info { "Constructing performance interference model" } - - val performanceInterferenceModel = - performanceInterferenceStream?.let { Sc20PerformanceInterferenceReader(it) } - - logger.info { "Creating experiment descriptor" } - val descriptor = object : - Experiment(environmentPath, tracePath, output, performanceInterferenceModel, vmPlacements, bufferSize) { - private val descriptor = this - override val children: Sequence<ExperimentDescriptor> = sequence { - for ((i, producer) in portfolios.withIndex()) { - yield(producer(descriptor, i)) - } - } - } - - logger.info { "Starting experiment runner [parallelism=$parallelism]" } - val scheduler = ThreadPoolExperimentScheduler(parallelism) - val runner = DefaultExperimentRunner(scheduler) - val reporter = ConsoleExperimentReporter() - try { - runner.execute(descriptor, reporter) - } finally { - scheduler.close() - reporter.close() - } - } -} - -/** - * Main entry point of the experiment. - */ -public fun main(args: Array<String>): Unit = ExperimentCli().main(args) diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/CompositeWorkloadPortfolio.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/CompositeWorkloadPortfolio.kt new file mode 100644 index 00000000..f4242456 --- /dev/null +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/CompositeWorkloadPortfolio.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.experiments.sc20.experiment + +import org.opendc.experiments.sc20.experiment.model.CompositeWorkload +import org.opendc.experiments.sc20.experiment.model.OperationalPhenomena +import org.opendc.experiments.sc20.experiment.model.Topology +import org.opendc.experiments.sc20.experiment.model.Workload +import org.opendc.harness.dsl.anyOf + +/** + * A [Portfolio] that explores the effect of a composite workload. + */ +public class CompositeWorkloadPortfolio : Portfolio("composite-workload") { + private val totalSampleLoad = 1.3301733005049648E12 + + override val topology: Topology by anyOf( + Topology("base"), + Topology("exp-vol-hor-hom"), + Topology("exp-vol-ver-hom"), + Topology("exp-vel-ver-hom") + ) + + override val workload: Workload by anyOf( + CompositeWorkload( + "all-azure", + listOf(Workload("solvinity-short", 0.0), Workload("azure", 1.0)), + totalSampleLoad + ), + CompositeWorkload( + "solvinity-25-azure-75", + listOf(Workload("solvinity-short", 0.25), Workload("azure", 0.75)), + totalSampleLoad + ), + CompositeWorkload( + "solvinity-50-azure-50", + listOf(Workload("solvinity-short", 0.5), Workload("azure", 0.5)), + totalSampleLoad + ), + CompositeWorkload( + "solvinity-75-azure-25", + listOf(Workload("solvinity-short", 0.75), Workload("azure", 0.25)), + totalSampleLoad + ), + CompositeWorkload( + "all-solvinity", + listOf(Workload("solvinity-short", 1.0), Workload("azure", 0.0)), + totalSampleLoad + ) + ) + + override val operationalPhenomena: OperationalPhenomena by anyOf( + OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = false) + ) + + override val allocationPolicy: String by anyOf( + "active-servers" + ) +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Experiment.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Experiment.kt deleted file mode 100644 index 34d7301b..00000000 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Experiment.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.experiments.sc20.experiment - -import org.opendc.experiments.sc20.runner.ContainerExperimentDescriptor -import org.opendc.experiments.sc20.runner.ExperimentDescriptor -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionContext -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionListener -import org.opendc.experiments.sc20.telemetry.RunEvent -import org.opendc.experiments.sc20.telemetry.parquet.ParquetRunEventWriter -import org.opendc.format.trace.PerformanceInterferenceModelReader -import java.io.File - -/** - * The global configuration of the experiment. - * - * @param environments The path to the topologies directory. - * @param traces The path to the traces directory. - * @param output The output directory. - * @param performanceInterferenceModel The optional performance interference model that has been specified. - * @param vmPlacements Original VM placement in the trace. - * @param bufferSize The buffer size of the event reporters. - */ -public abstract class Experiment( - public val environments: File, - public val traces: File, - public val output: File, - public val performanceInterferenceModel: PerformanceInterferenceModelReader?, - public val vmPlacements: Map<String, String>, - public val bufferSize: Int -) : ContainerExperimentDescriptor() { - override val parent: ExperimentDescriptor? = null - - override suspend fun invoke(context: ExperimentExecutionContext) { - val writer = ParquetRunEventWriter(File(output, "experiments.parquet"), bufferSize) - try { - val listener = object : ExperimentExecutionListener by context.listener { - override fun descriptorRegistered(descriptor: ExperimentDescriptor) { - if (descriptor is Run) { - writer.write(RunEvent(descriptor, System.currentTimeMillis())) - } - - context.listener.descriptorRegistered(descriptor) - } - } - - val newContext = object : ExperimentExecutionContext by context { - override val listener: ExperimentExecutionListener = listener - } - - super.invoke(newContext) - } finally { - writer.close() - } - } -} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/ExperimentHelpers.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/ExperimentHelpers.kt index 09f44199..1e01e892 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/ExperimentHelpers.kt +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/ExperimentHelpers.kt @@ -48,10 +48,12 @@ import org.opendc.experiments.sc20.experiment.monitor.ExperimentMonitor import org.opendc.experiments.sc20.trace.Sc20StreamingParquetTraceReader import org.opendc.format.environment.EnvironmentReader import org.opendc.format.trace.TraceReader +import org.opendc.simulator.compute.SimFairShareHypervisorProvider import org.opendc.simulator.compute.interference.PerformanceInterferenceModel import org.opendc.simulator.failures.CorrelatedFaultInjector import org.opendc.simulator.failures.FailureDomain import org.opendc.simulator.failures.FaultInjector +import org.opendc.trace.core.EventTracer import java.io.File import java.time.Clock import kotlin.math.ln @@ -140,7 +142,8 @@ public suspend fun createProvisioner( coroutineScope: CoroutineScope, clock: Clock, environmentReader: EnvironmentReader, - allocationPolicy: AllocationPolicy + allocationPolicy: AllocationPolicy, + eventTracer: EventTracer ): Pair<ProvisioningService, SimVirtProvisioningService> { val environment = environmentReader.use { it.construct(coroutineScope, clock) } val bareMetalProvisioner = environment.platforms[0].zones[0].services[ProvisioningService] @@ -148,7 +151,7 @@ public suspend fun createProvisioner( // Wait for the bare metal nodes to be spawned delay(10) - val scheduler = SimVirtProvisioningService(coroutineScope, clock, bareMetalProvisioner, allocationPolicy) + val scheduler = SimVirtProvisioningService(coroutineScope, clock, bareMetalProvisioner, allocationPolicy, eventTracer, SimFairShareHypervisorProvider()) // Wait for the hypervisors to be spawned delay(10) diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/HorVerPortfolio.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/HorVerPortfolio.kt new file mode 100644 index 00000000..aa97b808 --- /dev/null +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/HorVerPortfolio.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.experiments.sc20.experiment + +import org.opendc.experiments.sc20.experiment.model.OperationalPhenomena +import org.opendc.experiments.sc20.experiment.model.Topology +import org.opendc.experiments.sc20.experiment.model.Workload +import org.opendc.harness.dsl.anyOf + +/** + * A [Portfolio] that explores the difference between horizontal and vertical scaling. + */ +public class HorVerPortfolio : Portfolio("horizontal_vs_vertical") { + override val topology: Topology by anyOf( + Topology("base"), + Topology("rep-vol-hor-hom"), + Topology("rep-vol-hor-het"), + Topology("rep-vol-ver-hom"), + Topology("rep-vol-ver-het"), + Topology("exp-vol-hor-hom"), + Topology("exp-vol-hor-het"), + Topology("exp-vol-ver-hom"), + Topology("exp-vol-ver-het") + ) + + override val workload: Workload by anyOf( + Workload("solvinity", 0.1), + Workload("solvinity", 0.25), + Workload("solvinity", 0.5), + Workload("solvinity", 1.0) + ) + + override val operationalPhenomena: OperationalPhenomena by anyOf( + OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = true) + ) + + override val allocationPolicy: String by anyOf( + "active-servers" + ) +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/MoreHpcPortfolio.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/MoreHpcPortfolio.kt new file mode 100644 index 00000000..bdb33f59 --- /dev/null +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/MoreHpcPortfolio.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.experiments.sc20.experiment + +import org.opendc.experiments.sc20.experiment.model.* +import org.opendc.harness.dsl.anyOf + +/** + * A [Portfolio] to explore the effect of HPC workloads. + */ +public class MoreHpcPortfolio : Portfolio("more_hpc") { + override val topology: Topology by anyOf( + Topology("base"), + Topology("exp-vol-hor-hom"), + Topology("exp-vol-ver-hom"), + Topology("exp-vel-ver-hom") + ) + + override val workload: Workload by anyOf( + Workload("solvinity", 0.0, samplingStrategy = SamplingStrategy.HPC), + Workload("solvinity", 0.25, samplingStrategy = SamplingStrategy.HPC), + Workload("solvinity", 0.5, samplingStrategy = SamplingStrategy.HPC), + Workload("solvinity", 1.0, samplingStrategy = SamplingStrategy.HPC), + Workload("solvinity", 0.25, samplingStrategy = SamplingStrategy.HPC_LOAD), + Workload("solvinity", 0.5, samplingStrategy = SamplingStrategy.HPC_LOAD), + Workload("solvinity", 1.0, samplingStrategy = SamplingStrategy.HPC_LOAD) + ) + + override val operationalPhenomena: OperationalPhenomena by anyOf( + OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = true) + ) + + override val allocationPolicy: String by anyOf( + "active-servers" + ) +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/MoreVelocityPortfolio.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/MoreVelocityPortfolio.kt new file mode 100644 index 00000000..733dabf6 --- /dev/null +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/MoreVelocityPortfolio.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.experiments.sc20.experiment + +import org.opendc.experiments.sc20.experiment.model.OperationalPhenomena +import org.opendc.experiments.sc20.experiment.model.Topology +import org.opendc.experiments.sc20.experiment.model.Workload +import org.opendc.harness.dsl.anyOf + +/** + * A [Portfolio] that explores the effect of adding more velocity to a cluster (e.g., faster machines). + */ +public class MoreVelocityPortfolio : Portfolio("more_velocity") { + override val topology: Topology by anyOf( + Topology("base"), + Topology("rep-vel-ver-hom"), + Topology("rep-vel-ver-het"), + Topology("exp-vel-ver-hom"), + Topology("exp-vel-ver-het") + ) + + override val workload: Workload by anyOf( + Workload("solvinity", 0.1), + Workload("solvinity", 0.25), + Workload("solvinity", 0.5), + Workload("solvinity", 1.0) + ) + + override val operationalPhenomena: OperationalPhenomena by anyOf( + OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = true) + ) + + override val allocationPolicy: String by anyOf( + "active-servers" + ) +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/OperationalPhenomenaPortfolio.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/OperationalPhenomenaPortfolio.kt new file mode 100644 index 00000000..66b94faf --- /dev/null +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/OperationalPhenomenaPortfolio.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.experiments.sc20.experiment + +import org.opendc.experiments.sc20.experiment.model.OperationalPhenomena +import org.opendc.experiments.sc20.experiment.model.Topology +import org.opendc.experiments.sc20.experiment.model.Workload +import org.opendc.harness.dsl.anyOf + +/** + * A [Portfolio] that explores the effect of operational phenomena on metrics. + */ +public class OperationalPhenomenaPortfolio : Portfolio("operational_phenomena") { + override val topology: Topology by anyOf( + Topology("base") + ) + + override val workload: Workload by anyOf( + Workload("solvinity", 0.1), + Workload("solvinity", 0.25), + Workload("solvinity", 0.5), + Workload("solvinity", 1.0) + ) + + override val operationalPhenomena: OperationalPhenomena by anyOf( + OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = true), + OperationalPhenomena(failureFrequency = 0.0, hasInterference = true), + OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = false), + OperationalPhenomena(failureFrequency = 0.0, hasInterference = false) + ) + + override val allocationPolicy: String by anyOf( + "mem", + "mem-inv", + "core-mem", + "core-mem-inv", + "active-servers", + "active-servers-inv", + "random" + ) +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Portfolio.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Portfolio.kt index 37cf2880..4a82ad56 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Portfolio.kt +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Portfolio.kt @@ -22,67 +22,196 @@ package org.opendc.experiments.sc20.experiment +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestCoroutineScope +import mu.KotlinLogging +import org.opendc.compute.simulator.allocation.* +import org.opendc.experiments.sc20.experiment.model.CompositeWorkload import org.opendc.experiments.sc20.experiment.model.OperationalPhenomena import org.opendc.experiments.sc20.experiment.model.Topology import org.opendc.experiments.sc20.experiment.model.Workload -import org.opendc.experiments.sc20.runner.ContainerExperimentDescriptor +import org.opendc.experiments.sc20.experiment.monitor.ParquetExperimentMonitor +import org.opendc.experiments.sc20.trace.Sc20ParquetTraceReader +import org.opendc.experiments.sc20.trace.Sc20RawParquetTraceReader +import org.opendc.format.environment.sc20.Sc20ClusterEnvironmentReader +import org.opendc.format.trace.PerformanceInterferenceModelReader +import org.opendc.harness.dsl.Experiment +import org.opendc.harness.dsl.anyOf +import org.opendc.simulator.utils.DelayControllerClockAdapter +import org.opendc.trace.core.EventTracer +import java.io.File +import java.util.concurrent.ConcurrentHashMap +import kotlin.random.Random /** - * A portfolio represents a collection of scenarios are tested. + * A portfolio represents a collection of scenarios are tested for the work. + * + * @param name The name of the portfolio. */ -public abstract class Portfolio( - override val parent: Experiment, - public val id: Int, - public val name: String -) : ContainerExperimentDescriptor() { +public abstract class Portfolio(name: String) : Experiment(name) { + /** + * The logger for this portfolio instance. + */ + private val logger = KotlinLogging.logger {} + + /** + * The path to where the environments are located. + */ + private val environmentPath by anyOf(File("environments/")) + + /** + * The path to where the traces are located. + */ + private val tracePath by anyOf(File("traces/")) + /** - * The topologies to consider. + * The path to where the output results should be written. */ - protected abstract val topologies: List<Topology> + private val outputPath by anyOf(File("results/")) /** - * The workloads to consider. + * The path to the original VM placements file. */ - protected abstract val workloads: List<Workload> + private val vmPlacements by anyOf(emptyMap<String, String>()) + + /** + * The path to the performance interference model. + */ + private val performanceInterferenceModel by anyOf<PerformanceInterferenceModelReader?>(null) + + /** + * The topology to test. + */ + public abstract val topology: Topology + + /** + * The workload to test. + */ + public abstract val workload: Workload /** * The operational phenomenas to consider. */ - protected abstract val operationalPhenomenas: List<OperationalPhenomena> + public abstract val operationalPhenomena: OperationalPhenomena /** * The allocation policies to consider. */ - protected abstract val allocationPolicies: List<String> + public abstract val allocationPolicy: String /** - * The number of repetitions to perform. + * A map of trace readers. */ - public open val repetitions: Int = 32 + private val traceReaders = ConcurrentHashMap<String, Sc20RawParquetTraceReader>() /** - * Resolve the children of this container. + * Perform a single trial for this portfolio. */ - override val children: Sequence<Scenario> = sequence { - var id = 0 - for (topology in topologies) { - for (workload in workloads) { - for (operationalPhenomena in operationalPhenomenas) { - for (allocationPolicy in allocationPolicies) { - yield( - Scenario( - this@Portfolio, - id++, - repetitions, - topology, - workload, - allocationPolicy, - operationalPhenomena - ) - ) - } - } + @OptIn(ExperimentalCoroutinesApi::class) + override fun doRun(repeat: Int) { + val testScope = TestCoroutineScope() + val clock = DelayControllerClockAdapter(testScope) + val tracer = EventTracer(clock) + val seeder = Random(repeat) + val environment = Sc20ClusterEnvironmentReader(File(environmentPath, "${topology.name}.txt")) + + val chan = Channel<Unit>(Channel.CONFLATED) + val allocationPolicy = createAllocationPolicy(seeder) + + val workload = workload + val workloadNames = if (workload is CompositeWorkload) { + workload.workloads.map { it.name } + } else { + listOf(workload.name) + } + + val rawReaders = workloadNames.map { workloadName -> + traceReaders.computeIfAbsent(workloadName) { + logger.info { "Loading trace $workloadName" } + Sc20RawParquetTraceReader(File(tracePath, workloadName)) + } + } + + val performanceInterferenceModel = performanceInterferenceModel + ?.takeIf { operationalPhenomena.hasInterference } + ?.construct(seeder) ?: emptyMap() + val trace = Sc20ParquetTraceReader(rawReaders, performanceInterferenceModel, workload, seeder.nextInt()) + + val monitor = ParquetExperimentMonitor( + outputPath, + "portfolio_id=$name/scenario_id=$id/run_id=$repeat", + 4096 + ) + + testScope.launch { + val (bareMetalProvisioner, scheduler) = createProvisioner( + this, + clock, + environment, + allocationPolicy, + tracer + ) + + val failureDomain = if (operationalPhenomena.failureFrequency > 0) { + logger.debug("ENABLING failures") + createFailureDomain( + this, + clock, + seeder.nextInt(), + operationalPhenomena.failureFrequency, + bareMetalProvisioner, + chan + ) + } else { + null } + + attachMonitor(this, clock, scheduler, monitor) + processTrace( + this, + clock, + trace, + scheduler, + chan, + monitor + ) + + logger.debug("SUBMIT=${scheduler.submittedVms}") + logger.debug("FAIL=${scheduler.unscheduledVms}") + logger.debug("QUEUED=${scheduler.queuedVms}") + logger.debug("RUNNING=${scheduler.runningVms}") + logger.debug("FINISHED=${scheduler.finishedVms}") + + failureDomain?.cancel() + scheduler.terminate() + } + + try { + testScope.advanceUntilIdle() + } finally { + monitor.close() + } + } + + /** + * Create the [AllocationPolicy] instance to use for the trial. + */ + private fun createAllocationPolicy(seeder: Random): AllocationPolicy { + return when (allocationPolicy) { + "mem" -> AvailableMemoryAllocationPolicy() + "mem-inv" -> AvailableMemoryAllocationPolicy(true) + "core-mem" -> AvailableCoreMemoryAllocationPolicy() + "core-mem-inv" -> AvailableCoreMemoryAllocationPolicy(true) + "active-servers" -> NumberOfActiveServersAllocationPolicy() + "active-servers-inv" -> NumberOfActiveServersAllocationPolicy(true) + "provisioned-cores" -> ProvisionedCoresAllocationPolicy() + "provisioned-cores-inv" -> ProvisionedCoresAllocationPolicy(true) + "random" -> RandomAllocationPolicy(Random(seeder.nextInt())) + "replay" -> ReplayAllocationPolicy(vmPlacements) + else -> throw IllegalArgumentException("Unknown policy $allocationPolicy") } } } diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Portfolios.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Portfolios.kt deleted file mode 100644 index 249a63b9..00000000 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Portfolios.kt +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.experiments.sc20.experiment - -import org.opendc.experiments.sc20.experiment.model.* - -public class HorVerPortfolio(parent: Experiment, id: Int) : Portfolio(parent, id, "horizontal_vs_vertical") { - override val topologies: List<Topology> = listOf( - Topology("base"), - Topology("rep-vol-hor-hom"), - Topology("rep-vol-hor-het"), - Topology("rep-vol-ver-hom"), - Topology("rep-vol-ver-het"), - Topology("exp-vol-hor-hom"), - Topology("exp-vol-hor-het"), - Topology("exp-vol-ver-hom"), - Topology("exp-vol-ver-het") - ) - - override val workloads: List<Workload> = listOf( - Workload("solvinity", 0.1), - Workload("solvinity", 0.25), - Workload("solvinity", 0.5), - Workload("solvinity", 1.0) - ) - - override val operationalPhenomenas: List<OperationalPhenomena> = listOf( - OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = true) - ) - - override val allocationPolicies: List<String> = listOf( - "active-servers" - ) -} - -public class MoreVelocityPortfolio(parent: Experiment, id: Int) : Portfolio(parent, id, "more_velocity") { - override val topologies: List<Topology> = listOf( - Topology("base"), - Topology("rep-vel-ver-hom"), - Topology("rep-vel-ver-het"), - Topology("exp-vel-ver-hom"), - Topology("exp-vel-ver-het") - ) - - override val workloads: List<Workload> = listOf( - Workload("solvinity", 0.1), - Workload("solvinity", 0.25), - Workload("solvinity", 0.5), - Workload("solvinity", 1.0) - ) - - override val operationalPhenomenas: List<OperationalPhenomena> = listOf( - OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = true) - ) - - override val allocationPolicies: List<String> = listOf( - "active-servers" - ) -} - -public class CompositeWorkloadPortfolio(parent: Experiment, id: Int) : Portfolio(parent, id, "composite-workload") { - private val totalSampleLoad = 1.3301733005049648E12 - - override val topologies: List<Topology> = listOf( - Topology("base"), - Topology("exp-vol-hor-hom"), - Topology("exp-vol-ver-hom"), - Topology("exp-vel-ver-hom") - ) - - override val workloads: List<Workload> = listOf( - CompositeWorkload( - "all-azure", - listOf(Workload("solvinity-short", 0.0), Workload("azure", 1.0)), - totalSampleLoad - ), - CompositeWorkload( - "solvinity-25-azure-75", - listOf(Workload("solvinity-short", 0.25), Workload("azure", 0.75)), - totalSampleLoad - ), - CompositeWorkload( - "solvinity-50-azure-50", - listOf(Workload("solvinity-short", 0.5), Workload("azure", 0.5)), - totalSampleLoad - ), - CompositeWorkload( - "solvinity-75-azure-25", - listOf(Workload("solvinity-short", 0.75), Workload("azure", 0.25)), - totalSampleLoad - ), - CompositeWorkload( - "all-solvinity", - listOf(Workload("solvinity-short", 1.0), Workload("azure", 0.0)), - totalSampleLoad - ) - ) - - override val operationalPhenomenas: List<OperationalPhenomena> = listOf( - OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = false) - ) - - override val allocationPolicies: List<String> = listOf( - "active-servers" - ) -} - -public class OperationalPhenomenaPortfolio(parent: Experiment, id: Int) : - Portfolio(parent, id, "operational_phenomena") { - override val topologies: List<Topology> = listOf( - Topology("base") - ) - - override val workloads: List<Workload> = listOf( - Workload("solvinity", 0.1), - Workload("solvinity", 0.25), - Workload("solvinity", 0.5), - Workload("solvinity", 1.0) - ) - - override val operationalPhenomenas: List<OperationalPhenomena> = listOf( - OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = true), - OperationalPhenomena(failureFrequency = 0.0, hasInterference = true), - OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = false), - OperationalPhenomena(failureFrequency = 0.0, hasInterference = false) - ) - - override val allocationPolicies: List<String> = listOf( - "mem", - "mem-inv", - "core-mem", - "core-mem-inv", - "active-servers", - "active-servers-inv", - "random" - ) -} - -public class ReplayPortfolio(parent: Experiment, id: Int) : Portfolio(parent, id, "replay") { - override val topologies: List<Topology> = listOf( - Topology("base") - ) - - override val workloads: List<Workload> = listOf( - Workload("solvinity", 1.0) - ) - - override val operationalPhenomenas: List<OperationalPhenomena> = listOf( - OperationalPhenomena(failureFrequency = 0.0, hasInterference = false) - ) - - override val allocationPolicies: List<String> = listOf( - "replay", - "active-servers" - ) -} - -public class TestPortfolio(parent: Experiment, id: Int) : Portfolio(parent, id, "test") { - override val repetitions: Int = 1 - - override val topologies: List<Topology> = listOf( - Topology("base") - ) - - override val workloads: List<Workload> = listOf( - Workload("solvinity", 1.0) - ) - - override val operationalPhenomenas: List<OperationalPhenomena> = listOf( - OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = true) - ) - - override val allocationPolicies: List<String> = listOf("active-servers") -} - -public class MoreHpcPortfolio(parent: Experiment, id: Int) : Portfolio(parent, id, "more_hpc") { - override val topologies: List<Topology> = listOf( - Topology("base"), - Topology("exp-vol-hor-hom"), - Topology("exp-vol-ver-hom"), - Topology("exp-vel-ver-hom") - ) - - override val workloads: List<Workload> = listOf( - Workload("solvinity", 0.0, samplingStrategy = SamplingStrategy.HPC), - Workload("solvinity", 0.25, samplingStrategy = SamplingStrategy.HPC), - Workload("solvinity", 0.5, samplingStrategy = SamplingStrategy.HPC), - Workload("solvinity", 1.0, samplingStrategy = SamplingStrategy.HPC), - Workload("solvinity", 0.25, samplingStrategy = SamplingStrategy.HPC_LOAD), - Workload("solvinity", 0.5, samplingStrategy = SamplingStrategy.HPC_LOAD), - Workload("solvinity", 1.0, samplingStrategy = SamplingStrategy.HPC_LOAD) - ) - - override val operationalPhenomenas: List<OperationalPhenomena> = listOf( - OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = true) - ) - - override val allocationPolicies: List<String> = listOf( - "active-servers" - ) -} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Scenario.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/ReplayPortfolio.kt index d092ddd5..8a42a3b4 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Scenario.kt +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/ReplayPortfolio.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 AtLarge Research + * Copyright (c) 2021 AtLarge Research * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,22 +25,26 @@ package org.opendc.experiments.sc20.experiment import org.opendc.experiments.sc20.experiment.model.OperationalPhenomena import org.opendc.experiments.sc20.experiment.model.Topology import org.opendc.experiments.sc20.experiment.model.Workload -import org.opendc.experiments.sc20.runner.ContainerExperimentDescriptor -import org.opendc.experiments.sc20.runner.ExperimentDescriptor +import org.opendc.harness.dsl.anyOf /** - * A scenario represents a single point in the design space (a unique combination of parameters). + * A [Portfolio] that compares the original VM placements against our policies. */ -public class Scenario( - override val parent: Portfolio, - public val id: Int, - public val repetitions: Int, - public val topology: Topology, - public val workload: Workload, - public val allocationPolicy: String, - public val operationalPhenomena: OperationalPhenomena -) : ContainerExperimentDescriptor() { - override val children: Sequence<ExperimentDescriptor> = sequence { - repeat(repetitions) { i -> yield(Run(this@Scenario, i, i)) } - } +public class ReplayPortfolio : Portfolio("replay") { + override val topology: Topology by anyOf( + Topology("base") + ) + + override val workload: Workload by anyOf( + Workload("solvinity", 1.0) + ) + + override val operationalPhenomena: OperationalPhenomena by anyOf( + OperationalPhenomena(failureFrequency = 0.0, hasInterference = false) + ) + + override val allocationPolicy: String by anyOf( + "replay", + "active-servers" + ) } diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Run.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Run.kt deleted file mode 100644 index 660fc882..00000000 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/Run.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.experiments.sc20.experiment - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestCoroutineScope -import mu.KotlinLogging -import org.opendc.compute.simulator.allocation.* -import org.opendc.experiments.sc20.experiment.model.CompositeWorkload -import org.opendc.experiments.sc20.experiment.monitor.ParquetExperimentMonitor -import org.opendc.experiments.sc20.runner.TrialExperimentDescriptor -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionContext -import org.opendc.experiments.sc20.trace.Sc20ParquetTraceReader -import org.opendc.experiments.sc20.trace.Sc20RawParquetTraceReader -import org.opendc.format.environment.sc20.Sc20ClusterEnvironmentReader -import org.opendc.simulator.utils.DelayControllerClockAdapter -import java.io.File -import kotlin.random.Random - -/** - * The logger for the experiment scenario. - */ -private val logger = KotlinLogging.logger {} - -/** - * An experiment run represent a single invocation of a trial and is used to distinguish between repetitions of the - * same set of parameters. - */ -@OptIn(ExperimentalCoroutinesApi::class) -public data class Run(override val parent: Scenario, val id: Int, val seed: Int) : TrialExperimentDescriptor() { - override suspend fun invoke(context: ExperimentExecutionContext) { - val experiment = parent.parent.parent - val testScope = TestCoroutineScope() - val clock = DelayControllerClockAdapter(testScope) - val seeder = Random(seed) - val environment = Sc20ClusterEnvironmentReader(File(experiment.environments, "${parent.topology.name}.txt")) - - val chan = Channel<Unit>(Channel.CONFLATED) - val allocationPolicy = when (parent.allocationPolicy) { - "mem" -> AvailableMemoryAllocationPolicy() - "mem-inv" -> AvailableMemoryAllocationPolicy(true) - "core-mem" -> AvailableCoreMemoryAllocationPolicy() - "core-mem-inv" -> AvailableCoreMemoryAllocationPolicy(true) - "active-servers" -> NumberOfActiveServersAllocationPolicy() - "active-servers-inv" -> NumberOfActiveServersAllocationPolicy(true) - "provisioned-cores" -> ProvisionedCoresAllocationPolicy() - "provisioned-cores-inv" -> ProvisionedCoresAllocationPolicy(true) - "random" -> RandomAllocationPolicy(Random(seeder.nextInt())) - "replay" -> ReplayAllocationPolicy(experiment.vmPlacements) - else -> throw IllegalArgumentException("Unknown policy ${parent.allocationPolicy}") - } - - @Suppress("UNCHECKED_CAST") - val rawTraceReaders = - context.cache.computeIfAbsent("raw-trace-readers") { mutableMapOf<String, Sc20RawParquetTraceReader>() } as MutableMap<String, Sc20RawParquetTraceReader> - val rawReaders = synchronized(rawTraceReaders) { - val workloadNames = if (parent.workload is CompositeWorkload) { - parent.workload.workloads.map { it.name } - } else { - listOf(parent.workload.name) - } - - workloadNames.map { workloadName -> - rawTraceReaders.computeIfAbsent(workloadName) { - logger.info { "Loading trace $workloadName" } - Sc20RawParquetTraceReader(File(experiment.traces, workloadName)) - } - } - } - - val performanceInterferenceModel = experiment.performanceInterferenceModel - ?.takeIf { parent.operationalPhenomena.hasInterference } - ?.construct(seeder) ?: emptyMap() - val trace = Sc20ParquetTraceReader(rawReaders, performanceInterferenceModel, parent.workload, seed) - - val monitor = ParquetExperimentMonitor( - parent.parent.parent.output, - "portfolio_id=${parent.parent.id}/scenario_id=${parent.id}/run_id=$id", - parent.parent.parent.bufferSize - ) - - testScope.launch { - val (bareMetalProvisioner, scheduler) = createProvisioner( - this, - clock, - environment, - allocationPolicy - ) - - val failureDomain = if (parent.operationalPhenomena.failureFrequency > 0) { - logger.debug("ENABLING failures") - createFailureDomain( - this, - clock, - seeder.nextInt(), - parent.operationalPhenomena.failureFrequency, - bareMetalProvisioner, - chan - ) - } else { - null - } - - attachMonitor(this, clock, scheduler, monitor) - processTrace( - this, - clock, - trace, - scheduler, - chan, - monitor - ) - - logger.debug("SUBMIT=${scheduler.submittedVms}") - logger.debug("FAIL=${scheduler.unscheduledVms}") - logger.debug("QUEUED=${scheduler.queuedVms}") - logger.debug("RUNNING=${scheduler.runningVms}") - logger.debug("FINISHED=${scheduler.finishedVms}") - - failureDomain?.cancel() - scheduler.terminate() - } - - try { - testScope.advanceUntilIdle() - } finally { - monitor.close() - } - } -} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/TestPortfolio.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/TestPortfolio.kt new file mode 100644 index 00000000..2210fc97 --- /dev/null +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/experiment/TestPortfolio.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.experiments.sc20.experiment + +import org.opendc.experiments.sc20.experiment.model.OperationalPhenomena +import org.opendc.experiments.sc20.experiment.model.Topology +import org.opendc.experiments.sc20.experiment.model.Workload +import org.opendc.harness.dsl.anyOf + +/** + * A [Portfolio] to perform a simple test run. + */ +public class TestPortfolio : Portfolio("test") { + override val topology: Topology by anyOf( + Topology("base") + ) + + override val workload: Workload by anyOf( + Workload("solvinity", 1.0) + ) + + override val operationalPhenomena: OperationalPhenomena by anyOf( + OperationalPhenomena(failureFrequency = 24.0 * 7, hasInterference = true) + ) + + override val allocationPolicy: String by anyOf("active-servers") +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/ContainerExperimentDescriptor.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/ContainerExperimentDescriptor.kt deleted file mode 100644 index d70e8c9a..00000000 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/ContainerExperimentDescriptor.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.experiments.sc20.runner - -import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionContext -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionResult - -/** - * An abstract [ExperimentDescriptor] specifically for containers. - */ -public abstract class ContainerExperimentDescriptor : ExperimentDescriptor() { - /** - * The child descriptors of this container. - */ - public abstract val children: Sequence<ExperimentDescriptor> - - override val type: Type = Type.CONTAINER - - override suspend fun invoke(context: ExperimentExecutionContext) { - val materializedChildren = children.toList() - for (child in materializedChildren) { - context.listener.descriptorRegistered(child) - } - - supervisorScope { - for (child in materializedChildren) { - if (child.isTrial) { - launch { - val worker = context.scheduler.allocate() - context.listener.executionStarted(child) - try { - worker(child, context) - context.listener.executionFinished(child, ExperimentExecutionResult.Success) - } catch (e: Throwable) { - context.listener.executionFinished(child, ExperimentExecutionResult.Failed(e)) - } - } - } else { - launch { child(context) } - } - } - } - } -} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/ExperimentDescriptor.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/ExperimentDescriptor.kt deleted file mode 100644 index 1e67c086..00000000 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/ExperimentDescriptor.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.experiments.sc20.runner - -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionContext -import java.io.Serializable - -/** - * An immutable description of an experiment in the **odcsim* simulation framework, which may be a single atomic trial - * or a composition of multiple trials. - * - * This class represents a dynamic tree-like structure where the children of the nodes are not known at instantiation - * since they might be generated dynamically. - */ -public abstract class ExperimentDescriptor : Serializable { - /** - * The parent of this descriptor, or `null` if it has no parent. - */ - public abstract val parent: ExperimentDescriptor? - - /** - * The type of descriptor. - */ - public abstract val type: Type - - /** - * A flag to indicate that this descriptor is a root descriptor. - */ - public open val isRoot: Boolean - get() = parent == null - - /** - * A flag to indicate that this descriptor describes an experiment trial. - */ - public val isTrial: Boolean - get() = type == Type.TRIAL - - /** - * Execute this [ExperimentDescriptor]. - * - * @param context The context to execute the descriptor in. - */ - public abstract suspend operator fun invoke(context: ExperimentExecutionContext) - - /** - * The types of experiment descriptors. - */ - public enum class Type { - /** - * A composition of multiple experiment descriptions whose invocation happens on a single thread. - */ - CONTAINER, - - /** - * An invocation of a single scenario of an experiment whose invocation may happen on different threads. - */ - TRIAL - } -} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt deleted file mode 100644 index 942faed1..00000000 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ThreadPoolExperimentScheduler.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.experiments.sc20.runner.execution - -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.withContext -import org.opendc.experiments.sc20.runner.ExperimentDescriptor -import java.util.concurrent.Executors - -/** - * An [ExperimentScheduler] that runs experiments using a local thread pool. - * - * @param parallelism The maximum amount of parallel workers (default is the number of available processors). - */ -public class ThreadPoolExperimentScheduler(parallelism: Int = Runtime.getRuntime().availableProcessors() + 1) : ExperimentScheduler { - private val dispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher() - private val tickets = Semaphore(parallelism) - - override suspend fun allocate(): ExperimentScheduler.Worker { - tickets.acquire() - return object : ExperimentScheduler.Worker { - override suspend fun invoke( - descriptor: ExperimentDescriptor, - context: ExperimentExecutionContext - ) = supervisorScope { - val listener = - object : ExperimentExecutionListener { - override fun descriptorRegistered(descriptor: ExperimentDescriptor) { - launch { context.listener.descriptorRegistered(descriptor) } - } - - override fun executionFinished( - descriptor: ExperimentDescriptor, - result: ExperimentExecutionResult - ) { - launch { context.listener.executionFinished(descriptor, result) } - } - - override fun executionStarted(descriptor: ExperimentDescriptor) { - launch { context.listener.executionStarted(descriptor) } - } - } - - val newContext = object : ExperimentExecutionContext by context { - override val listener: ExperimentExecutionListener = listener - } - - try { - withContext(dispatcher) { - descriptor(newContext) - } - } finally { - tickets.release() - } - } - } - } - - override fun close(): Unit = dispatcher.close() -} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/internal/DefaultExperimentRunner.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/internal/DefaultExperimentRunner.kt deleted file mode 100644 index 26e4df89..00000000 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/internal/DefaultExperimentRunner.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.experiments.sc20.runner.internal - -import kotlinx.coroutines.runBlocking -import org.opendc.experiments.sc20.runner.ExperimentDescriptor -import org.opendc.experiments.sc20.runner.ExperimentRunner -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionContext -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionListener -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionResult -import org.opendc.experiments.sc20.runner.execution.ExperimentScheduler -import java.util.concurrent.ConcurrentHashMap - -/** - * The default implementation of the [ExperimentRunner] interface. - * - * @param scheduler The scheduler to use. - */ -public class DefaultExperimentRunner(private val scheduler: ExperimentScheduler) : ExperimentRunner { - override val id: String = "default" - - override val version: String? = "1.0" - - override fun execute(root: ExperimentDescriptor, listener: ExperimentExecutionListener): Unit = runBlocking { - val context = object : ExperimentExecutionContext { - override val listener: ExperimentExecutionListener = listener - override val scheduler: ExperimentScheduler = this@DefaultExperimentRunner.scheduler - override val cache: MutableMap<Any?, Any?> = ConcurrentHashMap() - } - - listener.descriptorRegistered(root) - context.listener.executionStarted(root) - try { - root(context) - context.listener.executionFinished(root, ExperimentExecutionResult.Success) - } catch (e: Throwable) { - context.listener.executionFinished(root, ExperimentExecutionResult.Failed(e)) - } - } -} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/telemetry/RunEvent.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/telemetry/RunEvent.kt index 3bcd10a1..4f4706f0 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/telemetry/RunEvent.kt +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/telemetry/RunEvent.kt @@ -22,12 +22,13 @@ package org.opendc.experiments.sc20.telemetry -import org.opendc.experiments.sc20.experiment.Run +import org.opendc.experiments.sc20.experiment.Portfolio /** * A periodic report of the host machine metrics. */ public data class RunEvent( - public val run: Run, + val portfolio: Portfolio, + val repeat: Int, override val timestamp: Long ) : Event("run") diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/telemetry/parquet/ParquetRunEventWriter.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/telemetry/parquet/ParquetRunEventWriter.kt index 74efb660..b50a698c 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/telemetry/parquet/ParquetRunEventWriter.kt +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/telemetry/parquet/ParquetRunEventWriter.kt @@ -38,33 +38,27 @@ public class ParquetRunEventWriter(path: File, bufferSize: Int) : public companion object { private val convert: (RunEvent, GenericData.Record) -> Unit = { event, record -> - val run = event.run - val scenario = run.parent - val portfolio = scenario.parent - record.put("portfolio_id", portfolio.id) + val portfolio = event.portfolio record.put("portfolio_name", portfolio.name) - record.put("scenario_id", scenario.id) - record.put("run_id", run.id) - record.put("repetitions", scenario.repetitions) - record.put("topology", scenario.topology.name) - record.put("workload_name", scenario.workload.name) - record.put("workload_fraction", scenario.workload.fraction) - record.put("workload_sampler", scenario.workload.samplingStrategy) - record.put("allocation_policy", scenario.allocationPolicy) - record.put("failure_frequency", scenario.operationalPhenomena.failureFrequency) - record.put("interference", scenario.operationalPhenomena.hasInterference) - record.put("seed", run.seed) + record.put("scenario_id", portfolio.id) + record.put("run_id", event.repeat) + record.put("topology", portfolio.topology.name) + record.put("workload_name", portfolio.workload.name) + record.put("workload_fraction", portfolio.workload.fraction) + record.put("workload_sampler", portfolio.workload.samplingStrategy) + record.put("allocation_policy", portfolio.allocationPolicy) + record.put("failure_frequency", portfolio.operationalPhenomena.failureFrequency) + record.put("interference", portfolio.operationalPhenomena.hasInterference) + record.put("seed", event.repeat) } private val schema: Schema = SchemaBuilder .record("runs") .namespace("org.opendc.experiments.sc20") .fields() - .name("portfolio_id").type().intType().noDefault() .name("portfolio_name").type().stringType().noDefault() .name("scenario_id").type().intType().noDefault() .name("run_id").type().intType().noDefault() - .name("repetitions").type().intType().noDefault() .name("topology").type().stringType().noDefault() .name("workload_name").type().stringType().noDefault() .name("workload_fraction").type().doubleType().noDefault() diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/trace/Sc20RawParquetTraceReader.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/trace/Sc20RawParquetTraceReader.kt index 9bc1a58e..4a318df4 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/trace/Sc20RawParquetTraceReader.kt +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/trace/Sc20RawParquetTraceReader.kt @@ -66,8 +66,6 @@ public class Sc20RawParquetTraceReader(private val path: File) { val flops = record["flops"] as Long val fragment = SimTraceWorkload.Fragment( - tick, - flops, duration, cpuUsage, cores diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/trace/Sc20StreamingParquetTraceReader.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/trace/Sc20StreamingParquetTraceReader.kt index edef276c..ba22ae15 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/trace/Sc20StreamingParquetTraceReader.kt +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/trace/Sc20StreamingParquetTraceReader.kt @@ -92,7 +92,7 @@ public class Sc20StreamingParquetTraceReader( /** * A poisonous fragment. */ - private val poison = Pair("\u0000", SimTraceWorkload.Fragment(0, 0, 0, 0.0, 0)) + private val poison = Pair("\u0000", SimTraceWorkload.Fragment(0, 0.0, 0)) /** * The thread to read the records in. @@ -120,8 +120,6 @@ public class Sc20StreamingParquetTraceReader( val flops = record["flops"] as Long val fragment = SimTraceWorkload.Fragment( - tick, - flops, duration, cpuUsage, cores @@ -204,6 +202,7 @@ public class Sc20StreamingParquetTraceReader( val externalBuffer = mutableListOf<SimTraceWorkload.Fragment>() buffers.getOrPut(id) { mutableListOf() }.add(externalBuffer) val fragments = sequence { + var time = submissionTime repeat@ while (true) { if (externalBuffer.isEmpty()) { if (hasNext) { @@ -220,7 +219,8 @@ public class Sc20StreamingParquetTraceReader( for (fragment in internalBuffer) { yield(fragment) - if (fragment.time >= endTime) { + time += fragment.duration + if (time >= endTime) { break@repeat } } diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/test/kotlin/org/opendc/experiments/sc20/Sc20IntegrationTest.kt b/simulator/opendc-experiments/opendc-experiments-sc20/src/test/kotlin/org/opendc/experiments/sc20/Sc20IntegrationTest.kt index 9c44edfc..c5ad345d 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/test/kotlin/org/opendc/experiments/sc20/Sc20IntegrationTest.kt +++ b/simulator/opendc-experiments/opendc-experiments-sc20/src/test/kotlin/org/opendc/experiments/sc20/Sc20IntegrationTest.kt @@ -48,6 +48,7 @@ import org.opendc.format.environment.EnvironmentReader import org.opendc.format.environment.sc20.Sc20ClusterEnvironmentReader import org.opendc.format.trace.TraceReader import org.opendc.simulator.utils.DelayControllerClockAdapter +import org.opendc.trace.core.EventTracer import java.io.File import java.time.Clock @@ -89,7 +90,7 @@ class Sc20IntegrationTest { fun tearDown() = testScope.cleanupTestCoroutines() @Test - fun smoke() { + fun testLarge() { val failures = false val seed = 0 val chan = Channel<Unit>(Channel.CONFLATED) @@ -97,13 +98,15 @@ class Sc20IntegrationTest { val traceReader = createTestTraceReader() val environmentReader = createTestEnvironmentReader() lateinit var scheduler: SimVirtProvisioningService + val tracer = EventTracer(clock) testScope.launch { val res = createProvisioner( this, clock, environmentReader, - allocationPolicy + allocationPolicy, + tracer ) val bareMetalProvisioner = res.first scheduler = res.second @@ -145,28 +148,30 @@ class Sc20IntegrationTest { assertAll( { assertEquals(50, scheduler.submittedVms, "The trace contains 50 VMs") }, { assertEquals(50, scheduler.finishedVms, "All VMs should finish after a run") }, - { assertEquals(207379117949, monitor.totalRequestedBurst) }, - { assertEquals(203388071813, monitor.totalGrantedBurst) }, - { assertEquals(3991046136, monitor.totalOvercommissionedBurst) }, + { assertEquals(1684849230562, monitor.totalRequestedBurst) }, + { assertEquals(447612683996, monitor.totalGrantedBurst) }, + { assertEquals(1219535757406, monitor.totalOvercommissionedBurst) }, { assertEquals(0, monitor.totalInterferedBurst) } ) } @Test - fun small() { + fun testSmall() { val seed = 1 val chan = Channel<Unit>(Channel.CONFLATED) val allocationPolicy = AvailableCoreMemoryAllocationPolicy() val traceReader = createTestTraceReader(0.5, seed) val environmentReader = createTestEnvironmentReader("single") lateinit var scheduler: SimVirtProvisioningService + val tracer = EventTracer(clock) testScope.launch { val res = createProvisioner( this, clock, environmentReader, - allocationPolicy + allocationPolicy, + tracer ) scheduler = res.second @@ -190,10 +195,10 @@ class Sc20IntegrationTest { // Note that these values have been verified beforehand assertAll( - { assertEquals(96344114723, monitor.totalRequestedBurst) }, - { assertEquals(96324378235, monitor.totalGrantedBurst) }, - { assertEquals(19736424, monitor.totalOvercommissionedBurst) }, - { assertEquals(0, monitor.totalInterferedBurst) } + { assertEquals(705128393966, monitor.totalRequestedBurst) { "Total requested work incorrect" } }, + { assertEquals(173489747029, monitor.totalGrantedBurst) { "Total granted work incorrect" } }, + { assertEquals(526858997740, monitor.totalOvercommissionedBurst) { "Total overcommitted work incorrect" } }, + { assertEquals(0, monitor.totalInterferedBurst) { "Total interfered work incorrect" } } ) } diff --git a/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/bitbrains/BitbrainsTraceReader.kt b/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/bitbrains/BitbrainsTraceReader.kt index 9353ef28..90d751ea 100644 --- a/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/bitbrains/BitbrainsTraceReader.kt +++ b/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/bitbrains/BitbrainsTraceReader.kt @@ -34,6 +34,7 @@ import java.io.BufferedReader import java.io.File import java.io.FileReader import java.util.* +import kotlin.math.min /** * A [TraceReader] for the public VM workload trace format. @@ -70,6 +71,7 @@ public class BitbrainsTraceReader( var vmId = -1L var cores = -1 var requiredMemory = -1L + var startTime = -1L BufferedReader(FileReader(vmFile)).use { reader -> reader.lineSequence() @@ -91,21 +93,17 @@ public class BitbrainsTraceReader( } vmId = vmFile.nameWithoutExtension.trim().toLong() - val timestamp = values[timestampCol].trim().toLong() - 5 * 60 + startTime = min(startTime, values[timestampCol].trim().toLong() - 5 * 60) cores = values[coreCol].trim().toInt() val cpuUsage = values[cpuUsageCol].trim().toDouble() // MHz requiredMemory = (values[provisionedMemoryCol].trim().toDouble() / 1000).toLong() - val flops: Long = (cpuUsage * 5 * 60 * cores).toLong() - if (flopsHistory.isEmpty()) { - flopsHistory.add(SimTraceWorkload.Fragment(timestamp, flops, traceInterval, cpuUsage, cores)) + flopsHistory.add(SimTraceWorkload.Fragment(traceInterval, cpuUsage, cores)) } else { - if (flopsHistory.last().flops != flops) { + if (flopsHistory.last().usage != cpuUsage) { flopsHistory.add( SimTraceWorkload.Fragment( - timestamp, - flops, traceInterval, cpuUsage, cores @@ -115,8 +113,6 @@ public class BitbrainsTraceReader( val oldFragment = flopsHistory.removeAt(flopsHistory.size - 1) flopsHistory.add( SimTraceWorkload.Fragment( - oldFragment.time, - oldFragment.flops + flops, oldFragment.duration + traceInterval, cpuUsage, cores @@ -151,7 +147,7 @@ public class BitbrainsTraceReader( ) ) entries[vmId] = TraceEntryImpl( - flopsHistory.firstOrNull()?.time ?: -1, + startTime, vmWorkload ) } diff --git a/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/gwf/GwfTraceReader.kt b/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/gwf/GwfTraceReader.kt index a20b4f29..c76889c8 100644 --- a/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/gwf/GwfTraceReader.kt +++ b/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/gwf/GwfTraceReader.kt @@ -29,6 +29,7 @@ import org.opendc.format.trace.TraceReader import org.opendc.simulator.compute.workload.SimFlopsWorkload import org.opendc.workflows.workload.Job import org.opendc.workflows.workload.Task +import org.opendc.workflows.workload.WORKFLOW_TASK_CORES import org.opendc.workflows.workload.WORKFLOW_TASK_DEADLINE import java.io.BufferedReader import java.io.File @@ -122,8 +123,8 @@ public class GwfTraceReader(reader: BufferedReader) : TraceReader<Job> { val workflowId = values[workflowIdCol].trim().toLong() val taskId = values[taskIdCol].trim().toLong() - val submitTime = values[submitTimeCol].trim().toLong() - val runtime = max(0, values[runtimeCol].trim().toLong()) + val submitTime = values[submitTimeCol].trim().toLong() * 1000 // ms + val runtime = max(0, values[runtimeCol].trim().toLong()) // s val cores = values[coreCol].trim().toInt() val dependencies = values[dependencyCol].split(" ") .filter { it.isNotEmpty() } @@ -138,9 +139,12 @@ public class GwfTraceReader(reader: BufferedReader) : TraceReader<Job> { val task = Task( UUID(0L, taskId), "<unnamed>", - SimWorkloadImage(UUID.randomUUID(), "<unnamed>", emptyMap(), SimFlopsWorkload(flops, cores)), + SimWorkloadImage(UUID.randomUUID(), "<unnamed>", emptyMap(), SimFlopsWorkload(flops)), HashSet(), - mapOf(WORKFLOW_TASK_DEADLINE to runtime) + mapOf( + WORKFLOW_TASK_CORES to cores, + WORKFLOW_TASK_DEADLINE to (runtime * 1000) + ), ) entry.submissionTime = min(entry.submissionTime, submitTime) (workflow.tasks as MutableSet<Task>).add(task) diff --git a/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/sc20/Sc20TraceReader.kt b/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/sc20/Sc20TraceReader.kt index 66efbcd0..78f581ca 100644 --- a/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/sc20/Sc20TraceReader.kt +++ b/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/sc20/Sc20TraceReader.kt @@ -125,20 +125,16 @@ public class Sc20TraceReader( requiredMemory = max(requiredMemory, values[provisionedMemoryCol].trim().toLong()) maxCores = max(maxCores, cores) - val flops: Long = (cpuUsage * 5 * 60).toLong() - - last = if (last != null && last!!.flops == 0L && flops == 0L) { + last = if (last != null && last!!.usage == 0.0 && cpuUsage == 0.0) { val oldFragment = last!! SimTraceWorkload.Fragment( - oldFragment.time, - oldFragment.flops + flops, oldFragment.duration + traceInterval, cpuUsage, cores ) } else { val fragment = - SimTraceWorkload.Fragment(timestamp, flops, traceInterval, cpuUsage, cores) + SimTraceWorkload.Fragment(traceInterval, cpuUsage, cores) if (last != null) { yield(last!!) } diff --git a/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/swf/SwfTraceReader.kt b/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/swf/SwfTraceReader.kt index 52d41c44..80c54354 100644 --- a/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/swf/SwfTraceReader.kt +++ b/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/swf/SwfTraceReader.kt @@ -113,8 +113,6 @@ public class SwfTraceReader( for (tick in submitTime until (submitTime + waitTime - sliceDuration) step sliceDuration) { flopsHistory.add( SimTraceWorkload.Fragment( - tick * 1000L, - 0L, sliceDuration * 1000L, 0.0, cores @@ -138,8 +136,6 @@ public class SwfTraceReader( ) { flopsHistory.add( SimTraceWorkload.Fragment( - tick * 1000L, - flopsFullSlice / sliceDuration, sliceDuration * 1000L, 1.0, cores @@ -150,8 +146,6 @@ public class SwfTraceReader( if (runtimePartialSliceRemainder > 0) { flopsHistory.add( SimTraceWorkload.Fragment( - submitTime + (slicedWaitTime + runTime - runtimePartialSliceRemainder), - flopsPartialSlice, sliceDuration, runtimePartialSliceRemainder / sliceDuration.toDouble(), cores diff --git a/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/wtf/WtfTraceReader.kt b/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/wtf/WtfTraceReader.kt index b2931468..d7dc09fa 100644 --- a/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/wtf/WtfTraceReader.kt +++ b/simulator/opendc-format/src/main/kotlin/org/opendc/format/trace/wtf/WtfTraceReader.kt @@ -32,6 +32,7 @@ import org.opendc.format.trace.TraceReader import org.opendc.simulator.compute.workload.SimFlopsWorkload import org.opendc.workflows.workload.Job import org.opendc.workflows.workload.Task +import org.opendc.workflows.workload.WORKFLOW_TASK_CORES import org.opendc.workflows.workload.WORKFLOW_TASK_DEADLINE import java.util.UUID import kotlin.math.min @@ -80,9 +81,12 @@ public class WtfTraceReader(path: String) : TraceReader<Job> { val task = Task( UUID(0L, taskId), "<unnamed>", - SimWorkloadImage(UUID.randomUUID(), "<unnamed>", emptyMap(), SimFlopsWorkload(flops, cores)), + SimWorkloadImage(UUID.randomUUID(), "<unnamed>", emptyMap(), SimFlopsWorkload(flops)), HashSet(), - mapOf(WORKFLOW_TASK_DEADLINE to runtime) + mapOf( + WORKFLOW_TASK_CORES to cores, + WORKFLOW_TASK_DEADLINE to runtime + ) ) entry.submissionTime = min(entry.submissionTime, submitTime) diff --git a/simulator/opendc-format/src/test/kotlin/org/opendc/format/trace/swf/SwfTraceReaderTest.kt b/simulator/opendc-format/src/test/kotlin/org/opendc/format/trace/swf/SwfTraceReaderTest.kt index 8db2ab40..45c125c4 100644 --- a/simulator/opendc-format/src/test/kotlin/org/opendc/format/trace/swf/SwfTraceReaderTest.kt +++ b/simulator/opendc-format/src/test/kotlin/org/opendc/format/trace/swf/SwfTraceReaderTest.kt @@ -41,7 +41,6 @@ class SwfTraceReaderTest { assertEquals(164472, entry.submissionTime) // 1188 slices for waiting, 0 full and 1 partial running slices assertEquals(1189, ((entry.workload.image as SimWorkloadImage).workload as SimTraceWorkload).trace.toList().size) - assertEquals(5_100_000L, ((entry.workload.image as SimWorkloadImage).workload as SimTraceWorkload).trace.toList().last().flops) assertEquals(0.25, ((entry.workload.image as SimWorkloadImage).workload as SimTraceWorkload).trace.toList().last().usage) } } diff --git a/simulator/opendc-harness/build.gradle.kts b/simulator/opendc-harness/build.gradle.kts new file mode 100644 index 00000000..359d2384 --- /dev/null +++ b/simulator/opendc-harness/build.gradle.kts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +description = "Harness for defining repeatable experiments using OpenDC" + +/* Build configuration */ +plugins { + `kotlin-library-convention` +} + +dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Library.KOTLINX_COROUTINES}") + api("org.junit.platform:junit-platform-commons:${Library.JUNIT_PLATFORM}") + + implementation("io.github.classgraph:classgraph:4.8.98") + implementation("me.tongfei:progressbar:0.9.0") + implementation("io.github.microutils:kotlin-logging:2.0.4") + implementation("com.github.ajalt.clikt:clikt:3.1.0") + + api("org.junit.platform:junit-platform-engine:${Library.JUNIT_PLATFORM}") + api("org.junit.platform:junit-platform-suite-api:${Library.JUNIT_PLATFORM}") + api("org.junit.platform:junit-platform-launcher:${Library.JUNIT_PLATFORM}") + + runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:2.14.0") + + testImplementation("org.junit.jupiter:junit-jupiter-api:${Library.JUNIT_JUPITER}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Library.JUNIT_JUPITER}") +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/ExperimentDefinition.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/ExperimentDefinition.kt new file mode 100644 index 00000000..88b26ee1 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/ExperimentDefinition.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.api + +/** + * A definition for a repeatable experiment which consists of multiple scenarios derived from pre-defined experiment + * parameters. + * + * @property name The name of the experiment. + * @property parameters The parameters of the experiments. + * @property evaluator The function to evaluate a single experiment trial. + * @property meta The metadata for the experiment. + */ +public data class ExperimentDefinition( + val name: String, + val parameters: Set<Parameter<*>>, + val evaluator: (Trial) -> Unit, + val meta: Map<String, Any> = emptyMap() +) diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ExperimentExecutionResult.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/Parameter.kt index a765c264..bb5c8c2b 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ExperimentExecutionResult.kt +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/Parameter.kt @@ -1,7 +1,5 @@ /* - * MIT License - * - * Copyright (c) 2020 atlarge-research + * Copyright (c) 2020 AtLarge Research * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,21 +20,19 @@ * SOFTWARE. */ -package org.opendc.experiments.sc20.runner.execution - -import java.io.Serializable +package org.opendc.harness.api /** - * The result of executing an experiment. + * A [Parameter] defines a single dimension of exploration in the design space of an experiment. */ -public sealed class ExperimentExecutionResult : Serializable { +public sealed class Parameter<T> { /** - * The experiment executed successfully + * The name of the parameter. */ - public object Success : ExperimentExecutionResult() + public abstract val name: String /** - * The experiment failed during execution. + * A generic dimension of the experiment design space that is defined fully by a collection of [values]. */ - public data class Failed(val throwable: Throwable) : ExperimentExecutionResult() + public data class Generic<T>(override val name: String, val values: Collection<T>) : Parameter<T>() } diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/Scenario.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/Scenario.kt new file mode 100644 index 00000000..a8dbf01e --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/Scenario.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.api + +/** + * A [Scenario] represents a single point in the design space of an experiment. + */ +public interface Scenario { + /** + * A unique identifier that identifies a single scenario. + */ + public val id: Int + + /** + * The [ExperimentDefinition] describing the experiment this scenario is part of. + */ + public val experiment: ExperimentDefinition + + /** + * Obtain the instantiated value for a [parameter][param] of the experiment. + * + * @param param The parameter to obtain the value of. + * @throws IllegalArgumentException if [param] is not defined for the experiment. + */ + public operator fun <T> get(param: Parameter<T>): T +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/Trial.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/Trial.kt new file mode 100644 index 00000000..2d6ecd19 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/api/Trial.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.api + +/** + * A [Trial] represents a single trial (run) of an experiment. + */ +public data class Trial(val scenario: Scenario, val repeat: Int) diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/dsl/Experiment.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/dsl/Experiment.kt new file mode 100644 index 00000000..41d4207a --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/dsl/Experiment.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.dsl + +import org.junit.platform.commons.annotation.Testable +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.api.Scenario +import org.opendc.harness.api.Trial +import org.opendc.harness.internal.ParameterDelegate + +/** + * An [Experiment] defines a blueprint for a repeatable experiment consisting of multiple scenarios based on pre-defined + * experiment parameters. + * + * @param name The name of the experiment or `null` to select the class name. + */ +@Testable +public abstract class Experiment(name: String? = null) : Cloneable { + /** + * The name of the experiment. + */ + public val name: String = name ?: javaClass.simpleName + + /** + * An identifier that uniquely identifies a single point in the design space of this [Experiment]. + */ + public val id: Int + get() { + val scenario = scenario ?: throw IllegalStateException("Cannot use id before activation") + return scenario.id + } + + /** + * Convert this experiment to an [ExperimentDefinition]. + */ + public fun toDefinition(): ExperimentDefinition = + ExperimentDefinition(name, HashSet(delegates.map { it.parameter }), this::run, mapOf("class.name" to javaClass.name)) + + /** + * Perform a single execution of the experiment based on the experiment parameters. + * + * @param repeat A number representing the repeat index of an identical scenario. + */ + protected abstract fun doRun(repeat: Int) + + /** + * A map to track the parameter delegates registered with this experiment. + */ + private val delegates: MutableSet<ParameterDelegate<*>> = mutableSetOf() + + /** + * The current active scenario. + */ + internal var scenario: Scenario? = null + + /** + * Perform a single execution of the experiment based on the experiment parameters. + * + * This operation will cause the [ParameterProvider]s of this group to be instantiated to some value in its design + * space. + * + * @param trial The experiment trial to run. + */ + private fun run(trial: Trial) { + val scenario = trial.scenario + + // XXX We clone the current class to prevent concurrency issues. + val res = clone() as Experiment + res.scenario = scenario + res.doRun(trial.repeat) + } + + /** + * Register a delegate for this experiment. + */ + internal fun register(delegate: ParameterDelegate<*>) { + delegates += delegate + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/dsl/ParameterProvider.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/dsl/ParameterProvider.kt new file mode 100644 index 00000000..e4bb9c64 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/dsl/ParameterProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.dsl + +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * A [ParameterProvider] defines a single dimension of exploration in the design space of an [Experiment]. + */ +public interface ParameterProvider<T> { + /** + * Provide a delegate defining a parameter for the specified [Experiment][experiment]. + * + * @param experiment The experiment for which the parameter is defined. + * @param prop The property to create the delegate for. + */ + public operator fun provideDelegate(experiment: Experiment, prop: KProperty<*>): ReadOnlyProperty<Experiment, T> +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/dsl/Parameters.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/dsl/Parameters.kt new file mode 100644 index 00000000..7d269ba1 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/dsl/Parameters.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.dsl + +import org.opendc.harness.api.Parameter +import org.opendc.harness.internal.ParameterDelegate +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * Define a dimension in the design space of an [Experiment] of type [T] consisting of the specified collection of + * [values]. + * + * @param values The values in the dimension to define.. + */ +public fun <T> anyOf(vararg values: T): ParameterProvider<T> = object : ParameterProvider<T> { + override fun provideDelegate(experiment: Experiment, prop: KProperty<*>): ReadOnlyProperty<Experiment, T> { + val delegate = ParameterDelegate(Parameter.Generic(prop.name, listOf(*values))) + experiment.register(delegate) + return delegate + } + + override fun toString(): String = "GenericParameter" +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/ExperimentEngine.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/ExperimentEngine.kt new file mode 100644 index 00000000..65a0604d --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/ExperimentEngine.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.api.Trial +import org.opendc.harness.engine.scheduler.ExperimentScheduler +import org.opendc.harness.engine.strategy.ExperimentStrategy + +/** + * The [ExperimentEngine] orchestrates the execution of experiments. + * + * @property strategy The [ExperimentStrategy] used to explore the experiment design space. + * @property scheduler The [ExperimentScheduler] to schedule the trials over compute resources. + * @property listener The [ExperimentExecutionListener] to observe the progress. + * @property repeats The number of repeats to perform. + */ +public class ExperimentEngine( + private val strategy: ExperimentStrategy, + private val scheduler: ExperimentScheduler, + private val listener: ExperimentExecutionListener, + private val repeats: Int +) { + /** + * Execute the specified [experiment][root]. + * + * @param root The experiment to execute. + */ + @OptIn(InternalCoroutinesApi::class) + public suspend fun execute(root: ExperimentDefinition): Unit = supervisorScope { + listener.experimentStarted(root) + + try { + strategy.generate(root) + .asFlow() + .map { scenario -> + listener.scenarioStarted(scenario) + scenario + } + .buffer(100) + .collect { scenario -> + val jobs = (0 until repeats).map { repeat -> + val worker = scheduler.allocate() + launch { + val trial = Trial(scenario, repeat) + try { + listener.trialStarted(trial) + worker.dispatch(trial) + listener.trialFinished(trial, null) + } catch (e: Throwable) { + listener.trialFinished(trial, e) + } + } + } + + launch { + jobs.joinAll() + listener.scenarioFinished(scenario, null) + } + } + listener.experimentFinished(root, null) + } catch (e: Throwable) { + listener.experimentFinished(root, e) + throw e + } + } + + override fun toString(): String = "ExperimentEngine" +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/ExperimentEngineLauncher.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/ExperimentEngineLauncher.kt new file mode 100644 index 00000000..ddd30483 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/ExperimentEngineLauncher.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.runBlocking +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.engine.scheduler.ExperimentScheduler +import org.opendc.harness.engine.scheduler.ThreadPoolExperimentScheduler +import org.opendc.harness.engine.strategy.CartesianExperimentStrategy +import org.opendc.harness.engine.strategy.ExperimentStrategy +import org.opendc.harness.internal.CompositeExperimentExecutionListener + +/** + * A builder class for conducting experiments via the [ExperimentEngine]. + */ +public class ExperimentEngineLauncher private constructor( + private val strategy: ExperimentStrategy?, + private val scheduler: ExperimentScheduler?, + private val listeners: List<ExperimentExecutionListener>, + private val repeats: Int +) { + /** + * Construct an [ExperimentEngineLauncher] instance. + */ + public constructor() : this(null, null, emptyList(), 1) + + /** + * Create an [ExperimentEngineLauncher] with the specified [strategy]. + */ + public fun withScheduler(strategy: ExperimentStrategy): ExperimentEngineLauncher { + return ExperimentEngineLauncher(strategy, scheduler, listeners, repeats) + } + + /** + * Create an [ExperimentEngineLauncher] with the specified [scheduler]. + */ + public fun withScheduler(scheduler: ExperimentScheduler): ExperimentEngineLauncher { + return ExperimentEngineLauncher(strategy, scheduler, listeners, repeats) + } + + /** + * Create an [ExperimentEngineLauncher] with the specified [listener] added. + */ + public fun withListener(listener: ExperimentExecutionListener): ExperimentEngineLauncher { + return ExperimentEngineLauncher(strategy, scheduler, listeners + listener, repeats) + } + + /** + * Create an [ExperimentEngineLauncher] with the specified number of repeats. + */ + public fun withRepeats(repeats: Int): ExperimentEngineLauncher { + require(repeats > 0) { "Invalid number of repeats; must be greater than zero. " } + return ExperimentEngineLauncher(strategy, scheduler, listeners, repeats) + } + + /** + * Launch the specified experiments via the [ExperimentEngine] and block execution until finished. + */ + public suspend fun run(experiments: Flow<ExperimentDefinition>) { + val engine = ExperimentEngine(createStrategy(), createScheduler(), createListener(), repeats) + experiments.collect { experiment -> engine.execute(experiment) } + } + + /** + * Launch the specified experiments via the [ExperimentEngine] and block the current thread until finished. + */ + public fun runBlocking(experiments: Flow<ExperimentDefinition>) { + runBlocking { + run(experiments) + } + } + + /** + * Return a string representation of this instance. + */ + public override fun toString(): String = "ExperimentEngineLauncher" + + /** + * Create the [ExperimentStrategy] that explores the experiment design space. + */ + private fun createStrategy(): ExperimentStrategy { + return strategy ?: CartesianExperimentStrategy + } + + /** + * Create the [ExperimentScheduler] that schedules the trials over the compute resources. + */ + private fun createScheduler(): ExperimentScheduler { + return scheduler ?: ThreadPoolExperimentScheduler(Runtime.getRuntime().availableProcessors()) + } + + /** + * Create the [ExperimentExecutionListener] that listens the to the execution of the experiments. + */ + private fun createListener(): ExperimentExecutionListener { + require(listeners.isNotEmpty()) { "No listeners registered." } + return CompositeExperimentExecutionListener(listeners) + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/ExperimentExecutionListener.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/ExperimentExecutionListener.kt new file mode 100644 index 00000000..9ef71863 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/ExperimentExecutionListener.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine + +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.api.Scenario +import org.opendc.harness.api.Trial + +/** + * Listener to be notified of experiment execution events by experiment runners. + */ +public interface ExperimentExecutionListener { + /** + * A method that is invoked when an experiment is started. + * + * @param experiment The [ExperimentDefinition] that started. + */ + public fun experimentStarted(experiment: ExperimentDefinition) {} + + /** + * A method that is invoked when an experiment is finished, regardless of the outcome. + * + * @param experiment The [ExperimentDefinition] that finished. + * @param throwable The exception that was thrown during execution or `null` if the execution completed successfully. + */ + public fun experimentFinished(experiment: ExperimentDefinition, throwable: Throwable?) {} + + /** + * A method that is invoked when a scenario is started. + * + * @param scenario The scenario that is started. + */ + public fun scenarioStarted(scenario: Scenario) {} + + /** + * A method that is invoked when a scenario is finished, regardless of the outcome. + * + * @param scenario The [Scenario] that has finished. + * @param throwable The exception that was thrown during execution or `null` if the execution completed successfully. + */ + public fun scenarioFinished(scenario: Scenario, throwable: Throwable?) {} + + /** + * A method that is invoked when a trial is started. + * + * @param trial The trial that is started. + */ + public fun trialStarted(trial: Trial) {} + + /** + * A method that is invoked when a scenario is finished, regardless of the outcome. + * + * @param trial The [Trial] that has finished. + * @param throwable The exception that was thrown during execution or `null` if the execution completed successfully. + */ + public fun trialFinished(trial: Trial, throwable: Throwable?) {} +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/Discovery.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/Discovery.kt new file mode 100644 index 00000000..f7f73b38 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/Discovery.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.discovery + +import kotlinx.coroutines.flow.Flow +import org.opendc.harness.api.ExperimentDefinition + +/** + * Component responsible for scanning for [ExperimentDefinition]s. + */ +public interface Discovery { + /** + * Start discovery of experiments. + * + * @param request The [DiscoveryRequest] to determine the experiments to discover. + * @return A flow of [ExperimentDefinition]s that have been discovered by the implementation. + */ + public fun discover(request: DiscoveryRequest): Flow<ExperimentDefinition> +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoveryFilter.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoveryFilter.kt new file mode 100644 index 00000000..219d09cd --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoveryFilter.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.discovery + +import org.opendc.harness.api.ExperimentDefinition +import java.util.function.Predicate + +/** + * A [DiscoveryFilter] decides how the selected experiments are filtered. + */ +public sealed class DiscoveryFilter { + /** + * Test whether the specified [ExperimentDefinition] should be selected. + */ + public abstract fun test(definition: ExperimentDefinition): Boolean + + /** + * Filter an experiment based on its name. + */ + public data class Name(val predicate: Predicate<String>) : DiscoveryFilter() { + override fun test(definition: ExperimentDefinition): Boolean = predicate.test(definition.name) + } + + /** + * Filter an experiment based on its metadata. + */ + public data class Meta(val key: String, val predicate: Predicate<Any>) : DiscoveryFilter() { + override fun test(definition: ExperimentDefinition): Boolean = + definition.meta[key]?.let { predicate.test(it) } ?: false + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoveryProvider.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoveryProvider.kt new file mode 100644 index 00000000..fad255de --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoveryProvider.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.discovery + +import org.opendc.harness.internal.CompositeDiscovery +import java.util.* + +/** + * A provider interface for the [Discovery] component. + */ +public interface DiscoveryProvider { + /** + * A unique identifier for this discovery implementation. + * + * Each discovery implementation must provide a unique ID, so that they can be selected by the user. + * When in doubt, you may use the fully qualified name of your custom [Discovery] implementation class. + */ + public val id: String + + /** + * Factory method for creating a new [Discovery] instance. + */ + public fun create(): Discovery + + public companion object { + /** + * The available [DiscoveryProvider]s. + */ + private val providers by lazy { ServiceLoader.load(DiscoveryProvider::class.java) } + + /** + * Obtain the [DiscoveryProvider] with the specified [id] or return `null`. + */ + public fun findById(id: String): DiscoveryProvider? { + return providers.find { it.id == id } + } + + /** + * Obtain a composite [Discovery] that combines the results of all available providers. + */ + public fun createComposite(): Discovery { + return CompositeDiscovery(providers) + } + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoveryRequest.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoveryRequest.kt new file mode 100644 index 00000000..5bc08dac --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoveryRequest.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.discovery + +/** + * A request for discovering experiments according to the specified information. + * + * @param selectors The selectors for this discovery request. + * @param filters The filters for this discovery request. + */ +public data class DiscoveryRequest( + val selectors: List<DiscoverySelector> = emptyList(), + val filters: List<DiscoveryFilter> = emptyList(), +) diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ExperimentExecutionListener.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoverySelector.kt index 42fef164..67681303 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ExperimentExecutionListener.kt +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/discovery/DiscoverySelector.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 AtLarge Research + * Copyright (c) 2021 AtLarge Research * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,27 +20,30 @@ * SOFTWARE. */ -package org.opendc.experiments.sc20.runner.execution +package org.opendc.harness.engine.discovery -import org.opendc.experiments.sc20.runner.ExperimentDescriptor +import org.opendc.harness.api.ExperimentDefinition /** - * Listener to be notified of experiment execution events by experiment runners. + * A [DiscoverySelector] defines the properties used to discover experiments. */ -public interface ExperimentExecutionListener { +public sealed class DiscoverySelector { /** - * A method that is invoked when a new [ExperimentDescriptor] is registered. + * Test whether the specified [ExperimentDefinition] should be selected. */ - public fun descriptorRegistered(descriptor: ExperimentDescriptor) + public abstract fun test(definition: ExperimentDefinition): Boolean /** - * A method that is invoked when when the execution of a leaf or subtree of the experiment tree has finished, - * regardless of the outcome. + * Select an experiment based on its name. */ - public fun executionFinished(descriptor: ExperimentDescriptor, result: ExperimentExecutionResult) + public data class Name(val name: String) : DiscoverySelector() { + override fun test(definition: ExperimentDefinition): Boolean = definition.name == name + } /** - * A method that is invoked when the execution of a leaf or subtree of the experiment tree is about to be started. + * Select an experiment based on its metadata. */ - public fun executionStarted(descriptor: ExperimentDescriptor) + public data class Meta(val key: String, val value: Any) : DiscoverySelector() { + override fun test(definition: ExperimentDefinition): Boolean = definition.meta[key] == value + } } diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ExperimentScheduler.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ExperimentScheduler.kt index 70095ccd..0265554a 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ExperimentScheduler.kt +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ExperimentScheduler.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 AtLarge Research + * Copyright (c) 2021 AtLarge Research * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,37 +20,33 @@ * SOFTWARE. */ -package org.opendc.experiments.sc20.runner.execution +package org.opendc.harness.engine.scheduler -import org.opendc.experiments.sc20.runner.ExperimentDescriptor -import java.io.Closeable +import org.opendc.harness.api.Trial /** - * A interface for scheduling the execution of experiment trials over compute resources (threads/containers/vms) + * The [ExperimentScheduler] is responsible for scheduling the execution of experiment runs over some set of compute + * resources (e.g., threads or even multiple machines). */ -public interface ExperimentScheduler : Closeable { +public interface ExperimentScheduler : AutoCloseable { /** * Allocate a [Worker] for executing an experiment trial. This method may suspend in case no resources are directly * available at the moment. * * @return The available worker. */ - public suspend fun allocate(): ExperimentScheduler.Worker + public suspend fun allocate(): Worker /** - * An isolated worker of an [ExperimentScheduler] that is responsible for executing a single experiment trial. + * An isolated worker of an [ExperimentScheduler] that is responsible for conducting a single experiment trial. */ public interface Worker { /** - * Dispatch the specified [ExperimentDescriptor] to execute some time in the future and return the results of - * the trial. + * Dispatch an experiment trial immediately to one of the available compute resources and block execution until + * the trial has finished. * - * @param descriptor The descriptor to execute. - * @param context The context to execute the descriptor in. + * @param trial The trial to dispatch. */ - public suspend operator fun invoke( - descriptor: ExperimentDescriptor, - context: ExperimentExecutionContext - ) + public suspend fun dispatch(trial: Trial) } } diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ExperimentSchedulerProvider.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ExperimentSchedulerProvider.kt new file mode 100644 index 00000000..a93d4bf6 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ExperimentSchedulerProvider.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.scheduler + +import java.util.* + +/** + * A factory for constructing an [ExperimentScheduler]. + */ +public interface ExperimentSchedulerProvider { + /** + * A unique identifier for this scheduler implementation. + * + * Each experiment scheduler must provide a unique ID, so that they can be selected by the user. + * When in doubt, you may use the fully qualified name of your custom [ExperimentScheduler] implementation class. + */ + public val id: String + + /** + * Factory method for creating a new [ExperimentScheduler] instance. + */ + public fun create(): ExperimentScheduler + + public companion object { + /** + * The available [ExperimentSchedulerProvider]s. + */ + private val providers by lazy { ServiceLoader.load(ExperimentSchedulerProvider::class.java) } + + /** + * Obtain the [ExperimentScheduler] with the specified [id] or return `null`. + */ + public fun findById(id: String): ExperimentSchedulerProvider? { + return providers.find { it.id == id } + } + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ThreadPoolExperimentScheduler.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ThreadPoolExperimentScheduler.kt new file mode 100644 index 00000000..1ae533cf --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ThreadPoolExperimentScheduler.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.scheduler + +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.withContext +import org.opendc.harness.api.Trial +import java.util.concurrent.Executors + +/** + * An [ExperimentScheduler] that runs experiment trials using a local thread pool. + * + * @param parallelism The maximum amount of concurrent workers. + */ +public class ThreadPoolExperimentScheduler(parallelism: Int) : ExperimentScheduler { + private val dispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher() + private val tickets = Semaphore(parallelism) + + override suspend fun allocate(): ExperimentScheduler.Worker { + tickets.acquire() + return object : ExperimentScheduler.Worker { + override suspend fun dispatch(trial: Trial) { + try { + withContext(dispatcher) { + trial.scenario.experiment.evaluator(trial) + } + } finally { + tickets.release() + } + } + } + } + + override fun close(): Unit = dispatcher.close() + + override fun toString(): String = "ThreadPoolScheduler" +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ThreadPoolExperimentSchedulerProvider.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ThreadPoolExperimentSchedulerProvider.kt new file mode 100644 index 00000000..cf9a132f --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/scheduler/ThreadPoolExperimentSchedulerProvider.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.scheduler + +/** + * An [ExperimentSchedulerProvider] for constructing a [ThreadPoolExperimentScheduler]. + */ +public class ThreadPoolExperimentSchedulerProvider : ExperimentSchedulerProvider { + override val id: String = "thread-pool" + + override fun create(): ExperimentScheduler = + ThreadPoolExperimentScheduler(Runtime.getRuntime().availableProcessors()) +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/CartesianExperimentStrategy.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/CartesianExperimentStrategy.kt new file mode 100644 index 00000000..e5e08003 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/CartesianExperimentStrategy.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.strategy + +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.api.Parameter +import org.opendc.harness.api.Scenario +import org.opendc.harness.internal.ScenarioImpl + +/** + * An [ExperimentStrategy] that takes the cartesian product of the parameters and evaluates every combination. + */ +public object CartesianExperimentStrategy : ExperimentStrategy { + /** + * Build the trials of an experiment. + */ + override fun generate(experiment: ExperimentDefinition): Sequence<Scenario> { + return experiment.parameters + .asSequence() + .map { param -> mapParameter(param).map { value -> listOf(param to value) } } + .reduce { acc, param -> + acc.flatMap { x -> param.map { y -> x + y } } + } + .mapIndexed { id, values -> ScenarioImpl(id, experiment, values.toMap()) } + } + + /** + * Instantiate a parameter and return a sequence of possible values. + */ + private fun <T> mapParameter(param: Parameter<T>): Sequence<T> { + return when (param) { + is Parameter.Generic<T> -> param.values.asSequence() + } + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/CartesianExperimentStrategyProvider.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/CartesianExperimentStrategyProvider.kt new file mode 100644 index 00000000..f18795a3 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/CartesianExperimentStrategyProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.strategy + +/** + * An [ExperimentStrategyProvider] for constructing a [CartesianExperimentStrategy]. + */ +public class CartesianExperimentStrategyProvider : ExperimentStrategyProvider { + override val id: String = "cartesian" + + override fun create(): ExperimentStrategy = CartesianExperimentStrategy +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/ExperimentStrategy.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/ExperimentStrategy.kt new file mode 100644 index 00000000..3a0148ad --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/ExperimentStrategy.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.strategy + +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.api.Scenario + +/** + * The [ExperimentStrategy] is responsible for traversing the design space of an [ExperimentDefinition] based on its + * parameters, generating concrete points in the space represented as [Scenario]s. + */ +public interface ExperimentStrategy { + /** + * Generate the points in the design space of the specified [experiment] to explore. + * + * @param experiment The experiment design space to explore. + * @return A sequence of [Scenario]s which may be explored by the [ExperimentEngine]. + */ + public fun generate(experiment: ExperimentDefinition): Sequence<Scenario> +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/ExperimentStrategyProvider.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/ExperimentStrategyProvider.kt new file mode 100644 index 00000000..7fa05f34 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/engine/strategy/ExperimentStrategyProvider.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.engine.strategy + +import java.util.* + +/** + * A factory for constructing an [ExperimentStrategy]. + */ +public interface ExperimentStrategyProvider { + /** + * A unique identifier for this strategy implementation. + * + * Each experiment strategy must provide a unique ID, so that they can be selected by the user. + * When in doubt, you may use the fully qualified name of your custom [ExperimentStrategy] implementation class. + */ + public val id: String + + /** + * Factory method for creating a new [ExperimentStrategy] instance. + */ + public fun create(): ExperimentStrategy + + public companion object { + /** + * The available [ExperimentStrategyProvider]s. + */ + private val providers by lazy { ServiceLoader.load(ExperimentStrategyProvider::class.java) } + + /** + * Obtain the [ExperimentStrategy] with the specified [id] or return `null`. + */ + public fun findById(id: String): ExperimentStrategyProvider? { + return providers.find { it.id == id } + } + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/CompositeDiscovery.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/CompositeDiscovery.kt new file mode 100644 index 00000000..67a895e4 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/CompositeDiscovery.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.internal + +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.engine.discovery.Discovery +import org.opendc.harness.engine.discovery.DiscoveryProvider +import org.opendc.harness.engine.discovery.DiscoveryRequest + +/** + * A composite [Discovery] instance that combines the results of multiple delegate instances. + */ +internal class CompositeDiscovery(providers: Iterable<DiscoveryProvider>) : Discovery { + /** + * The [Discovery] instances to delegate to. + */ + private val delegates = providers.map { it.create() } + + @OptIn(FlowPreview::class) + override fun discover(request: DiscoveryRequest): Flow<ExperimentDefinition> { + return delegates.asFlow() + .map { it.discover(request) } + .flattenMerge(delegates.size) + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/CompositeExperimentExecutionListener.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/CompositeExperimentExecutionListener.kt new file mode 100644 index 00000000..a3cd6bd2 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/CompositeExperimentExecutionListener.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.internal + +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.api.Scenario +import org.opendc.harness.api.Trial +import org.opendc.harness.engine.ExperimentExecutionListener + +/** + * An [ExperimentExecutionListener] that composes multiple other listeners. + */ +public class CompositeExperimentExecutionListener(private val listeners: List<ExperimentExecutionListener>) : ExperimentExecutionListener { + override fun experimentStarted(experiment: ExperimentDefinition) { + listeners.forEach { it.experimentStarted(experiment) } + } + + override fun experimentFinished(experiment: ExperimentDefinition, throwable: Throwable?) { + listeners.forEach { it.experimentFinished(experiment, throwable) } + } + + override fun scenarioStarted(scenario: Scenario) { + listeners.forEach { it.scenarioStarted(scenario) } + } + + override fun scenarioFinished(scenario: Scenario, throwable: Throwable?) { + listeners.forEach { it.scenarioFinished(scenario, throwable) } + } + + override fun trialStarted(trial: Trial) { + listeners.forEach { it.trialStarted(trial) } + } + + override fun trialFinished(trial: Trial, throwable: Throwable?) { + listeners.forEach { it.trialFinished(trial, throwable) } + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/DslDiscovery.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/DslDiscovery.kt new file mode 100644 index 00000000..eb6303d6 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/DslDiscovery.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.internal + +import io.github.classgraph.ClassGraph +import io.github.classgraph.ScanResult +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.dsl.Experiment +import org.opendc.harness.engine.discovery.Discovery +import org.opendc.harness.engine.discovery.DiscoveryFilter +import org.opendc.harness.engine.discovery.DiscoveryRequest +import org.opendc.harness.engine.discovery.DiscoverySelector + +/** + * A [Discovery] implementation that discovers [Experiment] instances on the classpath. + */ +internal class DslDiscovery : Discovery { + /* + * Lazily memoize the results of the classpath scan. + */ + private val scanResult by lazy { scan() } + + override fun discover(request: DiscoveryRequest): Flow<ExperimentDefinition> { + return findExperiments() + .map { cls -> + val exp = cls.constructors[0].newInstance() as Experiment + exp.toDefinition() + } + .filter(select(request.selectors)) + .filter(filter(request.filters)) + .asFlow() + } + + /** + * Find the classes on the classpath implementing the [Experiment] class. + */ + private fun findExperiments(): Sequence<Class<out Experiment>> { + return scanResult + .getSubclasses(Experiment::class.java.name) + .filter { !(it.isAbstract || it.isInterface) } + .map { it.loadClass() } + .filterIsInstance<Class<out Experiment>>() + .asSequence() + } + + /** + * Create a predicate for filtering the experiments based on the specified [filters]. + */ + private fun filter(filters: List<DiscoveryFilter>): (ExperimentDefinition) -> Boolean = { def -> + filters.isEmpty() || filters.all { it.test(def) } + } + + /** + * Create a predicate for selecting the experiments based on the specified [selectors]. + */ + private fun select(selectors: List<DiscoverySelector>): (ExperimentDefinition) -> Boolean = { def -> + selectors.isEmpty() || selectors.any { it.test(def) } + } + + /** + * Scan the classpath using [ClassGraph]. + */ + private fun scan(): ScanResult { + return ClassGraph() + .enableClassInfo() + .enableExternalClasses() + .ignoreClassVisibility() + .rejectPackages( + "java.*", + "javax.*", + "sun.*", + "com.sun.*", + "kotlin.*", + "androidx.*", + "org.jetbrains.kotlin.*", + "org.junit.*" + ).scan() + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/DslDiscoveryProvider.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/DslDiscoveryProvider.kt new file mode 100644 index 00000000..752ba4bb --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/DslDiscoveryProvider.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.internal + +import org.opendc.harness.dsl.Experiment +import org.opendc.harness.engine.discovery.Discovery +import org.opendc.harness.engine.discovery.DiscoveryProvider + +/** + * A [DiscoveryProvider] for the [Experiment]s on the classpath. + */ +public class DslDiscoveryProvider : DiscoveryProvider { + override val id: String = "dsl" + + override fun create(): Discovery = DslDiscovery() +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/ParameterDelegate.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/ParameterDelegate.kt new file mode 100644 index 00000000..aaf90b99 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/ParameterDelegate.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.internal + +import org.opendc.harness.api.Parameter +import org.opendc.harness.dsl.Experiment +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * A delegate for an experiment parameter. + * + * @property parameter The parameter descriptor of this delegate. + */ +internal class ParameterDelegate<T>(val parameter: Parameter<T>) : ReadOnlyProperty<Experiment, T> { + /** + * Obtain the value for the parameter. + */ + override fun getValue(thisRef: Experiment, property: KProperty<*>): T { + val scenario = thisRef.scenario ?: throw IllegalStateException("Cannot use parameters before activation") + return scenario[parameter] + } +} diff --git a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/RandomResourceSelectionPolicy.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/ScenarioImpl.kt index caf87c70..d255004d 100644 --- a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/RandomResourceSelectionPolicy.kt +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/internal/ScenarioImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 AtLarge Research + * Copyright (c) 2021 AtLarge Research * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,26 +20,30 @@ * SOFTWARE. */ -package org.opendc.workflows.service.stage.resource +package org.opendc.harness.internal -import org.opendc.compute.core.metal.Node -import org.opendc.workflows.service.StageWorkflowService -import java.util.* +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.api.Parameter +import org.opendc.harness.api.Scenario /** - * A [ResourceSelectionPolicy] that randomly orders the machines. + * Internal implementation of a [Scenario]. */ -public object RandomResourceSelectionPolicy : ResourceSelectionPolicy { - override fun invoke(scheduler: StageWorkflowService): Comparator<Node> = object : Comparator<Node> { - private val ids: Map<Node, Long> +internal data class ScenarioImpl( + override val id: Int, + override val experiment: ExperimentDefinition, + val parameters: Map<Parameter<*>, Any?> +) : Scenario { - init { - val random = Random(123) - ids = scheduler.nodes.associateWith { random.nextLong() } + override fun <T> get(param: Parameter<T>): T { + if (!parameters.containsKey(param)) { + throw IllegalArgumentException("Unknown parameter for this scenario.") } - override fun compare(o1: Node, o2: Node): Int = compareValuesBy(o1, o2) { ids[it] } + // This cast should always succeed + @Suppress("UNCHECKED_CAST") + return parameters[param] as T } - override fun toString(): String = "Random" + override fun toString(): String = "Scenario" } diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/reporter/ConsoleExperimentReporter.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/console/ConsoleExperimentReporter.kt index af61622a..2db74ef4 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/reporter/ConsoleExperimentReporter.kt +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/console/ConsoleExperimentReporter.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 AtLarge Research + * Copyright (c) 2021 AtLarge Research * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,24 +20,22 @@ * SOFTWARE. */ -package org.opendc.experiments.sc20.reporter +package org.opendc.harness.runner.console import me.tongfei.progressbar.ProgressBar import me.tongfei.progressbar.ProgressBarBuilder import mu.KotlinLogging -import org.opendc.experiments.sc20.experiment.Run -import org.opendc.experiments.sc20.runner.ExperimentDescriptor -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionListener -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionResult +import org.opendc.harness.api.Trial +import org.opendc.harness.engine.ExperimentExecutionListener /** * A reporter that reports the experiment progress to the console. */ public class ConsoleExperimentReporter : ExperimentExecutionListener, AutoCloseable { /** - * The active [Run]s. + * The active [Trial]s. */ - private val runs: MutableSet<Run> = mutableSetOf() + private val trials: MutableSet<Trial> = mutableSetOf() /** * The total number of runs. @@ -57,29 +55,23 @@ public class ConsoleExperimentReporter : ExperimentExecutionListener, AutoClosea .setInitialMax(1) .build() - override fun descriptorRegistered(descriptor: ExperimentDescriptor) { - if (descriptor is Run) { - runs += descriptor - pb.maxHint((++total).toLong()) - } - } + override fun trialFinished(trial: Trial, throwable: Throwable?) { + trials -= trial - override fun executionFinished(descriptor: ExperimentDescriptor, result: ExperimentExecutionResult) { - if (descriptor is Run) { - runs -= descriptor - - pb.stepTo(total - runs.size.toLong()) - if (runs.isEmpty()) { - pb.close() - } + pb.stepTo(total - trials.size.toLong()) + if (trials.isEmpty()) { + pb.close() } - if (result is ExperimentExecutionResult.Failed) { - logger.warn(result.throwable) { "Descriptor $descriptor failed" } + if (throwable != null) { + logger.warn(throwable) { "Trial $trial failed" } } } - override fun executionStarted(descriptor: ExperimentDescriptor) {} + override fun trialStarted(trial: Trial) { + trials += trial + pb.maxHint((++total).toLong()) + } override fun close() { pb.close() diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/console/ConsoleRunner.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/console/ConsoleRunner.kt new file mode 100644 index 00000000..ae221c7f --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/console/ConsoleRunner.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.runner.console + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.multiple +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.int +import mu.KotlinLogging +import org.opendc.harness.engine.ExperimentEngineLauncher +import org.opendc.harness.engine.discovery.DiscoveryProvider +import org.opendc.harness.engine.discovery.DiscoveryRequest +import org.opendc.harness.engine.discovery.DiscoverySelector +import org.opendc.harness.engine.scheduler.ThreadPoolExperimentScheduler + +/** + * The logger for this experiment runner. + */ +private val logger = KotlinLogging.logger {} + +/** + * The command line interface for the console experiment runner. + */ +public class ConsoleRunner : CliktCommand(name = "opendc-harness") { + /** + * The number of repeats per scenario. + */ + private val repeats by option("-r", "--repeats", help = "Number of repeats per scenario") + .int() + .default(1) + + /** + * The selected experiments to run by name. + */ + private val experiments by option("-e", "--experiments", help = "Names of experiments to explore") + .multiple(emptyList()) + + /** + * The maximum number of worker threads to use. + */ + private val parallelism by option("-p", "--parallelism", help = "Maximum number of concurrent simulation runs") + .int() + .default(Runtime.getRuntime().availableProcessors()) + + override fun run() { + logger.info { "Starting OpenDC Console Experiment Runner" } + + val discovery = DiscoveryProvider.createComposite() + val experiments = discovery.discover( + DiscoveryRequest( + selectors = experiments.map { DiscoverySelector.Name(it) } + ) + ) + + val reporter = ConsoleExperimentReporter() + val scheduler = ThreadPoolExperimentScheduler(parallelism) + + try { + ExperimentEngineLauncher() + .withListener(reporter) + .withRepeats(repeats) + .withScheduler(scheduler) + .runBlocking(experiments) + } catch (e: Throwable) { + logger.error(e) { "Failed to finish experiments" } + } finally { + reporter.close() + scheduler.close() + } + + logger.info { "Finished all experiments. Exiting." } + } +} + +/** + * Main entry point of the experiment runner. + */ +public fun main(args: Array<String>): Unit = ConsoleRunner().main(args) diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/junit5/JUnitExperimentExecutionListener.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/junit5/JUnitExperimentExecutionListener.kt new file mode 100644 index 00000000..58791549 --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/junit5/JUnitExperimentExecutionListener.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.runner.junit5 + +import org.junit.platform.engine.* +import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor +import org.junit.platform.engine.support.descriptor.EngineDescriptor +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.api.Scenario +import org.opendc.harness.api.Trial +import org.opendc.harness.engine.ExperimentExecutionListener + +/** + * An [ExperimentExecutionListener] that notifies JUnit platform of the progress of the experiment trials. + */ +public class JUnitExperimentExecutionListener( + private val listener: EngineExecutionListener, + private val root: EngineDescriptor +) : ExperimentExecutionListener { + /** + * The current active experiments. + */ + private val experiments = mutableMapOf<ExperimentDefinition, TestDescriptor>() + + /** + * The current active scenarios. + */ + private val scenarios = mutableMapOf<Scenario, TestDescriptor>() + + /** + * The current active trials. + */ + private val trials = mutableMapOf<Trial, TestDescriptor>() + + override fun experimentStarted(experiment: ExperimentDefinition) { + val descriptor = experiment.toDescriptor(root) + root.addChild(descriptor) + experiments[experiment] = descriptor + + listener.dynamicTestRegistered(descriptor) + listener.executionStarted(descriptor) + } + + override fun experimentFinished(experiment: ExperimentDefinition, throwable: Throwable?) { + val descriptor = experiments.remove(experiment) + + if (throwable != null) { + listener.executionFinished(descriptor, TestExecutionResult.failed(throwable)) + } else { + listener.executionFinished(descriptor, TestExecutionResult.successful()) + } + } + + override fun scenarioStarted(scenario: Scenario) { + val parent = experiments[scenario.experiment] ?: return + val descriptor = scenario.toDescriptor(parent) + parent.addChild(descriptor) + scenarios[scenario] = descriptor + + listener.dynamicTestRegistered(descriptor) + listener.executionStarted(descriptor) + } + + override fun scenarioFinished(scenario: Scenario, throwable: Throwable?) { + val descriptor = scenarios.remove(scenario) + + if (throwable != null) { + listener.executionFinished(descriptor, TestExecutionResult.failed(throwable)) + } else { + listener.executionFinished(descriptor, TestExecutionResult.successful()) + } + } + + override fun trialStarted(trial: Trial) { + val parent = scenarios[trial.scenario] ?: return + val descriptor = trial.toDescriptor(parent) + parent.addChild(descriptor) + trials[trial] = descriptor + + listener.dynamicTestRegistered(descriptor) + listener.executionStarted(descriptor) + } + + override fun trialFinished(trial: Trial, throwable: Throwable?) { + val descriptor = trials.remove(trial) + + if (throwable != null) { + listener.executionFinished(descriptor, TestExecutionResult.failed(throwable)) + } else { + listener.executionFinished(descriptor, TestExecutionResult.successful()) + } + } + + /** + * Create a [TestDescriptor] for an [ExperimentDefinition]. + */ + private fun ExperimentDefinition.toDescriptor(parent: TestDescriptor): TestDescriptor { + return object : AbstractTestDescriptor(parent.uniqueId.append("experiment", name), name) { + override fun getType(): TestDescriptor.Type = TestDescriptor.Type.CONTAINER + + override fun mayRegisterTests(): Boolean = true + + override fun toString(): String = "ExperimentDescriptor" + } + } + + /** + * Create a [TestDescriptor] for a [Scenario]. + */ + private fun Scenario.toDescriptor(parent: TestDescriptor): TestDescriptor { + return object : AbstractTestDescriptor(parent.uniqueId.append("scenario", id.toString()), "Scenario $id") { + override fun getType(): TestDescriptor.Type = TestDescriptor.Type.CONTAINER_AND_TEST + + override fun mayRegisterTests(): Boolean = true + + override fun toString(): String = "ScenarioDescriptor" + } + } + + /** + * Create a [TestDescriptor] for a [Trial]. + */ + private fun Trial.toDescriptor(parent: TestDescriptor): TestDescriptor { + return object : AbstractTestDescriptor(parent.uniqueId.append("repeat", repeat.toString()), "Repeat $repeat") { + override fun getType(): TestDescriptor.Type = TestDescriptor.Type.TEST + + override fun mayRegisterTests(): Boolean = false + + override fun toString(): String = "TrialDescriptor" + } + } +} diff --git a/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/junit5/OpenDCTestEngine.kt b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/junit5/OpenDCTestEngine.kt new file mode 100644 index 00000000..685cd41a --- /dev/null +++ b/simulator/opendc-harness/src/main/kotlin/org/opendc/harness/runner/junit5/OpenDCTestEngine.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness.runner.junit5 + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import mu.KotlinLogging +import org.junit.platform.engine.* +import org.junit.platform.engine.discovery.ClassNameFilter +import org.junit.platform.engine.discovery.ClassSelector +import org.junit.platform.engine.discovery.MethodSelector +import org.junit.platform.engine.support.descriptor.EngineDescriptor +import org.junit.platform.launcher.LauncherDiscoveryRequest +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.engine.ExperimentEngineLauncher +import org.opendc.harness.engine.discovery.DiscoveryFilter +import org.opendc.harness.engine.discovery.DiscoveryProvider +import org.opendc.harness.engine.discovery.DiscoveryRequest +import org.opendc.harness.engine.discovery.DiscoverySelector + +/** + * A [TestEngine] implementation that is able to run experiments defined using the harness. + */ +public class OpenDCTestEngine : TestEngine { + /** + * The logging instance for this engine. + */ + private val logger = KotlinLogging.logger {} + + override fun getId(): String = "opendc" + + override fun discover(request: EngineDiscoveryRequest, uniqueId: UniqueId): TestDescriptor { + // Test whether are excluded from the engines + val isEnabled = (request as? LauncherDiscoveryRequest)?.engineFilters?.all { it.toPredicate().test(this) } ?: true + if (!isEnabled) { + return ExperimentEngineDescriptor(uniqueId, emptyFlow()) + } + + // IntelliJ will pass a [MethodSelector] to run just a single method inside a file. In that + // case, no experiments should be discovered, since we support only experiments by class. + if (request.getSelectorsByType(MethodSelector::class.java).isNotEmpty()) { + return ExperimentEngineDescriptor(uniqueId, emptyFlow()) + } + + val classNames = request.getSelectorsByType(ClassSelector::class.java).map { DiscoverySelector.Meta("class.name", it.className) } + val classNameFilters = request.getFiltersByType(ClassNameFilter::class.java).map { DiscoveryFilter.Name(it.toPredicate()) } + + val discovery = DiscoveryProvider.createComposite() + val definitions = discovery.discover(DiscoveryRequest(classNames, classNameFilters)) + + return ExperimentEngineDescriptor(uniqueId, definitions) + } + + override fun execute(request: ExecutionRequest) { + logger.debug { "JUnit ExecutionRequest[${request::class.java.name}] [configurationParameters=${request.configurationParameters}; rootTestDescriptor=${request.rootTestDescriptor}]" } + val root = request.rootTestDescriptor as ExperimentEngineDescriptor + val listener = request.engineExecutionListener + + listener.executionStarted(root) + + try { + ExperimentEngineLauncher() + .withListener(JUnitExperimentExecutionListener(listener, root)) + .runBlocking(root.experiments) + listener.executionFinished(root, TestExecutionResult.successful()) + } catch (e: Throwable) { + listener.executionFinished(root, TestExecutionResult.failed(e)) + } + } + + private class ExperimentEngineDescriptor(id: UniqueId, val experiments: Flow<ExperimentDefinition>) : EngineDescriptor(id, "opendc") { + override fun mayRegisterTests(): Boolean = true + } +} diff --git a/simulator/opendc-harness/src/main/resources/META-INF/services/org.junit.platform.engine.TestEngine b/simulator/opendc-harness/src/main/resources/META-INF/services/org.junit.platform.engine.TestEngine new file mode 100644 index 00000000..b83eec0c --- /dev/null +++ b/simulator/opendc-harness/src/main/resources/META-INF/services/org.junit.platform.engine.TestEngine @@ -0,0 +1 @@ +org.opendc.harness.runner.junit5.OpenDCTestEngine diff --git a/simulator/opendc-harness/src/main/resources/META-INF/services/org.opendc.harness.engine.discovery.DiscoveryProvider b/simulator/opendc-harness/src/main/resources/META-INF/services/org.opendc.harness.engine.discovery.DiscoveryProvider new file mode 100644 index 00000000..d6a73ded --- /dev/null +++ b/simulator/opendc-harness/src/main/resources/META-INF/services/org.opendc.harness.engine.discovery.DiscoveryProvider @@ -0,0 +1 @@ +org.opendc.harness.internal.DslDiscoveryProvider diff --git a/simulator/opendc-harness/src/main/resources/META-INF/services/org.opendc.harness.engine.scheduler.ExperimentSchedulerProvider b/simulator/opendc-harness/src/main/resources/META-INF/services/org.opendc.harness.engine.scheduler.ExperimentSchedulerProvider new file mode 100644 index 00000000..2ba3a7cb --- /dev/null +++ b/simulator/opendc-harness/src/main/resources/META-INF/services/org.opendc.harness.engine.scheduler.ExperimentSchedulerProvider @@ -0,0 +1 @@ +org.opendc.harness.engine.scheduler.ThreadPoolExperimentSchedulerProvider diff --git a/simulator/opendc-harness/src/main/resources/META-INF/services/org.opendc.harness.engine.strategy.ExperimentStrategyProvider b/simulator/opendc-harness/src/main/resources/META-INF/services/org.opendc.harness.engine.strategy.ExperimentStrategyProvider new file mode 100644 index 00000000..cb1c70ac --- /dev/null +++ b/simulator/opendc-harness/src/main/resources/META-INF/services/org.opendc.harness.engine.strategy.ExperimentStrategyProvider @@ -0,0 +1 @@ +org.opendc.harness.engine.strategy.CartesianExperimentStrategyProvider diff --git a/simulator/opendc-harness/src/main/resources/log4j2.xml b/simulator/opendc-harness/src/main/resources/log4j2.xml new file mode 100644 index 00000000..9553d964 --- /dev/null +++ b/simulator/opendc-harness/src/main/resources/log4j2.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ MIT License + ~ + ~ Copyright (c) 2020 atlarge-research + ~ + ~ Permission is hereby granted, free of charge, to any person obtaining a copy + ~ of this software and associated documentation files (the "Software"), to deal + ~ in the Software without restriction, including without limitation the rights + ~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + ~ copies of the Software, and to permit persons to whom the Software is + ~ furnished to do so, subject to the following conditions: + ~ + ~ The above copyright notice and this permission notice shall be included in all + ~ copies or substantial portions of the Software. + ~ + ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + ~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + ~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + ~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + ~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + ~ SOFTWARE. + --> + +<Configuration status="WARN"> + <Appenders> + <Console name="Console" target="SYSTEM_OUT"> + <PatternLayout pattern="%d{HH:mm:ss.SSS} [%highlight{%-5level}] %logger{36} - %msg%n" disableAnsi="false"/> + </Console> + </Appenders> + <Loggers> + <Logger name="org.opendc" level="info" additivity="false"> + <AppenderRef ref="Console"/> + </Logger> + <Root level="error"> + <AppenderRef ref="Console"/> + </Root> + </Loggers> +</Configuration> diff --git a/simulator/opendc-harness/src/test/kotlin/org/opendc/harness/EngineTest.kt b/simulator/opendc-harness/src/test/kotlin/org/opendc/harness/EngineTest.kt new file mode 100644 index 00000000..6f2989db --- /dev/null +++ b/simulator/opendc-harness/src/test/kotlin/org/opendc/harness/EngineTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness + +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.opendc.harness.api.ExperimentDefinition +import org.opendc.harness.engine.ExperimentEngine +import org.opendc.harness.engine.ExperimentEngineLauncher +import org.opendc.harness.engine.ExperimentExecutionListener +import org.opendc.harness.engine.discovery.DiscoveryProvider +import org.opendc.harness.engine.discovery.DiscoveryRequest + +/** + * A test suite for the [ExperimentEngine]. + */ +internal class EngineTest { + @Test + fun test() { + val listener = object : ExperimentExecutionListener {} + ExperimentEngineLauncher() + .withListener(listener) + .runBlocking(flowOf(TestExperiment().toDefinition())) + } + + @Test + fun discovery() { + runBlocking { + val discovery = DiscoveryProvider.findById("dsl")?.create() + assertNotNull(discovery) + val res = mutableListOf<ExperimentDefinition>() + discovery?.discover(DiscoveryRequest())?.toList(res) + println(res) + assertEquals(1, res.size) + } + } +} diff --git a/simulator/opendc-harness/src/test/kotlin/org/opendc/harness/TestExperiment.kt b/simulator/opendc-harness/src/test/kotlin/org/opendc/harness/TestExperiment.kt new file mode 100644 index 00000000..bedd1c76 --- /dev/null +++ b/simulator/opendc-harness/src/test/kotlin/org/opendc/harness/TestExperiment.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.harness + +import org.opendc.harness.dsl.Experiment +import org.opendc.harness.dsl.anyOf + +/** + * An experiment to test the harness' functionality. + */ +class TestExperiment : Experiment("Design Space Exploration") { + /** + * The cloud environment to use. + */ + private val environment: String by anyOf( + "../../traces/setup-small.json", + "../../traces/setup.json", + "../../traces/setup-large.json" + ) + + /** + * The trace to use. + */ + private val trace: String by anyOf( + "../../traces/gwf/askalon_workload_olde.gwf", + "../../traces/gwf/askalon_workload_ee.gwf", + "../../traces/gwf/chronos_exp_noscaler_ca.gwf" + ) + + override fun doRun(repeat: Int) { + println("Id $id, Run $repeat, Environment $environment, Trace $trace") + Thread.sleep(500 * id.toLong()) + } +} diff --git a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/Main.kt b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/Main.kt index 2117b675..7796019a 100644 --- a/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/Main.kt +++ b/simulator/opendc-runner-web/src/main/kotlin/org/opendc/runner/web/Main.kt @@ -50,6 +50,7 @@ import org.opendc.experiments.sc20.trace.Sc20ParquetTraceReader import org.opendc.experiments.sc20.trace.Sc20RawParquetTraceReader import org.opendc.format.trace.sc20.Sc20PerformanceInterferenceReader import org.opendc.simulator.utils.DelayControllerClockAdapter +import org.opendc.trace.core.EventTracer import java.io.File import kotlin.coroutines.coroutineContext import kotlin.random.Random @@ -238,13 +239,15 @@ public class RunnerCli : CliktCommand(name = "runner") { val topologyId = scenario.getEmbedded(listOf("topology", "topologyId"), ObjectId::class.java) val environment = TopologyParser(topologies, topologyId) val monitor = WebExperimentMonitor() + val tracer = EventTracer(clock) testScope.launch { val (bareMetalProvisioner, scheduler) = createProvisioner( this, clock, environment, - allocationPolicy + allocationPolicy, + tracer ) val failureDomain = if (operational.getBoolean("failuresEnabled")) { diff --git a/simulator/opendc-simulator/opendc-simulator-compute/build.gradle.kts b/simulator/opendc-simulator/opendc-simulator-compute/build.gradle.kts index cd7e5706..844a7c6d 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/build.gradle.kts +++ b/simulator/opendc-simulator/opendc-simulator-compute/build.gradle.kts @@ -28,6 +28,7 @@ plugins { dependencies { api(project(":opendc-simulator:opendc-simulator-core")) + implementation(project(":opendc-utils")) testImplementation("org.junit.jupiter:junit-jupiter-api:${Library.JUNIT_JUPITER}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Library.JUNIT_JUPITER}") diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt index c6d5bdd1..812b5f20 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt @@ -25,13 +25,13 @@ package org.opendc.simulator.compute import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.intrinsics.startCoroutineCancellable -import kotlinx.coroutines.selects.SelectClause0 -import kotlinx.coroutines.selects.SelectInstance +import org.opendc.simulator.compute.model.ProcessingUnit +import org.opendc.simulator.compute.workload.SimResourceCommand import org.opendc.simulator.compute.workload.SimWorkload -import java.lang.Runnable +import org.opendc.utils.TimerScheduler import java.time.Clock -import kotlin.coroutines.ContinuationInterceptor +import java.util.* +import kotlin.coroutines.* import kotlin.math.ceil import kotlin.math.max import kotlin.math.min @@ -59,23 +59,29 @@ public class SimBareMetalMachine( get() = usageState /** + * A flag to indicate that the machine is terminated. + */ + private var isTerminated = false + + /** + * The [MutableStateFlow] containing the load of the server. + */ + private val usageState = MutableStateFlow(0.0) + + /** * The current active workload. */ - private var activeWorkload: SimWorkload? = null + private var cont: Continuation<Unit>? = null /** - * Run the specified [SimWorkload] on this machine and suspend execution util the workload has finished. + * The active CPUs of this machine. */ - override suspend fun run(workload: SimWorkload) { - require(activeWorkload == null) { "Run should not be called concurrently" } + private var cpus: List<Cpu> = emptyList() - try { - activeWorkload = workload - workload.run(ctx) - } finally { - activeWorkload = null - } - } + /** + * The [TimerScheduler] to use for scheduling the interrupts. + */ + private val scheduler = TimerScheduler<Cpu>(coroutineScope, clock) /** * The execution context in which the workload runs. @@ -87,195 +93,220 @@ public class SimBareMetalMachine( override val clock: Clock get() = this@SimBareMetalMachine.clock - override fun onRun( - batch: Sequence<SimExecutionContext.Slice>, - triggerMode: SimExecutionContext.TriggerMode, - merge: (SimExecutionContext.Slice, SimExecutionContext.Slice) -> SimExecutionContext.Slice - ): SelectClause0 { - return object : SelectClause0 { - @InternalCoroutinesApi - override fun <R> registerSelectClause0(select: SelectInstance<R>, block: suspend () -> R) { - // Do not reset the usage state: we will set it ourselves - usageFlush?.dispose() - usageFlush = null - - val queue = batch.iterator() - var start = Long.MIN_VALUE - var currentWork: SliceWork? = null - var currentDisposable: DisposableHandle? = null - - fun schedule(slice: SimExecutionContext.Slice) { - start = clock.millis() - - val isLastSlice = !queue.hasNext() - val work = SliceWork(slice) - val candidateDuration = when (triggerMode) { - SimExecutionContext.TriggerMode.FIRST -> work.minExit - SimExecutionContext.TriggerMode.LAST -> work.maxExit - SimExecutionContext.TriggerMode.DEADLINE -> slice.deadline - start - } - - // Check whether the deadline is exceeded during the run of the slice. - val duration = min(candidateDuration, slice.deadline - start) - - val action = Runnable { - currentWork = null - - // Flush all the work that was performed - val hasFinished = work.stop(duration) - - if (!isLastSlice) { - val candidateSlice = queue.next() - val nextSlice = - // If our previous slice exceeds its deadline, merge it with the next candidate slice - if (hasFinished) - candidateSlice - else - merge(candidateSlice, slice) - schedule(nextSlice) - } else if (select.trySelect()) { - block.startCoroutineCancellable(select.completion) - } - } - - // Schedule the flush after the entire slice has finished - currentDisposable = delay.invokeOnTimeout(duration, action) - - // Start the slice work - currentWork = work - work.start() - } + override fun interrupt(cpu: Int) { + require(cpu < cpus.size) { "Invalid CPU identifier" } + cpus[cpu].interrupt() + } + } - // Schedule the first work - if (queue.hasNext()) { - schedule(queue.next()) + /** + * Run the specified [SimWorkload] on this machine and suspend execution util the workload has finished. + */ + override suspend fun run(workload: SimWorkload) { + require(!isTerminated) { "Machine is terminated" } + require(cont == null) { "Run should not be called concurrently" } - // A DisposableHandle to flush the work in case the call is cancelled - val disposable = DisposableHandle { - val end = clock.millis() - val duration = end - start + workload.onStart(ctx) - currentWork?.stop(duration) - currentDisposable?.dispose() + return suspendCancellableCoroutine { cont -> + this.cont = cont + this.cpus = model.cpus.map { Cpu(it, workload) } - // Schedule reset the usage of the machine since the call is returning - usageFlush = delay.invokeOnTimeout(1) { - usageState.value = 0.0 - usageFlush = null - } - } - - select.disposeOnSelect(disposable) - } else if (select.trySelect()) { - // No work has been given: select immediately - block.startCoroutineCancellable(select.completion) - } - } + for (cpu in cpus) { + cpu.start() } } } /** - * The [MutableStateFlow] containing the load of the server. + * Terminate the specified bare-metal machine. */ - private val usageState = MutableStateFlow(0.0) + override fun close() { + isTerminated = true + } + + /** + * Update the usage of the machine. + */ + private fun updateUsage() { + usageState.value = cpus.sumByDouble { it.speed } / cpus.sumByDouble { it.model.frequency } + } /** - * A disposable to prevent resetting the usage state for subsequent calls to onRun. + * This method is invoked when one of the CPUs has exited. */ - private var usageFlush: DisposableHandle? = null + private fun onCpuExit(cpu: Int) { + // Check whether all other CPUs have finished + if (cpus.all { it.hasExited }) { + val cont = cont + this.cont = null + cont?.resume(Unit) + } + } /** - * Cache the [Delay] instance for timing. - * - * XXX We need to cache this before the call to [onRun] since doing this in [onRun] is too heavy. - * XXX Note however that this is an ugly hack which may break in the future. + * This method is invoked when one of the CPUs failed. */ - @OptIn(InternalCoroutinesApi::class) - private val delay = coroutineScope.coroutineContext[ContinuationInterceptor] as Delay + private fun onCpuFailure(e: Throwable) { + // Make sure no other tasks will be resumed. + scheduler.cancelAll() + + // In case the flush fails with an exception, immediately propagate to caller, cancelling all other + // tasks. + val cont = cont + this.cont = null + cont?.resumeWithException(e) + } /** - * A slice to be processed. + * A physical CPU of the machine. */ - private inner class SliceWork(val slice: SimExecutionContext.Slice) { + private inner class Cpu(val model: ProcessingUnit, val workload: SimWorkload) { /** - * The duration after which the first processor finishes processing this slice. + * The current command. */ - val minExit: Long + private var currentCommand: CommandWrapper? = null /** - * The duration after which the last processor finishes processing this slice. + * The actual processing speed. */ - val maxExit: Long + var speed: Double = 0.0 + set(value) { + field = value + updateUsage() + } /** - * A flag to indicate that the slice will exceed the deadline. + * A flag to indicate that the CPU is currently processing a command. */ - val exceedsDeadline: Boolean - get() = slice.deadline < maxExit + var isIntermediate: Boolean = false /** - * The total amount of CPU usage. + * A flag to indicate that the CPU has exited. */ - val totalUsage: Double + var hasExited: Boolean = false /** - * A flag to indicate that this slice is empty. + * Process the specified [SimResourceCommand] for this CPU. */ - val isEmpty: Boolean - - init { - var totalUsage = 0.0 - var minExit = Long.MAX_VALUE - var maxExit = 0L - var nonEmpty = false - - // Determine the duration of the first/last CPU to finish - for (i in 0 until min(model.cpus.size, slice.burst.size)) { - val cpu = model.cpus[i] - val usage = min(slice.limit[i], cpu.frequency) - val cpuDuration = ceil(slice.burst[i] / usage * 1000).toLong() // Convert from seconds to milliseconds - - totalUsage += usage / cpu.frequency - - if (cpuDuration != 0L) { // We only wait for processor cores with a non-zero burst - minExit = min(minExit, cpuDuration) - maxExit = max(maxExit, cpuDuration) - nonEmpty = true + fun process(command: SimResourceCommand) { + val timestamp = clock.millis() + + val task = when (command) { + is SimResourceCommand.Idle -> { + speed = 0.0 + + val deadline = command.deadline + + require(deadline >= timestamp) { "Deadline already passed" } + + if (deadline != Long.MAX_VALUE) { + scheduler.startSingleTimerTo(this, deadline) { flush() } + } else { + null + } + } + is SimResourceCommand.Consume -> { + val work = command.work + val limit = command.limit + val deadline = command.deadline + + require(deadline >= timestamp) { "Deadline already passed" } + + speed = min(model.frequency, limit) + + // The required duration to process all the work + val finishedAt = timestamp + ceil(work / speed * 1000).toLong() + + scheduler.startSingleTimerTo(this, min(finishedAt, deadline)) { flush() } + } + is SimResourceCommand.Exit -> { + speed = 0.0 + hasExited = true + + onCpuExit(model.id) + + null } } - this.isEmpty = !nonEmpty - this.totalUsage = totalUsage - this.minExit = minExit - this.maxExit = maxExit + assert(currentCommand == null) { "Concurrent access to current command" } + currentCommand = CommandWrapper(timestamp, command) } /** - * Indicate that the work on the slice has started. + * Request the workload for more work. */ - fun start() { - usageState.value = totalUsage / model.cpus.size + private fun next(remainingWork: Double) { + process(workload.onNext(ctx, model.id, remainingWork)) } /** - * Flush the work performed on the slice. + * Start the CPU. */ - fun stop(duration: Long): Boolean { - var hasFinished = true + fun start() { + try { + isIntermediate = true + + process(workload.onStart(ctx, model.id)) + } catch (e: Throwable) { + onCpuFailure(e) + } finally { + isIntermediate = false + } + } - for (i in 0 until min(model.cpus.size, slice.burst.size)) { - val usage = min(slice.limit[i], model.cpus[i].frequency) - val granted = ceil(duration / 1000.0 * usage).toLong() - val res = max(0, slice.burst[i] - granted) - slice.burst[i] = res + /** + * Flush the work performed by the CPU. + */ + fun flush() { + try { + val (timestamp, command) = currentCommand ?: return + + isIntermediate = true + currentCommand = null + + // Cancel the running task and flush the progress + scheduler.cancel(this) + + when (command) { + is SimResourceCommand.Idle -> next(remainingWork = 0.0) + is SimResourceCommand.Consume -> { + val duration = clock.millis() - timestamp + val remainingWork = if (duration > 0L) { + val processed = duration / 1000.0 * speed + max(0.0, command.work - processed) + } else { + 0.0 + } - if (res != 0L) { - hasFinished = false + next(remainingWork) + } + SimResourceCommand.Exit -> throw IllegalStateException() } + } catch (e: Throwable) { + onCpuFailure(e) + } finally { + isIntermediate = false + } + } + + /** + * Interrupt the CPU. + */ + fun interrupt() { + // Prevent users from interrupting the CPU while it is constructing its next command, this will only lead + // to infinite recursion. + if (isIntermediate) { + return } - return hasFinished + flush() } } + + /** + * This class wraps a [command] with the timestamp it was started and possibly the task associated with it. + */ + private data class CommandWrapper(val timestamp: Long, val command: SimResourceCommand) } diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimExecutionContext.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimExecutionContext.kt index 5801fcd5..c7c3d3cc 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimExecutionContext.kt +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimExecutionContext.kt @@ -22,8 +22,6 @@ package org.opendc.simulator.compute -import kotlinx.coroutines.selects.SelectClause0 -import kotlinx.coroutines.selects.select import java.time.Clock /** @@ -43,113 +41,10 @@ public interface SimExecutionContext { public val machine: SimMachineModel /** - * Ask the processor cores to run the specified [slice] and suspend execution until the trigger condition is met as - * specified by [triggerMode]. + * Ask the host machine to interrupt the specified vCPU. * - * After the method returns, [Slice.burst] will contain the remaining burst length for each of the cores (which - * may be zero). These changes may happen anytime during execution of this method and callers should not rely on - * the timing of this change. - * - * @param slice The representation of work to run on the processors. - * @param triggerMode The trigger condition to resume execution. - */ - public suspend fun run(slice: Slice, triggerMode: TriggerMode = TriggerMode.FIRST): Unit = - select { onRun(slice, triggerMode).invoke {} } - - /** - * Ask the processors cores to run the specified [batch] of work slices and suspend execution until the trigger - * condition is met as specified by [triggerMode]. - * - * After the method returns, [Slice.burst] will contain the remaining burst length for each of the cores (which - * may be zero). These changes may happen anytime during execution of this method and callers should not rely on - * the timing of this change. - * - * In case slices in the batch do not finish processing before their deadline, [merge] is called to merge these - * slices with the next slice to be executed. - * - * @param batch The batch of work to run on the processors. - * @param triggerMode The trigger condition to resume execution. - * @param merge The merge function for consecutive slices in case the last slice was not completed within its - * deadline. + * @param cpu The id of the vCPU to interrupt. + * @throws IllegalArgumentException if the identifier points to a non-existing vCPU. */ - public suspend fun run( - batch: Sequence<Slice>, - triggerMode: TriggerMode = TriggerMode.FIRST, - merge: (Slice, Slice) -> Slice = { _, r -> r } - ): Unit = select { onRun(batch, triggerMode, merge).invoke {} } - - /** - * Ask the processor cores to run the specified [slice] and select when the trigger condition is met as specified - * by [triggerMode]. - * - * After the method returns, [Slice.burst] will contain the remaining burst length for each of the cores (which - * may be zero). These changes may happen anytime during execution of this method and callers should not rely on - * the timing of this change. - * - * @param slice The representation of work to request from the processors. - * @param triggerMode The trigger condition to resume execution. - */ - public fun onRun(slice: Slice, triggerMode: TriggerMode = TriggerMode.FIRST): SelectClause0 = - onRun(sequenceOf(slice), triggerMode) - - /** - * Ask the processors cores to run the specified [batch] of work slices and select when the trigger condition is met - * as specified by [triggerMode]. - * - * After the method returns, [Slice.burst] will contain the remaining burst length for each of the cores (which - * may be zero). These changes may happen anytime during execution of this method and callers should not rely on - * the timing of this change. - * - * In case slices in the batch do not finish processing before their deadline, [merge] is called to merge these - * slices with the next slice to be executed. - * - * @param batch The batch of work to run on the processors. - * @param triggerMode The trigger condition to resume execution during the **last** slice. - * @param merge The merge function for consecutive slices in case the last slice was not completed within its - * deadline. - */ - public fun onRun( - batch: Sequence<Slice>, - triggerMode: TriggerMode = TriggerMode.FIRST, - merge: (Slice, Slice) -> Slice = { _, r -> r } - ): SelectClause0 - - /** - * A request to the host machine for a slice of CPU time from the processor cores. - * - * Both [burst] and [limit] must be of the same size and in any other case the method will throw an - * [IllegalArgumentException]. - * - * - * @param burst The burst time to request from each of the processor cores. - * @param limit The maximum usage in terms of MHz that the processing core may use while running the burst. - * @param deadline The instant at which this slice needs to be fulfilled. - */ - public class Slice(public val burst: LongArray, public val limit: DoubleArray, public val deadline: Long) { - init { - require(burst.size == limit.size) { "Incompatible array dimensions" } - } - } - - /** - * The modes for triggering a machine exit from the machine. - */ - public enum class TriggerMode { - /** - * A machine exit occurs when either the first processor finishes processing a **non-zero** burst or the - * deadline is reached. - */ - FIRST, - - /** - * A machine exit occurs when either the last processor finishes processing a **non-zero** burst or the deadline - * is reached. - */ - LAST, - - /** - * A machine exit occurs only when the deadline is reached. - */ - DEADLINE - } + public fun interrupt(cpu: Int) } diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimFairShareHypervisor.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimFairShareHypervisor.kt new file mode 100644 index 00000000..5e86d32b --- /dev/null +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimFairShareHypervisor.kt @@ -0,0 +1,590 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import org.opendc.simulator.compute.interference.PerformanceInterferenceModel +import org.opendc.simulator.compute.model.ProcessingUnit +import org.opendc.simulator.compute.workload.SimResourceCommand +import org.opendc.simulator.compute.workload.SimWorkload +import org.opendc.simulator.compute.workload.SimWorkloadBarrier +import java.time.Clock +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min + +/** + * A [SimHypervisor] that distributes the computing requirements of multiple [SimWorkload] on a single + * [SimBareMetalMachine] concurrently using weighted fair sharing. + * + * @param listener The hypervisor listener to use. + */ +public class SimFairShareHypervisor(private val listener: SimHypervisor.Listener? = null) : SimHypervisor { + + override fun onStart(ctx: SimExecutionContext) { + val model = ctx.machine + this.ctx = ctx + this.commands = Array(model.cpus.size) { SimResourceCommand.Idle() } + this.pCpus = model.cpus.indices.sortedBy { model.cpus[it].frequency }.toIntArray() + this.maxUsage = model.cpus.sumByDouble { it.frequency } + this.barrier = SimWorkloadBarrier(model.cpus.size) + } + + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + return commands[cpu] + } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + totalRemainingWork += remainingWork + val isLast = barrier.enter() + + // Flush the progress of the guest after the barrier has been reached. + if (isLast && isDirty) { + isDirty = false + flushGuests() + } + + return if (isDirty) { + // Wait for the scheduler determine the work after the barrier has been reached by all CPUs. + SimResourceCommand.Idle() + } else { + // Indicate that the scheduler needs to run next call. + if (isLast) { + isDirty = true + } + + commands[cpu] + } + } + + override fun canFit(model: SimMachineModel): Boolean = true + + override fun createMachine( + model: SimMachineModel, + performanceInterferenceModel: PerformanceInterferenceModel? + ): SimMachine = SimVm(model, performanceInterferenceModel) + + /** + * The execution context in which the hypervisor runs. + */ + private lateinit var ctx: SimExecutionContext + + /** + * The commands to submit to the underlying host. + */ + private lateinit var commands: Array<SimResourceCommand> + + /** + * The active vCPUs. + */ + private val vcpus: MutableList<VCpu> = mutableListOf() + + /** + * The indices of the physical CPU ordered by their speed. + */ + private lateinit var pCpus: IntArray + + /** + * The maximum amount of work to be performed per second. + */ + private var maxUsage: Double = 0.0 + + /** + * The current load on the hypervisor. + */ + private var load: Double = 0.0 + + /** + * The total amount of remaining work (of all pCPUs). + */ + private var totalRemainingWork: Double = 0.0 + + /** + * The total speed requested by the vCPUs. + */ + private var totalRequestedSpeed = 0.0 + + /** + * The total amount of work requested by the vCPUs. + */ + private var totalRequestedWork = 0.0 + + /** + * The total allocated speed for the vCPUs. + */ + private var totalAllocatedSpeed = 0.0 + + /** + * The total allocated work requested for the vCPUs. + */ + private var totalAllocatedWork = 0.0 + + /** + * The amount of work that could not be performed due to over-committing resources. + */ + private var totalOvercommittedWork = 0.0 + + /** + * The amount of work that was lost due to interference. + */ + private var totalInterferedWork = 0.0 + + /** + * A flag to indicate that the scheduler has submitted work that has not yet been completed. + */ + private var isDirty: Boolean = false + + /** + * The scheduler barrier. + */ + private lateinit var barrier: SimWorkloadBarrier + + /** + * Indicate that the workloads should be re-scheduled. + */ + private fun shouldSchedule() { + isDirty = true + ctx.interruptAll() + } + + /** + * Schedule the work over the physical CPUs. + */ + private fun doSchedule() { + // If there is no work yet, mark all pCPUs as idle. + if (vcpus.isEmpty()) { + commands.fill(SimResourceCommand.Idle()) + ctx.interruptAll() + } + + var duration: Double = Double.MAX_VALUE + var deadline: Long = Long.MAX_VALUE + var availableSpeed = maxUsage + var totalRequestedSpeed = 0.0 + var totalRequestedWork = 0.0 + + // Sort the vCPUs based on their requested usage + // Profiling shows that it is faster to sort every slice instead of maintaining some kind of sorted set + vcpus.sort() + + // Divide the available host capacity fairly across the vCPUs using max-min fair sharing + val vcpuIterator = vcpus.listIterator() + var remaining = vcpus.size + while (vcpuIterator.hasNext()) { + val vcpu = vcpuIterator.next() + val availableShare = availableSpeed / remaining-- + + when (val command = vcpu.command) { + is SimResourceCommand.Idle -> { + // Take into account the minimum deadline of this slice before we possible continue + deadline = min(deadline, command.deadline) + + vcpu.actualSpeed = 0.0 + } + is SimResourceCommand.Consume -> { + val grantedSpeed = min(vcpu.allowedSpeed, availableShare) + + // Take into account the minimum deadline of this slice before we possible continue + deadline = min(deadline, command.deadline) + + // Ignore idle computation + if (grantedSpeed <= 0.0 || command.work <= 0.0) { + vcpu.actualSpeed = 0.0 + continue + } + + totalRequestedSpeed += command.limit + totalRequestedWork += command.work + + vcpu.actualSpeed = grantedSpeed + availableSpeed -= grantedSpeed + + // The duration that we want to run is that of the shortest request from a vCPU + duration = min(duration, command.work / grantedSpeed) + } + SimResourceCommand.Exit -> { + // Apparently the vCPU has exited, so remove it from the scheduling queue. + vcpuIterator.remove() + } + } + } + + // Round the duration to milliseconds + duration = ceil(duration * 1000) / 1000 + + assert(deadline >= ctx.clock.millis()) { "Deadline already passed" } + + val totalAllocatedSpeed = maxUsage - availableSpeed + var totalAllocatedWork = 0.0 + availableSpeed = totalAllocatedSpeed + load = totalAllocatedSpeed / maxUsage + + // Divide the requests over the available capacity of the pCPUs fairly + for (i in pCpus) { + val maxCpuUsage = ctx.machine.cpus[i].frequency + val fraction = maxCpuUsage / maxUsage + val grantedSpeed = min(maxCpuUsage, totalAllocatedSpeed * fraction) + val grantedWork = duration * grantedSpeed + + commands[i] = + if (grantedWork > 0.0 && grantedSpeed > 0.0) + SimResourceCommand.Consume(grantedWork, grantedSpeed, deadline) + else + SimResourceCommand.Idle(deadline) + + totalAllocatedWork += grantedWork + availableSpeed -= grantedSpeed + } + + this.totalRequestedSpeed = totalRequestedSpeed + this.totalRequestedWork = totalRequestedWork + this.totalAllocatedSpeed = totalAllocatedSpeed + this.totalAllocatedWork = totalAllocatedWork + + ctx.interruptAll() + } + + /** + * Flush the progress of the vCPUs. + */ + private fun flushGuests() { + // Flush all the vCPUs work + for (vcpu in vcpus) { + vcpu.flush(interrupt = false) + } + + // Report metrics + listener?.onSliceFinish( + this, + totalRequestedWork.toLong(), + (totalAllocatedWork - totalRemainingWork).toLong(), + totalOvercommittedWork.toLong(), + totalInterferedWork.toLong(), + totalRequestedSpeed, + totalAllocatedSpeed + ) + totalRemainingWork = 0.0 + totalInterferedWork = 0.0 + totalOvercommittedWork = 0.0 + + // Force all pCPUs to re-schedule their work. + doSchedule() + } + + /** + * Interrupt all host CPUs. + */ + private fun SimExecutionContext.interruptAll() { + for (i in machine.cpus.indices) { + interrupt(i) + } + } + + /** + * A virtual machine running on the hypervisor. + * + * @property model The machine model of the virtual machine. + * @property performanceInterferenceModel The performance interference model to utilize. + */ + private inner class SimVm( + override val model: SimMachineModel, + val performanceInterferenceModel: PerformanceInterferenceModel? = null, + ) : SimMachine { + /** + * A [StateFlow] representing the CPU usage of the simulated machine. + */ + override val usage: MutableStateFlow<Double> = MutableStateFlow(0.0) + + /** + * A flag to indicate that the machine is terminated. + */ + private var isTerminated = false + + /** + * The current active workload. + */ + private var cont: Continuation<Unit>? = null + + /** + * The active CPUs of this virtual machine. + */ + private var cpus: List<VCpu> = emptyList() + + /** + * The execution context in which the workload runs. + */ + val ctx = object : SimExecutionContext { + override val machine: SimMachineModel + get() = model + + override val clock: Clock + get() = this@SimFairShareHypervisor.ctx.clock + + override fun interrupt(cpu: Int) { + require(cpu < cpus.size) { "Invalid CPU identifier" } + cpus[cpu].interrupt() + } + } + + /** + * Run the specified [SimWorkload] on this machine and suspend execution util the workload has finished. + */ + override suspend fun run(workload: SimWorkload) { + require(!isTerminated) { "Machine is terminated" } + require(cont == null) { "Run should not be called concurrently" } + + workload.onStart(ctx) + + return suspendCancellableCoroutine { cont -> + this.cont = cont + this.cpus = model.cpus.map { VCpu(this, it, workload) } + + for (cpu in cpus) { + // Register vCPU to scheduler + vcpus.add(cpu) + + cpu.start() + } + + // Re-schedule the work over the pCPUs + shouldSchedule() + } + } + + /** + * Terminate this VM instance. + */ + override fun close() { + isTerminated = true + } + + /** + * Update the usage of the VM. + */ + fun updateUsage() { + usage.value = cpus.sumByDouble { it.actualSpeed } / cpus.sumByDouble { it.model.frequency } + } + + /** + * This method is invoked when one of the CPUs has exited. + */ + fun onCpuExit(cpu: Int) { + // Check whether all other CPUs have finished + if (cpus.all { it.hasExited }) { + val cont = cont + this.cont = null + cont?.resume(Unit) + } + } + + /** + * This method is invoked when one of the CPUs failed. + */ + fun onCpuFailure(e: Throwable) { + // In case the flush fails with an exception, immediately propagate to caller, cancelling all other + // tasks. + val cont = cont + this.cont = null + cont?.resumeWithException(e) + } + } + + /** + * A CPU of the virtual machine. + */ + private inner class VCpu(val vm: SimVm, val model: ProcessingUnit, val workload: SimWorkload) : Comparable<VCpu> { + /** + * The latest command processed by the CPU. + */ + var command: SimResourceCommand = SimResourceCommand.Idle() + + /** + * The latest timestamp at which the vCPU was flushed. + */ + var latestFlush: Long = 0 + + /** + * The processing speed that is allowed by the model constraints. + */ + var allowedSpeed: Double = 0.0 + + /** + * The actual processing speed. + */ + var actualSpeed: Double = 0.0 + set(value) { + field = value + vm.updateUsage() + } + + /** + * A flag to indicate that the CPU is currently processing a command. + */ + var isIntermediate: Boolean = false + + /** + * A flag to indicate that the CPU has exited. + */ + val hasExited: Boolean + get() = command is SimResourceCommand.Exit + + /** + * Process the specified [SimResourceCommand] for this CPU. + */ + fun process(command: SimResourceCommand) { + // Assign command as the most recent executed command + this.command = command + + when (command) { + is SimResourceCommand.Idle -> { + require(command.deadline >= ctx.clock.millis()) { "Deadline already passed" } + + allowedSpeed = 0.0 + } + is SimResourceCommand.Consume -> { + require(command.deadline >= ctx.clock.millis()) { "Deadline already passed" } + + allowedSpeed = min(model.frequency, command.limit) + } + is SimResourceCommand.Exit -> { + allowedSpeed = 0.0 + actualSpeed = 0.0 + + vm.onCpuExit(model.id) + } + } + } + + /** + * Start the CPU. + */ + fun start() { + try { + isIntermediate = true + latestFlush = ctx.clock.millis() + + process(workload.onStart(vm.ctx, model.id)) + } catch (e: Throwable) { + fail(e) + } finally { + isIntermediate = false + } + } + + /** + * Flush the work performed by the CPU. + */ + fun flush(interrupt: Boolean) { + val now = ctx.clock.millis() + + // Fast path: if the CPU was already flushed at at the current instant, no need to flush the progress. + if (latestFlush >= now) { + return + } + + try { + isIntermediate = true + when (val command = command) { + is SimResourceCommand.Idle -> { + // Act like nothing has happened in case the vCPU did not reach its deadline or was not + // interrupted by the user. + if (interrupt || command.deadline <= now) { + process(workload.onNext(vm.ctx, model.id, 0.0)) + } + } + is SimResourceCommand.Consume -> { + // Apply performance interference model + val performanceScore = vm.performanceInterferenceModel?.apply(load) ?: 1.0 + + // Compute the remaining amount of work + val remainingWork = if (command.work > 0.0) { + // Compute the fraction of compute time allocated to the VM + val fraction = actualSpeed / totalAllocatedSpeed + + // Compute the work that was actually granted to the VM. + val processingAvailable = max(0.0, totalAllocatedWork - totalRemainingWork) * fraction + val processed = processingAvailable * performanceScore + + val interferedWork = processingAvailable - processed + totalInterferedWork += interferedWork + + max(0.0, command.work - processed) + } else { + 0.0 + } + + // Act like nothing has happened in case the vCPU did not finish yet or was not interrupted by + // the user. + if (interrupt || remainingWork == 0.0 || command.deadline <= now) { + if (!interrupt) { + totalOvercommittedWork += remainingWork + } + + process(workload.onNext(vm.ctx, model.id, remainingWork)) + } else { + process(SimResourceCommand.Consume(remainingWork, command.limit, command.deadline)) + } + } + SimResourceCommand.Exit -> + throw IllegalStateException() + } + } catch (e: Throwable) { + fail(e) + } finally { + latestFlush = now + isIntermediate = false + } + } + + /** + * Interrupt the CPU. + */ + fun interrupt() { + // Prevent users from interrupting the CPU while it is constructing its next command, this will only lead + // to infinite recursion. + if (isIntermediate) { + return + } + + flush(interrupt = true) + + // Force the scheduler to re-schedule + shouldSchedule() + } + + /** + * Fail the CPU. + */ + fun fail(e: Throwable) { + command = SimResourceCommand.Exit + vm.onCpuFailure(e) + } + + override fun compareTo(other: VCpu): Int = allowedSpeed.compareTo(other.allowedSpeed) + } +} diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimFairShareHypervisorProvider.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimFairShareHypervisorProvider.kt new file mode 100644 index 00000000..02eb6ad0 --- /dev/null +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimFairShareHypervisorProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute + +/** + * A [SimHypervisorProvider] for the [SimFairShareHypervisor] implementation. + */ +public class SimFairShareHypervisorProvider : SimHypervisorProvider { + override val id: String = "fair-share" + + override fun create(listener: SimHypervisor.Listener?): SimHypervisor = SimFairShareHypervisor(listener) +} diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimHypervisor.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimHypervisor.kt index 6087227b..d8f00bef 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimHypervisor.kt +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimHypervisor.kt @@ -22,267 +22,29 @@ package org.opendc.simulator.compute -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.intrinsics.startCoroutineCancellable -import kotlinx.coroutines.selects.SelectClause0 -import kotlinx.coroutines.selects.SelectInstance -import kotlinx.coroutines.selects.select import org.opendc.simulator.compute.interference.PerformanceInterferenceModel -import org.opendc.simulator.compute.model.ProcessingUnit import org.opendc.simulator.compute.workload.SimWorkload -import java.time.Clock -import kotlin.math.ceil -import kotlin.math.max -import kotlin.math.min /** - * SimHypervisor distributes the computing requirements of multiple [SimWorkload] on a single [SimBareMetalMachine] concurrently. - * - * @param coroutineScope The [CoroutineScope] to run the simulated workloads in. - * @param clock The virtual clock to track the simulation time. + * A SimHypervisor facilitates the execution of multiple concurrent [SimWorkload]s, while acting as a single workload + * to a [SimBareMetalMachine]. */ -@OptIn(ExperimentalCoroutinesApi::class, InternalCoroutinesApi::class) -public class SimHypervisor( - private val coroutineScope: CoroutineScope, - private val clock: Clock, - private val listener: Listener? = null -) : SimWorkload { +public interface SimHypervisor : SimWorkload { /** - * A set for tracking the VM context objects. + * Determine whether the specified machine characterized by [model] can fit on this hypervisor at this moment. */ - private val vms: MutableSet<VmExecutionContext> = mutableSetOf() - - /** - * A flag to indicate the driver is stopped. - */ - private var stopped: Boolean = false - - /** - * The channel for scheduling new CPU requests. - */ - private val schedulingQueue = Channel<SchedulerCommand>(Channel.UNLIMITED) + public fun canFit(model: SimMachineModel): Boolean /** * Create a [SimMachine] instance on which users may run a [SimWorkload]. * * @param model The machine to create. + * @param performanceInterferenceModel The performance interference model to use. */ - public fun createMachine(model: SimMachineModel, performanceInterferenceModel: PerformanceInterferenceModel? = null): SimMachine { - val vm = VmSession(model, performanceInterferenceModel) - val vmCtx = VmExecutionContext(vm) - - return object : SimMachine { - override val model: SimMachineModel - get() = vmCtx.machine - - override val usage: StateFlow<Double> - get() = vm.usage - - /** - * The current active workload. - */ - private var activeWorkload: SimWorkload? = null - - override suspend fun run(workload: SimWorkload) { - require(activeWorkload == null) { "Run should not be called concurrently" } - - try { - activeWorkload = workload - workload.run(vmCtx) - } finally { - activeWorkload = null - } - } - - override fun toString(): String = "SimVirtualMachine" - } - } - - /** - * Run the scheduling process of the hypervisor. - */ - override suspend fun run(ctx: SimExecutionContext) { - val model = ctx.machine - val maxUsage = model.cpus.sumByDouble { it.frequency } - val pCPUs = model.cpus.indices.sortedBy { model.cpus[it].frequency } - - val vms = mutableSetOf<VmSession>() - val vcpus = mutableListOf<VCpu>() - - val usage = DoubleArray(model.cpus.size) - val burst = LongArray(model.cpus.size) - - fun process(command: SchedulerCommand) { - when (command) { - is SchedulerCommand.Schedule -> { - vms += command.vm - vcpus.addAll(command.vm.vcpus) - } - is SchedulerCommand.Deschedule -> { - vms -= command.vm - vcpus.removeAll(command.vm.vcpus) - } - is SchedulerCommand.Interrupt -> { - } - } - } - - fun processRemaining() { - var command = schedulingQueue.poll() - while (command != null) { - process(command) - command = schedulingQueue.poll() - } - } - - while (!stopped) { - // Wait for a request to be submitted if we have no work yet. - if (vcpus.isEmpty()) { - process(schedulingQueue.receive()) - } - - processRemaining() - - val start = clock.millis() - - var duration: Double = Double.POSITIVE_INFINITY - var deadline: Long = Long.MAX_VALUE - var availableUsage = maxUsage - var totalRequestedUsage = 0.0 - var totalRequestedBurst = 0L - - // Sort the vCPUs based on their requested usage - // Profiling shows that it is faster to sort every slice instead of maintaining some kind of sorted set - vcpus.sort() - - // Divide the available host capacity fairly across the vCPUs using max-min fair sharing - for ((i, req) in vcpus.withIndex()) { - val remaining = vcpus.size - i - val availableShare = availableUsage / remaining - val grantedUsage = min(req.limit, availableShare) - - // Take into account the minimum deadline of this slice before we possible continue - deadline = min(deadline, req.vm.deadline) - - // Ignore empty CPUs - if (grantedUsage <= 0 || req.burst <= 0) { - req.allocatedLimit = 0.0 - continue - } - - totalRequestedUsage += req.limit - totalRequestedBurst += req.burst - - req.allocatedLimit = grantedUsage - availableUsage -= grantedUsage - - // The duration that we want to run is that of the shortest request from a vCPU - duration = min(duration, req.burst / grantedUsage) - } - - // XXX We set the minimum duration to 5 minutes here to prevent the rounding issues that are occurring with the FLOPs. - duration = 300.0 - - val totalAllocatedUsage = maxUsage - availableUsage - var totalAllocatedBurst = 0L - availableUsage = totalAllocatedUsage - val serverLoad = totalAllocatedUsage / maxUsage - - // Divide the requests over the available capacity of the pCPUs fairly - for (i in pCPUs) { - val maxCpuUsage = model.cpus[i].frequency - val fraction = maxCpuUsage / maxUsage - val grantedUsage = min(maxCpuUsage, totalAllocatedUsage * fraction) - val grantedBurst = ceil(duration * grantedUsage).toLong() - - usage[i] = grantedUsage - burst[i] = grantedBurst - totalAllocatedBurst += grantedBurst - availableUsage -= grantedUsage - } - - // We run the total burst on the host processor. Note that this call may be cancelled at any moment in - // time, so not all of the burst may be executed. - select<Boolean> { - schedulingQueue.onReceive { schedulingQueue.offer(it); true } - ctx.onRun(SimExecutionContext.Slice(burst, usage, deadline), SimExecutionContext.TriggerMode.DEADLINE) - .invoke { false } - } - - val end = clock.millis() - - // No work was performed - if ((end - start) <= 0) { - continue - } - - // The total requested burst that the VMs wanted to run in the time-frame that we ran. - val totalRequestedSubBurst = - vcpus.map { ceil((duration * 1000) / (it.vm.deadline - start) * it.burst).toLong() }.sum() - val totalRemainder = burst.sum() - val totalGrantedBurst = totalAllocatedBurst - totalRemainder - - // The burst that was lost due to overcommissioning of CPU resources - var totalOvercommissionedBurst = 0L - // The burst that was lost due to interference. - var totalInterferedBurst = 0L - - val vmIterator = vms.iterator() - while (vmIterator.hasNext()) { - val vm = vmIterator.next() - - // Apply performance interference model - val performanceScore = vm.performanceInterferenceModel?.apply(serverLoad) ?: 1.0 - var hasFinished = false - - for (vcpu in vm.vcpus) { - // Compute the fraction of compute time allocated to the VM - val fraction = vcpu.allocatedLimit / totalAllocatedUsage - - // Compute the burst time that the VM was actually granted - val grantedBurst = ceil(totalGrantedBurst * fraction).toLong() - - // The burst that was actually used by the VM - val usedBurst = ceil(grantedBurst * performanceScore).toLong() - - totalInterferedBurst += grantedBurst - usedBurst - - // Compute remaining burst time to be executed for the request - if (vcpu.consume(usedBurst)) { - hasFinished = true - } else if (vm.deadline <= end) { - // Request must have its entire burst consumed or otherwise we have overcommission - // Note that we count the overcommissioned burst if the hypervisor has failed. - totalOvercommissionedBurst += vcpu.burst - } - } - - if (hasFinished || vm.deadline <= end) { - // Mark the VM as finished and deschedule the VMs if needed - if (vm.finish()) { - vmIterator.remove() - vcpus.removeAll(vm.vcpus) - } - } - } - - listener?.onSliceFinish( - this, - totalRequestedBurst, - min(totalRequestedSubBurst, totalGrantedBurst), // We can run more than requested due to timing - totalOvercommissionedBurst, - totalInterferedBurst, // Might be smaller than zero due to FP rounding errors, - min( - totalAllocatedUsage, - totalRequestedUsage - ), // The allocated usage might be slightly higher due to FP rounding - totalRequestedUsage - ) - } - } + public fun createMachine( + model: SimMachineModel, + performanceInterferenceModel: PerformanceInterferenceModel? = null + ): SimMachine /** * Event listener for hypervisor events. @@ -293,243 +55,12 @@ public class SimHypervisor( */ public fun onSliceFinish( hypervisor: SimHypervisor, - requestedBurst: Long, - grantedBurst: Long, - overcommissionedBurst: Long, - interferedBurst: Long, + requestedWork: Long, + grantedWork: Long, + overcommittedWork: Long, + interferedWork: Long, cpuUsage: Double, cpuDemand: Double ) } - - /** - * A scheduling command processed by the scheduler. - */ - private sealed class SchedulerCommand { - /** - * Schedule the specified VM on the hypervisor. - */ - data class Schedule(val vm: VmSession) : SchedulerCommand() - - /** - * De-schedule the specified VM on the hypervisor. - */ - data class Deschedule(val vm: VmSession) : SchedulerCommand() - - /** - * Interrupt the scheduler. - */ - object Interrupt : SchedulerCommand() - } - - /** - * A virtual machine running on the hypervisor. - * - * @param ctx The execution context the vCPU runs in. - * @param triggerMode The mode when to trigger the VM exit. - * @param merge The function to merge consecutive slices on spillover. - * @param select The function to select on finish. - */ - @OptIn(InternalCoroutinesApi::class) - private data class VmSession( - val model: SimMachineModel, - val performanceInterferenceModel: PerformanceInterferenceModel? = null, - var triggerMode: SimExecutionContext.TriggerMode = SimExecutionContext.TriggerMode.FIRST, - var merge: (SimExecutionContext.Slice, SimExecutionContext.Slice) -> SimExecutionContext.Slice = { _, r -> r }, - var select: () -> Unit = {} - ) { - /** - * The vCPUs of this virtual machine. - */ - val vcpus: List<VCpu> - - /** - * The slices that the VM wants to run. - */ - var queue: Iterator<SimExecutionContext.Slice> = emptyList<SimExecutionContext.Slice>().iterator() - - /** - * The current active slice. - */ - var activeSlice: SimExecutionContext.Slice? = null - - /** - * The current deadline of the VM. - */ - val deadline: Long - get() = activeSlice?.deadline ?: Long.MAX_VALUE - - /** - * A flag to indicate that the VM is idle. - */ - val isIdle: Boolean - get() = activeSlice == null - - /** - * The usage of the virtual machine. - */ - val usage: MutableStateFlow<Double> = MutableStateFlow(0.0) - - init { - vcpus = model.cpus.mapIndexed { i, model -> VCpu(this, model, i) } - } - - /** - * Schedule the given slices on this vCPU, replacing the existing slices. - */ - fun schedule(slices: Sequence<SimExecutionContext.Slice>) { - queue = slices.iterator() - - if (queue.hasNext()) { - activeSlice = queue.next() - refresh() - } - } - - /** - * Cancel the existing workload on the VM. - */ - fun cancel() { - queue = emptyList<SimExecutionContext.Slice>().iterator() - activeSlice = null - refresh() - } - - /** - * Finish the current slice of the VM. - * - * @return `true` if the vCPUs may be descheduled, `false` otherwise. - */ - fun finish(): Boolean { - val activeSlice = activeSlice ?: return true - - return if (queue.hasNext()) { - val needsMerge = activeSlice.burst.any { it > 0 } - val candidateSlice = queue.next() - val slice = if (needsMerge) merge(activeSlice, candidateSlice) else candidateSlice - - this.activeSlice = slice - - // Update the vCPU cache - refresh() - - false - } else { - this.activeSlice = null - select() - true - } - } - - /** - * Refresh the vCPU cache. - */ - fun refresh() { - vcpus.forEach { it.refresh() } - usage.value = vcpus.sumByDouble { it.burst / it.limit } / vcpus.size - } - } - - /** - * A virtual CPU that can be scheduled on a physical CPU. - * - * @param vm The VM of which this vCPU is part. - * @param model The model of CPU that this vCPU models. - * @param id The id of the vCPU with respect to the VM. - */ - private data class VCpu( - val vm: VmSession, - val model: ProcessingUnit, - val id: Int - ) : Comparable<VCpu> { - /** - * The current limit on the vCPU. - */ - var limit: Double = 0.0 - - /** - * The limit allocated by the hypervisor. - */ - var allocatedLimit: Double = 0.0 - - /** - * The current burst running on the vCPU. - */ - var burst: Long = 0L - - /** - * Consume the specified burst on this vCPU. - */ - fun consume(burst: Long): Boolean { - this.burst = max(0, this.burst - burst) - - // Flush the result to the slice if it exists - vm.activeSlice?.burst?.takeIf { id < it.size }?.set(id, this.burst) - - return allocatedLimit > 0.0 && this.burst == 0L - } - - /** - * Refresh the information of this vCPU based on the current slice. - */ - fun refresh() { - limit = vm.activeSlice?.limit?.takeIf { id < it.size }?.get(id) ?: 0.0 - burst = vm.activeSlice?.burst?.takeIf { id < it.size }?.get(id) ?: 0 - } - - /** - * Compare to another vCPU based on the current load of the vCPU. - */ - override fun compareTo(other: VCpu): Int { - return limit.compareTo(other.limit) - } - - /** - * Create a string representation of the vCPU. - */ - override fun toString(): String = - "vCPU(id=$id,burst=$burst,limit=$limit,allocatedLimit=$allocatedLimit)" - } - - /** - * The execution context in which a VM runs. - * - */ - private inner class VmExecutionContext(val session: VmSession) : - SimExecutionContext, DisposableHandle { - override val machine: SimMachineModel - get() = session.model - - override val clock: Clock - get() = this@SimHypervisor.clock - - @OptIn(InternalCoroutinesApi::class) - override fun onRun( - batch: Sequence<SimExecutionContext.Slice>, - triggerMode: SimExecutionContext.TriggerMode, - merge: (SimExecutionContext.Slice, SimExecutionContext.Slice) -> SimExecutionContext.Slice - ): SelectClause0 = object : SelectClause0 { - @InternalCoroutinesApi - override fun <R> registerSelectClause0(select: SelectInstance<R>, block: suspend () -> R) { - session.triggerMode = triggerMode - session.merge = merge - session.select = { - if (select.trySelect()) { - block.startCoroutineCancellable(select.completion) - } - } - session.schedule(batch) - // Indicate to the hypervisor that the VM should be re-scheduled - schedulingQueue.offer(SchedulerCommand.Schedule(session)) - select.disposeOnSelect(this@VmExecutionContext) - } - } - - override fun dispose() { - if (!session.isIdle) { - session.cancel() - schedulingQueue.offer(SchedulerCommand.Deschedule(session)) - } - } - } } diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimHypervisorProvider.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimHypervisorProvider.kt new file mode 100644 index 00000000..a5b4526b --- /dev/null +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimHypervisorProvider.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute + +/** + * A service provider interface for constructing a [SimHypervisor]. + */ +public interface SimHypervisorProvider { + /** + * A unique identifier for this hypervisor implementation. + * + * Each hypervisor must provide a unique ID, so that they can be selected by the user. + * When in doubt, you may use the fully qualified name of your custom [SimHypervisor] implementation class. + */ + public val id: String + + /** + * Create a [SimHypervisor] instance with the specified [listener]. + */ + public fun create(listener: SimHypervisor.Listener? = null): SimHypervisor +} diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachine.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachine.kt index f66085af..ea8eeb37 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachine.kt +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachine.kt @@ -22,15 +22,13 @@ package org.opendc.simulator.compute -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.StateFlow import org.opendc.simulator.compute.workload.SimWorkload /** * A generic machine that is able to run a [SimWorkload]. */ -@OptIn(ExperimentalCoroutinesApi::class) -public interface SimMachine { +public interface SimMachine : AutoCloseable { /** * The model of the machine containing its specifications. */ @@ -45,4 +43,9 @@ public interface SimMachine { * Run the specified [SimWorkload] on this machine and suspend execution util the workload has finished. */ public suspend fun run(workload: SimWorkload) + + /** + * Terminate this machine. + */ + public override fun close() } diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimSpaceSharedHypervisor.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimSpaceSharedHypervisor.kt new file mode 100644 index 00000000..66d3eda7 --- /dev/null +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimSpaceSharedHypervisor.kt @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import org.opendc.simulator.compute.interference.PerformanceInterferenceModel +import org.opendc.simulator.compute.model.ProcessingUnit +import org.opendc.simulator.compute.workload.SimResourceCommand +import org.opendc.simulator.compute.workload.SimWorkload +import java.time.Clock +import java.util.ArrayDeque +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.math.min + +/** + * A [SimHypervisor] that allocates its sub-resources exclusively for the virtual machine that it hosts. + * + * @param listener The hypervisor listener to use. + */ +public class SimSpaceSharedHypervisor(private val listener: SimHypervisor.Listener? = null) : SimHypervisor { + /** + * The execution context in which the hypervisor runs. + */ + private lateinit var ctx: SimExecutionContext + + /** + * The mapping from pCPU to vCPU. + */ + private lateinit var vcpus: Array<VCpu?> + + /** + * The available physical CPUs to schedule on. + */ + private val availableCpus = ArrayDeque<Int>() + + override fun canFit(model: SimMachineModel): Boolean = availableCpus.size >= model.cpus.size + + override fun createMachine( + model: SimMachineModel, + performanceInterferenceModel: PerformanceInterferenceModel? + ): SimMachine { + require(canFit(model)) { "Cannot fit machine" } + return SimVm(model, performanceInterferenceModel) + } + + override fun onStart(ctx: SimExecutionContext) { + this.ctx = ctx + this.vcpus = arrayOfNulls(ctx.machine.cpus.size) + this.availableCpus.addAll(ctx.machine.cpus.indices) + } + + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + return onNext(ctx, cpu, 0.0) + } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + return vcpus[cpu]?.next(0.0) ?: SimResourceCommand.Idle() + } + + /** + * A virtual machine running on the hypervisor. + * + * @property model The machine model of the virtual machine. + * @property performanceInterferenceModel The performance interference model to utilize. + */ + private inner class SimVm( + override val model: SimMachineModel, + val performanceInterferenceModel: PerformanceInterferenceModel? = null, + ) : SimMachine { + /** + * A flag to indicate that the machine is terminated. + */ + private var isTerminated = false + + /** + * A [StateFlow] representing the CPU usage of the simulated machine. + */ + override val usage: MutableStateFlow<Double> = MutableStateFlow(0.0) + + /** + * The current active workload. + */ + private var cont: Continuation<Unit>? = null + + /** + * The physical CPUs that have been allocated. + */ + private val pCPUs = model.cpus.map { availableCpus.poll() }.toIntArray() + + /** + * The active CPUs of this virtual machine. + */ + private var cpus: List<VCpu> = emptyList() + + /** + * The execution context in which the workload runs. + */ + val ctx = object : SimExecutionContext { + override val machine: SimMachineModel + get() = model + + override val clock: Clock + get() = this@SimSpaceSharedHypervisor.ctx.clock + + override fun interrupt(cpu: Int) { + require(cpu < cpus.size) { "Invalid CPU identifier" } + cpus[cpu].interrupt() + } + } + + /** + * Run the specified [SimWorkload] on this machine and suspend execution util the workload has finished. + */ + override suspend fun run(workload: SimWorkload) { + require(!isTerminated) { "Machine is terminated" } + require(cont == null) { "Run should not be called concurrently" } + + workload.onStart(ctx) + + return suspendCancellableCoroutine { cont -> + this.cont = cont + this.cpus = model.cpus.mapIndexed { index, model -> VCpu(this, model, workload, pCPUs[index]) } + + for (cpu in cpus) { + cpu.start() + } + } + } + + override fun close() { + isTerminated = true + for (pCPU in pCPUs) { + vcpus[pCPU] = null + availableCpus.add(pCPU) + } + } + + /** + * Update the usage of the VM. + */ + fun updateUsage() { + usage.value = cpus.sumByDouble { it.speed } / cpus.sumByDouble { it.model.frequency } + } + + /** + * This method is invoked when one of the CPUs has exited. + */ + fun onCpuExit(cpu: Int) { + // Check whether all other CPUs have finished + if (cpus.all { it.hasExited }) { + val cont = cont + this.cont = null + cont?.resume(Unit) + } + } + + /** + * This method is invoked when one of the CPUs failed. + */ + fun onCpuFailure(e: Throwable) { + // In case the flush fails with an exception, immediately propagate to caller, cancelling all other + // tasks. + val cont = cont + this.cont = null + cont?.resumeWithException(e) + } + } + + /** + * A CPU of the virtual machine. + */ + private inner class VCpu(val vm: SimVm, val model: ProcessingUnit, val workload: SimWorkload, val pCPU: Int) { + /** + * The processing speed of the vCPU. + */ + var speed: Double = 0.0 + set(value) { + field = value + vm.updateUsage() + } + + /** + * A flag to indicate that the CPU has exited. + */ + var hasExited: Boolean = false + + /** + * A flag to indicate that the CPU was started. + */ + var hasStarted: Boolean = false + + /** + * Process the specified [SimResourceCommand] for this CPU. + */ + fun process(command: SimResourceCommand): SimResourceCommand { + return when (command) { + is SimResourceCommand.Idle -> { + speed = 0.0 + command + } + is SimResourceCommand.Consume -> { + speed = min(model.frequency, command.limit) + command + } + is SimResourceCommand.Exit -> { + speed = 0.0 + hasExited = true + + vm.onCpuExit(model.id) + + SimResourceCommand.Idle() + } + } + } + + /** + * Start the CPU. + */ + fun start() { + vcpus[pCPU] = this + interrupt() + } + + /** + * Request the workload for more work. + */ + fun next(remainingWork: Double): SimResourceCommand { + return try { + val command = + if (hasStarted) { + workload.onNext(ctx, model.id, remainingWork) + } else { + hasStarted = true + workload.onStart(ctx, model.id) + } + process(command) + } catch (e: Throwable) { + fail(e) + } + } + + /** + * Interrupt the CPU. + */ + fun interrupt() { + ctx.interrupt(pCPU) + } + + /** + * Fail the CPU. + */ + fun fail(e: Throwable): SimResourceCommand { + hasExited = true + + vm.onCpuFailure(e) + + return SimResourceCommand.Idle() + } + } +} diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimSpaceSharedHypervisorProvider.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimSpaceSharedHypervisorProvider.kt new file mode 100644 index 00000000..3d49e544 --- /dev/null +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimSpaceSharedHypervisorProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute + +/** + * A [SimHypervisorProvider] for the [SimSpaceSharedHypervisor] implementation. + */ +public class SimSpaceSharedHypervisorProvider : SimHypervisorProvider { + override val id: String = "space-shared" + + override fun create(listener: SimHypervisor.Listener?): SimHypervisor = SimSpaceSharedHypervisor(listener) +} diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkload.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkload.kt index 918a78bd..c22fcc07 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkload.kt +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkload.kt @@ -23,35 +23,46 @@ package org.opendc.simulator.compute.workload import org.opendc.simulator.compute.SimExecutionContext -import kotlin.math.min /** - * A [SimWorkload] that models applications performing a static number of floating point operations ([flops]) on - * a compute resource. + * A [SimWorkload] that models applications as a static number of floating point operations ([flops]) executed on + * multiple cores of a compute resource. * * @property flops The number of floating point operations to perform for this task in MFLOPs. - * @property cores The number of cores that the image is able to utilize. * @property utilization A model of the CPU utilization of the application. */ public class SimFlopsWorkload( public val flops: Long, - public val cores: Int, public val utilization: Double = 0.8 ) : SimWorkload { init { require(flops >= 0) { "Negative number of flops" } - require(cores > 0) { "Negative number of cores or no cores" } require(utilization > 0.0 && utilization <= 1.0) { "Utilization must be in (0, 1]" } } - /** - * Execute the runtime behavior based on a number of floating point operations to execute. - */ - override suspend fun run(ctx: SimExecutionContext) { - val cores = min(this.cores, ctx.machine.cpus.size) - val burst = LongArray(cores) { flops / cores } - val maxUsage = DoubleArray(cores) { i -> ctx.machine.cpus[i].frequency * utilization } + override fun onStart(ctx: SimExecutionContext) {} - ctx.run(SimExecutionContext.Slice(burst, maxUsage, Long.MAX_VALUE), triggerMode = SimExecutionContext.TriggerMode.LAST) + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + val cores = ctx.machine.cpus.size + val limit = ctx.machine.cpus[cpu].frequency * utilization + val work = flops.toDouble() / cores + + return if (work > 0.0) { + SimResourceCommand.Consume(work, limit) + } else { + SimResourceCommand.Exit + } } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + return if (remainingWork > 0.0) { + val limit = ctx.machine.cpus[cpu].frequency * utilization + + return SimResourceCommand.Consume(remainingWork, limit) + } else { + SimResourceCommand.Exit + } + } + + override fun toString(): String = "SimFlopsWorkload(FLOPs=$flops,utilization=$utilization)" } diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimResourceCommand.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimResourceCommand.kt new file mode 100644 index 00000000..41a5028e --- /dev/null +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimResourceCommand.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute.workload + +/** + * A command that is sent to the host machine. + */ +public sealed class SimResourceCommand { + /** + * A request to the host to process the specified amount of [work] on a vCPU before the specified [deadline]. + * + * @param work The amount of work to process on the CPU. + * @param limit The maximum amount of work to be processed per second. + * @param deadline The instant at which the work needs to be fulfilled. + */ + public data class Consume(val work: Double, val limit: Double, val deadline: Long = Long.MAX_VALUE) : SimResourceCommand() { + init { + require(work > 0) { "The amount of work must be positive." } + require(limit > 0) { "Limit must be positive." } + } + } + + /** + * An indication to the host that the vCPU will idle until the specified [deadline] or is interrupted. + */ + public data class Idle(val deadline: Long = Long.MAX_VALUE) : SimResourceCommand() + + /** + * An indication to the host that the vCPU has finished processing. + */ + public object Exit : SimResourceCommand() +} diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimRuntimeWorkload.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimRuntimeWorkload.kt new file mode 100644 index 00000000..00ebebce --- /dev/null +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimRuntimeWorkload.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute.workload + +import org.opendc.simulator.compute.SimExecutionContext + +/** + * A [SimWorkload] that models application execution as a single duration. + * + * @property duration The duration of the workload. + * @property utilization The utilization of the application during runtime. + */ +public class SimRuntimeWorkload( + public val duration: Long, + public val utilization: Double = 0.8 +) : SimWorkload { + init { + require(duration >= 0) { "Duration must be non-negative" } + require(utilization > 0.0 && utilization <= 1.0) { "Utilization must be in (0, 1]" } + } + + override fun onStart(ctx: SimExecutionContext) {} + + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + val limit = ctx.machine.cpus[cpu].frequency * utilization + val work = (limit / 1000) * duration + return SimResourceCommand.Consume(work, limit) + } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + return if (remainingWork > 0.0) { + val limit = ctx.machine.cpus[cpu].frequency * utilization + SimResourceCommand.Consume(remainingWork, limit) + } else { + SimResourceCommand.Exit + } + } + + override fun toString(): String = "SimRuntimeWorkload(duration=$duration,utilization=$utilization)" +} diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt index 7b1ddf32..deb10b98 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt @@ -23,31 +23,64 @@ package org.opendc.simulator.compute.workload import org.opendc.simulator.compute.SimExecutionContext -import kotlin.math.min /** * A [SimWorkload] that replays a workload trace consisting of multiple fragments, each indicating the resource * consumption for some period of time. */ public class SimTraceWorkload(public val trace: Sequence<Fragment>) : SimWorkload { - override suspend fun run(ctx: SimExecutionContext) { - var offset = ctx.clock.millis() - - val batch = trace.map { fragment -> - val cores = min(fragment.cores, ctx.machine.cpus.size) - val burst = LongArray(cores) { fragment.flops / cores } - val usage = DoubleArray(cores) { fragment.usage / cores } - offset += fragment.duration - SimExecutionContext.Slice(burst, usage, offset) + private var offset = 0L + private val iterator = trace.iterator() + private var fragment: Fragment? = null + private lateinit var barrier: SimWorkloadBarrier + + override fun onStart(ctx: SimExecutionContext) { + barrier = SimWorkloadBarrier(ctx.machine.cpus.size) + fragment = nextFragment() + offset = ctx.clock.millis() + } + + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + return onNext(ctx, cpu, 0.0) + } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + val now = ctx.clock.millis() + val fragment = fragment ?: return SimResourceCommand.Exit + val work = (fragment.duration / 1000) * fragment.usage + val deadline = offset + fragment.duration + + assert(deadline >= now) { "Deadline already passed" } + + val cmd = + if (cpu < fragment.cores && work > 0.0) + SimResourceCommand.Consume(work, fragment.usage, deadline) + else + SimResourceCommand.Idle(deadline) + + if (barrier.enter()) { + this.fragment = nextFragment() + this.offset += fragment.duration } - ctx.run(batch) + return cmd } override fun toString(): String = "SimTraceWorkload" /** + * Obtain the next fragment. + */ + private fun nextFragment(): Fragment? { + return if (iterator.hasNext()) { + iterator.next() + } else { + null + } + } + + /** * A fragment of the workload. */ - public data class Fragment(val time: Long, val flops: Long, val duration: Long, val usage: Double, val cores: Int) + public data class Fragment(val duration: Long, val usage: Double, val cores: Int) } diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkload.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkload.kt index 2add8cce..6fc78d56 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkload.kt +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkload.kt @@ -28,14 +28,31 @@ import org.opendc.simulator.compute.SimExecutionContext * A model that characterizes the runtime behavior of some particular workload. * * Workloads are stateful objects that may be paused and resumed at a later moment. As such, be careful when using the - * same [SimWorkload] from multiple contexts as only a single concurrent [run] call is expected. + * same [SimWorkload] from multiple contexts. */ public interface SimWorkload { /** - * Launch the workload in the specified [SimExecutionContext]. + * This method is invoked when the workload is started, before the (virtual) CPUs assigned to the workload will + * start. + */ + public fun onStart(ctx: SimExecutionContext) + + /** + * This method is invoked when a (virtual) CPU assigned to the workload has started. + * + * @param ctx The execution context in which the workload runs. + * @param cpu The index of the (virtual) CPU to start. + * @return The command to perform on the CPU. + */ + public fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand + + /** + * This method is invoked when a (virtual) CPU assigned to the workload was interrupted or reached its deadline. * - * This method should encapsulate and characterize the runtime behavior of the instance resulting from launching - * the workload on some machine, in terms of the resource consumption on the machine. + * @param ctx The execution context in which the workload runs. + * @param cpu The index of the (virtual) CPU to obtain the resource consumption of. + * @param remainingWork The remaining work that was not yet completed. + * @return The next command to perform on the CPU. */ - public suspend fun run(ctx: SimExecutionContext) + public fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand } diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadBarrier.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadBarrier.kt new file mode 100644 index 00000000..45a299be --- /dev/null +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadBarrier.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute.workload + +/** + * The [SimWorkloadBarrier] is a barrier that allows workloads to wait for a select number of CPUs to complete, before + * proceeding its operation. + */ +public class SimWorkloadBarrier(public val parties: Int) { + private var counter = 0 + + /** + * Enter the barrier and determine whether the caller is the last to reach the barrier. + * + * @return `true` if the caller is the last to reach the barrier, `false` otherwise. + */ + public fun enter(): Boolean { + val last = ++counter == parties + if (last) { + counter = 0 + return true + } + return false + } +} diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimHypervisorTest.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimHypervisorTest.kt index 78bd2940..b8eee4f0 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimHypervisorTest.kt +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimHypervisorTest.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.yield -import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll @@ -51,7 +51,7 @@ internal class SimHypervisorTest { scope = TestCoroutineScope() clock = DelayControllerClockAdapter(scope) - val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 2) + val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 1) machineModel = SimMachineModel( cpus = List(cpuNode.coreCount) { ProcessingUnit(cpuNode, it, 3200.0) }, memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } @@ -59,27 +59,27 @@ internal class SimHypervisorTest { } /** - * Test overcommissioning of a hypervisor. + * Test overcommitting of resources via the hypervisor with a single VM. */ @Test - fun overcommission() { + fun testOvercommittedSingle() { val listener = object : SimHypervisor.Listener { - var totalRequestedBurst = 0L - var totalGrantedBurst = 0L - var totalOvercommissionedBurst = 0L + var totalRequestedWork = 0L + var totalGrantedWork = 0L + var totalOvercommittedWork = 0L override fun onSliceFinish( hypervisor: SimHypervisor, - requestedBurst: Long, - grantedBurst: Long, - overcommissionedBurst: Long, - interferedBurst: Long, + requestedWork: Long, + grantedWork: Long, + overcommittedWork: Long, + interferedWork: Long, cpuUsage: Double, cpuDemand: Double ) { - totalRequestedBurst += requestedBurst - totalGrantedBurst += grantedBurst - totalOvercommissionedBurst += overcommissionedBurst + totalRequestedWork += requestedWork + totalGrantedWork += grantedWork + totalOvercommittedWork += overcommittedWork } } @@ -88,24 +88,84 @@ internal class SimHypervisorTest { val workloadA = SimTraceWorkload( sequenceOf( - SimTraceWorkload.Fragment(0, 28L * duration, duration * 1000, 28.0, 2), - SimTraceWorkload.Fragment(0, 3500L * duration, duration * 1000, 3500.0, 2), - SimTraceWorkload.Fragment(0, 0, duration * 1000, 0.0, 2), - SimTraceWorkload.Fragment(0, 183L * duration, duration * 1000, 183.0, 2) + SimTraceWorkload.Fragment(duration * 1000, 28.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 3500.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 0.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 183.0, 1) + ), + ) + + val machine = SimBareMetalMachine(scope, clock, machineModel) + val hypervisor = SimFairShareHypervisor(listener) + + launch { + machine.run(hypervisor) + } + + yield() + launch { hypervisor.createMachine(machineModel).run(workloadA) } + } + + scope.advanceUntilIdle() + scope.uncaughtExceptions.forEach { it.printStackTrace() } + + assertAll( + { assertEquals(emptyList<Throwable>(), scope.uncaughtExceptions, "No errors") }, + { assertEquals(1113300, listener.totalRequestedWork, "Requested Burst does not match") }, + { assertEquals(1023300, listener.totalGrantedWork, "Granted Burst does not match") }, + { assertEquals(90000, listener.totalOvercommittedWork, "Overcommissioned Burst does not match") }, + { assertEquals(1200000, scope.currentTime) } + ) + } + + /** + * Test overcommitting of resources via the hypervisor with two VMs. + */ + @Test + fun testOvercommittedDual() { + val listener = object : SimHypervisor.Listener { + var totalRequestedWork = 0L + var totalGrantedWork = 0L + var totalOvercommittedWork = 0L + + override fun onSliceFinish( + hypervisor: SimHypervisor, + requestedWork: Long, + grantedWork: Long, + overcommittedWork: Long, + interferedWork: Long, + cpuUsage: Double, + cpuDemand: Double + ) { + totalRequestedWork += requestedWork + totalGrantedWork += grantedWork + totalOvercommittedWork += overcommittedWork + } + } + + scope.launch { + val duration = 5 * 60L + val workloadA = + SimTraceWorkload( + sequenceOf( + SimTraceWorkload.Fragment(duration * 1000, 28.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 3500.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 0.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 183.0, 1) ), ) val workloadB = SimTraceWorkload( sequenceOf( - SimTraceWorkload.Fragment(0, 28L * duration, duration * 1000, 28.0, 2), - SimTraceWorkload.Fragment(0, 3100L * duration, duration * 1000, 3100.0, 2), - SimTraceWorkload.Fragment(0, 0, duration * 1000, 0.0, 2), - SimTraceWorkload.Fragment(0, 73L * duration, duration * 1000, 73.0, 2) + SimTraceWorkload.Fragment(duration * 1000, 28.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 3100.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 0.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 73.0, 1) ) ) val machine = SimBareMetalMachine(scope, clock, machineModel) - val hypervisor = SimHypervisor(scope, clock, listener) + val hypervisor = SimFairShareHypervisor(listener) launch { machine.run(hypervisor) @@ -117,13 +177,14 @@ internal class SimHypervisorTest { } scope.advanceUntilIdle() + scope.uncaughtExceptions.forEach { it.printStackTrace() } assertAll( - { Assertions.assertEquals(emptyList<Throwable>(), scope.uncaughtExceptions, "No errors") }, - { Assertions.assertEquals(2073600, listener.totalRequestedBurst, "Requested Burst does not match") }, - { Assertions.assertEquals(2013600, listener.totalGrantedBurst, "Granted Burst does not match") }, - { Assertions.assertEquals(60000, listener.totalOvercommissionedBurst, "Overcommissioned Burst does not match") }, - { Assertions.assertEquals(1200001, scope.currentTime) } + { assertEquals(emptyList<Throwable>(), scope.uncaughtExceptions, "No errors") }, + { assertEquals(2082000, listener.totalRequestedWork, "Requested Burst does not match") }, + { assertEquals(1062000, listener.totalGrantedWork, "Granted Burst does not match") }, + { assertEquals(1020000, listener.totalOvercommittedWork, "Overcommissioned Burst does not match") }, + { assertEquals(1200000, scope.currentTime) } ) } } diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt index 332ca8e9..1036f1ac 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt @@ -23,15 +23,20 @@ package org.opendc.simulator.compute import kotlinx.coroutines.* +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows import org.opendc.simulator.compute.model.MemoryUnit import org.opendc.simulator.compute.model.ProcessingNode import org.opendc.simulator.compute.model.ProcessingUnit import org.opendc.simulator.compute.workload.SimFlopsWorkload +import org.opendc.simulator.compute.workload.SimResourceCommand +import org.opendc.simulator.compute.workload.SimWorkload import org.opendc.simulator.utils.DelayControllerClockAdapter /** @@ -58,7 +63,7 @@ class SimMachineTest { val machine = SimBareMetalMachine(testScope, clock, machineModel) testScope.runBlockingTest { - machine.run(SimFlopsWorkload(2_000, 2, utilization = 1.0)) + machine.run(SimFlopsWorkload(2_000, utilization = 1.0)) // Two cores execute 1000 MFlOps per second (1000 ms) assertEquals(1000, testScope.currentTime) @@ -72,12 +77,83 @@ class SimMachineTest { val machine = SimBareMetalMachine(testScope, clock, machineModel) testScope.runBlockingTest { - machine.run(SimFlopsWorkload(2_000, 2, utilization = 1.0)) - assertEquals(1.0, machine.usage.value) + val res = mutableListOf<Double>() + val job = launch { machine.usage.toList(res) } - // Wait for the usage to reset - delay(1) - assertEquals(0.0, machine.usage.value) + machine.run(SimFlopsWorkload(2_000, utilization = 1.0)) + + job.cancel() + assertEquals(listOf(0.0, 0.5, 1.0, 0.5, 0.0), res) { "Machine is fully utilized" } + } + } + + @Test + fun testInterrupt() { + val testScope = TestCoroutineScope() + val clock = DelayControllerClockAdapter(testScope) + val machine = SimBareMetalMachine(testScope, clock, machineModel) + + val workload = object : SimWorkload { + override fun onStart(ctx: SimExecutionContext) {} + + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + ctx.interrupt(cpu) + return SimResourceCommand.Exit + } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + throw IllegalStateException() + } + } + + assertDoesNotThrow { + testScope.runBlockingTest { machine.run(workload) } + } + } + + @Test + fun testExceptionPropagationOnStart() { + val testScope = TestCoroutineScope() + val clock = DelayControllerClockAdapter(testScope) + val machine = SimBareMetalMachine(testScope, clock, machineModel) + + val workload = object : SimWorkload { + override fun onStart(ctx: SimExecutionContext) {} + + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + throw IllegalStateException() + } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + throw IllegalStateException() + } + } + + assertThrows<IllegalStateException> { + testScope.runBlockingTest { machine.run(workload) } + } + } + + @Test + fun testExceptionPropagationOnNext() { + val testScope = TestCoroutineScope() + val clock = DelayControllerClockAdapter(testScope) + val machine = SimBareMetalMachine(testScope, clock, machineModel) + + val workload = object : SimWorkload { + override fun onStart(ctx: SimExecutionContext) {} + + override fun onStart(ctx: SimExecutionContext, cpu: Int): SimResourceCommand { + return SimResourceCommand.Consume(1.0, 1.0) + } + + override fun onNext(ctx: SimExecutionContext, cpu: Int, remainingWork: Double): SimResourceCommand { + throw IllegalStateException() + } + } + + assertThrows<IllegalStateException> { + testScope.runBlockingTest { machine.run(workload) } } } } diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimSpaceSharedHypervisorTest.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimSpaceSharedHypervisorTest.kt new file mode 100644 index 00000000..1a9faf11 --- /dev/null +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimSpaceSharedHypervisorTest.kt @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.simulator.compute + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.yield +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.compute.model.MemoryUnit +import org.opendc.simulator.compute.model.ProcessingNode +import org.opendc.simulator.compute.model.ProcessingUnit +import org.opendc.simulator.compute.workload.SimRuntimeWorkload +import org.opendc.simulator.compute.workload.SimTraceWorkload +import org.opendc.simulator.utils.DelayControllerClockAdapter +import java.time.Clock + +/** + * A test suite for the [SimSpaceSharedHypervisor]. + */ +@OptIn(ExperimentalCoroutinesApi::class) +internal class SimSpaceSharedHypervisorTest { + private lateinit var scope: TestCoroutineScope + private lateinit var clock: Clock + private lateinit var machineModel: SimMachineModel + + @BeforeEach + fun setUp() { + scope = TestCoroutineScope() + clock = DelayControllerClockAdapter(scope) + + val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 1) + machineModel = SimMachineModel( + cpus = List(cpuNode.coreCount) { ProcessingUnit(cpuNode, it, 3200.0) }, + memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } + ) + } + + /** + * Test a trace workload. + */ + @Test + fun testTrace() { + val usagePm = mutableListOf<Double>() + val usageVm = mutableListOf<Double>() + + scope.launch { + val duration = 5 * 60L + val workloadA = + SimTraceWorkload( + sequenceOf( + SimTraceWorkload.Fragment(duration * 1000, 28.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 3500.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 0.0, 1), + SimTraceWorkload.Fragment(duration * 1000, 183.0, 1) + ), + ) + + val machine = SimBareMetalMachine(scope, clock, machineModel) + val hypervisor = SimSpaceSharedHypervisor() + + launch { machine.usage.toList(usagePm) } + launch { machine.run(hypervisor) } + + yield() + launch { + val vm = hypervisor.createMachine(machineModel) + launch { vm.usage.toList(usageVm) } + vm.run(workloadA) + } + } + + scope.advanceUntilIdle() + + assertAll( + { assertEquals(listOf(0.0, 0.00875, 1.0, 0.0, 0.0571875, 0.0), usagePm) { "Correct PM usage" } }, + { assertEquals(listOf(0.0, 0.00875, 1.0, 0.0, 0.0571875, 0.0), usageVm) { "Correct VM usage" } }, + { assertEquals(5 * 60L * 4000, scope.currentTime) { "Took enough time" } } + ) + } + + /** + * Test runtime workload on hypervisor. + */ + @Test + fun testRuntimeWorkload() { + val duration = 5 * 60L * 1000 + val workload = SimRuntimeWorkload(duration) + val machine = SimBareMetalMachine(scope, clock, machineModel) + val hypervisor = SimSpaceSharedHypervisor() + + scope.launch { + launch { machine.run(hypervisor) } + + yield() + launch { hypervisor.createMachine(machineModel).run(workload) } + } + + scope.advanceUntilIdle() + + assertEquals(duration, scope.currentTime) { "Took enough time" } + } + + /** + * Test concurrent workloads on the machine. + */ + @Test + fun testConcurrentWorkloadFails() { + val machine = SimBareMetalMachine(scope, clock, machineModel) + val hypervisor = SimSpaceSharedHypervisor() + + scope.launch { + launch { machine.run(hypervisor) } + + yield() + + hypervisor.createMachine(machineModel) + + assertAll( + { assertFalse(hypervisor.canFit(machineModel)) }, + { assertThrows<IllegalStateException> { hypervisor.createMachine(machineModel) } } + ) + } + + scope.advanceUntilIdle() + } + + /** + * Test concurrent workloads on the machine. + */ + @Test + fun testConcurrentWorkloadSucceeds() { + val machine = SimBareMetalMachine(scope, clock, machineModel) + val hypervisor = SimSpaceSharedHypervisor() + + scope.launch { + launch { machine.run(hypervisor) } + + yield() + + hypervisor.createMachine(machineModel).close() + + assertAll( + { assertTrue(hypervisor.canFit(machineModel)) }, + { assertDoesNotThrow { hypervisor.createMachine(machineModel) } } + ) + } + + scope.advanceUntilIdle() + } +} diff --git a/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkloadTest.kt b/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkloadTest.kt index 51bed76c..b3e57453 100644 --- a/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkloadTest.kt +++ b/simulator/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkloadTest.kt @@ -32,42 +32,28 @@ class SimFlopsWorkloadTest { @Test fun testFlopsNonNegative() { assertThrows<IllegalArgumentException>("FLOPs must be non-negative") { - SimFlopsWorkload(-1, 1) - } - } - - @Test - fun testCoresNonZero() { - assertThrows<IllegalArgumentException>("Cores cannot be zero") { - SimFlopsWorkload(1, 0) - } - } - - @Test - fun testCoresPositive() { - assertThrows<IllegalArgumentException>("Cores cannot be negative") { - SimFlopsWorkload(1, -1) + SimFlopsWorkload(-1) } } @Test fun testUtilizationNonZero() { assertThrows<IllegalArgumentException>("Utilization cannot be zero") { - SimFlopsWorkload(1, 1, 0.0) + SimFlopsWorkload(1, 0.0) } } @Test fun testUtilizationPositive() { assertThrows<IllegalArgumentException>("Utilization cannot be negative") { - SimFlopsWorkload(1, 1, -1.0) + SimFlopsWorkload(1, -1.0) } } @Test fun testUtilizationNotLargerThanOne() { assertThrows<IllegalArgumentException>("Utilization cannot be larger than one") { - SimFlopsWorkload(1, 1, 2.0) + SimFlopsWorkload(1, 2.0) } } } diff --git a/simulator/opendc-trace/build.gradle.kts b/simulator/opendc-trace/build.gradle.kts new file mode 100644 index 00000000..a1a751a2 --- /dev/null +++ b/simulator/opendc-trace/build.gradle.kts @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ diff --git a/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimVirtDriverWorkload.kt b/simulator/opendc-trace/opendc-trace-core/build.gradle.kts index 58b9408a..3db6669a 100644 --- a/simulator/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimVirtDriverWorkload.kt +++ b/simulator/opendc-trace/opendc-trace-core/build.gradle.kts @@ -20,19 +20,13 @@ * SOFTWARE. */ -package org.opendc.compute.simulator +description = "Event tracing library for OpenDC" -import kotlinx.coroutines.coroutineScope -import org.opendc.simulator.compute.SimExecutionContext -import org.opendc.simulator.compute.workload.SimWorkload - -public class SimVirtDriverWorkload : SimWorkload { - public lateinit var driver: SimVirtDriver +/* Build configuration */ +plugins { + `kotlin-library-convention` +} - override suspend fun run(ctx: SimExecutionContext) { - coroutineScope { - driver = SimVirtDriver(this, ctx.clock, ctx) - driver.run() - } - } +dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Library.KOTLINX_COROUTINES}") } diff --git a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/FirstFitResourceSelectionPolicy.kt b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/Event.kt index 8dc323ec..1f4bb267 100644 --- a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/FirstFitResourceSelectionPolicy.kt +++ b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/Event.kt @@ -20,17 +20,15 @@ * SOFTWARE. */ -package org.opendc.workflows.service.stage.resource - -import org.opendc.compute.core.metal.Node -import org.opendc.workflows.service.StageWorkflowService +package org.opendc.trace.core /** - * A [ResourceSelectionPolicy] that selects the first machine that is available. + * Base class for events reported by the OpenDC tracing library. */ -public object FirstFitResourceSelectionPolicy : ResourceSelectionPolicy { - override fun invoke(scheduler: StageWorkflowService): Comparator<Node> = - Comparator<Node> { _, _ -> 1 } - - override fun toString(): String = "First-Fit" +public abstract class Event(timestamp: Long = Long.MIN_VALUE) { + /** + * The timestamp at which the event has occurred. + */ + public var timestamp: Long = timestamp + internal set } diff --git a/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/EventStream.kt b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/EventStream.kt new file mode 100644 index 00000000..ac2b5e9b --- /dev/null +++ b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/EventStream.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.trace.core + +/** + * A stream of [Event]s. + */ +public interface EventStream : AutoCloseable { + /** + * Register the specified [action] to be performed on every event in the stream. + */ + public fun onEvent(action: (Event) -> Unit) + + /** + * Register the specified [action] to be performed on events of type [E]. + */ + public fun <E : Event> onEvent(type: Class<E>, action: (E) -> Unit) + + /** + * Register the specified [action] to be performed on errors. + */ + public fun onError(action: (Throwable) -> Unit) + + /** + * Register the specified [action] to be performed when the stream is closed. + */ + public fun onClose(action: Runnable) + + /** + * Unregister the specified [action]. + * + * @return `true` if an action was unregistered, `false` otherwise. + */ + public fun remove(action: Any): Boolean + + /** + * Start the processing of events in the current coroutine. + * + * @throws IllegalStateException if the stream was already started. + */ + public suspend fun start() + + /** + * Release all resources associated with this stream. + * + * @throws IllegalStateException if the stream was already stopped. + */ + public override fun close() +} + +/** + * Register the specified [action] to be performed on events of type [E]. + */ +public inline fun <reified E : Event> EventStream.onEvent(noinline action: (E) -> Unit) { + onEvent(E::class.java, action) +} diff --git a/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/EventTracer.kt b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/EventTracer.kt new file mode 100644 index 00000000..4f978f4f --- /dev/null +++ b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/EventTracer.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.trace.core + +import org.opendc.trace.core.internal.EventTracerImpl +import java.time.Clock + +/** + * An [EventTracer] is responsible for recording the events that occur in a system. + */ +public interface EventTracer : AutoCloseable { + /** + * The [Clock] used to measure the timestamp and duration of the events. + */ + public val clock: Clock + + /** + * Determine whether the specified [Event] class is currently enabled in any of the active recordings. + * + * @return `true` if the event is enabled, `false` otherwise. + */ + public fun isEnabled(type: Class<out Event>): Boolean + + /** + * Commit the specified [event] to the appropriate event streams. + */ + public fun commit(event: Event) + + /** + * Create a new [RecordingStream] which is able to actively capture events emitted to the [EventTracer]. + */ + public fun openRecording(): RecordingStream + + /** + * Terminate the lifecycle of the [EventTracer] and close its associated event streams. + */ + public override fun close() + + public companion object { + /** + * Construct a new [EventTracer] instance. + * + * @param clock The [Clock] used to measure the timestamps. + */ + @JvmName("create") + public operator fun invoke(clock: Clock): EventTracer = EventTracerImpl(clock) + } +} + +/** + * Determine whether the [Event] of type [E] is currently enabled in any of the active recordings. + * + * @return `true` if the event is enabled, `false` otherwise. + */ +public inline fun <reified E : Event> EventTracer.isEnabled(): Boolean = isEnabled(E::class.java) + +/** + * Lazily construct an [Event] of type [E] if it is enabled and commit it to the appropriate event streams. + */ +public inline fun <reified E : Event> EventTracer.commit(block: () -> E) { + if (isEnabled<E>()) { + commit(block()) + } +} diff --git a/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/Extensions.kt b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/Extensions.kt new file mode 100644 index 00000000..84dcc61a --- /dev/null +++ b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/Extensions.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.trace.core + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow + +/** + * Convert an [EventStream] to a [Flow] of [Event]s but do not start collection of the stream. + */ +@OptIn(ExperimentalCoroutinesApi::class) +public fun EventStream.asFlow(): Flow<Event> = callbackFlow { + onEvent { sendBlocking(it) } + onError { cancel(CancellationException("API error", it)) } + onClose { channel.close() } + awaitClose { this@asFlow.close() } +} + +/** + * Convert an [EventStream] to a [Flow] of [Event]s but do not start collection of the stream. + */ +@OptIn(ExperimentalCoroutinesApi::class) +public fun EventStream.consumeAsFlow(): Flow<Event> = callbackFlow { + onEvent { sendBlocking(it) } + onError { cancel(CancellationException("API error", it)) } + start() +} + +/** + * Convert an [EventStream] to a [Flow] of [Event] of type [E] but do not start collection of the stream. + */ +@OptIn(ExperimentalCoroutinesApi::class) +public inline fun <reified E : Event> EventStream.asTypedFlow(): Flow<E> = callbackFlow { + onEvent<E> { sendBlocking(it) } + onError { cancel(CancellationException("API error", it)) } + onClose { channel.close() } + awaitClose { this@asTypedFlow.close() } +} + +/** + * Convert an [EventStream] to a [Flow] of [Event] of type [E] but do not start collection of the stream. + */ +@OptIn(ExperimentalCoroutinesApi::class) +public inline fun <reified E : Event> EventStream.consumeAsTypedFlow(): Flow<E> = callbackFlow { + onEvent<E> { sendBlocking(it) } + onError { cancel(CancellationException("API error", it)) } + start() +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/ExperimentRunner.kt b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/RecordingStream.kt index a59481c0..f49e7c49 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/ExperimentRunner.kt +++ b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/RecordingStream.kt @@ -20,30 +20,33 @@ * SOFTWARE. */ -package org.opendc.experiments.sc20.runner - -import org.opendc.experiments.sc20.runner.execution.ExperimentExecutionListener +package org.opendc.trace.core /** - * An [ExperimentRunner] facilitates discovery and execution of experiments. + * A recording stream that produces events from an [EventTracer]. */ -public interface ExperimentRunner { +public interface RecordingStream : EventStream { /** - * The unique identifier of this runner. + * Enable recording of the specified event [type]. */ - public val id: String + public fun enable(type: Class<out Event>) /** - * The version of this runner. + * Disable recording of the specified event [type] */ - public val version: String? - get() = null + public fun disable(type: Class<out Event>) +} - /** - * Execute the specified experiment represented as [ExperimentDescriptor]. - * - * @param root The experiment to execute. - * @param listener The listener to report events to. - */ - public fun execute(root: ExperimentDescriptor, listener: ExperimentExecutionListener) +/** + * Enable recording of events of type [E]. + */ +public inline fun <reified E : Event> RecordingStream.enable() { + enable(E::class.java) +} + +/** + * Disable recording of events of type [E]. + */ +public inline fun <reified E : Event> RecordingStream.disable() { + enable(E::class.java) } diff --git a/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/AbstractEventStream.kt b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/AbstractEventStream.kt new file mode 100644 index 00000000..fac99664 --- /dev/null +++ b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/AbstractEventStream.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.trace.core.internal + +import kotlinx.coroutines.suspendCancellableCoroutine +import org.opendc.trace.core.Event +import org.opendc.trace.core.EventStream +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume + +/** + * Base implementation of the [EventStream] implementation. + */ +internal abstract class AbstractEventStream : EventStream { + /** + * The state of the stream. + */ + protected var state = StreamState.Pending + + /** + * The event actions to dispatch to. + */ + private val eventActions = mutableListOf<EventDispatcher>() + + /** + * The error actions to use. + */ + private val errorActions = mutableListOf<(Throwable) -> Unit>() + + /** + * The close actions to use. + */ + private val closeActions = mutableListOf<Runnable>() + + /** + * The continuation that is invoked when the stream closes. + */ + private var cont: Continuation<Unit>? = null + + /** + * Dispatch the specified [event] to this stream. + */ + fun dispatch(event: Event) { + val actions = eventActions + + // TODO Opportunity for further optimizations if needed (e.g. dispatch based on event type) + for (action in actions) { + if (!action.accepts(event)) { + continue + } + + try { + action(event) + } catch (e: Exception) { + handleError(e) + } + } + } + + /** + * Handle the specified [throwable] that occurred while dispatching an event. + */ + private fun handleError(throwable: Throwable) { + val actions = errorActions + + // Default exception handler + if (actions.isEmpty()) { + throwable.printStackTrace() + return + } + + for (action in actions) { + action(throwable) + } + } + + override fun onEvent(action: (Event) -> Unit) { + eventActions += EventDispatcher(null, action) + } + + override fun <E : Event> onEvent(type: Class<E>, action: (E) -> Unit) { + @Suppress("UNCHECKED_CAST") // This cast must succeed + eventActions += EventDispatcher(type, action as (Event) -> Unit) + } + + override fun onError(action: (Throwable) -> Unit) { + errorActions += action + } + + override fun onClose(action: Runnable) { + closeActions += action + } + + override fun remove(action: Any): Boolean { + return eventActions.removeIf { it.action == action } || errorActions.remove(action) || closeActions.remove(action) + } + + override suspend fun start() { + check(state == StreamState.Pending) { "Stream has already started/closed" } + + state = StreamState.Started + + return suspendCancellableCoroutine { cont -> this.cont = cont } + } + + override fun close() { + if (state == StreamState.Closed) { + return + } + + state = StreamState.Closed + cont?.resume(Unit) + + val actions = closeActions + for (action in actions) { + action.run() + } + } +} diff --git a/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/Dispatcher.kt b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/Dispatcher.kt new file mode 100644 index 00000000..8b6de75e --- /dev/null +++ b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/Dispatcher.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.trace.core.internal + +import org.opendc.trace.core.Event + +/** + * The [Dispatcher] is responsible for dispatching events onto configured actions. + */ +internal class Dispatcher { + /** + * The event actions to dispatch to. + */ + private val eventActions = mutableListOf<EventDispatcher>() + + /** + * The error actions to use. + */ + private val errorActions = mutableListOf<(Throwable) -> Unit>() + + /** + * Dispatch the specified [event]. + */ + fun dispatch(event: Event) { + val actions = eventActions + + // TODO Opportunity for further optimizations if needed (e.g. dispatch based on event type) + for (action in actions) { + if (!action.accepts(event)) { + continue + } + + try { + action(event) + } catch (e: Exception) { + handleError(e) + } + } + } + + /** + * Handle the specified [throwable] that occurred while dispatching an event. + */ + private fun handleError(throwable: Throwable) { + val actions = errorActions + + // Default exception handler + if (actions.isEmpty()) { + throwable.printStackTrace() + return + } + + for (action in actions) { + action(throwable) + } + } +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ExperimentExecutionContext.kt b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/EventDispatcher.kt index 942eb891..b2a662eb 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/execution/ExperimentExecutionContext.kt +++ b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/EventDispatcher.kt @@ -1,7 +1,5 @@ /* - * MIT License - * - * Copyright (c) 2020 atlarge-research + * Copyright (c) 2020 AtLarge Research * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,24 +20,25 @@ * SOFTWARE. */ -package org.opendc.experiments.sc20.runner.execution +package org.opendc.trace.core.internal + +import org.opendc.trace.core.Event /** - * The execution context of an experiment. + * A dispatcher responsible for conditionally dispatching an event. */ -public interface ExperimentExecutionContext { - /** - * The execution listener to use. - */ - public val listener: ExperimentExecutionListener - +internal class EventDispatcher(val type: Class<out Event>?, val action: (Event) -> Unit) { /** - * The experiment scheduler to use. + * Determine whether this dispatcher accepts the specified event. */ - public val scheduler: ExperimentScheduler + fun accepts(event: Event): Boolean { + return type == null || type.isAssignableFrom(event.javaClass) + } /** - * A cache for objects within a single runner. + * Invoke the specified [event] on this action. */ - public val cache: MutableMap<Any?, Any?> + operator fun invoke(event: Event) { + action(event) + } } diff --git a/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/EventTracerImpl.kt b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/EventTracerImpl.kt new file mode 100644 index 00000000..e85d0779 --- /dev/null +++ b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/EventTracerImpl.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2020 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.trace.core.internal + +import org.opendc.trace.core.Event +import org.opendc.trace.core.EventTracer +import org.opendc.trace.core.RecordingStream +import java.lang.reflect.Modifier +import java.time.Clock +import java.util.* + +/** + * Default implementation of the [EventTracer] interface. + */ +internal class EventTracerImpl(override val clock: Clock) : EventTracer { + /** + * The set of enabled events. + */ + private val enabledEvents = IdentityHashMap<Class<out Event>, MutableList<Stream>>() + + /** + * The event streams created by the tracer. + */ + private val streams = WeakHashMap<Stream, Unit>() + + /** + * A flag to indicate that the stream is closed. + */ + private var isClosed: Boolean = false + + override fun isEnabled(type: Class<out Event>): Boolean = enabledEvents.containsKey(type) + + override fun commit(event: Event) { + val type = event.javaClass + + // Assign timestamp if not set + if (event.timestamp == Long.MIN_VALUE) { + event.timestamp = clock.millis() + } + + if (!isEnabled(type) || isClosed) { + return + } + + val streams = enabledEvents[type] ?: return + for (stream in streams) { + stream.dispatch(event) + } + } + + override fun openRecording(): RecordingStream = Stream() + + override fun close() { + isClosed = true + + val streams = streams + for ((stream, _) in streams) { + stream.close() + } + + enabledEvents.clear() + } + + /** + * Enable the specified [type] for the given [stream]. + */ + private fun enableFor(type: Class<out Event>, stream: Stream) { + val res = enabledEvents.computeIfAbsent(type) { mutableListOf() } + res.add(stream) + } + + /** + * Disable the specified [type] for the given [stream]. + */ + private fun disableFor(type: Class<out Event>, stream: Stream) { + enabledEvents[type]?.remove(stream) + } + + /** + * The [RecordingStream] associated with this [EventTracer] implementation. + */ + private inner class Stream : AbstractEventStream(), RecordingStream { + /** + * The set of enabled events for this stream. + */ + private val enabledEvents = IdentityHashMap<Class<out Event>, Unit>() + + init { + streams[this] = Unit + } + + override fun enable(type: Class<out Event>) { + validateEventClass(type) + + if (enabledEvents.put(type, Unit) == null && state == StreamState.Started) { + enableFor(type, this) + } + } + + override fun disable(type: Class<out Event>) { + validateEventClass(type) + + if (enabledEvents.remove(type) != null && state == StreamState.Started) { + disableFor(type, this) + } + } + + override suspend fun start() { + val enabledEvents = enabledEvents + for ((event, _) in enabledEvents) { + enableFor(event, this) + } + + super.start() + } + + override fun close() { + val enabledEvents = enabledEvents + for ((event, _) in enabledEvents) { + disableFor(event, this) + } + + // Remove this stream from the active streams + streams.remove(this) + + super.close() + } + + /** + * Validate the specified event subclass. + */ + private fun validateEventClass(type: Class<out Event>) { + require(!Modifier.isAbstract(type.modifiers)) { "Abstract event classes are not allowed" } + require(Event::class.java.isAssignableFrom(type)) { "Must be subclass to ${Event::class.qualifiedName}" } + } + } +} diff --git a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/TrialExperimentDescriptor.kt b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/StreamState.kt index abc52997..9f411e0d 100644 --- a/simulator/opendc-experiments/opendc-experiments-sc20/src/main/kotlin/org/opendc/experiments/sc20/runner/TrialExperimentDescriptor.kt +++ b/simulator/opendc-trace/opendc-trace-core/src/main/kotlin/org/opendc/trace/core/internal/StreamState.kt @@ -1,7 +1,5 @@ /* - * MIT License - * - * Copyright (c) 2020 atlarge-research + * Copyright (c) 2020 AtLarge Research * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,11 +20,11 @@ * SOFTWARE. */ -package org.opendc.experiments.sc20.runner +package org.opendc.trace.core.internal /** - * An abstract [ExperimentDescriptor] specifically for trials. + * The state of a [Stream]. */ -public abstract class TrialExperimentDescriptor : ExperimentDescriptor() { - override val type: Type = Type.TRIAL +internal enum class StreamState { + Pending, Started, Closed } diff --git a/simulator/opendc-utils/build.gradle.kts b/simulator/opendc-utils/build.gradle.kts index d66148c4..d4b8c514 100644 --- a/simulator/opendc-utils/build.gradle.kts +++ b/simulator/opendc-utils/build.gradle.kts @@ -29,4 +29,9 @@ plugins { dependencies { api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Library.KOTLINX_COROUTINES}") + + testImplementation(project(":opendc-simulator:opendc-simulator-core")) + testImplementation("org.junit.jupiter:junit-jupiter-api:${Library.JUNIT_JUPITER}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Library.JUNIT_JUPITER}") + testImplementation("org.junit.platform:junit-platform-launcher:${Library.JUNIT_PLATFORM}") } diff --git a/simulator/opendc-utils/src/main/kotlin/org/opendc/utils/TimerScheduler.kt b/simulator/opendc-utils/src/main/kotlin/org/opendc/utils/TimerScheduler.kt new file mode 100644 index 00000000..ff116443 --- /dev/null +++ b/simulator/opendc-utils/src/main/kotlin/org/opendc/utils/TimerScheduler.kt @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.launch +import kotlinx.coroutines.selects.select +import java.time.Clock +import java.util.* +import kotlin.math.max + +/** + * A TimerScheduler facilitates scheduled execution of future tasks. + * + * @property coroutineScope The [CoroutineScope] to run the tasks in. + * @property clock The clock to keep track of the time. + */ +@OptIn(ExperimentalCoroutinesApi::class) +public class TimerScheduler<T>(private val coroutineScope: CoroutineScope, private val clock: Clock) : AutoCloseable { + /** + * A priority queue containing the tasks to be scheduled in the future. + */ + private val queue = PriorityQueue<Timer>() + + /** + * A map that keeps track of the timers. + */ + private val timers = mutableMapOf<T, Timer>() + + /** + * The channel to communicate with the + */ + private val channel = Channel<Long?>(Channel.CONFLATED) + + /** + * The scheduling job. + */ + private val job = coroutineScope.launch { + val queue = queue + var next: Long? = channel.receive() + + while (true) { + next = select { + channel.onReceive { it } + + val delay = next?.let { max(0L, it - clock.millis()) } ?: return@select + + onTimeout(delay) { + while (queue.isNotEmpty()) { + val timer = queue.peek() + val timestamp = clock.millis() + + assert(timer.timestamp >= timestamp) { "Found task in the past" } + + if (timer.timestamp > timestamp && !timer.isCancelled) { + // Schedule a task for the next event to occur. + return@onTimeout timer.timestamp + } + + queue.poll() + + if (!timer.isCancelled) { + timers.remove(timer.key) + timer() + } + } + + null + } + } + } + } + + /** + * Stop the scheduler. + */ + override fun close() { + cancelAll() + job.cancel() + } + + /** + * Cancel a timer with a given key. + * + * If canceling a timer that was already canceled, or key never was used to start + * a timer this operation will do nothing. + * + * @param key The key of the timer to cancel. + */ + public fun cancel(key: T) { + if (!job.isActive) { + return + } + + val timer = timers.remove(key) + + // Mark the timer as cancelled + timer?.isCancelled = true + + // Optimization: check whether we are the head of the queue + if (queue.peek() == timer) { + queue.poll() + + if (queue.isNotEmpty()) { + channel.sendBlocking(queue.peek().timestamp) + } else { + channel.sendBlocking(null) + } + } + } + + /** + * Cancel all timers. + */ + public fun cancelAll() { + queue.clear() + timers.clear() + } + + /** + * Check if a timer with a given key is active. + * + * @param key The key to check if active. + * @return `true` if the timer with the specified [key] is active, `false` otherwise. + */ + public fun isTimerActive(key: T): Boolean = key in timers + + /** + * Start a timer that will invoke the specified [block] after [delay]. + * + * Each timer has a key and if a new timer with same key is started the previous is cancelled. + * + * @param key The key of the timer to start. + * @param delay The delay before invoking the block. + * @param block The block to invoke. + */ + public fun startSingleTimer(key: T, delay: Long, block: () -> Unit) { + startSingleTimerTo(key, clock.millis() + delay, block) + } + + /** + * Start a timer that will invoke the specified [block] at [timestamp]. + * + * Each timer has a key and if a new timer with same key is started the previous is cancelled. + * + * @param key The key of the timer to start. + * @param timestamp The timestamp at which to invoke the block. + * @param block The block to invoke. + */ + public fun startSingleTimerTo(key: T, timestamp: Long, block: () -> Unit) { + val now = clock.millis() + + require(timestamp >= now) { "Timestamp must be in the future" } + check(job.isActive) { "Timer is stopped" } + + val timer = Timer(key, timestamp, block) + + timers.compute(key) { _, old -> + old?.isCancelled = true + timer + } + queue.add(timer) + + // Check if we need to push the interruption forward + if (queue.peek() == timer) { + channel.sendBlocking(timer.timestamp) + } + } + + /** + * A task that is scheduled to run in the future. + */ + private inner class Timer(val key: T, val timestamp: Long, val block: () -> Unit) : Comparable<Timer> { + /** + * A flag to indicate that the task has been cancelled. + */ + var isCancelled: Boolean = false + + /** + * Run the task. + */ + operator fun invoke(): Unit = block() + + override fun compareTo(other: Timer): Int = timestamp.compareTo(other.timestamp) + } +} diff --git a/simulator/opendc-utils/src/test/kotlin/org/opendc/utils/TimerSchedulerTest.kt b/simulator/opendc-utils/src/test/kotlin/org/opendc/utils/TimerSchedulerTest.kt new file mode 100644 index 00000000..3a4acc90 --- /dev/null +++ b/simulator/opendc-utils/src/test/kotlin/org/opendc/utils/TimerSchedulerTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.utils + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.utils.DelayControllerClockAdapter + +/** + * A test suite for the [TimerScheduler] class. + */ +@OptIn(ExperimentalCoroutinesApi::class) +internal class TimerSchedulerTest { + @Test + fun testBasicTimer() { + runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val scheduler = TimerScheduler<Int>(this, clock) + + scheduler.startSingleTimer(0, 1000) { + scheduler.close() + assertEquals(1000, clock.millis()) + } + } + } + + @Test + fun testCancelNonExisting() { + runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val scheduler = TimerScheduler<Int>(this, clock) + + scheduler.cancel(1) + scheduler.close() + } + } + + @Test + fun testCancelExisting() { + runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val scheduler = TimerScheduler<Int>(this, clock) + + scheduler.startSingleTimer(0, 1000) { + assertFalse(false) + } + + scheduler.startSingleTimer(1, 100) { + scheduler.cancel(0) + scheduler.close() + + assertEquals(100, clock.millis()) + } + } + } + + @Test + fun testCancelAll() { + runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val scheduler = TimerScheduler<Int>(this, clock) + + scheduler.startSingleTimer(0, 1000) { + assertFalse(false) + } + + scheduler.startSingleTimer(1, 100) { + assertFalse(false) + } + + scheduler.close() + } + } + + @Test + fun testOverride() { + runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val scheduler = TimerScheduler<Int>(this, clock) + + scheduler.startSingleTimer(0, 1000) { + assertFalse(false) + } + + scheduler.startSingleTimer(0, 200) { + scheduler.close() + + assertEquals(200, clock.millis()) + } + } + } + + @Test + fun testStopped() { + runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val scheduler = TimerScheduler<Int>(this, clock) + + scheduler.close() + + assertThrows<IllegalStateException> { + scheduler.startSingleTimer(1, 100) { + assertFalse(false) + } + } + } + } + + @Test + fun testNegativeDelay() { + runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val scheduler = TimerScheduler<Int>(this, clock) + + assertThrows<IllegalArgumentException> { + scheduler.startSingleTimer(1, -1) { + assertFalse(false) + } + } + + scheduler.close() + } + } +} diff --git a/simulator/opendc-workflows/build.gradle.kts b/simulator/opendc-workflows/build.gradle.kts index f61bdac6..e9c85de5 100644 --- a/simulator/opendc-workflows/build.gradle.kts +++ b/simulator/opendc-workflows/build.gradle.kts @@ -30,14 +30,18 @@ plugins { dependencies { api(project(":opendc-core")) api(project(":opendc-compute:opendc-compute-core")) + api(project(":opendc-trace:opendc-trace-core")) implementation(project(":opendc-utils")) + implementation("io.github.microutils:kotlin-logging:1.7.9") testImplementation(project(":opendc-simulator:opendc-simulator-core")) + testImplementation(project(":opendc-compute:opendc-compute-simulator")) testImplementation(project(":opendc-format")) testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8") { exclude("org.jetbrains.kotlin", module = "kotlin-reflect") } testImplementation(kotlin("reflect")) + testRuntimeOnly("org.slf4j:slf4j-simple:${Library.SLF4J}") testImplementation("org.junit.jupiter:junit-jupiter-api:${Library.JUNIT_JUPITER}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Library.JUNIT_JUPITER}") testImplementation("org.junit.platform:junit-platform-launcher:${Library.JUNIT_PLATFORM}") diff --git a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/StageWorkflowService.kt b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/StageWorkflowService.kt index 3b4e6eab..e04c8a4c 100644 --- a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/StageWorkflowService.kt +++ b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/StageWorkflowService.kt @@ -26,40 +26,46 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import mu.KotlinLogging +import org.opendc.compute.core.Flavor import org.opendc.compute.core.Server import org.opendc.compute.core.ServerEvent import org.opendc.compute.core.ServerState -import org.opendc.compute.core.metal.Node -import org.opendc.compute.core.metal.service.ProvisioningService -import org.opendc.utils.flow.EventFlow +import org.opendc.compute.core.virt.service.VirtProvisioningService +import org.opendc.trace.core.EventTracer +import org.opendc.trace.core.consumeAsFlow +import org.opendc.trace.core.enable import org.opendc.workflows.service.stage.job.JobAdmissionPolicy import org.opendc.workflows.service.stage.job.JobOrderPolicy -import org.opendc.workflows.service.stage.resource.ResourceFilterPolicy -import org.opendc.workflows.service.stage.resource.ResourceSelectionPolicy import org.opendc.workflows.service.stage.task.TaskEligibilityPolicy import org.opendc.workflows.service.stage.task.TaskOrderPolicy import org.opendc.workflows.workload.Job +import org.opendc.workflows.workload.WORKFLOW_TASK_CORES import java.time.Clock import java.util.* /** * A [WorkflowService] that distributes work through a multi-stage process based on the Reference Architecture for - * Topology Scheduling. + * Datacenter Scheduling. */ public class StageWorkflowService( internal val coroutineScope: CoroutineScope, internal val clock: Clock, - private val provisioningService: ProvisioningService, + internal val tracer: EventTracer, + private val provisioningService: VirtProvisioningService, mode: WorkflowSchedulerMode, jobAdmissionPolicy: JobAdmissionPolicy, jobOrderPolicy: JobOrderPolicy, taskEligibilityPolicy: TaskEligibilityPolicy, - taskOrderPolicy: TaskOrderPolicy, - resourceFilterPolicy: ResourceFilterPolicy, - resourceSelectionPolicy: ResourceSelectionPolicy + taskOrderPolicy: TaskOrderPolicy ) : WorkflowService { + /** + * The logger instance to use. + */ + private val logger = KotlinLogging.logger {} /** * The incoming jobs ready to be processed by the scheduler. @@ -97,25 +103,10 @@ public class StageWorkflowService( internal val taskByServer = mutableMapOf<Server, TaskState>() /** - * The nodes that are controlled by the service. - */ - internal lateinit var nodes: List<Node> - - /** - * The available nodes. - */ - internal val available: MutableSet<Node> = mutableSetOf() - - /** - * The maximum number of incoming jobs. - */ - private val throttleLimit: Int = 20000 - - /** * The load of the system. */ internal val load: Double - get() = (available.size / nodes.size.toDouble()) + get() = (activeTasks.size / provisioningService.hostCount.toDouble()) /** * The root listener of this scheduler. @@ -166,26 +157,23 @@ public class StageWorkflowService( private val mode: WorkflowSchedulerMode.Logic private val jobAdmissionPolicy: JobAdmissionPolicy.Logic private val taskEligibilityPolicy: TaskEligibilityPolicy.Logic - private val resourceFilterPolicy: ResourceFilterPolicy.Logic - private val resourceSelectionPolicy: Comparator<Node> - private val eventFlow = EventFlow<WorkflowEvent>() init { - coroutineScope.launch { - nodes = provisioningService.nodes().toList() - available.addAll(nodes) - } - this.mode = mode(this) this.jobAdmissionPolicy = jobAdmissionPolicy(this) this.jobQueue = PriorityQueue(100, jobOrderPolicy(this).thenBy { it.job.uid }) this.taskEligibilityPolicy = taskEligibilityPolicy(this) this.taskQueue = PriorityQueue(1000, taskOrderPolicy(this).thenBy { it.task.uid }) - this.resourceFilterPolicy = resourceFilterPolicy(this) - this.resourceSelectionPolicy = resourceSelectionPolicy(this) } - override val events: Flow<WorkflowEvent> = eventFlow + override val events: Flow<WorkflowEvent> = tracer.openRecording().let { + it.enable<WorkflowEvent.JobSubmitted>() + it.enable<WorkflowEvent.JobStarted>() + it.enable<WorkflowEvent.JobFinished>() + it.enable<WorkflowEvent.TaskStarted>() + it.enable<WorkflowEvent.TaskFinished>() + it.consumeAsFlow().map { event -> event as WorkflowEvent } + } override suspend fun submit(job: Job) { // J1 Incoming Jobs @@ -209,6 +197,7 @@ public class StageWorkflowService( instances.values.toCollection(jobInstance.tasks) incomingJobs += jobInstance rootListener.jobSubmitted(jobInstance) + tracer.commit(WorkflowEvent.JobSubmitted(this, jobInstance.job)) requestCycle() } @@ -237,7 +226,7 @@ public class StageWorkflowService( iterator.remove() jobQueue.add(jobInstance) activeJobs += jobInstance - eventFlow.emit(WorkflowEvent.JobStarted(this, jobInstance.job, clock.millis())) + tracer.commit(WorkflowEvent.JobStarted(this, jobInstance.job)) rootListener.jobStarted(jobInstance) } @@ -279,26 +268,25 @@ public class StageWorkflowService( // T3 Per task while (taskQueue.isNotEmpty()) { val instance = taskQueue.peek() - val host: Node? = available.firstOrNull() - if (host != null) { - // T4 Submit task to machine - available -= host + val cores = instance.task.metadata[WORKFLOW_TASK_CORES] as? Int ?: 1 + val flavor = Flavor(cores, 1000) // TODO How to determine memory usage for workflow task + val image = instance.task.image + coroutineScope.launch { + val server = provisioningService.deploy(instance.task.name, image, flavor) + instance.state = TaskStatus.ACTIVE - val newHost = provisioningService.deploy(host, instance.task.image) - val server = newHost.server!! - instance.host = newHost + instance.server = server taskByServer[server] = instance + server.events .onEach { event -> if (event is ServerEvent.StateChanged) stateChanged(event.server) } .launchIn(coroutineScope) - - activeTasks += instance - taskQueue.poll() - rootListener.taskAssigned(instance) - } else { - break } + + activeTasks += instance + taskQueue.poll() + rootListener.taskAssigned(instance) } } @@ -307,12 +295,11 @@ public class StageWorkflowService( ServerState.ACTIVE -> { val task = taskByServer.getValue(server) task.startedAt = clock.millis() - eventFlow.emit( + tracer.commit( WorkflowEvent.TaskStarted( this@StageWorkflowService, task.job.job, - task.task, - clock.millis() + task.task ) ) rootListener.taskStarted(task) @@ -323,14 +310,12 @@ public class StageWorkflowService( task.state = TaskStatus.FINISHED task.finishedAt = clock.millis() job.tasks.remove(task) - available += task.host!! activeTasks -= task - eventFlow.emit( + tracer.commit( WorkflowEvent.TaskFinished( this@StageWorkflowService, task.job.job, - task.task, - clock.millis() + task.task ) ) rootListener.taskFinished(task) @@ -355,9 +340,9 @@ public class StageWorkflowService( } } - private suspend fun finishJob(job: JobState) { + private fun finishJob(job: JobState) { activeJobs -= job - eventFlow.emit(WorkflowEvent.JobFinished(this, job.job, clock.millis())) + tracer.commit(WorkflowEvent.JobFinished(this, job.job)) rootListener.jobFinished(job) } diff --git a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/TaskState.kt b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/TaskState.kt index ed023c82..d1eb6704 100644 --- a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/TaskState.kt +++ b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/TaskState.kt @@ -22,7 +22,7 @@ package org.opendc.workflows.service -import org.opendc.compute.core.metal.Node +import org.opendc.compute.core.Server import org.opendc.workflows.workload.Task public class TaskState(public val job: JobState, public val task: Task) { @@ -62,7 +62,7 @@ public class TaskState(public val job: JobState, public val task: Task) { } } - public var host: Node? = null + public var server: Server? = null /** * Mark the specified [TaskView] as terminated. diff --git a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/WorkflowEvent.kt b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/WorkflowEvent.kt index dadccb50..bcf93562 100644 --- a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/WorkflowEvent.kt +++ b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/WorkflowEvent.kt @@ -22,25 +22,33 @@ package org.opendc.workflows.service +import org.opendc.trace.core.Event import org.opendc.workflows.workload.Job import org.opendc.workflows.workload.Task /** * An event emitted by the [WorkflowService]. */ -public sealed class WorkflowEvent { +public sealed class WorkflowEvent : Event() { /** * The [WorkflowService] that emitted the event. */ public abstract val service: WorkflowService /** + * This event is emitted when a job was submitted to the scheduler. + */ + public data class JobSubmitted( + override val service: WorkflowService, + public val job: Job + ) : WorkflowEvent() + + /** * This event is emitted when a job has become active. */ public data class JobStarted( override val service: WorkflowService, - public val job: Job, - public val time: Long + public val job: Job ) : WorkflowEvent() /** @@ -48,8 +56,7 @@ public sealed class WorkflowEvent { */ public data class JobFinished( override val service: WorkflowService, - public val job: Job, - public val time: Long + public val job: Job ) : WorkflowEvent() /** @@ -58,8 +65,7 @@ public sealed class WorkflowEvent { public data class TaskStarted( override val service: WorkflowService, public val job: Job, - public val task: Task, - public val time: Long + public val task: Task ) : WorkflowEvent() /** @@ -68,7 +74,6 @@ public sealed class WorkflowEvent { public data class TaskFinished( override val service: WorkflowService, public val job: Job, - public val task: Task, - public val time: Long + public val task: Task ) : WorkflowEvent() } diff --git a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/WorkflowService.kt b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/WorkflowService.kt index 319a8b85..b24f80da 100644 --- a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/WorkflowService.kt +++ b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/WorkflowService.kt @@ -34,7 +34,7 @@ import java.util.* */ public interface WorkflowService { /** - * Thie events emitted by the workflow scheduler. + * The events emitted by the workflow scheduler. */ public val events: Flow<WorkflowEvent> diff --git a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/FunctionalResourceFilterPolicy.kt b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/FunctionalResourceFilterPolicy.kt deleted file mode 100644 index ac79a9ce..00000000 --- a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/FunctionalResourceFilterPolicy.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.workflows.service.stage.resource - -import org.opendc.compute.core.metal.Node -import org.opendc.workflows.service.StageWorkflowService -import org.opendc.workflows.service.TaskState - -/** - * A [ResourceFilterPolicy] based on the amount of cores available on the machine and the cores required for - * the task. - */ -public object FunctionalResourceFilterPolicy : ResourceFilterPolicy { - override fun invoke(scheduler: StageWorkflowService): ResourceFilterPolicy.Logic = - object : ResourceFilterPolicy.Logic { - override fun invoke(hosts: Sequence<Node>, task: TaskState): Sequence<Node> = - hosts.filter { it in scheduler.available } - } - - override fun toString(): String = "functional" -} diff --git a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/ResourceFilterPolicy.kt b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/ResourceFilterPolicy.kt deleted file mode 100644 index 4923a34b..00000000 --- a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/service/stage/resource/ResourceFilterPolicy.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2020 AtLarge Research - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.opendc.workflows.service.stage.resource - -import org.opendc.compute.core.metal.Node -import org.opendc.workflows.service.TaskState -import org.opendc.workflows.service.stage.StagePolicy - -/** - * This interface represents stages **R2**, **R3** and **R4** stage of the Reference Architecture for Schedulers and - * acts as a filter yielding a list of resources with sufficient resource-capacities, based on fixed or dynamic - * requirements, and on predicted or monitored information about processing unit availability, memory occupancy, etc. - */ -public interface ResourceFilterPolicy : StagePolicy<ResourceFilterPolicy.Logic> { - public interface Logic { - /** - * Filter the list of machines based on dynamic information. - * - * @param hosts The hosts to filter. - * @param task The task that is to be scheduled. - * @return The machines on which the task can be scheduled. - */ - public operator fun invoke(hosts: Sequence<Node>, task: TaskState): Sequence<Node> - } -} diff --git a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/workload/Metadata.kt b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/workload/Metadata.kt index d02e2b4e..4305aa57 100644 --- a/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/workload/Metadata.kt +++ b/simulator/opendc-workflows/src/main/kotlin/org/opendc/workflows/workload/Metadata.kt @@ -28,3 +28,8 @@ package org.opendc.workflows.workload * Meta-data key for the deadline of a task. */ public const val WORKFLOW_TASK_DEADLINE: String = "workflow:task:deadline" + +/** + * Meta-data key for the number of cores needed for a task. + */ +public const val WORKFLOW_TASK_CORES: String = "workflow:task:cores" diff --git a/simulator/opendc-workflows/src/test/kotlin/org/opendc/workflows/service/StageWorkflowSchedulerIntegrationTest.kt b/simulator/opendc-workflows/src/test/kotlin/org/opendc/workflows/service/StageWorkflowSchedulerIntegrationTest.kt index 90cf5b99..2bfcba35 100644 --- a/simulator/opendc-workflows/src/test/kotlin/org/opendc/workflows/service/StageWorkflowSchedulerIntegrationTest.kt +++ b/simulator/opendc-workflows/src/test/kotlin/org/opendc/workflows/service/StageWorkflowSchedulerIntegrationTest.kt @@ -35,14 +35,17 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll import org.opendc.compute.core.metal.service.ProvisioningService +import org.opendc.compute.simulator.SimVirtProvisioningService +import org.opendc.compute.simulator.allocation.NumberOfActiveServersAllocationPolicy import org.opendc.format.environment.sc18.Sc18EnvironmentReader import org.opendc.format.trace.gwf.GwfTraceReader +import org.opendc.simulator.compute.SimSpaceSharedHypervisorProvider import org.opendc.simulator.utils.DelayControllerClockAdapter +import org.opendc.trace.core.EventTracer import org.opendc.workflows.service.stage.job.NullJobAdmissionPolicy import org.opendc.workflows.service.stage.job.SubmissionTimeJobOrderPolicy -import org.opendc.workflows.service.stage.resource.FirstFitResourceSelectionPolicy -import org.opendc.workflows.service.stage.resource.FunctionalResourceFilterPolicy import org.opendc.workflows.service.stage.task.NullTaskEligibilityPolicy import org.opendc.workflows.service.stage.task.SubmissionTimeTaskOrderPolicy import kotlin.math.max @@ -57,7 +60,7 @@ internal class StageWorkflowSchedulerIntegrationTest { * A large integration test where we check whether all tasks in some trace are executed correctly. */ @Test - fun `should execute all tasks in trace`() { + fun testTrace() { var jobsSubmitted = 0L var jobsStarted = 0L var jobsFinished = 0L @@ -66,22 +69,32 @@ internal class StageWorkflowSchedulerIntegrationTest { val testScope = TestCoroutineScope() val clock = DelayControllerClockAdapter(testScope) + val tracer = EventTracer(clock) val schedulerAsync = testScope.async { val environment = Sc18EnvironmentReader(object {}.javaClass.getResourceAsStream("/environment.json")) .use { it.construct(testScope, clock) } + val bareMetal = environment.platforms[0].zones[0].services[ProvisioningService] + + // Wait for the bare metal nodes to be spawned + delay(10) + + val provisioner = SimVirtProvisioningService(testScope, clock, bareMetal, NumberOfActiveServersAllocationPolicy(), tracer, SimSpaceSharedHypervisorProvider(), schedulingQuantum = 1000) + + // Wait for the hypervisors to be spawned + delay(10) + StageWorkflowService( testScope, clock, - environment.platforms[0].zones[0].services[ProvisioningService], + tracer, + provisioner, mode = WorkflowSchedulerMode.Batch(100), jobAdmissionPolicy = NullJobAdmissionPolicy, jobOrderPolicy = SubmissionTimeJobOrderPolicy(), taskEligibilityPolicy = NullTaskEligibilityPolicy, taskOrderPolicy = SubmissionTimeTaskOrderPolicy(), - resourceFilterPolicy = FunctionalResourceFilterPolicy, - resourceSelectionPolicy = FirstFitResourceSelectionPolicy ) } @@ -106,16 +119,19 @@ internal class StageWorkflowSchedulerIntegrationTest { while (reader.hasNext()) { val (time, job) = reader.next() jobsSubmitted++ - delay(max(0, time * 1000 - clock.millis())) + delay(max(0, time - clock.millis())) scheduler.submit(job) } } testScope.advanceUntilIdle() - assertNotEquals(0, jobsSubmitted, "No jobs submitted") - assertEquals(jobsSubmitted, jobsStarted, "Not all submitted jobs started") - assertEquals(jobsSubmitted, jobsFinished, "Not all started jobs finished") - assertEquals(tasksStarted, tasksFinished, "Not all started tasks finished") + assertAll( + { assertEquals(emptyList<Throwable>(), testScope.uncaughtExceptions) }, + { assertNotEquals(0, jobsSubmitted, "No jobs submitted") }, + { assertEquals(jobsSubmitted, jobsStarted, "Not all submitted jobs started") }, + { assertEquals(jobsSubmitted, jobsFinished, "Not all started jobs finished") }, + { assertEquals(tasksStarted, tasksFinished, "Not all started tasks finished") } + ) } } diff --git a/simulator/settings.gradle.kts b/simulator/settings.gradle.kts index 935a18d0..470303f4 100644 --- a/simulator/settings.gradle.kts +++ b/simulator/settings.gradle.kts @@ -32,4 +32,6 @@ include(":opendc-runner-web") include(":opendc-simulator:opendc-simulator-core") include(":opendc-simulator:opendc-simulator-compute") include(":opendc-simulator:opendc-simulator-failures") +include(":opendc-trace:opendc-trace-core") +include(":opendc-harness") include(":opendc-utils") |
