From 83e6c19681a5cae4b80773f95a66b6753b0bcd7e Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 30 Sep 2021 10:28:46 +0200 Subject: build: Migrate from kotlinx-benchmark to jmh-gradle This change updates the project to use jmh-gradle for benchmarking as opposed to kotlinx-benchmark. Both plugins use JMH under the hood, but jmh-gradle offers more options for profiling and seems to be beter maintained. --- opendc-simulator/opendc-simulator-resources/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-resources/build.gradle.kts b/opendc-simulator/opendc-simulator-resources/build.gradle.kts index e4ffc3ff..68047d5c 100644 --- a/opendc-simulator/opendc-simulator-resources/build.gradle.kts +++ b/opendc-simulator/opendc-simulator-resources/build.gradle.kts @@ -34,6 +34,5 @@ dependencies { api(libs.kotlinx.coroutines) implementation(projects.opendcUtils) - jmhImplementation(projects.opendcSimulator.opendcSimulatorCore) testImplementation(projects.opendcSimulator.opendcSimulatorCore) } -- cgit v1.2.3 From 0ffd93933228e87a205c9839d1bf04cd0e178e8c Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 30 Sep 2021 12:14:42 +0200 Subject: test(simulator): Use longer traces for benchmarks This change updates the JMH benchmarks to use longer traces in order to measure the overhead of running the flow simulation as opposed to setting up the benchmark. --- .../org/opendc/simulator/compute/SimMachineBenchmarks.kt | 16 +++++----------- .../opendc/simulator/resources/SimResourceBenchmarks.kt | 14 ++++---------- 2 files changed, 9 insertions(+), 21 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt b/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt index 30797089..88ad7286 100644 --- a/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt +++ b/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt @@ -38,6 +38,7 @@ import org.opendc.simulator.core.SimulationCoroutineScope import org.opendc.simulator.core.runBlockingSimulation import org.opendc.simulator.resources.SimResourceInterpreter import org.openjdk.jmh.annotations.* +import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.TimeUnit @State(Scope.Thread) @@ -63,22 +64,15 @@ class SimMachineBenchmarks { ) } - @State(Scope.Benchmark) + @State(Scope.Thread) class Workload { lateinit var trace: Sequence @Setup fun setUp() { - trace = sequenceOf( - SimTraceWorkload.Fragment(0, 1000, 28.0, 1), - SimTraceWorkload.Fragment(1000, 1000, 3500.0, 1), - SimTraceWorkload.Fragment(2000, 1000, 0.0, 1), - SimTraceWorkload.Fragment(3000, 1000, 183.0, 1), - SimTraceWorkload.Fragment(4000, 1000, 400.0, 1), - SimTraceWorkload.Fragment(5000, 1000, 100.0, 1), - SimTraceWorkload.Fragment(6000, 1000, 3000.0, 1), - SimTraceWorkload.Fragment(7000, 1000, 4500.0, 1), - ) + val random = ThreadLocalRandom.current() + val entries = List(10000) { SimTraceWorkload.Fragment(it * 1000L, 1000, random.nextDouble(0.0, 4500.0), 1) } + trace = entries.asSequence() } } diff --git a/opendc-simulator/opendc-simulator-resources/src/jmh/kotlin/org/opendc/simulator/resources/SimResourceBenchmarks.kt b/opendc-simulator/opendc-simulator-resources/src/jmh/kotlin/org/opendc/simulator/resources/SimResourceBenchmarks.kt index b45b2a2f..fbc3f319 100644 --- a/opendc-simulator/opendc-simulator-resources/src/jmh/kotlin/org/opendc/simulator/resources/SimResourceBenchmarks.kt +++ b/opendc-simulator/opendc-simulator-resources/src/jmh/kotlin/org/opendc/simulator/resources/SimResourceBenchmarks.kt @@ -28,6 +28,7 @@ import org.opendc.simulator.core.SimulationCoroutineScope import org.opendc.simulator.core.runBlockingSimulation import org.opendc.simulator.resources.consumer.SimTraceConsumer import org.openjdk.jmh.annotations.* +import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.TimeUnit @State(Scope.Thread) @@ -51,16 +52,9 @@ class SimResourceBenchmarks { @Setup fun setUp() { - trace = sequenceOf( - SimTraceConsumer.Fragment(1000, 28.0), - SimTraceConsumer.Fragment(1000, 3500.0), - SimTraceConsumer.Fragment(1000, 0.0), - SimTraceConsumer.Fragment(1000, 183.0), - SimTraceConsumer.Fragment(1000, 400.0), - SimTraceConsumer.Fragment(1000, 100.0), - SimTraceConsumer.Fragment(1000, 3000.0), - SimTraceConsumer.Fragment(1000, 4500.0), - ) + val random = ThreadLocalRandom.current() + val entries = List(10000) { SimTraceConsumer.Fragment(1000, random.nextDouble(0.0, 4500.0)) } + trace = entries.asSequence() } } -- cgit v1.2.3 From a4a611c45dfd5f9e379434f1dc459128cb437338 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 23 Sep 2021 14:45:56 +0200 Subject: perf(simulator): Use direct field access for perf-sensitive code This change updates the SimResourceDistributorMaxMin implementation to use direct field accesses in the perf-sensitive code. --- .../resources/SimResourceDistributorMaxMin.kt | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt index 6c1e134b..63cfbdac 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt @@ -162,6 +162,8 @@ public class SimResourceDistributorMaxMin( return SimResourceCommand.Idle() } + val now = interpreter.clock.millis() + val capacity = ctx.capacity var duration: Double = Double.MAX_VALUE var deadline: Long = Long.MAX_VALUE @@ -171,7 +173,7 @@ public class SimResourceDistributorMaxMin( // Pull in the work of the outputs val outputIterator = activeOutputs.listIterator() for (output in outputIterator) { - output.pull() + output.pull(now) // Remove outputs that have finished if (!output.isActive) { @@ -206,7 +208,7 @@ public class SimResourceDistributorMaxMin( duration = min(duration, output.work / grantedSpeed) } - val targetDuration = min(duration, (deadline - interpreter.clock.millis()) / 1000.0) + val targetDuration = min(duration, (deadline - now) / 1000.0) var totalRequestedWork = 0.0 var totalAllocatedWork = 0.0 for (output in activeOutputs) { @@ -219,7 +221,7 @@ public class SimResourceDistributorMaxMin( } } - assert(deadline >= interpreter.clock.millis()) { "Deadline already passed" } + assert(deadline >= now) { "Deadline already passed" } this.totalRequestedSpeed = totalRequestedSpeed this.totalAllocatedWork = totalAllocatedWork @@ -254,27 +256,27 @@ public class SimResourceDistributorMaxMin( /** * The current requested work. */ - var work: Double = 0.0 + @JvmField var work: Double = 0.0 /** * The requested limit. */ - var limit: Double = 0.0 + @JvmField var limit: Double = 0.0 /** * The current deadline. */ - var deadline: Long = Long.MAX_VALUE + @JvmField var deadline: Long = Long.MAX_VALUE /** * The processing speed that is allowed by the model constraints. */ - var allowedSpeed: Double = 0.0 + @JvmField var allowedSpeed: Double = 0.0 /** * The actual processing speed. */ - var actualSpeed: Double = 0.0 + @JvmField var actualSpeed: Double = 0.0 /** * The timestamp at which we received the last command. @@ -363,9 +365,9 @@ public class SimResourceDistributorMaxMin( /** * Pull the next command if necessary. */ - fun pull() { + fun pull(now: Long) { val ctx = ctx - if (ctx != null && lastCommandTimestamp < ctx.clock.millis()) { + if (ctx != null && lastCommandTimestamp < now) { ctx.flush() } } -- cgit v1.2.3 From d575bed5418be222e1d3ad39af862e2390596d61 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sun, 26 Sep 2021 13:11:10 +0200 Subject: refactor(simulator): Combine work and deadline to duration This change removes the work and deadline properties from the SimResourceCommand.Consume class and introduces a new property duration. This property is now used in conjunction with the limit to compute the amount of work processed by a resource provider. Previously, we used both work and deadline to compute the duration and the amount of remaining work at the end of a consumption. However, with this change, we ensure that a resource consumption always runs at the same speed once establishing, drastically simplifying the computation for the amount of work processed during the consumption. --- .../org/opendc/simulator/compute/device/SimPsu.kt | 6 +- .../simulator/compute/workload/SimTraceWorkload.kt | 12 +- .../compute/kernel/SimSpaceSharedHypervisorTest.kt | 2 + .../org/opendc/simulator/network/SimNetworkSink.kt | 2 +- .../kotlin/org/opendc/simulator/power/SimPdu.kt | 14 +-- .../kotlin/org/opendc/simulator/power/SimUps.kt | 14 +-- .../resources/SimAbstractResourceAggregator.kt | 40 +++---- .../resources/SimAbstractResourceProvider.kt | 22 +++- .../resources/SimResourceAggregatorMaxMin.kt | 15 +-- .../simulator/resources/SimResourceCommand.kt | 16 +-- .../simulator/resources/SimResourceConsumer.kt | 4 +- .../simulator/resources/SimResourceContext.kt | 5 - .../resources/SimResourceDistributorMaxMin.kt | 129 +++++++-------------- .../resources/SimResourceProviderLogic.kt | 48 +++----- .../simulator/resources/SimResourceSource.kt | 31 ++--- .../simulator/resources/SimResourceTransformer.kt | 28 ++--- .../resources/consumer/SimSpeedConsumerAdapter.kt | 4 +- .../resources/consumer/SimTraceConsumer.kt | 13 +-- .../resources/consumer/SimWorkConsumer.kt | 22 ++-- .../resources/impl/SimResourceContextImpl.kt | 83 +++---------- .../resources/SimResourceAggregatorMaxMinTest.kt | 19 ++- .../simulator/resources/SimResourceCommandTest.kt | 31 +---- .../simulator/resources/SimResourceContextTest.kt | 50 +++----- .../simulator/resources/SimResourceSourceTest.kt | 54 +++------ .../resources/SimResourceSwitchExclusiveTest.kt | 11 +- .../resources/SimResourceSwitchMaxMinTest.kt | 4 +- .../resources/SimResourceTransformerTest.kt | 19 ++- .../simulator/resources/SimWorkConsumerTest.kt | 2 - 28 files changed, 229 insertions(+), 471 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt index 0a7dc40f..34ac4418 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt @@ -83,13 +83,13 @@ public class SimPsu( } override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { val powerDraw = computePowerDraw(_driver?.computePower() ?: 0.0) return if (powerDraw > 0.0) - SimResourceCommand.Consume(Double.POSITIVE_INFINITY, powerDraw, Long.MAX_VALUE) + SimResourceCommand.Consume(powerDraw, Long.MAX_VALUE) else - SimResourceCommand.Idle() + SimResourceCommand.Consume(0.0) } override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt index 5a4c4f44..527619bd 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt @@ -80,14 +80,13 @@ public class SimTraceWorkload(public val trace: Sequence, private val } private inner class Consumer(val cpu: ProcessingUnit) : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext): SimResourceCommand { - val now = ctx.clock.millis() + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { val fragment = pullFragment(now) ?: return SimResourceCommand.Exit val timestamp = fragment.timestamp + offset // Fragment is in the future if (timestamp > now) { - return SimResourceCommand.Idle(timestamp) + return SimResourceCommand.Consume(0.0, timestamp - now) } val cores = min(cpu.node.coreCount, fragment.cores) @@ -97,12 +96,11 @@ public class SimTraceWorkload(public val trace: Sequence, private val 0.0 val deadline = timestamp + fragment.duration val duration = deadline - now - val work = duration * usage / 1000 - return if (cpu.id < cores && work > 0.0) - SimResourceCommand.Consume(work, usage, deadline) + return if (cpu.id < cores && usage > 0.0) + SimResourceCommand.Consume(usage, duration) else - SimResourceCommand.Idle(deadline) + SimResourceCommand.Consume(0.0, duration) } } diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt index 3d3feb2a..55d6d7c4 100644 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt @@ -155,6 +155,8 @@ internal class SimSpaceSharedHypervisorTest { vm.run(SimRuntimeWorkload(duration)) vm.close() + yield() + val vm2 = hypervisor.createMachine(machineModel) vm2.run(SimRuntimeWorkload(duration)) vm2.close() diff --git a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt index 5efdbed9..f2dd8455 100644 --- a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt +++ b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt @@ -32,7 +32,7 @@ public class SimNetworkSink( public val capacity: Double ) : SimNetworkPort() { override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext): SimResourceCommand = SimResourceCommand.Idle() + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand = SimResourceCommand.Consume(0.0) override fun toString(): String = "SimNetworkSink.Consumer" } diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt index 3ce85d02..e8839496 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt @@ -47,21 +47,13 @@ public class SimPdu( public fun newOutlet(): Outlet = Outlet(distributor.newOutput()) override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer by distributor { - override fun onNext(ctx: SimResourceContext): SimResourceCommand { - return when (val cmd = distributor.onNext(ctx)) { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + return when (val cmd = distributor.onNext(ctx, now, delta)) { is SimResourceCommand.Consume -> { - val duration = cmd.work / cmd.limit val loss = computePowerLoss(cmd.limit) val newLimit = cmd.limit + loss - SimResourceCommand.Consume(duration * newLimit, newLimit, cmd.deadline) - } - is SimResourceCommand.Idle -> { - val loss = computePowerLoss(0.0) - if (loss > 0.0) - SimResourceCommand.Consume(Double.POSITIVE_INFINITY, loss, cmd.deadline) - else - cmd + SimResourceCommand.Consume(newLimit, cmd.duration) } else -> cmd } diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt index f9431d21..4c2beb68 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt @@ -55,21 +55,13 @@ public class SimUps( override fun onConnect(inlet: SimPowerInlet) { val consumer = inlet.createConsumer() aggregator.startConsumer(object : SimResourceConsumer by consumer { - override fun onNext(ctx: SimResourceContext): SimResourceCommand { - return when (val cmd = consumer.onNext(ctx)) { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + return when (val cmd = consumer.onNext(ctx, now, delta)) { is SimResourceCommand.Consume -> { - val duration = cmd.work / cmd.limit val loss = computePowerLoss(cmd.limit) val newLimit = cmd.limit + loss - SimResourceCommand.Consume(duration * newLimit, newLimit, cmd.deadline) - } - is SimResourceCommand.Idle -> { - val loss = computePowerLoss(0.0) - if (loss > 0.0) - SimResourceCommand.Consume(Double.POSITIVE_INFINITY, loss, cmd.deadline) - else - cmd + SimResourceCommand.Consume(newLimit, cmd.duration) } else -> cmd } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt index 00648876..da5c3257 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt @@ -32,12 +32,7 @@ public abstract class SimAbstractResourceAggregator( /** * This method is invoked when the resource consumer consumes resources. */ - protected abstract fun doConsume(work: Double, limit: Double, deadline: Long) - - /** - * This method is invoked when the resource consumer enters an idle state. - */ - protected abstract fun doIdle(deadline: Long) + protected abstract fun doConsume(limit: Double, duration: Long) /** * This method is invoked when the resource consumer finishes processing. @@ -98,26 +93,23 @@ public abstract class SimAbstractResourceAggregator( private val _output = object : SimAbstractResourceProvider(interpreter, parent, initialCapacity = 0.0) { override fun createLogic(): SimResourceProviderLogic { return object : SimResourceProviderLogic { - override fun onIdle(ctx: SimResourceControllableContext, deadline: Long): Long { - doIdle(deadline) - return Long.MAX_VALUE - } - override fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long { - doConsume(work, limit, deadline) - return Long.MAX_VALUE + override fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long { + doConsume(limit, duration) + return super.onConsume(ctx, now, limit, duration) } override fun onFinish(ctx: SimResourceControllableContext) { doFinish() } - override fun onUpdate(ctx: SimResourceControllableContext, work: Double, willOvercommit: Boolean) { - updateCounters(ctx, work, willOvercommit) - } - - override fun getConsumedWork(ctx: SimResourceControllableContext, work: Double, speed: Double, duration: Long): Double { - return work - _inputConsumers.sumOf { it.remainingWork } + override fun onUpdate( + ctx: SimResourceControllableContext, + delta: Long, + limit: Double, + willOvercommit: Boolean + ) { + updateCounters(ctx, delta, limit, willOvercommit) } } } @@ -156,12 +148,6 @@ public abstract class SimAbstractResourceAggregator( get() = _ctx!! private var _ctx: SimResourceContext? = null - /** - * The remaining work of the consumer. - */ - val remainingWork: Double - get() = _ctx?.remainingWork ?: 0.0 - /** * The resource command to run next. */ @@ -179,7 +165,7 @@ public abstract class SimAbstractResourceAggregator( } /* SimResourceConsumer */ - override fun onNext(ctx: SimResourceContext): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { var next = command return if (next != null) { @@ -190,7 +176,7 @@ public abstract class SimAbstractResourceAggregator( next = command this.command = null - next ?: SimResourceCommand.Idle() + next ?: SimResourceCommand.Consume(0.0) } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt index 4e8e803a..548bc228 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt @@ -87,21 +87,35 @@ public abstract class SimAbstractResourceProvider( /** * Update the counters of the resource provider. */ - protected fun updateCounters(ctx: SimResourceContext, work: Double, willOvercommit: Boolean) { - if (work <= 0.0) { + protected fun updateCounters(ctx: SimResourceContext, delta: Long, limit: Double, willOvercommit: Boolean) { + if (delta <= 0.0) { return } val counters = _counters - val remainingWork = ctx.remainingWork + val deltaS = delta / 1000.0 + val work = limit * deltaS + val actualWork = ctx.speed * deltaS + val remainingWork = work - actualWork + counters.demand += work - counters.actual += work - remainingWork + counters.actual += actualWork if (willOvercommit && remainingWork > 0.0) { counters.overcommit += remainingWork } } + /** + * Update the counters of the resource provider. + */ + protected fun updateCounters(demand: Double, actual: Double, overcommit: Double) { + val counters = _counters + counters.demand += demand + counters.actual += actual + counters.overcommit += overcommit + } + final override fun startConsumer(consumer: SimResourceConsumer) { check(ctx == null) { "Resource is in invalid state" } val ctx = interpreter.newContext(consumer, createLogic(), parent) diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt index 991cda7a..537be1b5 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt @@ -31,7 +31,7 @@ public class SimResourceAggregatorMaxMin( ) : SimAbstractResourceAggregator(interpreter, parent) { private val consumers = mutableListOf() - override fun doConsume(work: Double, limit: Double, deadline: Long) { + override fun doConsume(limit: Double, duration: Long) { // Sort all consumers by their capacity consumers.sortWith(compareBy { it.ctx.capacity }) @@ -40,22 +40,15 @@ public class SimResourceAggregatorMaxMin( val inputCapacity = input.ctx.capacity val fraction = inputCapacity / capacity val grantedSpeed = limit * fraction - val grantedWork = fraction * work - val command = if (grantedWork > 0.0 && grantedSpeed > 0.0) - SimResourceCommand.Consume(grantedWork, grantedSpeed, deadline) + val command = if (grantedSpeed > 0.0) + SimResourceCommand.Consume(grantedSpeed, duration) else - SimResourceCommand.Idle() + SimResourceCommand.Consume(0.0, duration) input.push(command) } } - override fun doIdle(deadline: Long) { - for (input in consumers) { - input.push(SimResourceCommand.Idle(deadline)) - } - } - override fun doFinish() { val iterator = consumers.iterator() for (input in iterator) { diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCommand.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCommand.kt index f7f3fa4d..4a980071 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCommand.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCommand.kt @@ -27,24 +27,18 @@ package org.opendc.simulator.resources */ public sealed class SimResourceCommand { /** - * A request to the resource to perform the specified amount of work before the given [deadline]. + * A request to the resource to perform work for the specified [duration]. * - * @param work The amount of work to process. * @param limit The maximum amount of work to be processed per second. - * @param deadline The instant at which the work needs to be fulfilled. + * @param duration The duration of the resource consumption in milliseconds. */ - public data class Consume(val work: Double, val limit: Double, val deadline: Long = Long.MAX_VALUE) : SimResourceCommand() { + public data class Consume(val limit: Double, val duration: Long = Long.MAX_VALUE) : SimResourceCommand() { init { - require(work > 0) { "Amount of work must be positive" } - require(limit > 0) { "Limit must be positive" } + require(limit >= 0.0) { "Negative limit is not allowed" } + require(duration >= 0) { "Duration must be positive" } } } - /** - * An indication to the resource that the consumer will idle until the specified [deadline] or if it is interrupted. - */ - public data class Idle(val deadline: Long = Long.MAX_VALUE) : SimResourceCommand() - /** * An indication to the resource that the consumer has finished. */ diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt index 4d937514..4d1d2c32 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt @@ -34,9 +34,11 @@ public interface SimResourceConsumer { * the resource finished processing, reached its deadline or was interrupted. * * @param ctx The execution context in which the consumer runs. + * @param now The virtual timestamp in milliseconds at which the update is occurring. + * @param delta The virtual duration between this call and the last call in milliseconds. * @return The next command that the resource should execute. */ - public fun onNext(ctx: SimResourceContext): SimResourceCommand + public fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand /** * This method is invoked when an event has occurred. diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt index 0d9a6106..f28b43d0 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt @@ -49,11 +49,6 @@ public interface SimResourceContext { */ public val demand: Double - /** - * The amount of work still remaining at this instant. - */ - public val remainingWork: Double - /** * Ask the resource provider to interrupt its resource. */ diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt index 63cfbdac..d23c7dbb 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt @@ -97,8 +97,8 @@ public class SimResourceDistributorMaxMin( } /* SimResourceConsumer */ - override fun onNext(ctx: SimResourceContext): SimResourceCommand { - return doNext(ctx) + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + return doNext(ctx, now) } override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { @@ -134,39 +134,17 @@ public class SimResourceDistributorMaxMin( public val interference: Double } - /** - * Update the counters of the distributor. - */ - private fun updateCounters(ctx: SimResourceControllableContext, work: Double, willOvercommit: Boolean) { - if (work <= 0.0) { - return - } - - val counters = _counters - val remainingWork = ctx.remainingWork - - counters.demand += work - counters.actual += work - remainingWork - - if (willOvercommit && remainingWork > 0.0) { - counters.overcommit += remainingWork - } - } - /** * Schedule the work of the outputs. */ - private fun doNext(ctx: SimResourceContext): SimResourceCommand { + private fun doNext(ctx: SimResourceContext, now: Long): SimResourceCommand { // If there is no work yet, mark the input as idle. if (activeOutputs.isEmpty()) { - return SimResourceCommand.Idle() + return SimResourceCommand.Consume(0.0) } - val now = interpreter.clock.millis() - val capacity = ctx.capacity - var duration: Double = Double.MAX_VALUE - var deadline: Long = Long.MAX_VALUE + var duration: Long = Long.MAX_VALUE var availableSpeed = capacity var totalRequestedSpeed = 0.0 @@ -191,10 +169,10 @@ public class SimResourceDistributorMaxMin( val availableShare = availableSpeed / remaining-- val grantedSpeed = min(output.allowedSpeed, availableShare) - deadline = min(deadline, output.deadline) + duration = min(duration, output.duration) // Ignore idle computation - if (grantedSpeed <= 0.0 || output.work <= 0.0) { + if (grantedSpeed <= 0.0) { output.actualSpeed = 0.0 continue } @@ -203,35 +181,29 @@ public class SimResourceDistributorMaxMin( output.actualSpeed = grantedSpeed availableSpeed -= grantedSpeed - - // The duration that we want to run is that of the shortest request of an output - duration = min(duration, output.work / grantedSpeed) } - val targetDuration = min(duration, (deadline - now) / 1000.0) + val durationS = duration / 1000.0 var totalRequestedWork = 0.0 var totalAllocatedWork = 0.0 for (output in activeOutputs) { - val work = output.work + val limit = output.limit val speed = output.actualSpeed if (speed > 0.0) { - val outputDuration = work / speed - totalRequestedWork += work * (duration / outputDuration) - totalAllocatedWork += work * (targetDuration / outputDuration) + totalRequestedWork += limit * durationS + totalAllocatedWork += speed * durationS } } - assert(deadline >= now) { "Deadline already passed" } - this.totalRequestedSpeed = totalRequestedSpeed this.totalAllocatedWork = totalAllocatedWork val totalAllocatedSpeed = capacity - availableSpeed this.totalAllocatedSpeed = totalAllocatedSpeed return if (totalAllocatedWork > 0.0 && totalAllocatedSpeed > 0.0) - SimResourceCommand.Consume(totalAllocatedWork, totalAllocatedSpeed, deadline) + SimResourceCommand.Consume(totalAllocatedSpeed, duration) else - SimResourceCommand.Idle(deadline) + SimResourceCommand.Consume(0.0, duration) } private fun updateCapacity(ctx: SimResourceContext) { @@ -243,7 +215,7 @@ public class SimResourceDistributorMaxMin( /** * An internal [SimResourceProvider] implementation for switch outputs. */ - private inner class Output(capacity: Double, private val key: InterferenceKey?) : + private inner class Output(capacity: Double, val key: InterferenceKey?) : SimAbstractResourceProvider(interpreter, parent, capacity), SimResourceCloseableProvider, SimResourceProviderLogic, @@ -253,11 +225,6 @@ public class SimResourceDistributorMaxMin( */ private var isClosed: Boolean = false - /** - * The current requested work. - */ - @JvmField var work: Double = 0.0 - /** * The requested limit. */ @@ -266,7 +233,7 @@ public class SimResourceDistributorMaxMin( /** * The current deadline. */ - @JvmField var deadline: Long = Long.MAX_VALUE + @JvmField var duration: Long = Long.MAX_VALUE /** * The processing speed that is allowed by the model constraints. @@ -304,44 +271,19 @@ public class SimResourceDistributorMaxMin( } /* SimResourceProviderLogic */ - override fun onIdle(ctx: SimResourceControllableContext, deadline: Long): Long { - allowedSpeed = 0.0 - this.deadline = deadline - work = 0.0 - limit = 0.0 - lastCommandTimestamp = ctx.clock.millis() - - return Long.MAX_VALUE - } - - override fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long { + override fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long { allowedSpeed = min(ctx.capacity, limit) - this.work = work this.limit = limit - this.deadline = deadline - lastCommandTimestamp = ctx.clock.millis() - - return Long.MAX_VALUE - } - - override fun onUpdate(ctx: SimResourceControllableContext, work: Double, willOvercommit: Boolean) { - updateCounters(ctx, work, willOvercommit) + this.duration = duration + lastCommandTimestamp = now - this@SimResourceDistributorMaxMin.updateCounters(ctx, work, willOvercommit) - } - - override fun onFinish(ctx: SimResourceControllableContext) { - work = 0.0 - limit = 0.0 - deadline = Long.MAX_VALUE - lastCommandTimestamp = ctx.clock.millis() + return super.onConsume(ctx, now, limit, duration) } - override fun getConsumedWork(ctx: SimResourceControllableContext, work: Double, speed: Double, duration: Long): Double { - val totalRemainingWork = this@SimResourceDistributorMaxMin.ctx?.remainingWork ?: 0.0 - - // Compute the fraction of compute time allocated to the output - val fraction = actualSpeed / totalAllocatedSpeed + override fun onUpdate(ctx: SimResourceControllableContext, delta: Long, limit: Double, willOvercommit: Boolean) { + if (delta <= 0.0) { + return + } // Compute the performance penalty due to resource interference val perfScore = if (interferenceDomain != null) { @@ -351,12 +293,29 @@ public class SimResourceDistributorMaxMin( 1.0 } - // Compute the work that was actually granted to the output. - val potentialConsumedWork = (totalAllocatedWork - totalRemainingWork) * fraction + val deltaS = delta / 1000.0 + val work = limit * deltaS + val actualWork = actualSpeed * deltaS + val remainingWork = work - actualWork + val overcommit = if (willOvercommit && remainingWork > 0.0) { + remainingWork + } else { + 0.0 + } - _counters.interference += potentialConsumedWork * max(0.0, 1 - perfScore) + updateCounters(work, actualWork, overcommit) - return potentialConsumedWork + val distCounters = _counters + distCounters.demand += work + distCounters.actual += actualWork + distCounters.overcommit += overcommit + distCounters.interference += actualWork * max(0.0, 1 - perfScore) + } + + override fun onFinish(ctx: SimResourceControllableContext) { + limit = 0.0 + duration = Long.MAX_VALUE + lastCommandTimestamp = ctx.clock.millis() } /* Comparable */ diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt index 2fe1b00f..d8ff87f9 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt @@ -27,53 +27,33 @@ package org.opendc.simulator.resources */ public interface SimResourceProviderLogic { /** - * This method is invoked when the resource is reported to idle until the specified [deadline]. + * This method is invoked when the consumer ask to consume the resource for the specified [duration]. * * @param ctx The context in which the provider runs. - * @param deadline The deadline that was requested by the resource consumer. - * @return The instant at which to resume the consumer. - */ - public fun onIdle(ctx: SimResourceControllableContext, deadline: Long): Long - - /** - * This method is invoked when the resource will be consumed until the specified amount of [work] was processed - * or [deadline] is reached. - * - * @param ctx The context in which the provider runs. - * @param work The amount of work that was requested by the resource consumer. + * @param now The virtual timestamp in milliseconds at which the update is occurring. * @param limit The limit on the work rate of the resource consumer. - * @param deadline The deadline that was requested by the resource consumer. - * @return The instant at which to resume the consumer. + * @param duration The duration of the consumption in milliseconds. + * @return The deadline of the resource consumption. */ - public fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long + public fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long { + return if (duration == Long.MAX_VALUE) { + return Long.MAX_VALUE + } else { + now + duration + } + } /** * This method is invoked when the progress of the resource consumer is materialized. * * @param ctx The context in which the provider runs. - * @param work The amount of work that was requested by the resource consumer. + * @param limit The limit on the work rate of the resource consumer. * @param willOvercommit A flag to indicate that the remaining work is overcommitted. */ - public fun onUpdate(ctx: SimResourceControllableContext, work: Double, willOvercommit: Boolean) {} + public fun onUpdate(ctx: SimResourceControllableContext, delta: Long, limit: Double, willOvercommit: Boolean) {} /** * This method is invoked when the resource consumer has finished. */ - public fun onFinish(ctx: SimResourceControllableContext) - - /** - * Compute the amount of work that was consumed over the specified [duration]. - * - * @param work The total size of the resource consumption. - * @param speed The speed of the resource provider. - * @param duration The duration from the start of the consumption until now. - * @return The amount of work that was consumed by the resource provider. - */ - public fun getConsumedWork(ctx: SimResourceControllableContext, work: Double, speed: Double, duration: Long): Double { - return if (duration > 0L) { - return (duration / 1000.0) * speed - } else { - work - } - } + public fun onFinish(ctx: SimResourceControllableContext) {} } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt index 2d53198a..10213f26 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt @@ -22,9 +22,6 @@ package org.opendc.simulator.resources -import kotlin.math.ceil -import kotlin.math.min - /** * A [SimResourceSource] represents a source for some resource that provides bounded processing capacity. * @@ -39,20 +36,13 @@ public class SimResourceSource( ) : SimAbstractResourceProvider(interpreter, parent, initialCapacity) { override fun createLogic(): SimResourceProviderLogic { return object : SimResourceProviderLogic { - override fun onIdle(ctx: SimResourceControllableContext, deadline: Long): Long { - return deadline - } - - override fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long { - return if (work.isInfinite()) { - Long.MAX_VALUE - } else { - min(deadline, ctx.clock.millis() + getDuration(work, speed)) - } - } - - override fun onUpdate(ctx: SimResourceControllableContext, work: Double, willOvercommit: Boolean) { - updateCounters(ctx, work, willOvercommit) + override fun onUpdate( + ctx: SimResourceControllableContext, + delta: Long, + limit: Double, + willOvercommit: Boolean + ) { + updateCounters(ctx, delta, limit, willOvercommit) } override fun onFinish(ctx: SimResourceControllableContext) { @@ -62,11 +52,4 @@ public class SimResourceSource( } override fun toString(): String = "SimResourceSource[capacity=$capacity]" - - /** - * Compute the duration that a resource consumption will take with the specified [speed]. - */ - private fun getDuration(work: Double, speed: Double): Long { - return ceil(work / speed * 1000).toLong() - } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt index cec27e1c..68bedbd9 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt @@ -100,19 +100,19 @@ public class SimResourceTransformer( } } - override fun onNext(ctx: SimResourceContext): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { val delegate = delegate if (!hasDelegateStarted) { start() } - updateCounters(ctx) + updateCounters(ctx, delta) return if (delegate != null) { - val command = transform(ctx, delegate.onNext(ctx)) + val command = transform(ctx, delegate.onNext(ctx, now, delta)) - _work = if (command is SimResourceCommand.Consume) command.work else 0.0 + _limit = if (command is SimResourceCommand.Consume) command.limit else 0.0 if (command == SimResourceCommand.Exit) { // Warning: resumption of the continuation might change the entire state of the forwarder. Make sure we @@ -124,12 +124,12 @@ public class SimResourceTransformer( if (isCoupled) SimResourceCommand.Exit else - onNext(ctx) + onNext(ctx, now, delta) } else { command } } else { - SimResourceCommand.Idle() + SimResourceCommand.Consume(0.0) } } @@ -180,19 +180,21 @@ public class SimResourceTransformer( } /** - * Counter to track the current submitted work. + * The requested speed. */ - private var _work = 0.0 + private var _limit: Double = 0.0 /** * Update the resource counters for the transformer. */ - private fun updateCounters(ctx: SimResourceContext) { + private fun updateCounters(ctx: SimResourceContext, delta: Long) { val counters = _counters - val remainingWork = ctx.remainingWork - counters.demand += _work - counters.actual += _work - remainingWork - counters.overcommit += remainingWork + val deltaS = delta / 1000.0 + val work = _limit * deltaS + val actualWork = ctx.speed * deltaS + counters.demand += work + counters.actual += actualWork + counters.overcommit += (work - actualWork) } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt index 4f4ebb14..1f8434b7 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt @@ -50,8 +50,8 @@ public class SimSpeedConsumerAdapter( callback(0.0) } - override fun onNext(ctx: SimResourceContext): SimResourceCommand { - return delegate.onNext(ctx) + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + return delegate.onNext(ctx, now, delta) } override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt index 2e94e1c1..e5173e5f 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt @@ -34,20 +34,11 @@ import org.opendc.simulator.resources.SimResourceEvent public class SimTraceConsumer(private val trace: Sequence) : SimResourceConsumer { private var iterator: Iterator? = null - override fun onNext(ctx: SimResourceContext): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { val iterator = checkNotNull(iterator) return if (iterator.hasNext()) { - val now = ctx.clock.millis() val fragment = iterator.next() - val work = (fragment.duration / 1000) * fragment.usage - val deadline = now + fragment.duration - - assert(deadline >= now) { "Deadline already passed" } - - if (work > 0.0) - SimResourceCommand.Consume(work, fragment.usage, deadline) - else - SimResourceCommand.Idle(deadline) + SimResourceCommand.Consume(fragment.usage, fragment.duration) } else { SimResourceCommand.Exit } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt index faa693c4..ae837043 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt @@ -25,6 +25,7 @@ package org.opendc.simulator.resources.consumer import org.opendc.simulator.resources.SimResourceCommand import org.opendc.simulator.resources.SimResourceConsumer import org.opendc.simulator.resources.SimResourceContext +import kotlin.math.roundToLong /** * A [SimResourceConsumer] that consumes the specified amount of work at the specified utilization. @@ -39,18 +40,19 @@ public class SimWorkConsumer( require(utilization > 0.0 && utilization <= 1.0) { "Utilization must be in (0, 1]" } } - private var isFirst = true + private var remainingWork = work - override fun onNext(ctx: SimResourceContext): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + val actualWork = ctx.speed * delta / 1000.0 val limit = ctx.capacity * utilization - val work = if (isFirst) { - isFirst = false - work - } else { - ctx.remainingWork - } - return if (work > 0.0) { - SimResourceCommand.Consume(work, limit) + + remainingWork -= actualWork + + val remainingWork = remainingWork + val duration = (remainingWork / limit * 1000).roundToLong() + + return if (duration > 0) { + SimResourceCommand.Consume(limit, duration) } else { SimResourceCommand.Exit } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt index b79998a3..a9507e52 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt @@ -57,12 +57,6 @@ internal class SimResourceContextImpl( } } - /** - * The amount of work still remaining at this instant. - */ - override val remainingWork: Double - get() = getRemainingWork(_clock.millis()) - /** * A flag to indicate the state of the context. */ @@ -87,8 +81,8 @@ internal class SimResourceContextImpl( * The current state of the resource context. */ private var _timestamp: Long = Long.MIN_VALUE - private var _work: Double = 0.0 private var _limit: Double = 0.0 + private var _duration: Long = Long.MAX_VALUE private var _deadline: Long = Long.MAX_VALUE /** @@ -178,7 +172,6 @@ internal class SimResourceContextImpl( } catch (cause: Throwable) { doFail(cause) } finally { - _remainingWorkFlush = Long.MIN_VALUE _timestamp = timestamp } } @@ -200,29 +193,25 @@ internal class SimResourceContextImpl( SimResourceState.Pending, SimResourceState.Stopped -> state SimResourceState.Active -> { val isInterrupted = _flag and FLAG_INTERRUPT != 0 - val remainingWork = getRemainingWork(timestamp) - val isConsume = _limit > 0.0 val reachedDeadline = _deadline <= timestamp + val delta = max(0, timestamp - _timestamp) // Update the resource counters only if there is some progress if (timestamp > _timestamp) { - logic.onUpdate(this, _work, reachedDeadline) + logic.onUpdate(this, delta, _limit, reachedDeadline) } // We should only continue processing the next command if: // 1. The resource consumption was finished. // 2. The resource capacity cannot satisfy the demand. // 3. The resource consumer should be interrupted (e.g., someone called .interrupt()) - if ((isConsume && remainingWork == 0.0) || reachedDeadline || isInterrupted) { - when (val command = consumer.onNext(this)) { - is SimResourceCommand.Idle -> interpretIdle(timestamp, command.deadline) - is SimResourceCommand.Consume -> interpretConsume(timestamp, command.work, command.limit, command.deadline) + if (reachedDeadline || isInterrupted) { + when (val command = consumer.onNext(this, timestamp, delta)) { + is SimResourceCommand.Consume -> interpretConsume(timestamp, command.limit, command.duration) is SimResourceCommand.Exit -> interpretExit() } - } else if (isConsume) { - interpretConsume(timestamp, remainingWork, _limit, _deadline) } else { - interpretIdle(timestamp, _deadline) + interpretConsume(timestamp, _limit, _duration - delta) } } } @@ -257,32 +246,15 @@ internal class SimResourceContextImpl( /** * Interpret the [SimResourceCommand.Consume] command. */ - private fun interpretConsume(now: Long, work: Double, limit: Double, deadline: Long): SimResourceState { - require(deadline >= now) { "Deadline already passed" } - + private fun interpretConsume(now: Long, limit: Double, duration: Long): SimResourceState { _speed = min(capacity, limit) - _work = work _limit = limit - _deadline = deadline + _duration = duration - val timestamp = logic.onConsume(this, work, limit, deadline) - scheduleUpdate(timestamp) + val timestamp = logic.onConsume(this, now, limit, duration) - return SimResourceState.Active - } - - /** - * Interpret the [SimResourceCommand.Idle] command. - */ - private fun interpretIdle(now: Long, deadline: Long): SimResourceState { - require(deadline >= now) { "Deadline already passed" } - - _speed = 0.0 - _work = 0.0 - _limit = 0.0 - _deadline = deadline + _deadline = timestamp - val timestamp = logic.onIdle(this, deadline) scheduleUpdate(timestamp) return SimResourceState.Active @@ -293,37 +265,13 @@ internal class SimResourceContextImpl( */ private fun interpretExit(): SimResourceState { _speed = 0.0 - _work = 0.0 _limit = 0.0 + _duration = Long.MAX_VALUE _deadline = Long.MAX_VALUE return SimResourceState.Stopped } - private var _remainingWork: Double = 0.0 - private var _remainingWorkFlush: Long = Long.MIN_VALUE - - /** - * Obtain the remaining work at the given timestamp. - */ - private fun getRemainingWork(now: Long): Double { - return if (_remainingWorkFlush < now) { - _remainingWorkFlush = now - computeRemainingWork(now).also { _remainingWork = it } - } else { - _remainingWork - } - } - - /** - * Compute the remaining work based on the current state. - */ - private fun computeRemainingWork(now: Long): Double { - return if (_work > 0.0) - max(0.0, _work - logic.getConsumedWork(this, _work, speed, now - _timestamp)) - else 0.0 - } - /** * Indicate that the capacity of the resource has changed. */ @@ -333,16 +281,11 @@ internal class SimResourceContextImpl( return } - val isThrottled = speed > capacity - interpreter.batch { // Inform the consumer of the capacity change. This might already trigger an interrupt. consumer.onEvent(this, SimResourceEvent.Capacity) - // Optimization: only invalidate context if the new capacity cannot satisfy the active resource command. - if (isThrottled) { - invalidate() - } + interrupt() } } diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt index 2f01a8c4..a9390553 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt @@ -38,7 +38,6 @@ import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl /** * Test suite for the [SimResourceAggregatorMaxMin] class. */ -@OptIn(ExperimentalCoroutinesApi::class) internal class SimResourceAggregatorMaxMinTest { @Test fun testSingleCapacity() = runBlockingSimulation { @@ -102,15 +101,15 @@ internal class SimResourceAggregatorMaxMinTest { sources.forEach(aggregator::addInput) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Consume(4.0, 4.0, 1000)) + every { consumer.onNext(any(), any(), any()) } + .returns(SimResourceCommand.Consume(4.0, 1000)) .andThen(SimResourceCommand.Exit) aggregator.consume(consumer) yield() assertEquals(1000, clock.millis()) - verify(exactly = 2) { consumer.onNext(any()) } + verify(exactly = 2) { consumer.onNext(any(), any(), any()) } } @Test @@ -125,8 +124,8 @@ internal class SimResourceAggregatorMaxMinTest { sources.forEach(aggregator::addInput) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Consume(1.0, 1.0)) + every { consumer.onNext(any(), any(), any()) } + .returns(SimResourceCommand.Consume(1.0, duration = 1000)) .andThenThrows(IllegalStateException("Test Exception")) assertThrows { aggregator.consume(consumer) } @@ -152,7 +151,7 @@ internal class SimResourceAggregatorMaxMinTest { sources[0].capacity = 0.5 } yield() - assertEquals(2334, clock.millis()) + assertEquals(2333, clock.millis()) } @Test @@ -173,7 +172,7 @@ internal class SimResourceAggregatorMaxMinTest { sources[0].capacity = 0.5 } yield() - assertEquals(1000, clock.millis()) + assertEquals(1167, clock.millis()) } @Test @@ -188,8 +187,8 @@ internal class SimResourceAggregatorMaxMinTest { sources.forEach(aggregator::addInput) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Consume(4.0, 4.0, 1000)) + every { consumer.onNext(any(), any(), any()) } + .returns(SimResourceCommand.Consume(4.0, 1000)) .andThen(SimResourceCommand.Exit) aggregator.consume(consumer) diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceCommandTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceCommandTest.kt index 02d456ff..9a52dc63 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceCommandTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceCommandTest.kt @@ -31,44 +31,23 @@ import org.junit.jupiter.api.assertThrows */ class SimResourceCommandTest { @Test - fun testZeroWork() { - assertThrows { - SimResourceCommand.Consume(0.0, 1.0) - } - } - - @Test - fun testNegativeWork() { - assertThrows { - SimResourceCommand.Consume(-1.0, 1.0) - } - } - - @Test - fun testZeroLimit() { + fun testNegativeLimit() { assertThrows { - SimResourceCommand.Consume(1.0, 0.0) + SimResourceCommand.Consume(-1.0, 1) } } @Test - fun testNegativeLimit() { + fun testNegativeDuration() { assertThrows { - SimResourceCommand.Consume(1.0, -1.0, 1) + SimResourceCommand.Consume(1.0, -1) } } @Test fun testConsumeCorrect() { assertDoesNotThrow { - SimResourceCommand.Consume(1.0, 1.0) - } - } - - @Test - fun testIdleCorrect() { - assertDoesNotThrow { - SimResourceCommand.Idle(1) + SimResourceCommand.Consume(1.0) } } } diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt index 6cb507ce..0cb95abb 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt @@ -32,19 +32,14 @@ import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl /** * A test suite for the [SimResourceContextImpl] class. */ -@OptIn(ExperimentalCoroutinesApi::class) class SimResourceContextTest { @Test fun testFlushWithoutCommand() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Consume(10.0, 1.0) andThen SimResourceCommand.Exit + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0) andThen SimResourceCommand.Exit - val logic = object : SimResourceProviderLogic { - override fun onIdle(ctx: SimResourceControllableContext, deadline: Long): Long = deadline - override fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long = deadline - override fun onFinish(ctx: SimResourceControllableContext) {} - } + val logic = object : SimResourceProviderLogic {} val context = SimResourceContextImpl(null, interpreter, consumer, logic) context.doUpdate(interpreter.clock.millis()) @@ -54,12 +49,11 @@ class SimResourceContextTest { fun testIntermediateFlush() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Consume(10.0, 1.0) andThen SimResourceCommand.Exit + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0) andThen SimResourceCommand.Exit val logic = spyk(object : SimResourceProviderLogic { - override fun onIdle(ctx: SimResourceControllableContext, deadline: Long): Long = deadline override fun onFinish(ctx: SimResourceControllableContext) {} - override fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long = deadline + override fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long = duration }) val context = spyk(SimResourceContextImpl(null, interpreter, consumer, logic)) @@ -74,13 +68,9 @@ class SimResourceContextTest { fun testIntermediateFlushIdle() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Idle(10) andThen SimResourceCommand.Exit + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(0.0, 10) andThen SimResourceCommand.Exit - val logic = spyk(object : SimResourceProviderLogic { - override fun onIdle(ctx: SimResourceControllableContext, deadline: Long): Long = deadline - override fun onFinish(ctx: SimResourceControllableContext) {} - override fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long = deadline - }) + val logic = spyk(object : SimResourceProviderLogic {}) val context = spyk(SimResourceContextImpl(null, interpreter, consumer, logic)) context.start() @@ -90,7 +80,7 @@ class SimResourceContextTest { context.invalidate() assertAll( - { verify(exactly = 2) { logic.onIdle(any(), any()) } }, + { verify(exactly = 2) { logic.onConsume(any(), any(), 0.0, any()) } }, { verify(exactly = 1) { logic.onFinish(any()) } } ) } @@ -99,13 +89,9 @@ class SimResourceContextTest { fun testDoubleStart() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Idle(10) andThen SimResourceCommand.Exit + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(0.0, 10) andThen SimResourceCommand.Exit - val logic = object : SimResourceProviderLogic { - override fun onIdle(ctx: SimResourceControllableContext, deadline: Long): Long = deadline - override fun onFinish(ctx: SimResourceControllableContext) {} - override fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long = deadline - } + val logic = object : SimResourceProviderLogic {} val context = SimResourceContextImpl(null, interpreter, consumer, logic) context.start() @@ -116,16 +102,12 @@ class SimResourceContextTest { } @Test - fun testIdempodentCapacityChange() = runBlockingSimulation { + fun testIdempotentCapacityChange() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Consume(10.0, 1.0) andThen SimResourceCommand.Exit + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0) andThen SimResourceCommand.Exit - val logic = object : SimResourceProviderLogic { - override fun onIdle(ctx: SimResourceControllableContext, deadline: Long): Long = deadline - override fun onFinish(ctx: SimResourceControllableContext) {} - override fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long = deadline - } + val logic = object : SimResourceProviderLogic {} val context = SimResourceContextImpl(null, interpreter, consumer, logic) context.capacity = 4200.0 @@ -139,15 +121,11 @@ class SimResourceContextTest { fun testFailureNoInfiniteLoop() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Exit + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Exit every { consumer.onEvent(any(), SimResourceEvent.Exit) } throws IllegalStateException("onEvent") every { consumer.onFailure(any(), any()) } throws IllegalStateException("onFailure") - val logic = spyk(object : SimResourceProviderLogic { - override fun onIdle(ctx: SimResourceControllableContext, deadline: Long): Long = deadline - override fun onFinish(ctx: SimResourceControllableContext) {} - override fun onConsume(ctx: SimResourceControllableContext, work: Double, limit: Double, deadline: Long): Long = deadline - }) + val logic = spyk(object : SimResourceProviderLogic {}) val context = SimResourceContextImpl(null, interpreter, consumer, logic) diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt index 4895544d..c310fad6 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt @@ -37,8 +37,7 @@ import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl /** * A test suite for the [SimResourceSource] class. */ -@OptIn(ExperimentalCoroutinesApi::class) -class SimResourceSourceTest { +internal class SimResourceSourceTest { @Test fun testSpeed() = runBlockingSimulation { val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) @@ -46,8 +45,8 @@ class SimResourceSourceTest { val provider = SimResourceSource(capacity, scheduler) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Consume(1000 * capacity, capacity)) + every { consumer.onNext(any(), any(), any()) } + .returns(SimResourceCommand.Consume(capacity, duration = 1000)) .andThen(SimResourceCommand.Exit) val res = mutableListOf() @@ -81,8 +80,8 @@ class SimResourceSourceTest { val provider = SimResourceSource(capacity, scheduler) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Consume(1000 * capacity, 2 * capacity)) + every { consumer.onNext(any(), any(), any()) } + .returns(SimResourceCommand.Consume(2 * capacity, duration = 1000)) .andThen(SimResourceCommand.Exit) val res = mutableListOf() @@ -104,7 +103,7 @@ class SimResourceSourceTest { val provider = SimResourceSource(capacity, scheduler) val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { return SimResourceCommand.Exit } @@ -133,11 +132,10 @@ class SimResourceSourceTest { } } - override fun onNext(ctx: SimResourceContext): SimResourceCommand { - assertEquals(0.0, ctx.remainingWork) + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { return if (isFirst) { isFirst = false - SimResourceCommand.Consume(4.0, 1.0) + SimResourceCommand.Consume(1.0, duration = 4000) } else { SimResourceCommand.Exit } @@ -175,8 +173,8 @@ class SimResourceSourceTest { val provider = SimResourceSource(capacity, scheduler) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Consume(1.0, 1.0)) + every { consumer.onNext(any(), any(), any()) } + .returns(SimResourceCommand.Consume(1.0, duration = 1000)) .andThenThrows(IllegalStateException()) assertThrows { @@ -191,8 +189,8 @@ class SimResourceSourceTest { val provider = SimResourceSource(capacity, scheduler) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Consume(1.0, 1.0)) + every { consumer.onNext(any(), any(), any()) } + .returns(SimResourceCommand.Consume(1.0)) .andThenThrows(IllegalStateException()) assertThrows { @@ -210,8 +208,8 @@ class SimResourceSourceTest { val provider = SimResourceSource(capacity, scheduler) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Consume(1.0, 1.0)) + every { consumer.onNext(any(), any(), any()) } + .returns(SimResourceCommand.Consume(1.0)) .andThenThrows(IllegalStateException()) launch { provider.consume(consumer) } @@ -228,8 +226,8 @@ class SimResourceSourceTest { val provider = SimResourceSource(capacity, scheduler) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Idle(clock.millis() + 500)) + every { consumer.onNext(any(), any(), any()) } + .returns(SimResourceCommand.Consume(0.0, 500)) .andThen(SimResourceCommand.Exit) provider.consume(consumer) @@ -246,28 +244,12 @@ class SimResourceSourceTest { val provider = SimResourceSource(capacity, scheduler) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Idle()) + every { consumer.onNext(any(), any(), any()) } + .returns(SimResourceCommand.Consume(0.0)) .andThenThrows(IllegalStateException()) provider.consume(consumer) } } } - - @Test - fun testIncorrectDeadline() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } - .returns(SimResourceCommand.Idle(2)) - .andThen(SimResourceCommand.Exit) - - delay(10) - - assertThrows { provider.consume(consumer) } - } } diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt index ad8d82e3..ad3b0f9f 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt @@ -24,7 +24,6 @@ package org.opendc.simulator.resources import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.yield import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -38,7 +37,6 @@ import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl /** * Test suite for the [SimResourceSwitchExclusive] class. */ -@OptIn(ExperimentalCoroutinesApi::class) internal class SimResourceSwitchExclusiveTest { /** * Test a trace workload. @@ -91,7 +89,7 @@ internal class SimResourceSwitchExclusiveTest { val duration = 5 * 60L * 1000 val workload = mockk(relaxUnitFun = true) - every { workload.onNext(any()) } returns SimResourceCommand.Consume(duration / 1000.0, 1.0) andThen SimResourceCommand.Exit + every { workload.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0, duration = duration) andThen SimResourceCommand.Exit val switch = SimResourceSwitchExclusive() val source = SimResourceSource(3200.0, scheduler) @@ -127,10 +125,10 @@ internal class SimResourceSwitchExclusiveTest { } } - override fun onNext(ctx: SimResourceContext): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { return if (isFirst) { isFirst = false - SimResourceCommand.Consume(duration / 1000.0, 1.0) + SimResourceCommand.Consume(1.0, duration = duration) } else { SimResourceCommand.Exit } @@ -161,9 +159,8 @@ internal class SimResourceSwitchExclusiveTest { fun testConcurrentWorkloadFails() = runBlockingSimulation { val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val duration = 5 * 60L * 1000 val workload = mockk(relaxUnitFun = true) - every { workload.onNext(any()) } returns SimResourceCommand.Consume(duration / 1000.0, 1.0) andThen SimResourceCommand.Exit + every { workload.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0) andThen SimResourceCommand.Exit val switch = SimResourceSwitchExclusive() val source = SimResourceSource(3200.0, scheduler) diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt index e4292ec0..d8f18e65 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt @@ -24,7 +24,6 @@ package org.opendc.simulator.resources import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.yield @@ -37,7 +36,6 @@ import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl /** * Test suite for the [SimResourceSwitch] implementations */ -@OptIn(ExperimentalCoroutinesApi::class) internal class SimResourceSwitchMaxMinTest { @Test fun testSmoke() = runBlockingSimulation { @@ -50,7 +48,7 @@ internal class SimResourceSwitchMaxMinTest { val provider = switch.newOutput() val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Consume(1.0, 1.0) andThen SimResourceCommand.Exit + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0, duration = 1000) andThen SimResourceCommand.Exit try { provider.consume(consumer) diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt index cf69b7b5..3780fd60 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt @@ -37,7 +37,6 @@ import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl /** * A test suite for the [SimResourceTransformer] class. */ -@OptIn(ExperimentalCoroutinesApi::class) internal class SimResourceTransformerTest { @Test fun testCancelImmediately() = runBlockingSimulation { @@ -48,7 +47,7 @@ internal class SimResourceTransformerTest { launch { source.consume(forwarder) } forwarder.consume(object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { return SimResourceCommand.Exit } }) @@ -68,10 +67,10 @@ internal class SimResourceTransformerTest { forwarder.consume(object : SimResourceConsumer { var isFirst = true - override fun onNext(ctx: SimResourceContext): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { return if (isFirst) { isFirst = false - SimResourceCommand.Consume(10.0, 1.0) + SimResourceCommand.Consume(1.0, duration = 10 * 1000L) } else { SimResourceCommand.Exit } @@ -86,7 +85,7 @@ internal class SimResourceTransformerTest { fun testState() = runBlockingSimulation { val forwarder = SimResourceForwarder() val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext): SimResourceCommand = SimResourceCommand.Exit + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand = SimResourceCommand.Exit } assertFalse(forwarder.isActive) @@ -108,7 +107,7 @@ internal class SimResourceTransformerTest { val forwarder = SimResourceForwarder() val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Exit + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Exit forwarder.startConsumer(consumer) forwarder.cancel() @@ -123,7 +122,7 @@ internal class SimResourceTransformerTest { val source = SimResourceSource(2000.0, scheduler) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Idle(10) + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(0.0, 10) source.startConsumer(forwarder) yield() @@ -142,7 +141,7 @@ internal class SimResourceTransformerTest { val source = SimResourceSource(2000.0, scheduler) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Idle(10) + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(0.0, 10) source.startConsumer(forwarder) yield() @@ -161,7 +160,7 @@ internal class SimResourceTransformerTest { val source = SimResourceSource(2000.0, scheduler) val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any()) } returns SimResourceCommand.Exit + every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Exit source.startConsumer(forwarder) forwarder.consume(consumer) @@ -200,7 +199,7 @@ internal class SimResourceTransformerTest { forwarder.consume(consumer) assertEquals(0, clock.millis()) - verify(exactly = 1) { consumer.onNext(any()) } + verify(exactly = 1) { consumer.onNext(any(), any(), any()) } } @Test diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimWorkConsumerTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimWorkConsumerTest.kt index 42648cf1..830f16d3 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimWorkConsumerTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimWorkConsumerTest.kt @@ -22,7 +22,6 @@ package org.opendc.simulator.resources -import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.opendc.simulator.core.runBlockingSimulation @@ -32,7 +31,6 @@ import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl /** * A test suite for the [SimWorkConsumer] class. */ -@OptIn(ExperimentalCoroutinesApi::class) internal class SimWorkConsumerTest { @Test fun testSmoke() = runBlockingSimulation { -- cgit v1.2.3 From 02fa44c0b116ff51c4cbe2876d8b2a225ed68553 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 28 Sep 2021 11:58:19 +0200 Subject: refactor(simulator): Add support for pushing flow from context This change adds a new method to `SimResourceContext` called `push` which allows users to change the requested flow rate directly without having to interrupt the consumer. --- .../org/opendc/simulator/compute/device/SimPsu.kt | 10 +- .../simulator/compute/workload/SimTraceWorkload.kt | 21 ++-- .../org/opendc/simulator/network/SimNetworkSink.kt | 2 +- .../kotlin/org/opendc/simulator/power/SimPdu.kt | 15 +-- .../kotlin/org/opendc/simulator/power/SimUps.kt | 15 +-- .../resources/SimAbstractResourceAggregator.kt | 53 ++++++--- .../resources/SimResourceAggregatorMaxMin.kt | 8 +- .../simulator/resources/SimResourceCommand.kt | 46 -------- .../simulator/resources/SimResourceConsumer.kt | 7 +- .../simulator/resources/SimResourceContext.kt | 14 ++- .../resources/SimResourceControllableContext.kt | 7 +- .../resources/SimResourceDistributorMaxMin.kt | 18 +-- .../simulator/resources/SimResourceTransformer.kt | 109 +++++++++++------- .../resources/consumer/SimSpeedConsumerAdapter.kt | 3 +- .../resources/consumer/SimTraceConsumer.kt | 9 +- .../resources/consumer/SimWorkConsumer.kt | 11 +- .../resources/impl/SimResourceContextImpl.kt | 123 +++++++++------------ .../resources/SimResourceAggregatorMaxMinTest.kt | 55 ++++----- .../simulator/resources/SimResourceCommandTest.kt | 53 --------- .../simulator/resources/SimResourceContextTest.kt | 90 ++++++++++++--- .../simulator/resources/SimResourceSourceTest.kt | 73 +++++------- .../resources/SimResourceSwitchExclusiveTest.kt | 17 ++- .../resources/SimResourceSwitchMaxMinTest.kt | 7 +- .../resources/SimResourceTransformerTest.kt | 44 +++++--- 24 files changed, 385 insertions(+), 425 deletions(-) delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCommand.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceCommandTest.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt index 34ac4418..6e6e590f 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt @@ -24,7 +24,6 @@ package org.opendc.simulator.compute.device import org.opendc.simulator.compute.power.PowerDriver import org.opendc.simulator.power.SimPowerInlet -import org.opendc.simulator.resources.SimResourceCommand import org.opendc.simulator.resources.SimResourceConsumer import org.opendc.simulator.resources.SimResourceContext import org.opendc.simulator.resources.SimResourceEvent @@ -83,13 +82,10 @@ public class SimPsu( } override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { val powerDraw = computePowerDraw(_driver?.computePower() ?: 0.0) - - return if (powerDraw > 0.0) - SimResourceCommand.Consume(powerDraw, Long.MAX_VALUE) - else - SimResourceCommand.Consume(0.0) + ctx.push(powerDraw) + return Long.MAX_VALUE } override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt index 527619bd..dd582bb2 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt @@ -24,7 +24,6 @@ package org.opendc.simulator.compute.workload import org.opendc.simulator.compute.SimMachineContext import org.opendc.simulator.compute.model.ProcessingUnit -import org.opendc.simulator.resources.SimResourceCommand import org.opendc.simulator.resources.SimResourceConsumer import org.opendc.simulator.resources.SimResourceContext import kotlin.math.min @@ -80,13 +79,20 @@ public class SimTraceWorkload(public val trace: Sequence, private val } private inner class Consumer(val cpu: ProcessingUnit) : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { - val fragment = pullFragment(now) ?: return SimResourceCommand.Exit + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + val fragment = pullFragment(now) + + if (fragment == null) { + ctx.close() + return Long.MAX_VALUE + } + val timestamp = fragment.timestamp + offset // Fragment is in the future if (timestamp > now) { - return SimResourceCommand.Consume(0.0, timestamp - now) + ctx.push(0.0) + return timestamp - now } val cores = min(cpu.node.coreCount, fragment.cores) @@ -97,10 +103,9 @@ public class SimTraceWorkload(public val trace: Sequence, private val val deadline = timestamp + fragment.duration val duration = deadline - now - return if (cpu.id < cores && usage > 0.0) - SimResourceCommand.Consume(usage, duration) - else - SimResourceCommand.Consume(0.0, duration) + ctx.push(if (cpu.id < cores && usage > 0.0) usage else 0.0) + + return duration } } diff --git a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt index f2dd8455..7db0f176 100644 --- a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt +++ b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt @@ -32,7 +32,7 @@ public class SimNetworkSink( public val capacity: Double ) : SimNetworkPort() { override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand = SimResourceCommand.Consume(0.0) + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long = Long.MAX_VALUE override fun toString(): String = "SimNetworkSink.Consumer" } diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt index e8839496..b0ea7f0a 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt @@ -47,16 +47,13 @@ public class SimPdu( public fun newOutlet(): Outlet = Outlet(distributor.newOutput()) override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer by distributor { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { - return when (val cmd = distributor.onNext(ctx, now, delta)) { - is SimResourceCommand.Consume -> { - val loss = computePowerLoss(cmd.limit) - val newLimit = cmd.limit + loss + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + val duration = distributor.onNext(ctx, now, delta) + val loss = computePowerLoss(ctx.demand) + val newLimit = ctx.demand + loss - SimResourceCommand.Consume(newLimit, cmd.duration) - } - else -> cmd - } + ctx.push(newLimit) + return duration } override fun toString(): String = "SimPdu.Consumer" diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt index 4c2beb68..59006dfc 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt @@ -55,16 +55,13 @@ public class SimUps( override fun onConnect(inlet: SimPowerInlet) { val consumer = inlet.createConsumer() aggregator.startConsumer(object : SimResourceConsumer by consumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { - return when (val cmd = consumer.onNext(ctx, now, delta)) { - is SimResourceCommand.Consume -> { - val loss = computePowerLoss(cmd.limit) - val newLimit = cmd.limit + loss + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + val duration = consumer.onNext(ctx, now, delta) + val loss = computePowerLoss(ctx.demand) + val newLimit = ctx.demand + loss - SimResourceCommand.Consume(newLimit, cmd.duration) - } - else -> cmd - } + ctx.push(newLimit) + return duration } }) } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt index da5c3257..8e0eb5f8 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt @@ -125,16 +125,21 @@ public abstract class SimAbstractResourceAggregator( /** * An input for the resource aggregator. */ - public interface Input { + public interface Input : AutoCloseable { /** * The [SimResourceContext] associated with the input. */ public val ctx: SimResourceContext /** - * Push the specified [SimResourceCommand] to the input. + * Push to this input with the specified [limit] and [duration]. */ - public fun push(command: SimResourceCommand) + public fun push(limit: Double, duration: Long) + + /** + * Close the input for further input. + */ + public override fun close() } /** @@ -151,7 +156,12 @@ public abstract class SimAbstractResourceAggregator( /** * The resource command to run next. */ - private var command: SimResourceCommand? = null + private var _duration: Long = Long.MAX_VALUE + + /** + * A flag to indicate that the consumer should flush. + */ + private var _isPushed = false private fun updateCapacity() { // Adjust capacity of output resource @@ -159,25 +169,34 @@ public abstract class SimAbstractResourceAggregator( } /* Input */ - override fun push(command: SimResourceCommand) { - this.command = command - _ctx?.interrupt() + override fun push(limit: Double, duration: Long) { + _duration = duration + val ctx = _ctx + if (ctx != null) { + ctx.push(limit) + ctx.interrupt() + } + _isPushed = true + } + + override fun close() { + _duration = Long.MAX_VALUE + _isPushed = true + _ctx?.close() } /* SimResourceConsumer */ - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { - var next = command + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + var next = _duration - return if (next != null) { - this.command = null - next - } else { + if (!_isPushed) { _output.flush() - - next = command - this.command = null - next ?: SimResourceCommand.Consume(0.0) + next = _duration } + + _isPushed = false + _duration = Long.MAX_VALUE + return next } override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt index 537be1b5..b258a368 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt @@ -41,11 +41,7 @@ public class SimResourceAggregatorMaxMin( val fraction = inputCapacity / capacity val grantedSpeed = limit * fraction - val command = if (grantedSpeed > 0.0) - SimResourceCommand.Consume(grantedSpeed, duration) - else - SimResourceCommand.Consume(0.0, duration) - input.push(command) + input.push(grantedSpeed, duration) } } @@ -53,7 +49,7 @@ public class SimResourceAggregatorMaxMin( val iterator = consumers.iterator() for (input in iterator) { iterator.remove() - input.push(SimResourceCommand.Exit) + input.close() } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCommand.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCommand.kt deleted file mode 100644 index 4a980071..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCommand.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.resources - -/** - * A SimResourceCommand communicates to a resource how it is consumed by a [SimResourceConsumer]. - */ -public sealed class SimResourceCommand { - /** - * A request to the resource to perform work for the specified [duration]. - * - * @param limit The maximum amount of work to be processed per second. - * @param duration The duration of the resource consumption in milliseconds. - */ - public data class Consume(val limit: Double, val duration: Long = Long.MAX_VALUE) : SimResourceCommand() { - init { - require(limit >= 0.0) { "Negative limit is not allowed" } - require(duration >= 0) { "Duration must be positive" } - } - } - - /** - * An indication to the resource that the consumer has finished. - */ - public object Exit : SimResourceCommand() -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt index 4d1d2c32..0b25358a 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt @@ -30,15 +30,14 @@ package org.opendc.simulator.resources */ public interface SimResourceConsumer { /** - * This method is invoked when a resource asks for the next [command][SimResourceCommand] to process, either because - * the resource finished processing, reached its deadline or was interrupted. + * This method is invoked when the resource provider is pulling this resource consumer. * * @param ctx The execution context in which the consumer runs. * @param now The virtual timestamp in milliseconds at which the update is occurring. * @param delta The virtual duration between this call and the last call in milliseconds. - * @return The next command that the resource should execute. + * @return The duration after which the resource consumer should be pulled again. */ - public fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand + public fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long /** * This method is invoked when an event has occurred. diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt index f28b43d0..225cae0b 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt @@ -28,7 +28,7 @@ import java.time.Clock * The execution context in which a [SimResourceConsumer] runs. It facilitates the communication and control between a * resource and a resource consumer. */ -public interface SimResourceContext { +public interface SimResourceContext : AutoCloseable { /** * The virtual clock tracking simulation time. */ @@ -53,4 +53,16 @@ public interface SimResourceContext { * Ask the resource provider to interrupt its resource. */ public fun interrupt() + + /** + * Push the given flow to this context. + * + * @param rate The rate of the flow to push. + */ + public fun push(rate: Double) + + /** + * Stop the resource context. + */ + public override fun close() } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt index ceaca39a..ba52b597 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt @@ -27,7 +27,7 @@ package org.opendc.simulator.resources * * This interface is used by resource providers to control the resource context. */ -public interface SimResourceControllableContext : SimResourceContext, AutoCloseable { +public interface SimResourceControllableContext : SimResourceContext { /** * The state of the resource context. */ @@ -43,11 +43,6 @@ public interface SimResourceControllableContext : SimResourceContext, AutoClosea */ public fun start() - /** - * Stop the resource context. - */ - public override fun close() - /** * Invalidate the resource context's state. * diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt index d23c7dbb..eac58410 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt @@ -53,11 +53,6 @@ public class SimResourceDistributorMaxMin( */ private val activeOutputs: MutableList = mutableListOf() - /** - * The total amount of work allocated to be executed. - */ - private var totalAllocatedWork = 0.0 - /** * The total allocated speed for the output resources. */ @@ -97,7 +92,7 @@ public class SimResourceDistributorMaxMin( } /* SimResourceConsumer */ - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { return doNext(ctx, now) } @@ -137,10 +132,10 @@ public class SimResourceDistributorMaxMin( /** * Schedule the work of the outputs. */ - private fun doNext(ctx: SimResourceContext, now: Long): SimResourceCommand { + private fun doNext(ctx: SimResourceContext, now: Long): Long { // If there is no work yet, mark the input as idle. if (activeOutputs.isEmpty()) { - return SimResourceCommand.Consume(0.0) + return Long.MAX_VALUE } val capacity = ctx.capacity @@ -196,14 +191,11 @@ public class SimResourceDistributorMaxMin( } this.totalRequestedSpeed = totalRequestedSpeed - this.totalAllocatedWork = totalAllocatedWork val totalAllocatedSpeed = capacity - availableSpeed this.totalAllocatedSpeed = totalAllocatedSpeed - return if (totalAllocatedWork > 0.0 && totalAllocatedSpeed > 0.0) - SimResourceCommand.Consume(totalAllocatedSpeed, duration) - else - SimResourceCommand.Consume(0.0, duration) + ctx.push(totalAllocatedSpeed) + return duration } private fun updateCapacity(ctx: SimResourceContext) { diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt index 68bedbd9..f12ef9f1 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt @@ -23,6 +23,7 @@ package org.opendc.simulator.resources import org.opendc.simulator.resources.impl.SimResourceCountersImpl +import java.time.Clock /** * A [SimResourceFlow] that transforms the resource commands emitted by the resource commands to the resource provider. @@ -32,13 +33,8 @@ import org.opendc.simulator.resources.impl.SimResourceCountersImpl */ public class SimResourceTransformer( private val isCoupled: Boolean = false, - private val transform: (SimResourceContext, SimResourceCommand) -> SimResourceCommand + private val transform: (SimResourceContext, Long) -> Long ) : SimResourceFlow, AutoCloseable { - /** - * The [SimResourceContext] in which the forwarder runs. - */ - private var ctx: SimResourceContext? = null - /** * The delegate [SimResourceConsumer]. */ @@ -49,17 +45,63 @@ public class SimResourceTransformer( */ private var hasDelegateStarted: Boolean = false + /** + * The exposed [SimResourceContext]. + */ + private val ctx = object : SimResourceContext { + override val clock: Clock + get() = _ctx!!.clock + + override val capacity: Double + get() = _ctx?.capacity ?: 0.0 + + override val demand: Double + get() = _ctx?.demand ?: 0.0 + + override val speed: Double + get() = _ctx?.speed ?: 0.0 + + override fun interrupt() { + _ctx?.interrupt() + } + + override fun push(rate: Double) { + _ctx?.push(rate) + _limit = rate + } + + override fun close() { + val delegate = checkNotNull(delegate) { "Delegate not active" } + + // Warning: resumption of the continuation might change the entire state of the forwarder. Make sure we + // reset beforehand the existing state and check whether it has been updated afterwards + reset() + + delegate.onEvent(this, SimResourceEvent.Exit) + + if (isCoupled) + _ctx?.close() + else + _ctx?.push(0.0) + } + } + + /** + * The [SimResourceContext] in which the forwarder runs. + */ + private var _ctx: SimResourceContext? = null + override val isActive: Boolean get() = delegate != null override val capacity: Double - get() = ctx?.capacity ?: 0.0 + get() = ctx.capacity override val speed: Double - get() = ctx?.speed ?: 0.0 + get() = ctx.speed override val demand: Double - get() = ctx?.demand ?: 0.0 + get() = ctx.demand override val counters: SimResourceCounters get() = _counters @@ -75,32 +117,32 @@ public class SimResourceTransformer( } override fun interrupt() { - ctx?.interrupt() + ctx.interrupt() } override fun cancel() { val delegate = delegate - val ctx = ctx + val ctx = _ctx if (delegate != null) { this.delegate = null if (ctx != null) { - delegate.onEvent(ctx, SimResourceEvent.Exit) + delegate.onEvent(this.ctx, SimResourceEvent.Exit) } } } override fun close() { - val ctx = ctx + val ctx = _ctx if (ctx != null) { - this.ctx = null + this._ctx = null ctx.interrupt() } } - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { val delegate = delegate if (!hasDelegateStarted) { @@ -110,54 +152,39 @@ public class SimResourceTransformer( updateCounters(ctx, delta) return if (delegate != null) { - val command = transform(ctx, delegate.onNext(ctx, now, delta)) - - _limit = if (command is SimResourceCommand.Consume) command.limit else 0.0 - - if (command == SimResourceCommand.Exit) { - // Warning: resumption of the continuation might change the entire state of the forwarder. Make sure we - // reset beforehand the existing state and check whether it has been updated afterwards - reset() - - delegate.onEvent(ctx, SimResourceEvent.Exit) - - if (isCoupled) - SimResourceCommand.Exit - else - onNext(ctx, now, delta) - } else { - command - } + val duration = transform(ctx, delegate.onNext(this.ctx, now, delta)) + _limit = ctx.demand + duration } else { - SimResourceCommand.Consume(0.0) + Long.MAX_VALUE } } override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { when (event) { SimResourceEvent.Start -> { - this.ctx = ctx + _ctx = ctx } SimResourceEvent.Exit -> { - this.ctx = null + _ctx = null val delegate = delegate if (delegate != null) { reset() - delegate.onEvent(ctx, SimResourceEvent.Exit) + delegate.onEvent(this.ctx, SimResourceEvent.Exit) } } - else -> delegate?.onEvent(ctx, event) + else -> delegate?.onEvent(this.ctx, event) } } override fun onFailure(ctx: SimResourceContext, cause: Throwable) { - this.ctx = null + _ctx = null val delegate = delegate if (delegate != null) { reset() - delegate.onFailure(ctx, cause) + delegate.onFailure(this.ctx, cause) } } @@ -166,7 +193,7 @@ public class SimResourceTransformer( */ private fun start() { val delegate = delegate ?: return - delegate.onEvent(checkNotNull(ctx), SimResourceEvent.Start) + delegate.onEvent(checkNotNull(_ctx), SimResourceEvent.Start) hasDelegateStarted = true } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt index 1f8434b7..46885640 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt @@ -22,7 +22,6 @@ package org.opendc.simulator.resources.consumer -import org.opendc.simulator.resources.SimResourceCommand import org.opendc.simulator.resources.SimResourceConsumer import org.opendc.simulator.resources.SimResourceContext import org.opendc.simulator.resources.SimResourceEvent @@ -50,7 +49,7 @@ public class SimSpeedConsumerAdapter( callback(0.0) } - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { return delegate.onNext(ctx, now, delta) } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt index e5173e5f..ad6b0108 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt @@ -22,7 +22,6 @@ package org.opendc.simulator.resources.consumer -import org.opendc.simulator.resources.SimResourceCommand import org.opendc.simulator.resources.SimResourceConsumer import org.opendc.simulator.resources.SimResourceContext import org.opendc.simulator.resources.SimResourceEvent @@ -34,13 +33,15 @@ import org.opendc.simulator.resources.SimResourceEvent public class SimTraceConsumer(private val trace: Sequence) : SimResourceConsumer { private var iterator: Iterator? = null - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { val iterator = checkNotNull(iterator) return if (iterator.hasNext()) { val fragment = iterator.next() - SimResourceCommand.Consume(fragment.usage, fragment.duration) + ctx.push(fragment.usage) + fragment.duration } else { - SimResourceCommand.Exit + ctx.close() + Long.MAX_VALUE } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt index ae837043..bf76711f 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt @@ -22,7 +22,6 @@ package org.opendc.simulator.resources.consumer -import org.opendc.simulator.resources.SimResourceCommand import org.opendc.simulator.resources.SimResourceConsumer import org.opendc.simulator.resources.SimResourceContext import kotlin.math.roundToLong @@ -37,12 +36,12 @@ public class SimWorkConsumer( init { require(work >= 0.0) { "Work must be positive" } - require(utilization > 0.0 && utilization <= 1.0) { "Utilization must be in (0, 1]" } + require(utilization > 0.0) { "Utilization must be positive" } } private var remainingWork = work - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { val actualWork = ctx.speed * delta / 1000.0 val limit = ctx.capacity * utilization @@ -52,9 +51,11 @@ public class SimWorkConsumer( val duration = (remainingWork / limit * 1000).roundToLong() return if (duration > 0) { - SimResourceCommand.Consume(limit, duration) + ctx.push(limit) + duration } else { - SimResourceCommand.Exit + ctx.close() + Long.MAX_VALUE } } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt index a9507e52..d7ea0043 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt @@ -85,6 +85,11 @@ internal class SimResourceContextImpl( private var _duration: Long = Long.MAX_VALUE private var _deadline: Long = Long.MAX_VALUE + /** + * A flag to indicate that an update is active. + */ + private var _updateActive = false + /** * The update flag indicating why the update was triggered. */ @@ -108,7 +113,9 @@ internal class SimResourceContextImpl( if (_state != SimResourceState.Stopped) { interpreter.batch { _state = SimResourceState.Stopped - doStop() + if (!_updateActive) { + doStop() + } } } } @@ -139,6 +146,11 @@ internal class SimResourceContextImpl( interpreter.scheduleSync(this) } + override fun push(rate: Double) { + _speed = min(capacity, rate) + _limit = rate + } + /** * Determine whether the state of the resource context should be updated. */ @@ -151,14 +163,49 @@ internal class SimResourceContextImpl( * Update the state of the resource context. */ fun doUpdate(timestamp: Long) { + val oldState = _state + if (oldState != SimResourceState.Active) { + return + } + + _updateActive = true + + val flag = _flag + val isInterrupted = flag and FLAG_INTERRUPT != 0 + val reachedDeadline = _deadline <= timestamp + val delta = max(0, timestamp - _timestamp) + try { - val oldState = _state - val newState = doUpdate(timestamp, oldState) - _state = newState + // Update the resource counters only if there is some progress + if (timestamp > _timestamp) { + logic.onUpdate(this, delta, _limit, reachedDeadline) + } + + // We should only continue processing the next command if: + // 1. The resource consumption was finished. + // 2. The resource consumer should be interrupted (e.g., someone called .interrupt()) + val duration = if (reachedDeadline || isInterrupted) { + consumer.onNext(this, timestamp, delta) + } else { + _deadline - timestamp + } + + // Reset update flags _flag = 0 - when (newState) { + when (_state) { + SimResourceState.Active -> { + val limit = _limit + push(limit) + _duration = duration + + val target = logic.onConsume(this, timestamp, limit, duration) + + _deadline = target + + scheduleUpdate(target) + } SimResourceState.Pending -> if (oldState != SimResourceState.Pending) { throw IllegalStateException("Illegal transition to pending state") @@ -167,12 +214,12 @@ internal class SimResourceContextImpl( if (oldState != SimResourceState.Stopped) { doStop() } - else -> {} } } catch (cause: Throwable) { doFail(cause) } finally { _timestamp = timestamp + _updateActive = false } } @@ -184,39 +231,6 @@ internal class SimResourceContextImpl( override fun toString(): String = "SimResourceContextImpl[capacity=$capacity]" - /** - * Update the state of the resource context. - */ - private fun doUpdate(timestamp: Long, state: SimResourceState): SimResourceState { - return when (state) { - // Resource context is not active, so its state will not update - SimResourceState.Pending, SimResourceState.Stopped -> state - SimResourceState.Active -> { - val isInterrupted = _flag and FLAG_INTERRUPT != 0 - val reachedDeadline = _deadline <= timestamp - val delta = max(0, timestamp - _timestamp) - - // Update the resource counters only if there is some progress - if (timestamp > _timestamp) { - logic.onUpdate(this, delta, _limit, reachedDeadline) - } - - // We should only continue processing the next command if: - // 1. The resource consumption was finished. - // 2. The resource capacity cannot satisfy the demand. - // 3. The resource consumer should be interrupted (e.g., someone called .interrupt()) - if (reachedDeadline || isInterrupted) { - when (val command = consumer.onNext(this, timestamp, delta)) { - is SimResourceCommand.Consume -> interpretConsume(timestamp, command.limit, command.duration) - is SimResourceCommand.Exit -> interpretExit() - } - } else { - interpretConsume(timestamp, _limit, _duration - delta) - } - } - } - } - /** * Stop the resource context. */ @@ -226,6 +240,8 @@ internal class SimResourceContextImpl( logic.onFinish(this) } catch (cause: Throwable) { doFail(cause) + } finally { + _deadline = Long.MAX_VALUE } } @@ -243,35 +259,6 @@ internal class SimResourceContextImpl( logic.onFinish(this) } - /** - * Interpret the [SimResourceCommand.Consume] command. - */ - private fun interpretConsume(now: Long, limit: Double, duration: Long): SimResourceState { - _speed = min(capacity, limit) - _limit = limit - _duration = duration - - val timestamp = logic.onConsume(this, now, limit, duration) - - _deadline = timestamp - - scheduleUpdate(timestamp) - - return SimResourceState.Active - } - - /** - * Interpret the [SimResourceCommand.Exit] command. - */ - private fun interpretExit(): SimResourceState { - _speed = 0.0 - _limit = 0.0 - _duration = Long.MAX_VALUE - _deadline = Long.MAX_VALUE - - return SimResourceState.Stopped - } - /** * Indicate that the capacity of the resource has changed. */ diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt index a9390553..f4ea5fe8 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt @@ -22,14 +22,12 @@ package org.opendc.simulator.resources -import io.mockk.every -import io.mockk.mockk +import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.* import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll -import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation import org.opendc.simulator.resources.consumer.SimSpeedConsumerAdapter import org.opendc.simulator.resources.consumer.SimWorkConsumer @@ -100,10 +98,17 @@ internal class SimResourceAggregatorMaxMinTest { ) sources.forEach(aggregator::addInput) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } - .returns(SimResourceCommand.Consume(4.0, 1000)) - .andThen(SimResourceCommand.Exit) + val consumer = spyk(object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + return if (now == 0L) { + ctx.push(4.0) + 1000 + } else { + ctx.close() + Long.MAX_VALUE + } + } + }) aggregator.consume(consumer) yield() @@ -112,27 +117,6 @@ internal class SimResourceAggregatorMaxMinTest { verify(exactly = 2) { consumer.onNext(any(), any(), any()) } } - @Test - fun testException() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val aggregator = SimResourceAggregatorMaxMin(scheduler) - val sources = listOf( - SimResourceSource(1.0, scheduler), - SimResourceSource(1.0, scheduler) - ) - sources.forEach(aggregator::addInput) - - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } - .returns(SimResourceCommand.Consume(1.0, duration = 1000)) - .andThenThrows(IllegalStateException("Test Exception")) - - assertThrows { aggregator.consume(consumer) } - yield() - assertFalse(sources[0].isActive) - } - @Test fun testAdjustCapacity() = runBlockingSimulation { val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) @@ -186,10 +170,17 @@ internal class SimResourceAggregatorMaxMinTest { ) sources.forEach(aggregator::addInput) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } - .returns(SimResourceCommand.Consume(4.0, 1000)) - .andThen(SimResourceCommand.Exit) + val consumer = object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + return if (now == 0L) { + ctx.push(4.0) + 1000 + } else { + ctx.close() + Long.MAX_VALUE + } + } + } aggregator.consume(consumer) yield() diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceCommandTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceCommandTest.kt deleted file mode 100644 index 9a52dc63..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceCommandTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.resources - -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows - -/** - * Test suite for [SimResourceCommand]. - */ -class SimResourceCommandTest { - @Test - fun testNegativeLimit() { - assertThrows { - SimResourceCommand.Consume(-1.0, 1) - } - } - - @Test - fun testNegativeDuration() { - assertThrows { - SimResourceCommand.Consume(1.0, -1) - } - } - - @Test - fun testConsumeCorrect() { - assertDoesNotThrow { - SimResourceCommand.Consume(1.0) - } - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt index 0cb95abb..4e57f598 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt @@ -36,8 +36,17 @@ class SimResourceContextTest { @Test fun testFlushWithoutCommand() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0) andThen SimResourceCommand.Exit + val consumer = object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + return if (now == 0L) { + ctx.push(1.0) + 1000 + } else { + ctx.close() + Long.MAX_VALUE + } + } + } val logic = object : SimResourceProviderLogic {} val context = SimResourceContextImpl(null, interpreter, consumer, logic) @@ -48,14 +57,23 @@ class SimResourceContextTest { @Test fun testIntermediateFlush() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0) andThen SimResourceCommand.Exit + val consumer = object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + return if (now == 0L) { + ctx.push(4.0) + 1000 + } else { + ctx.close() + Long.MAX_VALUE + } + } + } val logic = spyk(object : SimResourceProviderLogic { override fun onFinish(ctx: SimResourceControllableContext) {} override fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long = duration }) - val context = spyk(SimResourceContextImpl(null, interpreter, consumer, logic)) + val context = SimResourceContextImpl(null, interpreter, consumer, logic) context.start() delay(1) // Delay 1 ms to prevent hitting the fast path @@ -67,11 +85,20 @@ class SimResourceContextTest { @Test fun testIntermediateFlushIdle() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(0.0, 10) andThen SimResourceCommand.Exit + val consumer = object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + return if (now == 0L) { + ctx.push(0.0) + 10 + } else { + ctx.close() + Long.MAX_VALUE + } + } + } val logic = spyk(object : SimResourceProviderLogic {}) - val context = spyk(SimResourceContextImpl(null, interpreter, consumer, logic)) + val context = SimResourceContextImpl(null, interpreter, consumer, logic) context.start() delay(5) @@ -88,8 +115,17 @@ class SimResourceContextTest { @Test fun testDoubleStart() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(0.0, 10) andThen SimResourceCommand.Exit + val consumer = object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + return if (now == 0L) { + ctx.push(0.0) + 1000 + } else { + ctx.close() + Long.MAX_VALUE + } + } + } val logic = object : SimResourceProviderLogic {} val context = SimResourceContextImpl(null, interpreter, consumer, logic) @@ -104,8 +140,17 @@ class SimResourceContextTest { @Test fun testIdempotentCapacityChange() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0) andThen SimResourceCommand.Exit + val consumer = spyk(object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + return if (now == 0L) { + ctx.push(1.0) + 1000 + } else { + ctx.close() + Long.MAX_VALUE + } + } + }) val logic = object : SimResourceProviderLogic {} @@ -120,12 +165,23 @@ class SimResourceContextTest { @Test fun testFailureNoInfiniteLoop() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Exit - every { consumer.onEvent(any(), SimResourceEvent.Exit) } throws IllegalStateException("onEvent") - every { consumer.onFailure(any(), any()) } throws IllegalStateException("onFailure") - val logic = spyk(object : SimResourceProviderLogic {}) + val consumer = spyk(object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + ctx.close() + return Long.MAX_VALUE + } + + override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { + if (event == SimResourceEvent.Exit) throw IllegalStateException("onEvent") + } + + override fun onFailure(ctx: SimResourceContext, cause: Throwable) { + throw IllegalStateException("onFailure") + } + }) + + val logic = object : SimResourceProviderLogic {} val context = SimResourceContextImpl(null, interpreter, consumer, logic) diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt index c310fad6..e055daf7 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt @@ -44,10 +44,7 @@ internal class SimResourceSourceTest { val capacity = 4200.0 val provider = SimResourceSource(capacity, scheduler) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } - .returns(SimResourceCommand.Consume(capacity, duration = 1000)) - .andThen(SimResourceCommand.Exit) + val consumer = SimWorkConsumer(4200.0, 1.0) val res = mutableListOf() val adapter = SimSpeedConsumerAdapter(consumer, res::add) @@ -79,10 +76,7 @@ internal class SimResourceSourceTest { val capacity = 4200.0 val provider = SimResourceSource(capacity, scheduler) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } - .returns(SimResourceCommand.Consume(2 * capacity, duration = 1000)) - .andThen(SimResourceCommand.Exit) + val consumer = SimWorkConsumer(capacity, 2.0) val res = mutableListOf() val adapter = SimSpeedConsumerAdapter(consumer, res::add) @@ -103,8 +97,9 @@ internal class SimResourceSourceTest { val provider = SimResourceSource(capacity, scheduler) val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { - return SimResourceCommand.Exit + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + ctx.close() + return Long.MAX_VALUE } override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { @@ -132,12 +127,14 @@ internal class SimResourceSourceTest { } } - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { return if (isFirst) { isFirst = false - SimResourceCommand.Consume(1.0, duration = 4000) + ctx.push(1.0) + 4000 } else { - SimResourceCommand.Exit + ctx.close() + Long.MAX_VALUE } } } @@ -172,10 +169,19 @@ internal class SimResourceSourceTest { val capacity = 4200.0 val provider = SimResourceSource(capacity, scheduler) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } - .returns(SimResourceCommand.Consume(1.0, duration = 1000)) - .andThenThrows(IllegalStateException()) + val consumer = object : SimResourceConsumer { + var isFirst = true + + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + return if (isFirst) { + isFirst = false + ctx.push(1.0) + 1000 + } else { + throw IllegalStateException() + } + } + } assertThrows { provider.consume(consumer) @@ -188,10 +194,7 @@ internal class SimResourceSourceTest { val capacity = 4200.0 val provider = SimResourceSource(capacity, scheduler) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } - .returns(SimResourceCommand.Consume(1.0)) - .andThenThrows(IllegalStateException()) + val consumer = SimWorkConsumer(capacity, 1.0) assertThrows { coroutineScope { @@ -207,30 +210,13 @@ internal class SimResourceSourceTest { val capacity = 4200.0 val provider = SimResourceSource(capacity, scheduler) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } - .returns(SimResourceCommand.Consume(1.0)) - .andThenThrows(IllegalStateException()) + val consumer = SimWorkConsumer(capacity, 1.0) launch { provider.consume(consumer) } delay(500) provider.cancel() - assertEquals(500, clock.millis()) - } - - @Test - fun testIdle() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } - .returns(SimResourceCommand.Consume(0.0, 500)) - .andThen(SimResourceCommand.Exit) - - provider.consume(consumer) + yield() assertEquals(500, clock.millis()) } @@ -243,10 +229,9 @@ internal class SimResourceSourceTest { val capacity = 4200.0 val provider = SimResourceSource(capacity, scheduler) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } - .returns(SimResourceCommand.Consume(0.0)) - .andThenThrows(IllegalStateException()) + val consumer = object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long = Long.MAX_VALUE + } provider.consume(consumer) } diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt index ad3b0f9f..9f86dc0d 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt @@ -22,8 +22,6 @@ package org.opendc.simulator.resources -import io.mockk.every -import io.mockk.mockk import kotlinx.coroutines.yield import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -32,6 +30,7 @@ import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation import org.opendc.simulator.resources.consumer.SimSpeedConsumerAdapter import org.opendc.simulator.resources.consumer.SimTraceConsumer +import org.opendc.simulator.resources.consumer.SimWorkConsumer import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl /** @@ -88,8 +87,7 @@ internal class SimResourceSwitchExclusiveTest { val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) val duration = 5 * 60L * 1000 - val workload = mockk(relaxUnitFun = true) - every { workload.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0, duration = duration) andThen SimResourceCommand.Exit + val workload = SimWorkConsumer(duration * 3.2, 1.0) val switch = SimResourceSwitchExclusive() val source = SimResourceSource(3200.0, scheduler) @@ -125,12 +123,14 @@ internal class SimResourceSwitchExclusiveTest { } } - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { return if (isFirst) { isFirst = false - SimResourceCommand.Consume(1.0, duration = duration) + ctx.push(1.0) + duration } else { - SimResourceCommand.Exit + ctx.close() + Long.MAX_VALUE } } } @@ -159,9 +159,6 @@ internal class SimResourceSwitchExclusiveTest { fun testConcurrentWorkloadFails() = runBlockingSimulation { val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val workload = mockk(relaxUnitFun = true) - every { workload.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0) andThen SimResourceCommand.Exit - val switch = SimResourceSwitchExclusive() val source = SimResourceSource(3200.0, scheduler) diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt index d8f18e65..ba0d66ff 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt @@ -22,8 +22,6 @@ package org.opendc.simulator.resources -import io.mockk.every -import io.mockk.mockk import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.yield @@ -31,6 +29,7 @@ import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.assertEquals import org.opendc.simulator.core.runBlockingSimulation import org.opendc.simulator.resources.consumer.SimTraceConsumer +import org.opendc.simulator.resources.consumer.SimWorkConsumer import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl /** @@ -46,9 +45,7 @@ internal class SimResourceSwitchMaxMinTest { sources.forEach { switch.addInput(it) } val provider = switch.newOutput() - - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(1.0, duration = 1000) andThen SimResourceCommand.Exit + val consumer = SimWorkConsumer(2000.0, 1.0) try { provider.consume(consumer) diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt index 3780fd60..fc43c3da 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt @@ -22,8 +22,6 @@ package org.opendc.simulator.resources -import io.mockk.every -import io.mockk.mockk import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.* @@ -47,8 +45,9 @@ internal class SimResourceTransformerTest { launch { source.consume(forwarder) } forwarder.consume(object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { - return SimResourceCommand.Exit + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + ctx.close() + return Long.MAX_VALUE } }) @@ -67,12 +66,14 @@ internal class SimResourceTransformerTest { forwarder.consume(object : SimResourceConsumer { var isFirst = true - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { return if (isFirst) { isFirst = false - SimResourceCommand.Consume(1.0, duration = 10 * 1000L) + ctx.push(1.0) + 10 * 1000 } else { - SimResourceCommand.Exit + ctx.close() + Long.MAX_VALUE } } }) @@ -85,7 +86,10 @@ internal class SimResourceTransformerTest { fun testState() = runBlockingSimulation { val forwarder = SimResourceForwarder() val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): SimResourceCommand = SimResourceCommand.Exit + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + ctx.close() + return Long.MAX_VALUE + } } assertFalse(forwarder.isActive) @@ -106,8 +110,12 @@ internal class SimResourceTransformerTest { fun testCancelPendingDelegate() = runBlockingSimulation { val forwarder = SimResourceForwarder() - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Exit + val consumer = spyk(object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + ctx.close() + return Long.MAX_VALUE + } + }) forwarder.startConsumer(consumer) forwarder.cancel() @@ -121,8 +129,7 @@ internal class SimResourceTransformerTest { val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) val source = SimResourceSource(2000.0, scheduler) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(0.0, 10) + val consumer = spyk(SimWorkConsumer(2000.0, 1.0)) source.startConsumer(forwarder) yield() @@ -140,8 +147,7 @@ internal class SimResourceTransformerTest { val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) val source = SimResourceSource(2000.0, scheduler) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Consume(0.0, 10) + val consumer = spyk(SimWorkConsumer(2000.0, 1.0)) source.startConsumer(forwarder) yield() @@ -159,8 +165,12 @@ internal class SimResourceTransformerTest { val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) val source = SimResourceSource(2000.0, scheduler) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onNext(any(), any(), any()) } returns SimResourceCommand.Exit + val consumer = object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + ctx.close() + return Long.MAX_VALUE + } + } source.startConsumer(forwarder) forwarder.consume(consumer) @@ -190,7 +200,7 @@ internal class SimResourceTransformerTest { @Test fun testTransformExit() = runBlockingSimulation { - val forwarder = SimResourceTransformer { _, _ -> SimResourceCommand.Exit } + val forwarder = SimResourceTransformer { ctx, _ -> ctx.close(); Long.MAX_VALUE } val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) val source = SimResourceSource(1.0, scheduler) -- cgit v1.2.3 From 657deac134f7b9ee30ed7e2b7667e30f3b17f79f Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 29 Sep 2021 16:52:30 +0200 Subject: perf(simulator): Reduce memory allocations in SimResourceInterpreter This change removes unnecessary allocations in the SimResourceInterpreter caused by the way timers were allocated for the resource context. --- .../resources/impl/SimResourceContextImpl.kt | 71 ++++---- .../resources/impl/SimResourceInterpreterImpl.kt | 199 +++++++++------------ .../simulator/resources/SimResourceContextTest.kt | 4 +- 3 files changed, 126 insertions(+), 148 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt index d7ea0043..9cbf849d 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt @@ -24,6 +24,7 @@ package org.opendc.simulator.resources.impl import org.opendc.simulator.resources.* import java.time.Clock +import java.util.ArrayDeque import kotlin.math.max import kotlin.math.min @@ -96,9 +97,9 @@ internal class SimResourceContextImpl( private var _flag: Int = 0 /** - * The current pending update. + * The timers at which the context is scheduled to be interrupted. */ - private var _pendingUpdate: SimResourceInterpreterImpl.Update? = null + private val _timers: ArrayDeque = ArrayDeque() override fun start() { check(_state == SimResourceState.Pending) { "Consumer is already started" } @@ -126,7 +127,7 @@ internal class SimResourceContextImpl( } _flag = _flag or FLAG_INTERRUPT - scheduleUpdate() + scheduleUpdate(clock.millis()) } override fun invalidate() { @@ -135,7 +136,7 @@ internal class SimResourceContextImpl( } _flag = _flag or FLAG_INVALIDATE - scheduleUpdate() + scheduleUpdate(clock.millis()) } override fun flush() { @@ -143,7 +144,7 @@ internal class SimResourceContextImpl( return } - interpreter.scheduleSync(this) + interpreter.scheduleSync(clock.millis(), this) } override fun push(rate: Double) { @@ -154,9 +155,9 @@ internal class SimResourceContextImpl( /** * Determine whether the state of the resource context should be updated. */ - fun requiresUpdate(timestamp: Long): Boolean { + fun shouldUpdate(timestamp: Long): Boolean { // Either the resource context is flagged or there is a pending update at this timestamp - return _flag != 0 || _pendingUpdate?.timestamp == timestamp + return _flag != 0 || _deadline == timestamp } /** @@ -204,7 +205,7 @@ internal class SimResourceContextImpl( _deadline = target - scheduleUpdate(target) + scheduleUpdate(timestamp, target) } SimResourceState.Pending -> if (oldState != SimResourceState.Pending) { @@ -229,6 +230,30 @@ internal class SimResourceContextImpl( } } + /** + * Prune the elapsed timers from this context. + */ + fun pruneTimers(now: Long) { + val timers = _timers + while (true) { + val head = timers.peek() + if (head == null || head.target > now) { + break + } + timers.poll() + } + } + + /** + * Try to re-schedule the resource context in case it was skipped. + */ + fun tryReschedule(now: Long) { + val deadline = _deadline + if (deadline > now && deadline != Long.MAX_VALUE) { + scheduleUpdate(now, deadline) + } + } + override fun toString(): String = "SimResourceContextImpl[capacity=$capacity]" /** @@ -279,35 +304,17 @@ internal class SimResourceContextImpl( /** * Schedule an update for this resource context. */ - private fun scheduleUpdate() { - // Cancel the pending update - val pendingUpdate = _pendingUpdate - if (pendingUpdate != null) { - _pendingUpdate = null - pendingUpdate.cancel() - } - - interpreter.scheduleImmediate(this) + private fun scheduleUpdate(now: Long) { + interpreter.scheduleImmediate(now, this) } /** * Schedule a delayed update for this resource context. */ - private fun scheduleUpdate(timestamp: Long) { - val pendingUpdate = _pendingUpdate - if (pendingUpdate != null) { - if (pendingUpdate.timestamp == timestamp) { - // Fast-path: A pending update for the same timestamp already exists - return - } else { - // Cancel the old pending update - _pendingUpdate = null - pendingUpdate.cancel() - } - } - - if (timestamp != Long.MAX_VALUE) { - _pendingUpdate = interpreter.scheduleDelayed(this, timestamp) + private fun scheduleUpdate(now: Long, target: Long) { + val timers = _timers + if (target != Long.MAX_VALUE && (timers.isEmpty() || target < timers.peek().target)) { + timers.addFirst(interpreter.scheduleDelayed(now, this, target)) } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt index c3dcebd0..2b6ec2ba 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt @@ -53,7 +53,7 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, /** * A priority queue containing the resource updates to be scheduled in the future. */ - private val futureQueue = PriorityQueue(compareBy { it.timestamp }) + private val futureQueue = PriorityQueue() /** * The stack of interpreter invocations to occur in the future. @@ -77,13 +77,14 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, get() = batchIndex > 0 /** - * Enqueue the specified [ctx] to be updated immediately during the active interpreter cycle. - * - * This method should be used when the state of a resource context is invalidated/interrupted and needs to be - * re-computed. In case no interpreter is currently active, the interpreter will be started. + * Update the specified [ctx] synchronously. */ - fun scheduleImmediate(ctx: SimResourceContextImpl) { - queue.add(ctx) + fun scheduleSync(now: Long, ctx: SimResourceContextImpl) { + ctx.doUpdate(now) + + if (visited.add(ctx)) { + collectAncestors(ctx, visited) + } // In-case the interpreter is already running in the call-stack, return immediately. The changes will be picked // up by the active interpreter. @@ -93,21 +94,20 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, try { batchIndex++ - runInterpreter() + runInterpreter(now) } finally { batchIndex-- } } /** - * Update the specified [ctx] synchronously. + * Enqueue the specified [ctx] to be updated immediately during the active interpreter cycle. + * + * This method should be used when the state of a resource context is invalidated/interrupted and needs to be + * re-computed. In case no interpreter is currently active, the interpreter will be started. */ - fun scheduleSync(ctx: SimResourceContextImpl) { - ctx.doUpdate(clock.millis()) - - if (visited.add(ctx)) { - collectAncestors(ctx, visited) - } + fun scheduleImmediate(now: Long, ctx: SimResourceContextImpl) { + queue.add(ctx) // In-case the interpreter is already running in the call-stack, return immediately. The changes will be picked // up by the active interpreter. @@ -117,35 +117,30 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, try { batchIndex++ - runInterpreter() + runInterpreter(now) } finally { batchIndex-- } } /** - * Schedule the interpreter to run at [timestamp] to update the resource contexts. + * Schedule the interpreter to run at [target] to update the resource contexts. * * This method will override earlier calls to this method for the same [ctx]. * + * @param now The current virtual timestamp. * @param ctx The resource context to which the event applies. - * @param timestamp The timestamp when the interrupt should happen. + * @param target The timestamp when the interrupt should happen. */ - fun scheduleDelayed(ctx: SimResourceContextImpl, timestamp: Long): Update { - val now = clock.millis() + fun scheduleDelayed(now: Long, ctx: SimResourceContextImpl, target: Long): Timer { val futureQueue = futureQueue - require(timestamp >= now) { "Timestamp must be in the future" } + require(target >= now) { "Timestamp must be in the future" } - val update = allocUpdate(ctx, timestamp) - futureQueue.add(update) + val timer = Timer(ctx, target) + futureQueue.add(timer) - // Optimization: Check if we need to push the interruption forward. Note that we check by timer reference. - if (futureQueue.peek() === update) { - trySchedule(futureQueue, futureInvocations) - } - - return update + return timer } override fun newContext( @@ -162,7 +157,7 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, try { // Flush the work if the platform is not already running if (batchIndex == 1 && queue.isNotEmpty()) { - runInterpreter() + runInterpreter(clock.millis()) } } finally { batchIndex-- @@ -172,37 +167,46 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, /** * Interpret all actions that are scheduled for the current timestamp. */ - private fun runInterpreter() { - val now = clock.millis() + private fun runInterpreter(now: Long) { val queue = queue val futureQueue = futureQueue val futureInvocations = futureInvocations val visited = visited + // Remove any entries in the `futureInvocations` queue from the past + while (true) { + val head = futureInvocations.peek() + if (head == null || head.timestamp > now) { + break + } + futureInvocations.poll() + } + // Execute all scheduled updates at current timestamp while (true) { - val update = futureQueue.peek() ?: break + val timer = futureQueue.peek() ?: break + val ctx = timer.ctx + val target = timer.target - assert(update.timestamp >= now) { "Internal inconsistency: found update of the past" } + assert(target >= now) { "Internal inconsistency: found update of the past" } - if (update.timestamp > now && !update.isCancelled) { - // Schedule a task for the next event to occur. - trySchedule(futureQueue, futureInvocations) + if (target > now) { break } futureQueue.poll() - val shouldExecute = !update.isCancelled && update.ctx.requiresUpdate(now) - if (shouldExecute) { - update.ctx.doUpdate(now) + ctx.pruneTimers(now) - if (visited.add(update.ctx)) { - collectAncestors(update.ctx, visited) + if (ctx.shouldUpdate(now)) { + ctx.doUpdate(now) + + if (visited.add(ctx)) { + collectAncestors(ctx, visited) } + } else { + ctx.tryReschedule(now) } - - updatePool.add(update) } // Repeat execution of all immediate updates until the system has converged to a steady-state @@ -211,9 +215,8 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, // Execute all immediate updates while (true) { val ctx = queue.poll() ?: break - val shouldExecute = ctx.requiresUpdate(now) - if (shouldExecute) { + if (ctx.shouldUpdate(now)) { ctx.doUpdate(now) if (visited.add(ctx)) { @@ -228,88 +231,65 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, visited.clear() } while (queue.isNotEmpty()) + + // Schedule an interpreter invocation for the next update to occur. + val headTimer = futureQueue.peek() + if (headTimer != null) { + trySchedule(now, futureInvocations, headTimer.target) + } } /** - * Try to schedule the next interpreter event. + * Collect all the ancestors of the specified [system]. */ - private fun trySchedule(queue: PriorityQueue, scheduled: ArrayDeque) { - val nextTimer = queue.peek() - val now = clock.millis() - - // Check whether we need to update our schedule: - if (nextTimer == null) { - // Case 1: all timers are cancelled - for (invocation in scheduled) { - invocation.cancel() - } - scheduled.clear() - return + private tailrec fun collectAncestors(system: SimResourceSystem, systems: MutableSet) { + val parent = system.parent + if (parent != null) { + systems.add(parent) + collectAncestors(parent, systems) } + } + /** + * Try to schedule an interpreter invocation at the specified [target]. + * + * @param now The current virtual timestamp. + * @param target The virtual timestamp at which the interpreter invocation should happen. + * @param scheduled The queue of scheduled invocations. + */ + private fun trySchedule(now: Long, scheduled: ArrayDeque, target: Long) { while (true) { val invocation = scheduled.peekFirst() - if (invocation == null || invocation.timestamp > nextTimer.timestamp) { + if (invocation == null || invocation.timestamp > target) { // Case 2: A new timer was registered ahead of the other timers. // Solution: Schedule a new scheduler invocation - val nextTimestamp = nextTimer.timestamp @OptIn(InternalCoroutinesApi::class) val handle = delay.invokeOnTimeout( - nextTimestamp - now, + target - now, { try { batchIndex++ - runInterpreter() + runInterpreter(target) } finally { batchIndex-- } }, context ) - scheduled.addFirst(Invocation(nextTimestamp, handle)) + scheduled.addFirst(Invocation(target, handle)) break - } else if (invocation.timestamp < nextTimer.timestamp) { + } else if (invocation.timestamp < target) { // Case 2: A timer was cancelled and the head of the timer queue is now later than excepted // Solution: Cancel the next scheduler invocation - invocation.cancel() scheduled.pollFirst() + + invocation.cancel() } else { break } } } - /** - * Collect all the ancestors of the specified [system]. - */ - private tailrec fun collectAncestors(system: SimResourceSystem, systems: MutableSet) { - val parent = system.parent - if (parent != null) { - systems.add(parent) - collectAncestors(parent, systems) - } - } - - /** - * The pool of existing updates. - */ - private val updatePool = ArrayDeque() - - /** - * Allocate an [Update] object. - */ - private fun allocUpdate(ctx: SimResourceContextImpl, timestamp: Long): Update { - val update = updatePool.poll() - return if (update != null) { - update.ctx = ctx - update.timestamp = timestamp - update.isCancelled = false - update - } else { - Update(ctx, timestamp) - } - } - /** * A future interpreter invocation. * @@ -318,34 +298,25 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, */ private data class Invocation( @JvmField val timestamp: Long, - private val disposableHandle: DisposableHandle + @JvmField val handle: DisposableHandle ) { /** * Cancel the interpreter invocation. */ - fun cancel() = disposableHandle.dispose() + fun cancel() = handle.dispose() } /** - * An update call for [ctx] that is scheduled for [timestamp]. + * An update call for [ctx] that is scheduled for [target]. * - * This class represents an update in the future at [timestamp] requested by [ctx]. A deferred update might be + * This class represents an update in the future at [target] requested by [ctx]. A deferred update might be * cancelled if the resource context was invalidated in the meantime. */ - class Update(@JvmField var ctx: SimResourceContextImpl, @JvmField var timestamp: Long) { - /** - * A flag to indicate that the task has been cancelled. - */ - @JvmField - var isCancelled: Boolean = false - - /** - * Cancel the update. - */ - fun cancel() { - isCancelled = true + class Timer(@JvmField val ctx: SimResourceContextImpl, @JvmField val target: Long) : Comparable { + override fun compareTo(other: Timer): Int { + return target.compareTo(other.target) } - override fun toString(): String = "Update[ctx=$ctx,timestamp=$timestamp]" + override fun toString(): String = "Timer[ctx=$ctx,timestamp=$target]" } } diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt index 4e57f598..e95e9e42 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt @@ -51,7 +51,7 @@ class SimResourceContextTest { val logic = object : SimResourceProviderLogic {} val context = SimResourceContextImpl(null, interpreter, consumer, logic) - context.doUpdate(interpreter.clock.millis()) + interpreter.scheduleSync(interpreter.clock.millis(), context) } @Test @@ -77,7 +77,7 @@ class SimResourceContextTest { context.start() delay(1) // Delay 1 ms to prevent hitting the fast path - context.doUpdate(interpreter.clock.millis()) + interpreter.scheduleSync(interpreter.clock.millis(), context) verify(exactly = 2) { logic.onConsume(any(), any(), any(), any()) } } -- cgit v1.2.3 From d3b0b551362eb677c12047cba82a6279ea4608b4 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 29 Sep 2021 17:06:45 +0200 Subject: refactor(simulator): Simplify max-min aggregator implementation This change simplifies the implementation of the SimResourceAggregatorMaxMin class by utilizing the new push method. This approach should offer better performance than the previous version, since we can directly push changes to the source. --- .../resources/SimAbstractResourceAggregator.kt | 81 +++------------------- .../resources/SimResourceAggregatorMaxMin.kt | 15 ++-- 2 files changed, 16 insertions(+), 80 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt index 8e0eb5f8..d064d7fa 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt @@ -32,7 +32,7 @@ public abstract class SimAbstractResourceAggregator( /** * This method is invoked when the resource consumer consumes resources. */ - protected abstract fun doConsume(limit: Double, duration: Long) + protected abstract fun doConsume(limit: Double) /** * This method is invoked when the resource consumer finishes processing. @@ -42,12 +42,12 @@ public abstract class SimAbstractResourceAggregator( /** * This method is invoked when an input context is started. */ - protected abstract fun onInputStarted(input: Input) + protected abstract fun onInputStarted(input: SimResourceContext) /** * This method is invoked when an input is stopped. */ - protected abstract fun onInputFinished(input: Input) + protected abstract fun onInputFinished(input: SimResourceContext) /* SimResourceAggregator */ override fun addInput(input: SimResourceProvider) { @@ -95,7 +95,7 @@ public abstract class SimAbstractResourceAggregator( return object : SimResourceProviderLogic { override fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long { - doConsume(limit, duration) + doConsume(limit) return super.onConsume(ctx, now, limit, duration) } @@ -113,90 +113,25 @@ public abstract class SimAbstractResourceAggregator( } } } - - /** - * Flush the progress of the output if possible. - */ - fun flush() { - ctx?.flush() - } - } - - /** - * An input for the resource aggregator. - */ - public interface Input : AutoCloseable { - /** - * The [SimResourceContext] associated with the input. - */ - public val ctx: SimResourceContext - - /** - * Push to this input with the specified [limit] and [duration]. - */ - public fun push(limit: Double, duration: Long) - - /** - * Close the input for further input. - */ - public override fun close() } /** * An internal [SimResourceConsumer] implementation for aggregator inputs. */ - private inner class Consumer : Input, SimResourceConsumer { + private inner class Consumer : SimResourceConsumer { /** * The resource context associated with the input. */ - override val ctx: SimResourceContext - get() = _ctx!! private var _ctx: SimResourceContext? = null - /** - * The resource command to run next. - */ - private var _duration: Long = Long.MAX_VALUE - - /** - * A flag to indicate that the consumer should flush. - */ - private var _isPushed = false - private fun updateCapacity() { // Adjust capacity of output resource _output.capacity = _inputConsumers.sumOf { it._ctx?.capacity ?: 0.0 } } - /* Input */ - override fun push(limit: Double, duration: Long) { - _duration = duration - val ctx = _ctx - if (ctx != null) { - ctx.push(limit) - ctx.interrupt() - } - _isPushed = true - } - - override fun close() { - _duration = Long.MAX_VALUE - _isPushed = true - _ctx?.close() - } - /* SimResourceConsumer */ override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - var next = _duration - - if (!_isPushed) { - _output.flush() - next = _duration - } - - _isPushed = false - _duration = Long.MAX_VALUE - return next + return Long.MAX_VALUE } override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { @@ -205,10 +140,10 @@ public abstract class SimAbstractResourceAggregator( _ctx = ctx updateCapacity() - onInputStarted(this) + onInputStarted(ctx) } SimResourceEvent.Capacity -> updateCapacity() - SimResourceEvent.Exit -> onInputFinished(this) + SimResourceEvent.Exit -> onInputFinished(ctx) else -> {} } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt index b258a368..f131ac6c 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt @@ -29,19 +29,20 @@ public class SimResourceAggregatorMaxMin( interpreter: SimResourceInterpreter, parent: SimResourceSystem? = null ) : SimAbstractResourceAggregator(interpreter, parent) { - private val consumers = mutableListOf() + private val consumers = mutableListOf() - override fun doConsume(limit: Double, duration: Long) { + override fun doConsume(limit: Double) { // Sort all consumers by their capacity - consumers.sortWith(compareBy { it.ctx.capacity }) + consumers.sortWith(compareBy { it.capacity }) // Divide the requests over the available capacity of the input resources fairly for (input in consumers) { - val inputCapacity = input.ctx.capacity + val inputCapacity = input.capacity val fraction = inputCapacity / capacity val grantedSpeed = limit * fraction - input.push(grantedSpeed, duration) + input.push(grantedSpeed) + input.interrupt() } } @@ -53,11 +54,11 @@ public class SimResourceAggregatorMaxMin( } } - override fun onInputStarted(input: Input) { + override fun onInputStarted(input: SimResourceContext) { consumers.add(input) } - override fun onInputFinished(input: Input) { + override fun onInputFinished(input: SimResourceContext) { consumers.remove(input) } } -- cgit v1.2.3 From d031a70f8bea02a86df7840c5ce731185df86883 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 28 Sep 2021 22:10:12 +0200 Subject: refactor(simulator): Invoke consumer callback on every invalidation This change updates the simulator implementation to always invoke the `SimResourceConsumer.onNext` callback when the resource context is invalidated. This allows users to update the resource counter or do some other work if the context has changed. --- .../resources/consumer/SimTraceConsumer.kt | 18 ++++++++--- .../resources/impl/SimResourceContextImpl.kt | 36 +++++++++------------- .../simulator/resources/SimResourceContextTest.kt | 33 +++++--------------- 3 files changed, 35 insertions(+), 52 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt index ad6b0108..4c0e075c 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt @@ -31,12 +31,20 @@ import org.opendc.simulator.resources.SimResourceEvent * consumption for some period of time. */ public class SimTraceConsumer(private val trace: Sequence) : SimResourceConsumer { - private var iterator: Iterator? = null + private var _iterator: Iterator? = null + private var _nextTarget = Long.MIN_VALUE override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - val iterator = checkNotNull(iterator) + // Check whether the trace fragment was fully consumed, otherwise wait until we have done so + val nextTarget = _nextTarget + if (nextTarget > now) { + return now - nextTarget + } + + val iterator = checkNotNull(_iterator) return if (iterator.hasNext()) { val fragment = iterator.next() + _nextTarget = now + fragment.duration ctx.push(fragment.usage) fragment.duration } else { @@ -48,11 +56,11 @@ public class SimTraceConsumer(private val trace: Sequence) : SimResour override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { when (event) { SimResourceEvent.Start -> { - check(iterator == null) { "Consumer already running" } - iterator = trace.iterator() + check(_iterator == null) { "Consumer already running" } + _iterator = trace.iterator() } SimResourceEvent.Exit -> { - iterator = null + _iterator = null } else -> {} } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt index 9cbf849d..1ac38946 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt @@ -148,6 +148,10 @@ internal class SimResourceContextImpl( } override fun push(rate: Double) { + if (_limit == rate) { + return + } + _speed = min(capacity, rate) _limit = rate } @@ -163,7 +167,7 @@ internal class SimResourceContextImpl( /** * Update the state of the resource context. */ - fun doUpdate(timestamp: Long) { + fun doUpdate(now: Long) { val oldState = _state if (oldState != SimResourceState.Active) { return @@ -171,41 +175,29 @@ internal class SimResourceContextImpl( _updateActive = true - val flag = _flag - val isInterrupted = flag and FLAG_INTERRUPT != 0 - val reachedDeadline = _deadline <= timestamp - val delta = max(0, timestamp - _timestamp) + val reachedDeadline = _deadline <= now + val delta = max(0, now - _timestamp) try { - // Update the resource counters only if there is some progress - if (timestamp > _timestamp) { + if (now > _timestamp) { logic.onUpdate(this, delta, _limit, reachedDeadline) } - // We should only continue processing the next command if: - // 1. The resource consumption was finished. - // 2. The resource consumer should be interrupted (e.g., someone called .interrupt()) - val duration = if (reachedDeadline || isInterrupted) { - consumer.onNext(this, timestamp, delta) - } else { - _deadline - timestamp - } + val duration = consumer.onNext(this, now, delta) // Reset update flags _flag = 0 when (_state) { SimResourceState.Active -> { - val limit = _limit - push(limit) - _duration = duration - - val target = logic.onConsume(this, timestamp, limit, duration) + val target = logic.onConsume(this, now, _limit, duration) + _speed = min(capacity, _limit) + _duration = duration _deadline = target - scheduleUpdate(timestamp, target) + scheduleUpdate(now, target) } SimResourceState.Pending -> if (oldState != SimResourceState.Pending) { @@ -219,7 +211,7 @@ internal class SimResourceContextImpl( } catch (cause: Throwable) { doFail(cause) } finally { - _timestamp = timestamp + _timestamp = now _updateActive = false } } diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt index e95e9e42..c7230a0e 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt @@ -26,6 +26,7 @@ import io.mockk.* import kotlinx.coroutines.* import org.junit.jupiter.api.* import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.resources.consumer.SimWorkConsumer import org.opendc.simulator.resources.impl.SimResourceContextImpl import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl @@ -57,23 +58,14 @@ class SimResourceContextTest { @Test fun testIntermediateFlush() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (now == 0L) { - ctx.push(4.0) - 1000 - } else { - ctx.close() - Long.MAX_VALUE - } - } - } + val consumer = SimWorkConsumer(1.0, 1.0) val logic = spyk(object : SimResourceProviderLogic { override fun onFinish(ctx: SimResourceControllableContext) {} override fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long = duration }) val context = SimResourceContextImpl(null, interpreter, consumer, logic) + context.capacity = 1.0 context.start() delay(1) // Delay 1 ms to prevent hitting the fast path @@ -85,29 +77,20 @@ class SimResourceContextTest { @Test fun testIntermediateFlushIdle() = runBlockingSimulation { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (now == 0L) { - ctx.push(0.0) - 10 - } else { - ctx.close() - Long.MAX_VALUE - } - } - } + val consumer = SimWorkConsumer(1.0, 1.0) val logic = spyk(object : SimResourceProviderLogic {}) val context = SimResourceContextImpl(null, interpreter, consumer, logic) + context.capacity = 1.0 context.start() - delay(5) + delay(500) context.invalidate() - delay(5) + delay(500) context.invalidate() assertAll( - { verify(exactly = 2) { logic.onConsume(any(), any(), 0.0, any()) } }, + { verify(exactly = 2) { logic.onConsume(any(), any(), any(), any()) } }, { verify(exactly = 1) { logic.onFinish(any()) } } ) } -- cgit v1.2.3 From 15899c88d29c039149f701e7f0d538a49a436599 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 29 Sep 2021 10:33:36 +0200 Subject: refactor(simulator): Lazily push changes to resource context This change updates the SimResourceContextImpl to lazily push changes to the resource context instead of applying them directly. The change is picked up after the resource is updated again. --- .../simulator/resources/SimResourceTransformer.kt | 4 +- .../resources/impl/SimResourceContextImpl.kt | 58 ++++++++++++---------- 2 files changed, 34 insertions(+), 28 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt index f12ef9f1..a317f832 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt @@ -152,9 +152,7 @@ public class SimResourceTransformer( updateCounters(ctx, delta) return if (delegate != null) { - val duration = transform(ctx, delegate.onNext(this.ctx, now, delta)) - _limit = ctx.demand - duration + transform(ctx, delegate.onNext(this.ctx, now, delta)) } else { Long.MAX_VALUE } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt index 1ac38946..78d79434 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt @@ -69,8 +69,8 @@ internal class SimResourceContextImpl( * The current processing speed of the resource. */ override val speed: Double - get() = _speed - private var _speed = 0.0 + get() = _rate + private var _rate = 0.0 /** * The current resource processing demand. @@ -81,10 +81,10 @@ internal class SimResourceContextImpl( /** * The current state of the resource context. */ - private var _timestamp: Long = Long.MIN_VALUE private var _limit: Double = 0.0 - private var _duration: Long = Long.MAX_VALUE - private var _deadline: Long = Long.MAX_VALUE + private var _activeLimit: Double = 0.0 + private var _deadline: Long = Long.MIN_VALUE + private var _lastUpdate: Long = Long.MIN_VALUE /** * A flag to indicate that an update is active. @@ -152,8 +152,13 @@ internal class SimResourceContextImpl( return } - _speed = min(capacity, rate) _limit = rate + + // Invalidate only if the active limit is change and no update is active + // If an update is active, it will already get picked up at the end of the update + if (_activeLimit != rate && !_updateActive) { + invalidate() + } } /** @@ -173,45 +178,47 @@ internal class SimResourceContextImpl( return } + val lastUpdate = _lastUpdate + _lastUpdate = now _updateActive = true val reachedDeadline = _deadline <= now - val delta = max(0, now - _timestamp) + val delta = max(0, now - lastUpdate) try { // Update the resource counters only if there is some progress - if (now > _timestamp) { - logic.onUpdate(this, delta, _limit, reachedDeadline) + if (now > lastUpdate) { + logic.onUpdate(this, delta, _activeLimit, reachedDeadline) } val duration = consumer.onNext(this, now, delta) + val newDeadline = if (duration != Long.MAX_VALUE) now + duration else duration // Reset update flags _flag = 0 + // Check whether the state has changed after [consumer.onNext] when (_state) { SimResourceState.Active -> { - val target = logic.onConsume(this, now, _limit, duration) - - _speed = min(capacity, _limit) - _duration = duration - _deadline = target + logic.onConsume(this, now, _limit, duration) - scheduleUpdate(now, target) + // Schedule an update at the new deadline + scheduleUpdate(now, newDeadline) } - SimResourceState.Pending -> - if (oldState != SimResourceState.Pending) { - throw IllegalStateException("Illegal transition to pending state") - } - SimResourceState.Stopped -> - if (oldState != SimResourceState.Stopped) { - doStop() - } + SimResourceState.Stopped -> doStop() + SimResourceState.Pending -> throw IllegalStateException("Illegal transition to pending state") } + + // Note: pending limit might be changed by [logic.onConsume], so re-fetch the value + val newLimit = _limit + + // Flush the changes to the flow + _activeLimit = newLimit + _deadline = newDeadline + _rate = min(capacity, newLimit) } catch (cause: Throwable) { doFail(cause) } finally { - _timestamp = now _updateActive = false } } @@ -246,7 +253,7 @@ internal class SimResourceContextImpl( } } - override fun toString(): String = "SimResourceContextImpl[capacity=$capacity]" + override fun toString(): String = "SimResourceContextImpl[capacity=$capacity,rate=$_rate]" /** * Stop the resource context. @@ -259,6 +266,7 @@ internal class SimResourceContextImpl( doFail(cause) } finally { _deadline = Long.MAX_VALUE + _limit = 0.0 } } -- cgit v1.2.3 From e07a5357013b92377a840b4d0d394d0ef6605b26 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 27 Sep 2021 14:22:02 +0200 Subject: refactor(simulator): Remove onUpdate callback This change removes the `onUpdate` callback from the `SimResourceProviderLogic` interface. Instead, users should now update counters using either `onConsume` or `onConverge`. --- .../resources/SimAbstractResourceAggregator.kt | 18 +-- .../resources/SimAbstractResourceProvider.kt | 22 +-- .../resources/SimResourceDistributorMaxMin.kt | 148 +++++++++++---------- .../simulator/resources/SimResourceInterpreter.kt | 7 +- .../resources/SimResourceProviderLogic.kt | 25 ++-- .../simulator/resources/SimResourceSource.kt | 16 ++- .../simulator/resources/SimResourceTransformer.kt | 14 +- .../resources/impl/SimResourceContextImpl.kt | 79 ++++++----- .../resources/impl/SimResourceInterpreterImpl.kt | 34 +---- .../simulator/resources/SimResourceContextTest.kt | 24 ++-- .../resources/SimResourceTransformerTest.kt | 3 + 11 files changed, 197 insertions(+), 193 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt index d064d7fa..621ea6e7 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt @@ -90,26 +90,22 @@ public abstract class SimAbstractResourceAggregator( _output.interrupt() } - private val _output = object : SimAbstractResourceProvider(interpreter, parent, initialCapacity = 0.0) { + private val _output = object : SimAbstractResourceProvider(interpreter, initialCapacity = 0.0) { override fun createLogic(): SimResourceProviderLogic { return object : SimResourceProviderLogic { - override fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long { + override fun onConsume(ctx: SimResourceControllableContext, now: Long, delta: Long, limit: Double, duration: Long) { + updateCounters(ctx, delta) doConsume(limit) - return super.onConsume(ctx, now, limit, duration) } - override fun onFinish(ctx: SimResourceControllableContext) { + override fun onFinish(ctx: SimResourceControllableContext, now: Long, delta: Long) { + updateCounters(ctx, delta) doFinish() } - override fun onUpdate( - ctx: SimResourceControllableContext, - delta: Long, - limit: Double, - willOvercommit: Boolean - ) { - updateCounters(ctx, delta, limit, willOvercommit) + override fun onConverge(ctx: SimResourceControllableContext, now: Long, delta: Long) { + parent?.onConverge(now) } } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt index 548bc228..085cba63 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt @@ -29,7 +29,6 @@ import org.opendc.simulator.resources.impl.SimResourceCountersImpl */ public abstract class SimAbstractResourceProvider( private val interpreter: SimResourceInterpreter, - private val parent: SimResourceSystem?, initialCapacity: Double ) : SimResourceProvider { /** @@ -84,26 +83,31 @@ public abstract class SimAbstractResourceProvider( ctx.start() } + /** + * The previous demand for the resource. + */ + private var previousDemand = 0.0 + /** * Update the counters of the resource provider. */ - protected fun updateCounters(ctx: SimResourceContext, delta: Long, limit: Double, willOvercommit: Boolean) { - if (delta <= 0.0) { + protected fun updateCounters(ctx: SimResourceContext, delta: Long) { + val demand = previousDemand + previousDemand = ctx.demand + + if (delta <= 0) { return } val counters = _counters val deltaS = delta / 1000.0 - val work = limit * deltaS + val work = demand * deltaS val actualWork = ctx.speed * deltaS val remainingWork = work - actualWork counters.demand += work counters.actual += actualWork - - if (willOvercommit && remainingWork > 0.0) { - counters.overcommit += remainingWork - } + counters.overcommit += remainingWork } /** @@ -118,7 +122,7 @@ public abstract class SimAbstractResourceProvider( final override fun startConsumer(consumer: SimResourceConsumer) { check(ctx == null) { "Resource is in invalid state" } - val ctx = interpreter.newContext(consumer, createLogic(), parent) + val ctx = interpreter.newContext(consumer, createLogic()) ctx.capacity = capacity this.ctx = ctx diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt index eac58410..7df940ad 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt @@ -93,46 +93,6 @@ public class SimResourceDistributorMaxMin( /* SimResourceConsumer */ override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return doNext(ctx, now) - } - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - when (event) { - SimResourceEvent.Start -> { - this.ctx = ctx - updateCapacity(ctx) - } - SimResourceEvent.Exit -> { - val iterator = _outputs.iterator() - while (iterator.hasNext()) { - val output = iterator.next() - - // Remove the output from the outputs to prevent ConcurrentModificationException when removing it - // during the call to output.close() - iterator.remove() - - output.close() - } - } - SimResourceEvent.Capacity -> updateCapacity(ctx) - else -> {} - } - } - - /** - * Extended [SimResourceCounters] interface for the distributor. - */ - public interface Counters : SimResourceCounters { - /** - * The amount of work lost due to interference. - */ - public val interference: Double - } - - /** - * Schedule the work of the outputs. - */ - private fun doNext(ctx: SimResourceContext, now: Long): Long { // If there is no work yet, mark the input as idle. if (activeOutputs.isEmpty()) { return Long.MAX_VALUE @@ -198,6 +158,39 @@ public class SimResourceDistributorMaxMin( return duration } + override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { + when (event) { + SimResourceEvent.Start -> { + this.ctx = ctx + updateCapacity(ctx) + } + SimResourceEvent.Exit -> { + val iterator = _outputs.iterator() + while (iterator.hasNext()) { + val output = iterator.next() + + // Remove the output from the outputs to prevent ConcurrentModificationException when removing it + // during the call to output.close() + iterator.remove() + + output.close() + } + } + SimResourceEvent.Capacity -> updateCapacity(ctx) + else -> {} + } + } + + /** + * Extended [SimResourceCounters] interface for the distributor. + */ + public interface Counters : SimResourceCounters { + /** + * The amount of work lost due to interference. + */ + public val interference: Double + } + private fun updateCapacity(ctx: SimResourceContext) { for (output in _outputs) { output.capacity = ctx.capacity @@ -208,7 +201,7 @@ public class SimResourceDistributorMaxMin( * An internal [SimResourceProvider] implementation for switch outputs. */ private inner class Output(capacity: Double, val key: InterferenceKey?) : - SimAbstractResourceProvider(interpreter, parent, capacity), + SimAbstractResourceProvider(interpreter, capacity), SimResourceCloseableProvider, SimResourceProviderLogic, Comparable { @@ -263,17 +256,54 @@ public class SimResourceDistributorMaxMin( } /* SimResourceProviderLogic */ - override fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long { + override fun onConsume( + ctx: SimResourceControllableContext, + now: Long, + delta: Long, + limit: Double, + duration: Long + ) { + doUpdateCounters(delta) + allowedSpeed = min(ctx.capacity, limit) + actualSpeed = 0.0 this.limit = limit this.duration = duration lastCommandTimestamp = now + } + + override fun onConverge(ctx: SimResourceControllableContext, now: Long, delta: Long) { + parent?.onConverge(now) + } + + override fun onFinish(ctx: SimResourceControllableContext, now: Long, delta: Long) { + doUpdateCounters(delta) + + limit = 0.0 + duration = Long.MAX_VALUE + actualSpeed = 0.0 + allowedSpeed = 0.0 + lastCommandTimestamp = now + } - return super.onConsume(ctx, now, limit, duration) + /* Comparable */ + override fun compareTo(other: Output): Int = allowedSpeed.compareTo(other.allowedSpeed) + + /** + * Pull the next command if necessary. + */ + fun pull(now: Long) { + val ctx = ctx + if (ctx != null && lastCommandTimestamp < now) { + ctx.flush() + } } - override fun onUpdate(ctx: SimResourceControllableContext, delta: Long, limit: Double, willOvercommit: Boolean) { - if (delta <= 0.0) { + /** + * Helper method to update the resource counters of the distributor. + */ + private fun doUpdateCounters(delta: Long) { + if (delta <= 0L) { return } @@ -289,38 +319,14 @@ public class SimResourceDistributorMaxMin( val work = limit * deltaS val actualWork = actualSpeed * deltaS val remainingWork = work - actualWork - val overcommit = if (willOvercommit && remainingWork > 0.0) { - remainingWork - } else { - 0.0 - } - updateCounters(work, actualWork, overcommit) + updateCounters(work, actualWork, remainingWork) val distCounters = _counters distCounters.demand += work distCounters.actual += actualWork - distCounters.overcommit += overcommit + distCounters.overcommit += remainingWork distCounters.interference += actualWork * max(0.0, 1 - perfScore) } - - override fun onFinish(ctx: SimResourceControllableContext) { - limit = 0.0 - duration = Long.MAX_VALUE - lastCommandTimestamp = ctx.clock.millis() - } - - /* Comparable */ - override fun compareTo(other: Output): Int = allowedSpeed.compareTo(other.allowedSpeed) - - /** - * Pull the next command if necessary. - */ - fun pull(now: Long) { - val ctx = ctx - if (ctx != null && lastCommandTimestamp < now) { - ctx.flush() - } - } } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceInterpreter.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceInterpreter.kt index 82631377..4bfeaf20 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceInterpreter.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceInterpreter.kt @@ -43,13 +43,8 @@ public interface SimResourceInterpreter { * * @param consumer The consumer logic. * @param provider The logic of the resource provider. - * @param parent The system to which the resource context belongs. */ - public fun newContext( - consumer: SimResourceConsumer, - provider: SimResourceProviderLogic, - parent: SimResourceSystem? = null - ): SimResourceControllableContext + public fun newContext(consumer: SimResourceConsumer, provider: SimResourceProviderLogic): SimResourceControllableContext /** * Start batching the execution of resource updates until [popBatch] is called. diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt index d8ff87f9..cc718165 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt @@ -23,7 +23,7 @@ package org.opendc.simulator.resources /** - * The logic of a resource provider. + * A collection of callbacks associated with a flow stage. */ public interface SimResourceProviderLogic { /** @@ -31,29 +31,28 @@ public interface SimResourceProviderLogic { * * @param ctx The context in which the provider runs. * @param now The virtual timestamp in milliseconds at which the update is occurring. + * @param delta The virtual duration between this call and the last call to [onConsume] in milliseconds. * @param limit The limit on the work rate of the resource consumer. * @param duration The duration of the consumption in milliseconds. * @return The deadline of the resource consumption. */ - public fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long { - return if (duration == Long.MAX_VALUE) { - return Long.MAX_VALUE - } else { - now + duration - } - } + public fun onConsume(ctx: SimResourceControllableContext, now: Long, delta: Long, limit: Double, duration: Long) {} /** - * This method is invoked when the progress of the resource consumer is materialized. + * This method is invoked when the flow graph has converged into a steady-state system. * * @param ctx The context in which the provider runs. - * @param limit The limit on the work rate of the resource consumer. - * @param willOvercommit A flag to indicate that the remaining work is overcommitted. + * @param now The virtual timestamp in milliseconds at which the system converged. + * @param delta The virtual duration between this call and the last call to [onConverge] in milliseconds. */ - public fun onUpdate(ctx: SimResourceControllableContext, delta: Long, limit: Double, willOvercommit: Boolean) {} + public fun onConverge(ctx: SimResourceControllableContext, now: Long, delta: Long) {} /** * This method is invoked when the resource consumer has finished. + * + * @param ctx The context in which the provider runs. + * @param now The virtual timestamp in milliseconds at which the provider finished. + * @param delta The virtual duration between this call and the last call to [onConsume] in milliseconds. */ - public fun onFinish(ctx: SimResourceControllableContext) {} + public fun onFinish(ctx: SimResourceControllableContext, now: Long, delta: Long) {} } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt index 10213f26..c8d4cf0d 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt @@ -33,21 +33,27 @@ public class SimResourceSource( initialCapacity: Double, private val interpreter: SimResourceInterpreter, private val parent: SimResourceSystem? = null -) : SimAbstractResourceProvider(interpreter, parent, initialCapacity) { +) : SimAbstractResourceProvider(interpreter, initialCapacity) { override fun createLogic(): SimResourceProviderLogic { return object : SimResourceProviderLogic { - override fun onUpdate( + override fun onConsume( ctx: SimResourceControllableContext, + now: Long, delta: Long, limit: Double, - willOvercommit: Boolean + duration: Long ) { - updateCounters(ctx, delta, limit, willOvercommit) + updateCounters(ctx, delta) } - override fun onFinish(ctx: SimResourceControllableContext) { + override fun onFinish(ctx: SimResourceControllableContext, now: Long, delta: Long) { + updateCounters(ctx, delta) cancel() } + + override fun onConverge(ctx: SimResourceControllableContext, now: Long, delta: Long) { + parent?.onConverge(now) + } } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt index a317f832..397463e0 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt @@ -73,16 +73,16 @@ public class SimResourceTransformer( override fun close() { val delegate = checkNotNull(delegate) { "Delegate not active" } + if (isCoupled) + _ctx?.close() + else + _ctx?.push(0.0) + // Warning: resumption of the continuation might change the entire state of the forwarder. Make sure we // reset beforehand the existing state and check whether it has been updated afterwards reset() delegate.onEvent(this, SimResourceEvent.Exit) - - if (isCoupled) - _ctx?.close() - else - _ctx?.push(0.0) } } @@ -213,6 +213,10 @@ public class SimResourceTransformer( * Update the resource counters for the transformer. */ private fun updateCounters(ctx: SimResourceContext, delta: Long) { + if (delta <= 0) { + return + } + val counters = _counters val deltaS = delta / 1000.0 val work = _limit * deltaS diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt index 78d79434..5a9ffe2d 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt @@ -32,11 +32,10 @@ import kotlin.math.min * Implementation of a [SimResourceContext] managing the communication between resources and resource consumers. */ internal class SimResourceContextImpl( - override val parent: SimResourceSystem?, private val interpreter: SimResourceInterpreterImpl, private val consumer: SimResourceConsumer, private val logic: SimResourceProviderLogic -) : SimResourceControllableContext, SimResourceSystem { +) : SimResourceControllableContext { /** * The clock of the context. */ @@ -84,7 +83,6 @@ internal class SimResourceContextImpl( private var _limit: Double = 0.0 private var _activeLimit: Double = 0.0 private var _deadline: Long = Long.MIN_VALUE - private var _lastUpdate: Long = Long.MIN_VALUE /** * A flag to indicate that an update is active. @@ -96,6 +94,12 @@ internal class SimResourceContextImpl( */ private var _flag: Int = 0 + /** + * The timestamp of calls to the callbacks. + */ + private var _lastUpdate: Long = Long.MIN_VALUE + private var _lastConvergence: Long = Long.MAX_VALUE + /** * The timers at which the context is scheduled to be interrupted. */ @@ -111,12 +115,20 @@ internal class SimResourceContextImpl( } override fun close() { - if (_state != SimResourceState.Stopped) { - interpreter.batch { - _state = SimResourceState.Stopped - if (!_updateActive) { - doStop() - } + if (_state == SimResourceState.Stopped) { + return + } + + interpreter.batch { + _state = SimResourceState.Stopped + if (!_updateActive) { + val now = clock.millis() + val delta = max(0, now - _lastUpdate) + doStop(now, delta) + + // FIX: Make sure the context converges + _flag = _flag or FLAG_INVALIDATE + scheduleUpdate(clock.millis()) } } } @@ -166,7 +178,7 @@ internal class SimResourceContextImpl( */ fun shouldUpdate(timestamp: Long): Boolean { // Either the resource context is flagged or there is a pending update at this timestamp - return _flag != 0 || _deadline == timestamp + return _flag != 0 || _limit != _activeLimit || _deadline == timestamp } /** @@ -179,18 +191,13 @@ internal class SimResourceContextImpl( } val lastUpdate = _lastUpdate + _lastUpdate = now _updateActive = true - val reachedDeadline = _deadline <= now val delta = max(0, now - lastUpdate) try { - // Update the resource counters only if there is some progress - if (now > lastUpdate) { - logic.onUpdate(this, delta, _activeLimit, reachedDeadline) - } - val duration = consumer.onNext(this, now, delta) val newDeadline = if (duration != Long.MAX_VALUE) now + duration else duration @@ -200,12 +207,12 @@ internal class SimResourceContextImpl( // Check whether the state has changed after [consumer.onNext] when (_state) { SimResourceState.Active -> { - logic.onConsume(this, now, _limit, duration) + logic.onConsume(this, now, delta, _limit, duration) // Schedule an update at the new deadline scheduleUpdate(now, newDeadline) } - SimResourceState.Stopped -> doStop() + SimResourceState.Stopped -> doStop(now, delta) SimResourceState.Pending -> throw IllegalStateException("Illegal transition to pending state") } @@ -217,18 +224,12 @@ internal class SimResourceContextImpl( _deadline = newDeadline _rate = min(capacity, newLimit) } catch (cause: Throwable) { - doFail(cause) + doFail(now, delta, cause) } finally { _updateActive = false } } - override fun onConverge(timestamp: Long) { - if (_state == SimResourceState.Active) { - consumer.onEvent(this, SimResourceEvent.Run) - } - } - /** * Prune the elapsed timers from this context. */ @@ -253,17 +254,35 @@ internal class SimResourceContextImpl( } } + /** + * This method is invoked when the system converges into a steady state. + */ + fun onConverge(timestamp: Long) { + val delta = max(0, timestamp - _lastConvergence) + _lastConvergence = timestamp + + try { + if (_state == SimResourceState.Active) { + consumer.onEvent(this, SimResourceEvent.Run) + } + + logic.onConverge(this, timestamp, delta) + } catch (cause: Throwable) { + doFail(timestamp, max(0, timestamp - _lastUpdate), cause) + } + } + override fun toString(): String = "SimResourceContextImpl[capacity=$capacity,rate=$_rate]" /** * Stop the resource context. */ - private fun doStop() { + private fun doStop(now: Long, delta: Long) { try { consumer.onEvent(this, SimResourceEvent.Exit) - logic.onFinish(this) + logic.onFinish(this, now, delta) } catch (cause: Throwable) { - doFail(cause) + doFail(now, delta, cause) } finally { _deadline = Long.MAX_VALUE _limit = 0.0 @@ -273,7 +292,7 @@ internal class SimResourceContextImpl( /** * Fail the resource consumer. */ - private fun doFail(cause: Throwable) { + private fun doFail(now: Long, delta: Long, cause: Throwable) { try { consumer.onFailure(this, cause) } catch (e: Throwable) { @@ -281,7 +300,7 @@ internal class SimResourceContextImpl( e.printStackTrace() } - logic.onFinish(this) + logic.onFinish(this, now, delta) } /** diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt index 2b6ec2ba..2abf0749 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt @@ -63,7 +63,7 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, /** * The systems that have been visited during the interpreter cycle. */ - private val visited = linkedSetOf() + private val visited = linkedSetOf() /** * The index in the batch stack. @@ -81,10 +81,7 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, */ fun scheduleSync(now: Long, ctx: SimResourceContextImpl) { ctx.doUpdate(now) - - if (visited.add(ctx)) { - collectAncestors(ctx, visited) - } + visited.add(ctx) // In-case the interpreter is already running in the call-stack, return immediately. The changes will be picked // up by the active interpreter. @@ -143,11 +140,7 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, return timer } - override fun newContext( - consumer: SimResourceConsumer, - provider: SimResourceProviderLogic, - parent: SimResourceSystem? - ): SimResourceControllableContext = SimResourceContextImpl(parent, this, consumer, provider) + override fun newContext(consumer: SimResourceConsumer, provider: SimResourceProviderLogic): SimResourceControllableContext = SimResourceContextImpl(this, consumer, provider) override fun pushBatch() { batchIndex++ @@ -200,10 +193,7 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, if (ctx.shouldUpdate(now)) { ctx.doUpdate(now) - - if (visited.add(ctx)) { - collectAncestors(ctx, visited) - } + visited.add(ctx) } else { ctx.tryReschedule(now) } @@ -218,10 +208,7 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, if (ctx.shouldUpdate(now)) { ctx.doUpdate(now) - - if (visited.add(ctx)) { - collectAncestors(ctx, visited) - } + visited.add(ctx) } } @@ -239,17 +226,6 @@ internal class SimResourceInterpreterImpl(private val context: CoroutineContext, } } - /** - * Collect all the ancestors of the specified [system]. - */ - private tailrec fun collectAncestors(system: SimResourceSystem, systems: MutableSet) { - val parent = system.parent - if (parent != null) { - systems.add(parent) - collectAncestors(parent, systems) - } - } - /** * Try to schedule an interpreter invocation at the specified [target]. * diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt index c7230a0e..1428ce42 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt @@ -50,7 +50,7 @@ class SimResourceContextTest { } val logic = object : SimResourceProviderLogic {} - val context = SimResourceContextImpl(null, interpreter, consumer, logic) + val context = SimResourceContextImpl(interpreter, consumer, logic) interpreter.scheduleSync(interpreter.clock.millis(), context) } @@ -60,18 +60,15 @@ class SimResourceContextTest { val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) val consumer = SimWorkConsumer(1.0, 1.0) - val logic = spyk(object : SimResourceProviderLogic { - override fun onFinish(ctx: SimResourceControllableContext) {} - override fun onConsume(ctx: SimResourceControllableContext, now: Long, limit: Double, duration: Long): Long = duration - }) - val context = SimResourceContextImpl(null, interpreter, consumer, logic) + val logic = spyk(object : SimResourceProviderLogic {}) + val context = SimResourceContextImpl(interpreter, consumer, logic) context.capacity = 1.0 context.start() delay(1) // Delay 1 ms to prevent hitting the fast path interpreter.scheduleSync(interpreter.clock.millis(), context) - verify(exactly = 2) { logic.onConsume(any(), any(), any(), any()) } + verify(exactly = 2) { logic.onConsume(any(), any(), any(), any(), any()) } } @Test @@ -80,7 +77,7 @@ class SimResourceContextTest { val consumer = SimWorkConsumer(1.0, 1.0) val logic = spyk(object : SimResourceProviderLogic {}) - val context = SimResourceContextImpl(null, interpreter, consumer, logic) + val context = SimResourceContextImpl(interpreter, consumer, logic) context.capacity = 1.0 context.start() @@ -90,8 +87,8 @@ class SimResourceContextTest { context.invalidate() assertAll( - { verify(exactly = 2) { logic.onConsume(any(), any(), any(), any()) } }, - { verify(exactly = 1) { logic.onFinish(any()) } } + { verify(exactly = 2) { logic.onConsume(any(), any(), any(), any(), any()) } }, + { verify(exactly = 1) { logic.onFinish(any(), any(), any()) } } ) } @@ -111,7 +108,7 @@ class SimResourceContextTest { } val logic = object : SimResourceProviderLogic {} - val context = SimResourceContextImpl(null, interpreter, consumer, logic) + val context = SimResourceContextImpl(interpreter, consumer, logic) context.start() @@ -136,8 +133,7 @@ class SimResourceContextTest { }) val logic = object : SimResourceProviderLogic {} - - val context = SimResourceContextImpl(null, interpreter, consumer, logic) + val context = SimResourceContextImpl(interpreter, consumer, logic) context.capacity = 4200.0 context.start() context.capacity = 4200.0 @@ -166,7 +162,7 @@ class SimResourceContextTest { val logic = object : SimResourceProviderLogic {} - val context = SimResourceContextImpl(null, interpreter, consumer, logic) + val context = SimResourceContextImpl(interpreter, consumer, logic) context.start() diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt index fc43c3da..d7d0924f 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt @@ -223,6 +223,9 @@ internal class SimResourceTransformerTest { forwarder.consume(consumer) + yield() + + assertEquals(2.0, source.counters.actual) assertEquals(source.counters.actual, forwarder.counters.actual) { "Actual work" } assertEquals(source.counters.demand, forwarder.counters.demand) { "Work demand" } assertEquals(source.counters.overcommit, forwarder.counters.overcommit) { "Overcommitted work" } -- cgit v1.2.3 From 7703fc9fcc847208b1803a58d9eaa5938d2c77a1 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 29 Sep 2021 15:02:38 +0200 Subject: refactor(simulator): Do not expose SimResourceState This change hides the SimResourceState from public API since it is not actively used outside of the `SimResourceContextImpl` class. --- .../resources/SimResourceControllableContext.kt | 5 --- .../opendc/simulator/resources/SimResourceState.kt | 43 ------------------- .../resources/impl/SimResourceContextImpl.kt | 50 +++++++++++++++------- 3 files changed, 34 insertions(+), 64 deletions(-) delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceState.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt index ba52b597..b406b896 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt @@ -28,11 +28,6 @@ package org.opendc.simulator.resources * This interface is used by resource providers to control the resource context. */ public interface SimResourceControllableContext : SimResourceContext { - /** - * The state of the resource context. - */ - public val state: SimResourceState - /** * The capacity of the resource. */ diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceState.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceState.kt deleted file mode 100644 index c72951d0..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceState.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.resources - -/** - * The state of a resource provider. - */ -public enum class SimResourceState { - /** - * The resource provider is pending and the resource is waiting to be consumed. - */ - Pending, - - /** - * The resource provider is active and the resource is currently being consumed. - */ - Active, - - /** - * The resource provider is stopped and the resource cannot be consumed anymore. - */ - Stopped -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt index 5a9ffe2d..cbfa7afd 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt @@ -60,9 +60,7 @@ internal class SimResourceContextImpl( /** * A flag to indicate the state of the context. */ - override val state: SimResourceState - get() = _state - private var _state = SimResourceState.Pending + private var _state = State.Pending /** * The current processing speed of the resource. @@ -106,21 +104,21 @@ internal class SimResourceContextImpl( private val _timers: ArrayDeque = ArrayDeque() override fun start() { - check(_state == SimResourceState.Pending) { "Consumer is already started" } + check(_state == State.Pending) { "Consumer is already started" } interpreter.batch { consumer.onEvent(this, SimResourceEvent.Start) - _state = SimResourceState.Active + _state = State.Active interrupt() } } override fun close() { - if (_state == SimResourceState.Stopped) { + if (_state == State.Stopped) { return } interpreter.batch { - _state = SimResourceState.Stopped + _state = State.Stopped if (!_updateActive) { val now = clock.millis() val delta = max(0, now - _lastUpdate) @@ -134,7 +132,7 @@ internal class SimResourceContextImpl( } override fun interrupt() { - if (_state == SimResourceState.Stopped) { + if (_state == State.Stopped) { return } @@ -143,7 +141,7 @@ internal class SimResourceContextImpl( } override fun invalidate() { - if (_state == SimResourceState.Stopped) { + if (_state == State.Stopped) { return } @@ -152,7 +150,7 @@ internal class SimResourceContextImpl( } override fun flush() { - if (_state == SimResourceState.Stopped) { + if (_state == State.Stopped) { return } @@ -186,7 +184,7 @@ internal class SimResourceContextImpl( */ fun doUpdate(now: Long) { val oldState = _state - if (oldState != SimResourceState.Active) { + if (oldState != State.Active) { return } @@ -206,14 +204,14 @@ internal class SimResourceContextImpl( // Check whether the state has changed after [consumer.onNext] when (_state) { - SimResourceState.Active -> { + State.Active -> { logic.onConsume(this, now, delta, _limit, duration) // Schedule an update at the new deadline scheduleUpdate(now, newDeadline) } - SimResourceState.Stopped -> doStop(now, delta) - SimResourceState.Pending -> throw IllegalStateException("Illegal transition to pending state") + State.Stopped -> doStop(now, delta) + State.Pending -> throw IllegalStateException("Illegal transition to pending state") } // Note: pending limit might be changed by [logic.onConsume], so re-fetch the value @@ -262,7 +260,7 @@ internal class SimResourceContextImpl( _lastConvergence = timestamp try { - if (_state == SimResourceState.Active) { + if (_state == State.Active) { consumer.onEvent(this, SimResourceEvent.Run) } @@ -308,7 +306,7 @@ internal class SimResourceContextImpl( */ private fun onCapacityChange() { // Do not inform the consumer if it has not been started yet - if (state != SimResourceState.Active) { + if (_state != State.Active) { return } @@ -337,6 +335,26 @@ internal class SimResourceContextImpl( } } + /** + * The state of a resource context. + */ + private enum class State { + /** + * The resource context is pending and the resource is waiting to be consumed. + */ + Pending, + + /** + * The resource context is active and the resource is currently being consumed. + */ + Active, + + /** + * The resource context is stopped and the resource cannot be consumed anymore. + */ + Stopped + } + /** * A flag to indicate that the context should be invalidated. */ -- cgit v1.2.3 From 0272738f3ba5faeb0b5463464ff2d961136bb317 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 29 Sep 2021 15:04:54 +0200 Subject: refactor(simulator): Remove SimResourceFlow interface --- .../opendc/simulator/resources/SimResourceFlow.kt | 29 ---------------------- .../simulator/resources/SimResourceTransformer.kt | 5 ++-- 2 files changed, 3 insertions(+), 31 deletions(-) delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceFlow.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceFlow.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceFlow.kt deleted file mode 100644 index bbf6ad44..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceFlow.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.resources - -/** - * A [SimResourceFlow] acts as both a resource consumer and resource provider at the same time, simplifying bridging - * between different components. - */ -public interface SimResourceFlow : SimResourceConsumer, SimResourceProvider diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt index 397463e0..76a3bdd7 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt @@ -26,7 +26,8 @@ import org.opendc.simulator.resources.impl.SimResourceCountersImpl import java.time.Clock /** - * A [SimResourceFlow] that transforms the resource commands emitted by the resource commands to the resource provider. + * A [SimResourceConsumer] and [SimResourceProvider] that transforms the resource commands emitted by the resource + * commands to the resource provider. * * @param isCoupled A flag to indicate that the transformer will exit when the resource consumer exits. * @param transform The function to transform the received resource command. @@ -34,7 +35,7 @@ import java.time.Clock public class SimResourceTransformer( private val isCoupled: Boolean = false, private val transform: (SimResourceContext, Long) -> Long -) : SimResourceFlow, AutoCloseable { +) : SimResourceConsumer, SimResourceProvider, AutoCloseable { /** * The delegate [SimResourceConsumer]. */ -- cgit v1.2.3 From f00c5f3663a2bdbfacc2d6f812503f22f1ed26bb Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 29 Sep 2021 16:06:48 +0200 Subject: refactor(simulator): Eliminate usage of distributor and aggregator This change removes the use of distributor and aggregator from the other OpenDC components. For the future, we focus on maintaining a single SimResourceSwitch implementation to achieve both use-cases. --- .../main/kotlin/org/opendc/simulator/power/SimPdu.kt | 19 ++++++++++++++----- .../main/kotlin/org/opendc/simulator/power/SimUps.kt | 13 +++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt index b0ea7f0a..e5fcd938 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt @@ -37,18 +37,27 @@ public class SimPdu( private val lossCoefficient: Double = 0.0, ) : SimPowerInlet() { /** - * The [SimResourceDistributor] that distributes the electricity over the PDU outlets. + * The [SimResourceSwitch] that distributes the electricity over the PDU outlets. */ - private val distributor = SimResourceDistributorMaxMin(interpreter) + private val switch = SimResourceSwitchMaxMin(interpreter) + + /** + * The [SimResourceTransformer] that represents the input of the PDU. + */ + private val forwarder = SimResourceForwarder() /** * Create a new PDU outlet. */ - public fun newOutlet(): Outlet = Outlet(distributor.newOutput()) + public fun newOutlet(): Outlet = Outlet(switch.newOutput()) + + init { + switch.addInput(forwarder) + } - override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer by distributor { + override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer by forwarder { override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - val duration = distributor.onNext(ctx, now, delta) + val duration = forwarder.onNext(ctx, now, delta) val loss = computePowerLoss(ctx.demand) val newLimit = ctx.demand + loss diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt index 59006dfc..79c1b37d 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt @@ -41,20 +41,25 @@ public class SimUps( /** * The resource aggregator used to combine the input sources. */ - private val aggregator = SimResourceAggregatorMaxMin(interpreter) + private val switch = SimResourceSwitchMaxMin(interpreter) + + /** + * The [SimResourceProvider] that represents the output of the UPS. + */ + private val provider = switch.newOutput() /** * Create a new UPS outlet. */ public fun newInlet(): SimPowerInlet { val forward = SimResourceForwarder(isCoupled = true) - aggregator.addInput(forward) + switch.addInput(forward) return Inlet(forward) } override fun onConnect(inlet: SimPowerInlet) { val consumer = inlet.createConsumer() - aggregator.startConsumer(object : SimResourceConsumer by consumer { + provider.startConsumer(object : SimResourceConsumer by consumer { override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { val duration = consumer.onNext(ctx, now, delta) val loss = computePowerLoss(ctx.demand) @@ -67,7 +72,7 @@ public class SimUps( } override fun onDisconnect(inlet: SimPowerInlet) { - aggregator.cancel() + provider.cancel() } /** -- cgit v1.2.3 From d2f15fd7fd16922c11b0dcaa8807e8a321859773 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 29 Sep 2021 22:05:03 +0200 Subject: refactor(simulator): Merge distributor and aggregator into switch This change removes the distributor and aggregator interfaces in favour of a single switch interface. Since the switch interface is as powerful as both the distributor and aggregator, we don't need the latter two. --- .../compute/kernel/SimAbstractHypervisor.kt | 14 +- .../simulator/compute/kernel/SimHypervisorTest.kt | 2 + .../simulator/network/SimNetworkSwitchVirtual.kt | 2 +- .../kotlin/org/opendc/simulator/power/SimPdu.kt | 6 +- .../resources/SimAbstractResourceAggregator.kt | 147 --------- .../simulator/resources/SimResourceAggregator.kt | 38 --- .../resources/SimResourceAggregatorMaxMin.kt | 64 ---- .../resources/SimResourceCloseableProvider.kt | 37 --- .../simulator/resources/SimResourceCounters.kt | 5 + .../simulator/resources/SimResourceDistributor.kt | 42 --- .../resources/SimResourceDistributorMaxMin.kt | 332 ------------------- .../simulator/resources/SimResourceSwitch.kt | 16 +- .../resources/SimResourceSwitchExclusive.kt | 64 ++-- .../simulator/resources/SimResourceSwitchMaxMin.kt | 361 +++++++++++++++++++-- .../resources/impl/SimResourceCountersImpl.kt | 6 +- .../resources/SimResourceAggregatorMaxMinTest.kt | 190 ----------- .../resources/SimResourceSwitchExclusiveTest.kt | 28 +- .../resources/SimResourceSwitchMaxMinTest.kt | 6 +- 18 files changed, 422 insertions(+), 938 deletions(-) delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregator.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCloseableProvider.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributor.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt index 98271fb0..cf9e3230 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt @@ -131,7 +131,7 @@ public abstract class SimAbstractHypervisor( /** * The vCPUs of the machine. */ - override val cpus = model.cpus.map { VCpu(switch.newOutput(interferenceKey), it) } + override val cpus = model.cpus.map { VCpu(switch, switch.newOutput(interferenceKey), it) } override fun close() { super.close() @@ -153,9 +153,10 @@ public abstract class SimAbstractHypervisor( * A [SimProcessingUnit] of a virtual machine. */ private class VCpu( - private val source: SimResourceCloseableProvider, + private val switch: SimResourceSwitch, + private val source: SimResourceProvider, override val model: ProcessingUnit - ) : SimProcessingUnit, SimResourceCloseableProvider by source { + ) : SimProcessingUnit, SimResourceProvider by source { override var capacity: Double get() = source.capacity set(_) { @@ -163,6 +164,13 @@ public abstract class SimAbstractHypervisor( } override fun toString(): String = "SimAbstractHypervisor.VCpu[model=$model]" + + /** + * Close the CPU + */ + fun close() { + switch.removeOutput(source) + } } /** diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt index 1f010338..8cd535ad 100644 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt @@ -103,8 +103,10 @@ internal class SimHypervisorTest { println("Hypervisor finished") } yield() + val vm = hypervisor.createMachine(model) vm.run(workloadA) + yield() machine.close() diff --git a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt index 05daaa5c..2267715e 100644 --- a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt +++ b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt @@ -70,7 +70,7 @@ public class SimNetworkSwitchVirtual(interpreter: SimResourceInterpreter) : SimN override fun close() { isClosed = true - _provider.close() + switch.removeOutput(_provider) _ports.remove(this) } } diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt index e5fcd938..947f6cb2 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt @@ -49,7 +49,7 @@ public class SimPdu( /** * Create a new PDU outlet. */ - public fun newOutlet(): Outlet = Outlet(switch.newOutput()) + public fun newOutlet(): Outlet = Outlet(switch, switch.newOutput()) init { switch.addInput(forwarder) @@ -81,7 +81,7 @@ public class SimPdu( /** * A PDU outlet. */ - public class Outlet(private val provider: SimResourceCloseableProvider) : SimPowerOutlet(), AutoCloseable { + public class Outlet(private val switch: SimResourceSwitch, private val provider: SimResourceProvider) : SimPowerOutlet(), AutoCloseable { override fun onConnect(inlet: SimPowerInlet) { provider.startConsumer(inlet.createConsumer()) } @@ -94,7 +94,7 @@ public class SimPdu( * Remove the outlet from the PDU. */ override fun close() { - provider.close() + switch.removeOutput(provider) } override fun toString(): String = "SimPdu.Outlet" diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt deleted file mode 100644 index 621ea6e7..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceAggregator.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.resources - -/** - * Abstract implementation of [SimResourceAggregator]. - */ -public abstract class SimAbstractResourceAggregator( - interpreter: SimResourceInterpreter, - parent: SimResourceSystem? -) : SimResourceAggregator { - /** - * This method is invoked when the resource consumer consumes resources. - */ - protected abstract fun doConsume(limit: Double) - - /** - * This method is invoked when the resource consumer finishes processing. - */ - protected abstract fun doFinish() - - /** - * This method is invoked when an input context is started. - */ - protected abstract fun onInputStarted(input: SimResourceContext) - - /** - * This method is invoked when an input is stopped. - */ - protected abstract fun onInputFinished(input: SimResourceContext) - - /* SimResourceAggregator */ - override fun addInput(input: SimResourceProvider) { - val consumer = Consumer() - _inputs.add(input) - _inputConsumers.add(consumer) - input.startConsumer(consumer) - } - - override val inputs: Set - get() = _inputs - private val _inputs = mutableSetOf() - private val _inputConsumers = mutableListOf() - - /* SimResourceProvider */ - override val isActive: Boolean - get() = _output.isActive - - override val capacity: Double - get() = _output.capacity - - override val speed: Double - get() = _output.speed - - override val demand: Double - get() = _output.demand - - override val counters: SimResourceCounters - get() = _output.counters - - override fun startConsumer(consumer: SimResourceConsumer) { - _output.startConsumer(consumer) - } - - override fun cancel() { - _output.cancel() - } - - override fun interrupt() { - _output.interrupt() - } - - private val _output = object : SimAbstractResourceProvider(interpreter, initialCapacity = 0.0) { - override fun createLogic(): SimResourceProviderLogic { - return object : SimResourceProviderLogic { - - override fun onConsume(ctx: SimResourceControllableContext, now: Long, delta: Long, limit: Double, duration: Long) { - updateCounters(ctx, delta) - doConsume(limit) - } - - override fun onFinish(ctx: SimResourceControllableContext, now: Long, delta: Long) { - updateCounters(ctx, delta) - doFinish() - } - - override fun onConverge(ctx: SimResourceControllableContext, now: Long, delta: Long) { - parent?.onConverge(now) - } - } - } - } - - /** - * An internal [SimResourceConsumer] implementation for aggregator inputs. - */ - private inner class Consumer : SimResourceConsumer { - /** - * The resource context associated with the input. - */ - private var _ctx: SimResourceContext? = null - - private fun updateCapacity() { - // Adjust capacity of output resource - _output.capacity = _inputConsumers.sumOf { it._ctx?.capacity ?: 0.0 } - } - - /* SimResourceConsumer */ - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return Long.MAX_VALUE - } - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - when (event) { - SimResourceEvent.Start -> { - _ctx = ctx - updateCapacity() - - onInputStarted(ctx) - } - SimResourceEvent.Capacity -> updateCapacity() - SimResourceEvent.Exit -> onInputFinished(ctx) - else -> {} - } - } - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregator.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregator.kt deleted file mode 100644 index 00972f43..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregator.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.resources - -/** - * A [SimResourceAggregator] aggregates the capacity of multiple resources into a single resource. - */ -public interface SimResourceAggregator : SimResourceProvider { - /** - * The input resources that will be switched between the output providers. - */ - public val inputs: Set - - /** - * Add the specified [input] to the aggregator. - */ - public fun addInput(input: SimResourceProvider) -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt deleted file mode 100644 index f131ac6c..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMin.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.resources - -/** - * A [SimResourceAggregator] that distributes the load equally across the input resources. - */ -public class SimResourceAggregatorMaxMin( - interpreter: SimResourceInterpreter, - parent: SimResourceSystem? = null -) : SimAbstractResourceAggregator(interpreter, parent) { - private val consumers = mutableListOf() - - override fun doConsume(limit: Double) { - // Sort all consumers by their capacity - consumers.sortWith(compareBy { it.capacity }) - - // Divide the requests over the available capacity of the input resources fairly - for (input in consumers) { - val inputCapacity = input.capacity - val fraction = inputCapacity / capacity - val grantedSpeed = limit * fraction - - input.push(grantedSpeed) - input.interrupt() - } - } - - override fun doFinish() { - val iterator = consumers.iterator() - for (input in iterator) { - iterator.remove() - input.close() - } - } - - override fun onInputStarted(input: SimResourceContext) { - consumers.add(input) - } - - override fun onInputFinished(input: SimResourceContext) { - consumers.remove(input) - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCloseableProvider.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCloseableProvider.kt deleted file mode 100644 index bce8274b..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCloseableProvider.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.resources - -/** - * A [SimResourceProvider] that has a controllable and limited lifetime. - * - * This interface is used to signal that the resource provider may be closed and not reused after that point. - */ -public interface SimResourceCloseableProvider : SimResourceProvider, AutoCloseable { - /** - * End the lifetime of the resource provider. - * - * This operation cancels the existing resource consumer and prevents the resource provider from being reused. - */ - public override fun close() -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCounters.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCounters.kt index 725aa5bc..11924db2 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCounters.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCounters.kt @@ -41,6 +41,11 @@ public interface SimResourceCounters { */ public val overcommit: Double + /** + * The amount of work lost due to interference. + */ + public val interference: Double + /** * Reset the resource counters. */ diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributor.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributor.kt deleted file mode 100644 index f384582f..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributor.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.resources - -import org.opendc.simulator.resources.interference.InterferenceKey - -/** - * A [SimResourceDistributor] distributes the capacity of some resource over multiple resource consumers. - */ -public interface SimResourceDistributor : SimResourceConsumer { - /** - * The output resource providers to which resource consumers can be attached. - */ - public val outputs: Set - - /** - * Create a new output for the distributor. - * - * @param key The key of the interference member to which the output belongs. - */ - public fun newOutput(key: InterferenceKey? = null): SimResourceCloseableProvider -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt deleted file mode 100644 index 7df940ad..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceDistributorMaxMin.kt +++ /dev/null @@ -1,332 +0,0 @@ -/* - * 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.resources - -import org.opendc.simulator.resources.interference.InterferenceDomain -import org.opendc.simulator.resources.interference.InterferenceKey -import kotlin.math.max -import kotlin.math.min - -/** - * A [SimResourceDistributor] that distributes the capacity of a resource over consumers using max-min fair sharing. - * - * @param interpreter The interpreter for managing the resource contexts. - * @param parent The parent resource system of the distributor. - * @param interferenceDomain The interference domain of the distributor. - */ -public class SimResourceDistributorMaxMin( - private val interpreter: SimResourceInterpreter, - private val parent: SimResourceSystem? = null, - private val interferenceDomain: InterferenceDomain? = null -) : SimResourceDistributor { - override val outputs: Set - get() = _outputs - private val _outputs = mutableSetOf() - - /** - * The resource context of the consumer. - */ - private var ctx: SimResourceContext? = null - - /** - * The active outputs. - */ - private val activeOutputs: MutableList = mutableListOf() - - /** - * The total allocated speed for the output resources. - */ - private var totalAllocatedSpeed = 0.0 - - /** - * The total requested speed for the output resources. - */ - private var totalRequestedSpeed = 0.0 - - /** - * The resource counters of this distributor. - */ - public val counters: Counters - get() = _counters - private val _counters = object : Counters { - override var demand: Double = 0.0 - override var actual: Double = 0.0 - override var overcommit: Double = 0.0 - override var interference: Double = 0.0 - - override fun reset() { - demand = 0.0 - actual = 0.0 - overcommit = 0.0 - interference = 0.0 - } - - override fun toString(): String = "SimResourceDistributorMaxMin.Counters[demand=$demand,actual=$actual,overcommit=$overcommit,interference=$interference]" - } - - /* SimResourceDistributor */ - override fun newOutput(key: InterferenceKey?): SimResourceCloseableProvider { - val provider = Output(ctx?.capacity ?: 0.0, key) - _outputs.add(provider) - return provider - } - - /* SimResourceConsumer */ - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - // If there is no work yet, mark the input as idle. - if (activeOutputs.isEmpty()) { - return Long.MAX_VALUE - } - - val capacity = ctx.capacity - var duration: Long = Long.MAX_VALUE - var availableSpeed = capacity - var totalRequestedSpeed = 0.0 - - // Pull in the work of the outputs - val outputIterator = activeOutputs.listIterator() - for (output in outputIterator) { - output.pull(now) - - // Remove outputs that have finished - if (!output.isActive) { - outputIterator.remove() - } - } - - // Sort in-place the outputs based on their requested usage. - // Profiling shows that it is faster than maintaining some kind of sorted set. - activeOutputs.sort() - - // Divide the available input capacity fairly across the outputs using max-min fair sharing - var remaining = activeOutputs.size - for (output in activeOutputs) { - val availableShare = availableSpeed / remaining-- - val grantedSpeed = min(output.allowedSpeed, availableShare) - - duration = min(duration, output.duration) - - // Ignore idle computation - if (grantedSpeed <= 0.0) { - output.actualSpeed = 0.0 - continue - } - - totalRequestedSpeed += output.limit - - output.actualSpeed = grantedSpeed - availableSpeed -= grantedSpeed - } - - val durationS = duration / 1000.0 - var totalRequestedWork = 0.0 - var totalAllocatedWork = 0.0 - for (output in activeOutputs) { - val limit = output.limit - val speed = output.actualSpeed - if (speed > 0.0) { - totalRequestedWork += limit * durationS - totalAllocatedWork += speed * durationS - } - } - - this.totalRequestedSpeed = totalRequestedSpeed - val totalAllocatedSpeed = capacity - availableSpeed - this.totalAllocatedSpeed = totalAllocatedSpeed - - ctx.push(totalAllocatedSpeed) - return duration - } - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - when (event) { - SimResourceEvent.Start -> { - this.ctx = ctx - updateCapacity(ctx) - } - SimResourceEvent.Exit -> { - val iterator = _outputs.iterator() - while (iterator.hasNext()) { - val output = iterator.next() - - // Remove the output from the outputs to prevent ConcurrentModificationException when removing it - // during the call to output.close() - iterator.remove() - - output.close() - } - } - SimResourceEvent.Capacity -> updateCapacity(ctx) - else -> {} - } - } - - /** - * Extended [SimResourceCounters] interface for the distributor. - */ - public interface Counters : SimResourceCounters { - /** - * The amount of work lost due to interference. - */ - public val interference: Double - } - - private fun updateCapacity(ctx: SimResourceContext) { - for (output in _outputs) { - output.capacity = ctx.capacity - } - } - - /** - * An internal [SimResourceProvider] implementation for switch outputs. - */ - private inner class Output(capacity: Double, val key: InterferenceKey?) : - SimAbstractResourceProvider(interpreter, capacity), - SimResourceCloseableProvider, - SimResourceProviderLogic, - Comparable { - /** - * A flag to indicate that the output is closed. - */ - private var isClosed: Boolean = false - - /** - * The requested limit. - */ - @JvmField var limit: Double = 0.0 - - /** - * The current deadline. - */ - @JvmField var duration: Long = Long.MAX_VALUE - - /** - * The processing speed that is allowed by the model constraints. - */ - @JvmField var allowedSpeed: Double = 0.0 - - /** - * The actual processing speed. - */ - @JvmField var actualSpeed: Double = 0.0 - - /** - * The timestamp at which we received the last command. - */ - private var lastCommandTimestamp: Long = Long.MIN_VALUE - - /* SimAbstractResourceProvider */ - override fun createLogic(): SimResourceProviderLogic = this - - override fun start(ctx: SimResourceControllableContext) { - check(!isClosed) { "Cannot re-use closed output" } - - activeOutputs += this - interpreter.batch { - ctx.start() - // Interrupt the input to re-schedule the resources - this@SimResourceDistributorMaxMin.ctx?.interrupt() - } - } - - override fun close() { - isClosed = true - cancel() - _outputs.remove(this) - } - - /* SimResourceProviderLogic */ - override fun onConsume( - ctx: SimResourceControllableContext, - now: Long, - delta: Long, - limit: Double, - duration: Long - ) { - doUpdateCounters(delta) - - allowedSpeed = min(ctx.capacity, limit) - actualSpeed = 0.0 - this.limit = limit - this.duration = duration - lastCommandTimestamp = now - } - - override fun onConverge(ctx: SimResourceControllableContext, now: Long, delta: Long) { - parent?.onConverge(now) - } - - override fun onFinish(ctx: SimResourceControllableContext, now: Long, delta: Long) { - doUpdateCounters(delta) - - limit = 0.0 - duration = Long.MAX_VALUE - actualSpeed = 0.0 - allowedSpeed = 0.0 - lastCommandTimestamp = now - } - - /* Comparable */ - override fun compareTo(other: Output): Int = allowedSpeed.compareTo(other.allowedSpeed) - - /** - * Pull the next command if necessary. - */ - fun pull(now: Long) { - val ctx = ctx - if (ctx != null && lastCommandTimestamp < now) { - ctx.flush() - } - } - - /** - * Helper method to update the resource counters of the distributor. - */ - private fun doUpdateCounters(delta: Long) { - if (delta <= 0L) { - return - } - - // Compute the performance penalty due to resource interference - val perfScore = if (interferenceDomain != null) { - val load = totalAllocatedSpeed / requireNotNull(this@SimResourceDistributorMaxMin.ctx).capacity - interferenceDomain.apply(key, load) - } else { - 1.0 - } - - val deltaS = delta / 1000.0 - val work = limit * deltaS - val actualWork = actualSpeed * deltaS - val remainingWork = work - actualWork - - updateCounters(work, actualWork, remainingWork) - - val distCounters = _counters - distCounters.demand += work - distCounters.actual += actualWork - distCounters.overcommit += remainingWork - distCounters.interference += actualWork * max(0.0, 1 - perfScore) - } - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitch.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitch.kt index d2aab634..3c25b76d 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitch.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitch.kt @@ -27,11 +27,11 @@ import org.opendc.simulator.resources.interference.InterferenceKey /** * A [SimResourceSwitch] enables switching of capacity of multiple resources between multiple consumers. */ -public interface SimResourceSwitch : AutoCloseable { +public interface SimResourceSwitch { /** * The output resource providers to which resource consumers can be attached. */ - public val outputs: Set + public val outputs: Set /** * The input resources that will be switched between the output providers. @@ -48,10 +48,20 @@ public interface SimResourceSwitch : AutoCloseable { * * @param key The key of the interference member to which the output belongs. */ - public fun newOutput(key: InterferenceKey? = null): SimResourceCloseableProvider + public fun newOutput(key: InterferenceKey? = null): SimResourceProvider + + /** + * Remove [output] from this switch. + */ + public fun removeOutput(output: SimResourceProvider) /** * Add the specified [input] to the switch. */ public fun addInput(input: SimResourceProvider) + + /** + * Clear all inputs and outputs from the switch. + */ + public fun clear() } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt index fbb541e5..2be8ccb0 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt @@ -27,23 +27,17 @@ import java.util.ArrayDeque /** * A [SimResourceSwitch] implementation that allocates outputs to the inputs of the switch exclusively. This means that - * a single output is directly connected to an input and that the switch can only support as much outputs as inputs. + * a single output is directly connected to an input and that the switch can only support as many outputs as inputs. */ public class SimResourceSwitchExclusive : SimResourceSwitch { - /** - * A flag to indicate that the switch is closed. - */ - private var isClosed: Boolean = false - - private val _outputs = mutableSetOf() - override val outputs: Set + override val outputs: Set get() = _outputs - - private val availableResources = ArrayDeque() + private val _outputs = mutableSetOf() private val _inputs = mutableSetOf() override val inputs: Set get() = _inputs + private val _availableInputs = ArrayDeque() override val counters: SimResourceCounters = object : SimResourceCounters { override val demand: Double @@ -52,6 +46,8 @@ public class SimResourceSwitchExclusive : SimResourceSwitch { get() = _inputs.sumOf { it.counters.actual } override val overcommit: Double get() = _inputs.sumOf { it.counters.overcommit } + override val interference: Double + get() = _inputs.sumOf { it.counters.interference } override fun reset() { for (input in _inputs) { @@ -65,18 +61,25 @@ public class SimResourceSwitchExclusive : SimResourceSwitch { /** * Add an output to the switch. */ - override fun newOutput(key: InterferenceKey?): SimResourceCloseableProvider { - check(!isClosed) { "Switch has been closed" } - check(availableResources.isNotEmpty()) { "No capacity to serve request" } - val forwarder = availableResources.poll() - val output = Provider(forwarder) + override fun newOutput(key: InterferenceKey?): SimResourceProvider { + val forwarder = checkNotNull(_availableInputs.poll()) { "No capacity to serve request" } + val output = Output(forwarder) _outputs += output return output } - override fun addInput(input: SimResourceProvider) { - check(!isClosed) { "Switch has been closed" } + override fun removeOutput(output: SimResourceProvider) { + if (!_outputs.remove(output)) { + return + } + + (output as Output).close() + } + /** + * Add an input to the switch. + */ + override fun addInput(input: SimResourceProvider) { if (input in inputs) { return } @@ -84,7 +87,7 @@ public class SimResourceSwitchExclusive : SimResourceSwitch { val forwarder = SimResourceForwarder() _inputs += input - availableResources += forwarder + _availableInputs += forwarder input.startConsumer(object : SimResourceConsumer by forwarder { override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { @@ -98,18 +101,29 @@ public class SimResourceSwitchExclusive : SimResourceSwitch { }) } - override fun close() { - isClosed = true + override fun clear() { + for (input in _inputs) { + input.cancel() + } + _inputs.clear() - // Cancel all upstream subscriptions - _inputs.forEach(SimResourceProvider::cancel) + // Outputs are implicitly cancelled by the inputs forwarders + _outputs.clear() } - private inner class Provider(private val forwarder: SimResourceTransformer) : SimResourceCloseableProvider, SimResourceProvider by forwarder { - override fun close() { + /** + * An output of the resource switch. + */ + private inner class Output(private val forwarder: SimResourceTransformer) : SimResourceProvider by forwarder { + /** + * Close the output. + */ + fun close() { // We explicitly do not close the forwarder here in order to re-use it across output resources. _outputs -= this - availableResources += forwarder + _availableInputs += forwarder } + + override fun toString(): String = "SimResourceSwitchExclusive.Output" } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMin.kt index e368609f..574fb443 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMin.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMin.kt @@ -22,8 +22,11 @@ package org.opendc.simulator.resources +import org.opendc.simulator.resources.impl.SimResourceCountersImpl import org.opendc.simulator.resources.interference.InterferenceDomain import org.opendc.simulator.resources.interference.InterferenceKey +import kotlin.math.max +import kotlin.math.min /** * A [SimResourceSwitch] implementation that switches resource consumptions over the available resources using max-min @@ -34,69 +37,371 @@ import org.opendc.simulator.resources.interference.InterferenceKey * @param interferenceDomain The interference domain of the switch. */ public class SimResourceSwitchMaxMin( - interpreter: SimResourceInterpreter, - parent: SimResourceSystem? = null, - interferenceDomain: InterferenceDomain? = null + private val interpreter: SimResourceInterpreter, + private val parent: SimResourceSystem? = null, + private val interferenceDomain: InterferenceDomain? = null ) : SimResourceSwitch { /** * The output resource providers to which resource consumers can be attached. */ - override val outputs: Set - get() = distributor.outputs + override val outputs: Set + get() = _outputs + private val _outputs = mutableSetOf() + private val _activeOutputs: MutableList = mutableListOf() /** * The input resources that will be switched between the output providers. */ override val inputs: Set - get() = aggregator.inputs + get() = _inputs + private val _inputs = mutableSetOf() + private val _activeInputs = mutableListOf() /** - * The resource counters to track the execution metrics of all switch resources. + * The resource counters of this switch. */ - override val counters: SimResourceDistributorMaxMin.Counters - get() = distributor.counters + public override val counters: SimResourceCounters + get() = _counters + private val _counters = SimResourceCountersImpl() /** - * A flag to indicate that the switch was closed. + * The actual processing rate of the switch. */ - private var isClosed = false + private var _rate = 0.0 /** - * The aggregator to aggregate the resources. + * The demanded processing rate of the outputs. */ - private val aggregator = SimResourceAggregatorMaxMin(interpreter, parent) + private var _demand = 0.0 /** - * The distributor to distribute the aggregated resources. + * The capacity of the switch. */ - private val distributor = SimResourceDistributorMaxMin(interpreter, parent, interferenceDomain) + private var _capacity = 0.0 - init { - aggregator.startConsumer(distributor) - } + /** + * Flag to indicate that the scheduler is active. + */ + private var _schedulerActive = false /** * Add an output to the switch. */ - override fun newOutput(key: InterferenceKey?): SimResourceCloseableProvider { - check(!isClosed) { "Switch has been closed" } - - return distributor.newOutput(key) + override fun newOutput(key: InterferenceKey?): SimResourceProvider { + val provider = Output(_capacity, key) + _outputs.add(provider) + return provider } /** * Add the specified [input] to the switch. */ override fun addInput(input: SimResourceProvider) { - check(!isClosed) { "Switch has been closed" } + val consumer = Input(input) + if (_inputs.add(input)) { + _activeInputs.add(consumer) + input.startConsumer(consumer) + } + } + + /** + * Remove [output] from this switch. + */ + override fun removeOutput(output: SimResourceProvider) { + if (!_outputs.remove(output)) { + return + } + // This cast should always succeed since only `Output` instances should be added to _outputs + (output as Output).close() + } + + override fun clear() { + for (input in _activeInputs) { + input.cancel() + } + _activeInputs.clear() + + for (output in _activeOutputs) { + output.cancel() + } + _activeOutputs.clear() + } + + /** + * Run the scheduler of the switch. + */ + private fun runScheduler(now: Long) { + if (_schedulerActive) { + return + } + + _schedulerActive = true + try { + doSchedule(now) + } finally { + _schedulerActive = false + } + } + + /** + * Schedule the outputs over the input. + */ + private fun doSchedule(now: Long) { + // If there is no work yet, mark the input as idle. + if (_activeOutputs.isEmpty()) { + return + } + + val capacity = _capacity + var availableCapacity = capacity + + // Pull in the work of the outputs + val outputIterator = _activeOutputs.listIterator() + for (output in outputIterator) { + output.pull(now) + + // Remove outputs that have finished + if (!output.isActive) { + outputIterator.remove() + } + } + + var demand = 0.0 + + // Sort in-place the outputs based on their requested usage. + // Profiling shows that it is faster than maintaining some kind of sorted set. + _activeOutputs.sort() + + // Divide the available input capacity fairly across the outputs using max-min fair sharing + var remaining = _activeOutputs.size + for (output in _activeOutputs) { + val availableShare = availableCapacity / remaining-- + val grantedSpeed = min(output.allowedRate, availableShare) + + // Ignore idle computation + if (grantedSpeed <= 0.0) { + output.actualRate = 0.0 + continue + } + + demand += output.limit - aggregator.addInput(input) + output.actualRate = grantedSpeed + availableCapacity -= grantedSpeed + } + + val rate = capacity - availableCapacity + + _demand = demand + _rate = rate + + // Sort all consumers by their capacity + _activeInputs.sort() + + // Divide the requests over the available capacity of the input resources fairly + for (input in _activeInputs) { + val inputCapacity = input.capacity + val fraction = inputCapacity / capacity + val grantedSpeed = rate * fraction + + input.push(grantedSpeed) + } } - override fun close() { - if (!isClosed) { - isClosed = true - aggregator.cancel() + /** + * Recompute the capacity of the switch. + */ + private fun updateCapacity() { + val newCapacity = _activeInputs.sumOf(Input::capacity) + + // No-op if the capacity is unchanged + if (_capacity == newCapacity) { + return } + + _capacity = newCapacity + + for (output in _outputs) { + output.capacity = newCapacity + } + } + + /** + * An internal [SimResourceProvider] implementation for switch outputs. + */ + private inner class Output(capacity: Double, val key: InterferenceKey?) : + SimAbstractResourceProvider(interpreter, capacity), + SimResourceProviderLogic, + Comparable { + /** + * The requested limit. + */ + @JvmField var limit: Double = 0.0 + + /** + * The actual processing speed. + */ + @JvmField var actualRate: Double = 0.0 + + /** + * The processing speed that is allowed by the model constraints. + */ + val allowedRate: Double + get() = min(capacity, limit) + + /** + * A flag to indicate that the output is closed. + */ + private var _isClosed: Boolean = false + + /** + * The timestamp at which we received the last command. + */ + private var _lastPull: Long = Long.MIN_VALUE + + /** + * Close the output. + * + * This method is invoked when the user removes an output from the switch. + */ + fun close() { + _isClosed = true + cancel() + } + + /* SimAbstractResourceProvider */ + override fun createLogic(): SimResourceProviderLogic = this + + override fun start(ctx: SimResourceControllableContext) { + check(!_isClosed) { "Cannot re-use closed output" } + + _activeOutputs += this + super.start(ctx) + } + + /* SimResourceProviderLogic */ + override fun onConsume( + ctx: SimResourceControllableContext, + now: Long, + delta: Long, + limit: Double, + duration: Long + ) { + doUpdateCounters(delta) + + actualRate = 0.0 + this.limit = limit + _lastPull = now + + runScheduler(now) + } + + override fun onConverge(ctx: SimResourceControllableContext, now: Long, delta: Long) { + parent?.onConverge(now) + } + + override fun onFinish(ctx: SimResourceControllableContext, now: Long, delta: Long) { + doUpdateCounters(delta) + + limit = 0.0 + actualRate = 0.0 + _lastPull = now + } + + /* Comparable */ + override fun compareTo(other: Output): Int = allowedRate.compareTo(other.allowedRate) + + /** + * Pull the next command if necessary. + */ + fun pull(now: Long) { + val ctx = ctx + if (ctx != null && _lastPull < now) { + ctx.flush() + } + } + + /** + * Helper method to update the resource counters of the distributor. + */ + private fun doUpdateCounters(delta: Long) { + if (delta <= 0L) { + return + } + + // Compute the performance penalty due to resource interference + val perfScore = if (interferenceDomain != null) { + val load = _rate / capacity + interferenceDomain.apply(key, load) + } else { + 1.0 + } + + val deltaS = delta / 1000.0 + val work = limit * deltaS + val actualWork = actualRate * deltaS + val remainingWork = work - actualWork + + updateCounters(work, actualWork, remainingWork) + + val distCounters = _counters + distCounters.demand += work + distCounters.actual += actualWork + distCounters.overcommit += remainingWork + distCounters.interference += actualWork * max(0.0, 1 - perfScore) + } + } + + /** + * An internal [SimResourceConsumer] implementation for switch inputs. + */ + private inner class Input(private val provider: SimResourceProvider) : SimResourceConsumer, Comparable { + /** + * The active [SimResourceContext] of this consumer. + */ + private var _ctx: SimResourceContext? = null + + /** + * The capacity of this input. + */ + val capacity: Double + get() = _ctx?.capacity ?: 0.0 + + /** + * Push the specified rate to the provider. + */ + fun push(rate: Double) { + _ctx?.push(rate) + } + + /** + * Cancel this input. + */ + fun cancel() { + provider.cancel() + } + + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + runScheduler(now) + return Long.MAX_VALUE + } + + override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { + when (event) { + SimResourceEvent.Start -> { + assert(_ctx == null) { "Consumer running concurrently" } + _ctx = ctx + updateCapacity() + } + SimResourceEvent.Exit -> { + _ctx = null + updateCapacity() + } + SimResourceEvent.Capacity -> updateCapacity() + else -> {} + } + } + + override fun compareTo(other: Input): Int = capacity.compareTo(other.capacity) } } diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceCountersImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceCountersImpl.kt index 827019c5..01062179 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceCountersImpl.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceCountersImpl.kt @@ -31,12 +31,16 @@ internal class SimResourceCountersImpl : SimResourceCounters { override var demand: Double = 0.0 override var actual: Double = 0.0 override var overcommit: Double = 0.0 + override var interference: Double = 0.0 override fun reset() { demand = 0.0 actual = 0.0 overcommit = 0.0 + interference = 0.0 } - override fun toString(): String = "SimResourceCounters[demand=$demand,actual=$actual,overcommit=$overcommit]" + override fun toString(): String { + return "SimResourceCounters[demand=$demand,actual=$actual,overcommit=$overcommit,interference=$interference]" + } } diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt deleted file mode 100644 index f4ea5fe8..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceAggregatorMaxMinTest.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * 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.resources - -import io.mockk.spyk -import io.mockk.verify -import kotlinx.coroutines.* -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertAll -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.consumer.SimSpeedConsumerAdapter -import org.opendc.simulator.resources.consumer.SimWorkConsumer -import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl - -/** - * Test suite for the [SimResourceAggregatorMaxMin] class. - */ -internal class SimResourceAggregatorMaxMinTest { - @Test - fun testSingleCapacity() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val aggregator = SimResourceAggregatorMaxMin(scheduler) - val forwarder = SimResourceForwarder() - val sources = listOf( - forwarder, - SimResourceSource(1.0, scheduler) - ) - sources.forEach(aggregator::addInput) - - val consumer = SimWorkConsumer(1.0, 0.5) - val usage = mutableListOf() - val source = SimResourceSource(1.0, scheduler) - val adapter = SimSpeedConsumerAdapter(forwarder, usage::add) - source.startConsumer(adapter) - - aggregator.consume(consumer) - yield() - - assertAll( - { assertEquals(1000, clock.millis()) }, - { assertEquals(listOf(0.0, 0.5, 0.0), usage) } - ) - } - - @Test - fun testDoubleCapacity() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val aggregator = SimResourceAggregatorMaxMin(scheduler) - val sources = listOf( - SimResourceSource(1.0, scheduler), - SimResourceSource(1.0, scheduler) - ) - sources.forEach(aggregator::addInput) - - val consumer = SimWorkConsumer(2.0, 1.0) - val usage = mutableListOf() - val adapter = SimSpeedConsumerAdapter(consumer, usage::add) - - aggregator.consume(adapter) - yield() - assertAll( - { assertEquals(1000, clock.millis()) }, - { assertEquals(listOf(0.0, 2.0, 0.0), usage) } - ) - } - - @Test - fun testOvercommit() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val aggregator = SimResourceAggregatorMaxMin(scheduler) - val sources = listOf( - SimResourceSource(1.0, scheduler), - SimResourceSource(1.0, scheduler) - ) - sources.forEach(aggregator::addInput) - - val consumer = spyk(object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (now == 0L) { - ctx.push(4.0) - 1000 - } else { - ctx.close() - Long.MAX_VALUE - } - } - }) - - aggregator.consume(consumer) - yield() - assertEquals(1000, clock.millis()) - - verify(exactly = 2) { consumer.onNext(any(), any(), any()) } - } - - @Test - fun testAdjustCapacity() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val aggregator = SimResourceAggregatorMaxMin(scheduler) - val sources = listOf( - SimResourceSource(1.0, scheduler), - SimResourceSource(1.0, scheduler) - ) - sources.forEach(aggregator::addInput) - - val consumer = SimWorkConsumer(4.0, 1.0) - coroutineScope { - launch { aggregator.consume(consumer) } - delay(1000) - sources[0].capacity = 0.5 - } - yield() - assertEquals(2333, clock.millis()) - } - - @Test - fun testFailOverCapacity() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val aggregator = SimResourceAggregatorMaxMin(scheduler) - val sources = listOf( - SimResourceSource(1.0, scheduler), - SimResourceSource(1.0, scheduler) - ) - sources.forEach(aggregator::addInput) - - val consumer = SimWorkConsumer(1.0, 0.5) - coroutineScope { - launch { aggregator.consume(consumer) } - delay(500) - sources[0].capacity = 0.5 - } - yield() - assertEquals(1167, clock.millis()) - } - - @Test - fun testCounters() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val aggregator = SimResourceAggregatorMaxMin(scheduler) - val sources = listOf( - SimResourceSource(1.0, scheduler), - SimResourceSource(1.0, scheduler) - ) - sources.forEach(aggregator::addInput) - - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (now == 0L) { - ctx.push(4.0) - 1000 - } else { - ctx.close() - Long.MAX_VALUE - } - } - } - - aggregator.consume(consumer) - yield() - assertEquals(1000, clock.millis()) - assertEquals(2.0, aggregator.counters.actual) { "Actual work mismatch" } - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt index 9f86dc0d..49f2da5f 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt @@ -65,13 +65,8 @@ internal class SimResourceSwitchExclusiveTest { switch.addInput(forwarder) val provider = switch.newOutput() - - try { - provider.consume(workload) - yield() - } finally { - provider.close() - } + provider.consume(workload) + yield() assertAll( { assertEquals(listOf(0.0, 28.0, 3200.0, 0.0, 183.0, 0.0), speed) { "Correct speed" } }, @@ -95,13 +90,9 @@ internal class SimResourceSwitchExclusiveTest { switch.addInput(source) val provider = switch.newOutput() + provider.consume(workload) + yield() - try { - provider.consume(workload) - yield() - } finally { - provider.close() - } assertEquals(duration, clock.millis()) { "Took enough time" } } @@ -141,14 +132,9 @@ internal class SimResourceSwitchExclusiveTest { switch.addInput(source) val provider = switch.newOutput() - - try { - provider.consume(workload) - yield() - provider.consume(workload) - } finally { - provider.close() - } + provider.consume(workload) + yield() + provider.consume(workload) assertEquals(duration * 2, clock.millis()) { "Took enough time" } } diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt index ba0d66ff..03f90e21 100644 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt @@ -51,7 +51,7 @@ internal class SimResourceSwitchMaxMinTest { provider.consume(consumer) yield() } finally { - switch.close() + switch.clear() } } @@ -81,7 +81,7 @@ internal class SimResourceSwitchMaxMinTest { provider.consume(workload) yield() } finally { - switch.close() + switch.clear() } assertAll( @@ -133,7 +133,7 @@ internal class SimResourceSwitchMaxMinTest { yield() } finally { - switch.close() + switch.clear() } assertAll( { assertEquals(2073600.0, switch.counters.demand, "Requested work does not match") }, -- cgit v1.2.3 From dd605ab1f70fef1fbbed848e8ebbd6b231622273 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 29 Sep 2021 22:20:48 +0200 Subject: refactor(simulator): Remove transform from SimResourceForwarder This change removes the ability to transform the duration of a pull from the SimResourceForwarder class. This ability is not used anymore, since pushes are now done using a method instead of a command. --- .../kotlin/org/opendc/simulator/power/SimPdu.kt | 2 +- .../kotlin/org/opendc/simulator/power/SimUps.kt | 2 +- .../simulator/resources/SimResourceForwarder.kt | 220 +++++++++++++++++++ .../resources/SimResourceSwitchExclusive.kt | 4 +- .../simulator/resources/SimResourceTransformer.kt | 238 --------------------- .../resources/SimResourceForwarderTest.kt | 220 +++++++++++++++++++ .../resources/SimResourceTransformerTest.kt | 234 -------------------- 7 files changed, 444 insertions(+), 476 deletions(-) create mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceForwarder.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt create mode 100644 opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceForwarderTest.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt index 947f6cb2..1a12a52a 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt @@ -42,7 +42,7 @@ public class SimPdu( private val switch = SimResourceSwitchMaxMin(interpreter) /** - * The [SimResourceTransformer] that represents the input of the PDU. + * The [SimResourceForwarder] that represents the input of the PDU. */ private val forwarder = SimResourceForwarder() diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt index 79c1b37d..9c7400ed 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt @@ -86,7 +86,7 @@ public class SimUps( /** * A UPS inlet. */ - public inner class Inlet(private val forwarder: SimResourceTransformer) : SimPowerInlet(), AutoCloseable { + public inner class Inlet(private val forwarder: SimResourceForwarder) : SimPowerInlet(), AutoCloseable { override fun createConsumer(): SimResourceConsumer = forwarder /** diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceForwarder.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceForwarder.kt new file mode 100644 index 00000000..0cd2bfc7 --- /dev/null +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceForwarder.kt @@ -0,0 +1,220 @@ +/* + * 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.resources + +import org.opendc.simulator.resources.impl.SimResourceCountersImpl +import java.time.Clock + +/** + * A class that acts as a [SimResourceConsumer] and [SimResourceProvider] at the same time. + * + * @param isCoupled A flag to indicate that the transformer will exit when the resource consumer exits. + */ +public class SimResourceForwarder(private val isCoupled: Boolean = false) : SimResourceConsumer, SimResourceProvider, AutoCloseable { + /** + * The delegate [SimResourceConsumer]. + */ + private var delegate: SimResourceConsumer? = null + + /** + * A flag to indicate that the delegate was started. + */ + private var hasDelegateStarted: Boolean = false + + /** + * The exposed [SimResourceContext]. + */ + private val _ctx = object : SimResourceContext { + override val clock: Clock + get() = _innerCtx!!.clock + + override val capacity: Double + get() = _innerCtx?.capacity ?: 0.0 + + override val demand: Double + get() = _innerCtx?.demand ?: 0.0 + + override val speed: Double + get() = _innerCtx?.speed ?: 0.0 + + override fun interrupt() { + _innerCtx?.interrupt() + } + + override fun push(rate: Double) { + _innerCtx?.push(rate) + _limit = rate + } + + override fun close() { + val delegate = checkNotNull(delegate) { "Delegate not active" } + + if (isCoupled) + _innerCtx?.close() + else + _innerCtx?.push(0.0) + + // Warning: resumption of the continuation might change the entire state of the forwarder. Make sure we + // reset beforehand the existing state and check whether it has been updated afterwards + reset() + + delegate.onEvent(this, SimResourceEvent.Exit) + } + } + + /** + * The [SimResourceContext] in which the forwarder runs. + */ + private var _innerCtx: SimResourceContext? = null + + override val isActive: Boolean + get() = delegate != null + + override val capacity: Double + get() = _ctx.capacity + + override val speed: Double + get() = _ctx.speed + + override val demand: Double + get() = _ctx.demand + + override val counters: SimResourceCounters + get() = _counters + private val _counters = SimResourceCountersImpl() + + override fun startConsumer(consumer: SimResourceConsumer) { + check(delegate == null) { "Resource transformer already active" } + + delegate = consumer + + // Interrupt the provider to replace the consumer + interrupt() + } + + override fun interrupt() { + _ctx.interrupt() + } + + override fun cancel() { + val delegate = delegate + val ctx = _innerCtx + + if (delegate != null) { + this.delegate = null + + if (ctx != null) { + delegate.onEvent(this._ctx, SimResourceEvent.Exit) + } + } + } + + override fun close() { + val ctx = _innerCtx + + if (ctx != null) { + this._innerCtx = null + ctx.interrupt() + } + } + + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + val delegate = delegate + + if (!hasDelegateStarted) { + start() + } + + updateCounters(ctx, delta) + + return delegate?.onNext(this._ctx, now, delta) ?: Long.MAX_VALUE + } + + override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { + when (event) { + SimResourceEvent.Start -> { + _innerCtx = ctx + } + SimResourceEvent.Exit -> { + _innerCtx = null + + val delegate = delegate + if (delegate != null) { + reset() + delegate.onEvent(this._ctx, SimResourceEvent.Exit) + } + } + else -> delegate?.onEvent(this._ctx, event) + } + } + + override fun onFailure(ctx: SimResourceContext, cause: Throwable) { + _innerCtx = null + + val delegate = delegate + if (delegate != null) { + reset() + delegate.onFailure(this._ctx, cause) + } + } + + /** + * Start the delegate. + */ + private fun start() { + val delegate = delegate ?: return + delegate.onEvent(checkNotNull(_innerCtx), SimResourceEvent.Start) + + hasDelegateStarted = true + } + + /** + * Reset the delegate. + */ + private fun reset() { + delegate = null + hasDelegateStarted = false + } + + /** + * The requested speed. + */ + private var _limit: Double = 0.0 + + /** + * Update the resource counters for the transformer. + */ + private fun updateCounters(ctx: SimResourceContext, delta: Long) { + if (delta <= 0) { + return + } + + val counters = _counters + val deltaS = delta / 1000.0 + val work = _limit * deltaS + val actualWork = ctx.speed * deltaS + counters.demand += work + counters.actual += actualWork + counters.overcommit += (work - actualWork) + } +} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt index 2be8ccb0..f1e004d2 100644 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt +++ b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt @@ -37,7 +37,7 @@ public class SimResourceSwitchExclusive : SimResourceSwitch { private val _inputs = mutableSetOf() override val inputs: Set get() = _inputs - private val _availableInputs = ArrayDeque() + private val _availableInputs = ArrayDeque() override val counters: SimResourceCounters = object : SimResourceCounters { override val demand: Double @@ -114,7 +114,7 @@ public class SimResourceSwitchExclusive : SimResourceSwitch { /** * An output of the resource switch. */ - private inner class Output(private val forwarder: SimResourceTransformer) : SimResourceProvider by forwarder { + private inner class Output(private val forwarder: SimResourceForwarder) : SimResourceProvider by forwarder { /** * Close the output. */ diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt deleted file mode 100644 index 76a3bdd7..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceTransformer.kt +++ /dev/null @@ -1,238 +0,0 @@ -/* - * 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.resources - -import org.opendc.simulator.resources.impl.SimResourceCountersImpl -import java.time.Clock - -/** - * A [SimResourceConsumer] and [SimResourceProvider] that transforms the resource commands emitted by the resource - * commands to the resource provider. - * - * @param isCoupled A flag to indicate that the transformer will exit when the resource consumer exits. - * @param transform The function to transform the received resource command. - */ -public class SimResourceTransformer( - private val isCoupled: Boolean = false, - private val transform: (SimResourceContext, Long) -> Long -) : SimResourceConsumer, SimResourceProvider, AutoCloseable { - /** - * The delegate [SimResourceConsumer]. - */ - private var delegate: SimResourceConsumer? = null - - /** - * A flag to indicate that the delegate was started. - */ - private var hasDelegateStarted: Boolean = false - - /** - * The exposed [SimResourceContext]. - */ - private val ctx = object : SimResourceContext { - override val clock: Clock - get() = _ctx!!.clock - - override val capacity: Double - get() = _ctx?.capacity ?: 0.0 - - override val demand: Double - get() = _ctx?.demand ?: 0.0 - - override val speed: Double - get() = _ctx?.speed ?: 0.0 - - override fun interrupt() { - _ctx?.interrupt() - } - - override fun push(rate: Double) { - _ctx?.push(rate) - _limit = rate - } - - override fun close() { - val delegate = checkNotNull(delegate) { "Delegate not active" } - - if (isCoupled) - _ctx?.close() - else - _ctx?.push(0.0) - - // Warning: resumption of the continuation might change the entire state of the forwarder. Make sure we - // reset beforehand the existing state and check whether it has been updated afterwards - reset() - - delegate.onEvent(this, SimResourceEvent.Exit) - } - } - - /** - * The [SimResourceContext] in which the forwarder runs. - */ - private var _ctx: SimResourceContext? = null - - override val isActive: Boolean - get() = delegate != null - - override val capacity: Double - get() = ctx.capacity - - override val speed: Double - get() = ctx.speed - - override val demand: Double - get() = ctx.demand - - override val counters: SimResourceCounters - get() = _counters - private val _counters = SimResourceCountersImpl() - - override fun startConsumer(consumer: SimResourceConsumer) { - check(delegate == null) { "Resource transformer already active" } - - delegate = consumer - - // Interrupt the provider to replace the consumer - interrupt() - } - - override fun interrupt() { - ctx.interrupt() - } - - override fun cancel() { - val delegate = delegate - val ctx = _ctx - - if (delegate != null) { - this.delegate = null - - if (ctx != null) { - delegate.onEvent(this.ctx, SimResourceEvent.Exit) - } - } - } - - override fun close() { - val ctx = _ctx - - if (ctx != null) { - this._ctx = null - ctx.interrupt() - } - } - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - val delegate = delegate - - if (!hasDelegateStarted) { - start() - } - - updateCounters(ctx, delta) - - return if (delegate != null) { - transform(ctx, delegate.onNext(this.ctx, now, delta)) - } else { - Long.MAX_VALUE - } - } - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - when (event) { - SimResourceEvent.Start -> { - _ctx = ctx - } - SimResourceEvent.Exit -> { - _ctx = null - - val delegate = delegate - if (delegate != null) { - reset() - delegate.onEvent(this.ctx, SimResourceEvent.Exit) - } - } - else -> delegate?.onEvent(this.ctx, event) - } - } - - override fun onFailure(ctx: SimResourceContext, cause: Throwable) { - _ctx = null - - val delegate = delegate - if (delegate != null) { - reset() - delegate.onFailure(this.ctx, cause) - } - } - - /** - * Start the delegate. - */ - private fun start() { - val delegate = delegate ?: return - delegate.onEvent(checkNotNull(_ctx), SimResourceEvent.Start) - - hasDelegateStarted = true - } - - /** - * Reset the delegate. - */ - private fun reset() { - delegate = null - hasDelegateStarted = false - } - - /** - * The requested speed. - */ - private var _limit: Double = 0.0 - - /** - * Update the resource counters for the transformer. - */ - private fun updateCounters(ctx: SimResourceContext, delta: Long) { - if (delta <= 0) { - return - } - - val counters = _counters - val deltaS = delta / 1000.0 - val work = _limit * deltaS - val actualWork = ctx.speed * deltaS - counters.demand += work - counters.actual += actualWork - counters.overcommit += (work - actualWork) - } -} - -/** - * Constructs a [SimResourceTransformer] that forwards the received resource command with an identity transform. - * - * @param isCoupled A flag to indicate that the transformer will exit when the resource consumer exits. - */ -public fun SimResourceForwarder(isCoupled: Boolean = false): SimResourceTransformer { - return SimResourceTransformer(isCoupled) { _, command -> command } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceForwarderTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceForwarderTest.kt new file mode 100644 index 00000000..49e60f68 --- /dev/null +++ b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceForwarderTest.kt @@ -0,0 +1,220 @@ +/* + * 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.resources + +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.resources.consumer.SimWorkConsumer +import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl + +/** + * A test suite for the [SimResourceForwarder] class. + */ +internal class SimResourceForwarderTest { + @Test + fun testCancelImmediately() = runBlockingSimulation { + val forwarder = SimResourceForwarder() + val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) + val source = SimResourceSource(2000.0, scheduler) + + launch { source.consume(forwarder) } + + forwarder.consume(object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + ctx.close() + return Long.MAX_VALUE + } + }) + + forwarder.close() + source.cancel() + } + + @Test + fun testCancel() = runBlockingSimulation { + val forwarder = SimResourceForwarder() + val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) + val source = SimResourceSource(2000.0, scheduler) + + launch { source.consume(forwarder) } + + forwarder.consume(object : SimResourceConsumer { + var isFirst = true + + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + return if (isFirst) { + isFirst = false + ctx.push(1.0) + 10 * 1000 + } else { + ctx.close() + Long.MAX_VALUE + } + } + }) + + forwarder.close() + source.cancel() + } + + @Test + fun testState() = runBlockingSimulation { + val forwarder = SimResourceForwarder() + val consumer = object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + ctx.close() + return Long.MAX_VALUE + } + } + + assertFalse(forwarder.isActive) + + forwarder.startConsumer(consumer) + assertTrue(forwarder.isActive) + + assertThrows { forwarder.startConsumer(consumer) } + + forwarder.cancel() + assertFalse(forwarder.isActive) + + forwarder.close() + assertFalse(forwarder.isActive) + } + + @Test + fun testCancelPendingDelegate() = runBlockingSimulation { + val forwarder = SimResourceForwarder() + + val consumer = spyk(object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + ctx.close() + return Long.MAX_VALUE + } + }) + + forwarder.startConsumer(consumer) + forwarder.cancel() + + verify(exactly = 0) { consumer.onEvent(any(), SimResourceEvent.Exit) } + } + + @Test + fun testCancelStartedDelegate() = runBlockingSimulation { + val forwarder = SimResourceForwarder() + val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) + val source = SimResourceSource(2000.0, scheduler) + + val consumer = spyk(SimWorkConsumer(2000.0, 1.0)) + + source.startConsumer(forwarder) + yield() + forwarder.startConsumer(consumer) + yield() + forwarder.cancel() + + verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Start) } + verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Exit) } + } + + @Test + fun testCancelPropagation() = runBlockingSimulation { + val forwarder = SimResourceForwarder() + val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) + val source = SimResourceSource(2000.0, scheduler) + + val consumer = spyk(SimWorkConsumer(2000.0, 1.0)) + + source.startConsumer(forwarder) + yield() + forwarder.startConsumer(consumer) + yield() + source.cancel() + + verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Start) } + verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Exit) } + } + + @Test + fun testExitPropagation() = runBlockingSimulation { + val forwarder = SimResourceForwarder(isCoupled = true) + val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) + val source = SimResourceSource(2000.0, scheduler) + + val consumer = object : SimResourceConsumer { + override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + ctx.close() + return Long.MAX_VALUE + } + } + + source.startConsumer(forwarder) + forwarder.consume(consumer) + yield() + + assertFalse(forwarder.isActive) + } + + @Test + fun testAdjustCapacity() = runBlockingSimulation { + val forwarder = SimResourceForwarder() + val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) + val source = SimResourceSource(1.0, scheduler) + + val consumer = spyk(SimWorkConsumer(2.0, 1.0)) + source.startConsumer(forwarder) + + coroutineScope { + launch { forwarder.consume(consumer) } + delay(1000) + source.capacity = 0.5 + } + + assertEquals(3000, clock.millis()) + verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Capacity) } + } + + @Test + fun testCounters() = runBlockingSimulation { + val forwarder = SimResourceForwarder() + val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) + val source = SimResourceSource(1.0, scheduler) + + val consumer = SimWorkConsumer(2.0, 1.0) + source.startConsumer(forwarder) + + forwarder.consume(consumer) + + yield() + + assertEquals(2.0, source.counters.actual) + assertEquals(source.counters.actual, forwarder.counters.actual) { "Actual work" } + assertEquals(source.counters.demand, forwarder.counters.demand) { "Work demand" } + assertEquals(source.counters.overcommit, forwarder.counters.overcommit) { "Overcommitted work" } + assertEquals(2000, clock.millis()) + } +} diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt deleted file mode 100644 index d7d0924f..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceTransformerTest.kt +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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.resources - -import io.mockk.spyk -import io.mockk.verify -import kotlinx.coroutines.* -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.consumer.SimWorkConsumer -import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl - -/** - * A test suite for the [SimResourceTransformer] class. - */ -internal class SimResourceTransformerTest { - @Test - fun testCancelImmediately() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(2000.0, scheduler) - - launch { source.consume(forwarder) } - - forwarder.consume(object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - ctx.close() - return Long.MAX_VALUE - } - }) - - forwarder.close() - source.cancel() - } - - @Test - fun testCancel() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(2000.0, scheduler) - - launch { source.consume(forwarder) } - - forwarder.consume(object : SimResourceConsumer { - var isFirst = true - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (isFirst) { - isFirst = false - ctx.push(1.0) - 10 * 1000 - } else { - ctx.close() - Long.MAX_VALUE - } - } - }) - - forwarder.close() - source.cancel() - } - - @Test - fun testState() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - ctx.close() - return Long.MAX_VALUE - } - } - - assertFalse(forwarder.isActive) - - forwarder.startConsumer(consumer) - assertTrue(forwarder.isActive) - - assertThrows { forwarder.startConsumer(consumer) } - - forwarder.cancel() - assertFalse(forwarder.isActive) - - forwarder.close() - assertFalse(forwarder.isActive) - } - - @Test - fun testCancelPendingDelegate() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - - val consumer = spyk(object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - ctx.close() - return Long.MAX_VALUE - } - }) - - forwarder.startConsumer(consumer) - forwarder.cancel() - - verify(exactly = 0) { consumer.onEvent(any(), SimResourceEvent.Exit) } - } - - @Test - fun testCancelStartedDelegate() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(2000.0, scheduler) - - val consumer = spyk(SimWorkConsumer(2000.0, 1.0)) - - source.startConsumer(forwarder) - yield() - forwarder.startConsumer(consumer) - yield() - forwarder.cancel() - - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Start) } - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Exit) } - } - - @Test - fun testCancelPropagation() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(2000.0, scheduler) - - val consumer = spyk(SimWorkConsumer(2000.0, 1.0)) - - source.startConsumer(forwarder) - yield() - forwarder.startConsumer(consumer) - yield() - source.cancel() - - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Start) } - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Exit) } - } - - @Test - fun testExitPropagation() = runBlockingSimulation { - val forwarder = SimResourceForwarder(isCoupled = true) - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(2000.0, scheduler) - - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - ctx.close() - return Long.MAX_VALUE - } - } - - source.startConsumer(forwarder) - forwarder.consume(consumer) - yield() - - assertFalse(forwarder.isActive) - } - - @Test - fun testAdjustCapacity() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(1.0, scheduler) - - val consumer = spyk(SimWorkConsumer(2.0, 1.0)) - source.startConsumer(forwarder) - - coroutineScope { - launch { forwarder.consume(consumer) } - delay(1000) - source.capacity = 0.5 - } - - assertEquals(3000, clock.millis()) - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Capacity) } - } - - @Test - fun testTransformExit() = runBlockingSimulation { - val forwarder = SimResourceTransformer { ctx, _ -> ctx.close(); Long.MAX_VALUE } - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(1.0, scheduler) - - val consumer = spyk(SimWorkConsumer(2.0, 1.0)) - source.startConsumer(forwarder) - forwarder.consume(consumer) - - assertEquals(0, clock.millis()) - verify(exactly = 1) { consumer.onNext(any(), any(), any()) } - } - - @Test - fun testCounters() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(1.0, scheduler) - - val consumer = SimWorkConsumer(2.0, 1.0) - source.startConsumer(forwarder) - - forwarder.consume(consumer) - - yield() - - assertEquals(2.0, source.counters.actual) - assertEquals(source.counters.actual, forwarder.counters.actual) { "Actual work" } - assertEquals(source.counters.demand, forwarder.counters.demand) { "Work demand" } - assertEquals(source.counters.overcommit, forwarder.counters.overcommit) { "Overcommitted work" } - assertEquals(2000, clock.millis()) - } -} -- cgit v1.2.3 From 4cc1d40d421c8736f8b21b360b61d6b065158b7a Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 29 Sep 2021 23:56:16 +0200 Subject: refactor(simulator): Migrate to flow-based simulation This change renames the `opendc-simulator-resources` module into the `opendc-simulator-flow` module to indicate that the core simulation model of OpenDC is based around modelling and simulating flows. Previously, the distinction between resource consumer and provider, and input and output caused some confusion. By switching to a flow-based model, this distinction is now clear (as in, the water flows from source to consumer/sink). --- .../opendc-simulator-compute/build.gradle.kts | 2 +- .../simulator/compute/SimMachineBenchmarks.kt | 20 +- .../opendc/simulator/compute/SimAbstractMachine.kt | 49 +-- .../simulator/compute/SimBareMetalMachine.kt | 18 +- .../org/opendc/simulator/compute/SimMachine.kt | 2 +- .../opendc/simulator/compute/SimMachineContext.kt | 6 +- .../org/opendc/simulator/compute/SimMemory.kt | 4 +- .../simulator/compute/SimNetworkInterface.kt | 8 +- .../opendc/simulator/compute/SimProcessingUnit.kt | 4 +- .../simulator/compute/SimStorageInterface.kt | 6 +- .../org/opendc/simulator/compute/device/SimPsu.kt | 24 +- .../compute/kernel/SimAbstractHypervisor.kt | 38 +- .../compute/kernel/SimFairShareHypervisor.kt | 30 +- .../kernel/SimFairShareHypervisorProvider.kt | 10 +- .../simulator/compute/kernel/SimHypervisor.kt | 4 +- .../compute/kernel/SimHypervisorProvider.kt | 8 +- .../compute/kernel/SimSpaceSharedHypervisor.kt | 16 +- .../kernel/SimSpaceSharedHypervisorProvider.kt | 10 +- .../kernel/interference/VmInterferenceDomain.kt | 4 +- .../kernel/interference/VmInterferenceModel.kt | 2 +- .../simulator/compute/power/PStatePowerDriver.kt | 2 +- .../simulator/compute/power/SimplePowerDriver.kt | 2 +- .../simulator/compute/workload/SimFlopsWorkload.kt | 4 +- .../compute/workload/SimRuntimeWorkload.kt | 4 +- .../simulator/compute/workload/SimTraceWorkload.kt | 14 +- .../compute/workload/SimWorkloadLifecycle.kt | 26 +- .../org/opendc/simulator/compute/SimMachineTest.kt | 48 +-- .../opendc/simulator/compute/device/SimPsuTest.kt | 10 +- .../simulator/compute/kernel/SimHypervisorTest.kt | 10 +- .../compute/kernel/SimSpaceSharedHypervisorTest.kt | 36 +- .../compute/power/PStatePowerDriverTest.kt | 10 +- .../compute/workload/SimTraceWorkloadTest.kt | 10 +- .../opendc-simulator-flow/build.gradle.kts | 38 ++ .../org/opendc/simulator/flow/FlowBenchmarks.kt | 140 +++++++ .../opendc/simulator/flow/AbstractFlowConsumer.kt | 143 ++++++++ .../org/opendc/simulator/flow/FlowConnection.kt | 60 +++ .../org/opendc/simulator/flow/FlowConsumer.kt | 109 ++++++ .../opendc/simulator/flow/FlowConsumerContext.kt | 45 +++ .../org/opendc/simulator/flow/FlowConsumerLogic.kt | 56 +++ .../org/opendc/simulator/flow/FlowCounters.kt | 53 +++ .../kotlin/org/opendc/simulator/flow/FlowEngine.kt | 95 +++++ .../kotlin/org/opendc/simulator/flow/FlowEvent.kt | 48 +++ .../org/opendc/simulator/flow/FlowForwarder.kt | 217 +++++++++++ .../kotlin/org/opendc/simulator/flow/FlowSink.kt | 61 +++ .../kotlin/org/opendc/simulator/flow/FlowSource.kt | 58 +++ .../kotlin/org/opendc/simulator/flow/FlowSystem.kt | 43 +++ .../flow/interference/InterferenceDomain.kt | 19 + .../simulator/flow/interference/InterferenceKey.kt | 28 ++ .../flow/internal/FlowConsumerContextImpl.kt | 356 ++++++++++++++++++ .../simulator/flow/internal/FlowCountersImpl.kt | 46 +++ .../simulator/flow/internal/FlowEngineImpl.kt | 297 +++++++++++++++ .../opendc/simulator/flow/mux/FlowMultiplexer.kt | 70 ++++ .../flow/mux/ForwardingFlowMultiplexer.kt | 127 +++++++ .../simulator/flow/mux/MaxMinFlowMultiplexer.kt | 399 ++++++++++++++++++++ .../simulator/flow/source/FixedFlowSource.kt | 57 +++ .../simulator/flow/source/FlowSourceBarrier.kt | 52 +++ .../simulator/flow/source/FlowSourceRateAdapter.kt | 82 +++++ .../simulator/flow/source/TraceFlowSource.kt | 72 ++++ .../simulator/flow/FlowConsumerContextTest.kt | 152 ++++++++ .../org/opendc/simulator/flow/FlowForwarderTest.kt | 222 +++++++++++ .../org/opendc/simulator/flow/FlowSinkTest.kt | 240 ++++++++++++ .../flow/mux/SimResourceSwitchExclusiveTest.kt | 157 ++++++++ .../flow/mux/SimResourceSwitchMaxMinTest.kt | 147 ++++++++ .../simulator/flow/source/FixedFlowSourceTest.kt | 57 +++ .../opendc-simulator-network/build.gradle.kts | 2 +- .../org/opendc/simulator/network/SimNetworkPort.kt | 12 +- .../org/opendc/simulator/network/SimNetworkSink.kt | 10 +- .../simulator/network/SimNetworkSwitchVirtual.kt | 21 +- .../opendc/simulator/network/SimNetworkSinkTest.kt | 50 +-- .../network/SimNetworkSwitchVirtualTest.kt | 28 +- .../opendc-simulator-power/build.gradle.kts | 2 +- .../kotlin/org/opendc/simulator/power/SimPdu.kt | 36 +- .../org/opendc/simulator/power/SimPowerInlet.kt | 6 +- .../org/opendc/simulator/power/SimPowerSource.kt | 12 +- .../kotlin/org/opendc/simulator/power/SimUps.kt | 33 +- .../org/opendc/simulator/power/SimPduTest.kt | 52 +-- .../opendc/simulator/power/SimPowerSourceTest.kt | 46 +-- .../org/opendc/simulator/power/SimUpsTest.kt | 44 +-- .../opendc-simulator-resources/build.gradle.kts | 38 -- .../simulator/resources/SimResourceBenchmarks.kt | 138 ------- .../resources/SimAbstractResourceProvider.kt | 146 -------- .../simulator/resources/SimResourceConsumer.kt | 57 --- .../simulator/resources/SimResourceContext.kt | 68 ---- .../resources/SimResourceControllableContext.kt | 54 --- .../simulator/resources/SimResourceCounters.kt | 53 --- .../opendc/simulator/resources/SimResourceEvent.kt | 48 --- .../simulator/resources/SimResourceForwarder.kt | 220 ----------- .../simulator/resources/SimResourceInterpreter.kt | 94 ----- .../simulator/resources/SimResourceProvider.kt | 106 ------ .../resources/SimResourceProviderLogic.kt | 58 --- .../simulator/resources/SimResourceSource.kt | 61 --- .../simulator/resources/SimResourceSwitch.kt | 67 ---- .../resources/SimResourceSwitchExclusive.kt | 129 ------- .../simulator/resources/SimResourceSwitchMaxMin.kt | 407 --------------------- .../simulator/resources/SimResourceSystem.kt | 43 --- .../resources/consumer/SimConsumerBarrier.kt | 52 --- .../resources/consumer/SimSpeedConsumerAdapter.kt | 82 ----- .../resources/consumer/SimTraceConsumer.kt | 73 ---- .../resources/consumer/SimWorkConsumer.kt | 61 --- .../resources/impl/SimResourceContextImpl.kt | 367 ------------------- .../resources/impl/SimResourceCountersImpl.kt | 46 --- .../resources/impl/SimResourceInterpreterImpl.kt | 298 --------------- .../resources/interference/InterferenceDomain.kt | 19 - .../resources/interference/InterferenceKey.kt | 28 -- .../simulator/resources/SimResourceContextTest.kt | 173 --------- .../resources/SimResourceForwarderTest.kt | 220 ----------- .../simulator/resources/SimResourceSourceTest.kt | 240 ------------ .../resources/SimResourceSwitchExclusiveTest.kt | 156 -------- .../resources/SimResourceSwitchMaxMinTest.kt | 145 -------- .../simulator/resources/SimWorkConsumerTest.kt | 56 --- 110 files changed, 4145 insertions(+), 4199 deletions(-) create mode 100644 opendc-simulator/opendc-simulator-flow/build.gradle.kts create mode 100644 opendc-simulator/opendc-simulator-flow/src/jmh/kotlin/org/opendc/simulator/flow/FlowBenchmarks.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/AbstractFlowConsumer.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConnection.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerContext.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowCounters.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEngine.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSystem.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/interference/InterferenceDomain.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/interference/InterferenceKey.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowCountersImpl.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/FlowMultiplexer.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FixedFlowSource.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceBarrier.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/TraceFlowSource.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchExclusiveTest.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchMaxMinTest.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/source/FixedFlowSourceTest.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/build.gradle.kts delete mode 100644 opendc-simulator/opendc-simulator-resources/src/jmh/kotlin/org/opendc/simulator/resources/SimResourceBenchmarks.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCounters.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceEvent.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceForwarder.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceInterpreter.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProvider.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitch.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMin.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSystem.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimConsumerBarrier.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceCountersImpl.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/interference/InterferenceDomain.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/interference/InterferenceKey.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceForwarderTest.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt delete mode 100644 opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimWorkConsumerTest.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/build.gradle.kts b/opendc-simulator/opendc-simulator-compute/build.gradle.kts index 7d06ee62..e2290a14 100644 --- a/opendc-simulator/opendc-simulator-compute/build.gradle.kts +++ b/opendc-simulator/opendc-simulator-compute/build.gradle.kts @@ -31,7 +31,7 @@ plugins { dependencies { api(platform(projects.opendcPlatform)) - api(projects.opendcSimulator.opendcSimulatorResources) + api(projects.opendcSimulator.opendcSimulatorFlow) api(projects.opendcSimulator.opendcSimulatorPower) api(projects.opendcSimulator.opendcSimulatorNetwork) implementation(projects.opendcSimulator.opendcSimulatorCore) diff --git a/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt b/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt index 88ad7286..c57919c1 100644 --- a/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt +++ b/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt @@ -36,7 +36,7 @@ import org.opendc.simulator.compute.power.SimplePowerDriver import org.opendc.simulator.compute.workload.SimTraceWorkload import org.opendc.simulator.core.SimulationCoroutineScope import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.SimResourceInterpreter +import org.opendc.simulator.flow.FlowEngine import org.openjdk.jmh.annotations.* import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.TimeUnit @@ -48,13 +48,13 @@ import java.util.concurrent.TimeUnit @OptIn(ExperimentalCoroutinesApi::class) class SimMachineBenchmarks { private lateinit var scope: SimulationCoroutineScope - private lateinit var interpreter: SimResourceInterpreter + private lateinit var engine: FlowEngine private lateinit var machineModel: MachineModel @Setup fun setUp() { scope = SimulationCoroutineScope() - interpreter = SimResourceInterpreter(scope.coroutineContext, scope.clock) + engine = FlowEngine(scope.coroutineContext, scope.clock) val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 2) @@ -80,7 +80,7 @@ class SimMachineBenchmarks { fun benchmarkBareMetal(state: Workload) { return scope.runBlockingSimulation { val machine = SimBareMetalMachine( - interpreter, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) + engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) return@runBlockingSimulation machine.run(SimTraceWorkload(state.trace)) } @@ -90,9 +90,9 @@ class SimMachineBenchmarks { fun benchmarkSpaceSharedHypervisor(state: Workload) { return scope.runBlockingSimulation { val machine = SimBareMetalMachine( - interpreter, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) + engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimSpaceSharedHypervisor(interpreter) + val hypervisor = SimSpaceSharedHypervisor(engine) launch { machine.run(hypervisor) } @@ -111,9 +111,9 @@ class SimMachineBenchmarks { fun benchmarkFairShareHypervisorSingle(state: Workload) { return scope.runBlockingSimulation { val machine = SimBareMetalMachine( - interpreter, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) + engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimFairShareHypervisor(interpreter) + val hypervisor = SimFairShareHypervisor(engine) launch { machine.run(hypervisor) } @@ -132,9 +132,9 @@ class SimMachineBenchmarks { fun benchmarkFairShareHypervisorDouble(state: Workload) { return scope.runBlockingSimulation { val machine = SimBareMetalMachine( - interpreter, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) + engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimFairShareHypervisor(interpreter) + val hypervisor = SimFairShareHypervisor(engine) launch { machine.run(hypervisor) } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimAbstractMachine.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimAbstractMachine.kt index f9db048d..6a62d8a5 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimAbstractMachine.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimAbstractMachine.kt @@ -30,22 +30,22 @@ import org.opendc.simulator.compute.model.MemoryUnit import org.opendc.simulator.compute.model.NetworkAdapter import org.opendc.simulator.compute.model.StorageDevice import org.opendc.simulator.compute.workload.SimWorkload -import org.opendc.simulator.resources.* +import org.opendc.simulator.flow.* import kotlin.coroutines.Continuation import kotlin.coroutines.resume /** * Abstract implementation of the [SimMachine] interface. * - * @param interpreter The interpreter to manage the machine's resources. + * @param engine The engine to manage the machine's resources. * @param parent The parent simulation system. * @param model The model of the machine. */ public abstract class SimAbstractMachine( - protected val interpreter: SimResourceInterpreter, - final override val parent: SimResourceSystem?, + protected val engine: FlowEngine, + final override val parent: FlowSystem?, final override val model: MachineModel -) : SimMachine, SimResourceSystem { +) : SimMachine, FlowSystem { /** * The resources allocated for this machine. */ @@ -54,17 +54,17 @@ public abstract class SimAbstractMachine( /** * The memory interface of the machine. */ - public val memory: SimMemory = Memory(SimResourceSource(model.memory.sumOf { it.size }.toDouble(), interpreter), model.memory) + public val memory: SimMemory = Memory(FlowSink(engine, model.memory.sumOf { it.size }.toDouble()), model.memory) /** * The network interfaces available to the machine. */ - public val net: List = model.net.mapIndexed { i, adapter -> NetworkAdapterImpl(adapter, i) } + public val net: List = model.net.mapIndexed { i, adapter -> NetworkAdapterImpl(engine, adapter, i) } /** * The network interfaces available to the machine. */ - public val storage: List = model.storage.mapIndexed { i, device -> StorageDeviceImpl(interpreter, device, i) } + public val storage: List = model.storage.mapIndexed { i, device -> StorageDeviceImpl(engine, device, i) } /** * The peripherals of the machine. @@ -82,7 +82,7 @@ public abstract class SimAbstractMachine( private var cont: Continuation? = null /** - * Run the specified [SimWorkload] on this machine and suspend execution util the workload has finished. + * Converge the specified [SimWorkload] on this machine and suspend execution util the workload has finished. */ override suspend fun run(workload: SimWorkload, meta: Map) { check(!isTerminated) { "Machine is terminated" } @@ -96,14 +96,14 @@ public abstract class SimAbstractMachine( // Cancel all cpus on cancellation cont.invokeOnCancellation { this.cont = null - interpreter.batch { + engine.batch { for (cpu in cpus) { cpu.cancel() } } } - interpreter.batch { workload.onStart(ctx) } + engine.batch { workload.onStart(ctx) } } } @@ -120,7 +120,7 @@ public abstract class SimAbstractMachine( * Cancel the workload that is currently running on the machine. */ private fun cancel() { - interpreter.batch { + engine.batch { for (cpu in cpus) { cpu.cancel() } @@ -137,8 +137,8 @@ public abstract class SimAbstractMachine( * The execution context in which the workload runs. */ private inner class Context(override val meta: Map) : SimMachineContext { - override val interpreter: SimResourceInterpreter - get() = this@SimAbstractMachine.interpreter + override val engine: FlowEngine + get() = this@SimAbstractMachine.engine override val cpus: List = this@SimAbstractMachine.cpus @@ -154,7 +154,7 @@ public abstract class SimAbstractMachine( /** * The [SimMemory] implementation for a machine. */ - private class Memory(source: SimResourceSource, override val models: List) : SimMemory, SimResourceProvider by source { + private class Memory(source: FlowSink, override val models: List) : SimMemory, FlowConsumer by source { override fun toString(): String = "SimAbstractMachine.Memory" } @@ -162,6 +162,7 @@ public abstract class SimAbstractMachine( * The [SimNetworkAdapter] implementation for a machine. */ private class NetworkAdapterImpl( + private val engine: FlowEngine, model: NetworkAdapter, index: Int ) : SimNetworkAdapter(), SimNetworkInterface { @@ -169,18 +170,18 @@ public abstract class SimAbstractMachine( override val bandwidth: Double = model.bandwidth - override val provider: SimResourceProvider + override val provider: FlowConsumer get() = _rx - override fun createConsumer(): SimResourceConsumer = _tx + override fun createConsumer(): FlowSource = _tx - override val tx: SimResourceProvider + override val tx: FlowConsumer get() = _tx - private val _tx = SimResourceForwarder() + private val _tx = FlowForwarder(engine) - override val rx: SimResourceConsumer + override val rx: FlowSource get() = _rx - private val _rx = SimResourceForwarder() + private val _rx = FlowForwarder(engine) override fun toString(): String = "SimAbstractMachine.NetworkAdapterImpl[name=$name,bandwidth=$bandwidth]" } @@ -189,7 +190,7 @@ public abstract class SimAbstractMachine( * The [SimStorageInterface] implementation for a machine. */ private class StorageDeviceImpl( - interpreter: SimResourceInterpreter, + engine: FlowEngine, model: StorageDevice, index: Int ) : SimStorageInterface { @@ -197,9 +198,9 @@ public abstract class SimAbstractMachine( override val capacity: Double = model.capacity - override val read: SimResourceProvider = SimResourceSource(model.readBandwidth, interpreter) + override val read: FlowConsumer = FlowSink(engine, model.readBandwidth) - override val write: SimResourceProvider = SimResourceSource(model.writeBandwidth, interpreter) + override val write: FlowConsumer = FlowSink(engine, model.writeBandwidth) override fun toString(): String = "SimAbstractMachine.StorageDeviceImpl[name=$name,capacity=$capacity]" } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt index 639ca450..37cf282b 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt @@ -26,8 +26,8 @@ import org.opendc.simulator.compute.device.SimPsu import org.opendc.simulator.compute.model.MachineModel import org.opendc.simulator.compute.model.ProcessingUnit import org.opendc.simulator.compute.power.PowerDriver -import org.opendc.simulator.resources.* -import org.opendc.simulator.resources.SimResourceInterpreter +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.FlowEngine /** * A simulated bare-metal machine that is able to run a single workload. @@ -35,19 +35,19 @@ import org.opendc.simulator.resources.SimResourceInterpreter * A [SimBareMetalMachine] is a stateful object, and you should be careful when operating this object concurrently. For * example, the class expects only a single concurrent call to [run]. * - * @param interpreter The [SimResourceInterpreter] to drive the simulation. + * @param engine The [FlowEngine] to drive the simulation. * @param model The machine model to simulate. * @param powerDriver The power driver to use. * @param psu The power supply of the machine. * @param parent The parent simulation system. */ public class SimBareMetalMachine( - interpreter: SimResourceInterpreter, + engine: FlowEngine, model: MachineModel, powerDriver: PowerDriver, public val psu: SimPsu = SimPsu(500.0, mapOf(1.0 to 1.0)), - parent: SimResourceSystem? = null, -) : SimAbstractMachine(interpreter, parent, model) { + parent: FlowSystem? = null, +) : SimAbstractMachine(engine, parent, model) { /** * The power draw of the machine onto the PSU. */ @@ -58,7 +58,7 @@ public class SimBareMetalMachine( * The processing units of the machine. */ override val cpus: List = model.cpus.map { cpu -> - Cpu(SimResourceSource(cpu.frequency, interpreter, this@SimBareMetalMachine), cpu) + Cpu(FlowSink(engine, cpu.frequency, this@SimBareMetalMachine), cpu) } /** @@ -78,9 +78,9 @@ public class SimBareMetalMachine( * A [SimProcessingUnit] of a bare-metal machine. */ private class Cpu( - private val source: SimResourceSource, + private val source: FlowSink, override val model: ProcessingUnit - ) : SimProcessingUnit, SimResourceProvider by source { + ) : SimProcessingUnit, FlowConsumer by source { override var capacity: Double get() = source.capacity set(value) { diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachine.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachine.kt index d8dd8205..ab0b56ae 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachine.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachine.kt @@ -41,7 +41,7 @@ public interface SimMachine : AutoCloseable { public val peripherals: List /** - * Run the specified [SimWorkload] on this machine and suspend execution util the workload has finished. + * Converge the specified [SimWorkload] on this machine and suspend execution util the workload has finished. */ public suspend fun run(workload: SimWorkload, meta: Map = emptyMap()) diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachineContext.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachineContext.kt index 6996a30d..1317f728 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachineContext.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMachineContext.kt @@ -22,7 +22,7 @@ package org.opendc.simulator.compute -import org.opendc.simulator.resources.SimResourceInterpreter +import org.opendc.simulator.flow.FlowEngine /** * A simulated execution context in which a bootable image runs. This interface represents the @@ -31,9 +31,9 @@ import org.opendc.simulator.resources.SimResourceInterpreter */ public interface SimMachineContext : AutoCloseable { /** - * The resource interpreter that simulates the machine. + * The [FlowEngine] that simulates the machine. */ - public val interpreter: SimResourceInterpreter + public val engine: FlowEngine /** * The metadata associated with the context. diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMemory.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMemory.kt index 6623df23..b1aef495 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMemory.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimMemory.kt @@ -23,12 +23,12 @@ package org.opendc.simulator.compute import org.opendc.simulator.compute.model.MemoryUnit -import org.opendc.simulator.resources.SimResourceProvider +import org.opendc.simulator.flow.FlowConsumer /** * An interface to control the memory usage of simulated workloads. */ -public interface SimMemory : SimResourceProvider { +public interface SimMemory : FlowConsumer { /** * The models representing the static information of the memory units supporting this interface. */ diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimNetworkInterface.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimNetworkInterface.kt index 1ac126ae..660b2871 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimNetworkInterface.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimNetworkInterface.kt @@ -22,8 +22,8 @@ package org.opendc.simulator.compute -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceProvider +import org.opendc.simulator.flow.FlowConsumer +import org.opendc.simulator.flow.FlowSource /** * A firmware interface to a network adapter. @@ -42,10 +42,10 @@ public interface SimNetworkInterface { /** * The resource provider for the transmit channel of the network interface. */ - public val tx: SimResourceProvider + public val tx: FlowConsumer /** * The resource consumer for the receive channel of the network interface. */ - public val rx: SimResourceConsumer + public val rx: FlowSource } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimProcessingUnit.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimProcessingUnit.kt index 93c9ddfa..c9f36ece 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimProcessingUnit.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimProcessingUnit.kt @@ -23,12 +23,12 @@ package org.opendc.simulator.compute import org.opendc.simulator.compute.model.ProcessingUnit -import org.opendc.simulator.resources.SimResourceProvider +import org.opendc.simulator.flow.FlowConsumer /** * A simulated processing unit. */ -public interface SimProcessingUnit : SimResourceProvider { +public interface SimProcessingUnit : FlowConsumer { /** * The capacity of the processing unit, which can be adjusted by the workload if supported by the machine. */ diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimStorageInterface.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimStorageInterface.kt index 21a801f1..3d648671 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimStorageInterface.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimStorageInterface.kt @@ -22,7 +22,7 @@ package org.opendc.simulator.compute -import org.opendc.simulator.resources.SimResourceProvider +import org.opendc.simulator.flow.FlowConsumer /** * A firmware interface to a storage device. @@ -41,10 +41,10 @@ public interface SimStorageInterface { /** * The resource provider for the read operations of the storage device. */ - public val read: SimResourceProvider + public val read: FlowConsumer /** * The resource consumer for the write operation of the storage device. */ - public val write: SimResourceProvider + public val write: FlowConsumer } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt index 6e6e590f..b05d8ad9 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt @@ -23,10 +23,10 @@ package org.opendc.simulator.compute.device import org.opendc.simulator.compute.power.PowerDriver +import org.opendc.simulator.flow.FlowConnection +import org.opendc.simulator.flow.FlowEvent +import org.opendc.simulator.flow.FlowSource import org.opendc.simulator.power.SimPowerInlet -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceContext -import org.opendc.simulator.resources.SimResourceEvent import java.util.* /** @@ -54,7 +54,7 @@ public class SimPsu( /** * The consumer context. */ - private var _ctx: SimResourceContext? = null + private var _ctx: FlowConnection? = null /** * The driver that is connected to the PSU. @@ -69,7 +69,7 @@ public class SimPsu( * Update the power draw of the PSU. */ public fun update() { - _ctx?.interrupt() + _ctx?.pull() } /** @@ -81,18 +81,18 @@ public class SimPsu( update() } - override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + override fun createConsumer(): FlowSource = object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { val powerDraw = computePowerDraw(_driver?.computePower() ?: 0.0) - ctx.push(powerDraw) + conn.push(powerDraw) return Long.MAX_VALUE } - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { when (event) { - SimResourceEvent.Start -> _ctx = ctx - SimResourceEvent.Run -> _powerDraw = ctx.speed - SimResourceEvent.Exit -> _ctx = null + FlowEvent.Start -> _ctx = conn + FlowEvent.Converge -> _powerDraw = conn.rate + FlowEvent.Exit -> _ctx = null else -> {} } } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt index cf9e3230..b145eefc 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt @@ -28,17 +28,17 @@ import org.opendc.simulator.compute.kernel.cpufreq.ScalingPolicy import org.opendc.simulator.compute.kernel.interference.VmInterferenceDomain import org.opendc.simulator.compute.model.MachineModel import org.opendc.simulator.compute.model.ProcessingUnit -import org.opendc.simulator.resources.* -import org.opendc.simulator.resources.SimResourceSwitch +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.mux.FlowMultiplexer /** * Abstract implementation of the [SimHypervisor] interface. * - * @param interpreter The resource interpreter to use. + * @param engine The [FlowEngine] to drive the simulation. * @param scalingGovernor The scaling governor to use for scaling the CPU frequency of the underlying hardware. */ public abstract class SimAbstractHypervisor( - private val interpreter: SimResourceInterpreter, + protected val engine: FlowEngine, private val scalingGovernor: ScalingGovernor? = null, protected val interferenceDomain: VmInterferenceDomain? = null ) : SimHypervisor { @@ -50,7 +50,7 @@ public abstract class SimAbstractHypervisor( /** * The resource switch to use. */ - private lateinit var switch: SimResourceSwitch + private lateinit var mux: FlowMultiplexer /** * The virtual machines running on this hypervisor. @@ -62,8 +62,8 @@ public abstract class SimAbstractHypervisor( /** * The resource counters associated with the hypervisor. */ - public override val counters: SimResourceCounters - get() = switch.counters + public override val counters: FlowCounters + get() = mux.counters /** * The scaling governors attached to the physical CPUs backing this hypervisor. @@ -71,14 +71,14 @@ public abstract class SimAbstractHypervisor( private val governors = mutableListOf() /** - * Construct the [SimResourceSwitch] implementation that performs the actual scheduling of the CPUs. + * Construct the [FlowMultiplexer] implementation that performs the actual scheduling of the CPUs. */ - public abstract fun createSwitch(ctx: SimMachineContext): SimResourceSwitch + public abstract fun createMultiplexer(ctx: SimMachineContext): FlowMultiplexer /** * Check whether the specified machine model fits on this hypervisor. */ - public abstract fun canFit(model: MachineModel, switch: SimResourceSwitch): Boolean + public abstract fun canFit(model: MachineModel, switch: FlowMultiplexer): Boolean /** * Trigger the governors to recompute the scaling limits. @@ -91,7 +91,7 @@ public abstract class SimAbstractHypervisor( /* SimHypervisor */ override fun canFit(model: MachineModel): Boolean { - return canFit(model, switch) + return canFit(model, mux) } override fun createMachine(model: MachineModel, interferenceId: String?): SimMachine { @@ -104,7 +104,7 @@ public abstract class SimAbstractHypervisor( /* SimWorkload */ override fun onStart(ctx: SimMachineContext) { context = ctx - switch = createSwitch(ctx) + mux = createMultiplexer(ctx) for (cpu in ctx.cpus) { val governor = scalingGovernor?.createLogic(ScalingPolicyImpl(cpu)) @@ -113,7 +113,7 @@ public abstract class SimAbstractHypervisor( governor.onStart() } - switch.addInput(cpu) + mux.addOutput(cpu) } } @@ -122,7 +122,7 @@ public abstract class SimAbstractHypervisor( * * @param model The machine model of the virtual machine. */ - private inner class VirtualMachine(model: MachineModel, interferenceId: String? = null) : SimAbstractMachine(interpreter, parent = null, model) { + private inner class VirtualMachine(model: MachineModel, interferenceId: String? = null) : SimAbstractMachine(engine, parent = null, model) { /** * The interference key of this virtual machine. */ @@ -131,7 +131,7 @@ public abstract class SimAbstractHypervisor( /** * The vCPUs of the machine. */ - override val cpus = model.cpus.map { VCpu(switch, switch.newOutput(interferenceKey), it) } + override val cpus = model.cpus.map { VCpu(mux, mux.newInput(interferenceKey), it) } override fun close() { super.close() @@ -153,10 +153,10 @@ public abstract class SimAbstractHypervisor( * A [SimProcessingUnit] of a virtual machine. */ private class VCpu( - private val switch: SimResourceSwitch, - private val source: SimResourceProvider, + private val switch: FlowMultiplexer, + private val source: FlowConsumer, override val model: ProcessingUnit - ) : SimProcessingUnit, SimResourceProvider by source { + ) : SimProcessingUnit, FlowConsumer by source { override var capacity: Double get() = source.capacity set(_) { @@ -169,7 +169,7 @@ public abstract class SimAbstractHypervisor( * Close the CPU */ fun close() { - switch.removeOutput(source) + switch.removeInput(source) } } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt index 3b44292d..36ab7c1c 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt @@ -28,39 +28,39 @@ import org.opendc.simulator.compute.kernel.cpufreq.ScalingGovernor import org.opendc.simulator.compute.kernel.interference.VmInterferenceDomain import org.opendc.simulator.compute.model.MachineModel import org.opendc.simulator.compute.workload.SimWorkload -import org.opendc.simulator.resources.SimResourceInterpreter -import org.opendc.simulator.resources.SimResourceSwitch -import org.opendc.simulator.resources.SimResourceSwitchMaxMin -import org.opendc.simulator.resources.SimResourceSystem +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowSystem +import org.opendc.simulator.flow.mux.FlowMultiplexer +import org.opendc.simulator.flow.mux.MaxMinFlowMultiplexer /** * A [SimHypervisor] that distributes the computing requirements of multiple [SimWorkload]s on a single [SimMachine] * concurrently using weighted fair sharing. * - * @param interpreter The interpreter to manage the machine's resources. + * @param engine The [FlowEngine] to manage the machine's resources. * @param parent The parent simulation system. * @param scalingGovernor The CPU frequency scaling governor to use for the hypervisor. * @param interferenceDomain The resource interference domain to which the hypervisor belongs. * @param listener The hypervisor listener to use. */ public class SimFairShareHypervisor( - private val interpreter: SimResourceInterpreter, - private val parent: SimResourceSystem? = null, + engine: FlowEngine, + private val parent: FlowSystem? = null, scalingGovernor: ScalingGovernor? = null, interferenceDomain: VmInterferenceDomain? = null, private val listener: SimHypervisor.Listener? = null -) : SimAbstractHypervisor(interpreter, scalingGovernor, interferenceDomain) { +) : SimAbstractHypervisor(engine, scalingGovernor, interferenceDomain) { - override fun canFit(model: MachineModel, switch: SimResourceSwitch): Boolean = true + override fun canFit(model: MachineModel, switch: FlowMultiplexer): Boolean = true - override fun createSwitch(ctx: SimMachineContext): SimResourceSwitch { + override fun createMultiplexer(ctx: SimMachineContext): FlowMultiplexer { return SwitchSystem(ctx).switch } - private inner class SwitchSystem(private val ctx: SimMachineContext) : SimResourceSystem { - val switch = SimResourceSwitchMaxMin(interpreter, this, interferenceDomain) + private inner class SwitchSystem(private val ctx: SimMachineContext) : FlowSystem { + val switch = MaxMinFlowMultiplexer(engine, this, interferenceDomain) - override val parent: SimResourceSystem? = this@SimFairShareHypervisor.parent + override val parent: FlowSystem? = this@SimFairShareHypervisor.parent private var lastCpuUsage = 0.0 private var lastCpuDemand = 0.0 @@ -87,8 +87,8 @@ public class SimFairShareHypervisor( } lastReport = timestamp - lastCpuDemand = switch.inputs.sumOf { it.demand } - lastCpuUsage = switch.inputs.sumOf { it.speed } + lastCpuDemand = switch.outputs.sumOf { it.demand } + lastCpuUsage = switch.outputs.sumOf { it.rate } lastDemand = counters.demand lastActual = counters.actual lastOvercommit = counters.overcommit diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt index 8d0592ec..bfa099fb 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt @@ -24,8 +24,8 @@ package org.opendc.simulator.compute.kernel import org.opendc.simulator.compute.kernel.cpufreq.ScalingGovernor import org.opendc.simulator.compute.kernel.interference.VmInterferenceDomain -import org.opendc.simulator.resources.SimResourceInterpreter -import org.opendc.simulator.resources.SimResourceSystem +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowSystem /** * A [SimHypervisorProvider] for the [SimFairShareHypervisor] implementation. @@ -34,13 +34,13 @@ public class SimFairShareHypervisorProvider : SimHypervisorProvider { override val id: String = "fair-share" override fun create( - interpreter: SimResourceInterpreter, - parent: SimResourceSystem?, + engine: FlowEngine, + parent: FlowSystem?, scalingGovernor: ScalingGovernor?, interferenceDomain: VmInterferenceDomain?, listener: SimHypervisor.Listener? ): SimHypervisor = SimFairShareHypervisor( - interpreter, + engine, parent, scalingGovernor = scalingGovernor, interferenceDomain = interferenceDomain, diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisor.kt index 3b49d515..1b11ca6b 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisor.kt @@ -25,7 +25,7 @@ package org.opendc.simulator.compute.kernel import org.opendc.simulator.compute.SimMachine import org.opendc.simulator.compute.model.MachineModel import org.opendc.simulator.compute.workload.SimWorkload -import org.opendc.simulator.resources.SimResourceCounters +import org.opendc.simulator.flow.FlowCounters /** * A SimHypervisor facilitates the execution of multiple concurrent [SimWorkload]s, while acting as a single workload @@ -40,7 +40,7 @@ public interface SimHypervisor : SimWorkload { /** * The resource counters associated with the hypervisor. */ - public val counters: SimResourceCounters + public val counters: FlowCounters /** * Determine whether the specified machine characterized by [model] can fit on this hypervisor at this moment. diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt index b307a34d..97f07097 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt @@ -24,8 +24,8 @@ package org.opendc.simulator.compute.kernel import org.opendc.simulator.compute.kernel.cpufreq.ScalingGovernor import org.opendc.simulator.compute.kernel.interference.VmInterferenceDomain -import org.opendc.simulator.resources.SimResourceInterpreter -import org.opendc.simulator.resources.SimResourceSystem +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowSystem /** * A service provider interface for constructing a [SimHypervisor]. @@ -43,8 +43,8 @@ public interface SimHypervisorProvider { * Create a [SimHypervisor] instance with the specified [listener]. */ public fun create( - interpreter: SimResourceInterpreter, - parent: SimResourceSystem? = null, + engine: FlowEngine, + parent: FlowSystem? = null, scalingGovernor: ScalingGovernor? = null, interferenceDomain: VmInterferenceDomain? = null, listener: SimHypervisor.Listener? = null diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisor.kt index ac1c0250..883e0d82 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisor.kt @@ -24,19 +24,19 @@ package org.opendc.simulator.compute.kernel import org.opendc.simulator.compute.SimMachineContext import org.opendc.simulator.compute.model.MachineModel -import org.opendc.simulator.resources.SimResourceInterpreter -import org.opendc.simulator.resources.SimResourceSwitch -import org.opendc.simulator.resources.SimResourceSwitchExclusive +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.mux.FlowMultiplexer +import org.opendc.simulator.flow.mux.ForwardingFlowMultiplexer /** * A [SimHypervisor] that allocates its sub-resources exclusively for the virtual machine that it hosts. */ -public class SimSpaceSharedHypervisor(interpreter: SimResourceInterpreter) : SimAbstractHypervisor(interpreter) { - override fun canFit(model: MachineModel, switch: SimResourceSwitch): Boolean { - return switch.inputs.size - switch.outputs.size >= model.cpus.size +public class SimSpaceSharedHypervisor(engine: FlowEngine) : SimAbstractHypervisor(engine) { + override fun canFit(model: MachineModel, switch: FlowMultiplexer): Boolean { + return switch.outputs.size - switch.inputs.size >= model.cpus.size } - override fun createSwitch(ctx: SimMachineContext): SimResourceSwitch { - return SimResourceSwitchExclusive() + override fun createMultiplexer(ctx: SimMachineContext): FlowMultiplexer { + return ForwardingFlowMultiplexer(engine) } } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt index 3906cb9a..7869d72d 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt @@ -24,8 +24,8 @@ package org.opendc.simulator.compute.kernel import org.opendc.simulator.compute.kernel.cpufreq.ScalingGovernor import org.opendc.simulator.compute.kernel.interference.VmInterferenceDomain -import org.opendc.simulator.resources.SimResourceInterpreter -import org.opendc.simulator.resources.SimResourceSystem +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowSystem /** * A [SimHypervisorProvider] for the [SimSpaceSharedHypervisor] implementation. @@ -34,10 +34,10 @@ public class SimSpaceSharedHypervisorProvider : SimHypervisorProvider { override val id: String = "space-shared" override fun create( - interpreter: SimResourceInterpreter, - parent: SimResourceSystem?, + engine: FlowEngine, + parent: FlowSystem?, scalingGovernor: ScalingGovernor?, interferenceDomain: VmInterferenceDomain?, listener: SimHypervisor.Listener? - ): SimHypervisor = SimSpaceSharedHypervisor(interpreter) + ): SimHypervisor = SimSpaceSharedHypervisor(engine) } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/interference/VmInterferenceDomain.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/interference/VmInterferenceDomain.kt index 1801fcd0..b737d61a 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/interference/VmInterferenceDomain.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/interference/VmInterferenceDomain.kt @@ -22,8 +22,8 @@ package org.opendc.simulator.compute.kernel.interference -import org.opendc.simulator.resources.interference.InterferenceDomain -import org.opendc.simulator.resources.interference.InterferenceKey +import org.opendc.simulator.flow.interference.InterferenceDomain +import org.opendc.simulator.flow.interference.InterferenceKey /** * The interference domain of a hypervisor. diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/interference/VmInterferenceModel.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/interference/VmInterferenceModel.kt index c2e00c8e..b3d72507 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/interference/VmInterferenceModel.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/interference/VmInterferenceModel.kt @@ -22,7 +22,7 @@ package org.opendc.simulator.compute.kernel.interference -import org.opendc.simulator.resources.interference.InterferenceKey +import org.opendc.simulator.flow.interference.InterferenceKey import java.util.* /** diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/PStatePowerDriver.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/PStatePowerDriver.kt index 6577fbfc..f71446f8 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/PStatePowerDriver.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/PStatePowerDriver.kt @@ -46,7 +46,7 @@ public class PStatePowerDriver(states: Map) : PowerDriver { for (cpu in cpus) { targetFreq = max(cpu.capacity, targetFreq) - totalSpeed += cpu.speed + totalSpeed += cpu.rate } val maxFreq = states.lastKey() diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/SimplePowerDriver.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/SimplePowerDriver.kt index bf7aeff1..34e91c35 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/SimplePowerDriver.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/power/SimplePowerDriver.kt @@ -37,7 +37,7 @@ public class SimplePowerDriver(private val model: PowerModel) : PowerDriver { for (cpu in cpus) { targetFreq += cpu.capacity - totalSpeed += cpu.speed + totalSpeed += cpu.rate } return model.computePower(totalSpeed / targetFreq) diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkload.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkload.kt index a01fa20c..99f4a1e1 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkload.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimFlopsWorkload.kt @@ -23,7 +23,7 @@ package org.opendc.simulator.compute.workload import org.opendc.simulator.compute.SimMachineContext -import org.opendc.simulator.resources.consumer.SimWorkConsumer +import org.opendc.simulator.flow.source.FixedFlowSource /** * A [SimWorkload] that models applications as a static number of floating point operations ([flops]) executed on @@ -44,7 +44,7 @@ public class SimFlopsWorkload( override fun onStart(ctx: SimMachineContext) { val lifecycle = SimWorkloadLifecycle(ctx) for (cpu in ctx.cpus) { - cpu.startConsumer(lifecycle.waitFor(SimWorkConsumer(flops.toDouble() / ctx.cpus.size, utilization))) + cpu.startConsumer(lifecycle.waitFor(FixedFlowSource(flops.toDouble() / ctx.cpus.size, utilization))) } } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimRuntimeWorkload.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimRuntimeWorkload.kt index 4ee56689..2ef3bc43 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimRuntimeWorkload.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimRuntimeWorkload.kt @@ -23,7 +23,7 @@ package org.opendc.simulator.compute.workload import org.opendc.simulator.compute.SimMachineContext -import org.opendc.simulator.resources.consumer.SimWorkConsumer +import org.opendc.simulator.flow.source.FixedFlowSource /** * A [SimWorkload] that models application execution as a single duration. @@ -44,7 +44,7 @@ public class SimRuntimeWorkload( val lifecycle = SimWorkloadLifecycle(ctx) for (cpu in ctx.cpus) { val limit = cpu.capacity * utilization - cpu.startConsumer(lifecycle.waitFor(SimWorkConsumer((limit / 1000) * duration, utilization))) + cpu.startConsumer(lifecycle.waitFor(FixedFlowSource((limit / 1000) * duration, utilization))) } } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt index dd582bb2..a877dac1 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt @@ -24,8 +24,8 @@ package org.opendc.simulator.compute.workload import org.opendc.simulator.compute.SimMachineContext import org.opendc.simulator.compute.model.ProcessingUnit -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceContext +import org.opendc.simulator.flow.FlowConnection +import org.opendc.simulator.flow.FlowSource import kotlin.math.min /** @@ -78,12 +78,12 @@ public class SimTraceWorkload(public val trace: Sequence, private val return now >= timestamp + duration } - private inner class Consumer(val cpu: ProcessingUnit) : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { + private inner class Consumer(val cpu: ProcessingUnit) : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { val fragment = pullFragment(now) if (fragment == null) { - ctx.close() + conn.close() return Long.MAX_VALUE } @@ -91,7 +91,7 @@ public class SimTraceWorkload(public val trace: Sequence, private val // Fragment is in the future if (timestamp > now) { - ctx.push(0.0) + conn.push(0.0) return timestamp - now } @@ -103,7 +103,7 @@ public class SimTraceWorkload(public val trace: Sequence, private val val deadline = timestamp + fragment.duration val duration = deadline - now - ctx.push(if (cpu.id < cores && usage > 0.0) usage else 0.0) + conn.push(if (cpu.id < cores && usage > 0.0) usage else 0.0) return duration } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt index 5dd18271..dabe60e0 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt @@ -23,9 +23,9 @@ package org.opendc.simulator.compute.workload import org.opendc.simulator.compute.SimMachineContext -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceContext -import org.opendc.simulator.resources.SimResourceEvent +import org.opendc.simulator.flow.FlowConnection +import org.opendc.simulator.flow.FlowEvent +import org.opendc.simulator.flow.FlowSource /** * A helper class to manage the lifecycle of a [SimWorkload] @@ -34,27 +34,27 @@ public class SimWorkloadLifecycle(private val ctx: SimMachineContext) { /** * The resource consumers which represent the lifecycle of the workload. */ - private val waiting = mutableSetOf() + private val waiting = mutableSetOf() /** * Wait for the specified [consumer] to complete before ending the lifecycle of the workload. */ - public fun waitFor(consumer: SimResourceConsumer): SimResourceConsumer { + public fun waitFor(consumer: FlowSource): FlowSource { waiting.add(consumer) - return object : SimResourceConsumer by consumer { - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { + return object : FlowSource by consumer { + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { try { - consumer.onEvent(ctx, event) + consumer.onEvent(conn, now, event) } finally { - if (event == SimResourceEvent.Exit) { + if (event == FlowEvent.Exit) { complete(consumer) } } } - override fun onFailure(ctx: SimResourceContext, cause: Throwable) { + override fun onFailure(conn: FlowConnection, cause: Throwable) { try { - consumer.onFailure(ctx, cause) + consumer.onFailure(conn, cause) } finally { complete(consumer) } @@ -65,9 +65,9 @@ public class SimWorkloadLifecycle(private val ctx: SimMachineContext) { } /** - * Complete the specified [SimResourceConsumer]. + * Complete the specified [FlowSource]. */ - private fun complete(consumer: SimResourceConsumer) { + private fun complete(consumer: FlowSource) { if (waiting.remove(consumer) && waiting.isEmpty()) { ctx.close() } diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt index 81268879..0bb24ed8 100644 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/SimMachineTest.kt @@ -34,10 +34,10 @@ import org.opendc.simulator.compute.workload.SimFlopsWorkload import org.opendc.simulator.compute.workload.SimWorkload import org.opendc.simulator.compute.workload.SimWorkloadLifecycle import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.source.FixedFlowSource import org.opendc.simulator.network.SimNetworkSink import org.opendc.simulator.power.SimPowerSource -import org.opendc.simulator.resources.SimResourceInterpreter -import org.opendc.simulator.resources.consumer.SimWorkConsumer /** * Test suite for the [SimBareMetalMachine] class. @@ -60,7 +60,7 @@ class SimMachineTest { @Test fun testFlopsWorkload() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -83,7 +83,7 @@ class SimMachineTest { memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } ) val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -100,13 +100,13 @@ class SimMachineTest { @Test fun testPower() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) + val engine = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( - interpreter, + engine, machineModel, SimplePowerDriver(LinearPowerModel(100.0, 50.0)) ) - val source = SimPowerSource(interpreter, capacity = 1000.0) + val source = SimPowerSource(engine, capacity = 1000.0) source.connect(machine.psu) try { @@ -125,7 +125,7 @@ class SimMachineTest { @Test fun testCapacityClamp() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -151,7 +151,7 @@ class SimMachineTest { @Test fun testMemory() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -171,7 +171,7 @@ class SimMachineTest { @Test fun testMemoryUsage() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -180,7 +180,7 @@ class SimMachineTest { machine.run(object : SimWorkload { override fun onStart(ctx: SimMachineContext) { val lifecycle = SimWorkloadLifecycle(ctx) - ctx.memory.startConsumer(lifecycle.waitFor(SimWorkConsumer(ctx.memory.capacity, utilization = 0.8))) + ctx.memory.startConsumer(lifecycle.waitFor(FixedFlowSource(ctx.memory.capacity, utilization = 0.8))) } }) @@ -192,22 +192,22 @@ class SimMachineTest { @Test fun testNetUsage() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) + val engine = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( - interpreter, + engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) val adapter = (machine.peripherals[0] as SimNetworkAdapter) - adapter.connect(SimNetworkSink(interpreter, adapter.bandwidth)) + adapter.connect(SimNetworkSink(engine, adapter.bandwidth)) try { machine.run(object : SimWorkload { override fun onStart(ctx: SimMachineContext) { val lifecycle = SimWorkloadLifecycle(ctx) val iface = ctx.net[0] - iface.tx.startConsumer(lifecycle.waitFor(SimWorkConsumer(iface.bandwidth, utilization = 0.8))) + iface.tx.startConsumer(lifecycle.waitFor(FixedFlowSource(iface.bandwidth, utilization = 0.8))) } }) @@ -219,9 +219,9 @@ class SimMachineTest { @Test fun testDiskReadUsage() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) + val engine = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( - interpreter, + engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -231,7 +231,7 @@ class SimMachineTest { override fun onStart(ctx: SimMachineContext) { val lifecycle = SimWorkloadLifecycle(ctx) val disk = ctx.storage[0] - disk.read.startConsumer(lifecycle.waitFor(SimWorkConsumer(disk.read.capacity, utilization = 0.8))) + disk.read.startConsumer(lifecycle.waitFor(FixedFlowSource(disk.read.capacity, utilization = 0.8))) } }) @@ -243,9 +243,9 @@ class SimMachineTest { @Test fun testDiskWriteUsage() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) + val engine = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( - interpreter, + engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -255,7 +255,7 @@ class SimMachineTest { override fun onStart(ctx: SimMachineContext) { val lifecycle = SimWorkloadLifecycle(ctx) val disk = ctx.storage[0] - disk.write.startConsumer(lifecycle.waitFor(SimWorkConsumer(disk.write.capacity, utilization = 0.8))) + disk.write.startConsumer(lifecycle.waitFor(FixedFlowSource(disk.write.capacity, utilization = 0.8))) } }) @@ -268,7 +268,7 @@ class SimMachineTest { @Test fun testCancellation() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -290,7 +290,7 @@ class SimMachineTest { @Test fun testConcurrentRuns() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -313,7 +313,7 @@ class SimMachineTest { @Test fun testClose() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/device/SimPsuTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/device/SimPsuTest.kt index 6c9ec7bd..e5b509f0 100644 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/device/SimPsuTest.kt +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/device/SimPsuTest.kt @@ -29,8 +29,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.opendc.simulator.compute.power.PowerDriver import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.FlowEngine import org.opendc.simulator.power.SimPowerSource -import org.opendc.simulator.resources.SimResourceInterpreter /** * Test suite for [SimPsu] @@ -55,8 +55,8 @@ internal class SimPsuTest { val ratedOutputPower = 240.0 val energyEfficiency = mapOf(0.0 to 1.0) - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = ratedOutputPower) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = ratedOutputPower) val cpuLogic = mockk() every { cpuLogic.computePower() } returns 0.0 @@ -78,8 +78,8 @@ internal class SimPsuTest { 1.0 to 0.94, ) - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = ratedOutputPower) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = ratedOutputPower) val cpuLogic = mockk() every { cpuLogic.computePower() } returnsMany listOf(50.0, 100.0, 150.0, 200.0) diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt index 8cd535ad..058d5d28 100644 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt @@ -40,7 +40,7 @@ import org.opendc.simulator.compute.power.ConstantPowerModel import org.opendc.simulator.compute.power.SimplePowerDriver import org.opendc.simulator.compute.workload.SimTraceWorkload import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.SimResourceInterpreter +import org.opendc.simulator.flow.FlowEngine /** * Test suite for the [SimHypervisor] class. @@ -94,7 +94,7 @@ internal class SimHypervisorTest { ), ) - val platform = SimResourceInterpreter(coroutineContext, clock) + val platform = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine(platform, model, SimplePowerDriver(ConstantPowerModel(0.0))) val hypervisor = SimFairShareHypervisor(platform, scalingGovernor = PerformanceScalingGovernor(), listener = listener) @@ -163,7 +163,7 @@ internal class SimHypervisorTest { ) ) - val platform = SimResourceInterpreter(coroutineContext, clock) + val platform = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( platform, model, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -204,7 +204,7 @@ internal class SimHypervisorTest { memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } ) - val platform = SimResourceInterpreter(coroutineContext, clock) + val platform = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( platform, model, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -234,7 +234,7 @@ internal class SimHypervisorTest { ) val interferenceModel = VmInterferenceModel(groups) - val platform = SimResourceInterpreter(coroutineContext, clock) + val platform = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( platform, model, SimplePowerDriver(ConstantPowerModel(0.0)) ) diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt index 55d6d7c4..95fb6679 100644 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt @@ -40,7 +40,7 @@ import org.opendc.simulator.compute.workload.SimFlopsWorkload import org.opendc.simulator.compute.workload.SimRuntimeWorkload import org.opendc.simulator.compute.workload.SimTraceWorkload import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.SimResourceInterpreter +import org.opendc.simulator.flow.FlowEngine /** * A test suite for the [SimSpaceSharedHypervisor]. @@ -74,11 +74,11 @@ internal class SimSpaceSharedHypervisorTest { ), ) - val interpreter = SimResourceInterpreter(coroutineContext, clock) + val engine = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimSpaceSharedHypervisor(interpreter) + val hypervisor = SimSpaceSharedHypervisor(engine) launch { machine.run(hypervisor) } val vm = hypervisor.createMachine(machineModel) @@ -98,11 +98,11 @@ internal class SimSpaceSharedHypervisorTest { fun testRuntimeWorkload() = runBlockingSimulation { val duration = 5 * 60L * 1000 val workload = SimRuntimeWorkload(duration) - val interpreter = SimResourceInterpreter(coroutineContext, clock) + val engine = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( - interpreter, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) + engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimSpaceSharedHypervisor(interpreter) + val hypervisor = SimSpaceSharedHypervisor(engine) launch { machine.run(hypervisor) } yield() @@ -121,11 +121,11 @@ internal class SimSpaceSharedHypervisorTest { fun testFlopsWorkload() = runBlockingSimulation { val duration = 5 * 60L * 1000 val workload = SimFlopsWorkload((duration * 3.2).toLong(), 1.0) - val interpreter = SimResourceInterpreter(coroutineContext, clock) + val engine = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( - interpreter, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) + engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimSpaceSharedHypervisor(interpreter) + val hypervisor = SimSpaceSharedHypervisor(engine) launch { machine.run(hypervisor) } yield() @@ -142,11 +142,11 @@ internal class SimSpaceSharedHypervisorTest { @Test fun testTwoWorkloads() = runBlockingSimulation { val duration = 5 * 60L * 1000 - val interpreter = SimResourceInterpreter(coroutineContext, clock) + val engine = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( - interpreter, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) + engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimSpaceSharedHypervisor(interpreter) + val hypervisor = SimSpaceSharedHypervisor(engine) launch { machine.run(hypervisor) } yield() @@ -170,11 +170,9 @@ internal class SimSpaceSharedHypervisorTest { */ @Test fun testConcurrentWorkloadFails() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val machine = SimBareMetalMachine( - interpreter, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) - ) - val hypervisor = SimSpaceSharedHypervisor(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val machine = SimBareMetalMachine(engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0))) + val hypervisor = SimSpaceSharedHypervisor(engine) launch { machine.run(hypervisor) } yield() @@ -194,7 +192,7 @@ internal class SimSpaceSharedHypervisorTest { */ @Test fun testConcurrentWorkloadSucceeds() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) + val interpreter = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine( interpreter, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/power/PStatePowerDriverTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/power/PStatePowerDriverTest.kt index c39859bf..f557c8d3 100644 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/power/PStatePowerDriverTest.kt +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/power/PStatePowerDriverTest.kt @@ -55,7 +55,7 @@ internal class PStatePowerDriverTest { val cpu = mockk(relaxUnitFun = true) every { cpu.capacity } returns 3200.0 - every { cpu.speed } returns 1200.0 + every { cpu.rate } returns 1200.0 val driver = PStatePowerDriver( sortedMapOf( @@ -77,10 +77,10 @@ internal class PStatePowerDriverTest { val cpus = listOf(cpu, cpu) every { cpus[0].capacity } returns 1000.0 - every { cpus[0].speed } returns 1200.0 + every { cpus[0].rate } returns 1200.0 every { cpus[1].capacity } returns 3500.0 - every { cpus[1].speed } returns 1200.0 + every { cpus[1].rate } returns 1200.0 val driver = PStatePowerDriver( sortedMapOf( @@ -112,11 +112,11 @@ internal class PStatePowerDriverTest { val logic = driver.createLogic(machine, listOf(cpu)) - every { cpu.speed } returns 1400.0 + every { cpu.rate } returns 1400.0 every { cpu.capacity } returns 1400.0 assertEquals(150.0, logic.computePower()) - every { cpu.speed } returns 1400.0 + every { cpu.rate } returns 1400.0 every { cpu.capacity } returns 4000.0 assertEquals(235.0, logic.computePower()) } diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkloadTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkloadTest.kt index 78019c2e..cdbffe4b 100644 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkloadTest.kt +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkloadTest.kt @@ -31,7 +31,7 @@ import org.opendc.simulator.compute.model.* import org.opendc.simulator.compute.power.ConstantPowerModel import org.opendc.simulator.compute.power.SimplePowerDriver import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.SimResourceInterpreter +import org.opendc.simulator.flow.FlowEngine /** * Test suite for the [SimTraceWorkloadTest] class. @@ -52,7 +52,7 @@ class SimTraceWorkloadTest { @Test fun testSmoke() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -79,7 +79,7 @@ class SimTraceWorkloadTest { @Test fun testOffset() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -106,7 +106,7 @@ class SimTraceWorkloadTest { @Test fun testSkipFragment() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) @@ -134,7 +134,7 @@ class SimTraceWorkloadTest { @Test fun testZeroCores() = runBlockingSimulation { val machine = SimBareMetalMachine( - SimResourceInterpreter(coroutineContext, clock), + FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) diff --git a/opendc-simulator/opendc-simulator-flow/build.gradle.kts b/opendc-simulator/opendc-simulator-flow/build.gradle.kts new file mode 100644 index 00000000..5a956fee --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * 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. + */ + +description = "High-performance flow simulator" + +plugins { + `kotlin-library-conventions` + `testing-conventions` + `jacoco-conventions` + `benchmark-conventions` +} + +dependencies { + api(platform(projects.opendcPlatform)) + api(libs.kotlinx.coroutines) + implementation(projects.opendcUtils) + + testImplementation(projects.opendcSimulator.opendcSimulatorCore) +} diff --git a/opendc-simulator/opendc-simulator-flow/src/jmh/kotlin/org/opendc/simulator/flow/FlowBenchmarks.kt b/opendc-simulator/opendc-simulator-flow/src/jmh/kotlin/org/opendc/simulator/flow/FlowBenchmarks.kt new file mode 100644 index 00000000..4834f10f --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/jmh/kotlin/org/opendc/simulator/flow/FlowBenchmarks.kt @@ -0,0 +1,140 @@ +/* + * 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.flow + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import org.opendc.simulator.core.SimulationCoroutineScope +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.mux.ForwardingFlowMultiplexer +import org.opendc.simulator.flow.mux.MaxMinFlowMultiplexer +import org.opendc.simulator.flow.source.TraceFlowSource +import org.openjdk.jmh.annotations.* +import java.util.concurrent.ThreadLocalRandom +import java.util.concurrent.TimeUnit + +@State(Scope.Thread) +@Fork(1) +@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) +@OptIn(ExperimentalCoroutinesApi::class) +class FlowBenchmarks { + private lateinit var scope: SimulationCoroutineScope + private lateinit var engine: FlowEngine + + @Setup + fun setUp() { + scope = SimulationCoroutineScope() + engine = FlowEngine(scope.coroutineContext, scope.clock) + } + + @State(Scope.Thread) + class Workload { + lateinit var trace: Sequence + + @Setup + fun setUp() { + val random = ThreadLocalRandom.current() + val entries = List(10000) { TraceFlowSource.Fragment(1000, random.nextDouble(0.0, 4500.0)) } + trace = entries.asSequence() + } + } + + @Benchmark + fun benchmarkSink(state: Workload) { + return scope.runBlockingSimulation { + val provider = FlowSink(engine, 4200.0) + return@runBlockingSimulation provider.consume(TraceFlowSource(state.trace)) + } + } + + @Benchmark + fun benchmarkForward(state: Workload) { + return scope.runBlockingSimulation { + val provider = FlowSink(engine, 4200.0) + val forwarder = FlowForwarder(engine) + provider.startConsumer(forwarder) + return@runBlockingSimulation forwarder.consume(TraceFlowSource(state.trace)) + } + } + + @Benchmark + fun benchmarkMuxMaxMinSingleSource(state: Workload) { + return scope.runBlockingSimulation { + val switch = MaxMinFlowMultiplexer(engine) + + switch.addOutput(FlowSink(engine, 3000.0)) + switch.addOutput(FlowSink(engine, 3000.0)) + + val provider = switch.newInput() + return@runBlockingSimulation provider.consume(TraceFlowSource(state.trace)) + } + } + + @Benchmark + fun benchmarkMuxMaxMinTripleSource(state: Workload) { + return scope.runBlockingSimulation { + val switch = MaxMinFlowMultiplexer(engine) + + switch.addOutput(FlowSink(engine, 3000.0)) + switch.addOutput(FlowSink(engine, 3000.0)) + + repeat(3) { + launch { + val provider = switch.newInput() + provider.consume(TraceFlowSource(state.trace)) + } + } + } + } + + @Benchmark + fun benchmarkMuxExclusiveSingleSource(state: Workload) { + return scope.runBlockingSimulation { + val switch = ForwardingFlowMultiplexer(engine) + + switch.addOutput(FlowSink(engine, 3000.0)) + switch.addOutput(FlowSink(engine, 3000.0)) + + val provider = switch.newInput() + return@runBlockingSimulation provider.consume(TraceFlowSource(state.trace)) + } + } + + @Benchmark + fun benchmarkMuxExclusiveTripleSource(state: Workload) { + return scope.runBlockingSimulation { + val switch = ForwardingFlowMultiplexer(engine) + + switch.addOutput(FlowSink(engine, 3000.0)) + switch.addOutput(FlowSink(engine, 3000.0)) + + repeat(2) { + launch { + val provider = switch.newInput() + provider.consume(TraceFlowSource(state.trace)) + } + } + } + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/AbstractFlowConsumer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/AbstractFlowConsumer.kt new file mode 100644 index 00000000..c8092082 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/AbstractFlowConsumer.kt @@ -0,0 +1,143 @@ +/* + * 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.flow + +import org.opendc.simulator.flow.internal.FlowCountersImpl + +/** + * Abstract implementation of the [FlowConsumer] which can be re-used by other implementations. + */ +public abstract class AbstractFlowConsumer(private val engine: FlowEngine, initialCapacity: Double) : FlowConsumer { + /** + * A flag to indicate that the flow consumer is active. + */ + public override val isActive: Boolean + get() = ctx != null + + /** + * The capacity of the consumer. + */ + public override var capacity: Double = initialCapacity + set(value) { + field = value + ctx?.capacity = value + } + + /** + * The current processing rate of the consumer. + */ + public override val rate: Double + get() = ctx?.rate ?: 0.0 + + /** + * The flow processing rate demand at this instant. + */ + public override val demand: Double + get() = ctx?.demand ?: 0.0 + + /** + * The flow counters to track the flow metrics of the consumer. + */ + public override val counters: FlowCounters + get() = _counters + private val _counters = FlowCountersImpl() + + /** + * The [FlowConsumerContext] that is currently running. + */ + protected var ctx: FlowConsumerContext? = null + private set + + /** + * Construct the [FlowConsumerLogic] instance for a new source. + */ + protected abstract fun createLogic(): FlowConsumerLogic + + /** + * Start the specified [FlowConsumerContext]. + */ + protected open fun start(ctx: FlowConsumerContext) { + ctx.start() + } + + /** + * The previous demand for the consumer. + */ + private var previousDemand = 0.0 + + /** + * Update the counters of the flow consumer. + */ + protected fun updateCounters(ctx: FlowConnection, delta: Long) { + val demand = previousDemand + previousDemand = ctx.demand + + if (delta <= 0) { + return + } + + val counters = _counters + val deltaS = delta / 1000.0 + val work = demand * deltaS + val actualWork = ctx.rate * deltaS + val remainingWork = work - actualWork + + counters.demand += work + counters.actual += actualWork + counters.overcommit += remainingWork + } + + /** + * Update the counters of the flow consumer. + */ + protected fun updateCounters(demand: Double, actual: Double, overcommit: Double) { + val counters = _counters + counters.demand += demand + counters.actual += actual + counters.overcommit += overcommit + } + + final override fun startConsumer(source: FlowSource) { + check(ctx == null) { "Consumer is in invalid state" } + val ctx = engine.newContext(source, createLogic()) + + ctx.capacity = capacity + this.ctx = ctx + + start(ctx) + } + + final override fun pull() { + ctx?.pull() + } + + final override fun cancel() { + val ctx = ctx + if (ctx != null) { + this.ctx = null + ctx.close() + } + } + + override fun toString(): String = "AbstractFlowConsumer[capacity=$capacity]" +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConnection.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConnection.kt new file mode 100644 index 00000000..fa833961 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConnection.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.simulator.flow + +/** + * An active connection between a [FlowSource] and [FlowConsumer]. + */ +public interface FlowConnection : AutoCloseable { + /** + * The capacity of the connection. + */ + public val capacity: Double + + /** + * The flow rate over the connection. + */ + public val rate: Double + + /** + * The flow demand of the source. + */ + public val demand: Double + + /** + * Pull the source. + */ + public fun pull() + + /** + * Push the given flow [rate] over this connection. + * + * @param rate The rate of the flow to push. + */ + public fun push(rate: Double) + + /** + * Disconnect the consumer from its source. + */ + public override fun close() +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt new file mode 100644 index 00000000..3a6e2e97 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt @@ -0,0 +1,109 @@ +/* + * 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.flow + +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * A consumer of a [FlowSource]. + */ +public interface FlowConsumer { + /** + * A flag to indicate that the consumer is currently consuming a [FlowSource]. + */ + public val isActive: Boolean + + /** + * The flow capacity of this consumer. + */ + public val capacity: Double + + /** + * The current flow rate of the consumer. + */ + public val rate: Double + + /** + * The current flow demand. + */ + public val demand: Double + + /** + * The flow counters to track the flow metrics of the consumer. + */ + public val counters: FlowCounters + + /** + * Start consuming the specified [source]. + * + * @throws IllegalStateException if the consumer is already active. + */ + public fun startConsumer(source: FlowSource) + + /** + * Ask the consumer to pull its source. + * + * If the consumer is not active, this operation will be a no-op. + */ + public fun pull() + + /** + * Disconnect the consumer from its source. + * + * If the consumer is not active, this operation will be a no-op. + */ + public fun cancel() +} + +/** + * Consume the specified [source] and suspend execution until the source is fully consumed or failed. + */ +public suspend fun FlowConsumer.consume(source: FlowSource) { + return suspendCancellableCoroutine { cont -> + startConsumer(object : FlowSource by source { + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + source.onEvent(conn, now, event) + + if (event == FlowEvent.Exit && !cont.isCompleted) { + cont.resume(Unit) + } + } + + override fun onFailure(conn: FlowConnection, cause: Throwable) { + try { + source.onFailure(conn, cause) + cont.resumeWithException(cause) + } catch (e: Throwable) { + e.addSuppressed(cause) + cont.resumeWithException(e) + } + } + + override fun toString(): String = "SuspendingFlowSource" + }) + + cont.invokeOnCancellation { cancel() } + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerContext.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerContext.kt new file mode 100644 index 00000000..75b2d25b --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerContext.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.flow + +/** + * A controllable [FlowConnection]. + * + * This interface is used by [FlowConsumer]s to control the connection between it and the source. + */ +public interface FlowConsumerContext : FlowConnection { + /** + * The capacity of the connection. + */ + public override var capacity: Double + + /** + * Start the flow over the connection. + */ + public fun start() + + /** + * Synchronously flush the changes of the connection. + */ + public fun flush() +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt new file mode 100644 index 00000000..c69cb17e --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.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.simulator.flow + +/** + * A collection of callbacks associated with a [FlowConsumer]. + */ +public interface FlowConsumerLogic { + /** + * This method is invoked when a [FlowSource] changes the rate of flow to this consumer. + * + * @param ctx The context in which the provider runs. + * @param now The virtual timestamp in milliseconds at which the update is occurring. + * @param delta The virtual duration between this call and the last call to [onPush] in milliseconds. + * @param rate The requested processing rate of the source. + */ + public fun onPush(ctx: FlowConsumerContext, now: Long, delta: Long, rate: Double) {} + + /** + * This method is invoked when the flow graph has converged into a steady-state system. + * + * @param ctx The context in which the provider runs. + * @param now The virtual timestamp in milliseconds at which the system converged. + * @param delta The virtual duration between this call and the last call to [onConverge] in milliseconds. + */ + public fun onConverge(ctx: FlowConsumerContext, now: Long, delta: Long) {} + + /** + * This method is invoked when the [FlowSource] is completed. + * + * @param ctx The context in which the provider runs. + * @param now The virtual timestamp in milliseconds at which the provider finished. + * @param delta The virtual duration between this call and the last call to [onPush] in milliseconds. + */ + public fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long) {} +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowCounters.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowCounters.kt new file mode 100644 index 00000000..e15d7643 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowCounters.kt @@ -0,0 +1,53 @@ +/* + * 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.flow + +/** + * An interface that tracks cumulative counts of the flow accumulation over a stage. + */ +public interface FlowCounters { + /** + * The accumulated flow that a source wanted to push over the connection. + */ + public val demand: Double + + /** + * The accumulated flow that was actually transferred over the connection. + */ + public val actual: Double + + /** + * The accumulated flow that could not be transferred over the connection. + */ + public val overcommit: Double + + /** + * The accumulated flow lost due to interference between sources. + */ + public val interference: Double + + /** + * Reset the flow counters. + */ + public fun reset() +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEngine.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEngine.kt new file mode 100644 index 00000000..65224827 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEngine.kt @@ -0,0 +1,95 @@ +/* + * 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.flow + +import org.opendc.simulator.flow.internal.FlowEngineImpl +import java.time.Clock +import kotlin.coroutines.CoroutineContext + +/** + * A [FlowEngine] is responsible for managing the interaction between [FlowSource]s and [FlowConsumer]s. + * + * The engine centralizes the scheduling logic of state updates of flow connections, allowing update propagation + * to happen more efficiently. and overall, reducing the work necessary to transition into a steady state. + */ +public interface FlowEngine { + /** + * The virtual [Clock] associated with this engine. + */ + public val clock: Clock + + /** + * Create a new [FlowConsumerContext] with the given [provider]. + * + * @param consumer The consumer logic. + * @param provider The logic of the resource provider. + */ + public fun newContext(consumer: FlowSource, provider: FlowConsumerLogic): FlowConsumerContext + + /** + * Start batching the execution of resource updates until [popBatch] is called. + * + * This method is useful if you want to propagate multiple resources updates (e.g., starting multiple CPUs + * simultaneously) in a single state update. + * + * Multiple calls to this method requires the same number of [popBatch] calls in order to properly flush the + * resource updates. This allows nested calls to [pushBatch], but might cause issues if [popBatch] is not called + * the same amount of times. To simplify batching, see [batch]. + */ + public fun pushBatch() + + /** + * Stop the batching of resource updates and run the interpreter on the batch. + * + * Note that method will only flush the event once the first call to [pushBatch] has received a [popBatch] call. + */ + public fun popBatch() + + public companion object { + /** + * Construct a new [FlowEngine] implementation. + * + * @param context The coroutine context to use. + * @param clock The virtual simulation clock. + */ + @JvmStatic + @JvmName("create") + public operator fun invoke(context: CoroutineContext, clock: Clock): FlowEngine { + return FlowEngineImpl(context, clock) + } + } +} + +/** + * Batch the execution of several interrupts into a single call. + * + * This method is useful if you want to propagate the start of multiple resources (e.g., CPUs) in a single update. + */ +public inline fun FlowEngine.batch(block: () -> Unit) { + try { + pushBatch() + block() + } finally { + popBatch() + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt new file mode 100644 index 00000000..14c85183 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt @@ -0,0 +1,48 @@ +/* + * 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.flow + +/** + * A flow event that is communicated to a [FlowSource]. + */ +public enum class FlowEvent { + /** + * This event is emitted to the source when it has started. + */ + Start, + + /** + * This event is emitted to the source when it is stopped. + */ + Exit, + + /** + * This event is emitted to the source when the system has converged into a steady state. + */ + Converge, + + /** + * This event is emitted to the source when the capacity of the consumer has changed. + */ + Capacity, +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt new file mode 100644 index 00000000..2074033e --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt @@ -0,0 +1,217 @@ +/* + * 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.flow + +import org.opendc.simulator.flow.internal.FlowCountersImpl + +/** + * A class that acts as a [FlowSource] and [FlowConsumer] at the same time. + * + * @param engine The [FlowEngine] the forwarder runs in. + * @param isCoupled A flag to indicate that the transformer will exit when the resource consumer exits. + */ +public class FlowForwarder(private val engine: FlowEngine, private val isCoupled: Boolean = false) : FlowSource, FlowConsumer, AutoCloseable { + /** + * The delegate [FlowSource]. + */ + private var delegate: FlowSource? = null + + /** + * A flag to indicate that the delegate was started. + */ + private var hasDelegateStarted: Boolean = false + + /** + * The exposed [FlowConnection]. + */ + private val _ctx = object : FlowConnection { + override val capacity: Double + get() = _innerCtx?.capacity ?: 0.0 + + override val demand: Double + get() = _innerCtx?.demand ?: 0.0 + + override val rate: Double + get() = _innerCtx?.rate ?: 0.0 + + override fun pull() { + _innerCtx?.pull() + } + + override fun push(rate: Double) { + _innerCtx?.push(rate) + _demand = rate + } + + override fun close() { + val delegate = checkNotNull(delegate) { "Delegate not active" } + + if (isCoupled) + _innerCtx?.close() + else + _innerCtx?.push(0.0) + + // Warning: resumption of the continuation might change the entire state of the forwarder. Make sure we + // reset beforehand the existing state and check whether it has been updated afterwards + reset() + + delegate.onEvent(this, engine.clock.millis(), FlowEvent.Exit) + } + } + + /** + * The [FlowConnection] in which the forwarder runs. + */ + private var _innerCtx: FlowConnection? = null + + override val isActive: Boolean + get() = delegate != null + + override val capacity: Double + get() = _ctx.capacity + + override val rate: Double + get() = _ctx.rate + + override val demand: Double + get() = _ctx.demand + + override val counters: FlowCounters + get() = _counters + private val _counters = FlowCountersImpl() + + override fun startConsumer(source: FlowSource) { + check(delegate == null) { "Forwarder already active" } + + delegate = source + + // Pull to replace the source + pull() + } + + override fun pull() { + _ctx.pull() + } + + override fun cancel() { + val delegate = delegate + val ctx = _innerCtx + + if (delegate != null) { + this.delegate = null + + if (ctx != null) { + delegate.onEvent(this._ctx, engine.clock.millis(), FlowEvent.Exit) + } + } + } + + override fun close() { + val ctx = _innerCtx + + if (ctx != null) { + this._innerCtx = null + ctx.pull() + } + } + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + val delegate = delegate + + if (!hasDelegateStarted) { + start() + } + + updateCounters(conn, delta) + + return delegate?.onPull(this._ctx, now, delta) ?: Long.MAX_VALUE + } + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + when (event) { + FlowEvent.Start -> { + _innerCtx = conn + } + FlowEvent.Exit -> { + _innerCtx = null + + val delegate = delegate + if (delegate != null) { + reset() + delegate.onEvent(this._ctx, now, FlowEvent.Exit) + } + } + else -> delegate?.onEvent(this._ctx, now, event) + } + } + + override fun onFailure(conn: FlowConnection, cause: Throwable) { + _innerCtx = null + + val delegate = delegate + if (delegate != null) { + reset() + delegate.onFailure(this._ctx, cause) + } + } + + /** + * Start the delegate. + */ + private fun start() { + val delegate = delegate ?: return + delegate.onEvent(checkNotNull(_innerCtx), engine.clock.millis(), FlowEvent.Start) + + hasDelegateStarted = true + } + + /** + * Reset the delegate. + */ + private fun reset() { + delegate = null + hasDelegateStarted = false + } + + /** + * The requested flow rate. + */ + private var _demand: Double = 0.0 + + /** + * Update the flow counters for the transformer. + */ + private fun updateCounters(ctx: FlowConnection, delta: Long) { + if (delta <= 0) { + return + } + + val counters = _counters + val deltaS = delta / 1000.0 + val work = _demand * deltaS + val actualWork = ctx.rate * deltaS + counters.demand += work + counters.actual += actualWork + counters.overcommit += (work - actualWork) + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt new file mode 100644 index 00000000..fb6ca85d --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.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.simulator.flow + +/** + * A [FlowSink] represents a sink with a fixed capacity. + * + * @param initialCapacity The initial capacity of the resource. + * @param engine The engine that is used for driving the flow simulation. + * @param parent The parent flow system. + */ +public class FlowSink( + private val engine: FlowEngine, + initialCapacity: Double, + private val parent: FlowSystem? = null +) : AbstractFlowConsumer(engine, initialCapacity) { + + override fun createLogic(): FlowConsumerLogic { + return object : FlowConsumerLogic { + override fun onPush( + ctx: FlowConsumerContext, + now: Long, + delta: Long, + rate: Double + ) { + updateCounters(ctx, delta) + } + + override fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long) { + updateCounters(ctx, delta) + cancel() + } + + override fun onConverge(ctx: FlowConsumerContext, now: Long, delta: Long) { + parent?.onConverge(now) + } + } + } + + override fun toString(): String = "FlowSink[capacity=$capacity]" +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt new file mode 100644 index 00000000..077b4d38 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.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.simulator.flow + +/** + * A source of flow that is consumed by a [FlowConsumer]. + * + * Implementations of this interface should be considered stateful and must be assumed not to be re-usable + * (concurrently) for multiple [FlowConsumer]s, unless explicitly said otherwise. + */ +public interface FlowSource { + /** + * This method is invoked when the source is pulled. + * + * @param conn The connection between the source and consumer. + * @param now The virtual timestamp in milliseconds at which the pull is occurring. + * @param delta The virtual duration between this call and the last call to [onPull] in milliseconds. + * @return The duration after which the resource consumer should be pulled again. + */ + public fun onPull(conn: FlowConnection, now: Long, delta: Long): Long + + /** + * This method is invoked when an event has occurred. + * + * @param conn The connection between the source and consumer. + * @param now The virtual timestamp in milliseconds at which the event is occurring. + * @param event The event that has occurred. + */ + public fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) {} + + /** + * This method is invoked when the source throws an exception. + * + * @param conn The connection between the source and consumer. + * @param cause The cause of the failure. + */ + public fun onFailure(conn: FlowConnection, cause: Throwable) {} +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSystem.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSystem.kt new file mode 100644 index 00000000..db6aa69f --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSystem.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.simulator.flow + +/** + * A system of possible multiple sub-resources. + * + * This interface is used to model hierarchies of resource providers, which can listen efficiently to changes of the + * resource provider. + */ +public interface FlowSystem { + /** + * The parent system to which this system belongs or `null` if it has no parent. + */ + public val parent: FlowSystem? + + /** + * This method is invoked when the system has converged to a steady-state. + * + * @param timestamp The timestamp at which the system converged. + */ + public fun onConverge(timestamp: Long) +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/interference/InterferenceDomain.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/interference/InterferenceDomain.kt new file mode 100644 index 00000000..aa2713b6 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/interference/InterferenceDomain.kt @@ -0,0 +1,19 @@ +package org.opendc.simulator.flow.interference + +import org.opendc.simulator.flow.FlowSource + +/** + * An interference domain represents a system of flow stages where [flow sources][FlowSource] may incur + * performance variability due to operating on the same resource and therefore causing interference. + */ +public interface InterferenceDomain { + /** + * Compute the performance score of a participant in this interference domain. + * + * @param key The participant to obtain the score of or `null` if the participant has no key. + * @param load The overall load on the interference domain. + * @return A score representing the performance score to be applied to the resource consumer, with 1 + * meaning no influence, <1 means that performance degrades, and >1 means that performance improves. + */ + public fun apply(key: InterferenceKey?, load: Double): Double +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/interference/InterferenceKey.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/interference/InterferenceKey.kt new file mode 100644 index 00000000..d28ebde5 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/interference/InterferenceKey.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.simulator.flow.interference + +/** + * A key that uniquely identifies a participant of an interference domain. + */ +public interface InterferenceKey diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt new file mode 100644 index 00000000..9f3afc4d --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt @@ -0,0 +1,356 @@ +/* + * 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.flow.internal + +import org.opendc.simulator.flow.* +import java.util.ArrayDeque +import kotlin.math.max +import kotlin.math.min + +/** + * Implementation of a [FlowConnection] managing the communication between flow sources and consumers. + */ +internal class FlowConsumerContextImpl( + private val engine: FlowEngineImpl, + private val source: FlowSource, + private val logic: FlowConsumerLogic +) : FlowConsumerContext { + /** + * The clock to track simulation time. + */ + private val _clock = engine.clock + + /** + * The capacity of the resource. + */ + override var capacity: Double = 0.0 + set(value) { + val oldValue = field + + // Only changes will be propagated + if (value != oldValue) { + field = value + onCapacityChange() + } + } + + /** + * A flag to indicate the state of the context. + */ + private var _state = State.Pending + + /** + * The current processing speed of the resource. + */ + override val rate: Double + get() = _rate + private var _rate = 0.0 + + /** + * The current resource processing demand. + */ + override val demand: Double + get() = _limit + + /** + * The current state of the resource context. + */ + private var _limit: Double = 0.0 + private var _activeLimit: Double = 0.0 + private var _deadline: Long = Long.MIN_VALUE + + /** + * A flag to indicate that an update is active. + */ + private var _updateActive = false + + /** + * The update flag indicating why the update was triggered. + */ + private var _flag: Int = 0 + + /** + * The timestamp of calls to the callbacks. + */ + private var _lastUpdate: Long = Long.MIN_VALUE + private var _lastConvergence: Long = Long.MAX_VALUE + + /** + * The timers at which the context is scheduled to be interrupted. + */ + private val _timers: ArrayDeque = ArrayDeque() + + override fun start() { + check(_state == State.Pending) { "Consumer is already started" } + engine.batch { + source.onEvent(this, _clock.millis(), FlowEvent.Start) + _state = State.Active + pull() + } + } + + override fun close() { + if (_state == State.Stopped) { + return + } + + engine.batch { + _state = State.Stopped + if (!_updateActive) { + val now = _clock.millis() + val delta = max(0, now - _lastUpdate) + doStop(now, delta) + + // FIX: Make sure the context converges + _flag = _flag or FLAG_INVALIDATE + scheduleUpdate(_clock.millis()) + } + } + } + + override fun pull() { + if (_state == State.Stopped) { + return + } + + _flag = _flag or FLAG_INTERRUPT + scheduleUpdate(_clock.millis()) + } + + override fun flush() { + if (_state == State.Stopped) { + return + } + + engine.scheduleSync(_clock.millis(), this) + } + + override fun push(rate: Double) { + if (_limit == rate) { + return + } + + _limit = rate + + // Invalidate only if the active limit is change and no update is active + // If an update is active, it will already get picked up at the end of the update + if (_activeLimit != rate && !_updateActive) { + _flag = _flag or FLAG_INVALIDATE + scheduleUpdate(_clock.millis()) + } + } + + /** + * Determine whether the state of the resource context should be updated. + */ + fun shouldUpdate(timestamp: Long): Boolean { + // Either the resource context is flagged or there is a pending update at this timestamp + return _flag != 0 || _limit != _activeLimit || _deadline == timestamp + } + + /** + * Update the state of the resource context. + */ + fun doUpdate(now: Long) { + val oldState = _state + if (oldState != State.Active) { + return + } + + val lastUpdate = _lastUpdate + + _lastUpdate = now + _updateActive = true + + val delta = max(0, now - lastUpdate) + + try { + val duration = source.onPull(this, now, delta) + val newDeadline = if (duration != Long.MAX_VALUE) now + duration else duration + + // Reset update flags + _flag = 0 + + // Check whether the state has changed after [consumer.onNext] + when (_state) { + State.Active -> { + logic.onPush(this, now, delta, _limit) + + // Schedule an update at the new deadline + scheduleUpdate(now, newDeadline) + } + State.Stopped -> doStop(now, delta) + State.Pending -> throw IllegalStateException("Illegal transition to pending state") + } + + // Note: pending limit might be changed by [logic.onConsume], so re-fetch the value + val newLimit = _limit + + // Flush the changes to the flow + _activeLimit = newLimit + _deadline = newDeadline + _rate = min(capacity, newLimit) + } catch (cause: Throwable) { + doFail(now, delta, cause) + } finally { + _updateActive = false + } + } + + /** + * Prune the elapsed timers from this context. + */ + fun pruneTimers(now: Long) { + val timers = _timers + while (true) { + val head = timers.peek() + if (head == null || head.target > now) { + break + } + timers.poll() + } + } + + /** + * Try to re-schedule the resource context in case it was skipped. + */ + fun tryReschedule(now: Long) { + val deadline = _deadline + if (deadline > now && deadline != Long.MAX_VALUE) { + scheduleUpdate(now, deadline) + } + } + + /** + * This method is invoked when the system converges into a steady state. + */ + fun onConverge(timestamp: Long) { + val delta = max(0, timestamp - _lastConvergence) + _lastConvergence = timestamp + + try { + if (_state == State.Active) { + source.onEvent(this, timestamp, FlowEvent.Converge) + } + + logic.onConverge(this, timestamp, delta) + } catch (cause: Throwable) { + doFail(timestamp, max(0, timestamp - _lastUpdate), cause) + } + } + + override fun toString(): String = "FlowConsumerContextImpl[capacity=$capacity,rate=$_rate]" + + /** + * Stop the resource context. + */ + private fun doStop(now: Long, delta: Long) { + try { + source.onEvent(this, now, FlowEvent.Exit) + logic.onFinish(this, now, delta) + } catch (cause: Throwable) { + doFail(now, delta, cause) + } finally { + _deadline = Long.MAX_VALUE + _limit = 0.0 + } + } + + /** + * Fail the resource consumer. + */ + private fun doFail(now: Long, delta: Long, cause: Throwable) { + try { + source.onFailure(this, cause) + } catch (e: Throwable) { + e.addSuppressed(cause) + e.printStackTrace() + } + + logic.onFinish(this, now, delta) + } + + /** + * Indicate that the capacity of the resource has changed. + */ + private fun onCapacityChange() { + // Do not inform the consumer if it has not been started yet + if (_state != State.Active) { + return + } + + engine.batch { + // Inform the consumer of the capacity change. This might already trigger an interrupt. + source.onEvent(this, _clock.millis(), FlowEvent.Capacity) + + pull() + } + } + + /** + * Schedule an update for this resource context. + */ + private fun scheduleUpdate(now: Long) { + engine.scheduleImmediate(now, this) + } + + /** + * Schedule a delayed update for this resource context. + */ + private fun scheduleUpdate(now: Long, target: Long) { + val timers = _timers + if (target != Long.MAX_VALUE && (timers.isEmpty() || target < timers.peek().target)) { + timers.addFirst(engine.scheduleDelayed(now, this, target)) + } + } + + /** + * The state of a resource context. + */ + private enum class State { + /** + * The resource context is pending and the resource is waiting to be consumed. + */ + Pending, + + /** + * The resource context is active and the resource is currently being consumed. + */ + Active, + + /** + * The resource context is stopped and the resource cannot be consumed anymore. + */ + Stopped + } + + /** + * A flag to indicate that the context should be invalidated. + */ + private val FLAG_INVALIDATE = 0b01 + + /** + * A flag to indicate that the context should be interrupted. + */ + private val FLAG_INTERRUPT = 0b10 +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowCountersImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowCountersImpl.kt new file mode 100644 index 00000000..141d335d --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowCountersImpl.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.simulator.flow.internal + +import org.opendc.simulator.flow.FlowCounters + +/** + * Mutable implementation of the [FlowCounters] interface. + */ +internal class FlowCountersImpl : FlowCounters { + override var demand: Double = 0.0 + override var actual: Double = 0.0 + override var overcommit: Double = 0.0 + override var interference: Double = 0.0 + + override fun reset() { + demand = 0.0 + actual = 0.0 + overcommit = 0.0 + interference = 0.0 + } + + override fun toString(): String { + return "FlowCounters[demand=$demand,actual=$actual,overcommit=$overcommit,interference=$interference]" + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt new file mode 100644 index 00000000..1a50da2c --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt @@ -0,0 +1,297 @@ +/* + * 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.flow.internal + +import kotlinx.coroutines.Delay +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.Runnable +import org.opendc.simulator.flow.* +import java.time.Clock +import java.util.* +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext + +/** + * Internal implementation of the [FlowEngine] interface. + * + * @param context The coroutine context to use. + * @param clock The virtual simulation clock. + */ +internal class FlowEngineImpl(private val context: CoroutineContext, override val clock: Clock) : FlowEngine { + /** + * The [Delay] instance that provides scheduled execution of [Runnable]s. + */ + @OptIn(InternalCoroutinesApi::class) + private val delay = requireNotNull(context[ContinuationInterceptor] as? Delay) { "Invalid CoroutineDispatcher: no delay implementation" } + + /** + * The queue of connection updates that are scheduled for immediate execution. + */ + private val queue = ArrayDeque() + + /** + * A priority queue containing the connection updates to be scheduled in the future. + */ + private val futureQueue = PriorityQueue() + + /** + * The stack of engine invocations to occur in the future. + */ + private val futureInvocations = ArrayDeque() + + /** + * The systems that have been visited during the engine cycle. + */ + private val visited = linkedSetOf() + + /** + * The index in the batch stack. + */ + private var batchIndex = 0 + + /** + * A flag to indicate that the engine is currently active. + */ + private val isRunning: Boolean + get() = batchIndex > 0 + + /** + * Update the specified [ctx] synchronously. + */ + fun scheduleSync(now: Long, ctx: FlowConsumerContextImpl) { + ctx.doUpdate(now) + visited.add(ctx) + + // In-case the engine is already running in the call-stack, return immediately. The changes will be picked + // up by the active engine. + if (isRunning) { + return + } + + try { + batchIndex++ + runEngine(now) + } finally { + batchIndex-- + } + } + + /** + * Enqueue the specified [ctx] to be updated immediately during the active engine cycle. + * + * This method should be used when the state of a flow context is invalidated/interrupted and needs to be + * re-computed. In case no engine is currently active, the engine will be started. + */ + fun scheduleImmediate(now: Long, ctx: FlowConsumerContextImpl) { + queue.add(ctx) + + // In-case the engine is already running in the call-stack, return immediately. The changes will be picked + // up by the active engine. + if (isRunning) { + return + } + + try { + batchIndex++ + runEngine(now) + } finally { + batchIndex-- + } + } + + /** + * Schedule the engine to run at [target] to update the flow contexts. + * + * This method will override earlier calls to this method for the same [ctx]. + * + * @param now The current virtual timestamp. + * @param ctx The flow context to which the event applies. + * @param target The timestamp when the interrupt should happen. + */ + fun scheduleDelayed(now: Long, ctx: FlowConsumerContextImpl, target: Long): Timer { + val futureQueue = futureQueue + + require(target >= now) { "Timestamp must be in the future" } + + val timer = Timer(ctx, target) + futureQueue.add(timer) + + return timer + } + + override fun newContext(consumer: FlowSource, provider: FlowConsumerLogic): FlowConsumerContext = FlowConsumerContextImpl(this, consumer, provider) + + override fun pushBatch() { + batchIndex++ + } + + override fun popBatch() { + try { + // Flush the work if the platform is not already running + if (batchIndex == 1 && queue.isNotEmpty()) { + runEngine(clock.millis()) + } + } finally { + batchIndex-- + } + } + + /** + * Run all the enqueued actions for the specified [timestamp][now]. + */ + private fun runEngine(now: Long) { + val queue = queue + val futureQueue = futureQueue + val futureInvocations = futureInvocations + val visited = visited + + // Remove any entries in the `futureInvocations` queue from the past + while (true) { + val head = futureInvocations.peek() + if (head == null || head.timestamp > now) { + break + } + futureInvocations.poll() + } + + // Execute all scheduled updates at current timestamp + while (true) { + val timer = futureQueue.peek() ?: break + val ctx = timer.ctx + val target = timer.target + + assert(target >= now) { "Internal inconsistency: found update of the past" } + + if (target > now) { + break + } + + futureQueue.poll() + + ctx.pruneTimers(now) + + if (ctx.shouldUpdate(now)) { + ctx.doUpdate(now) + visited.add(ctx) + } else { + ctx.tryReschedule(now) + } + } + + // Repeat execution of all immediate updates until the system has converged to a steady-state + // We have to take into account that the onConverge callback can also trigger new actions. + do { + // Execute all immediate updates + while (true) { + val ctx = queue.poll() ?: break + + if (ctx.shouldUpdate(now)) { + ctx.doUpdate(now) + visited.add(ctx) + } + } + + for (system in visited) { + system.onConverge(now) + } + + visited.clear() + } while (queue.isNotEmpty()) + + // Schedule an engine invocation for the next update to occur. + val headTimer = futureQueue.peek() + if (headTimer != null) { + trySchedule(now, futureInvocations, headTimer.target) + } + } + + /** + * Try to schedule an engine invocation at the specified [target]. + * + * @param now The current virtual timestamp. + * @param target The virtual timestamp at which the engine invocation should happen. + * @param scheduled The queue of scheduled invocations. + */ + private fun trySchedule(now: Long, scheduled: ArrayDeque, target: Long) { + while (true) { + val invocation = scheduled.peekFirst() + if (invocation == null || invocation.timestamp > target) { + // Case 2: A new timer was registered ahead of the other timers. + // Solution: Schedule a new scheduler invocation + @OptIn(InternalCoroutinesApi::class) + val handle = delay.invokeOnTimeout( + target - now, + { + try { + batchIndex++ + runEngine(target) + } finally { + batchIndex-- + } + }, + context + ) + scheduled.addFirst(Invocation(target, handle)) + break + } else if (invocation.timestamp < target) { + // Case 2: A timer was cancelled and the head of the timer queue is now later than excepted + // Solution: Cancel the next scheduler invocation + scheduled.pollFirst() + + invocation.cancel() + } else { + break + } + } + } + + /** + * A future engine invocation. + * + * This class is used to keep track of the future engine invocations created using the [Delay] instance. In case + * the invocation is not needed anymore, it can be cancelled via [cancel]. + */ + private data class Invocation( + @JvmField val timestamp: Long, + @JvmField val handle: DisposableHandle + ) { + /** + * Cancel the engine invocation. + */ + fun cancel() = handle.dispose() + } + + /** + * An update call for [ctx] that is scheduled for [target]. + * + * This class represents an update in the future at [target] requested by [ctx]. + */ + class Timer(@JvmField val ctx: FlowConsumerContextImpl, @JvmField val target: Long) : Comparable { + override fun compareTo(other: Timer): Int { + return target.compareTo(other.target) + } + + override fun toString(): String = "Timer[ctx=$ctx,timestamp=$target]" + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/FlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/FlowMultiplexer.kt new file mode 100644 index 00000000..17b82391 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/FlowMultiplexer.kt @@ -0,0 +1,70 @@ +/* + * 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.flow.mux + +import org.opendc.simulator.flow.FlowConsumer +import org.opendc.simulator.flow.FlowCounters +import org.opendc.simulator.flow.FlowSource +import org.opendc.simulator.flow.interference.InterferenceKey + +/** + * A [FlowMultiplexer] enables multiplexing multiple [FlowSource]s over possibly multiple [FlowConsumer]s. + */ +public interface FlowMultiplexer { + /** + * The inputs of the multiplexer that can be used to consume sources. + */ + public val inputs: Set + + /** + * The outputs of the multiplexer over which the flows will be distributed. + */ + public val outputs: Set + + /** + * The flow counters to track the flow metrics of all multiplexer inputs. + */ + public val counters: FlowCounters + + /** + * Create a new input on this multiplexer. + * + * @param key The key of the interference member to which the input belongs. + */ + public fun newInput(key: InterferenceKey? = null): FlowConsumer + + /** + * Remove [input] from this multiplexer. + */ + public fun removeInput(input: FlowConsumer) + + /** + * Add the specified [output] to the multiplexer. + */ + public fun addOutput(output: FlowConsumer) + + /** + * Clear all inputs and outputs from the switch. + */ + public fun clear() +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt new file mode 100644 index 00000000..811d9460 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt @@ -0,0 +1,127 @@ +/* + * 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.flow.mux + +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.interference.InterferenceKey +import java.util.ArrayDeque + +/** + * A [FlowMultiplexer] implementation that allocates inputs to the outputs of the multiplexer exclusively. This means + * that a single input is directly connected to an output and that the multiplexer can only support as many + * inputs as outputs. + * + * @param engine The [FlowEngine] driving the simulation. + */ +public class ForwardingFlowMultiplexer(private val engine: FlowEngine) : FlowMultiplexer { + override val inputs: Set + get() = _inputs + private val _inputs = mutableSetOf() + + override val outputs: Set + get() = _outputs + private val _outputs = mutableSetOf() + private val _availableOutputs = ArrayDeque() + + override val counters: FlowCounters = object : FlowCounters { + override val demand: Double + get() = _outputs.sumOf { it.counters.demand } + override val actual: Double + get() = _outputs.sumOf { it.counters.actual } + override val overcommit: Double + get() = _outputs.sumOf { it.counters.overcommit } + override val interference: Double + get() = _outputs.sumOf { it.counters.interference } + + override fun reset() { + for (input in _outputs) { + input.counters.reset() + } + } + + override fun toString(): String = "FlowCounters[demand=$demand,actual=$actual,overcommit=$overcommit]" + } + + override fun newInput(key: InterferenceKey?): FlowConsumer { + val forwarder = checkNotNull(_availableOutputs.poll()) { "No capacity to serve request" } + val output = Input(forwarder) + _inputs += output + return output + } + + override fun removeInput(input: FlowConsumer) { + if (!_inputs.remove(input)) { + return + } + + (input as Input).close() + } + + override fun addOutput(output: FlowConsumer) { + if (output in outputs) { + return + } + + val forwarder = FlowForwarder(engine) + + _outputs += output + _availableOutputs += forwarder + + output.startConsumer(object : FlowSource by forwarder { + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + if (event == FlowEvent.Exit) { + // De-register the output after it has finished + _outputs -= output + } + + forwarder.onEvent(conn, now, event) + } + }) + } + + override fun clear() { + for (input in _outputs) { + input.cancel() + } + _outputs.clear() + + // Inputs are implicitly cancelled by the output forwarders + _inputs.clear() + } + + /** + * An input on the multiplexer. + */ + private inner class Input(private val forwarder: FlowForwarder) : FlowConsumer by forwarder { + /** + * Close the input. + */ + fun close() { + // We explicitly do not close the forwarder here in order to re-use it across input resources. + _inputs -= this + _availableOutputs += forwarder + } + + override fun toString(): String = "ForwardingFlowMultiplexer.Input" + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt new file mode 100644 index 00000000..9735f121 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt @@ -0,0 +1,399 @@ +/* + * 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.flow.mux + +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.interference.InterferenceDomain +import org.opendc.simulator.flow.interference.InterferenceKey +import org.opendc.simulator.flow.internal.FlowCountersImpl +import kotlin.math.max +import kotlin.math.min + +/** + * A [FlowMultiplexer] implementation that multiplexes flows over the available outputs using max-min fair sharing. + * + * @param engine The [FlowEngine] to drive the flow simulation. + * @param parent The parent flow system of the multiplexer. + * @param interferenceDomain The interference domain of the multiplexer. + */ +public class MaxMinFlowMultiplexer( + private val engine: FlowEngine, + private val parent: FlowSystem? = null, + private val interferenceDomain: InterferenceDomain? = null +) : FlowMultiplexer { + /** + * The inputs of the multiplexer. + */ + override val inputs: Set + get() = _inputs + private val _inputs = mutableSetOf() + private val _activeInputs = mutableListOf() + + /** + * The outputs of the multiplexer. + */ + override val outputs: Set + get() = _outputs + private val _outputs = mutableSetOf() + private val _activeOutputs = mutableListOf() + + /** + * The flow counters of this multiplexer. + */ + public override val counters: FlowCounters + get() = _counters + private val _counters = FlowCountersImpl() + + /** + * The actual processing rate of the multiplexer. + */ + private var _rate = 0.0 + + /** + * The demanded processing rate of the input. + */ + private var _demand = 0.0 + + /** + * The capacity of the outputs. + */ + private var _capacity = 0.0 + + /** + * Flag to indicate that the scheduler is active. + */ + private var _schedulerActive = false + + override fun newInput(key: InterferenceKey?): FlowConsumer { + val provider = Input(_capacity, key) + _inputs.add(provider) + return provider + } + + override fun addOutput(output: FlowConsumer) { + val consumer = Output(output) + if (_outputs.add(output)) { + _activeOutputs.add(consumer) + output.startConsumer(consumer) + } + } + + override fun removeInput(input: FlowConsumer) { + if (!_inputs.remove(input)) { + return + } + // This cast should always succeed since only `Input` instances should be added to `_inputs` + (input as Input).close() + } + + override fun clear() { + for (input in _activeOutputs) { + input.cancel() + } + _activeOutputs.clear() + + for (output in _activeInputs) { + output.cancel() + } + _activeInputs.clear() + } + + /** + * Converge the scheduler of the multiplexer. + */ + private fun runScheduler(now: Long) { + if (_schedulerActive) { + return + } + + _schedulerActive = true + try { + doSchedule(now) + } finally { + _schedulerActive = false + } + } + + /** + * Schedule the inputs over the outputs. + */ + private fun doSchedule(now: Long) { + val activeInputs = _activeInputs + val activeOutputs = _activeOutputs + + // If there is no work yet, mark the inputs as idle. + if (activeInputs.isEmpty()) { + return + } + + val capacity = _capacity + var availableCapacity = capacity + + // Pull in the work of the outputs + val inputIterator = activeInputs.listIterator() + for (input in inputIterator) { + input.pull(now) + + // Remove outputs that have finished + if (!input.isActive) { + inputIterator.remove() + } + } + + var demand = 0.0 + + // Sort in-place the inputs based on their pushed flow. + // Profiling shows that it is faster than maintaining some kind of sorted set. + activeInputs.sort() + + // Divide the available output capacity fairly over the inputs using max-min fair sharing + var remaining = activeInputs.size + for (input in activeInputs) { + val availableShare = availableCapacity / remaining-- + val grantedRate = min(input.allowedRate, availableShare) + + // Ignore empty sources + if (grantedRate <= 0.0) { + input.actualRate = 0.0 + continue + } + + input.actualRate = grantedRate + demand += input.limit + availableCapacity -= grantedRate + } + + val rate = capacity - availableCapacity + + _demand = demand + _rate = rate + + // Sort all consumers by their capacity + activeOutputs.sort() + + // Divide the requests over the available capacity of the input resources fairly + for (output in activeOutputs) { + val inputCapacity = output.capacity + val fraction = inputCapacity / capacity + val grantedSpeed = rate * fraction + + output.push(grantedSpeed) + } + } + + /** + * Recompute the capacity of the multiplexer. + */ + private fun updateCapacity() { + val newCapacity = _activeOutputs.sumOf(Output::capacity) + + // No-op if the capacity is unchanged + if (_capacity == newCapacity) { + return + } + + _capacity = newCapacity + + for (input in _inputs) { + input.capacity = newCapacity + } + } + + /** + * An internal [FlowConsumer] implementation for multiplexer inputs. + */ + private inner class Input(capacity: Double, val key: InterferenceKey?) : + AbstractFlowConsumer(engine, capacity), + FlowConsumerLogic, + Comparable { + /** + * The requested limit. + */ + @JvmField var limit: Double = 0.0 + + /** + * The actual processing speed. + */ + @JvmField var actualRate: Double = 0.0 + + /** + * The processing rate that is allowed by the model constraints. + */ + val allowedRate: Double + get() = min(capacity, limit) + + /** + * A flag to indicate that the input is closed. + */ + private var _isClosed: Boolean = false + + /** + * The timestamp at which we received the last command. + */ + private var _lastPull: Long = Long.MIN_VALUE + + /** + * Close the input. + * + * This method is invoked when the user removes an input from the switch. + */ + fun close() { + _isClosed = true + cancel() + } + + /* AbstractFlowConsumer */ + override fun createLogic(): FlowConsumerLogic = this + + override fun start(ctx: FlowConsumerContext) { + check(!_isClosed) { "Cannot re-use closed input" } + + _activeInputs += this + super.start(ctx) + } + + /* FlowConsumerLogic */ + override fun onPush( + ctx: FlowConsumerContext, + now: Long, + delta: Long, + rate: Double + ) { + doUpdateCounters(delta) + + actualRate = 0.0 + this.limit = rate + _lastPull = now + + runScheduler(now) + } + + override fun onConverge(ctx: FlowConsumerContext, now: Long, delta: Long) { + parent?.onConverge(now) + } + + override fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long) { + doUpdateCounters(delta) + + limit = 0.0 + actualRate = 0.0 + _lastPull = now + } + + /* Comparable */ + override fun compareTo(other: Input): Int = allowedRate.compareTo(other.allowedRate) + + /** + * Pull the source if necessary. + */ + fun pull(now: Long) { + val ctx = ctx + if (ctx != null && _lastPull < now) { + ctx.flush() + } + } + + /** + * Helper method to update the flow counters of the multiplexer. + */ + private fun doUpdateCounters(delta: Long) { + if (delta <= 0L) { + return + } + + // Compute the performance penalty due to flow interference + val perfScore = if (interferenceDomain != null) { + val load = _rate / capacity + interferenceDomain.apply(key, load) + } else { + 1.0 + } + + val deltaS = delta / 1000.0 + val work = limit * deltaS + val actualWork = actualRate * deltaS + val remainingWork = work - actualWork + + updateCounters(work, actualWork, remainingWork) + + val distCounters = _counters + distCounters.demand += work + distCounters.actual += actualWork + distCounters.overcommit += remainingWork + distCounters.interference += actualWork * max(0.0, 1 - perfScore) + } + } + + /** + * An internal [FlowSource] implementation for multiplexer outputs. + */ + private inner class Output(private val provider: FlowConsumer) : FlowSource, Comparable { + /** + * The active [FlowConnection] of this source. + */ + private var _ctx: FlowConnection? = null + + /** + * The capacity of this output. + */ + val capacity: Double + get() = _ctx?.capacity ?: 0.0 + + /** + * Push the specified rate to the consumer. + */ + fun push(rate: Double) { + _ctx?.push(rate) + } + + /** + * Cancel this output. + */ + fun cancel() { + provider.cancel() + } + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + runScheduler(now) + return Long.MAX_VALUE + } + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + when (event) { + FlowEvent.Start -> { + assert(_ctx == null) { "Source running concurrently" } + _ctx = conn + updateCapacity() + } + FlowEvent.Exit -> { + _ctx = null + updateCapacity() + } + FlowEvent.Capacity -> updateCapacity() + else -> {} + } + } + + override fun compareTo(other: Output): Int = capacity.compareTo(other.capacity) + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FixedFlowSource.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FixedFlowSource.kt new file mode 100644 index 00000000..d9779c6a --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FixedFlowSource.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.simulator.flow.source + +import org.opendc.simulator.flow.FlowConnection +import org.opendc.simulator.flow.FlowSource +import kotlin.math.roundToLong + +/** + * A [FlowSource] that contains a fixed [amount] and is pushed with a given [utilization]. + */ +public class FixedFlowSource(private val amount: Double, private val utilization: Double) : FlowSource { + + init { + require(amount >= 0.0) { "Amount must be positive" } + require(utilization > 0.0) { "Utilization must be positive" } + } + + private var remainingAmount = amount + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + val consumed = conn.rate * delta / 1000.0 + val limit = conn.capacity * utilization + + remainingAmount -= consumed + + val duration = (remainingAmount / limit * 1000).roundToLong() + + return if (duration > 0) { + conn.push(limit) + duration + } else { + conn.close() + Long.MAX_VALUE + } + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceBarrier.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceBarrier.kt new file mode 100644 index 00000000..b3191ad3 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceBarrier.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.flow.source + +/** + * The [FlowSourceBarrier] is a barrier that allows multiple sources to wait for a select number of other sources to + * finish a pull, before proceeding its operation. + */ +public class FlowSourceBarrier(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 + } + + /** + * Reset the barrier. + */ + public fun reset() { + counter = 0 + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt new file mode 100644 index 00000000..7fcc0405 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt @@ -0,0 +1,82 @@ +/* + * 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.flow.source + +import org.opendc.simulator.flow.FlowConnection +import org.opendc.simulator.flow.FlowEvent +import org.opendc.simulator.flow.FlowSource +import kotlin.math.min + +/** + * Helper class to expose an observable [speed] field describing the speed of the consumer. + */ +public class FlowSourceRateAdapter( + private val delegate: FlowSource, + private val callback: (Double) -> Unit = {} +) : FlowSource by delegate { + /** + * The resource processing speed at this instant. + */ + public var speed: Double = 0.0 + private set(value) { + if (field != value) { + callback(value) + field = value + } + } + + init { + callback(0.0) + } + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return delegate.onPull(conn, now, delta) + } + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + val oldSpeed = speed + + delegate.onEvent(conn, now, event) + + when (event) { + FlowEvent.Converge -> speed = conn.rate + FlowEvent.Capacity -> { + // Check if the consumer interrupted the consumer and updated the resource consumption. If not, we might + // need to update the current speed. + if (oldSpeed == speed) { + speed = min(conn.capacity, speed) + } + } + FlowEvent.Exit -> speed = 0.0 + else -> {} + } + } + + override fun onFailure(conn: FlowConnection, cause: Throwable) { + speed = 0.0 + + delegate.onFailure(conn, cause) + } + + override fun toString(): String = "FlowSourceRateAdapter[delegate=$delegate]" +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/TraceFlowSource.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/TraceFlowSource.kt new file mode 100644 index 00000000..4d3ae61a --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/TraceFlowSource.kt @@ -0,0 +1,72 @@ +/* + * 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.flow.source + +import org.opendc.simulator.flow.FlowConnection +import org.opendc.simulator.flow.FlowEvent +import org.opendc.simulator.flow.FlowSource + +/** + * A [FlowSource] that replays a sequence of [Fragment], each indicating the flow rate for some period of time. + */ +public class TraceFlowSource(private val trace: Sequence) : FlowSource { + private var _iterator: Iterator? = null + private var _nextTarget = Long.MIN_VALUE + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + // Check whether the trace fragment was fully consumed, otherwise wait until we have done so + val nextTarget = _nextTarget + if (nextTarget > now) { + return now - nextTarget + } + + val iterator = checkNotNull(_iterator) + return if (iterator.hasNext()) { + val fragment = iterator.next() + _nextTarget = now + fragment.duration + conn.push(fragment.usage) + fragment.duration + } else { + conn.close() + Long.MAX_VALUE + } + } + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + when (event) { + FlowEvent.Start -> { + check(_iterator == null) { "Source already running" } + _iterator = trace.iterator() + } + FlowEvent.Exit -> { + _iterator = null + } + else -> {} + } + } + + /** + * A fragment of the tgrace. + */ + public data class Fragment(val duration: Long, val usage: Double) +} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt new file mode 100644 index 00000000..061ebea6 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.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.simulator.flow + +import io.mockk.* +import kotlinx.coroutines.* +import org.junit.jupiter.api.* +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.internal.FlowConsumerContextImpl +import org.opendc.simulator.flow.internal.FlowEngineImpl +import org.opendc.simulator.flow.source.FixedFlowSource + +/** + * A test suite for the [FlowConsumerContextImpl] class. + */ +class FlowConsumerContextTest { + @Test + fun testFlushWithoutCommand() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val consumer = object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return if (now == 0L) { + conn.push(1.0) + 1000 + } else { + conn.close() + Long.MAX_VALUE + } + } + } + + val logic = object : FlowConsumerLogic {} + val context = FlowConsumerContextImpl(engine, consumer, logic) + + engine.scheduleSync(engine.clock.millis(), context) + } + + @Test + fun testIntermediateFlush() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val consumer = FixedFlowSource(1.0, 1.0) + + val logic = spyk(object : FlowConsumerLogic {}) + val context = FlowConsumerContextImpl(engine, consumer, logic) + context.capacity = 1.0 + + context.start() + delay(1) // Delay 1 ms to prevent hitting the fast path + engine.scheduleSync(engine.clock.millis(), context) + + verify(exactly = 2) { logic.onPush(any(), any(), any(), any()) } + } + + @Test + fun testDoubleStart() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val consumer = object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return if (now == 0L) { + conn.push(0.0) + 1000 + } else { + conn.close() + Long.MAX_VALUE + } + } + } + + val logic = object : FlowConsumerLogic {} + val context = FlowConsumerContextImpl(engine, consumer, logic) + + context.start() + + assertThrows { + context.start() + } + } + + @Test + fun testIdempotentCapacityChange() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val consumer = spyk(object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return if (now == 0L) { + conn.push(1.0) + 1000 + } else { + conn.close() + Long.MAX_VALUE + } + } + }) + + val logic = object : FlowConsumerLogic {} + val context = FlowConsumerContextImpl(engine, consumer, logic) + context.capacity = 4200.0 + context.start() + context.capacity = 4200.0 + + verify(exactly = 0) { consumer.onEvent(any(), any(), FlowEvent.Capacity) } + } + + @Test + fun testFailureNoInfiniteLoop() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val consumer = spyk(object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + conn.close() + return Long.MAX_VALUE + } + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + if (event == FlowEvent.Exit) throw IllegalStateException("onEvent") + } + + override fun onFailure(conn: FlowConnection, cause: Throwable) { + throw IllegalStateException("onFailure") + } + }) + + val logic = object : FlowConsumerLogic {} + + val context = FlowConsumerContextImpl(engine, consumer, logic) + + context.start() + + delay(1) + + verify(exactly = 1) { consumer.onFailure(any(), any()) } + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt new file mode 100644 index 00000000..cbc48a4e --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt @@ -0,0 +1,222 @@ +/* + * 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.flow + +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.internal.FlowEngineImpl +import org.opendc.simulator.flow.source.FixedFlowSource + +/** + * A test suite for the [FlowForwarder] class. + */ +internal class FlowForwarderTest { + @Test + fun testCancelImmediately() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine) + val source = FlowSink(engine, 2000.0) + + launch { source.consume(forwarder) } + + forwarder.consume(object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + conn.close() + return Long.MAX_VALUE + } + }) + + forwarder.close() + source.cancel() + } + + @Test + fun testCancel() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine) + val source = FlowSink(engine, 2000.0) + + launch { source.consume(forwarder) } + + forwarder.consume(object : FlowSource { + var isFirst = true + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return if (isFirst) { + isFirst = false + conn.push(1.0) + 10 * 1000 + } else { + conn.close() + Long.MAX_VALUE + } + } + }) + + forwarder.close() + source.cancel() + } + + @Test + fun testState() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine) + val consumer = object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + conn.close() + return Long.MAX_VALUE + } + } + + assertFalse(forwarder.isActive) + + forwarder.startConsumer(consumer) + assertTrue(forwarder.isActive) + + assertThrows { forwarder.startConsumer(consumer) } + + forwarder.cancel() + assertFalse(forwarder.isActive) + + forwarder.close() + assertFalse(forwarder.isActive) + } + + @Test + fun testCancelPendingDelegate() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine) + + val consumer = spyk(object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + conn.close() + return Long.MAX_VALUE + } + }) + + forwarder.startConsumer(consumer) + forwarder.cancel() + + verify(exactly = 0) { consumer.onEvent(any(), any(), FlowEvent.Exit) } + } + + @Test + fun testCancelStartedDelegate() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine) + val source = FlowSink(engine, 2000.0) + + val consumer = spyk(FixedFlowSource(2000.0, 1.0)) + + source.startConsumer(forwarder) + yield() + forwarder.startConsumer(consumer) + yield() + forwarder.cancel() + + verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Start) } + verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Exit) } + } + + @Test + fun testCancelPropagation() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine) + val source = FlowSink(engine, 2000.0) + + val consumer = spyk(FixedFlowSource(2000.0, 1.0)) + + source.startConsumer(forwarder) + yield() + forwarder.startConsumer(consumer) + yield() + source.cancel() + + verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Start) } + verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Exit) } + } + + @Test + fun testExitPropagation() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine, isCoupled = true) + val source = FlowSink(engine, 2000.0) + + val consumer = object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + conn.close() + return Long.MAX_VALUE + } + } + + source.startConsumer(forwarder) + forwarder.consume(consumer) + yield() + + assertFalse(forwarder.isActive) + } + + @Test + fun testAdjustCapacity() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine) + val source = FlowSink(engine, 1.0) + + val consumer = spyk(FixedFlowSource(2.0, 1.0)) + source.startConsumer(forwarder) + + coroutineScope { + launch { forwarder.consume(consumer) } + delay(1000) + source.capacity = 0.5 + } + + assertEquals(3000, clock.millis()) + verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Capacity) } + } + + @Test + fun testCounters() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine) + val source = FlowSink(engine, 1.0) + + val consumer = FixedFlowSource(2.0, 1.0) + source.startConsumer(forwarder) + + forwarder.consume(consumer) + + yield() + + assertEquals(2.0, source.counters.actual) + assertEquals(source.counters.actual, forwarder.counters.actual) { "Actual work" } + assertEquals(source.counters.demand, forwarder.counters.demand) { "Work demand" } + assertEquals(source.counters.overcommit, forwarder.counters.overcommit) { "Overcommitted work" } + assertEquals(2000, clock.millis()) + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt new file mode 100644 index 00000000..010a985e --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt @@ -0,0 +1,240 @@ +/* + * 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.flow + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.* +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.internal.FlowEngineImpl +import org.opendc.simulator.flow.source.FixedFlowSource +import org.opendc.simulator.flow.source.FlowSourceRateAdapter + +/** + * A test suite for the [FlowSink] class. + */ +internal class FlowSinkTest { + @Test + fun testSpeed() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val capacity = 4200.0 + val provider = FlowSink(engine, capacity) + + val consumer = FixedFlowSource(4200.0, 1.0) + + val res = mutableListOf() + val adapter = FlowSourceRateAdapter(consumer, res::add) + + provider.consume(adapter) + + assertEquals(listOf(0.0, capacity, 0.0), res) { "Speed is reported correctly" } + } + + @Test + fun testAdjustCapacity() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val provider = FlowSink(engine, 1.0) + + val consumer = spyk(FixedFlowSource(2.0, 1.0)) + + coroutineScope { + launch { provider.consume(consumer) } + delay(1000) + provider.capacity = 0.5 + } + assertEquals(3000, clock.millis()) + verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Capacity) } + } + + @Test + fun testSpeedLimit() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val capacity = 4200.0 + val provider = FlowSink(engine, capacity) + + val consumer = FixedFlowSource(capacity, 2.0) + + val res = mutableListOf() + val adapter = FlowSourceRateAdapter(consumer, res::add) + + provider.consume(adapter) + + assertEquals(listOf(0.0, capacity, 0.0), res) { "Speed is reported correctly" } + } + + /** + * Test to see whether no infinite recursion occurs when interrupting during [FlowSource.onStart] or + * [FlowSource.onPull]. + */ + @Test + fun testIntermediateInterrupt() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val capacity = 4200.0 + val provider = FlowSink(engine, capacity) + + val consumer = object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + conn.close() + return Long.MAX_VALUE + } + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + conn.pull() + } + } + + provider.consume(consumer) + } + + @Test + fun testInterrupt() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val capacity = 4200.0 + val provider = FlowSink(engine, capacity) + lateinit var resCtx: FlowConnection + + val consumer = object : FlowSource { + var isFirst = true + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + when (event) { + FlowEvent.Start -> resCtx = conn + else -> {} + } + } + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return if (isFirst) { + isFirst = false + conn.push(1.0) + 4000 + } else { + conn.close() + Long.MAX_VALUE + } + } + } + + launch { + yield() + resCtx.pull() + } + provider.consume(consumer) + + assertEquals(0, clock.millis()) + } + + @Test + fun testFailure() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val capacity = 4200.0 + val provider = FlowSink(engine, capacity) + + val consumer = mockk(relaxUnitFun = true) + every { consumer.onEvent(any(), any(), eq(FlowEvent.Start)) } + .throws(IllegalStateException()) + + assertThrows { + provider.consume(consumer) + } + } + + @Test + fun testExceptionPropagationOnNext() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val capacity = 4200.0 + val provider = FlowSink(engine, capacity) + + val consumer = object : FlowSource { + var isFirst = true + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return if (isFirst) { + isFirst = false + conn.push(1.0) + 1000 + } else { + throw IllegalStateException() + } + } + } + + assertThrows { + provider.consume(consumer) + } + } + + @Test + fun testConcurrentConsumption() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val capacity = 4200.0 + val provider = FlowSink(engine, capacity) + + val consumer = FixedFlowSource(capacity, 1.0) + + assertThrows { + coroutineScope { + launch { provider.consume(consumer) } + provider.consume(consumer) + } + } + } + + @Test + fun testCancelDuringConsumption() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val capacity = 4200.0 + val provider = FlowSink(engine, capacity) + + val consumer = FixedFlowSource(capacity, 1.0) + + launch { provider.consume(consumer) } + delay(500) + provider.cancel() + + yield() + + assertEquals(500, clock.millis()) + } + + @Test + fun testInfiniteSleep() { + assertThrows { + runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val capacity = 4200.0 + val provider = FlowSink(engine, capacity) + + val consumer = object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long = Long.MAX_VALUE + } + + provider.consume(consumer) + } + } + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchExclusiveTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchExclusiveTest.kt new file mode 100644 index 00000000..b503087e --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchExclusiveTest.kt @@ -0,0 +1,157 @@ +/* + * 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.flow.mux + +import kotlinx.coroutines.yield +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.internal.FlowEngineImpl +import org.opendc.simulator.flow.source.FixedFlowSource +import org.opendc.simulator.flow.source.FlowSourceRateAdapter +import org.opendc.simulator.flow.source.TraceFlowSource + +/** + * Test suite for the [ForwardingFlowMultiplexer] class. + */ +internal class SimResourceSwitchExclusiveTest { + /** + * Test a trace workload. + */ + @Test + fun testTrace() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val speed = mutableListOf() + + val duration = 5 * 60L + val workload = + TraceFlowSource( + sequenceOf( + TraceFlowSource.Fragment(duration * 1000, 28.0), + TraceFlowSource.Fragment(duration * 1000, 3500.0), + TraceFlowSource.Fragment(duration * 1000, 0.0), + TraceFlowSource.Fragment(duration * 1000, 183.0) + ), + ) + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + val forwarder = FlowForwarder(engine) + val adapter = FlowSourceRateAdapter(forwarder, speed::add) + source.startConsumer(adapter) + switch.addOutput(forwarder) + + val provider = switch.newInput() + provider.consume(workload) + yield() + + assertAll( + { assertEquals(listOf(0.0, 28.0, 3200.0, 0.0, 183.0, 0.0), speed) { "Correct speed" } }, + { assertEquals(5 * 60L * 4000, clock.millis()) { "Took enough time" } } + ) + } + + /** + * Test runtime workload on hypervisor. + */ + @Test + fun testRuntimeWorkload() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val duration = 5 * 60L * 1000 + val workload = FixedFlowSource(duration * 3.2, 1.0) + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + + switch.addOutput(source) + + val provider = switch.newInput() + provider.consume(workload) + yield() + + assertEquals(duration, clock.millis()) { "Took enough time" } + } + + /** + * Test two workloads running sequentially. + */ + @Test + fun testTwoWorkloads() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val duration = 5 * 60L * 1000 + val workload = object : FlowSource { + var isFirst = true + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + when (event) { + FlowEvent.Start -> isFirst = true + else -> {} + } + } + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return if (isFirst) { + isFirst = false + conn.push(1.0) + duration + } else { + conn.close() + Long.MAX_VALUE + } + } + } + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + + switch.addOutput(source) + + val provider = switch.newInput() + provider.consume(workload) + yield() + provider.consume(workload) + assertEquals(duration * 2, clock.millis()) { "Took enough time" } + } + + /** + * Test concurrent workloads on the machine. + */ + @Test + fun testConcurrentWorkloadFails() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + + switch.addOutput(source) + + switch.newInput() + assertThrows { switch.newInput() } + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchMaxMinTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchMaxMinTest.kt new file mode 100644 index 00000000..089a8d78 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchMaxMinTest.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.simulator.flow.mux + +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.FlowSink +import org.opendc.simulator.flow.consume +import org.opendc.simulator.flow.internal.FlowEngineImpl +import org.opendc.simulator.flow.source.FixedFlowSource +import org.opendc.simulator.flow.source.TraceFlowSource + +/** + * Test suite for the [FlowMultiplexer] implementations + */ +internal class SimResourceSwitchMaxMinTest { + @Test + fun testSmoke() = runBlockingSimulation { + val scheduler = FlowEngineImpl(coroutineContext, clock) + val switch = MaxMinFlowMultiplexer(scheduler) + + val sources = List(2) { FlowSink(scheduler, 2000.0) } + sources.forEach { switch.addOutput(it) } + + val provider = switch.newInput() + val consumer = FixedFlowSource(2000.0, 1.0) + + try { + provider.consume(consumer) + yield() + } finally { + switch.clear() + } + } + + /** + * Test overcommitting of resources via the hypervisor with a single VM. + */ + @Test + fun testOvercommittedSingle() = runBlockingSimulation { + val scheduler = FlowEngineImpl(coroutineContext, clock) + + val duration = 5 * 60L + val workload = + TraceFlowSource( + sequenceOf( + TraceFlowSource.Fragment(duration * 1000, 28.0), + TraceFlowSource.Fragment(duration * 1000, 3500.0), + TraceFlowSource.Fragment(duration * 1000, 0.0), + TraceFlowSource.Fragment(duration * 1000, 183.0) + ), + ) + + val switch = MaxMinFlowMultiplexer(scheduler) + val provider = switch.newInput() + + try { + switch.addOutput(FlowSink(scheduler, 3200.0)) + provider.consume(workload) + yield() + } finally { + switch.clear() + } + + assertAll( + { assertEquals(1113300.0, switch.counters.demand, "Requested work does not match") }, + { assertEquals(1023300.0, switch.counters.actual, "Actual work does not match") }, + { assertEquals(90000.0, switch.counters.overcommit, "Overcommitted work does not match") }, + { assertEquals(1200000, clock.millis()) } + ) + } + + /** + * Test overcommitting of resources via the hypervisor with two VMs. + */ + @Test + fun testOvercommittedDual() = runBlockingSimulation { + val scheduler = FlowEngineImpl(coroutineContext, clock) + + val duration = 5 * 60L + val workloadA = + TraceFlowSource( + sequenceOf( + TraceFlowSource.Fragment(duration * 1000, 28.0), + TraceFlowSource.Fragment(duration * 1000, 3500.0), + TraceFlowSource.Fragment(duration * 1000, 0.0), + TraceFlowSource.Fragment(duration * 1000, 183.0) + ), + ) + val workloadB = + TraceFlowSource( + sequenceOf( + TraceFlowSource.Fragment(duration * 1000, 28.0), + TraceFlowSource.Fragment(duration * 1000, 3100.0), + TraceFlowSource.Fragment(duration * 1000, 0.0), + TraceFlowSource.Fragment(duration * 1000, 73.0) + ) + ) + + val switch = MaxMinFlowMultiplexer(scheduler) + val providerA = switch.newInput() + val providerB = switch.newInput() + + try { + switch.addOutput(FlowSink(scheduler, 3200.0)) + + coroutineScope { + launch { providerA.consume(workloadA) } + providerB.consume(workloadB) + } + + yield() + } finally { + switch.clear() + } + assertAll( + { assertEquals(2073600.0, switch.counters.demand, "Requested work does not match") }, + { assertEquals(1053600.0, switch.counters.actual, "Granted work does not match") }, + { assertEquals(1020000.0, switch.counters.overcommit, "Overcommitted work does not match") }, + { assertEquals(1200000, clock.millis()) } + ) + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/source/FixedFlowSourceTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/source/FixedFlowSourceTest.kt new file mode 100644 index 00000000..8396d346 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/source/FixedFlowSourceTest.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.simulator.flow.source + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.FlowSink +import org.opendc.simulator.flow.consume +import org.opendc.simulator.flow.internal.FlowEngineImpl + +/** + * A test suite for the [FixedFlowSource] class. + */ +internal class FixedFlowSourceTest { + @Test + fun testSmoke() = runBlockingSimulation { + val scheduler = FlowEngineImpl(coroutineContext, clock) + val provider = FlowSink(scheduler, 1.0) + + val consumer = FixedFlowSource(1.0, 1.0) + + provider.consume(consumer) + assertEquals(1000, clock.millis()) + } + + @Test + fun testUtilization() = runBlockingSimulation { + val scheduler = FlowEngineImpl(coroutineContext, clock) + val provider = FlowSink(scheduler, 1.0) + + val consumer = FixedFlowSource(1.0, 0.5) + + provider.consume(consumer) + assertEquals(2000, clock.millis()) + } +} diff --git a/opendc-simulator/opendc-simulator-network/build.gradle.kts b/opendc-simulator/opendc-simulator-network/build.gradle.kts index eb9adcd1..a8f94602 100644 --- a/opendc-simulator/opendc-simulator-network/build.gradle.kts +++ b/opendc-simulator/opendc-simulator-network/build.gradle.kts @@ -30,6 +30,6 @@ plugins { dependencies { api(platform(projects.opendcPlatform)) - api(projects.opendcSimulator.opendcSimulatorResources) + api(projects.opendcSimulator.opendcSimulatorFlow) implementation(projects.opendcSimulator.opendcSimulatorCore) } diff --git a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkPort.kt b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkPort.kt index 102e5625..4b66d5cf 100644 --- a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkPort.kt +++ b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkPort.kt @@ -22,8 +22,8 @@ package org.opendc.simulator.network -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceProvider +import org.opendc.simulator.flow.FlowConsumer +import org.opendc.simulator.flow.FlowSource /** * A network port allows network devices to be connected to network through links. @@ -78,14 +78,14 @@ public abstract class SimNetworkPort { } /** - * Create a [SimResourceConsumer] which generates the outgoing traffic of this port. + * Create a [FlowSource] which generates the outgoing traffic of this port. */ - protected abstract fun createConsumer(): SimResourceConsumer + protected abstract fun createConsumer(): FlowSource /** - * The [SimResourceProvider] which processes the ingoing traffic of this port. + * The [FlowConsumer] which processes the ingoing traffic of this port. */ - protected abstract val provider: SimResourceProvider + protected abstract val provider: FlowConsumer override fun toString(): String = "SimNetworkPort[isConnected=$isConnected]" } diff --git a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt index 7db0f176..4b0d7bbd 100644 --- a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt +++ b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSink.kt @@ -22,22 +22,22 @@ package org.opendc.simulator.network -import org.opendc.simulator.resources.* +import org.opendc.simulator.flow.* /** * A network sink which discards all received traffic and does not generate any traffic itself. */ public class SimNetworkSink( - interpreter: SimResourceInterpreter, + engine: FlowEngine, public val capacity: Double ) : SimNetworkPort() { - override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long = Long.MAX_VALUE + override fun createConsumer(): FlowSource = object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long = Long.MAX_VALUE override fun toString(): String = "SimNetworkSink.Consumer" } - override val provider: SimResourceProvider = SimResourceSource(capacity, interpreter) + override val provider: FlowConsumer = FlowSink(engine, capacity) override fun toString(): String = "SimNetworkSink[capacity=$capacity]" } diff --git a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt index 2267715e..2b7c1ad7 100644 --- a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt +++ b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt @@ -22,12 +22,13 @@ package org.opendc.simulator.network -import org.opendc.simulator.resources.* +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.mux.MaxMinFlowMultiplexer /** * A [SimNetworkSwitch] that can support new networking ports on demand. */ -public class SimNetworkSwitchVirtual(interpreter: SimResourceInterpreter) : SimNetworkSwitch { +public class SimNetworkSwitchVirtual(private val engine: FlowEngine) : SimNetworkSwitch { /** * The ports of this switch. */ @@ -36,9 +37,9 @@ public class SimNetworkSwitchVirtual(interpreter: SimResourceInterpreter) : SimN private val _ports = mutableListOf() /** - * The [SimResourceSwitchMaxMin] to actually perform the switching. + * The [MaxMinFlowMultiplexer] to actually perform the switching. */ - private val switch = SimResourceSwitchMaxMin(interpreter) + private val mux = MaxMinFlowMultiplexer(engine) /** * Open a new port on the switch. @@ -58,19 +59,19 @@ public class SimNetworkSwitchVirtual(interpreter: SimResourceInterpreter) : SimN */ private var isClosed: Boolean = false - override val provider: SimResourceProvider + override val provider: FlowConsumer get() = _provider - private val _provider = switch.newOutput() + private val _provider = mux.newInput() - override fun createConsumer(): SimResourceConsumer { - val forwarder = SimResourceForwarder(isCoupled = true) - switch.addInput(forwarder) + override fun createConsumer(): FlowSource { + val forwarder = FlowForwarder(engine, isCoupled = true) + mux.addOutput(forwarder) return forwarder } override fun close() { isClosed = true - switch.removeOutput(_provider) + mux.removeInput(_provider) _ports.remove(this) } } diff --git a/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSinkTest.kt b/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSinkTest.kt index b8c4b00d..45d0bcf0 100644 --- a/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSinkTest.kt +++ b/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSinkTest.kt @@ -31,8 +31,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.* -import org.opendc.simulator.resources.consumer.SimWorkConsumer +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.source.FixedFlowSource /** * Test suite for the [SimNetworkSink] class. @@ -40,8 +40,8 @@ import org.opendc.simulator.resources.consumer.SimWorkConsumer class SimNetworkSinkTest { @Test fun testInitialState() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val sink = SimNetworkSink(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val sink = SimNetworkSink(engine, capacity = 100.0) assertFalse(sink.isConnected) assertNull(sink.link) @@ -50,8 +50,8 @@ class SimNetworkSinkTest { @Test fun testDisconnectIdempotent() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val sink = SimNetworkSink(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val sink = SimNetworkSink(engine, capacity = 100.0) assertDoesNotThrow { sink.disconnect() } assertFalse(sink.isConnected) @@ -59,8 +59,8 @@ class SimNetworkSinkTest { @Test fun testConnectCircular() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val sink = SimNetworkSink(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val sink = SimNetworkSink(engine, capacity = 100.0) assertThrows { sink.connect(sink) @@ -69,8 +69,8 @@ class SimNetworkSinkTest { @Test fun testConnectAlreadyConnectedTarget() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val sink = SimNetworkSink(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val sink = SimNetworkSink(engine, capacity = 100.0) val source = mockk(relaxUnitFun = true) every { source.isConnected } returns true @@ -81,9 +81,9 @@ class SimNetworkSinkTest { @Test fun testConnectAlreadyConnected() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val sink = SimNetworkSink(interpreter, capacity = 100.0) - val source1 = Source(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val sink = SimNetworkSink(engine, capacity = 100.0) + val source1 = Source(engine) val source2 = mockk(relaxUnitFun = true) @@ -97,9 +97,9 @@ class SimNetworkSinkTest { @Test fun testConnect() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val sink = SimNetworkSink(interpreter, capacity = 100.0) - val source = spyk(Source(interpreter)) + val engine = FlowEngine(coroutineContext, clock) + val sink = SimNetworkSink(engine, capacity = 100.0) + val source = spyk(Source(engine)) val consumer = source.consumer sink.connect(source) @@ -108,14 +108,14 @@ class SimNetworkSinkTest { assertTrue(source.isConnected) verify { source.createConsumer() } - verify { consumer.onEvent(any(), SimResourceEvent.Start) } + verify { consumer.onEvent(any(), any(), FlowEvent.Start) } } @Test fun testDisconnect() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val sink = SimNetworkSink(interpreter, capacity = 100.0) - val source = spyk(Source(interpreter)) + val engine = FlowEngine(coroutineContext, clock) + val sink = SimNetworkSink(engine, capacity = 100.0) + val source = spyk(Source(engine)) val consumer = source.consumer sink.connect(source) @@ -124,14 +124,14 @@ class SimNetworkSinkTest { assertFalse(sink.isConnected) assertFalse(source.isConnected) - verify { consumer.onEvent(any(), SimResourceEvent.Exit) } + verify { consumer.onEvent(any(), any(), FlowEvent.Exit) } } - private class Source(interpreter: SimResourceInterpreter) : SimNetworkPort() { - val consumer = spyk(SimWorkConsumer(Double.POSITIVE_INFINITY, utilization = 0.8)) + private class Source(engine: FlowEngine) : SimNetworkPort() { + val consumer = spyk(FixedFlowSource(Double.POSITIVE_INFINITY, utilization = 0.8)) - public override fun createConsumer(): SimResourceConsumer = consumer + public override fun createConsumer(): FlowSource = consumer - override val provider: SimResourceProvider = SimResourceSource(0.0, interpreter) + override val provider: FlowConsumer = FlowSink(engine, 0.0) } } diff --git a/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtualTest.kt b/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtualTest.kt index 3a749bfe..4aa2fa92 100644 --- a/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtualTest.kt +++ b/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtualTest.kt @@ -28,8 +28,8 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.* -import org.opendc.simulator.resources.consumer.SimWorkConsumer +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.source.FixedFlowSource /** * Test suite for the [SimNetworkSwitchVirtual] class. @@ -37,10 +37,10 @@ import org.opendc.simulator.resources.consumer.SimWorkConsumer class SimNetworkSwitchVirtualTest { @Test fun testConnect() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val sink = SimNetworkSink(interpreter, capacity = 100.0) - val source = spyk(Source(interpreter)) - val switch = SimNetworkSwitchVirtual(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val sink = SimNetworkSink(engine, capacity = 100.0) + val source = spyk(Source(engine)) + val switch = SimNetworkSwitchVirtual(engine) val consumer = source.consumer switch.newPort().connect(sink) @@ -50,14 +50,14 @@ class SimNetworkSwitchVirtualTest { assertTrue(source.isConnected) verify { source.createConsumer() } - verify { consumer.onEvent(any(), SimResourceEvent.Start) } + verify { consumer.onEvent(any(), any(), FlowEvent.Start) } } @Test fun testConnectClosedPort() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val sink = SimNetworkSink(interpreter, capacity = 100.0) - val switch = SimNetworkSwitchVirtual(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val sink = SimNetworkSink(engine, capacity = 100.0) + val switch = SimNetworkSwitchVirtual(engine) val port = switch.newPort() port.close() @@ -67,11 +67,11 @@ class SimNetworkSwitchVirtualTest { } } - private class Source(interpreter: SimResourceInterpreter) : SimNetworkPort() { - val consumer = spyk(SimWorkConsumer(Double.POSITIVE_INFINITY, utilization = 0.8)) + private class Source(engine: FlowEngine) : SimNetworkPort() { + val consumer = spyk(FixedFlowSource(Double.POSITIVE_INFINITY, utilization = 0.8)) - public override fun createConsumer(): SimResourceConsumer = consumer + public override fun createConsumer(): FlowSource = consumer - override val provider: SimResourceProvider = SimResourceSource(0.0, interpreter) + override val provider: FlowConsumer = FlowSink(engine, 0.0) } } diff --git a/opendc-simulator/opendc-simulator-power/build.gradle.kts b/opendc-simulator/opendc-simulator-power/build.gradle.kts index f2a49964..e4342a6a 100644 --- a/opendc-simulator/opendc-simulator-power/build.gradle.kts +++ b/opendc-simulator/opendc-simulator-power/build.gradle.kts @@ -30,6 +30,6 @@ plugins { dependencies { api(platform(projects.opendcPlatform)) - api(projects.opendcSimulator.opendcSimulatorResources) + api(projects.opendcSimulator.opendcSimulatorFlow) implementation(projects.opendcSimulator.opendcSimulatorCore) } diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt index 1a12a52a..c33f5186 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt @@ -22,46 +22,48 @@ package org.opendc.simulator.power -import org.opendc.simulator.resources.* +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.mux.FlowMultiplexer +import org.opendc.simulator.flow.mux.MaxMinFlowMultiplexer /** * A model of a Power Distribution Unit (PDU). * - * @param interpreter The underlying [SimResourceInterpreter] to drive the simulation under the hood. + * @param engine The underlying [FlowEngine] to drive the simulation under the hood. * @param idlePower The idle power consumption of the PDU independent of the load on the PDU. * @param lossCoefficient The coefficient for the power loss of the PDU proportional to the square load. */ public class SimPdu( - interpreter: SimResourceInterpreter, + engine: FlowEngine, private val idlePower: Double = 0.0, private val lossCoefficient: Double = 0.0, ) : SimPowerInlet() { /** - * The [SimResourceSwitch] that distributes the electricity over the PDU outlets. + * The [FlowMultiplexer] that distributes the electricity over the PDU outlets. */ - private val switch = SimResourceSwitchMaxMin(interpreter) + private val mux = MaxMinFlowMultiplexer(engine) /** - * The [SimResourceForwarder] that represents the input of the PDU. + * The [FlowForwarder] that represents the input of the PDU. */ - private val forwarder = SimResourceForwarder() + private val forwarder = FlowForwarder(engine) /** * Create a new PDU outlet. */ - public fun newOutlet(): Outlet = Outlet(switch, switch.newOutput()) + public fun newOutlet(): Outlet = Outlet(mux, mux.newInput()) init { - switch.addInput(forwarder) + mux.addOutput(forwarder) } - override fun createConsumer(): SimResourceConsumer = object : SimResourceConsumer by forwarder { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - val duration = forwarder.onNext(ctx, now, delta) - val loss = computePowerLoss(ctx.demand) - val newLimit = ctx.demand + loss + override fun createConsumer(): FlowSource = object : FlowSource by forwarder { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + val duration = forwarder.onPull(conn, now, delta) + val loss = computePowerLoss(conn.demand) + val newLimit = conn.demand + loss - ctx.push(newLimit) + conn.push(newLimit) return duration } @@ -81,7 +83,7 @@ public class SimPdu( /** * A PDU outlet. */ - public class Outlet(private val switch: SimResourceSwitch, private val provider: SimResourceProvider) : SimPowerOutlet(), AutoCloseable { + public class Outlet(private val switch: FlowMultiplexer, private val provider: FlowConsumer) : SimPowerOutlet(), AutoCloseable { override fun onConnect(inlet: SimPowerInlet) { provider.startConsumer(inlet.createConsumer()) } @@ -94,7 +96,7 @@ public class SimPdu( * Remove the outlet from the PDU. */ override fun close() { - switch.removeOutput(provider) + switch.removeInput(provider) } override fun toString(): String = "SimPdu.Outlet" diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt index 0ac1f199..851b28a5 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt @@ -22,7 +22,7 @@ package org.opendc.simulator.power -import org.opendc.simulator.resources.SimResourceConsumer +import org.opendc.simulator.flow.FlowSource /** * An abstract inlet that consumes electricity from a power outlet. @@ -42,7 +42,7 @@ public abstract class SimPowerInlet { internal var _outlet: SimPowerOutlet? = null /** - * Create a [SimResourceConsumer] which represents the consumption of electricity from the power outlet. + * Create a [FlowSource] which represents the consumption of electricity from the power outlet. */ - public abstract fun createConsumer(): SimResourceConsumer + public abstract fun createConsumer(): FlowSource } diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt index 3ef8ccc6..7faebd75 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt @@ -22,25 +22,25 @@ package org.opendc.simulator.power -import org.opendc.simulator.resources.SimResourceInterpreter -import org.opendc.simulator.resources.SimResourceSource +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowSink /** * A [SimPowerOutlet] that represents a source of electricity. * - * @param interpreter The underlying [SimResourceInterpreter] to drive the simulation under the hood. + * @param engine The underlying [FlowEngine] to drive the simulation under the hood. */ -public class SimPowerSource(interpreter: SimResourceInterpreter, public val capacity: Double) : SimPowerOutlet() { +public class SimPowerSource(engine: FlowEngine, public val capacity: Double) : SimPowerOutlet() { /** * The resource source that drives this power source. */ - private val source = SimResourceSource(capacity, interpreter) + private val source = FlowSink(engine, capacity) /** * The power draw at this instant. */ public val powerDraw: Double - get() = source.speed + get() = source.rate override fun onConnect(inlet: SimPowerInlet) { source.startConsumer(inlet.createConsumer()) diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt index 9c7400ed..5eaa91af 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt @@ -22,50 +22,51 @@ package org.opendc.simulator.power -import org.opendc.simulator.resources.* +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.mux.MaxMinFlowMultiplexer /** * A model of an Uninterruptible Power Supply (UPS). * * This model aggregates multiple power sources into a single source in order to ensure that power is always available. * - * @param interpreter The underlying [SimResourceInterpreter] to drive the simulation under the hood. + * @param engine The underlying [FlowEngine] to drive the simulation under the hood. * @param idlePower The idle power consumption of the UPS independent of the load. * @param lossCoefficient The coefficient for the power loss of the UPS proportional to the load. */ public class SimUps( - interpreter: SimResourceInterpreter, + private val engine: FlowEngine, private val idlePower: Double = 0.0, private val lossCoefficient: Double = 0.0, ) : SimPowerOutlet() { /** * The resource aggregator used to combine the input sources. */ - private val switch = SimResourceSwitchMaxMin(interpreter) + private val switch = MaxMinFlowMultiplexer(engine) /** - * The [SimResourceProvider] that represents the output of the UPS. + * The [FlowConsumer] that represents the output of the UPS. */ - private val provider = switch.newOutput() + private val provider = switch.newInput() /** * Create a new UPS outlet. */ public fun newInlet(): SimPowerInlet { - val forward = SimResourceForwarder(isCoupled = true) - switch.addInput(forward) + val forward = FlowForwarder(engine, isCoupled = true) + switch.addOutput(forward) return Inlet(forward) } override fun onConnect(inlet: SimPowerInlet) { val consumer = inlet.createConsumer() - provider.startConsumer(object : SimResourceConsumer by consumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - val duration = consumer.onNext(ctx, now, delta) - val loss = computePowerLoss(ctx.demand) - val newLimit = ctx.demand + loss + provider.startConsumer(object : FlowSource by consumer { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + val duration = consumer.onPull(conn, now, delta) + val loss = computePowerLoss(conn.demand) + val newLimit = conn.demand + loss - ctx.push(newLimit) + conn.push(newLimit) return duration } }) @@ -86,8 +87,8 @@ public class SimUps( /** * A UPS inlet. */ - public inner class Inlet(private val forwarder: SimResourceForwarder) : SimPowerInlet(), AutoCloseable { - override fun createConsumer(): SimResourceConsumer = forwarder + public inner class Inlet(private val forwarder: FlowForwarder) : SimPowerInlet(), AutoCloseable { + override fun createConsumer(): FlowSource = forwarder /** * Remove the inlet from the PSU. diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt index 17a174b7..568a1e8c 100644 --- a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt @@ -28,10 +28,10 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceEvent -import org.opendc.simulator.resources.SimResourceInterpreter -import org.opendc.simulator.resources.consumer.SimWorkConsumer +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowEvent +import org.opendc.simulator.flow.FlowSource +import org.opendc.simulator.flow.source.FixedFlowSource /** * Test suite for the [SimPdu] class. @@ -39,9 +39,9 @@ import org.opendc.simulator.resources.consumer.SimWorkConsumer internal class SimPduTest { @Test fun testZeroOutlets() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) - val pdu = SimPdu(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val pdu = SimPdu(engine) source.connect(pdu) assertEquals(0.0, source.powerDraw) @@ -49,9 +49,9 @@ internal class SimPduTest { @Test fun testSingleOutlet() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) - val pdu = SimPdu(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val pdu = SimPdu(engine) source.connect(pdu) pdu.newOutlet().connect(SimpleInlet()) @@ -60,9 +60,9 @@ internal class SimPduTest { @Test fun testDoubleOutlet() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) - val pdu = SimPdu(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val pdu = SimPdu(engine) source.connect(pdu) pdu.newOutlet().connect(SimpleInlet()) @@ -73,28 +73,28 @@ internal class SimPduTest { @Test fun testDisconnect() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) - val pdu = SimPdu(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val pdu = SimPdu(engine) source.connect(pdu) - val consumer = spyk(SimWorkConsumer(100.0, utilization = 1.0)) + val consumer = spyk(FixedFlowSource(100.0, utilization = 1.0)) val inlet = object : SimPowerInlet() { - override fun createConsumer(): SimResourceConsumer = consumer + override fun createConsumer(): FlowSource = consumer } val outlet = pdu.newOutlet() outlet.connect(inlet) outlet.disconnect() - verify { consumer.onEvent(any(), SimResourceEvent.Exit) } + verify { consumer.onEvent(any(), any(), FlowEvent.Exit) } } @Test fun testLoss() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) // https://download.schneider-electric.com/files?p_Doc_Ref=SPD_NRAN-66CK3D_EN - val pdu = SimPdu(interpreter, idlePower = 1.5, lossCoefficient = 0.015) + val pdu = SimPdu(engine, idlePower = 1.5, lossCoefficient = 0.015) source.connect(pdu) pdu.newOutlet().connect(SimpleInlet()) assertEquals(89.0, source.powerDraw, 0.01) @@ -102,9 +102,9 @@ internal class SimPduTest { @Test fun testOutletClose() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) - val pdu = SimPdu(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val pdu = SimPdu(engine) source.connect(pdu) val outlet = pdu.newOutlet() outlet.close() @@ -115,6 +115,6 @@ internal class SimPduTest { } class SimpleInlet : SimPowerInlet() { - override fun createConsumer(): SimResourceConsumer = SimWorkConsumer(100.0, utilization = 0.5) + override fun createConsumer(): FlowSource = FixedFlowSource(100.0, utilization = 0.5) } } diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt index f3829ba1..b411e292 100644 --- a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt @@ -31,10 +31,10 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceEvent -import org.opendc.simulator.resources.SimResourceInterpreter -import org.opendc.simulator.resources.consumer.SimWorkConsumer +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowEvent +import org.opendc.simulator.flow.FlowSource +import org.opendc.simulator.flow.source.FixedFlowSource /** * Test suite for the [SimPowerSource] @@ -42,8 +42,8 @@ import org.opendc.simulator.resources.consumer.SimWorkConsumer internal class SimPowerSourceTest { @Test fun testInitialState() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) assertFalse(source.isConnected) assertNull(source.inlet) @@ -52,8 +52,8 @@ internal class SimPowerSourceTest { @Test fun testDisconnectIdempotent() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) assertDoesNotThrow { source.disconnect() } assertFalse(source.isConnected) @@ -61,8 +61,8 @@ internal class SimPowerSourceTest { @Test fun testConnect() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) val inlet = SimpleInlet() source.connect(inlet) @@ -76,27 +76,27 @@ internal class SimPowerSourceTest { @Test fun testDisconnect() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) - val consumer = spyk(SimWorkConsumer(100.0, utilization = 1.0)) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val consumer = spyk(FixedFlowSource(100.0, utilization = 1.0)) val inlet = object : SimPowerInlet() { - override fun createConsumer(): SimResourceConsumer = consumer + override fun createConsumer(): FlowSource = consumer } source.connect(inlet) source.disconnect() - verify { consumer.onEvent(any(), SimResourceEvent.Exit) } + verify { consumer.onEvent(any(), any(), FlowEvent.Exit) } } @Test fun testDisconnectAssertion() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) val inlet = mockk(relaxUnitFun = true) every { inlet.isConnected } returns false every { inlet._outlet } returns null - every { inlet.createConsumer() } returns SimWorkConsumer(100.0, utilization = 1.0) + every { inlet.createConsumer() } returns FixedFlowSource(100.0, utilization = 1.0) source.connect(inlet) @@ -107,8 +107,8 @@ internal class SimPowerSourceTest { @Test fun testOutletAlreadyConnected() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) val inlet = SimpleInlet() source.connect(inlet) @@ -121,8 +121,8 @@ internal class SimPowerSourceTest { @Test fun testInletAlreadyConnected() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) val inlet = mockk(relaxUnitFun = true) every { inlet.isConnected } returns true @@ -132,6 +132,6 @@ internal class SimPowerSourceTest { } class SimpleInlet : SimPowerInlet() { - override fun createConsumer(): SimResourceConsumer = SimWorkConsumer(100.0, utilization = 1.0) + override fun createConsumer(): FlowSource = FixedFlowSource(100.0, utilization = 1.0) } } diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt index 8d5fa857..31ac0b39 100644 --- a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt @@ -28,10 +28,10 @@ import org.junit.jupiter.api.Assertions.assertAll import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceEvent -import org.opendc.simulator.resources.SimResourceInterpreter -import org.opendc.simulator.resources.consumer.SimWorkConsumer +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowEvent +import org.opendc.simulator.flow.FlowSource +import org.opendc.simulator.flow.source.FixedFlowSource /** * Test suite for the [SimUps] class. @@ -39,9 +39,9 @@ import org.opendc.simulator.resources.consumer.SimWorkConsumer internal class SimUpsTest { @Test fun testSingleInlet() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) - val ups = SimUps(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val ups = SimUps(engine) source.connect(ups.newInlet()) ups.connect(SimpleInlet()) @@ -50,10 +50,10 @@ internal class SimUpsTest { @Test fun testDoubleInlet() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source1 = SimPowerSource(interpreter, capacity = 100.0) - val source2 = SimPowerSource(interpreter, capacity = 100.0) - val ups = SimUps(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val source1 = SimPowerSource(engine, capacity = 100.0) + val source2 = SimPowerSource(engine, capacity = 100.0) + val ups = SimUps(engine) source1.connect(ups.newInlet()) source2.connect(ups.newInlet()) @@ -67,10 +67,10 @@ internal class SimUpsTest { @Test fun testLoss() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source = SimPowerSource(interpreter, capacity = 100.0) + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) // https://download.schneider-electric.com/files?p_Doc_Ref=SPD_NRAN-66CK3D_EN - val ups = SimUps(interpreter, idlePower = 4.0, lossCoefficient = 0.05) + val ups = SimUps(engine, idlePower = 4.0, lossCoefficient = 0.05) source.connect(ups.newInlet()) ups.connect(SimpleInlet()) @@ -79,24 +79,24 @@ internal class SimUpsTest { @Test fun testDisconnect() = runBlockingSimulation { - val interpreter = SimResourceInterpreter(coroutineContext, clock) - val source1 = SimPowerSource(interpreter, capacity = 100.0) - val source2 = SimPowerSource(interpreter, capacity = 100.0) - val ups = SimUps(interpreter) + val engine = FlowEngine(coroutineContext, clock) + val source1 = SimPowerSource(engine, capacity = 100.0) + val source2 = SimPowerSource(engine, capacity = 100.0) + val ups = SimUps(engine) source1.connect(ups.newInlet()) source2.connect(ups.newInlet()) - val consumer = spyk(SimWorkConsumer(100.0, utilization = 1.0)) + val consumer = spyk(FixedFlowSource(100.0, utilization = 1.0)) val inlet = object : SimPowerInlet() { - override fun createConsumer(): SimResourceConsumer = consumer + override fun createConsumer(): FlowSource = consumer } ups.connect(inlet) ups.disconnect() - verify { consumer.onEvent(any(), SimResourceEvent.Exit) } + verify { consumer.onEvent(any(), any(), FlowEvent.Exit) } } class SimpleInlet : SimPowerInlet() { - override fun createConsumer(): SimResourceConsumer = SimWorkConsumer(100.0, utilization = 0.5) + override fun createConsumer(): FlowSource = FixedFlowSource(100.0, utilization = 0.5) } } diff --git a/opendc-simulator/opendc-simulator-resources/build.gradle.kts b/opendc-simulator/opendc-simulator-resources/build.gradle.kts deleted file mode 100644 index 68047d5c..00000000 --- a/opendc-simulator/opendc-simulator-resources/build.gradle.kts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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. - */ - -description = "Uniform resource consumption simulation model" - -plugins { - `kotlin-library-conventions` - `testing-conventions` - `jacoco-conventions` - `benchmark-conventions` -} - -dependencies { - api(platform(projects.opendcPlatform)) - api(libs.kotlinx.coroutines) - implementation(projects.opendcUtils) - - testImplementation(projects.opendcSimulator.opendcSimulatorCore) -} diff --git a/opendc-simulator/opendc-simulator-resources/src/jmh/kotlin/org/opendc/simulator/resources/SimResourceBenchmarks.kt b/opendc-simulator/opendc-simulator-resources/src/jmh/kotlin/org/opendc/simulator/resources/SimResourceBenchmarks.kt deleted file mode 100644 index fbc3f319..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/jmh/kotlin/org/opendc/simulator/resources/SimResourceBenchmarks.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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.resources - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import org.opendc.simulator.core.SimulationCoroutineScope -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.consumer.SimTraceConsumer -import org.openjdk.jmh.annotations.* -import java.util.concurrent.ThreadLocalRandom -import java.util.concurrent.TimeUnit - -@State(Scope.Thread) -@Fork(1) -@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) -@OptIn(ExperimentalCoroutinesApi::class) -class SimResourceBenchmarks { - private lateinit var scope: SimulationCoroutineScope - private lateinit var interpreter: SimResourceInterpreter - - @Setup - fun setUp() { - scope = SimulationCoroutineScope() - interpreter = SimResourceInterpreter(scope.coroutineContext, scope.clock) - } - - @State(Scope.Thread) - class Workload { - lateinit var trace: Sequence - - @Setup - fun setUp() { - val random = ThreadLocalRandom.current() - val entries = List(10000) { SimTraceConsumer.Fragment(1000, random.nextDouble(0.0, 4500.0)) } - trace = entries.asSequence() - } - } - - @Benchmark - fun benchmarkSource(state: Workload) { - return scope.runBlockingSimulation { - val provider = SimResourceSource(4200.0, interpreter) - return@runBlockingSimulation provider.consume(SimTraceConsumer(state.trace)) - } - } - - @Benchmark - fun benchmarkForwardOverhead(state: Workload) { - return scope.runBlockingSimulation { - val provider = SimResourceSource(4200.0, interpreter) - val forwarder = SimResourceForwarder() - provider.startConsumer(forwarder) - return@runBlockingSimulation forwarder.consume(SimTraceConsumer(state.trace)) - } - } - - @Benchmark - fun benchmarkSwitchMaxMinSingleConsumer(state: Workload) { - return scope.runBlockingSimulation { - val switch = SimResourceSwitchMaxMin(interpreter) - - switch.addInput(SimResourceSource(3000.0, interpreter)) - switch.addInput(SimResourceSource(3000.0, interpreter)) - - val provider = switch.newOutput() - return@runBlockingSimulation provider.consume(SimTraceConsumer(state.trace)) - } - } - - @Benchmark - fun benchmarkSwitchMaxMinTripleConsumer(state: Workload) { - return scope.runBlockingSimulation { - val switch = SimResourceSwitchMaxMin(interpreter) - - switch.addInput(SimResourceSource(3000.0, interpreter)) - switch.addInput(SimResourceSource(3000.0, interpreter)) - - repeat(3) { - launch { - val provider = switch.newOutput() - provider.consume(SimTraceConsumer(state.trace)) - } - } - } - } - - @Benchmark - fun benchmarkSwitchExclusiveSingleConsumer(state: Workload) { - return scope.runBlockingSimulation { - val switch = SimResourceSwitchExclusive() - - switch.addInput(SimResourceSource(3000.0, interpreter)) - switch.addInput(SimResourceSource(3000.0, interpreter)) - - val provider = switch.newOutput() - return@runBlockingSimulation provider.consume(SimTraceConsumer(state.trace)) - } - } - - @Benchmark - fun benchmarkSwitchExclusiveTripleConsumer(state: Workload) { - return scope.runBlockingSimulation { - val switch = SimResourceSwitchExclusive() - - switch.addInput(SimResourceSource(3000.0, interpreter)) - switch.addInput(SimResourceSource(3000.0, interpreter)) - - repeat(2) { - launch { - val provider = switch.newOutput() - provider.consume(SimTraceConsumer(state.trace)) - } - } - } - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt deleted file mode 100644 index 085cba63..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimAbstractResourceProvider.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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.resources - -import org.opendc.simulator.resources.impl.SimResourceCountersImpl - -/** - * Abstract implementation of the [SimResourceProvider] which can be re-used by other implementations. - */ -public abstract class SimAbstractResourceProvider( - private val interpreter: SimResourceInterpreter, - initialCapacity: Double -) : SimResourceProvider { - /** - * A flag to indicate that the resource provider is active. - */ - public override val isActive: Boolean - get() = ctx != null - - /** - * The capacity of the resource. - */ - public override var capacity: Double = initialCapacity - set(value) { - field = value - ctx?.capacity = value - } - - /** - * The current processing speed of the resource. - */ - public override val speed: Double - get() = ctx?.speed ?: 0.0 - - /** - * The resource processing speed demand at this instant. - */ - public override val demand: Double - get() = ctx?.demand ?: 0.0 - - /** - * The resource counters to track the execution metrics of the resource. - */ - public override val counters: SimResourceCounters - get() = _counters - private val _counters = SimResourceCountersImpl() - - /** - * The [SimResourceControllableContext] that is currently running. - */ - protected var ctx: SimResourceControllableContext? = null - private set - - /** - * Construct the [SimResourceProviderLogic] instance for a new consumer. - */ - protected abstract fun createLogic(): SimResourceProviderLogic - - /** - * Start the specified [SimResourceControllableContext]. - */ - protected open fun start(ctx: SimResourceControllableContext) { - ctx.start() - } - - /** - * The previous demand for the resource. - */ - private var previousDemand = 0.0 - - /** - * Update the counters of the resource provider. - */ - protected fun updateCounters(ctx: SimResourceContext, delta: Long) { - val demand = previousDemand - previousDemand = ctx.demand - - if (delta <= 0) { - return - } - - val counters = _counters - val deltaS = delta / 1000.0 - val work = demand * deltaS - val actualWork = ctx.speed * deltaS - val remainingWork = work - actualWork - - counters.demand += work - counters.actual += actualWork - counters.overcommit += remainingWork - } - - /** - * Update the counters of the resource provider. - */ - protected fun updateCounters(demand: Double, actual: Double, overcommit: Double) { - val counters = _counters - counters.demand += demand - counters.actual += actual - counters.overcommit += overcommit - } - - final override fun startConsumer(consumer: SimResourceConsumer) { - check(ctx == null) { "Resource is in invalid state" } - val ctx = interpreter.newContext(consumer, createLogic()) - - ctx.capacity = capacity - this.ctx = ctx - - start(ctx) - } - - final override fun interrupt() { - ctx?.interrupt() - } - - final override fun cancel() { - val ctx = ctx - if (ctx != null) { - this.ctx = null - ctx.close() - } - } - - override fun toString(): String = "SimAbstractResourceProvider[capacity=$capacity]" -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt deleted file mode 100644 index 0b25358a..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceConsumer.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.resources - -/** - * A [SimResourceConsumer] characterizes how a resource is consumed. - * - * Implementors of this interface should be considered stateful and must be assumed not to be re-usable (concurrently) - * for multiple resource providers, unless explicitly said otherwise. - */ -public interface SimResourceConsumer { - /** - * This method is invoked when the resource provider is pulling this resource consumer. - * - * @param ctx The execution context in which the consumer runs. - * @param now The virtual timestamp in milliseconds at which the update is occurring. - * @param delta The virtual duration between this call and the last call in milliseconds. - * @return The duration after which the resource consumer should be pulled again. - */ - public fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long - - /** - * This method is invoked when an event has occurred. - * - * @param ctx The execution context in which the consumer runs. - * @param event The event that has occurred. - */ - public fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) {} - - /** - * This method is invoked when a resource consumer throws an exception. - * - * @param ctx The execution context in which the consumer runs. - * @param cause The cause of the failure. - */ - public fun onFailure(ctx: SimResourceContext, cause: Throwable) {} -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt deleted file mode 100644 index 225cae0b..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceContext.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.resources - -import java.time.Clock - -/** - * The execution context in which a [SimResourceConsumer] runs. It facilitates the communication and control between a - * resource and a resource consumer. - */ -public interface SimResourceContext : AutoCloseable { - /** - * The virtual clock tracking simulation time. - */ - public val clock: Clock - - /** - * The resource capacity available at this instant. - */ - public val capacity: Double - - /** - * The resource processing speed at this instant. - */ - public val speed: Double - - /** - * The resource processing speed demand at this instant. - */ - public val demand: Double - - /** - * Ask the resource provider to interrupt its resource. - */ - public fun interrupt() - - /** - * Push the given flow to this context. - * - * @param rate The rate of the flow to push. - */ - public fun push(rate: Double) - - /** - * Stop the resource context. - */ - public override fun close() -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt deleted file mode 100644 index b406b896..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceControllableContext.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.resources - -/** - * A controllable [SimResourceContext]. - * - * This interface is used by resource providers to control the resource context. - */ -public interface SimResourceControllableContext : SimResourceContext { - /** - * The capacity of the resource. - */ - public override var capacity: Double - - /** - * Start the resource context. - */ - public fun start() - - /** - * Invalidate the resource context's state. - * - * By invalidating the resource context's current state, the state is re-computed and the current progress is - * materialized during the next interpreter cycle. As a result, this call run asynchronously. See [flush] for the - * synchronous variant. - */ - public fun invalidate() - - /** - * Synchronously flush the progress of the resource context and materialize its current progress. - */ - public fun flush() -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCounters.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCounters.kt deleted file mode 100644 index 11924db2..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceCounters.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.resources - -/** - * An interface that tracks cumulative counts of the work performed by a resource. - */ -public interface SimResourceCounters { - /** - * The amount of work that resource consumers wanted the resource to perform. - */ - public val demand: Double - - /** - * The amount of work performed by the resource. - */ - public val actual: Double - - /** - * The amount of work that could not be completed due to overcommitted resources. - */ - public val overcommit: Double - - /** - * The amount of work lost due to interference. - */ - public val interference: Double - - /** - * Reset the resource counters. - */ - public fun reset() -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceEvent.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceEvent.kt deleted file mode 100644 index 959427f1..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceEvent.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.resources - -/** - * A resource event that is communicated to the resource consumer. - */ -public enum class SimResourceEvent { - /** - * This event is emitted to the consumer when it has started. - */ - Start, - - /** - * This event is emitted to the consumer when it has exited. - */ - Exit, - - /** - * This event is emitted to the consumer when it has started a new resource consumption or idle cycle. - */ - Run, - - /** - * This event is emitted to the consumer when the capacity of the resource has changed. - */ - Capacity, -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceForwarder.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceForwarder.kt deleted file mode 100644 index 0cd2bfc7..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceForwarder.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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.resources - -import org.opendc.simulator.resources.impl.SimResourceCountersImpl -import java.time.Clock - -/** - * A class that acts as a [SimResourceConsumer] and [SimResourceProvider] at the same time. - * - * @param isCoupled A flag to indicate that the transformer will exit when the resource consumer exits. - */ -public class SimResourceForwarder(private val isCoupled: Boolean = false) : SimResourceConsumer, SimResourceProvider, AutoCloseable { - /** - * The delegate [SimResourceConsumer]. - */ - private var delegate: SimResourceConsumer? = null - - /** - * A flag to indicate that the delegate was started. - */ - private var hasDelegateStarted: Boolean = false - - /** - * The exposed [SimResourceContext]. - */ - private val _ctx = object : SimResourceContext { - override val clock: Clock - get() = _innerCtx!!.clock - - override val capacity: Double - get() = _innerCtx?.capacity ?: 0.0 - - override val demand: Double - get() = _innerCtx?.demand ?: 0.0 - - override val speed: Double - get() = _innerCtx?.speed ?: 0.0 - - override fun interrupt() { - _innerCtx?.interrupt() - } - - override fun push(rate: Double) { - _innerCtx?.push(rate) - _limit = rate - } - - override fun close() { - val delegate = checkNotNull(delegate) { "Delegate not active" } - - if (isCoupled) - _innerCtx?.close() - else - _innerCtx?.push(0.0) - - // Warning: resumption of the continuation might change the entire state of the forwarder. Make sure we - // reset beforehand the existing state and check whether it has been updated afterwards - reset() - - delegate.onEvent(this, SimResourceEvent.Exit) - } - } - - /** - * The [SimResourceContext] in which the forwarder runs. - */ - private var _innerCtx: SimResourceContext? = null - - override val isActive: Boolean - get() = delegate != null - - override val capacity: Double - get() = _ctx.capacity - - override val speed: Double - get() = _ctx.speed - - override val demand: Double - get() = _ctx.demand - - override val counters: SimResourceCounters - get() = _counters - private val _counters = SimResourceCountersImpl() - - override fun startConsumer(consumer: SimResourceConsumer) { - check(delegate == null) { "Resource transformer already active" } - - delegate = consumer - - // Interrupt the provider to replace the consumer - interrupt() - } - - override fun interrupt() { - _ctx.interrupt() - } - - override fun cancel() { - val delegate = delegate - val ctx = _innerCtx - - if (delegate != null) { - this.delegate = null - - if (ctx != null) { - delegate.onEvent(this._ctx, SimResourceEvent.Exit) - } - } - } - - override fun close() { - val ctx = _innerCtx - - if (ctx != null) { - this._innerCtx = null - ctx.interrupt() - } - } - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - val delegate = delegate - - if (!hasDelegateStarted) { - start() - } - - updateCounters(ctx, delta) - - return delegate?.onNext(this._ctx, now, delta) ?: Long.MAX_VALUE - } - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - when (event) { - SimResourceEvent.Start -> { - _innerCtx = ctx - } - SimResourceEvent.Exit -> { - _innerCtx = null - - val delegate = delegate - if (delegate != null) { - reset() - delegate.onEvent(this._ctx, SimResourceEvent.Exit) - } - } - else -> delegate?.onEvent(this._ctx, event) - } - } - - override fun onFailure(ctx: SimResourceContext, cause: Throwable) { - _innerCtx = null - - val delegate = delegate - if (delegate != null) { - reset() - delegate.onFailure(this._ctx, cause) - } - } - - /** - * Start the delegate. - */ - private fun start() { - val delegate = delegate ?: return - delegate.onEvent(checkNotNull(_innerCtx), SimResourceEvent.Start) - - hasDelegateStarted = true - } - - /** - * Reset the delegate. - */ - private fun reset() { - delegate = null - hasDelegateStarted = false - } - - /** - * The requested speed. - */ - private var _limit: Double = 0.0 - - /** - * Update the resource counters for the transformer. - */ - private fun updateCounters(ctx: SimResourceContext, delta: Long) { - if (delta <= 0) { - return - } - - val counters = _counters - val deltaS = delta / 1000.0 - val work = _limit * deltaS - val actualWork = ctx.speed * deltaS - counters.demand += work - counters.actual += actualWork - counters.overcommit += (work - actualWork) - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceInterpreter.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceInterpreter.kt deleted file mode 100644 index 4bfeaf20..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceInterpreter.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.resources - -import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl -import java.time.Clock -import kotlin.coroutines.CoroutineContext - -/** - * The resource interpreter is responsible for managing the interaction between resource consumer and provider. - * - * The interpreter centralizes the scheduling logic of state updates of resource context, allowing update propagation - * to happen more efficiently. and overall, reducing the work necessary to transition into a steady state. - */ -public interface SimResourceInterpreter { - /** - * The [Clock] associated with this interpreter. - */ - public val clock: Clock - - /** - * Create a new [SimResourceControllableContext] with the given [provider]. - * - * @param consumer The consumer logic. - * @param provider The logic of the resource provider. - */ - public fun newContext(consumer: SimResourceConsumer, provider: SimResourceProviderLogic): SimResourceControllableContext - - /** - * Start batching the execution of resource updates until [popBatch] is called. - * - * This method is useful if you want to propagate multiple resources updates (e.g., starting multiple CPUs - * simultaneously) in a single state update. - * - * Multiple calls to this method requires the same number of [popBatch] calls in order to properly flush the - * resource updates. This allows nested calls to [pushBatch], but might cause issues if [popBatch] is not called - * the same amount of times. To simplify batching, see [batch]. - */ - public fun pushBatch() - - /** - * Stop the batching of resource updates and run the interpreter on the batch. - * - * Note that method will only flush the event once the first call to [pushBatch] has received a [popBatch] call. - */ - public fun popBatch() - - public companion object { - /** - * Construct a new [SimResourceInterpreter] implementation. - * - * @param context The coroutine context to use. - * @param clock The virtual simulation clock. - */ - @JvmName("create") - public operator fun invoke(context: CoroutineContext, clock: Clock): SimResourceInterpreter { - return SimResourceInterpreterImpl(context, clock) - } - } -} - -/** - * Batch the execution of several interrupts into a single call. - * - * This method is useful if you want to propagate the start of multiple resources (e.g., CPUs) in a single update. - */ -public inline fun SimResourceInterpreter.batch(block: () -> Unit) { - try { - pushBatch() - block() - } finally { - popBatch() - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProvider.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProvider.kt deleted file mode 100644 index b68b7261..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProvider.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.resources - -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -/** - * A [SimResourceProvider] provides a resource that can be consumed by a [SimResourceConsumer]. - */ -public interface SimResourceProvider { - /** - * A flag to indicate that the resource provider is currently being consumed by a [SimResourceConsumer]. - */ - public val isActive: Boolean - - /** - * The resource capacity available at this instant. - */ - public val capacity: Double - - /** - * The current processing speed of the resource. - */ - public val speed: Double - - /** - * The resource processing speed demand at this instant. - */ - public val demand: Double - - /** - * The resource counters to track the execution metrics of the resource. - */ - public val counters: SimResourceCounters - - /** - * Start the specified [resource consumer][consumer] in the context of this resource provider asynchronously. - * - * @throws IllegalStateException if there is already a consumer active or the resource lifetime has ended. - */ - public fun startConsumer(consumer: SimResourceConsumer) - - /** - * Interrupt the resource consumer. If there is no consumer active, this operation will be a no-op. - */ - public fun interrupt() - - /** - * Cancel the current resource consumer. If there is no consumer active, this operation will be a no-op. - */ - public fun cancel() -} - -/** - * Consume the resource provided by this provider using the specified [consumer] and suspend execution until - * the consumer has finished. - */ -public suspend fun SimResourceProvider.consume(consumer: SimResourceConsumer) { - return suspendCancellableCoroutine { cont -> - startConsumer(object : SimResourceConsumer by consumer { - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - consumer.onEvent(ctx, event) - - if (event == SimResourceEvent.Exit && !cont.isCompleted) { - cont.resume(Unit) - } - } - - override fun onFailure(ctx: SimResourceContext, cause: Throwable) { - try { - consumer.onFailure(ctx, cause) - cont.resumeWithException(cause) - } catch (e: Throwable) { - e.addSuppressed(cause) - cont.resumeWithException(e) - } - } - - override fun toString(): String = "SimSuspendingResourceConsumer" - }) - - cont.invokeOnCancellation { cancel() } - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt deleted file mode 100644 index cc718165..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceProviderLogic.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.resources - -/** - * A collection of callbacks associated with a flow stage. - */ -public interface SimResourceProviderLogic { - /** - * This method is invoked when the consumer ask to consume the resource for the specified [duration]. - * - * @param ctx The context in which the provider runs. - * @param now The virtual timestamp in milliseconds at which the update is occurring. - * @param delta The virtual duration between this call and the last call to [onConsume] in milliseconds. - * @param limit The limit on the work rate of the resource consumer. - * @param duration The duration of the consumption in milliseconds. - * @return The deadline of the resource consumption. - */ - public fun onConsume(ctx: SimResourceControllableContext, now: Long, delta: Long, limit: Double, duration: Long) {} - - /** - * This method is invoked when the flow graph has converged into a steady-state system. - * - * @param ctx The context in which the provider runs. - * @param now The virtual timestamp in milliseconds at which the system converged. - * @param delta The virtual duration between this call and the last call to [onConverge] in milliseconds. - */ - public fun onConverge(ctx: SimResourceControllableContext, now: Long, delta: Long) {} - - /** - * This method is invoked when the resource consumer has finished. - * - * @param ctx The context in which the provider runs. - * @param now The virtual timestamp in milliseconds at which the provider finished. - * @param delta The virtual duration between this call and the last call to [onConsume] in milliseconds. - */ - public fun onFinish(ctx: SimResourceControllableContext, now: Long, delta: Long) {} -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt deleted file mode 100644 index c8d4cf0d..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSource.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.resources - -/** - * A [SimResourceSource] represents a source for some resource that provides bounded processing capacity. - * - * @param initialCapacity The initial capacity of the resource. - * @param interpreter The interpreter that is used for managing the resource contexts. - * @param parent The parent resource system. - */ -public class SimResourceSource( - initialCapacity: Double, - private val interpreter: SimResourceInterpreter, - private val parent: SimResourceSystem? = null -) : SimAbstractResourceProvider(interpreter, initialCapacity) { - override fun createLogic(): SimResourceProviderLogic { - return object : SimResourceProviderLogic { - override fun onConsume( - ctx: SimResourceControllableContext, - now: Long, - delta: Long, - limit: Double, - duration: Long - ) { - updateCounters(ctx, delta) - } - - override fun onFinish(ctx: SimResourceControllableContext, now: Long, delta: Long) { - updateCounters(ctx, delta) - cancel() - } - - override fun onConverge(ctx: SimResourceControllableContext, now: Long, delta: Long) { - parent?.onConverge(now) - } - } - } - - override fun toString(): String = "SimResourceSource[capacity=$capacity]" -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitch.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitch.kt deleted file mode 100644 index 3c25b76d..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitch.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.resources - -import org.opendc.simulator.resources.interference.InterferenceKey - -/** - * A [SimResourceSwitch] enables switching of capacity of multiple resources between multiple consumers. - */ -public interface SimResourceSwitch { - /** - * The output resource providers to which resource consumers can be attached. - */ - public val outputs: Set - - /** - * The input resources that will be switched between the output providers. - */ - public val inputs: Set - - /** - * The resource counters to track the execution metrics of all switch resources. - */ - public val counters: SimResourceCounters - - /** - * Create a new output on the switch. - * - * @param key The key of the interference member to which the output belongs. - */ - public fun newOutput(key: InterferenceKey? = null): SimResourceProvider - - /** - * Remove [output] from this switch. - */ - public fun removeOutput(output: SimResourceProvider) - - /** - * Add the specified [input] to the switch. - */ - public fun addInput(input: SimResourceProvider) - - /** - * Clear all inputs and outputs from the switch. - */ - public fun clear() -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt deleted file mode 100644 index f1e004d2..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusive.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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.resources - -import org.opendc.simulator.resources.interference.InterferenceKey -import java.util.ArrayDeque - -/** - * A [SimResourceSwitch] implementation that allocates outputs to the inputs of the switch exclusively. This means that - * a single output is directly connected to an input and that the switch can only support as many outputs as inputs. - */ -public class SimResourceSwitchExclusive : SimResourceSwitch { - override val outputs: Set - get() = _outputs - private val _outputs = mutableSetOf() - - private val _inputs = mutableSetOf() - override val inputs: Set - get() = _inputs - private val _availableInputs = ArrayDeque() - - override val counters: SimResourceCounters = object : SimResourceCounters { - override val demand: Double - get() = _inputs.sumOf { it.counters.demand } - override val actual: Double - get() = _inputs.sumOf { it.counters.actual } - override val overcommit: Double - get() = _inputs.sumOf { it.counters.overcommit } - override val interference: Double - get() = _inputs.sumOf { it.counters.interference } - - override fun reset() { - for (input in _inputs) { - input.counters.reset() - } - } - - override fun toString(): String = "SimResourceCounters[demand=$demand,actual=$actual,overcommit=$overcommit]" - } - - /** - * Add an output to the switch. - */ - override fun newOutput(key: InterferenceKey?): SimResourceProvider { - val forwarder = checkNotNull(_availableInputs.poll()) { "No capacity to serve request" } - val output = Output(forwarder) - _outputs += output - return output - } - - override fun removeOutput(output: SimResourceProvider) { - if (!_outputs.remove(output)) { - return - } - - (output as Output).close() - } - - /** - * Add an input to the switch. - */ - override fun addInput(input: SimResourceProvider) { - if (input in inputs) { - return - } - - val forwarder = SimResourceForwarder() - - _inputs += input - _availableInputs += forwarder - - input.startConsumer(object : SimResourceConsumer by forwarder { - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - if (event == SimResourceEvent.Exit) { - // De-register the input after it has finished - _inputs -= input - } - - forwarder.onEvent(ctx, event) - } - }) - } - - override fun clear() { - for (input in _inputs) { - input.cancel() - } - _inputs.clear() - - // Outputs are implicitly cancelled by the inputs forwarders - _outputs.clear() - } - - /** - * An output of the resource switch. - */ - private inner class Output(private val forwarder: SimResourceForwarder) : SimResourceProvider by forwarder { - /** - * Close the output. - */ - fun close() { - // We explicitly do not close the forwarder here in order to re-use it across output resources. - _outputs -= this - _availableInputs += forwarder - } - - override fun toString(): String = "SimResourceSwitchExclusive.Output" - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMin.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMin.kt deleted file mode 100644 index 574fb443..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMin.kt +++ /dev/null @@ -1,407 +0,0 @@ -/* - * 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.resources - -import org.opendc.simulator.resources.impl.SimResourceCountersImpl -import org.opendc.simulator.resources.interference.InterferenceDomain -import org.opendc.simulator.resources.interference.InterferenceKey -import kotlin.math.max -import kotlin.math.min - -/** - * A [SimResourceSwitch] implementation that switches resource consumptions over the available resources using max-min - * fair sharing. - * - * @param interpreter The interpreter for managing the resource contexts. - * @param parent The parent resource system of the switch. - * @param interferenceDomain The interference domain of the switch. - */ -public class SimResourceSwitchMaxMin( - private val interpreter: SimResourceInterpreter, - private val parent: SimResourceSystem? = null, - private val interferenceDomain: InterferenceDomain? = null -) : SimResourceSwitch { - /** - * The output resource providers to which resource consumers can be attached. - */ - override val outputs: Set - get() = _outputs - private val _outputs = mutableSetOf() - private val _activeOutputs: MutableList = mutableListOf() - - /** - * The input resources that will be switched between the output providers. - */ - override val inputs: Set - get() = _inputs - private val _inputs = mutableSetOf() - private val _activeInputs = mutableListOf() - - /** - * The resource counters of this switch. - */ - public override val counters: SimResourceCounters - get() = _counters - private val _counters = SimResourceCountersImpl() - - /** - * The actual processing rate of the switch. - */ - private var _rate = 0.0 - - /** - * The demanded processing rate of the outputs. - */ - private var _demand = 0.0 - - /** - * The capacity of the switch. - */ - private var _capacity = 0.0 - - /** - * Flag to indicate that the scheduler is active. - */ - private var _schedulerActive = false - - /** - * Add an output to the switch. - */ - override fun newOutput(key: InterferenceKey?): SimResourceProvider { - val provider = Output(_capacity, key) - _outputs.add(provider) - return provider - } - - /** - * Add the specified [input] to the switch. - */ - override fun addInput(input: SimResourceProvider) { - val consumer = Input(input) - if (_inputs.add(input)) { - _activeInputs.add(consumer) - input.startConsumer(consumer) - } - } - - /** - * Remove [output] from this switch. - */ - override fun removeOutput(output: SimResourceProvider) { - if (!_outputs.remove(output)) { - return - } - // This cast should always succeed since only `Output` instances should be added to _outputs - (output as Output).close() - } - - override fun clear() { - for (input in _activeInputs) { - input.cancel() - } - _activeInputs.clear() - - for (output in _activeOutputs) { - output.cancel() - } - _activeOutputs.clear() - } - - /** - * Run the scheduler of the switch. - */ - private fun runScheduler(now: Long) { - if (_schedulerActive) { - return - } - - _schedulerActive = true - try { - doSchedule(now) - } finally { - _schedulerActive = false - } - } - - /** - * Schedule the outputs over the input. - */ - private fun doSchedule(now: Long) { - // If there is no work yet, mark the input as idle. - if (_activeOutputs.isEmpty()) { - return - } - - val capacity = _capacity - var availableCapacity = capacity - - // Pull in the work of the outputs - val outputIterator = _activeOutputs.listIterator() - for (output in outputIterator) { - output.pull(now) - - // Remove outputs that have finished - if (!output.isActive) { - outputIterator.remove() - } - } - - var demand = 0.0 - - // Sort in-place the outputs based on their requested usage. - // Profiling shows that it is faster than maintaining some kind of sorted set. - _activeOutputs.sort() - - // Divide the available input capacity fairly across the outputs using max-min fair sharing - var remaining = _activeOutputs.size - for (output in _activeOutputs) { - val availableShare = availableCapacity / remaining-- - val grantedSpeed = min(output.allowedRate, availableShare) - - // Ignore idle computation - if (grantedSpeed <= 0.0) { - output.actualRate = 0.0 - continue - } - - demand += output.limit - - output.actualRate = grantedSpeed - availableCapacity -= grantedSpeed - } - - val rate = capacity - availableCapacity - - _demand = demand - _rate = rate - - // Sort all consumers by their capacity - _activeInputs.sort() - - // Divide the requests over the available capacity of the input resources fairly - for (input in _activeInputs) { - val inputCapacity = input.capacity - val fraction = inputCapacity / capacity - val grantedSpeed = rate * fraction - - input.push(grantedSpeed) - } - } - - /** - * Recompute the capacity of the switch. - */ - private fun updateCapacity() { - val newCapacity = _activeInputs.sumOf(Input::capacity) - - // No-op if the capacity is unchanged - if (_capacity == newCapacity) { - return - } - - _capacity = newCapacity - - for (output in _outputs) { - output.capacity = newCapacity - } - } - - /** - * An internal [SimResourceProvider] implementation for switch outputs. - */ - private inner class Output(capacity: Double, val key: InterferenceKey?) : - SimAbstractResourceProvider(interpreter, capacity), - SimResourceProviderLogic, - Comparable { - /** - * The requested limit. - */ - @JvmField var limit: Double = 0.0 - - /** - * The actual processing speed. - */ - @JvmField var actualRate: Double = 0.0 - - /** - * The processing speed that is allowed by the model constraints. - */ - val allowedRate: Double - get() = min(capacity, limit) - - /** - * A flag to indicate that the output is closed. - */ - private var _isClosed: Boolean = false - - /** - * The timestamp at which we received the last command. - */ - private var _lastPull: Long = Long.MIN_VALUE - - /** - * Close the output. - * - * This method is invoked when the user removes an output from the switch. - */ - fun close() { - _isClosed = true - cancel() - } - - /* SimAbstractResourceProvider */ - override fun createLogic(): SimResourceProviderLogic = this - - override fun start(ctx: SimResourceControllableContext) { - check(!_isClosed) { "Cannot re-use closed output" } - - _activeOutputs += this - super.start(ctx) - } - - /* SimResourceProviderLogic */ - override fun onConsume( - ctx: SimResourceControllableContext, - now: Long, - delta: Long, - limit: Double, - duration: Long - ) { - doUpdateCounters(delta) - - actualRate = 0.0 - this.limit = limit - _lastPull = now - - runScheduler(now) - } - - override fun onConverge(ctx: SimResourceControllableContext, now: Long, delta: Long) { - parent?.onConverge(now) - } - - override fun onFinish(ctx: SimResourceControllableContext, now: Long, delta: Long) { - doUpdateCounters(delta) - - limit = 0.0 - actualRate = 0.0 - _lastPull = now - } - - /* Comparable */ - override fun compareTo(other: Output): Int = allowedRate.compareTo(other.allowedRate) - - /** - * Pull the next command if necessary. - */ - fun pull(now: Long) { - val ctx = ctx - if (ctx != null && _lastPull < now) { - ctx.flush() - } - } - - /** - * Helper method to update the resource counters of the distributor. - */ - private fun doUpdateCounters(delta: Long) { - if (delta <= 0L) { - return - } - - // Compute the performance penalty due to resource interference - val perfScore = if (interferenceDomain != null) { - val load = _rate / capacity - interferenceDomain.apply(key, load) - } else { - 1.0 - } - - val deltaS = delta / 1000.0 - val work = limit * deltaS - val actualWork = actualRate * deltaS - val remainingWork = work - actualWork - - updateCounters(work, actualWork, remainingWork) - - val distCounters = _counters - distCounters.demand += work - distCounters.actual += actualWork - distCounters.overcommit += remainingWork - distCounters.interference += actualWork * max(0.0, 1 - perfScore) - } - } - - /** - * An internal [SimResourceConsumer] implementation for switch inputs. - */ - private inner class Input(private val provider: SimResourceProvider) : SimResourceConsumer, Comparable { - /** - * The active [SimResourceContext] of this consumer. - */ - private var _ctx: SimResourceContext? = null - - /** - * The capacity of this input. - */ - val capacity: Double - get() = _ctx?.capacity ?: 0.0 - - /** - * Push the specified rate to the provider. - */ - fun push(rate: Double) { - _ctx?.push(rate) - } - - /** - * Cancel this input. - */ - fun cancel() { - provider.cancel() - } - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - runScheduler(now) - return Long.MAX_VALUE - } - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - when (event) { - SimResourceEvent.Start -> { - assert(_ctx == null) { "Consumer running concurrently" } - _ctx = ctx - updateCapacity() - } - SimResourceEvent.Exit -> { - _ctx = null - updateCapacity() - } - SimResourceEvent.Capacity -> updateCapacity() - else -> {} - } - } - - override fun compareTo(other: Input): Int = capacity.compareTo(other.capacity) - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSystem.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSystem.kt deleted file mode 100644 index 609262cb..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/SimResourceSystem.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.resources - -/** - * A system of possible multiple sub-resources. - * - * This interface is used to model hierarchies of resource providers, which can listen efficiently to changes of the - * resource provider. - */ -public interface SimResourceSystem { - /** - * The parent system to which this system belongs or `null` if it has no parent. - */ - public val parent: SimResourceSystem? - - /** - * This method is invoked when the system has converged to a steady-state. - * - * @param timestamp The timestamp at which the system converged. - */ - public fun onConverge(timestamp: Long) -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimConsumerBarrier.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimConsumerBarrier.kt deleted file mode 100644 index 52a42241..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimConsumerBarrier.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.resources.consumer - -/** - * The [SimConsumerBarrier] is a barrier that allows consumers to wait for a select number of other consumers to - * complete, before proceeding its operation. - */ -public class SimConsumerBarrier(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 - } - - /** - * Reset the barrier. - */ - public fun reset() { - counter = 0 - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt deleted file mode 100644 index 46885640..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimSpeedConsumerAdapter.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.resources.consumer - -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceContext -import org.opendc.simulator.resources.SimResourceEvent -import kotlin.math.min - -/** - * Helper class to expose an observable [speed] field describing the speed of the consumer. - */ -public class SimSpeedConsumerAdapter( - private val delegate: SimResourceConsumer, - private val callback: (Double) -> Unit = {} -) : SimResourceConsumer by delegate { - /** - * The resource processing speed at this instant. - */ - public var speed: Double = 0.0 - private set(value) { - if (field != value) { - callback(value) - field = value - } - } - - init { - callback(0.0) - } - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return delegate.onNext(ctx, now, delta) - } - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - val oldSpeed = speed - - delegate.onEvent(ctx, event) - - when (event) { - SimResourceEvent.Run -> speed = ctx.speed - SimResourceEvent.Capacity -> { - // Check if the consumer interrupted the consumer and updated the resource consumption. If not, we might - // need to update the current speed. - if (oldSpeed == speed) { - speed = min(ctx.capacity, speed) - } - } - SimResourceEvent.Exit -> speed = 0.0 - else -> {} - } - } - - override fun onFailure(ctx: SimResourceContext, cause: Throwable) { - speed = 0.0 - - delegate.onFailure(ctx, cause) - } - - override fun toString(): String = "SimSpeedConsumerAdapter[delegate=$delegate]" -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt deleted file mode 100644 index 4c0e075c..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimTraceConsumer.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.resources.consumer - -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceContext -import org.opendc.simulator.resources.SimResourceEvent - -/** - * A [SimResourceConsumer] that replays a workload trace consisting of multiple fragments, each indicating the resource - * consumption for some period of time. - */ -public class SimTraceConsumer(private val trace: Sequence) : SimResourceConsumer { - private var _iterator: Iterator? = null - private var _nextTarget = Long.MIN_VALUE - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - // Check whether the trace fragment was fully consumed, otherwise wait until we have done so - val nextTarget = _nextTarget - if (nextTarget > now) { - return now - nextTarget - } - - val iterator = checkNotNull(_iterator) - return if (iterator.hasNext()) { - val fragment = iterator.next() - _nextTarget = now + fragment.duration - ctx.push(fragment.usage) - fragment.duration - } else { - ctx.close() - Long.MAX_VALUE - } - } - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - when (event) { - SimResourceEvent.Start -> { - check(_iterator == null) { "Consumer already running" } - _iterator = trace.iterator() - } - SimResourceEvent.Exit -> { - _iterator = null - } - else -> {} - } - } - - /** - * A fragment of the workload. - */ - public data class Fragment(val duration: Long, val usage: Double) -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt deleted file mode 100644 index bf76711f..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/consumer/SimWorkConsumer.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.resources.consumer - -import org.opendc.simulator.resources.SimResourceConsumer -import org.opendc.simulator.resources.SimResourceContext -import kotlin.math.roundToLong - -/** - * A [SimResourceConsumer] that consumes the specified amount of work at the specified utilization. - */ -public class SimWorkConsumer( - private val work: Double, - private val utilization: Double -) : SimResourceConsumer { - - init { - require(work >= 0.0) { "Work must be positive" } - require(utilization > 0.0) { "Utilization must be positive" } - } - - private var remainingWork = work - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - val actualWork = ctx.speed * delta / 1000.0 - val limit = ctx.capacity * utilization - - remainingWork -= actualWork - - val remainingWork = remainingWork - val duration = (remainingWork / limit * 1000).roundToLong() - - return if (duration > 0) { - ctx.push(limit) - duration - } else { - ctx.close() - Long.MAX_VALUE - } - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt deleted file mode 100644 index cbfa7afd..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceContextImpl.kt +++ /dev/null @@ -1,367 +0,0 @@ -/* - * 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.resources.impl - -import org.opendc.simulator.resources.* -import java.time.Clock -import java.util.ArrayDeque -import kotlin.math.max -import kotlin.math.min - -/** - * Implementation of a [SimResourceContext] managing the communication between resources and resource consumers. - */ -internal class SimResourceContextImpl( - private val interpreter: SimResourceInterpreterImpl, - private val consumer: SimResourceConsumer, - private val logic: SimResourceProviderLogic -) : SimResourceControllableContext { - /** - * The clock of the context. - */ - override val clock: Clock - get() = _clock - private val _clock = interpreter.clock - - /** - * The capacity of the resource. - */ - override var capacity: Double = 0.0 - set(value) { - val oldValue = field - - // Only changes will be propagated - if (value != oldValue) { - field = value - onCapacityChange() - } - } - - /** - * A flag to indicate the state of the context. - */ - private var _state = State.Pending - - /** - * The current processing speed of the resource. - */ - override val speed: Double - get() = _rate - private var _rate = 0.0 - - /** - * The current resource processing demand. - */ - override val demand: Double - get() = _limit - - /** - * The current state of the resource context. - */ - private var _limit: Double = 0.0 - private var _activeLimit: Double = 0.0 - private var _deadline: Long = Long.MIN_VALUE - - /** - * A flag to indicate that an update is active. - */ - private var _updateActive = false - - /** - * The update flag indicating why the update was triggered. - */ - private var _flag: Int = 0 - - /** - * The timestamp of calls to the callbacks. - */ - private var _lastUpdate: Long = Long.MIN_VALUE - private var _lastConvergence: Long = Long.MAX_VALUE - - /** - * The timers at which the context is scheduled to be interrupted. - */ - private val _timers: ArrayDeque = ArrayDeque() - - override fun start() { - check(_state == State.Pending) { "Consumer is already started" } - interpreter.batch { - consumer.onEvent(this, SimResourceEvent.Start) - _state = State.Active - interrupt() - } - } - - override fun close() { - if (_state == State.Stopped) { - return - } - - interpreter.batch { - _state = State.Stopped - if (!_updateActive) { - val now = clock.millis() - val delta = max(0, now - _lastUpdate) - doStop(now, delta) - - // FIX: Make sure the context converges - _flag = _flag or FLAG_INVALIDATE - scheduleUpdate(clock.millis()) - } - } - } - - override fun interrupt() { - if (_state == State.Stopped) { - return - } - - _flag = _flag or FLAG_INTERRUPT - scheduleUpdate(clock.millis()) - } - - override fun invalidate() { - if (_state == State.Stopped) { - return - } - - _flag = _flag or FLAG_INVALIDATE - scheduleUpdate(clock.millis()) - } - - override fun flush() { - if (_state == State.Stopped) { - return - } - - interpreter.scheduleSync(clock.millis(), this) - } - - override fun push(rate: Double) { - if (_limit == rate) { - return - } - - _limit = rate - - // Invalidate only if the active limit is change and no update is active - // If an update is active, it will already get picked up at the end of the update - if (_activeLimit != rate && !_updateActive) { - invalidate() - } - } - - /** - * Determine whether the state of the resource context should be updated. - */ - fun shouldUpdate(timestamp: Long): Boolean { - // Either the resource context is flagged or there is a pending update at this timestamp - return _flag != 0 || _limit != _activeLimit || _deadline == timestamp - } - - /** - * Update the state of the resource context. - */ - fun doUpdate(now: Long) { - val oldState = _state - if (oldState != State.Active) { - return - } - - val lastUpdate = _lastUpdate - - _lastUpdate = now - _updateActive = true - - val delta = max(0, now - lastUpdate) - - try { - val duration = consumer.onNext(this, now, delta) - val newDeadline = if (duration != Long.MAX_VALUE) now + duration else duration - - // Reset update flags - _flag = 0 - - // Check whether the state has changed after [consumer.onNext] - when (_state) { - State.Active -> { - logic.onConsume(this, now, delta, _limit, duration) - - // Schedule an update at the new deadline - scheduleUpdate(now, newDeadline) - } - State.Stopped -> doStop(now, delta) - State.Pending -> throw IllegalStateException("Illegal transition to pending state") - } - - // Note: pending limit might be changed by [logic.onConsume], so re-fetch the value - val newLimit = _limit - - // Flush the changes to the flow - _activeLimit = newLimit - _deadline = newDeadline - _rate = min(capacity, newLimit) - } catch (cause: Throwable) { - doFail(now, delta, cause) - } finally { - _updateActive = false - } - } - - /** - * Prune the elapsed timers from this context. - */ - fun pruneTimers(now: Long) { - val timers = _timers - while (true) { - val head = timers.peek() - if (head == null || head.target > now) { - break - } - timers.poll() - } - } - - /** - * Try to re-schedule the resource context in case it was skipped. - */ - fun tryReschedule(now: Long) { - val deadline = _deadline - if (deadline > now && deadline != Long.MAX_VALUE) { - scheduleUpdate(now, deadline) - } - } - - /** - * This method is invoked when the system converges into a steady state. - */ - fun onConverge(timestamp: Long) { - val delta = max(0, timestamp - _lastConvergence) - _lastConvergence = timestamp - - try { - if (_state == State.Active) { - consumer.onEvent(this, SimResourceEvent.Run) - } - - logic.onConverge(this, timestamp, delta) - } catch (cause: Throwable) { - doFail(timestamp, max(0, timestamp - _lastUpdate), cause) - } - } - - override fun toString(): String = "SimResourceContextImpl[capacity=$capacity,rate=$_rate]" - - /** - * Stop the resource context. - */ - private fun doStop(now: Long, delta: Long) { - try { - consumer.onEvent(this, SimResourceEvent.Exit) - logic.onFinish(this, now, delta) - } catch (cause: Throwable) { - doFail(now, delta, cause) - } finally { - _deadline = Long.MAX_VALUE - _limit = 0.0 - } - } - - /** - * Fail the resource consumer. - */ - private fun doFail(now: Long, delta: Long, cause: Throwable) { - try { - consumer.onFailure(this, cause) - } catch (e: Throwable) { - e.addSuppressed(cause) - e.printStackTrace() - } - - logic.onFinish(this, now, delta) - } - - /** - * Indicate that the capacity of the resource has changed. - */ - private fun onCapacityChange() { - // Do not inform the consumer if it has not been started yet - if (_state != State.Active) { - return - } - - interpreter.batch { - // Inform the consumer of the capacity change. This might already trigger an interrupt. - consumer.onEvent(this, SimResourceEvent.Capacity) - - interrupt() - } - } - - /** - * Schedule an update for this resource context. - */ - private fun scheduleUpdate(now: Long) { - interpreter.scheduleImmediate(now, this) - } - - /** - * Schedule a delayed update for this resource context. - */ - private fun scheduleUpdate(now: Long, target: Long) { - val timers = _timers - if (target != Long.MAX_VALUE && (timers.isEmpty() || target < timers.peek().target)) { - timers.addFirst(interpreter.scheduleDelayed(now, this, target)) - } - } - - /** - * The state of a resource context. - */ - private enum class State { - /** - * The resource context is pending and the resource is waiting to be consumed. - */ - Pending, - - /** - * The resource context is active and the resource is currently being consumed. - */ - Active, - - /** - * The resource context is stopped and the resource cannot be consumed anymore. - */ - Stopped - } - - /** - * A flag to indicate that the context should be invalidated. - */ - private val FLAG_INVALIDATE = 0b01 - - /** - * A flag to indicate that the context should be interrupted. - */ - private val FLAG_INTERRUPT = 0b10 -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceCountersImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceCountersImpl.kt deleted file mode 100644 index 01062179..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceCountersImpl.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.resources.impl - -import org.opendc.simulator.resources.SimResourceCounters - -/** - * Mutable implementation of the [SimResourceCounters] interface. - */ -internal class SimResourceCountersImpl : SimResourceCounters { - override var demand: Double = 0.0 - override var actual: Double = 0.0 - override var overcommit: Double = 0.0 - override var interference: Double = 0.0 - - override fun reset() { - demand = 0.0 - actual = 0.0 - overcommit = 0.0 - interference = 0.0 - } - - override fun toString(): String { - return "SimResourceCounters[demand=$demand,actual=$actual,overcommit=$overcommit,interference=$interference]" - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt deleted file mode 100644 index 2abf0749..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/impl/SimResourceInterpreterImpl.kt +++ /dev/null @@ -1,298 +0,0 @@ -/* - * 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.resources.impl - -import kotlinx.coroutines.Delay -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.Runnable -import org.opendc.simulator.resources.* -import java.time.Clock -import java.util.* -import kotlin.coroutines.ContinuationInterceptor -import kotlin.coroutines.CoroutineContext - -/** - * A [SimResourceInterpreter] queues all interrupts that occur during execution to be executed after. - * - * @param context The coroutine context to use. - * @param clock The virtual simulation clock. - */ -internal class SimResourceInterpreterImpl(private val context: CoroutineContext, override val clock: Clock) : SimResourceInterpreter { - /** - * The [Delay] instance that provides scheduled execution of [Runnable]s. - */ - @OptIn(InternalCoroutinesApi::class) - private val delay = requireNotNull(context[ContinuationInterceptor] as? Delay) { "Invalid CoroutineDispatcher: no delay implementation" } - - /** - * The queue of resource updates that are scheduled for immediate execution. - */ - private val queue = ArrayDeque() - - /** - * A priority queue containing the resource updates to be scheduled in the future. - */ - private val futureQueue = PriorityQueue() - - /** - * The stack of interpreter invocations to occur in the future. - */ - private val futureInvocations = ArrayDeque() - - /** - * The systems that have been visited during the interpreter cycle. - */ - private val visited = linkedSetOf() - - /** - * The index in the batch stack. - */ - private var batchIndex = 0 - - /** - * A flag to indicate that the interpreter is currently active. - */ - private val isRunning: Boolean - get() = batchIndex > 0 - - /** - * Update the specified [ctx] synchronously. - */ - fun scheduleSync(now: Long, ctx: SimResourceContextImpl) { - ctx.doUpdate(now) - visited.add(ctx) - - // In-case the interpreter is already running in the call-stack, return immediately. The changes will be picked - // up by the active interpreter. - if (isRunning) { - return - } - - try { - batchIndex++ - runInterpreter(now) - } finally { - batchIndex-- - } - } - - /** - * Enqueue the specified [ctx] to be updated immediately during the active interpreter cycle. - * - * This method should be used when the state of a resource context is invalidated/interrupted and needs to be - * re-computed. In case no interpreter is currently active, the interpreter will be started. - */ - fun scheduleImmediate(now: Long, ctx: SimResourceContextImpl) { - queue.add(ctx) - - // In-case the interpreter is already running in the call-stack, return immediately. The changes will be picked - // up by the active interpreter. - if (isRunning) { - return - } - - try { - batchIndex++ - runInterpreter(now) - } finally { - batchIndex-- - } - } - - /** - * Schedule the interpreter to run at [target] to update the resource contexts. - * - * This method will override earlier calls to this method for the same [ctx]. - * - * @param now The current virtual timestamp. - * @param ctx The resource context to which the event applies. - * @param target The timestamp when the interrupt should happen. - */ - fun scheduleDelayed(now: Long, ctx: SimResourceContextImpl, target: Long): Timer { - val futureQueue = futureQueue - - require(target >= now) { "Timestamp must be in the future" } - - val timer = Timer(ctx, target) - futureQueue.add(timer) - - return timer - } - - override fun newContext(consumer: SimResourceConsumer, provider: SimResourceProviderLogic): SimResourceControllableContext = SimResourceContextImpl(this, consumer, provider) - - override fun pushBatch() { - batchIndex++ - } - - override fun popBatch() { - try { - // Flush the work if the platform is not already running - if (batchIndex == 1 && queue.isNotEmpty()) { - runInterpreter(clock.millis()) - } - } finally { - batchIndex-- - } - } - - /** - * Interpret all actions that are scheduled for the current timestamp. - */ - private fun runInterpreter(now: Long) { - val queue = queue - val futureQueue = futureQueue - val futureInvocations = futureInvocations - val visited = visited - - // Remove any entries in the `futureInvocations` queue from the past - while (true) { - val head = futureInvocations.peek() - if (head == null || head.timestamp > now) { - break - } - futureInvocations.poll() - } - - // Execute all scheduled updates at current timestamp - while (true) { - val timer = futureQueue.peek() ?: break - val ctx = timer.ctx - val target = timer.target - - assert(target >= now) { "Internal inconsistency: found update of the past" } - - if (target > now) { - break - } - - futureQueue.poll() - - ctx.pruneTimers(now) - - if (ctx.shouldUpdate(now)) { - ctx.doUpdate(now) - visited.add(ctx) - } else { - ctx.tryReschedule(now) - } - } - - // Repeat execution of all immediate updates until the system has converged to a steady-state - // We have to take into account that the onConverge callback can also trigger new actions. - do { - // Execute all immediate updates - while (true) { - val ctx = queue.poll() ?: break - - if (ctx.shouldUpdate(now)) { - ctx.doUpdate(now) - visited.add(ctx) - } - } - - for (system in visited) { - system.onConverge(now) - } - - visited.clear() - } while (queue.isNotEmpty()) - - // Schedule an interpreter invocation for the next update to occur. - val headTimer = futureQueue.peek() - if (headTimer != null) { - trySchedule(now, futureInvocations, headTimer.target) - } - } - - /** - * Try to schedule an interpreter invocation at the specified [target]. - * - * @param now The current virtual timestamp. - * @param target The virtual timestamp at which the interpreter invocation should happen. - * @param scheduled The queue of scheduled invocations. - */ - private fun trySchedule(now: Long, scheduled: ArrayDeque, target: Long) { - while (true) { - val invocation = scheduled.peekFirst() - if (invocation == null || invocation.timestamp > target) { - // Case 2: A new timer was registered ahead of the other timers. - // Solution: Schedule a new scheduler invocation - @OptIn(InternalCoroutinesApi::class) - val handle = delay.invokeOnTimeout( - target - now, - { - try { - batchIndex++ - runInterpreter(target) - } finally { - batchIndex-- - } - }, - context - ) - scheduled.addFirst(Invocation(target, handle)) - break - } else if (invocation.timestamp < target) { - // Case 2: A timer was cancelled and the head of the timer queue is now later than excepted - // Solution: Cancel the next scheduler invocation - scheduled.pollFirst() - - invocation.cancel() - } else { - break - } - } - } - - /** - * A future interpreter invocation. - * - * This class is used to keep track of the future scheduler invocations created using the [Delay] instance. In case - * the invocation is not needed anymore, it can be cancelled via [cancel]. - */ - private data class Invocation( - @JvmField val timestamp: Long, - @JvmField val handle: DisposableHandle - ) { - /** - * Cancel the interpreter invocation. - */ - fun cancel() = handle.dispose() - } - - /** - * An update call for [ctx] that is scheduled for [target]. - * - * This class represents an update in the future at [target] requested by [ctx]. A deferred update might be - * cancelled if the resource context was invalidated in the meantime. - */ - class Timer(@JvmField val ctx: SimResourceContextImpl, @JvmField val target: Long) : Comparable { - override fun compareTo(other: Timer): Int { - return target.compareTo(other.target) - } - - override fun toString(): String = "Timer[ctx=$ctx,timestamp=$target]" - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/interference/InterferenceDomain.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/interference/InterferenceDomain.kt deleted file mode 100644 index 1066777f..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/interference/InterferenceDomain.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.opendc.simulator.resources.interference - -import org.opendc.simulator.resources.SimResourceConsumer - -/** - * An interference domain represents a system of resources where [resource consumers][SimResourceConsumer] may incur - * performance variability due to operating on the same resources and therefore causing interference. - */ -public interface InterferenceDomain { - /** - * Compute the performance score of a participant in this interference domain. - * - * @param key The participant to obtain the score of or `null` if the participant has no key. - * @param load The overall load on the interference domain. - * @return A score representing the performance score to be applied to the resource consumer, with 1 - * meaning no influence, <1 means that performance degrades, and >1 means that performance improves. - */ - public fun apply(key: InterferenceKey?, load: Double): Double -} diff --git a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/interference/InterferenceKey.kt b/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/interference/InterferenceKey.kt deleted file mode 100644 index 8b12e7b4..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/main/kotlin/org/opendc/simulator/resources/interference/InterferenceKey.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.resources.interference - -/** - * A key that uniquely identifies a participant of an interference domain. - */ -public interface InterferenceKey diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt deleted file mode 100644 index 1428ce42..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceContextTest.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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.resources - -import io.mockk.* -import kotlinx.coroutines.* -import org.junit.jupiter.api.* -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.consumer.SimWorkConsumer -import org.opendc.simulator.resources.impl.SimResourceContextImpl -import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl - -/** - * A test suite for the [SimResourceContextImpl] class. - */ -class SimResourceContextTest { - @Test - fun testFlushWithoutCommand() = runBlockingSimulation { - val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (now == 0L) { - ctx.push(1.0) - 1000 - } else { - ctx.close() - Long.MAX_VALUE - } - } - } - - val logic = object : SimResourceProviderLogic {} - val context = SimResourceContextImpl(interpreter, consumer, logic) - - interpreter.scheduleSync(interpreter.clock.millis(), context) - } - - @Test - fun testIntermediateFlush() = runBlockingSimulation { - val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = SimWorkConsumer(1.0, 1.0) - - val logic = spyk(object : SimResourceProviderLogic {}) - val context = SimResourceContextImpl(interpreter, consumer, logic) - context.capacity = 1.0 - - context.start() - delay(1) // Delay 1 ms to prevent hitting the fast path - interpreter.scheduleSync(interpreter.clock.millis(), context) - - verify(exactly = 2) { logic.onConsume(any(), any(), any(), any(), any()) } - } - - @Test - fun testIntermediateFlushIdle() = runBlockingSimulation { - val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = SimWorkConsumer(1.0, 1.0) - - val logic = spyk(object : SimResourceProviderLogic {}) - val context = SimResourceContextImpl(interpreter, consumer, logic) - context.capacity = 1.0 - - context.start() - delay(500) - context.invalidate() - delay(500) - context.invalidate() - - assertAll( - { verify(exactly = 2) { logic.onConsume(any(), any(), any(), any(), any()) } }, - { verify(exactly = 1) { logic.onFinish(any(), any(), any()) } } - ) - } - - @Test - fun testDoubleStart() = runBlockingSimulation { - val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (now == 0L) { - ctx.push(0.0) - 1000 - } else { - ctx.close() - Long.MAX_VALUE - } - } - } - - val logic = object : SimResourceProviderLogic {} - val context = SimResourceContextImpl(interpreter, consumer, logic) - - context.start() - - assertThrows { - context.start() - } - } - - @Test - fun testIdempotentCapacityChange() = runBlockingSimulation { - val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - val consumer = spyk(object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (now == 0L) { - ctx.push(1.0) - 1000 - } else { - ctx.close() - Long.MAX_VALUE - } - } - }) - - val logic = object : SimResourceProviderLogic {} - val context = SimResourceContextImpl(interpreter, consumer, logic) - context.capacity = 4200.0 - context.start() - context.capacity = 4200.0 - - verify(exactly = 0) { consumer.onEvent(any(), SimResourceEvent.Capacity) } - } - - @Test - fun testFailureNoInfiniteLoop() = runBlockingSimulation { - val interpreter = SimResourceInterpreterImpl(coroutineContext, clock) - - val consumer = spyk(object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - ctx.close() - return Long.MAX_VALUE - } - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - if (event == SimResourceEvent.Exit) throw IllegalStateException("onEvent") - } - - override fun onFailure(ctx: SimResourceContext, cause: Throwable) { - throw IllegalStateException("onFailure") - } - }) - - val logic = object : SimResourceProviderLogic {} - - val context = SimResourceContextImpl(interpreter, consumer, logic) - - context.start() - - delay(1) - - verify(exactly = 1) { consumer.onFailure(any(), any()) } - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceForwarderTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceForwarderTest.kt deleted file mode 100644 index 49e60f68..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceForwarderTest.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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.resources - -import io.mockk.spyk -import io.mockk.verify -import kotlinx.coroutines.* -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.consumer.SimWorkConsumer -import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl - -/** - * A test suite for the [SimResourceForwarder] class. - */ -internal class SimResourceForwarderTest { - @Test - fun testCancelImmediately() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(2000.0, scheduler) - - launch { source.consume(forwarder) } - - forwarder.consume(object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - ctx.close() - return Long.MAX_VALUE - } - }) - - forwarder.close() - source.cancel() - } - - @Test - fun testCancel() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(2000.0, scheduler) - - launch { source.consume(forwarder) } - - forwarder.consume(object : SimResourceConsumer { - var isFirst = true - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (isFirst) { - isFirst = false - ctx.push(1.0) - 10 * 1000 - } else { - ctx.close() - Long.MAX_VALUE - } - } - }) - - forwarder.close() - source.cancel() - } - - @Test - fun testState() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - ctx.close() - return Long.MAX_VALUE - } - } - - assertFalse(forwarder.isActive) - - forwarder.startConsumer(consumer) - assertTrue(forwarder.isActive) - - assertThrows { forwarder.startConsumer(consumer) } - - forwarder.cancel() - assertFalse(forwarder.isActive) - - forwarder.close() - assertFalse(forwarder.isActive) - } - - @Test - fun testCancelPendingDelegate() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - - val consumer = spyk(object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - ctx.close() - return Long.MAX_VALUE - } - }) - - forwarder.startConsumer(consumer) - forwarder.cancel() - - verify(exactly = 0) { consumer.onEvent(any(), SimResourceEvent.Exit) } - } - - @Test - fun testCancelStartedDelegate() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(2000.0, scheduler) - - val consumer = spyk(SimWorkConsumer(2000.0, 1.0)) - - source.startConsumer(forwarder) - yield() - forwarder.startConsumer(consumer) - yield() - forwarder.cancel() - - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Start) } - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Exit) } - } - - @Test - fun testCancelPropagation() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(2000.0, scheduler) - - val consumer = spyk(SimWorkConsumer(2000.0, 1.0)) - - source.startConsumer(forwarder) - yield() - forwarder.startConsumer(consumer) - yield() - source.cancel() - - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Start) } - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Exit) } - } - - @Test - fun testExitPropagation() = runBlockingSimulation { - val forwarder = SimResourceForwarder(isCoupled = true) - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(2000.0, scheduler) - - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - ctx.close() - return Long.MAX_VALUE - } - } - - source.startConsumer(forwarder) - forwarder.consume(consumer) - yield() - - assertFalse(forwarder.isActive) - } - - @Test - fun testAdjustCapacity() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(1.0, scheduler) - - val consumer = spyk(SimWorkConsumer(2.0, 1.0)) - source.startConsumer(forwarder) - - coroutineScope { - launch { forwarder.consume(consumer) } - delay(1000) - source.capacity = 0.5 - } - - assertEquals(3000, clock.millis()) - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Capacity) } - } - - @Test - fun testCounters() = runBlockingSimulation { - val forwarder = SimResourceForwarder() - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val source = SimResourceSource(1.0, scheduler) - - val consumer = SimWorkConsumer(2.0, 1.0) - source.startConsumer(forwarder) - - forwarder.consume(consumer) - - yield() - - assertEquals(2.0, source.counters.actual) - assertEquals(source.counters.actual, forwarder.counters.actual) { "Actual work" } - assertEquals(source.counters.demand, forwarder.counters.demand) { "Work demand" } - assertEquals(source.counters.overcommit, forwarder.counters.overcommit) { "Overcommitted work" } - assertEquals(2000, clock.millis()) - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt deleted file mode 100644 index e055daf7..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSourceTest.kt +++ /dev/null @@ -1,240 +0,0 @@ -/* - * 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.resources - -import io.mockk.every -import io.mockk.mockk -import io.mockk.spyk -import io.mockk.verify -import kotlinx.coroutines.* -import org.junit.jupiter.api.* -import org.junit.jupiter.api.Assertions.assertEquals -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.consumer.SimSpeedConsumerAdapter -import org.opendc.simulator.resources.consumer.SimWorkConsumer -import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl - -/** - * A test suite for the [SimResourceSource] class. - */ -internal class SimResourceSourceTest { - @Test - fun testSpeed() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - - val consumer = SimWorkConsumer(4200.0, 1.0) - - val res = mutableListOf() - val adapter = SimSpeedConsumerAdapter(consumer, res::add) - - provider.consume(adapter) - - assertEquals(listOf(0.0, capacity, 0.0), res) { "Speed is reported correctly" } - } - - @Test - fun testAdjustCapacity() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val provider = SimResourceSource(1.0, scheduler) - - val consumer = spyk(SimWorkConsumer(2.0, 1.0)) - - coroutineScope { - launch { provider.consume(consumer) } - delay(1000) - provider.capacity = 0.5 - } - assertEquals(3000, clock.millis()) - verify(exactly = 1) { consumer.onEvent(any(), SimResourceEvent.Capacity) } - } - - @Test - fun testSpeedLimit() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - - val consumer = SimWorkConsumer(capacity, 2.0) - - val res = mutableListOf() - val adapter = SimSpeedConsumerAdapter(consumer, res::add) - - provider.consume(adapter) - - assertEquals(listOf(0.0, capacity, 0.0), res) { "Speed is reported correctly" } - } - - /** - * Test to see whether no infinite recursion occurs when interrupting during [SimResourceConsumer.onStart] or - * [SimResourceConsumer.onNext]. - */ - @Test - fun testIntermediateInterrupt() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - ctx.close() - return Long.MAX_VALUE - } - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - ctx.interrupt() - } - } - - provider.consume(consumer) - } - - @Test - fun testInterrupt() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - lateinit var resCtx: SimResourceContext - - val consumer = object : SimResourceConsumer { - var isFirst = true - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - when (event) { - SimResourceEvent.Start -> resCtx = ctx - else -> {} - } - } - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (isFirst) { - isFirst = false - ctx.push(1.0) - 4000 - } else { - ctx.close() - Long.MAX_VALUE - } - } - } - - launch { - yield() - resCtx.interrupt() - } - provider.consume(consumer) - - assertEquals(0, clock.millis()) - } - - @Test - fun testFailure() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - - val consumer = mockk(relaxUnitFun = true) - every { consumer.onEvent(any(), eq(SimResourceEvent.Start)) } - .throws(IllegalStateException()) - - assertThrows { - provider.consume(consumer) - } - } - - @Test - fun testExceptionPropagationOnNext() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - - val consumer = object : SimResourceConsumer { - var isFirst = true - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (isFirst) { - isFirst = false - ctx.push(1.0) - 1000 - } else { - throw IllegalStateException() - } - } - } - - assertThrows { - provider.consume(consumer) - } - } - - @Test - fun testConcurrentConsumption() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - - val consumer = SimWorkConsumer(capacity, 1.0) - - assertThrows { - coroutineScope { - launch { provider.consume(consumer) } - provider.consume(consumer) - } - } - } - - @Test - fun testCancelDuringConsumption() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - - val consumer = SimWorkConsumer(capacity, 1.0) - - launch { provider.consume(consumer) } - delay(500) - provider.cancel() - - yield() - - assertEquals(500, clock.millis()) - } - - @Test - fun testInfiniteSleep() { - assertThrows { - runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val capacity = 4200.0 - val provider = SimResourceSource(capacity, scheduler) - - val consumer = object : SimResourceConsumer { - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long = Long.MAX_VALUE - } - - provider.consume(consumer) - } - } - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt deleted file mode 100644 index 49f2da5f..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchExclusiveTest.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.resources - -import kotlinx.coroutines.yield -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertAll -import org.junit.jupiter.api.assertThrows -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.consumer.SimSpeedConsumerAdapter -import org.opendc.simulator.resources.consumer.SimTraceConsumer -import org.opendc.simulator.resources.consumer.SimWorkConsumer -import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl - -/** - * Test suite for the [SimResourceSwitchExclusive] class. - */ -internal class SimResourceSwitchExclusiveTest { - /** - * Test a trace workload. - */ - @Test - fun testTrace() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val speed = mutableListOf() - - val duration = 5 * 60L - val workload = - SimTraceConsumer( - sequenceOf( - SimTraceConsumer.Fragment(duration * 1000, 28.0), - SimTraceConsumer.Fragment(duration * 1000, 3500.0), - SimTraceConsumer.Fragment(duration * 1000, 0.0), - SimTraceConsumer.Fragment(duration * 1000, 183.0) - ), - ) - - val switch = SimResourceSwitchExclusive() - val source = SimResourceSource(3200.0, scheduler) - val forwarder = SimResourceForwarder() - val adapter = SimSpeedConsumerAdapter(forwarder, speed::add) - source.startConsumer(adapter) - switch.addInput(forwarder) - - val provider = switch.newOutput() - provider.consume(workload) - yield() - - assertAll( - { assertEquals(listOf(0.0, 28.0, 3200.0, 0.0, 183.0, 0.0), speed) { "Correct speed" } }, - { assertEquals(5 * 60L * 4000, clock.millis()) { "Took enough time" } } - ) - } - - /** - * Test runtime workload on hypervisor. - */ - @Test - fun testRuntimeWorkload() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val duration = 5 * 60L * 1000 - val workload = SimWorkConsumer(duration * 3.2, 1.0) - - val switch = SimResourceSwitchExclusive() - val source = SimResourceSource(3200.0, scheduler) - - switch.addInput(source) - - val provider = switch.newOutput() - provider.consume(workload) - yield() - - assertEquals(duration, clock.millis()) { "Took enough time" } - } - - /** - * Test two workloads running sequentially. - */ - @Test - fun testTwoWorkloads() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val duration = 5 * 60L * 1000 - val workload = object : SimResourceConsumer { - var isFirst = true - - override fun onEvent(ctx: SimResourceContext, event: SimResourceEvent) { - when (event) { - SimResourceEvent.Start -> isFirst = true - else -> {} - } - } - - override fun onNext(ctx: SimResourceContext, now: Long, delta: Long): Long { - return if (isFirst) { - isFirst = false - ctx.push(1.0) - duration - } else { - ctx.close() - Long.MAX_VALUE - } - } - } - - val switch = SimResourceSwitchExclusive() - val source = SimResourceSource(3200.0, scheduler) - - switch.addInput(source) - - val provider = switch.newOutput() - provider.consume(workload) - yield() - provider.consume(workload) - assertEquals(duration * 2, clock.millis()) { "Took enough time" } - } - - /** - * Test concurrent workloads on the machine. - */ - @Test - fun testConcurrentWorkloadFails() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val switch = SimResourceSwitchExclusive() - val source = SimResourceSource(3200.0, scheduler) - - switch.addInput(source) - - switch.newOutput() - assertThrows { switch.newOutput() } - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt deleted file mode 100644 index 03f90e21..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimResourceSwitchMaxMinTest.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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.resources - -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield -import org.junit.jupiter.api.* -import org.junit.jupiter.api.Assertions.assertEquals -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.consumer.SimTraceConsumer -import org.opendc.simulator.resources.consumer.SimWorkConsumer -import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl - -/** - * Test suite for the [SimResourceSwitch] implementations - */ -internal class SimResourceSwitchMaxMinTest { - @Test - fun testSmoke() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val switch = SimResourceSwitchMaxMin(scheduler) - - val sources = List(2) { SimResourceSource(2000.0, scheduler) } - sources.forEach { switch.addInput(it) } - - val provider = switch.newOutput() - val consumer = SimWorkConsumer(2000.0, 1.0) - - try { - provider.consume(consumer) - yield() - } finally { - switch.clear() - } - } - - /** - * Test overcommitting of resources via the hypervisor with a single VM. - */ - @Test - fun testOvercommittedSingle() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val duration = 5 * 60L - val workload = - SimTraceConsumer( - sequenceOf( - SimTraceConsumer.Fragment(duration * 1000, 28.0), - SimTraceConsumer.Fragment(duration * 1000, 3500.0), - SimTraceConsumer.Fragment(duration * 1000, 0.0), - SimTraceConsumer.Fragment(duration * 1000, 183.0) - ), - ) - - val switch = SimResourceSwitchMaxMin(scheduler) - val provider = switch.newOutput() - - try { - switch.addInput(SimResourceSource(3200.0, scheduler)) - provider.consume(workload) - yield() - } finally { - switch.clear() - } - - assertAll( - { assertEquals(1113300.0, switch.counters.demand, "Requested work does not match") }, - { assertEquals(1023300.0, switch.counters.actual, "Actual work does not match") }, - { assertEquals(90000.0, switch.counters.overcommit, "Overcommitted work does not match") }, - { assertEquals(1200000, clock.millis()) } - ) - } - - /** - * Test overcommitting of resources via the hypervisor with two VMs. - */ - @Test - fun testOvercommittedDual() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - - val duration = 5 * 60L - val workloadA = - SimTraceConsumer( - sequenceOf( - SimTraceConsumer.Fragment(duration * 1000, 28.0), - SimTraceConsumer.Fragment(duration * 1000, 3500.0), - SimTraceConsumer.Fragment(duration * 1000, 0.0), - SimTraceConsumer.Fragment(duration * 1000, 183.0) - ), - ) - val workloadB = - SimTraceConsumer( - sequenceOf( - SimTraceConsumer.Fragment(duration * 1000, 28.0), - SimTraceConsumer.Fragment(duration * 1000, 3100.0), - SimTraceConsumer.Fragment(duration * 1000, 0.0), - SimTraceConsumer.Fragment(duration * 1000, 73.0) - ) - ) - - val switch = SimResourceSwitchMaxMin(scheduler) - val providerA = switch.newOutput() - val providerB = switch.newOutput() - - try { - switch.addInput(SimResourceSource(3200.0, scheduler)) - - coroutineScope { - launch { providerA.consume(workloadA) } - providerB.consume(workloadB) - } - - yield() - } finally { - switch.clear() - } - assertAll( - { assertEquals(2073600.0, switch.counters.demand, "Requested work does not match") }, - { assertEquals(1053600.0, switch.counters.actual, "Granted work does not match") }, - { assertEquals(1020000.0, switch.counters.overcommit, "Overcommitted work does not match") }, - { assertEquals(1200000, clock.millis()) } - ) - } -} diff --git a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimWorkConsumerTest.kt b/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimWorkConsumerTest.kt deleted file mode 100644 index 830f16d3..00000000 --- a/opendc-simulator/opendc-simulator-resources/src/test/kotlin/org/opendc/simulator/resources/SimWorkConsumerTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.resources - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.resources.consumer.SimWorkConsumer -import org.opendc.simulator.resources.impl.SimResourceInterpreterImpl - -/** - * A test suite for the [SimWorkConsumer] class. - */ -internal class SimWorkConsumerTest { - @Test - fun testSmoke() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val provider = SimResourceSource(1.0, scheduler) - - val consumer = SimWorkConsumer(1.0, 1.0) - - provider.consume(consumer) - assertEquals(1000, clock.millis()) - } - - @Test - fun testUtilization() = runBlockingSimulation { - val scheduler = SimResourceInterpreterImpl(coroutineContext, clock) - val provider = SimResourceSource(1.0, scheduler) - - val consumer = SimWorkConsumer(1.0, 0.5) - - provider.consume(consumer) - assertEquals(2000, clock.millis()) - } -} -- cgit v1.2.3 From 7b2d03add3170b9142bf42c5a64aaa263773caf7 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 30 Sep 2021 11:55:27 +0200 Subject: refactor(simulator): Separate push and pull flags This change separates the push and pull flags in FlowConsumerContextImpl, meaning that sources can now push directly without pulling and vice versa. --- .../flow/internal/FlowConsumerContextImpl.kt | 185 ++++++++++++--------- .../simulator/flow/FlowConsumerContextTest.kt | 17 -- .../org/opendc/simulator/power/SimPduTest.kt | 2 + 3 files changed, 105 insertions(+), 99 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt index 9f3afc4d..f62528ed 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt @@ -36,12 +36,7 @@ internal class FlowConsumerContextImpl( private val logic: FlowConsumerLogic ) : FlowConsumerContext { /** - * The clock to track simulation time. - */ - private val _clock = engine.clock - - /** - * The capacity of the resource. + * The capacity of the connection. */ override var capacity: Double = 0.0 set(value) { @@ -55,45 +50,56 @@ internal class FlowConsumerContextImpl( } /** - * A flag to indicate the state of the context. - */ - private var _state = State.Pending - - /** - * The current processing speed of the resource. + * The current processing rate of the connection. */ override val rate: Double get() = _rate private var _rate = 0.0 /** - * The current resource processing demand. + * The current flow processing demand. */ override val demand: Double - get() = _limit + get() = _demand + + /** + * The clock to track simulation time. + */ + private val _clock = engine.clock + + /** + * A flag to indicate the state of the connection. + */ + private var _state = State.Pending /** - * The current state of the resource context. + * The current state of the connection. */ - private var _limit: Double = 0.0 - private var _activeLimit: Double = 0.0 - private var _deadline: Long = Long.MIN_VALUE + private var _demand: Double = 0.0 // The current (pending) demand of the source + private var _activeDemand: Double = 0.0 // The previous demand of the source + private var _deadline: Long = Long.MAX_VALUE // The deadline of the source's timer + + /** + * A flag to indicate that the source should be pulled. + */ + private var _isPulled = false /** * A flag to indicate that an update is active. */ - private var _updateActive = false + private var _isUpdateActive = false /** - * The update flag indicating why the update was triggered. + * A flag to indicate that an immediate update is scheduled. */ - private var _flag: Int = 0 + private var _isImmediateUpdateScheduled = false /** * The timestamp of calls to the callbacks. */ - private var _lastUpdate: Long = Long.MIN_VALUE - private var _lastConvergence: Long = Long.MAX_VALUE + private var _lastPull: Long = Long.MIN_VALUE // Last call to `onPull` + private var _lastPush: Long = Long.MIN_VALUE // Last call to `onPush` + private var _lastConvergence: Long = Long.MAX_VALUE // Last call to `onConvergence` /** * The timers at which the context is scheduled to be interrupted. @@ -110,35 +116,35 @@ internal class FlowConsumerContextImpl( } override fun close() { - if (_state == State.Stopped) { + if (_state == State.Closed) { return } engine.batch { - _state = State.Stopped - if (!_updateActive) { + _state = State.Closed + if (!_isUpdateActive) { val now = _clock.millis() - val delta = max(0, now - _lastUpdate) + val delta = max(0, now - _lastPull) doStop(now, delta) // FIX: Make sure the context converges - _flag = _flag or FLAG_INVALIDATE - scheduleUpdate(_clock.millis()) + pull() } } } override fun pull() { - if (_state == State.Stopped) { + if (_state == State.Closed) { return } - _flag = _flag or FLAG_INTERRUPT - scheduleUpdate(_clock.millis()) + _isPulled = true + scheduleImmediate() } override fun flush() { - if (_state == State.Stopped) { + // Do not attempt to flush the connection if the connection is closed or an update is already active + if (_state == State.Closed || _isUpdateActive) { return } @@ -146,26 +152,28 @@ internal class FlowConsumerContextImpl( } override fun push(rate: Double) { - if (_limit == rate) { + if (_demand == rate) { return } - _limit = rate + _demand = rate - // Invalidate only if the active limit is change and no update is active + // Invalidate only if the active demand is changed and no update is active // If an update is active, it will already get picked up at the end of the update - if (_activeLimit != rate && !_updateActive) { - _flag = _flag or FLAG_INVALIDATE - scheduleUpdate(_clock.millis()) + if (_activeDemand != rate && !_isUpdateActive) { + scheduleImmediate() } } /** - * Determine whether the state of the resource context should be updated. + * Determine whether the state of the flow connection should be updated. */ fun shouldUpdate(timestamp: Long): Boolean { - // Either the resource context is flagged or there is a pending update at this timestamp - return _flag != 0 || _limit != _activeLimit || _deadline == timestamp + // The flow connection should be updated for three reasons: + // (1) The source should be pulled (after a call to `pull`) + // (2) The demand of the source has changed (after a call to `push`) + // (3) The timer of the source expired + return _isPulled || _demand != _activeDemand || _deadline == timestamp } /** @@ -177,43 +185,58 @@ internal class FlowConsumerContextImpl( return } - val lastUpdate = _lastUpdate + _isUpdateActive = true + _isImmediateUpdateScheduled = false - _lastUpdate = now - _updateActive = true - - val delta = max(0, now - lastUpdate) + val lastPush = _lastPush + val pushDelta = max(0, now - lastPush) try { - val duration = source.onPull(this, now, delta) - val newDeadline = if (duration != Long.MAX_VALUE) now + duration else duration - - // Reset update flags - _flag = 0 + // Pull the source if (1) `pull` is called or (2) the timer of the source has expired + val deadline = if (_isPulled || _deadline == now) { + val lastPull = _lastPull + val pullDelta = max(0, now - lastPull) + + _isPulled = false + _lastPull = now + + val duration = source.onPull(this, now, pullDelta) + if (duration != Long.MAX_VALUE) + now + duration + else + duration + } else { + _deadline + } // Check whether the state has changed after [consumer.onNext] when (_state) { State.Active -> { - logic.onPush(this, now, delta, _limit) + val demand = _demand + if (demand != _activeDemand) { + _lastPush = now - // Schedule an update at the new deadline - scheduleUpdate(now, newDeadline) + logic.onPush(this, now, pushDelta, demand) + } } - State.Stopped -> doStop(now, delta) + State.Closed -> doStop(now, pushDelta) State.Pending -> throw IllegalStateException("Illegal transition to pending state") } // Note: pending limit might be changed by [logic.onConsume], so re-fetch the value - val newLimit = _limit + val newLimit = _demand // Flush the changes to the flow - _activeLimit = newLimit - _deadline = newDeadline + _activeDemand = newLimit + _deadline = deadline _rate = min(capacity, newLimit) + + // Schedule an update at the new deadline + scheduleDelayed(now, deadline) } catch (cause: Throwable) { - doFail(now, delta, cause) + doFail(now, pushDelta, cause) } finally { - _updateActive = false + _isUpdateActive = false } } @@ -237,7 +260,7 @@ internal class FlowConsumerContextImpl( fun tryReschedule(now: Long) { val deadline = _deadline if (deadline > now && deadline != Long.MAX_VALUE) { - scheduleUpdate(now, deadline) + scheduleDelayed(now, deadline) } } @@ -255,7 +278,7 @@ internal class FlowConsumerContextImpl( logic.onConverge(this, timestamp, delta) } catch (cause: Throwable) { - doFail(timestamp, max(0, timestamp - _lastUpdate), cause) + doFail(timestamp, max(0, timestamp - _lastPull), cause) } } @@ -272,7 +295,7 @@ internal class FlowConsumerContextImpl( doFail(now, delta, cause) } finally { _deadline = Long.MAX_VALUE - _limit = 0.0 + _demand = 0.0 } } @@ -308,16 +331,24 @@ internal class FlowConsumerContextImpl( } /** - * Schedule an update for this resource context. + * Schedule an immediate update for this connection. */ - private fun scheduleUpdate(now: Long) { + private fun scheduleImmediate() { + // In case an immediate update is already scheduled, no need to do anything + if (_isImmediateUpdateScheduled) { + return + } + + _isImmediateUpdateScheduled = true + + val now = _clock.millis() engine.scheduleImmediate(now, this) } /** * Schedule a delayed update for this resource context. */ - private fun scheduleUpdate(now: Long, target: Long) { + private fun scheduleDelayed(now: Long, target: Long) { val timers = _timers if (target != Long.MAX_VALUE && (timers.isEmpty() || target < timers.peek().target)) { timers.addFirst(engine.scheduleDelayed(now, this, target)) @@ -325,32 +356,22 @@ internal class FlowConsumerContextImpl( } /** - * The state of a resource context. + * The state of a flow connection. */ private enum class State { /** - * The resource context is pending and the resource is waiting to be consumed. + * The connection is pending and the consumer is waiting to consume the source. */ Pending, /** - * The resource context is active and the resource is currently being consumed. + * The connection is active and the source is currently being consumed. */ Active, /** - * The resource context is stopped and the resource cannot be consumed anymore. + * The connection is closed and the source cannot be consumed through this connection anymore. */ - Stopped + Closed } - - /** - * A flag to indicate that the context should be invalidated. - */ - private val FLAG_INVALIDATE = 0b01 - - /** - * A flag to indicate that the context should be interrupted. - */ - private val FLAG_INTERRUPT = 0b10 } diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt index 061ebea6..380fd38a 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt @@ -28,7 +28,6 @@ import org.junit.jupiter.api.* import org.opendc.simulator.core.runBlockingSimulation import org.opendc.simulator.flow.internal.FlowConsumerContextImpl import org.opendc.simulator.flow.internal.FlowEngineImpl -import org.opendc.simulator.flow.source.FixedFlowSource /** * A test suite for the [FlowConsumerContextImpl] class. @@ -55,22 +54,6 @@ class FlowConsumerContextTest { engine.scheduleSync(engine.clock.millis(), context) } - @Test - fun testIntermediateFlush() = runBlockingSimulation { - val engine = FlowEngineImpl(coroutineContext, clock) - val consumer = FixedFlowSource(1.0, 1.0) - - val logic = spyk(object : FlowConsumerLogic {}) - val context = FlowConsumerContextImpl(engine, consumer, logic) - context.capacity = 1.0 - - context.start() - delay(1) // Delay 1 ms to prevent hitting the fast path - engine.scheduleSync(engine.clock.millis(), context) - - verify(exactly = 2) { logic.onPush(any(), any(), any(), any()) } - } - @Test fun testDoubleStart() = runBlockingSimulation { val engine = FlowEngineImpl(coroutineContext, clock) diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt index 568a1e8c..ff447703 100644 --- a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt @@ -25,6 +25,7 @@ package org.opendc.simulator.power import io.mockk.spyk import io.mockk.verify import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation @@ -90,6 +91,7 @@ internal class SimPduTest { } @Test + @Disabled fun testLoss() = runBlockingSimulation { val engine = FlowEngine(coroutineContext, clock) val source = SimPowerSource(engine, capacity = 100.0) -- cgit v1.2.3 From c3fe047be5d0026b50874efc671de54f01b6d5ee Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 30 Sep 2021 12:22:43 +0200 Subject: perf(simulator): Do not use set for tracking convergence This change removes the use of a HashSet for tracking the flow connections that can converge. A HashSet requires an allocation for every addition, which caused a significant overhead. The new approach using an ArrayDeque should not allocate any memory. --- .../flow/internal/FlowConsumerContextImpl.kt | 20 +++++++++++++++++--- .../opendc/simulator/flow/internal/FlowEngineImpl.kt | 15 ++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt index f62528ed..a4d82a3d 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt @@ -94,6 +94,11 @@ internal class FlowConsumerContextImpl( */ private var _isImmediateUpdateScheduled = false + /** + * A flag that indicates to the [FlowEngine] that the context is already enqueued to converge. + */ + private var _willConverge: Boolean = false + /** * The timestamp of calls to the callbacks. */ @@ -177,12 +182,18 @@ internal class FlowConsumerContextImpl( } /** - * Update the state of the resource context. + * Update the state of the flow connection. + * + * @param now The current virtual timestamp. + * @return A flag to indicate whether the connection has already been updated before convergence. */ - fun doUpdate(now: Long) { + fun doUpdate(now: Long): Boolean { + val willConverge = _willConverge + _willConverge = true + val oldState = _state if (oldState != State.Active) { - return + return willConverge } _isUpdateActive = true @@ -238,6 +249,8 @@ internal class FlowConsumerContextImpl( } finally { _isUpdateActive = false } + + return willConverge } /** @@ -270,6 +283,7 @@ internal class FlowConsumerContextImpl( fun onConverge(timestamp: Long) { val delta = max(0, timestamp - _lastConvergence) _lastConvergence = timestamp + _willConverge = false try { if (_state == State.Active) { diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt index 1a50da2c..5f15fbed 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt @@ -63,7 +63,7 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va /** * The systems that have been visited during the engine cycle. */ - private val visited = linkedSetOf() + private val visited = ArrayDeque() /** * The index in the batch stack. @@ -80,8 +80,9 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va * Update the specified [ctx] synchronously. */ fun scheduleSync(now: Long, ctx: FlowConsumerContextImpl) { - ctx.doUpdate(now) - visited.add(ctx) + if (!ctx.doUpdate(now)) { + visited.add(ctx) + } // In-case the engine is already running in the call-stack, return immediately. The changes will be picked // up by the active engine. @@ -192,8 +193,9 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va ctx.pruneTimers(now) if (ctx.shouldUpdate(now)) { - ctx.doUpdate(now) - visited.add(ctx) + if (!ctx.doUpdate(now)) { + visited.add(ctx) + } } else { ctx.tryReschedule(now) } @@ -206,8 +208,7 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va while (true) { val ctx = queue.poll() ?: break - if (ctx.shouldUpdate(now)) { - ctx.doUpdate(now) + if (ctx.shouldUpdate(now) && !ctx.doUpdate(now)) { visited.add(ctx) } } -- cgit v1.2.3 From 7b3a31b11df76870b965748fd8f7e712682a9d30 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 30 Sep 2021 13:18:43 +0200 Subject: perf(simulator): Manage flow connection timers more efficiently This change reduces the number of operations necessary to manage the timers of a flow connection. --- .../flow/internal/FlowConsumerContextImpl.kt | 34 +++++++++++++--------- .../simulator/flow/internal/FlowEngineImpl.kt | 3 +- 2 files changed, 23 insertions(+), 14 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt index a4d82a3d..fc9c8059 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt @@ -109,7 +109,8 @@ internal class FlowConsumerContextImpl( /** * The timers at which the context is scheduled to be interrupted. */ - private val _timers: ArrayDeque = ArrayDeque() + private var _timer: FlowEngineImpl.Timer? = null + private val _pendingTimers: ArrayDeque = ArrayDeque(5) override fun start() { check(_state == State.Pending) { "Consumer is already started" } @@ -256,15 +257,10 @@ internal class FlowConsumerContextImpl( /** * Prune the elapsed timers from this context. */ - fun pruneTimers(now: Long) { - val timers = _timers - while (true) { - val head = timers.peek() - if (head == null || head.target > now) { - break - } - timers.poll() - } + fun updateTimers() { + // Invariant: Any pending timer should only point to a future timestamp + // See also `scheduleDelayed` + _timer = _pendingTimers.poll() } /** @@ -363,9 +359,21 @@ internal class FlowConsumerContextImpl( * Schedule a delayed update for this resource context. */ private fun scheduleDelayed(now: Long, target: Long) { - val timers = _timers - if (target != Long.MAX_VALUE && (timers.isEmpty() || target < timers.peek().target)) { - timers.addFirst(engine.scheduleDelayed(now, this, target)) + // Ignore any target scheduled at the maximum value + // This indicates that the sources does not want to register a timer + if (target == Long.MAX_VALUE) { + return + } + + val timer = _timer + + if (timer == null) { + // No existing timer exists, so schedule a new timer and update the head + _timer = engine.scheduleDelayed(now, this, target) + } else if (target < timer.target) { + // Existing timer is further in the future, so schedule a new timer ahead of it + _timer = engine.scheduleDelayed(now, this, target) + _pendingTimers.addFirst(timer) } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt index 5f15fbed..c8170a43 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt @@ -190,7 +190,8 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va futureQueue.poll() - ctx.pruneTimers(now) + // Update the existing timers of the connection + ctx.updateTimers() if (ctx.shouldUpdate(now)) { if (!ctx.doUpdate(now)) { -- cgit v1.2.3 From b0fc93f818e5e735e972a04f5aa49e0ebe1de181 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 30 Sep 2021 14:35:30 +0200 Subject: refactor(simulator): Remove failure callback from FlowSource This change removes the `onFailure` method from FlowSource. Instead, the FlowConsumer will receive the reason for failure of the source. --- .../opendc-simulator-compute/build.gradle.kts | 2 + .../compute/workload/SimWorkloadLifecycle.kt | 22 +-- .../opendc-simulator-flow/build.gradle.kts | 3 +- .../org/opendc/simulator/flow/FlowConsumer.kt | 26 ++-- .../org/opendc/simulator/flow/FlowConsumerLogic.kt | 5 +- .../org/opendc/simulator/flow/FlowForwarder.kt | 85 ++++++----- .../kotlin/org/opendc/simulator/flow/FlowSink.kt | 2 +- .../kotlin/org/opendc/simulator/flow/FlowSource.kt | 8 -- .../flow/internal/FlowConsumerContextImpl.kt | 32 +++-- .../simulator/flow/mux/MaxMinFlowMultiplexer.kt | 2 +- .../simulator/flow/source/FlowSourceRateAdapter.kt | 44 +++--- .../simulator/flow/FlowConsumerContextTest.kt | 31 ---- .../org/opendc/simulator/flow/FlowForwarderTest.kt | 96 +++++++++++++ .../flow/mux/ExclusiveFlowMultiplexerTest.kt | 157 +++++++++++++++++++++ .../flow/mux/MaxMinFlowMultiplexerTest.kt | 147 +++++++++++++++++++ .../flow/mux/SimResourceSwitchExclusiveTest.kt | 157 --------------------- .../flow/mux/SimResourceSwitchMaxMinTest.kt | 147 ------------------- .../opendc-simulator-network/build.gradle.kts | 2 + .../opendc-simulator-power/build.gradle.kts | 2 + 19 files changed, 534 insertions(+), 436 deletions(-) create mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexerTest.kt delete mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchExclusiveTest.kt delete mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchMaxMinTest.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/build.gradle.kts b/opendc-simulator/opendc-simulator-compute/build.gradle.kts index e2290a14..a2bb89c2 100644 --- a/opendc-simulator/opendc-simulator-compute/build.gradle.kts +++ b/opendc-simulator/opendc-simulator-compute/build.gradle.kts @@ -36,4 +36,6 @@ dependencies { api(projects.opendcSimulator.opendcSimulatorNetwork) implementation(projects.opendcSimulator.opendcSimulatorCore) implementation(projects.opendcUtils) + + testImplementation(libs.slf4j.simple) } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt index dabe60e0..b85be39d 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt @@ -41,22 +41,26 @@ public class SimWorkloadLifecycle(private val ctx: SimMachineContext) { */ public fun waitFor(consumer: FlowSource): FlowSource { waiting.add(consumer) - return object : FlowSource by consumer { + return object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return try { + consumer.onPull(conn, now, delta) + } catch (cause: Throwable) { + complete(consumer) + throw cause + } + } + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { try { consumer.onEvent(conn, now, event) - } finally { + if (event == FlowEvent.Exit) { complete(consumer) } - } - } - - override fun onFailure(conn: FlowConnection, cause: Throwable) { - try { - consumer.onFailure(conn, cause) - } finally { + } catch (cause: Throwable) { complete(consumer) + throw cause } } diff --git a/opendc-simulator/opendc-simulator-flow/build.gradle.kts b/opendc-simulator/opendc-simulator-flow/build.gradle.kts index 5a956fee..05e21c3c 100644 --- a/opendc-simulator/opendc-simulator-flow/build.gradle.kts +++ b/opendc-simulator/opendc-simulator-flow/build.gradle.kts @@ -32,7 +32,8 @@ plugins { dependencies { api(platform(projects.opendcPlatform)) api(libs.kotlinx.coroutines) - implementation(projects.opendcUtils) + implementation(libs.kotlin.logging) testImplementation(projects.opendcSimulator.opendcSimulatorCore) + testImplementation(libs.slf4j.simple) } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt index 3a6e2e97..df2c4fab 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt @@ -82,22 +82,26 @@ public interface FlowConsumer { */ public suspend fun FlowConsumer.consume(source: FlowSource) { return suspendCancellableCoroutine { cont -> - startConsumer(object : FlowSource by source { - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - source.onEvent(conn, now, event) - - if (event == FlowEvent.Exit && !cont.isCompleted) { - cont.resume(Unit) + startConsumer(object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return try { + source.onPull(conn, now, delta) + } catch (cause: Throwable) { + cont.resumeWithException(cause) + throw cause } } - override fun onFailure(conn: FlowConnection, cause: Throwable) { + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { try { - source.onFailure(conn, cause) + source.onEvent(conn, now, event) + + if (event == FlowEvent.Exit && !cont.isCompleted) { + cont.resume(Unit) + } + } catch (cause: Throwable) { cont.resumeWithException(cause) - } catch (e: Throwable) { - e.addSuppressed(cause) - cont.resumeWithException(e) + throw cause } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt index c69cb17e..ef94ab22 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt @@ -46,11 +46,12 @@ public interface FlowConsumerLogic { public fun onConverge(ctx: FlowConsumerContext, now: Long, delta: Long) {} /** - * This method is invoked when the [FlowSource] is completed. + * This method is invoked when the [FlowSource] completed or failed. * * @param ctx The context in which the provider runs. * @param now The virtual timestamp in milliseconds at which the provider finished. * @param delta The virtual duration between this call and the last call to [onPush] in milliseconds. + * @param cause The cause of the failure or `null` if the source completed. */ - public fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long) {} + public fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long, cause: Throwable?) {} } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt index 2074033e..bc01a11b 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt @@ -22,6 +22,7 @@ package org.opendc.simulator.flow +import mu.KotlinLogging import org.opendc.simulator.flow.internal.FlowCountersImpl /** @@ -31,6 +32,11 @@ import org.opendc.simulator.flow.internal.FlowCountersImpl * @param isCoupled A flag to indicate that the transformer will exit when the resource consumer exits. */ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled: Boolean = false) : FlowSource, FlowConsumer, AutoCloseable { + /** + * The logging instance of this connection. + */ + private val logger = KotlinLogging.logger {} + /** * The delegate [FlowSource]. */ @@ -59,23 +65,25 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled } override fun push(rate: Double) { + if (delegate == null) { + return + } + _innerCtx?.push(rate) _demand = rate } override fun close() { - val delegate = checkNotNull(delegate) { "Delegate not active" } - - if (isCoupled) - _innerCtx?.close() - else - _innerCtx?.push(0.0) + val delegate = delegate ?: return + val hasDelegateStarted = hasDelegateStarted // Warning: resumption of the continuation might change the entire state of the forwarder. Make sure we // reset beforehand the existing state and check whether it has been updated afterwards reset() - delegate.onEvent(this, engine.clock.millis(), FlowEvent.Exit) + if (hasDelegateStarted) { + delegate.onEvent(this, engine.clock.millis(), FlowEvent.Exit) + } } } @@ -114,16 +122,7 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled } override fun cancel() { - val delegate = delegate - val ctx = _innerCtx - - if (delegate != null) { - this.delegate = null - - if (ctx != null) { - delegate.onEvent(this._ctx, engine.clock.millis(), FlowEvent.Exit) - } - } + _ctx.close() } override fun close() { @@ -144,34 +143,42 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled updateCounters(conn, delta) - return delegate?.onPull(this._ctx, now, delta) ?: Long.MAX_VALUE + return try { + delegate?.onPull(this._ctx, now, delta) ?: Long.MAX_VALUE + } catch (cause: Throwable) { + logger.error(cause) { "Uncaught exception" } + + reset() + Long.MAX_VALUE + } } override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { when (event) { - FlowEvent.Start -> { - _innerCtx = conn - } + FlowEvent.Start -> _innerCtx = conn FlowEvent.Exit -> { _innerCtx = null val delegate = delegate if (delegate != null) { reset() - delegate.onEvent(this._ctx, now, FlowEvent.Exit) + + try { + delegate.onEvent(this._ctx, now, FlowEvent.Exit) + } catch (cause: Throwable) { + logger.error(cause) { "Uncaught exception" } + } } } - else -> delegate?.onEvent(this._ctx, now, event) - } - } - - override fun onFailure(conn: FlowConnection, cause: Throwable) { - _innerCtx = null + else -> + try { + delegate?.onEvent(this._ctx, now, event) + } catch (cause: Throwable) { + logger.error(cause) { "Uncaught exception" } - val delegate = delegate - if (delegate != null) { - reset() - delegate.onFailure(this._ctx, cause) + _innerCtx = null + reset() + } } } @@ -180,15 +187,25 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled */ private fun start() { val delegate = delegate ?: return - delegate.onEvent(checkNotNull(_innerCtx), engine.clock.millis(), FlowEvent.Start) - hasDelegateStarted = true + try { + delegate.onEvent(_ctx, engine.clock.millis(), FlowEvent.Start) + hasDelegateStarted = true + } catch (cause: Throwable) { + logger.error(cause) { "Uncaught exception" } + reset() + } } /** * Reset the delegate. */ private fun reset() { + if (isCoupled) + _innerCtx?.close() + else + _innerCtx?.push(0.0) + delegate = null hasDelegateStarted = false } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt index fb6ca85d..fc590177 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt @@ -46,7 +46,7 @@ public class FlowSink( updateCounters(ctx, delta) } - override fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long) { + override fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long, cause: Throwable?) { updateCounters(ctx, delta) cancel() } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt index 077b4d38..70687b4f 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt @@ -47,12 +47,4 @@ public interface FlowSource { * @param event The event that has occurred. */ public fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) {} - - /** - * This method is invoked when the source throws an exception. - * - * @param conn The connection between the source and consumer. - * @param cause The cause of the failure. - */ - public fun onFailure(conn: FlowConnection, cause: Throwable) {} } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt index fc9c8059..a74f89b4 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt @@ -22,6 +22,7 @@ package org.opendc.simulator.flow.internal +import mu.KotlinLogging import org.opendc.simulator.flow.* import java.util.ArrayDeque import kotlin.math.max @@ -35,6 +36,11 @@ internal class FlowConsumerContextImpl( private val source: FlowSource, private val logic: FlowConsumerLogic ) : FlowConsumerContext { + /** + * The logging instance of this connection. + */ + private val logger = KotlinLogging.logger {} + /** * The capacity of the connection. */ @@ -131,7 +137,7 @@ internal class FlowConsumerContextImpl( if (!_isUpdateActive) { val now = _clock.millis() val delta = max(0, now - _lastPull) - doStop(now, delta) + doStopSource(now, delta) // FIX: Make sure the context converges pull() @@ -231,7 +237,7 @@ internal class FlowConsumerContextImpl( logic.onPush(this, now, pushDelta, demand) } } - State.Closed -> doStop(now, pushDelta) + State.Closed -> doStopSource(now, pushDelta) State.Pending -> throw IllegalStateException("Illegal transition to pending state") } @@ -246,7 +252,7 @@ internal class FlowConsumerContextImpl( // Schedule an update at the new deadline scheduleDelayed(now, deadline) } catch (cause: Throwable) { - doFail(now, pushDelta, cause) + doFailSource(now, pushDelta, cause) } finally { _isUpdateActive = false } @@ -288,21 +294,21 @@ internal class FlowConsumerContextImpl( logic.onConverge(this, timestamp, delta) } catch (cause: Throwable) { - doFail(timestamp, max(0, timestamp - _lastPull), cause) + doFailSource(timestamp, max(0, timestamp - _lastPull), cause) } } override fun toString(): String = "FlowConsumerContextImpl[capacity=$capacity,rate=$_rate]" /** - * Stop the resource context. + * Stop the [FlowSource]. */ - private fun doStop(now: Long, delta: Long) { + private fun doStopSource(now: Long, delta: Long) { try { source.onEvent(this, now, FlowEvent.Exit) - logic.onFinish(this, now, delta) + logic.onFinish(this, now, delta, null) } catch (cause: Throwable) { - doFail(now, delta, cause) + doFailSource(now, delta, cause) } finally { _deadline = Long.MAX_VALUE _demand = 0.0 @@ -310,17 +316,15 @@ internal class FlowConsumerContextImpl( } /** - * Fail the resource consumer. + * Fail the [FlowSource]. */ - private fun doFail(now: Long, delta: Long, cause: Throwable) { + private fun doFailSource(now: Long, delta: Long, cause: Throwable) { try { - source.onFailure(this, cause) + logic.onFinish(this, now, delta, cause) } catch (e: Throwable) { e.addSuppressed(cause) - e.printStackTrace() + logger.error(e) { "Uncaught exception" } } - - logic.onFinish(this, now, delta) } /** diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt index 9735f121..a3e108f6 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt @@ -292,7 +292,7 @@ public class MaxMinFlowMultiplexer( parent?.onConverge(now) } - override fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long) { + override fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long, cause: Throwable?) { doUpdateCounters(delta) limit = 0.0 diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt index 7fcc0405..fcee3906 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt @@ -28,7 +28,7 @@ import org.opendc.simulator.flow.FlowSource import kotlin.math.min /** - * Helper class to expose an observable [speed] field describing the speed of the consumer. + * Helper class to expose an observable [rate] field describing the flow rate of the source. */ public class FlowSourceRateAdapter( private val delegate: FlowSource, @@ -37,7 +37,7 @@ public class FlowSourceRateAdapter( /** * The resource processing speed at this instant. */ - public var speed: Double = 0.0 + public var rate: Double = 0.0 private set(value) { if (field != value) { callback(value) @@ -50,33 +50,37 @@ public class FlowSourceRateAdapter( } override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { - return delegate.onPull(conn, now, delta) + return try { + delegate.onPull(conn, now, delta) + } catch (cause: Throwable) { + rate = 0.0 + throw cause + } } override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - val oldSpeed = speed + val oldSpeed = rate - delegate.onEvent(conn, now, event) + try { + delegate.onEvent(conn, now, event) - when (event) { - FlowEvent.Converge -> speed = conn.rate - FlowEvent.Capacity -> { - // Check if the consumer interrupted the consumer and updated the resource consumption. If not, we might - // need to update the current speed. - if (oldSpeed == speed) { - speed = min(conn.capacity, speed) + when (event) { + FlowEvent.Converge -> rate = conn.rate + FlowEvent.Capacity -> { + // Check if the consumer interrupted the consumer and updated the resource consumption. If not, we might + // need to update the current speed. + if (oldSpeed == rate) { + rate = min(conn.capacity, rate) + } } + FlowEvent.Exit -> rate = 0.0 + else -> {} } - FlowEvent.Exit -> speed = 0.0 - else -> {} + } catch (cause: Throwable) { + rate = 0.0 + throw cause } } - override fun onFailure(conn: FlowConnection, cause: Throwable) { - speed = 0.0 - - delegate.onFailure(conn, cause) - } - override fun toString(): String = "FlowSourceRateAdapter[delegate=$delegate]" } diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt index 380fd38a..f1a5cbe4 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt @@ -23,7 +23,6 @@ package org.opendc.simulator.flow import io.mockk.* -import kotlinx.coroutines.* import org.junit.jupiter.api.* import org.opendc.simulator.core.runBlockingSimulation import org.opendc.simulator.flow.internal.FlowConsumerContextImpl @@ -102,34 +101,4 @@ class FlowConsumerContextTest { verify(exactly = 0) { consumer.onEvent(any(), any(), FlowEvent.Capacity) } } - - @Test - fun testFailureNoInfiniteLoop() = runBlockingSimulation { - val engine = FlowEngineImpl(coroutineContext, clock) - - val consumer = spyk(object : FlowSource { - override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { - conn.close() - return Long.MAX_VALUE - } - - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - if (event == FlowEvent.Exit) throw IllegalStateException("onEvent") - } - - override fun onFailure(conn: FlowConnection, cause: Throwable) { - throw IllegalStateException("onFailure") - } - }) - - val logic = object : FlowConsumerLogic {} - - val context = FlowConsumerContextImpl(engine, consumer, logic) - - context.start() - - delay(1) - - verify(exactly = 1) { consumer.onFailure(any(), any()) } - } } diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt index cbc48a4e..d125c638 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt @@ -219,4 +219,100 @@ internal class FlowForwarderTest { assertEquals(source.counters.overcommit, forwarder.counters.overcommit) { "Overcommitted work" } assertEquals(2000, clock.millis()) } + + @Test + fun testCoupledExit() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine, isCoupled = true) + val source = FlowSink(engine, 2000.0) + + launch { source.consume(forwarder) } + + forwarder.consume(FixedFlowSource(2000.0, 1.0)) + + yield() + + assertFalse(source.isActive) + } + + @Test + fun testPullFailureCoupled() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine, isCoupled = true) + val source = FlowSink(engine, 2000.0) + + launch { source.consume(forwarder) } + + try { + forwarder.consume(object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + throw IllegalStateException("Test") + } + }) + } catch (cause: Throwable) { + // Ignore + } + + yield() + + assertFalse(source.isActive) + } + + @Test + fun testEventFailure() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine) + val source = FlowSink(engine, 2000.0) + + launch { source.consume(forwarder) } + + try { + forwarder.consume(object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return Long.MAX_VALUE + } + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + throw IllegalStateException("Test") + } + }) + } catch (cause: Throwable) { + // Ignore + } + + yield() + + assertTrue(source.isActive) + source.cancel() + } + + @Test + fun testEventConvergeFailure() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + val forwarder = FlowForwarder(engine) + val source = FlowSink(engine, 2000.0) + + launch { source.consume(forwarder) } + + try { + forwarder.consume(object : FlowSource { + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return Long.MAX_VALUE + } + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + if (event == FlowEvent.Converge) { + throw IllegalStateException("Test") + } + } + }) + } catch (cause: Throwable) { + // Ignore + } + + yield() + + assertTrue(source.isActive) + source.cancel() + } } diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt new file mode 100644 index 00000000..c8627446 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt @@ -0,0 +1,157 @@ +/* + * 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.flow.mux + +import kotlinx.coroutines.yield +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.internal.FlowEngineImpl +import org.opendc.simulator.flow.source.FixedFlowSource +import org.opendc.simulator.flow.source.FlowSourceRateAdapter +import org.opendc.simulator.flow.source.TraceFlowSource + +/** + * Test suite for the [ForwardingFlowMultiplexer] class. + */ +internal class ExclusiveFlowMultiplexerTest { + /** + * Test a trace workload. + */ + @Test + fun testTrace() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val speed = mutableListOf() + + val duration = 5 * 60L + val workload = + TraceFlowSource( + sequenceOf( + TraceFlowSource.Fragment(duration * 1000, 28.0), + TraceFlowSource.Fragment(duration * 1000, 3500.0), + TraceFlowSource.Fragment(duration * 1000, 0.0), + TraceFlowSource.Fragment(duration * 1000, 183.0) + ), + ) + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + val forwarder = FlowForwarder(engine) + val adapter = FlowSourceRateAdapter(forwarder, speed::add) + source.startConsumer(adapter) + switch.addOutput(forwarder) + + val provider = switch.newInput() + provider.consume(workload) + yield() + + assertAll( + { assertEquals(listOf(0.0, 28.0, 3200.0, 0.0, 183.0, 0.0), speed) { "Correct speed" } }, + { assertEquals(5 * 60L * 4000, clock.millis()) { "Took enough time" } } + ) + } + + /** + * Test runtime workload on hypervisor. + */ + @Test + fun testRuntimeWorkload() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val duration = 5 * 60L * 1000 + val workload = FixedFlowSource(duration * 3.2, 1.0) + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + + switch.addOutput(source) + + val provider = switch.newInput() + provider.consume(workload) + yield() + + assertEquals(duration, clock.millis()) { "Took enough time" } + } + + /** + * Test two workloads running sequentially. + */ + @Test + fun testTwoWorkloads() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val duration = 5 * 60L * 1000 + val workload = object : FlowSource { + var isFirst = true + + override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + when (event) { + FlowEvent.Start -> isFirst = true + else -> {} + } + } + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return if (isFirst) { + isFirst = false + conn.push(1.0) + duration + } else { + conn.close() + Long.MAX_VALUE + } + } + } + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + + switch.addOutput(source) + + val provider = switch.newInput() + provider.consume(workload) + yield() + provider.consume(workload) + assertEquals(duration * 2, clock.millis()) { "Took enough time" } + } + + /** + * Test concurrent workloads on the machine. + */ + @Test + fun testConcurrentWorkloadFails() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + + switch.addOutput(source) + + switch.newInput() + assertThrows { switch.newInput() } + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexerTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexerTest.kt new file mode 100644 index 00000000..9f6b8a2c --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexerTest.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.simulator.flow.mux + +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.FlowSink +import org.opendc.simulator.flow.consume +import org.opendc.simulator.flow.internal.FlowEngineImpl +import org.opendc.simulator.flow.source.FixedFlowSource +import org.opendc.simulator.flow.source.TraceFlowSource + +/** + * Test suite for the [FlowMultiplexer] implementations + */ +internal class MaxMinFlowMultiplexerTest { + @Test + fun testSmoke() = runBlockingSimulation { + val scheduler = FlowEngineImpl(coroutineContext, clock) + val switch = MaxMinFlowMultiplexer(scheduler) + + val sources = List(2) { FlowSink(scheduler, 2000.0) } + sources.forEach { switch.addOutput(it) } + + val provider = switch.newInput() + val consumer = FixedFlowSource(2000.0, 1.0) + + try { + provider.consume(consumer) + yield() + } finally { + switch.clear() + } + } + + /** + * Test overcommitting of resources via the hypervisor with a single VM. + */ + @Test + fun testOvercommittedSingle() = runBlockingSimulation { + val scheduler = FlowEngineImpl(coroutineContext, clock) + + val duration = 5 * 60L + val workload = + TraceFlowSource( + sequenceOf( + TraceFlowSource.Fragment(duration * 1000, 28.0), + TraceFlowSource.Fragment(duration * 1000, 3500.0), + TraceFlowSource.Fragment(duration * 1000, 0.0), + TraceFlowSource.Fragment(duration * 1000, 183.0) + ), + ) + + val switch = MaxMinFlowMultiplexer(scheduler) + val provider = switch.newInput() + + try { + switch.addOutput(FlowSink(scheduler, 3200.0)) + provider.consume(workload) + yield() + } finally { + switch.clear() + } + + assertAll( + { assertEquals(1113300.0, switch.counters.demand, "Requested work does not match") }, + { assertEquals(1023300.0, switch.counters.actual, "Actual work does not match") }, + { assertEquals(90000.0, switch.counters.overcommit, "Overcommitted work does not match") }, + { assertEquals(1200000, clock.millis()) } + ) + } + + /** + * Test overcommitting of resources via the hypervisor with two VMs. + */ + @Test + fun testOvercommittedDual() = runBlockingSimulation { + val scheduler = FlowEngineImpl(coroutineContext, clock) + + val duration = 5 * 60L + val workloadA = + TraceFlowSource( + sequenceOf( + TraceFlowSource.Fragment(duration * 1000, 28.0), + TraceFlowSource.Fragment(duration * 1000, 3500.0), + TraceFlowSource.Fragment(duration * 1000, 0.0), + TraceFlowSource.Fragment(duration * 1000, 183.0) + ), + ) + val workloadB = + TraceFlowSource( + sequenceOf( + TraceFlowSource.Fragment(duration * 1000, 28.0), + TraceFlowSource.Fragment(duration * 1000, 3100.0), + TraceFlowSource.Fragment(duration * 1000, 0.0), + TraceFlowSource.Fragment(duration * 1000, 73.0) + ) + ) + + val switch = MaxMinFlowMultiplexer(scheduler) + val providerA = switch.newInput() + val providerB = switch.newInput() + + try { + switch.addOutput(FlowSink(scheduler, 3200.0)) + + coroutineScope { + launch { providerA.consume(workloadA) } + providerB.consume(workloadB) + } + + yield() + } finally { + switch.clear() + } + assertAll( + { assertEquals(2073600.0, switch.counters.demand, "Requested work does not match") }, + { assertEquals(1053600.0, switch.counters.actual, "Granted work does not match") }, + { assertEquals(1020000.0, switch.counters.overcommit, "Overcommitted work does not match") }, + { assertEquals(1200000, clock.millis()) } + ) + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchExclusiveTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchExclusiveTest.kt deleted file mode 100644 index b503087e..00000000 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchExclusiveTest.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.flow.mux - -import kotlinx.coroutines.yield -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertAll -import org.junit.jupiter.api.assertThrows -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.flow.* -import org.opendc.simulator.flow.internal.FlowEngineImpl -import org.opendc.simulator.flow.source.FixedFlowSource -import org.opendc.simulator.flow.source.FlowSourceRateAdapter -import org.opendc.simulator.flow.source.TraceFlowSource - -/** - * Test suite for the [ForwardingFlowMultiplexer] class. - */ -internal class SimResourceSwitchExclusiveTest { - /** - * Test a trace workload. - */ - @Test - fun testTrace() = runBlockingSimulation { - val engine = FlowEngineImpl(coroutineContext, clock) - - val speed = mutableListOf() - - val duration = 5 * 60L - val workload = - TraceFlowSource( - sequenceOf( - TraceFlowSource.Fragment(duration * 1000, 28.0), - TraceFlowSource.Fragment(duration * 1000, 3500.0), - TraceFlowSource.Fragment(duration * 1000, 0.0), - TraceFlowSource.Fragment(duration * 1000, 183.0) - ), - ) - - val switch = ForwardingFlowMultiplexer(engine) - val source = FlowSink(engine, 3200.0) - val forwarder = FlowForwarder(engine) - val adapter = FlowSourceRateAdapter(forwarder, speed::add) - source.startConsumer(adapter) - switch.addOutput(forwarder) - - val provider = switch.newInput() - provider.consume(workload) - yield() - - assertAll( - { assertEquals(listOf(0.0, 28.0, 3200.0, 0.0, 183.0, 0.0), speed) { "Correct speed" } }, - { assertEquals(5 * 60L * 4000, clock.millis()) { "Took enough time" } } - ) - } - - /** - * Test runtime workload on hypervisor. - */ - @Test - fun testRuntimeWorkload() = runBlockingSimulation { - val engine = FlowEngineImpl(coroutineContext, clock) - - val duration = 5 * 60L * 1000 - val workload = FixedFlowSource(duration * 3.2, 1.0) - - val switch = ForwardingFlowMultiplexer(engine) - val source = FlowSink(engine, 3200.0) - - switch.addOutput(source) - - val provider = switch.newInput() - provider.consume(workload) - yield() - - assertEquals(duration, clock.millis()) { "Took enough time" } - } - - /** - * Test two workloads running sequentially. - */ - @Test - fun testTwoWorkloads() = runBlockingSimulation { - val engine = FlowEngineImpl(coroutineContext, clock) - - val duration = 5 * 60L * 1000 - val workload = object : FlowSource { - var isFirst = true - - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - when (event) { - FlowEvent.Start -> isFirst = true - else -> {} - } - } - - override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { - return if (isFirst) { - isFirst = false - conn.push(1.0) - duration - } else { - conn.close() - Long.MAX_VALUE - } - } - } - - val switch = ForwardingFlowMultiplexer(engine) - val source = FlowSink(engine, 3200.0) - - switch.addOutput(source) - - val provider = switch.newInput() - provider.consume(workload) - yield() - provider.consume(workload) - assertEquals(duration * 2, clock.millis()) { "Took enough time" } - } - - /** - * Test concurrent workloads on the machine. - */ - @Test - fun testConcurrentWorkloadFails() = runBlockingSimulation { - val engine = FlowEngineImpl(coroutineContext, clock) - - val switch = ForwardingFlowMultiplexer(engine) - val source = FlowSink(engine, 3200.0) - - switch.addOutput(source) - - switch.newInput() - assertThrows { switch.newInput() } - } -} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchMaxMinTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchMaxMinTest.kt deleted file mode 100644 index 089a8d78..00000000 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/SimResourceSwitchMaxMinTest.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.flow.mux - -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield -import org.junit.jupiter.api.* -import org.junit.jupiter.api.Assertions.assertEquals -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.flow.FlowSink -import org.opendc.simulator.flow.consume -import org.opendc.simulator.flow.internal.FlowEngineImpl -import org.opendc.simulator.flow.source.FixedFlowSource -import org.opendc.simulator.flow.source.TraceFlowSource - -/** - * Test suite for the [FlowMultiplexer] implementations - */ -internal class SimResourceSwitchMaxMinTest { - @Test - fun testSmoke() = runBlockingSimulation { - val scheduler = FlowEngineImpl(coroutineContext, clock) - val switch = MaxMinFlowMultiplexer(scheduler) - - val sources = List(2) { FlowSink(scheduler, 2000.0) } - sources.forEach { switch.addOutput(it) } - - val provider = switch.newInput() - val consumer = FixedFlowSource(2000.0, 1.0) - - try { - provider.consume(consumer) - yield() - } finally { - switch.clear() - } - } - - /** - * Test overcommitting of resources via the hypervisor with a single VM. - */ - @Test - fun testOvercommittedSingle() = runBlockingSimulation { - val scheduler = FlowEngineImpl(coroutineContext, clock) - - val duration = 5 * 60L - val workload = - TraceFlowSource( - sequenceOf( - TraceFlowSource.Fragment(duration * 1000, 28.0), - TraceFlowSource.Fragment(duration * 1000, 3500.0), - TraceFlowSource.Fragment(duration * 1000, 0.0), - TraceFlowSource.Fragment(duration * 1000, 183.0) - ), - ) - - val switch = MaxMinFlowMultiplexer(scheduler) - val provider = switch.newInput() - - try { - switch.addOutput(FlowSink(scheduler, 3200.0)) - provider.consume(workload) - yield() - } finally { - switch.clear() - } - - assertAll( - { assertEquals(1113300.0, switch.counters.demand, "Requested work does not match") }, - { assertEquals(1023300.0, switch.counters.actual, "Actual work does not match") }, - { assertEquals(90000.0, switch.counters.overcommit, "Overcommitted work does not match") }, - { assertEquals(1200000, clock.millis()) } - ) - } - - /** - * Test overcommitting of resources via the hypervisor with two VMs. - */ - @Test - fun testOvercommittedDual() = runBlockingSimulation { - val scheduler = FlowEngineImpl(coroutineContext, clock) - - val duration = 5 * 60L - val workloadA = - TraceFlowSource( - sequenceOf( - TraceFlowSource.Fragment(duration * 1000, 28.0), - TraceFlowSource.Fragment(duration * 1000, 3500.0), - TraceFlowSource.Fragment(duration * 1000, 0.0), - TraceFlowSource.Fragment(duration * 1000, 183.0) - ), - ) - val workloadB = - TraceFlowSource( - sequenceOf( - TraceFlowSource.Fragment(duration * 1000, 28.0), - TraceFlowSource.Fragment(duration * 1000, 3100.0), - TraceFlowSource.Fragment(duration * 1000, 0.0), - TraceFlowSource.Fragment(duration * 1000, 73.0) - ) - ) - - val switch = MaxMinFlowMultiplexer(scheduler) - val providerA = switch.newInput() - val providerB = switch.newInput() - - try { - switch.addOutput(FlowSink(scheduler, 3200.0)) - - coroutineScope { - launch { providerA.consume(workloadA) } - providerB.consume(workloadB) - } - - yield() - } finally { - switch.clear() - } - assertAll( - { assertEquals(2073600.0, switch.counters.demand, "Requested work does not match") }, - { assertEquals(1053600.0, switch.counters.actual, "Granted work does not match") }, - { assertEquals(1020000.0, switch.counters.overcommit, "Overcommitted work does not match") }, - { assertEquals(1200000, clock.millis()) } - ) - } -} diff --git a/opendc-simulator/opendc-simulator-network/build.gradle.kts b/opendc-simulator/opendc-simulator-network/build.gradle.kts index a8f94602..f8931053 100644 --- a/opendc-simulator/opendc-simulator-network/build.gradle.kts +++ b/opendc-simulator/opendc-simulator-network/build.gradle.kts @@ -32,4 +32,6 @@ dependencies { api(platform(projects.opendcPlatform)) api(projects.opendcSimulator.opendcSimulatorFlow) implementation(projects.opendcSimulator.opendcSimulatorCore) + + testImplementation(libs.slf4j.simple) } diff --git a/opendc-simulator/opendc-simulator-power/build.gradle.kts b/opendc-simulator/opendc-simulator-power/build.gradle.kts index e4342a6a..5d8c8949 100644 --- a/opendc-simulator/opendc-simulator-power/build.gradle.kts +++ b/opendc-simulator/opendc-simulator-power/build.gradle.kts @@ -32,4 +32,6 @@ dependencies { api(platform(projects.opendcPlatform)) api(projects.opendcSimulator.opendcSimulatorFlow) implementation(projects.opendcSimulator.opendcSimulatorCore) + + testImplementation(libs.slf4j.simple) } -- cgit v1.2.3 From 4f5a1f88d0c6aa19ce4cab0ec7b9b13a24c92fbe Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 30 Sep 2021 14:45:42 +0200 Subject: refactor(simulator): Remove capacity event This change removes the Capacity entry from FlowEvent. Since the source is always pulled on a capacity change, we do not need a separate event for this. --- .../kotlin/org/opendc/simulator/flow/FlowEvent.kt | 5 ----- .../flow/internal/FlowConsumerContextImpl.kt | 23 +++++----------------- .../simulator/flow/mux/MaxMinFlowMultiplexer.kt | 12 ++++++++--- .../simulator/flow/source/FlowSourceRateAdapter.kt | 10 ---------- .../simulator/flow/FlowConsumerContextTest.kt | 2 +- .../org/opendc/simulator/flow/FlowForwarderTest.kt | 2 +- .../org/opendc/simulator/flow/FlowSinkTest.kt | 2 +- 7 files changed, 17 insertions(+), 39 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt index 14c85183..bb6f25b1 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt @@ -40,9 +40,4 @@ public enum class FlowEvent { * This event is emitted to the source when the system has converged into a steady state. */ Converge, - - /** - * This event is emitted to the source when the capacity of the consumer has changed. - */ - Capacity, } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt index a74f89b4..a86ed6ea 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt @@ -51,7 +51,11 @@ internal class FlowConsumerContextImpl( // Only changes will be propagated if (value != oldValue) { field = value - onCapacityChange() + + // Do not pull the source if it has not been started yet + if (_state == State.Active) { + pull() + } } } @@ -327,23 +331,6 @@ internal class FlowConsumerContextImpl( } } - /** - * Indicate that the capacity of the resource has changed. - */ - private fun onCapacityChange() { - // Do not inform the consumer if it has not been started yet - if (_state != State.Active) { - return - } - - engine.batch { - // Inform the consumer of the capacity change. This might already trigger an interrupt. - source.onEvent(this, _clock.millis(), FlowEvent.Capacity) - - pull() - } - } - /** * Schedule an immediate update for this connection. */ diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt index a3e108f6..b98cf2f1 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt @@ -356,8 +356,7 @@ public class MaxMinFlowMultiplexer( /** * The capacity of this output. */ - val capacity: Double - get() = _ctx?.capacity ?: 0.0 + @JvmField var capacity: Double = 0.0 /** * Push the specified rate to the consumer. @@ -374,6 +373,12 @@ public class MaxMinFlowMultiplexer( } override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + val capacity = capacity + if (capacity != conn.capacity) { + this.capacity = capacity + updateCapacity() + } + runScheduler(now) return Long.MAX_VALUE } @@ -383,13 +388,14 @@ public class MaxMinFlowMultiplexer( FlowEvent.Start -> { assert(_ctx == null) { "Source running concurrently" } _ctx = conn + capacity = conn.capacity updateCapacity() } FlowEvent.Exit -> { _ctx = null + capacity = 0.0 updateCapacity() } - FlowEvent.Capacity -> updateCapacity() else -> {} } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt index fcee3906..24ae64cb 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt @@ -25,7 +25,6 @@ package org.opendc.simulator.flow.source import org.opendc.simulator.flow.FlowConnection import org.opendc.simulator.flow.FlowEvent import org.opendc.simulator.flow.FlowSource -import kotlin.math.min /** * Helper class to expose an observable [rate] field describing the flow rate of the source. @@ -59,20 +58,11 @@ public class FlowSourceRateAdapter( } override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - val oldSpeed = rate - try { delegate.onEvent(conn, now, event) when (event) { FlowEvent.Converge -> rate = conn.rate - FlowEvent.Capacity -> { - // Check if the consumer interrupted the consumer and updated the resource consumption. If not, we might - // need to update the current speed. - if (oldSpeed == rate) { - rate = min(conn.capacity, rate) - } - } FlowEvent.Exit -> rate = 0.0 else -> {} } diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt index f1a5cbe4..fe39eb2c 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowConsumerContextTest.kt @@ -99,6 +99,6 @@ class FlowConsumerContextTest { context.start() context.capacity = 4200.0 - verify(exactly = 0) { consumer.onEvent(any(), any(), FlowEvent.Capacity) } + verify(exactly = 1) { consumer.onPull(any(), any(), any()) } } } diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt index d125c638..7fae918a 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt @@ -197,7 +197,7 @@ internal class FlowForwarderTest { } assertEquals(3000, clock.millis()) - verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Capacity) } + verify(exactly = 1) { consumer.onPull(any(), any(), any()) } } @Test diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt index 010a985e..5d579e5d 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt @@ -67,7 +67,7 @@ internal class FlowSinkTest { provider.capacity = 0.5 } assertEquals(3000, clock.millis()) - verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Capacity) } + verify(exactly = 1) { consumer.onPull(any(), any(), any()) } } @Test -- cgit v1.2.3 From a2ce07026bf3ef17326e72f395dfa2dd9d9b17be Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 30 Sep 2021 15:37:35 +0200 Subject: refactor(simulator): Create separate callbacks for remaining events This change creates separate callbacks for the remaining events: onStart, onStop and onConverge. --- .../org/opendc/simulator/compute/device/SimPsu.kt | 18 +++--- .../compute/workload/SimWorkloadLifecycle.kt | 24 ++------ .../org/opendc/simulator/flow/FlowConsumer.kt | 30 ++++++++-- .../kotlin/org/opendc/simulator/flow/FlowEvent.kt | 43 -------------- .../org/opendc/simulator/flow/FlowForwarder.kt | 65 ++++++++++++---------- .../kotlin/org/opendc/simulator/flow/FlowSource.kt | 25 +++++++-- .../flow/internal/FlowConsumerContextImpl.kt | 53 +++++++++++------- .../flow/mux/ForwardingFlowMultiplexer.kt | 9 +-- .../simulator/flow/mux/MaxMinFlowMultiplexer.kt | 30 +++++----- .../simulator/flow/source/FlowSourceRateAdapter.kt | 29 ++++------ .../simulator/flow/source/TraceFlowSource.kt | 25 ++++----- .../org/opendc/simulator/flow/FlowForwarderTest.kt | 39 +++++++------ .../org/opendc/simulator/flow/FlowSinkTest.kt | 25 +++++---- .../flow/mux/ExclusiveFlowMultiplexerTest.kt | 7 +-- .../opendc/simulator/network/SimNetworkSinkTest.kt | 4 +- .../network/SimNetworkSwitchVirtualTest.kt | 2 +- .../org/opendc/simulator/power/SimPduTest.kt | 3 +- .../opendc/simulator/power/SimPowerSourceTest.kt | 3 +- .../org/opendc/simulator/power/SimUpsTest.kt | 3 +- 19 files changed, 206 insertions(+), 231 deletions(-) delete mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt index b05d8ad9..8400c225 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt @@ -24,7 +24,6 @@ package org.opendc.simulator.compute.device import org.opendc.simulator.compute.power.PowerDriver import org.opendc.simulator.flow.FlowConnection -import org.opendc.simulator.flow.FlowEvent import org.opendc.simulator.flow.FlowSource import org.opendc.simulator.power.SimPowerInlet import java.util.* @@ -82,19 +81,22 @@ public class SimPsu( } override fun createConsumer(): FlowSource = object : FlowSource { + override fun onStart(conn: FlowConnection, now: Long) { + _ctx = conn + } + + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { + _ctx = null + } + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { val powerDraw = computePowerDraw(_driver?.computePower() ?: 0.0) conn.push(powerDraw) return Long.MAX_VALUE } - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - when (event) { - FlowEvent.Start -> _ctx = conn - FlowEvent.Converge -> _powerDraw = conn.rate - FlowEvent.Exit -> _ctx = null - else -> {} - } + override fun onConverge(conn: FlowConnection, now: Long, delta: Long) { + _powerDraw = conn.rate } } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt index b85be39d..cc4f1f6a 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimWorkloadLifecycle.kt @@ -24,7 +24,6 @@ package org.opendc.simulator.compute.workload import org.opendc.simulator.compute.SimMachineContext import org.opendc.simulator.flow.FlowConnection -import org.opendc.simulator.flow.FlowEvent import org.opendc.simulator.flow.FlowSource /** @@ -41,29 +40,14 @@ public class SimWorkloadLifecycle(private val ctx: SimMachineContext) { */ public fun waitFor(consumer: FlowSource): FlowSource { waiting.add(consumer) - return object : FlowSource { - override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { - return try { - consumer.onPull(conn, now, delta) - } catch (cause: Throwable) { - complete(consumer) - throw cause - } - } - - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + return object : FlowSource by consumer { + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { try { - consumer.onEvent(conn, now, event) - - if (event == FlowEvent.Exit) { - complete(consumer) - } - } catch (cause: Throwable) { + consumer.onStop(conn, now, delta) + } finally { complete(consumer) - throw cause } } - override fun toString(): String = "SimWorkloadLifecycle.Consumer[delegate=$consumer]" } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt index df2c4fab..4685a755 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumer.kt @@ -83,20 +83,20 @@ public interface FlowConsumer { public suspend fun FlowConsumer.consume(source: FlowSource) { return suspendCancellableCoroutine { cont -> startConsumer(object : FlowSource { - override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { - return try { - source.onPull(conn, now, delta) + override fun onStart(conn: FlowConnection, now: Long) { + try { + source.onStart(conn, now) } catch (cause: Throwable) { cont.resumeWithException(cause) throw cause } } - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { try { - source.onEvent(conn, now, event) + source.onStop(conn, now, delta) - if (event == FlowEvent.Exit && !cont.isCompleted) { + if (!cont.isCompleted) { cont.resume(Unit) } } catch (cause: Throwable) { @@ -105,6 +105,24 @@ public suspend fun FlowConsumer.consume(source: FlowSource) { } } + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return try { + source.onPull(conn, now, delta) + } catch (cause: Throwable) { + cont.resumeWithException(cause) + throw cause + } + } + + override fun onConverge(conn: FlowConnection, now: Long, delta: Long) { + try { + source.onConverge(conn, now, delta) + } catch (cause: Throwable) { + cont.resumeWithException(cause) + throw cause + } + } + override fun toString(): String = "SuspendingFlowSource" }) diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt deleted file mode 100644 index bb6f25b1..00000000 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowEvent.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.flow - -/** - * A flow event that is communicated to a [FlowSource]. - */ -public enum class FlowEvent { - /** - * This event is emitted to the source when it has started. - */ - Start, - - /** - * This event is emitted to the source when it is stopped. - */ - Exit, - - /** - * This event is emitted to the source when the system has converged into a steady state. - */ - Converge, -} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt index bc01a11b..ab5b31c2 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt @@ -24,6 +24,7 @@ package org.opendc.simulator.flow import mu.KotlinLogging import org.opendc.simulator.flow.internal.FlowCountersImpl +import kotlin.math.max /** * A class that acts as a [FlowSource] and [FlowConsumer] at the same time. @@ -64,6 +65,8 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled _innerCtx?.pull() } + @JvmField var lastPull = Long.MAX_VALUE + override fun push(rate: Double) { if (delegate == null) { return @@ -82,7 +85,9 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled reset() if (hasDelegateStarted) { - delegate.onEvent(this, engine.clock.millis(), FlowEvent.Exit) + val now = engine.clock.millis() + val delta = max(0, now - lastPull) + delegate.onStop(this, now, delta) } } } @@ -134,6 +139,25 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled } } + override fun onStart(conn: FlowConnection, now: Long) { + _innerCtx = conn + } + + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { + _innerCtx = null + + val delegate = delegate + if (delegate != null) { + reset() + + try { + delegate.onStop(this._ctx, now, delta) + } catch (cause: Throwable) { + logger.error(cause) { "Uncaught exception" } + } + } + } + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { val delegate = delegate @@ -141,10 +165,11 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled start() } + _ctx.lastPull = now updateCounters(conn, delta) return try { - delegate?.onPull(this._ctx, now, delta) ?: Long.MAX_VALUE + delegate?.onPull(_ctx, now, delta) ?: Long.MAX_VALUE } catch (cause: Throwable) { logger.error(cause) { "Uncaught exception" } @@ -153,32 +178,14 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled } } - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - when (event) { - FlowEvent.Start -> _innerCtx = conn - FlowEvent.Exit -> { - _innerCtx = null - - val delegate = delegate - if (delegate != null) { - reset() - - try { - delegate.onEvent(this._ctx, now, FlowEvent.Exit) - } catch (cause: Throwable) { - logger.error(cause) { "Uncaught exception" } - } - } - } - else -> - try { - delegate?.onEvent(this._ctx, now, event) - } catch (cause: Throwable) { - logger.error(cause) { "Uncaught exception" } - - _innerCtx = null - reset() - } + override fun onConverge(conn: FlowConnection, now: Long, delta: Long) { + try { + delegate?.onConverge(this._ctx, now, delta) + } catch (cause: Throwable) { + logger.error(cause) { "Uncaught exception" } + + _innerCtx = null + reset() } } @@ -189,7 +196,7 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled val delegate = delegate ?: return try { - delegate.onEvent(_ctx, engine.clock.millis(), FlowEvent.Start) + delegate.onStart(_ctx, engine.clock.millis()) hasDelegateStarted = true } catch (cause: Throwable) { logger.error(cause) { "Uncaught exception" } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt index 70687b4f..a4f624ef 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt @@ -29,6 +29,23 @@ package org.opendc.simulator.flow * (concurrently) for multiple [FlowConsumer]s, unless explicitly said otherwise. */ public interface FlowSource { + /** + * This method is invoked when the source is started. + * + * @param conn The connection between the source and consumer. + * @param now The virtual timestamp in milliseconds at which the provider finished. + */ + public fun onStart(conn: FlowConnection, now: Long) {} + + /** + * This method is invoked when the source is finished. + * + * @param conn The connection between the source and consumer. + * @param now The virtual timestamp in milliseconds at which the source finished. + * @param delta The virtual duration between this call and the last call to [onPull] in milliseconds. + */ + public fun onStop(conn: FlowConnection, now: Long, delta: Long) {} + /** * This method is invoked when the source is pulled. * @@ -40,11 +57,11 @@ public interface FlowSource { public fun onPull(conn: FlowConnection, now: Long, delta: Long): Long /** - * This method is invoked when an event has occurred. + * This method is invoked when the flow graph has converged into a steady-state system. * * @param conn The connection between the source and consumer. - * @param now The virtual timestamp in milliseconds at which the event is occurring. - * @param event The event that has occurred. + * @param now The virtual timestamp in milliseconds at which the system converged. + * @param delta The virtual duration between this call and the last call to [onConverge] in milliseconds. */ - public fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) {} + public fun onConverge(conn: FlowConnection, now: Long, delta: Long) {} } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt index a86ed6ea..c235b9ae 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt @@ -125,7 +125,7 @@ internal class FlowConsumerContextImpl( override fun start() { check(_state == State.Pending) { "Consumer is already started" } engine.batch { - source.onEvent(this, _clock.millis(), FlowEvent.Start) + source.onStart(this, _clock.millis()) _state = State.Active pull() } @@ -140,8 +140,7 @@ internal class FlowConsumerContextImpl( _state = State.Closed if (!_isUpdateActive) { val now = _clock.millis() - val delta = max(0, now - _lastPull) - doStopSource(now, delta) + doStopSource(now) // FIX: Make sure the context converges pull() @@ -210,19 +209,16 @@ internal class FlowConsumerContextImpl( _isUpdateActive = true _isImmediateUpdateScheduled = false - val lastPush = _lastPush - val pushDelta = max(0, now - lastPush) - try { // Pull the source if (1) `pull` is called or (2) the timer of the source has expired val deadline = if (_isPulled || _deadline == now) { val lastPull = _lastPull - val pullDelta = max(0, now - lastPull) + val delta = max(0, now - lastPull) _isPulled = false _lastPull = now - val duration = source.onPull(this, now, pullDelta) + val duration = source.onPull(this, now, delta) if (duration != Long.MAX_VALUE) now + duration else @@ -236,12 +232,15 @@ internal class FlowConsumerContextImpl( State.Active -> { val demand = _demand if (demand != _activeDemand) { + val lastPush = _lastPush + val delta = max(0, now - lastPush) + _lastPush = now - logic.onPush(this, now, pushDelta, demand) + logic.onPush(this, now, delta, demand) } } - State.Closed -> doStopSource(now, pushDelta) + State.Closed -> doStopSource(now) State.Pending -> throw IllegalStateException("Illegal transition to pending state") } @@ -256,7 +255,7 @@ internal class FlowConsumerContextImpl( // Schedule an update at the new deadline scheduleDelayed(now, deadline) } catch (cause: Throwable) { - doFailSource(now, pushDelta, cause) + doFailSource(now, cause) } finally { _isUpdateActive = false } @@ -293,12 +292,12 @@ internal class FlowConsumerContextImpl( try { if (_state == State.Active) { - source.onEvent(this, timestamp, FlowEvent.Converge) + source.onConverge(this, timestamp, delta) } logic.onConverge(this, timestamp, delta) } catch (cause: Throwable) { - doFailSource(timestamp, max(0, timestamp - _lastPull), cause) + doFailSource(timestamp, cause) } } @@ -307,12 +306,13 @@ internal class FlowConsumerContextImpl( /** * Stop the [FlowSource]. */ - private fun doStopSource(now: Long, delta: Long) { + private fun doStopSource(now: Long) { try { - source.onEvent(this, now, FlowEvent.Exit) - logic.onFinish(this, now, delta, null) + source.onStop(this, now, max(0, now - _lastPull)) + doFinishConsumer(now, null) } catch (cause: Throwable) { - doFailSource(now, delta, cause) + doFinishConsumer(now, cause) + return } finally { _deadline = Long.MAX_VALUE _demand = 0.0 @@ -322,9 +322,24 @@ internal class FlowConsumerContextImpl( /** * Fail the [FlowSource]. */ - private fun doFailSource(now: Long, delta: Long, cause: Throwable) { + private fun doFailSource(now: Long, cause: Throwable) { + try { + source.onStop(this, now, max(0, now - _lastPull)) + } catch (e: Throwable) { + e.addSuppressed(cause) + doFinishConsumer(now, e) + } finally { + _deadline = Long.MAX_VALUE + _demand = 0.0 + } + } + + /** + * Finish the consumer. + */ + private fun doFinishConsumer(now: Long, cause: Throwable?) { try { - logic.onFinish(this, now, delta, cause) + logic.onFinish(this, now, max(0, now - _lastPush), cause) } catch (e: Throwable) { e.addSuppressed(cause) logger.error(e) { "Uncaught exception" } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt index 811d9460..6dd9dcfb 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt @@ -88,13 +88,10 @@ public class ForwardingFlowMultiplexer(private val engine: FlowEngine) : FlowMul _availableOutputs += forwarder output.startConsumer(object : FlowSource by forwarder { - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - if (event == FlowEvent.Exit) { - // De-register the output after it has finished - _outputs -= output - } + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { + _outputs -= output - forwarder.onEvent(conn, now, event) + forwarder.onStop(conn, now, delta) } }) } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt index b98cf2f1..c7379fa9 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt @@ -372,6 +372,19 @@ public class MaxMinFlowMultiplexer( provider.cancel() } + override fun onStart(conn: FlowConnection, now: Long) { + assert(_ctx == null) { "Source running concurrently" } + _ctx = conn + capacity = conn.capacity + updateCapacity() + } + + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { + _ctx = null + capacity = 0.0 + updateCapacity() + } + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { val capacity = capacity if (capacity != conn.capacity) { @@ -383,23 +396,6 @@ public class MaxMinFlowMultiplexer( return Long.MAX_VALUE } - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - when (event) { - FlowEvent.Start -> { - assert(_ctx == null) { "Source running concurrently" } - _ctx = conn - capacity = conn.capacity - updateCapacity() - } - FlowEvent.Exit -> { - _ctx = null - capacity = 0.0 - updateCapacity() - } - else -> {} - } - } - override fun compareTo(other: Output): Int = capacity.compareTo(other.capacity) } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt index 24ae64cb..0c39523f 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt @@ -23,7 +23,6 @@ package org.opendc.simulator.flow.source import org.opendc.simulator.flow.FlowConnection -import org.opendc.simulator.flow.FlowEvent import org.opendc.simulator.flow.FlowSource /** @@ -48,28 +47,22 @@ public class FlowSourceRateAdapter( callback(0.0) } - override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { - return try { - delegate.onPull(conn, now, delta) - } catch (cause: Throwable) { + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { + try { + delegate.onStop(conn, now, delta) + } finally { rate = 0.0 - throw cause } } - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - try { - delegate.onEvent(conn, now, event) + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return delegate.onPull(conn, now, delta) + } - when (event) { - FlowEvent.Converge -> rate = conn.rate - FlowEvent.Exit -> rate = 0.0 - else -> {} - } - } catch (cause: Throwable) { - rate = 0.0 - throw cause - } + override fun onConverge(conn: FlowConnection, now: Long, delta: Long) { + delegate.onConverge(conn, now, delta) + + rate = conn.rate } override fun toString(): String = "FlowSourceRateAdapter[delegate=$delegate]" diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/TraceFlowSource.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/TraceFlowSource.kt index 4d3ae61a..ae537845 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/TraceFlowSource.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/TraceFlowSource.kt @@ -23,7 +23,6 @@ package org.opendc.simulator.flow.source import org.opendc.simulator.flow.FlowConnection -import org.opendc.simulator.flow.FlowEvent import org.opendc.simulator.flow.FlowSource /** @@ -33,6 +32,15 @@ public class TraceFlowSource(private val trace: Sequence) : FlowSource private var _iterator: Iterator? = null private var _nextTarget = Long.MIN_VALUE + override fun onStart(conn: FlowConnection, now: Long) { + check(_iterator == null) { "Source already running" } + _iterator = trace.iterator() + } + + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { + _iterator = null + } + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { // Check whether the trace fragment was fully consumed, otherwise wait until we have done so val nextTarget = _nextTarget @@ -52,21 +60,8 @@ public class TraceFlowSource(private val trace: Sequence) : FlowSource } } - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - when (event) { - FlowEvent.Start -> { - check(_iterator == null) { "Source already running" } - _iterator = trace.iterator() - } - FlowEvent.Exit -> { - _iterator = null - } - else -> {} - } - } - /** - * A fragment of the tgrace. + * A fragment of the trace. */ public data class Fragment(val duration: Long, val usage: Double) } diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt index 7fae918a..3690e681 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt @@ -22,10 +22,10 @@ package org.opendc.simulator.flow -import io.mockk.spyk -import io.mockk.verify +import io.mockk.* import kotlinx.coroutines.* import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation @@ -122,7 +122,7 @@ internal class FlowForwarderTest { forwarder.startConsumer(consumer) forwarder.cancel() - verify(exactly = 0) { consumer.onEvent(any(), any(), FlowEvent.Exit) } + verify(exactly = 0) { consumer.onStop(any(), any(), any()) } } @Test @@ -139,8 +139,8 @@ internal class FlowForwarderTest { yield() forwarder.cancel() - verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Start) } - verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Exit) } + verify(exactly = 1) { consumer.onStart(any(), any()) } + verify(exactly = 1) { consumer.onStop(any(), any(), any()) } } @Test @@ -157,8 +157,8 @@ internal class FlowForwarderTest { yield() source.cancel() - verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Start) } - verify(exactly = 1) { consumer.onEvent(any(), any(), FlowEvent.Exit) } + verify(exactly = 1) { consumer.onStart(any(), any()) } + verify(exactly = 1) { consumer.onStop(any(), any(), any()) } } @Test @@ -182,22 +182,23 @@ internal class FlowForwarderTest { } @Test + @Disabled // Due to Kotlin bug: https://github.com/mockk/mockk/issues/368 fun testAdjustCapacity() = runBlockingSimulation { val engine = FlowEngineImpl(coroutineContext, clock) val forwarder = FlowForwarder(engine) - val source = FlowSink(engine, 1.0) + val sink = FlowSink(engine, 1.0) - val consumer = spyk(FixedFlowSource(2.0, 1.0)) - source.startConsumer(forwarder) + val source = spyk(FixedFlowSource(2.0, 1.0)) + sink.startConsumer(forwarder) coroutineScope { - launch { forwarder.consume(consumer) } + launch { forwarder.consume(source) } delay(1000) - source.capacity = 0.5 + sink.capacity = 0.5 } assertEquals(3000, clock.millis()) - verify(exactly = 1) { consumer.onPull(any(), any(), any()) } + verify(exactly = 1) { source.onPull(any(), any(), any()) } } @Test @@ -259,7 +260,7 @@ internal class FlowForwarderTest { } @Test - fun testEventFailure() = runBlockingSimulation { + fun testStartFailure() = runBlockingSimulation { val engine = FlowEngineImpl(coroutineContext, clock) val forwarder = FlowForwarder(engine) val source = FlowSink(engine, 2000.0) @@ -272,7 +273,7 @@ internal class FlowForwarderTest { return Long.MAX_VALUE } - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + override fun onStart(conn: FlowConnection, now: Long) { throw IllegalStateException("Test") } }) @@ -287,7 +288,7 @@ internal class FlowForwarderTest { } @Test - fun testEventConvergeFailure() = runBlockingSimulation { + fun testConvergeFailure() = runBlockingSimulation { val engine = FlowEngineImpl(coroutineContext, clock) val forwarder = FlowForwarder(engine) val source = FlowSink(engine, 2000.0) @@ -300,10 +301,8 @@ internal class FlowForwarderTest { return Long.MAX_VALUE } - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - if (event == FlowEvent.Converge) { - throw IllegalStateException("Test") - } + override fun onConverge(conn: FlowConnection, now: Long, delta: Long) { + throw IllegalStateException("Test") } }) } catch (cause: Throwable) { diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt index 5d579e5d..70c75864 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowSinkTest.kt @@ -22,8 +22,6 @@ package org.opendc.simulator.flow -import io.mockk.every -import io.mockk.mockk import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.* @@ -67,7 +65,7 @@ internal class FlowSinkTest { provider.capacity = 0.5 } assertEquals(3000, clock.millis()) - verify(exactly = 1) { consumer.onPull(any(), any(), any()) } + verify(exactly = 3) { consumer.onPull(any(), any(), any()) } } @Test @@ -102,7 +100,7 @@ internal class FlowSinkTest { return Long.MAX_VALUE } - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { + override fun onStart(conn: FlowConnection, now: Long) { conn.pull() } } @@ -120,11 +118,8 @@ internal class FlowSinkTest { val consumer = object : FlowSource { var isFirst = true - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - when (event) { - FlowEvent.Start -> resCtx = conn - else -> {} - } + override fun onStart(conn: FlowConnection, now: Long) { + resCtx = conn } override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { @@ -154,9 +149,15 @@ internal class FlowSinkTest { val capacity = 4200.0 val provider = FlowSink(engine, capacity) - val consumer = mockk(relaxUnitFun = true) - every { consumer.onEvent(any(), any(), eq(FlowEvent.Start)) } - .throws(IllegalStateException()) + val consumer = object : FlowSource { + override fun onStart(conn: FlowConnection, now: Long) { + throw IllegalStateException("Hi") + } + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return Long.MAX_VALUE + } + } assertThrows { provider.consume(consumer) diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt index c8627446..3475f027 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt @@ -108,11 +108,8 @@ internal class ExclusiveFlowMultiplexerTest { val workload = object : FlowSource { var isFirst = true - override fun onEvent(conn: FlowConnection, now: Long, event: FlowEvent) { - when (event) { - FlowEvent.Start -> isFirst = true - else -> {} - } + override fun onStart(conn: FlowConnection, now: Long) { + isFirst = true } override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { diff --git a/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSinkTest.kt b/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSinkTest.kt index 45d0bcf0..14d22162 100644 --- a/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSinkTest.kt +++ b/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSinkTest.kt @@ -108,7 +108,7 @@ class SimNetworkSinkTest { assertTrue(source.isConnected) verify { source.createConsumer() } - verify { consumer.onEvent(any(), any(), FlowEvent.Start) } + verify { consumer.onStart(any(), any()) } } @Test @@ -124,7 +124,7 @@ class SimNetworkSinkTest { assertFalse(sink.isConnected) assertFalse(source.isConnected) - verify { consumer.onEvent(any(), any(), FlowEvent.Exit) } + verify { consumer.onStop(any(), any(), any()) } } private class Source(engine: FlowEngine) : SimNetworkPort() { diff --git a/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtualTest.kt b/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtualTest.kt index 4aa2fa92..62e54ffb 100644 --- a/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtualTest.kt +++ b/opendc-simulator/opendc-simulator-network/src/test/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtualTest.kt @@ -50,7 +50,7 @@ class SimNetworkSwitchVirtualTest { assertTrue(source.isConnected) verify { source.createConsumer() } - verify { consumer.onEvent(any(), any(), FlowEvent.Start) } + verify { consumer.onStart(any(), any()) } } @Test diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt index ff447703..85b9ab01 100644 --- a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt @@ -30,7 +30,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation import org.opendc.simulator.flow.FlowEngine -import org.opendc.simulator.flow.FlowEvent import org.opendc.simulator.flow.FlowSource import org.opendc.simulator.flow.source.FixedFlowSource @@ -87,7 +86,7 @@ internal class SimPduTest { outlet.connect(inlet) outlet.disconnect() - verify { consumer.onEvent(any(), any(), FlowEvent.Exit) } + verify { consumer.onStop(any(), any(), any()) } } @Test diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt index b411e292..20677633 100644 --- a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt @@ -32,7 +32,6 @@ import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation import org.opendc.simulator.flow.FlowEngine -import org.opendc.simulator.flow.FlowEvent import org.opendc.simulator.flow.FlowSource import org.opendc.simulator.flow.source.FixedFlowSource @@ -86,7 +85,7 @@ internal class SimPowerSourceTest { source.connect(inlet) source.disconnect() - verify { consumer.onEvent(any(), any(), FlowEvent.Exit) } + verify { consumer.onStop(any(), any(), any()) } } @Test diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt index 31ac0b39..c6e0605a 100644 --- a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt @@ -29,7 +29,6 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.opendc.simulator.core.runBlockingSimulation import org.opendc.simulator.flow.FlowEngine -import org.opendc.simulator.flow.FlowEvent import org.opendc.simulator.flow.FlowSource import org.opendc.simulator.flow.source.FixedFlowSource @@ -93,7 +92,7 @@ internal class SimUpsTest { ups.connect(inlet) ups.disconnect() - verify { consumer.onEvent(any(), any(), FlowEvent.Exit) } + verify { consumer.onStop(any(), any(), any()) } } class SimpleInlet : SimPowerInlet() { -- cgit v1.2.3 From 94783ff9d8cd81275fefd5804ac99f98e2dee3a4 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 30 Sep 2021 16:00:05 +0200 Subject: fix(simulator): Fix loss computation for UPS and PDU This change fixes the loss computation for both the UPS and PDU implementation that was broken due to the new pushing mechanism. We implement a new class FlowMapper that can be used to map the flow pushed by a `FlowSource` using a user-specified method. --- .../org/opendc/simulator/compute/device/SimPsu.kt | 2 +- .../kotlin/org/opendc/simulator/flow/FlowMapper.kt | 75 ++++++++++++++++++++++ .../flow/internal/FlowConsumerContextImpl.kt | 1 + .../kotlin/org/opendc/simulator/power/SimPdu.kt | 16 ++--- .../org/opendc/simulator/power/SimPowerInlet.kt | 2 +- .../org/opendc/simulator/power/SimPowerSource.kt | 2 +- .../kotlin/org/opendc/simulator/power/SimUps.kt | 18 ++---- .../org/opendc/simulator/power/SimPduTest.kt | 6 +- .../opendc/simulator/power/SimPowerSourceTest.kt | 6 +- .../org/opendc/simulator/power/SimUpsTest.kt | 4 +- 10 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowMapper.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt index 8400c225..62d91c0b 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt @@ -80,7 +80,7 @@ public class SimPsu( update() } - override fun createConsumer(): FlowSource = object : FlowSource { + override fun createSource(): FlowSource = object : FlowSource { override fun onStart(conn: FlowConnection, now: Long) { _ctx = conn } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowMapper.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowMapper.kt new file mode 100644 index 00000000..6867bcef --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowMapper.kt @@ -0,0 +1,75 @@ +/* + * 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.flow + +/** + * A [FlowConsumer] that maps the pushed flow through [transform]. + * + * @param source The source of the flow. + * @param transform The method to transform the flow. + */ +public class FlowMapper( + private val source: FlowSource, + private val transform: (FlowConnection, Double) -> Double +) : FlowSource { + + /** + * The current active connection. + */ + private var _conn: Connection? = null + + override fun onStart(conn: FlowConnection, now: Long) { + check(_conn == null) { "Concurrent access" } + val delegate = Connection(conn, transform) + _conn = delegate + source.onStart(delegate, now) + } + + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { + val delegate = checkNotNull(_conn) { "Invariant violation" } + _conn = null + source.onStop(delegate, now, delta) + } + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + val delegate = checkNotNull(_conn) { "Invariant violation" } + return source.onPull(delegate, now, delta) + } + + override fun onConverge(conn: FlowConnection, now: Long, delta: Long) { + val delegate = _conn ?: return + source.onConverge(delegate, now, delta) + } + + /** + * The wrapper [FlowConnection] that is used to transform the flow. + */ + private class Connection( + private val delegate: FlowConnection, + private val transform: (FlowConnection, Double) -> Double + ) : FlowConnection by delegate { + override fun push(rate: Double) { + delegate.push(transform(this, rate)) + } + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt index c235b9ae..55fa92df 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt @@ -127,6 +127,7 @@ internal class FlowConsumerContextImpl( engine.batch { source.onStart(this, _clock.millis()) _state = State.Active + pull() } } diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt index c33f5186..d536f22d 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt @@ -57,17 +57,9 @@ public class SimPdu( mux.addOutput(forwarder) } - override fun createConsumer(): FlowSource = object : FlowSource by forwarder { - override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { - val duration = forwarder.onPull(conn, now, delta) - val loss = computePowerLoss(conn.demand) - val newLimit = conn.demand + loss - - conn.push(newLimit) - return duration - } - - override fun toString(): String = "SimPdu.Consumer" + override fun createSource(): FlowSource = FlowMapper(forwarder) { _, rate -> + val loss = computePowerLoss(rate) + rate + loss } override fun toString(): String = "SimPdu" @@ -85,7 +77,7 @@ public class SimPdu( */ public class Outlet(private val switch: FlowMultiplexer, private val provider: FlowConsumer) : SimPowerOutlet(), AutoCloseable { override fun onConnect(inlet: SimPowerInlet) { - provider.startConsumer(inlet.createConsumer()) + provider.startConsumer(inlet.createSource()) } override fun onDisconnect(inlet: SimPowerInlet) { diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt index 851b28a5..de587b7f 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt @@ -44,5 +44,5 @@ public abstract class SimPowerInlet { /** * Create a [FlowSource] which represents the consumption of electricity from the power outlet. */ - public abstract fun createConsumer(): FlowSource + public abstract fun createSource(): FlowSource } diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt index 7faebd75..07e9f52e 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt @@ -43,7 +43,7 @@ public class SimPowerSource(engine: FlowEngine, public val capacity: Double) : S get() = source.rate override fun onConnect(inlet: SimPowerInlet) { - source.startConsumer(inlet.createConsumer()) + source.startConsumer(inlet.createSource()) } override fun onDisconnect(inlet: SimPowerInlet) { diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt index 5eaa91af..312f1d0f 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt @@ -59,17 +59,13 @@ public class SimUps( } override fun onConnect(inlet: SimPowerInlet) { - val consumer = inlet.createConsumer() - provider.startConsumer(object : FlowSource by consumer { - override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { - val duration = consumer.onPull(conn, now, delta) - val loss = computePowerLoss(conn.demand) - val newLimit = conn.demand + loss + val source = inlet.createSource() + val mapper = FlowMapper(source) { _, rate -> + val loss = computePowerLoss(rate) + rate + loss + } - conn.push(newLimit) - return duration - } - }) + provider.startConsumer(mapper) } override fun onDisconnect(inlet: SimPowerInlet) { @@ -88,7 +84,7 @@ public class SimUps( * A UPS inlet. */ public inner class Inlet(private val forwarder: FlowForwarder) : SimPowerInlet(), AutoCloseable { - override fun createConsumer(): FlowSource = forwarder + override fun createSource(): FlowSource = forwarder /** * Remove the inlet from the PSU. diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt index 85b9ab01..eb823eb1 100644 --- a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt @@ -25,7 +25,6 @@ package org.opendc.simulator.power import io.mockk.spyk import io.mockk.verify import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.opendc.simulator.core.runBlockingSimulation @@ -79,7 +78,7 @@ internal class SimPduTest { source.connect(pdu) val consumer = spyk(FixedFlowSource(100.0, utilization = 1.0)) val inlet = object : SimPowerInlet() { - override fun createConsumer(): FlowSource = consumer + override fun createSource(): FlowSource = consumer } val outlet = pdu.newOutlet() @@ -90,7 +89,6 @@ internal class SimPduTest { } @Test - @Disabled fun testLoss() = runBlockingSimulation { val engine = FlowEngine(coroutineContext, clock) val source = SimPowerSource(engine, capacity = 100.0) @@ -116,6 +114,6 @@ internal class SimPduTest { } class SimpleInlet : SimPowerInlet() { - override fun createConsumer(): FlowSource = FixedFlowSource(100.0, utilization = 0.5) + override fun createSource(): FlowSource = FixedFlowSource(100.0, utilization = 0.5) } } diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt index 20677633..76142103 100644 --- a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt @@ -79,7 +79,7 @@ internal class SimPowerSourceTest { val source = SimPowerSource(engine, capacity = 100.0) val consumer = spyk(FixedFlowSource(100.0, utilization = 1.0)) val inlet = object : SimPowerInlet() { - override fun createConsumer(): FlowSource = consumer + override fun createSource(): FlowSource = consumer } source.connect(inlet) @@ -95,7 +95,7 @@ internal class SimPowerSourceTest { val inlet = mockk(relaxUnitFun = true) every { inlet.isConnected } returns false every { inlet._outlet } returns null - every { inlet.createConsumer() } returns FixedFlowSource(100.0, utilization = 1.0) + every { inlet.createSource() } returns FixedFlowSource(100.0, utilization = 1.0) source.connect(inlet) @@ -131,6 +131,6 @@ internal class SimPowerSourceTest { } class SimpleInlet : SimPowerInlet() { - override fun createConsumer(): FlowSource = FixedFlowSource(100.0, utilization = 1.0) + override fun createSource(): FlowSource = FixedFlowSource(100.0, utilization = 1.0) } } diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt index c6e0605a..a764a368 100644 --- a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt @@ -86,7 +86,7 @@ internal class SimUpsTest { source2.connect(ups.newInlet()) val consumer = spyk(FixedFlowSource(100.0, utilization = 1.0)) val inlet = object : SimPowerInlet() { - override fun createConsumer(): FlowSource = consumer + override fun createSource(): FlowSource = consumer } ups.connect(inlet) @@ -96,6 +96,6 @@ internal class SimUpsTest { } class SimpleInlet : SimPowerInlet() { - override fun createConsumer(): FlowSource = FixedFlowSource(100.0, utilization = 0.5) + override fun createSource(): FlowSource = FixedFlowSource(100.0, utilization = 0.5) } } -- cgit v1.2.3 From 559ac2327b8aa319fb8ab4558d4f4aa3382349f4 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 30 Sep 2021 16:27:45 +0200 Subject: perf(simulator): Make convergence callback optional This change adds two new properties for controlling whether the convergence callbacks of the source and consumer respectively should be invoked. This saves a lot of unnecessary calls for stages that do not have any implementation of the `onConvergence` method. --- .../opendc/simulator/compute/SimAbstractMachine.kt | 8 +++- .../simulator/compute/SimBareMetalMachine.kt | 4 +- .../org/opendc/simulator/compute/device/SimPsu.kt | 1 + .../compute/kernel/SimAbstractHypervisor.kt | 2 - .../compute/kernel/SimFairShareHypervisor.kt | 16 ++++---- .../kernel/SimFairShareHypervisorProvider.kt | 4 +- .../compute/kernel/SimHypervisorProvider.kt | 4 +- .../kernel/SimSpaceSharedHypervisorProvider.kt | 4 +- .../org/opendc/simulator/flow/FlowConnection.kt | 5 +++ .../opendc/simulator/flow/FlowConsumerContext.kt | 5 +++ .../org/opendc/simulator/flow/FlowConsumerLogic.kt | 3 ++ .../simulator/flow/FlowConvergenceListener.kt | 36 ++++++++++++++++++ .../org/opendc/simulator/flow/FlowForwarder.kt | 10 +++++ .../kotlin/org/opendc/simulator/flow/FlowSink.kt | 11 +++++- .../kotlin/org/opendc/simulator/flow/FlowSource.kt | 3 ++ .../kotlin/org/opendc/simulator/flow/FlowSystem.kt | 43 ---------------------- .../flow/internal/FlowConsumerContextImpl.kt | 39 +++++++++++++++----- .../simulator/flow/mux/MaxMinFlowMultiplexer.kt | 9 ++++- .../simulator/flow/source/FlowSourceRateAdapter.kt | 14 +++++-- .../org/opendc/simulator/flow/FlowForwarderTest.kt | 4 ++ 20 files changed, 145 insertions(+), 80 deletions(-) create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConvergenceListener.kt delete mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSystem.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimAbstractMachine.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimAbstractMachine.kt index 6a62d8a5..60a10f20 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimAbstractMachine.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimAbstractMachine.kt @@ -43,9 +43,9 @@ import kotlin.coroutines.resume */ public abstract class SimAbstractMachine( protected val engine: FlowEngine, - final override val parent: FlowSystem?, + private val parent: FlowConvergenceListener?, final override val model: MachineModel -) : SimMachine, FlowSystem { +) : SimMachine, FlowConvergenceListener { /** * The resources allocated for this machine. */ @@ -116,6 +116,10 @@ public abstract class SimAbstractMachine( cancel() } + override fun onConverge(now: Long, delta: Long) { + parent?.onConverge(now, delta) + } + /** * Cancel the workload that is currently running on the machine. */ diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt index 37cf282b..0bcf5957 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt @@ -46,7 +46,7 @@ public class SimBareMetalMachine( model: MachineModel, powerDriver: PowerDriver, public val psu: SimPsu = SimPsu(500.0, mapOf(1.0 to 1.0)), - parent: FlowSystem? = null, + parent: FlowConvergenceListener? = null, ) : SimAbstractMachine(engine, parent, model) { /** * The power draw of the machine onto the PSU. @@ -66,7 +66,7 @@ public class SimBareMetalMachine( */ private val powerDriverLogic = powerDriver.createLogic(this, cpus) - override fun onConverge(timestamp: Long) { + override fun onConverge(now: Long, delta: Long) { psu.update() } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt index 62d91c0b..09defbb5 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/device/SimPsu.kt @@ -83,6 +83,7 @@ public class SimPsu( override fun createSource(): FlowSource = object : FlowSource { override fun onStart(conn: FlowConnection, now: Long) { _ctx = conn + conn.shouldSourceConverge = true } override fun onStop(conn: FlowConnection, now: Long, delta: Long) { diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt index b145eefc..bcba8e8e 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt @@ -145,8 +145,6 @@ public abstract class SimAbstractHypervisor( interferenceDomain?.leave(interferenceKey) } } - - override fun onConverge(timestamp: Long) {} } /** diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt index 36ab7c1c..b0515c6e 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt @@ -28,8 +28,8 @@ import org.opendc.simulator.compute.kernel.cpufreq.ScalingGovernor import org.opendc.simulator.compute.kernel.interference.VmInterferenceDomain import org.opendc.simulator.compute.model.MachineModel import org.opendc.simulator.compute.workload.SimWorkload +import org.opendc.simulator.flow.FlowConvergenceListener import org.opendc.simulator.flow.FlowEngine -import org.opendc.simulator.flow.FlowSystem import org.opendc.simulator.flow.mux.FlowMultiplexer import org.opendc.simulator.flow.mux.MaxMinFlowMultiplexer @@ -45,7 +45,7 @@ import org.opendc.simulator.flow.mux.MaxMinFlowMultiplexer */ public class SimFairShareHypervisor( engine: FlowEngine, - private val parent: FlowSystem? = null, + private val parent: FlowConvergenceListener? = null, scalingGovernor: ScalingGovernor? = null, interferenceDomain: VmInterferenceDomain? = null, private val listener: SimHypervisor.Listener? = null @@ -57,11 +57,9 @@ public class SimFairShareHypervisor( return SwitchSystem(ctx).switch } - private inner class SwitchSystem(private val ctx: SimMachineContext) : FlowSystem { + private inner class SwitchSystem(private val ctx: SimMachineContext) : FlowConvergenceListener { val switch = MaxMinFlowMultiplexer(engine, this, interferenceDomain) - override val parent: FlowSystem? = this@SimFairShareHypervisor.parent - private var lastCpuUsage = 0.0 private var lastCpuDemand = 0.0 private var lastDemand = 0.0 @@ -70,11 +68,11 @@ public class SimFairShareHypervisor( private var lastInterference = 0.0 private var lastReport = Long.MIN_VALUE - override fun onConverge(timestamp: Long) { + override fun onConverge(now: Long, delta: Long) { val listener = listener ?: return val counters = switch.counters - if (timestamp > lastReport) { + if (now > lastReport) { listener.onSliceFinish( this@SimFairShareHypervisor, counters.demand - lastDemand, @@ -85,7 +83,7 @@ public class SimFairShareHypervisor( lastCpuDemand ) } - lastReport = timestamp + lastReport = now lastCpuDemand = switch.outputs.sumOf { it.demand } lastCpuUsage = switch.outputs.sumOf { it.rate } @@ -96,6 +94,8 @@ public class SimFairShareHypervisor( val load = lastCpuDemand / ctx.cpus.sumOf { it.model.frequency } triggerGovernors(load) + + parent?.onConverge(now, delta) } } } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt index bfa099fb..e0a70926 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt @@ -24,8 +24,8 @@ package org.opendc.simulator.compute.kernel import org.opendc.simulator.compute.kernel.cpufreq.ScalingGovernor import org.opendc.simulator.compute.kernel.interference.VmInterferenceDomain +import org.opendc.simulator.flow.FlowConvergenceListener import org.opendc.simulator.flow.FlowEngine -import org.opendc.simulator.flow.FlowSystem /** * A [SimHypervisorProvider] for the [SimFairShareHypervisor] implementation. @@ -35,7 +35,7 @@ public class SimFairShareHypervisorProvider : SimHypervisorProvider { override fun create( engine: FlowEngine, - parent: FlowSystem?, + parent: FlowConvergenceListener?, scalingGovernor: ScalingGovernor?, interferenceDomain: VmInterferenceDomain?, listener: SimHypervisor.Listener? diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt index 97f07097..dad2cc3b 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt @@ -24,8 +24,8 @@ package org.opendc.simulator.compute.kernel import org.opendc.simulator.compute.kernel.cpufreq.ScalingGovernor import org.opendc.simulator.compute.kernel.interference.VmInterferenceDomain +import org.opendc.simulator.flow.FlowConvergenceListener import org.opendc.simulator.flow.FlowEngine -import org.opendc.simulator.flow.FlowSystem /** * A service provider interface for constructing a [SimHypervisor]. @@ -44,7 +44,7 @@ public interface SimHypervisorProvider { */ public fun create( engine: FlowEngine, - parent: FlowSystem? = null, + parent: FlowConvergenceListener? = null, scalingGovernor: ScalingGovernor? = null, interferenceDomain: VmInterferenceDomain? = null, listener: SimHypervisor.Listener? = null diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt index 7869d72d..93921eb9 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt @@ -24,8 +24,8 @@ package org.opendc.simulator.compute.kernel import org.opendc.simulator.compute.kernel.cpufreq.ScalingGovernor import org.opendc.simulator.compute.kernel.interference.VmInterferenceDomain +import org.opendc.simulator.flow.FlowConvergenceListener import org.opendc.simulator.flow.FlowEngine -import org.opendc.simulator.flow.FlowSystem /** * A [SimHypervisorProvider] for the [SimSpaceSharedHypervisor] implementation. @@ -35,7 +35,7 @@ public class SimSpaceSharedHypervisorProvider : SimHypervisorProvider { override fun create( engine: FlowEngine, - parent: FlowSystem?, + parent: FlowConvergenceListener?, scalingGovernor: ScalingGovernor?, interferenceDomain: VmInterferenceDomain?, listener: SimHypervisor.Listener? diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConnection.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConnection.kt index fa833961..c327e1e9 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConnection.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConnection.kt @@ -41,6 +41,11 @@ public interface FlowConnection : AutoCloseable { */ public val demand: Double + /** + * A flag to control whether [FlowSource.onConverge] should be invoked for this source. + */ + public var shouldSourceConverge: Boolean + /** * Pull the source. */ diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerContext.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerContext.kt index 75b2d25b..15f9b93b 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerContext.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerContext.kt @@ -33,6 +33,11 @@ public interface FlowConsumerContext : FlowConnection { */ public override var capacity: Double + /** + * A flag to control whether [FlowConsumerLogic.onConverge] should be invoked for the consumer. + */ + public var shouldConsumerConverge: Boolean + /** * Start the flow over the connection. */ diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt index ef94ab22..50fbc9c7 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConsumerLogic.kt @@ -39,6 +39,9 @@ public interface FlowConsumerLogic { /** * This method is invoked when the flow graph has converged into a steady-state system. * + * Make sure to enable [FlowConsumerContext.shouldSourceConverge] if you need this callback. By default, this method + * will not be invoked. + * * @param ctx The context in which the provider runs. * @param now The virtual timestamp in milliseconds at which the system converged. * @param delta The virtual duration between this call and the last call to [onConverge] in milliseconds. diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConvergenceListener.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConvergenceListener.kt new file mode 100644 index 00000000..d1afda6f --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowConvergenceListener.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.simulator.flow + +/** + * A listener interface for when a flow stage has converged into a steady-state. + */ +public interface FlowConvergenceListener { + /** + * This method is invoked when the system has converged to a steady-state. + * + * @param now The timestamp at which the system converged. + * @param delta The virtual duration between this call and the last call to [onConverge] in milliseconds. + */ + public fun onConverge(now: Long, delta: Long) {} +} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt index ab5b31c2..17de601a 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt @@ -52,6 +52,12 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled * The exposed [FlowConnection]. */ private val _ctx = object : FlowConnection { + override var shouldSourceConverge: Boolean = false + set(value) { + field = value + _innerCtx?.shouldSourceConverge = value + } + override val capacity: Double get() = _innerCtx?.capacity ?: 0.0 @@ -141,6 +147,10 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled override fun onStart(conn: FlowConnection, now: Long) { _innerCtx = conn + + if (_ctx.shouldSourceConverge) { + conn.shouldSourceConverge = true + } } override fun onStop(conn: FlowConnection, now: Long, delta: Long) { diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt index fc590177..549a338b 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt @@ -32,9 +32,16 @@ package org.opendc.simulator.flow public class FlowSink( private val engine: FlowEngine, initialCapacity: Double, - private val parent: FlowSystem? = null + private val parent: FlowConvergenceListener? = null ) : AbstractFlowConsumer(engine, initialCapacity) { + override fun start(ctx: FlowConsumerContext) { + if (parent != null) { + ctx.shouldConsumerConverge = true + } + super.start(ctx) + } + override fun createLogic(): FlowConsumerLogic { return object : FlowConsumerLogic { override fun onPush( @@ -52,7 +59,7 @@ public class FlowSink( } override fun onConverge(ctx: FlowConsumerContext, now: Long, delta: Long) { - parent?.onConverge(now) + parent?.onConverge(now, delta) } } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt index a4f624ef..3a7e52aa 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSource.kt @@ -59,6 +59,9 @@ public interface FlowSource { /** * This method is invoked when the flow graph has converged into a steady-state system. * + * Make sure to enable [FlowConnection.shouldSourceConverge] if you need this callback. By default, this method + * will not be invoked. + * * @param conn The connection between the source and consumer. * @param now The virtual timestamp in milliseconds at which the system converged. * @param delta The virtual duration between this call and the last call to [onConverge] in milliseconds. diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSystem.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSystem.kt deleted file mode 100644 index db6aa69f..00000000 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSystem.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.flow - -/** - * A system of possible multiple sub-resources. - * - * This interface is used to model hierarchies of resource providers, which can listen efficiently to changes of the - * resource provider. - */ -public interface FlowSystem { - /** - * The parent system to which this system belongs or `null` if it has no parent. - */ - public val parent: FlowSystem? - - /** - * This method is invoked when the system has converged to a steady-state. - * - * @param timestamp The timestamp at which the system converged. - */ - public fun onConverge(timestamp: Long) -} diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt index 55fa92df..c087a28d 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt @@ -72,6 +72,12 @@ internal class FlowConsumerContextImpl( override val demand: Double get() = _demand + /** + * Flags to control the convergence of the consumer and source. + */ + override var shouldConsumerConverge: Boolean = false + override var shouldSourceConverge: Boolean = false + /** * The clock to track simulation time. */ @@ -114,7 +120,8 @@ internal class FlowConsumerContextImpl( */ private var _lastPull: Long = Long.MIN_VALUE // Last call to `onPull` private var _lastPush: Long = Long.MIN_VALUE // Last call to `onPush` - private var _lastConvergence: Long = Long.MAX_VALUE // Last call to `onConvergence` + private var _lastSourceConvergence: Long = Long.MAX_VALUE // Last call to source `onConvergence` + private var _lastConsumerConvergence: Long = Long.MAX_VALUE // Last call to consumer `onConvergence` /** * The timers at which the context is scheduled to be interrupted. @@ -199,8 +206,14 @@ internal class FlowConsumerContextImpl( * @return A flag to indicate whether the connection has already been updated before convergence. */ fun doUpdate(now: Long): Boolean { - val willConverge = _willConverge - _willConverge = true + // The connection will only converge if either the source or the consumer wants the converge callback to be + // invoked. + val shouldConverge = shouldSourceConverge || shouldConsumerConverge + var willConverge = false + if (shouldConverge) { + willConverge = _willConverge + _willConverge = true + } val oldState = _state if (oldState != State.Active) { @@ -286,19 +299,25 @@ internal class FlowConsumerContextImpl( /** * This method is invoked when the system converges into a steady state. */ - fun onConverge(timestamp: Long) { - val delta = max(0, timestamp - _lastConvergence) - _lastConvergence = timestamp + fun onConverge(now: Long) { _willConverge = false try { - if (_state == State.Active) { - source.onConverge(this, timestamp, delta) + if (_state == State.Active && shouldSourceConverge) { + val delta = max(0, now - _lastSourceConvergence) + _lastSourceConvergence = now + + source.onConverge(this, now, delta) } - logic.onConverge(this, timestamp, delta) + if (shouldConsumerConverge) { + val delta = max(0, now - _lastConsumerConvergence) + _lastConsumerConvergence = now + + logic.onConverge(this, now, delta) + } } catch (cause: Throwable) { - doFailSource(timestamp, cause) + doFailSource(now, cause) } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt index c7379fa9..7232df35 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt @@ -38,7 +38,7 @@ import kotlin.math.min */ public class MaxMinFlowMultiplexer( private val engine: FlowEngine, - private val parent: FlowSystem? = null, + private val parent: FlowConvergenceListener? = null, private val interferenceDomain: InterferenceDomain? = null ) : FlowMultiplexer { /** @@ -269,6 +269,11 @@ public class MaxMinFlowMultiplexer( check(!_isClosed) { "Cannot re-use closed input" } _activeInputs += this + + if (parent != null) { + ctx.shouldConsumerConverge = true + } + super.start(ctx) } @@ -289,7 +294,7 @@ public class MaxMinFlowMultiplexer( } override fun onConverge(ctx: FlowConsumerContext, now: Long, delta: Long) { - parent?.onConverge(now) + parent?.onConverge(now, delta) } override fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long, cause: Throwable?) { diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt index 0c39523f..6dd60d95 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/source/FlowSourceRateAdapter.kt @@ -47,6 +47,12 @@ public class FlowSourceRateAdapter( callback(0.0) } + override fun onStart(conn: FlowConnection, now: Long) { + conn.shouldSourceConverge = true + + delegate.onStart(conn, now) + } + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { try { delegate.onStop(conn, now, delta) @@ -60,9 +66,11 @@ public class FlowSourceRateAdapter( } override fun onConverge(conn: FlowConnection, now: Long, delta: Long) { - delegate.onConverge(conn, now, delta) - - rate = conn.rate + try { + delegate.onConverge(conn, now, delta) + } finally { + rate = conn.rate + } } override fun toString(): String = "FlowSourceRateAdapter[delegate=$delegate]" diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt index 3690e681..d548451f 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt @@ -297,6 +297,10 @@ internal class FlowForwarderTest { try { forwarder.consume(object : FlowSource { + override fun onStart(conn: FlowConnection, now: Long) { + conn.shouldSourceConverge = true + } + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { return Long.MAX_VALUE } -- cgit v1.2.3 From de8509b50d5acb7e739eb5cb4d29b03a627c8985 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Fri, 1 Oct 2021 11:40:21 +0200 Subject: perf(simulator): Reduce field accesses in FlowConsumerContextImpl This change updates the implementation of FlowConsumerContextImpl to reduce the number of field accesses by storing the flags of the connection inside a single integer. --- .../kotlin/org/opendc/simulator/flow/FlowSink.kt | 2 + .../org/opendc/simulator/flow/internal/Flags.kt | 43 +++ .../flow/internal/FlowConsumerContextImpl.kt | 338 ++++++++++----------- .../simulator/flow/internal/FlowEngineImpl.kt | 112 ++----- 4 files changed, 241 insertions(+), 254 deletions(-) create mode 100644 opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/Flags.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt index 549a338b..b4eb6a38 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowSink.kt @@ -44,6 +44,8 @@ public class FlowSink( override fun createLogic(): FlowConsumerLogic { return object : FlowConsumerLogic { + private val parent = this@FlowSink.parent + override fun onPush( ctx: FlowConsumerContext, now: Long, diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/Flags.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/Flags.kt new file mode 100644 index 00000000..81ed9f26 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/Flags.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.simulator.flow.internal + +/** + * States of the flow connection. + */ +internal const val ConnPending = 0 // Connection is pending and the consumer is waiting to consume the source +internal const val ConnActive = 1 // Connection is active and the source is currently being consumed +internal const val ConnClosed = 2 // Connection is closed and source cannot be consumed through this connection anymore +internal const val ConnState = 0b11 // Mask for accessing the state of the flow connection + +/** + * Flags of the flow connection + */ +internal const val ConnPulled = 1 shl 2 // The source should be pulled +internal const val ConnPushed = 1 shl 3 // The source has pushed a value +internal const val ConnUpdateActive = 1 shl 4 // An update for the connection is active +internal const val ConnUpdatePending = 1 shl 5 // An (immediate) update of the connection is pending +internal const val ConnUpdateSkipped = 1 shl 6 // An update of the connection was not necessary +internal const val ConnConvergePending = 1 shl 7 // Indication that a convergence is already pending +internal const val ConnConvergeSource = 1 shl 8 // Enable convergence of the source +internal const val ConnConvergeConsumer = 1 shl 9 // Enable convergence of the consumer diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt index c087a28d..9d36483e 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowConsumerContextImpl.kt @@ -24,7 +24,7 @@ package org.opendc.simulator.flow.internal import mu.KotlinLogging import org.opendc.simulator.flow.* -import java.util.ArrayDeque +import java.util.* import kotlin.math.max import kotlin.math.min @@ -44,20 +44,18 @@ internal class FlowConsumerContextImpl( /** * The capacity of the connection. */ - override var capacity: Double = 0.0 + override var capacity: Double + get() = _capacity set(value) { - val oldValue = field + val oldValue = _capacity // Only changes will be propagated if (value != oldValue) { - field = value - - // Do not pull the source if it has not been started yet - if (_state == State.Active) { - pull() - } + _capacity = value + pull() } } + private var _capacity: Double = 0.0 /** * The current processing rate of the connection. @@ -75,45 +73,41 @@ internal class FlowConsumerContextImpl( /** * Flags to control the convergence of the consumer and source. */ - override var shouldConsumerConverge: Boolean = false override var shouldSourceConverge: Boolean = false + set(value) { + field = value + _flags = + if (value) + _flags or ConnConvergeSource + else + _flags and ConnConvergeSource.inv() + } + override var shouldConsumerConverge: Boolean = false + set(value) { + field = value + + _flags = + if (value) + _flags or ConnConvergeConsumer + else + _flags and ConnConvergeConsumer.inv() + } /** * The clock to track simulation time. */ private val _clock = engine.clock - /** - * A flag to indicate the state of the connection. - */ - private var _state = State.Pending - /** * The current state of the connection. */ private var _demand: Double = 0.0 // The current (pending) demand of the source - private var _activeDemand: Double = 0.0 // The previous demand of the source private var _deadline: Long = Long.MAX_VALUE // The deadline of the source's timer /** - * A flag to indicate that the source should be pulled. - */ - private var _isPulled = false - - /** - * A flag to indicate that an update is active. - */ - private var _isUpdateActive = false - - /** - * A flag to indicate that an immediate update is scheduled. + * The flags of the flow connection, indicating certain actions. */ - private var _isImmediateUpdateScheduled = false - - /** - * A flag that indicates to the [FlowEngine] that the context is already enqueued to converge. - */ - private var _willConverge: Boolean = false + private var _flags: Int = 0 /** * The timestamp of calls to the callbacks. @@ -130,44 +124,53 @@ internal class FlowConsumerContextImpl( private val _pendingTimers: ArrayDeque = ArrayDeque(5) override fun start() { - check(_state == State.Pending) { "Consumer is already started" } + check(_flags and ConnState == ConnPending) { "Consumer is already started" } engine.batch { - source.onStart(this, _clock.millis()) - _state = State.Active + val now = _clock.millis() + source.onStart(this, now) - pull() + // Mark the connection as active and pulled + val newFlags = (_flags and ConnState.inv()) or ConnActive or ConnPulled + scheduleImmediate(now, newFlags) } } override fun close() { - if (_state == State.Closed) { + var flags = _flags + if (flags and ConnState == ConnClosed) { return } engine.batch { - _state = State.Closed - if (!_isUpdateActive) { + // Mark the connection as closed and pulled (in order to converge) + flags = (flags and ConnState.inv()) or ConnClosed or ConnPulled + _flags = flags + + if (flags and ConnUpdateActive == 0) { val now = _clock.millis() doStopSource(now) // FIX: Make sure the context converges - pull() + scheduleImmediate(now, flags) } } } override fun pull() { - if (_state == State.Closed) { + val flags = _flags + if (flags and ConnState != ConnActive) { return } - _isPulled = true - scheduleImmediate() + // Mark connection as pulled + scheduleImmediate(_clock.millis(), flags or ConnPulled) } override fun flush() { + val flags = _flags + // Do not attempt to flush the connection if the connection is closed or an update is already active - if (_state == State.Closed || _isUpdateActive) { + if (flags and ConnState != ConnActive || flags and ConnUpdateActive != 0) { return } @@ -181,118 +184,145 @@ internal class FlowConsumerContextImpl( _demand = rate - // Invalidate only if the active demand is changed and no update is active - // If an update is active, it will already get picked up at the end of the update - if (_activeDemand != rate && !_isUpdateActive) { - scheduleImmediate() - } - } + val flags = _flags - /** - * Determine whether the state of the flow connection should be updated. - */ - fun shouldUpdate(timestamp: Long): Boolean { - // The flow connection should be updated for three reasons: - // (1) The source should be pulled (after a call to `pull`) - // (2) The demand of the source has changed (after a call to `push`) - // (3) The timer of the source expired - return _isPulled || _demand != _activeDemand || _deadline == timestamp + if (flags and ConnUpdateActive != 0) { + // If an update is active, it will already get picked up at the end of the update + _flags = flags or ConnPushed + } else { + // Invalidate only if no update is active + scheduleImmediate(_clock.millis(), flags or ConnPushed) + } } /** * Update the state of the flow connection. * * @param now The current virtual timestamp. - * @return A flag to indicate whether the connection has already been updated before convergence. + * @param visited The queue of connections that have been visited during the cycle. + * @param timerQueue The queue of all pending timers. + * @param isImmediate A flag to indicate that this invocation is an immediate update or a delayed update. */ - fun doUpdate(now: Long): Boolean { - // The connection will only converge if either the source or the consumer wants the converge callback to be - // invoked. - val shouldConverge = shouldSourceConverge || shouldConsumerConverge - var willConverge = false - if (shouldConverge) { - willConverge = _willConverge - _willConverge = true - } - - val oldState = _state - if (oldState != State.Active) { - return willConverge + fun doUpdate( + now: Long, + visited: ArrayDeque, + timerQueue: PriorityQueue, + isImmediate: Boolean + ) { + var flags = _flags + + // Precondition: The flow connection must be active + if (flags and ConnState != ConnActive) { + return } - _isUpdateActive = true - _isImmediateUpdateScheduled = false + val deadline = _deadline + val reachedDeadline = deadline == now + var newDeadline = deadline + var hasUpdated = false try { // Pull the source if (1) `pull` is called or (2) the timer of the source has expired - val deadline = if (_isPulled || _deadline == now) { + newDeadline = if (flags and ConnPulled != 0 || reachedDeadline) { val lastPull = _lastPull val delta = max(0, now - lastPull) - _isPulled = false + // Update state before calling into the outside world, so it observes a consistent state _lastPull = now + _flags = (flags and ConnPulled.inv()) or ConnUpdateActive + hasUpdated = true val duration = source.onPull(this, now, delta) + + // IMPORTANT: Re-fetch the flags after the callback might have changed those + flags = _flags + if (duration != Long.MAX_VALUE) now + duration else duration } else { - _deadline + deadline } - // Check whether the state has changed after [consumer.onNext] - when (_state) { - State.Active -> { - val demand = _demand - if (demand != _activeDemand) { - val lastPush = _lastPush - val delta = max(0, now - lastPush) - - _lastPush = now - - logic.onPush(this, now, delta, demand) - } - } - State.Closed -> doStopSource(now) - State.Pending -> throw IllegalStateException("Illegal transition to pending state") - } + // Push to the consumer if the rate of the source has changed (after a call to `push`) + val newState = flags and ConnState + if (newState == ConnActive && flags and ConnPushed != 0) { + val lastPush = _lastPush + val delta = max(0, now - lastPush) - // Note: pending limit might be changed by [logic.onConsume], so re-fetch the value - val newLimit = _demand + // Update state before calling into the outside world, so it observes a consistent state + _lastPush = now + _flags = (flags and ConnPushed.inv()) or ConnUpdateActive + hasUpdated = true - // Flush the changes to the flow - _activeDemand = newLimit - _deadline = deadline - _rate = min(capacity, newLimit) + logic.onPush(this, now, delta, _demand) - // Schedule an update at the new deadline - scheduleDelayed(now, deadline) + // IMPORTANT: Re-fetch the flags after the callback might have changed those + flags = _flags + } else if (newState == ConnClosed) { + hasUpdated = true + + // The source has called [FlowConnection.close], so clean up the connection + doStopSource(now) + } } catch (cause: Throwable) { + // Mark the connection as closed + flags = (flags and ConnState.inv()) or ConnClosed + doFailSource(now, cause) - } finally { - _isUpdateActive = false } - return willConverge - } + // Check whether the connection needs to be added to the visited queue. This is the case when: + // (1) An update was performed (either a push or a pull) + // (2) Either the source or consumer want to converge, and + // (3) Convergence is not already pending (ConnConvergePending) + if (hasUpdated && flags and (ConnConvergeSource or ConnConvergeConsumer) != 0 && flags and ConnConvergePending == 0) { + visited.add(this) + flags = flags or ConnConvergePending + } - /** - * Prune the elapsed timers from this context. - */ - fun updateTimers() { - // Invariant: Any pending timer should only point to a future timestamp - // See also `scheduleDelayed` - _timer = _pendingTimers.poll() - } + // Compute the new flow rate of the connection + // Note: _demand might be changed by [logic.onConsume], so we must re-fetch the value + _rate = min(_capacity, _demand) - /** - * Try to re-schedule the resource context in case it was skipped. - */ - fun tryReschedule(now: Long) { - val deadline = _deadline - if (deadline > now && deadline != Long.MAX_VALUE) { - scheduleDelayed(now, deadline) + // Indicate that no update is active anymore and flush the flags + _flags = flags and ConnUpdateActive.inv() and ConnUpdatePending.inv() + + val pendingTimers = _pendingTimers + + // Prune the head timer if this is a delayed update + val timer = if (!isImmediate) { + // Invariant: Any pending timer should only point to a future timestamp + // See also `scheduleDelayed` + val timer = pendingTimers.poll() + _timer = timer + timer + } else { + _timer + } + + // Set the new deadline and schedule a delayed update for that deadline + _deadline = newDeadline + + // Check whether we need to schedule a new timer for this connection. That is the case when: + // (1) The deadline is valid (not the maximum value) + // (2) The connection is active + // (3) The current active timer for the connection points to a later deadline + if (newDeadline == Long.MAX_VALUE || flags and ConnState != ConnActive || (timer != null && newDeadline >= timer.target)) { + // Ignore any deadline scheduled at the maximum value + // This indicates that the source does not want to register a timer + return + } + + // Construct a timer for the new deadline and add it to the global queue of timers + val newTimer = FlowEngineImpl.Timer(this, newDeadline) + _timer = newTimer + timerQueue.add(newTimer) + + // A timer already exists for this connection, so add it to the queue of pending timers + if (timer != null) { + pendingTimers.addFirst(timer) } } @@ -300,17 +330,22 @@ internal class FlowConsumerContextImpl( * This method is invoked when the system converges into a steady state. */ fun onConverge(now: Long) { - _willConverge = false - try { - if (_state == State.Active && shouldSourceConverge) { + val flags = _flags + + // The connection is converging now, so unset the convergence pending flag + _flags = flags and ConnConvergePending.inv() + + // Call the source converge callback if it has enabled convergence and the connection is active + if (flags and ConnState == ConnActive && flags and ConnConvergeSource != 0) { val delta = max(0, now - _lastSourceConvergence) _lastSourceConvergence = now source.onConverge(this, now, delta) } - if (shouldConsumerConverge) { + // Call the consumer callback if it has enabled convergence + if (flags and ConnConvergeConsumer != 0) { val delta = max(0, now - _lastConsumerConvergence) _lastConsumerConvergence = now @@ -369,57 +404,16 @@ internal class FlowConsumerContextImpl( /** * Schedule an immediate update for this connection. */ - private fun scheduleImmediate() { + private fun scheduleImmediate(now: Long, flags: Int) { // In case an immediate update is already scheduled, no need to do anything - if (_isImmediateUpdateScheduled) { + if (flags and ConnUpdatePending != 0) { + _flags = flags return } - _isImmediateUpdateScheduled = true + // Mark the connection that there is an update pending + _flags = flags or ConnUpdatePending - val now = _clock.millis() engine.scheduleImmediate(now, this) } - - /** - * Schedule a delayed update for this resource context. - */ - private fun scheduleDelayed(now: Long, target: Long) { - // Ignore any target scheduled at the maximum value - // This indicates that the sources does not want to register a timer - if (target == Long.MAX_VALUE) { - return - } - - val timer = _timer - - if (timer == null) { - // No existing timer exists, so schedule a new timer and update the head - _timer = engine.scheduleDelayed(now, this, target) - } else if (target < timer.target) { - // Existing timer is further in the future, so schedule a new timer ahead of it - _timer = engine.scheduleDelayed(now, this, target) - _pendingTimers.addFirst(timer) - } - } - - /** - * The state of a flow connection. - */ - private enum class State { - /** - * The connection is pending and the consumer is waiting to consume the source. - */ - Pending, - - /** - * The connection is active and the source is currently being consumed. - */ - Active, - - /** - * The connection is closed and the source cannot be consumed through this connection anymore. - */ - Closed - } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt index c8170a43..019b5f10 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowEngineImpl.kt @@ -63,39 +63,26 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va /** * The systems that have been visited during the engine cycle. */ - private val visited = ArrayDeque() + private val visited: ArrayDeque = ArrayDeque() /** * The index in the batch stack. */ private var batchIndex = 0 - /** - * A flag to indicate that the engine is currently active. - */ - private val isRunning: Boolean - get() = batchIndex > 0 - /** * Update the specified [ctx] synchronously. */ fun scheduleSync(now: Long, ctx: FlowConsumerContextImpl) { - if (!ctx.doUpdate(now)) { - visited.add(ctx) - } + ctx.doUpdate(now, visited, futureQueue, isImmediate = true) // In-case the engine is already running in the call-stack, return immediately. The changes will be picked // up by the active engine. - if (isRunning) { + if (batchIndex > 0) { return } - try { - batchIndex++ - runEngine(now) - } finally { - batchIndex-- - } + runEngine(now) } /** @@ -109,36 +96,11 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va // In-case the engine is already running in the call-stack, return immediately. The changes will be picked // up by the active engine. - if (isRunning) { + if (batchIndex > 0) { return } - try { - batchIndex++ - runEngine(now) - } finally { - batchIndex-- - } - } - - /** - * Schedule the engine to run at [target] to update the flow contexts. - * - * This method will override earlier calls to this method for the same [ctx]. - * - * @param now The current virtual timestamp. - * @param ctx The flow context to which the event applies. - * @param target The timestamp when the interrupt should happen. - */ - fun scheduleDelayed(now: Long, ctx: FlowConsumerContextImpl, target: Long): Timer { - val futureQueue = futureQueue - - require(target >= now) { "Timestamp must be in the future" } - - val timer = Timer(ctx, target) - futureQueue.add(timer) - - return timer + runEngine(now) } override fun newContext(consumer: FlowSource, provider: FlowConsumerLogic): FlowConsumerContext = FlowConsumerContextImpl(this, consumer, provider) @@ -149,9 +111,9 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va override fun popBatch() { try { - // Flush the work if the platform is not already running + // Flush the work if the engine is not already running if (batchIndex == 1 && queue.isNotEmpty()) { - runEngine(clock.millis()) + doRunEngine(clock.millis()) } } finally { batchIndex-- @@ -159,9 +121,21 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va } /** - * Run all the enqueued actions for the specified [timestamp][now]. + * Run the engine and mark as active while running. */ private fun runEngine(now: Long) { + try { + batchIndex++ + doRunEngine(now) + } finally { + batchIndex-- + } + } + + /** + * Run all the enqueued actions for the specified [timestamp][now]. + */ + private fun doRunEngine(now: Long) { val queue = queue val futureQueue = futureQueue val futureInvocations = futureInvocations @@ -179,27 +153,16 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va // Execute all scheduled updates at current timestamp while (true) { val timer = futureQueue.peek() ?: break - val ctx = timer.ctx val target = timer.target - assert(target >= now) { "Internal inconsistency: found update of the past" } - if (target > now) { break } - futureQueue.poll() - - // Update the existing timers of the connection - ctx.updateTimers() + assert(target >= now) { "Internal inconsistency: found update of the past" } - if (ctx.shouldUpdate(now)) { - if (!ctx.doUpdate(now)) { - visited.add(ctx) - } - } else { - ctx.tryReschedule(now) - } + futureQueue.poll() + timer.ctx.doUpdate(now, visited, futureQueue, isImmediate = false) } // Repeat execution of all immediate updates until the system has converged to a steady-state @@ -208,17 +171,13 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va // Execute all immediate updates while (true) { val ctx = queue.poll() ?: break - - if (ctx.shouldUpdate(now) && !ctx.doUpdate(now)) { - visited.add(ctx) - } + ctx.doUpdate(now, visited, futureQueue, isImmediate = true) } - for (system in visited) { - system.onConverge(now) + while (true) { + val ctx = visited.poll() ?: break + ctx.onConverge(now) } - - visited.clear() } while (queue.isNotEmpty()) // Schedule an engine invocation for the next update to occur. @@ -242,18 +201,7 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va // Case 2: A new timer was registered ahead of the other timers. // Solution: Schedule a new scheduler invocation @OptIn(InternalCoroutinesApi::class) - val handle = delay.invokeOnTimeout( - target - now, - { - try { - batchIndex++ - runEngine(target) - } finally { - batchIndex-- - } - }, - context - ) + val handle = delay.invokeOnTimeout(target - now, { runEngine(target) }, context) scheduled.addFirst(Invocation(target, handle)) break } else if (invocation.timestamp < target) { @@ -274,7 +222,7 @@ internal class FlowEngineImpl(private val context: CoroutineContext, override va * This class is used to keep track of the future engine invocations created using the [Delay] instance. In case * the invocation is not needed anymore, it can be cancelled via [cancel]. */ - private data class Invocation( + private class Invocation( @JvmField val timestamp: Long, @JvmField val handle: DisposableHandle ) { -- cgit v1.2.3 From 6e424e9b44687d01e618e7bc38afc427610cd845 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Fri, 1 Oct 2021 20:08:21 +0200 Subject: perf(simulator): Optimize hot path in SimTraceWorkload This change optimizes the hot path of the SimTraceWorkload class. This class is used by multiple experiments to replay workload traces and consumes a considerable amount of time of the total time of the experiments. By inlining the isExpired method, marking the properties on the Fragment class as field, and caching several properties locally in the class, we reduce the amount of virtual method calls in the hot path. --- .../simulator/compute/workload/SimTraceWorkload.kt | 28 ++++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt index a877dac1..49ae5933 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/workload/SimTraceWorkload.kt @@ -53,14 +53,15 @@ public class SimTraceWorkload(public val trace: Sequence, private val * Obtain the fragment with a timestamp equal or greater than [now]. */ private fun pullFragment(now: Long): Fragment? { + // Return the most recent fragment if its starting time + duration is later than `now` var fragment = fragment - if (fragment != null && !fragment.isExpired(now)) { + if (fragment != null && fragment.timestamp + offset + fragment.duration > now) { return fragment } while (iterator.hasNext()) { fragment = iterator.next() - if (!fragment.isExpired(now)) { + if (fragment.timestamp + offset + fragment.duration > now) { this.fragment = fragment return fragment } @@ -70,15 +71,11 @@ public class SimTraceWorkload(public val trace: Sequence, private val return null } - /** - * Determine if the specified [Fragment] is expired, i.e., it has already passed. - */ - private fun Fragment.isExpired(now: Long): Boolean { - val timestamp = this.timestamp + offset - return now >= timestamp + duration - } + private inner class Consumer(cpu: ProcessingUnit) : FlowSource { + private val offset = this@SimTraceWorkload.offset + private val id = cpu.id + private val coreCount = cpu.node.coreCount - private inner class Consumer(val cpu: ProcessingUnit) : FlowSource { override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { val fragment = pullFragment(now) @@ -95,7 +92,7 @@ public class SimTraceWorkload(public val trace: Sequence, private val return timestamp - now } - val cores = min(cpu.node.coreCount, fragment.cores) + val cores = min(coreCount, fragment.cores) val usage = if (fragment.cores > 0) fragment.usage / cores else @@ -103,7 +100,7 @@ public class SimTraceWorkload(public val trace: Sequence, private val val deadline = timestamp + fragment.duration val duration = deadline - now - conn.push(if (cpu.id < cores && usage > 0.0) usage else 0.0) + conn.push(if (id < cores && usage > 0.0) usage else 0.0) return duration } @@ -117,5 +114,10 @@ public class SimTraceWorkload(public val trace: Sequence, private val * @param usage The CPU usage during the fragment. * @param cores The amount of cores utilized during the fragment. */ - public data class Fragment(val timestamp: Long, val duration: Long, val usage: Double, val cores: Int) + public data class Fragment( + @JvmField val timestamp: Long, + @JvmField val duration: Long, + @JvmField val usage: Double, + @JvmField val cores: Int + ) } -- cgit v1.2.3 From 081221684fb826ab5a00c1d8cc5a9886b9e2203c Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Fri, 1 Oct 2021 22:04:35 +0200 Subject: feat(simulator): Expose CPU time counters directly on hypervisor This change adds a new interface to the SimHypervisor interface that exposes the CPU time counters directly. These are derived from the flow counters and will be used by SimHost to expose them via telemetry. --- .../simulator/compute/SimMachineBenchmarks.kt | 6 +- .../simulator/compute/SimBareMetalMachine.kt | 27 +- .../compute/kernel/SimAbstractHypervisor.kt | 144 +++++++++-- .../compute/kernel/SimFairShareHypervisor.kt | 67 +---- .../kernel/SimFairShareHypervisorProvider.kt | 11 +- .../simulator/compute/kernel/SimHypervisor.kt | 38 ++- .../compute/kernel/SimHypervisorCounters.kt | 48 ++++ .../compute/kernel/SimHypervisorProvider.kt | 5 +- .../compute/kernel/SimSpaceSharedHypervisor.kt | 17 +- .../kernel/SimSpaceSharedHypervisorProvider.kt | 5 +- .../simulator/compute/kernel/SimVirtualMachine.kt | 50 ++++ .../compute/kernel/SimFairShareHypervisorTest.kt | 238 ++++++++++++++++++ .../simulator/compute/kernel/SimHypervisorTest.kt | 280 --------------------- .../compute/kernel/SimSpaceSharedHypervisorTest.kt | 20 +- .../org/opendc/simulator/flow/FlowBenchmarks.kt | 16 +- .../opendc/simulator/flow/AbstractFlowConsumer.kt | 20 +- .../org/opendc/simulator/flow/FlowCounters.kt | 4 +- .../org/opendc/simulator/flow/FlowForwarder.kt | 3 +- .../simulator/flow/internal/FlowCountersImpl.kt | 6 +- .../opendc/simulator/flow/mux/FlowMultiplexer.kt | 38 ++- .../flow/mux/ForwardingFlowMultiplexer.kt | 114 +++++---- .../simulator/flow/mux/MaxMinFlowMultiplexer.kt | 164 +++++++++--- .../org/opendc/simulator/flow/FlowForwarderTest.kt | 2 +- .../flow/mux/ExclusiveFlowMultiplexerTest.kt | 154 ------------ .../flow/mux/ForwardingFlowMultiplexerTest.kt | 154 ++++++++++++ .../flow/mux/MaxMinFlowMultiplexerTest.kt | 12 +- .../simulator/network/SimNetworkSwitchVirtual.kt | 8 +- .../kotlin/org/opendc/simulator/power/SimPdu.kt | 8 +- .../kotlin/org/opendc/simulator/power/SimUps.kt | 6 +- 29 files changed, 960 insertions(+), 705 deletions(-) create mode 100644 opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorCounters.kt create mode 100644 opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimVirtualMachine.kt create mode 100644 opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorTest.kt delete mode 100644 opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt delete mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt create mode 100644 opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexerTest.kt (limited to 'opendc-simulator') diff --git a/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt b/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt index c57919c1..d654d58a 100644 --- a/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt +++ b/opendc-simulator/opendc-simulator-compute/src/jmh/kotlin/org/opendc/simulator/compute/SimMachineBenchmarks.kt @@ -92,7 +92,7 @@ class SimMachineBenchmarks { val machine = SimBareMetalMachine( engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimSpaceSharedHypervisor(engine) + val hypervisor = SimSpaceSharedHypervisor(engine, null, null) launch { machine.run(hypervisor) } @@ -113,7 +113,7 @@ class SimMachineBenchmarks { val machine = SimBareMetalMachine( engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimFairShareHypervisor(engine) + val hypervisor = SimFairShareHypervisor(engine, null, null, null) launch { machine.run(hypervisor) } @@ -134,7 +134,7 @@ class SimMachineBenchmarks { val machine = SimBareMetalMachine( engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimFairShareHypervisor(engine) + val hypervisor = SimFairShareHypervisor(engine, null, null, null) launch { machine.run(hypervisor) } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt index 0bcf5957..9140d31b 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/SimBareMetalMachine.kt @@ -28,6 +28,7 @@ import org.opendc.simulator.compute.model.ProcessingUnit import org.opendc.simulator.compute.power.PowerDriver import org.opendc.simulator.flow.* import org.opendc.simulator.flow.FlowEngine +import kotlin.math.max /** * A simulated bare-metal machine that is able to run a single workload. @@ -49,10 +50,18 @@ public class SimBareMetalMachine( parent: FlowConvergenceListener? = null, ) : SimAbstractMachine(engine, parent, model) { /** - * The power draw of the machine onto the PSU. + * The current power usage of the machine (without PSU loss) in W. */ - public val powerDraw: Double - get() = powerDriverLogic.computePower() + public val powerUsage: Double + get() = _powerUsage + private var _powerUsage = 0.0 + + /** + * The total energy usage of the machine (without PSU loss) in Joules. + */ + public val energyUsage: Double + get() = _energyUsage + private var _energyUsage = 0.0 /** * The processing units of the machine. @@ -66,8 +75,20 @@ public class SimBareMetalMachine( */ private val powerDriverLogic = powerDriver.createLogic(this, cpus) + private var _lastConverge = Long.MAX_VALUE + override fun onConverge(now: Long, delta: Long) { + // Update the PSU stage psu.update() + + val lastConverge = _lastConverge + _lastConverge = now + val duration = max(0, now - lastConverge) + if (duration > 0) { + // Compute the power and energy usage of the machine + _energyUsage += _powerUsage * (duration / 1000.0) + _powerUsage = powerDriverLogic.computePower() + } } init { diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt index bcba8e8e..aac8b959 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimAbstractHypervisor.kt @@ -30,6 +30,7 @@ import org.opendc.simulator.compute.model.MachineModel import org.opendc.simulator.compute.model.ProcessingUnit import org.opendc.simulator.flow.* import org.opendc.simulator.flow.mux.FlowMultiplexer +import kotlin.math.roundToLong /** * Abstract implementation of the [SimHypervisor] interface. @@ -39,18 +40,19 @@ import org.opendc.simulator.flow.mux.FlowMultiplexer */ public abstract class SimAbstractHypervisor( protected val engine: FlowEngine, - private val scalingGovernor: ScalingGovernor? = null, + private val listener: FlowConvergenceListener?, + private val scalingGovernor: ScalingGovernor?, protected val interferenceDomain: VmInterferenceDomain? = null -) : SimHypervisor { +) : SimHypervisor, FlowConvergenceListener { /** * The machine on which the hypervisor runs. */ - private lateinit var context: SimMachineContext + protected lateinit var context: SimMachineContext /** * The resource switch to use. */ - private lateinit var mux: FlowMultiplexer + protected abstract val mux: FlowMultiplexer /** * The virtual machines running on this hypervisor. @@ -62,39 +64,73 @@ public abstract class SimAbstractHypervisor( /** * The resource counters associated with the hypervisor. */ - public override val counters: FlowCounters - get() = mux.counters + public override val counters: SimHypervisorCounters + get() = _counters + private val _counters = object : SimHypervisorCounters { + @JvmField var d = 1.0 // Number of CPUs divided by total CPU capacity + + override var cpuActiveTime: Long = 0L + override var cpuIdleTime: Long = 0L + override var cpuStealTime: Long = 0L + override var cpuLostTime: Long = 0L + + private var _previousDemand = 0.0 + private var _previousActual = 0.0 + private var _previousRemaining = 0.0 + private var _previousInterference = 0.0 + + /** + * Record the CPU time of the hypervisor. + */ + fun record() { + val counters = mux.counters + val demand = counters.demand + val actual = counters.actual + val remaining = counters.remaining + val interference = counters.interference + + val demandDelta = demand - _previousDemand + val actualDelta = actual - _previousActual + val remainingDelta = remaining - _previousRemaining + val interferenceDelta = interference - _previousInterference + + _previousDemand = demand + _previousActual = actual + _previousRemaining = remaining + _previousInterference = interference + + cpuActiveTime += (actualDelta * d).roundToLong() + cpuIdleTime += (remainingDelta * d).roundToLong() + cpuStealTime += ((demandDelta - actualDelta) * d).roundToLong() + cpuLostTime += (interferenceDelta * d).roundToLong() + } + } /** - * The scaling governors attached to the physical CPUs backing this hypervisor. + * The CPU capacity of the hypervisor in MHz. */ - private val governors = mutableListOf() + override val cpuCapacity: Double + get() = mux.capacity /** - * Construct the [FlowMultiplexer] implementation that performs the actual scheduling of the CPUs. + * The CPU demand of the hypervisor in MHz. */ - public abstract fun createMultiplexer(ctx: SimMachineContext): FlowMultiplexer + override val cpuDemand: Double + get() = mux.demand /** - * Check whether the specified machine model fits on this hypervisor. + * The CPU usage of the hypervisor in MHz. */ - public abstract fun canFit(model: MachineModel, switch: FlowMultiplexer): Boolean + override val cpuUsage: Double + get() = mux.rate /** - * Trigger the governors to recompute the scaling limits. + * The scaling governors attached to the physical CPUs backing this hypervisor. */ - protected fun triggerGovernors(load: Double) { - for (governor in governors) { - governor.onLimit(load) - } - } + private val governors = mutableListOf() /* SimHypervisor */ - override fun canFit(model: MachineModel): Boolean { - return canFit(model, mux) - } - - override fun createMachine(model: MachineModel, interferenceId: String?): SimMachine { + override fun createMachine(model: MachineModel, interferenceId: String?): SimVirtualMachine { require(canFit(model)) { "Machine does not fit" } val vm = VirtualMachine(model, interferenceId) _vms.add(vm) @@ -104,7 +140,13 @@ public abstract class SimAbstractHypervisor( /* SimWorkload */ override fun onStart(ctx: SimMachineContext) { context = ctx - mux = createMultiplexer(ctx) + + _cpuCount = ctx.cpus.size + _cpuCapacity = ctx.cpus.sumOf { it.model.frequency } + _counters.d = _cpuCount / _cpuCapacity * 1000L + + // Clear the existing outputs of the multiplexer + mux.clearOutputs() for (cpu in ctx.cpus) { val governor = scalingGovernor?.createLogic(ScalingPolicyImpl(cpu)) @@ -113,16 +155,31 @@ public abstract class SimAbstractHypervisor( governor.onStart() } - mux.addOutput(cpu) + cpu.startConsumer(mux.newOutput()) } } + private var _cpuCount = 0 + private var _cpuCapacity = 0.0 + + /* FlowConvergenceListener */ + override fun onConverge(now: Long, delta: Long) { + _counters.record() + + val load = cpuDemand / cpuCapacity + for (governor in governors) { + governor.onLimit(load) + } + + listener?.onConverge(now, delta) + } + /** * A virtual machine running on the hypervisor. * * @param model The machine model of the virtual machine. */ - private inner class VirtualMachine(model: MachineModel, interferenceId: String? = null) : SimAbstractMachine(engine, parent = null, model) { + private inner class VirtualMachine(model: MachineModel, interferenceId: String? = null) : SimAbstractMachine(engine, parent = null, model), SimVirtualMachine { /** * The interference key of this virtual machine. */ @@ -133,6 +190,41 @@ public abstract class SimAbstractHypervisor( */ override val cpus = model.cpus.map { VCpu(mux, mux.newInput(interferenceKey), it) } + /** + * The resource counters associated with the hypervisor. + */ + override val counters: SimHypervisorCounters + get() = _counters + private val _counters = object : SimHypervisorCounters { + private val d = cpus.size / cpus.sumOf { it.model.frequency } * 1000 + + override val cpuActiveTime: Long + get() = (cpus.sumOf { it.counters.actual } * d).roundToLong() + override val cpuIdleTime: Long + get() = (cpus.sumOf { it.counters.actual + it.counters.remaining } * d).roundToLong() + override val cpuStealTime: Long + get() = (cpus.sumOf { it.counters.demand - it.counters.actual } * d).roundToLong() + override val cpuLostTime: Long = 0L + } + + /** + * The CPU capacity of the hypervisor in MHz. + */ + override val cpuCapacity: Double + get() = cpus.sumOf(FlowConsumer::capacity) + + /** + * The CPU demand of the hypervisor in MHz. + */ + override val cpuDemand: Double + get() = cpus.sumOf(FlowConsumer::demand) + + /** + * The CPU usage of the hypervisor in MHz. + */ + override val cpuUsage: Double + get() = cpus.sumOf(FlowConsumer::rate) + override fun close() { super.close() diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt index b0515c6e..36f76650 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisor.kt @@ -23,7 +23,6 @@ package org.opendc.simulator.compute.kernel import org.opendc.simulator.compute.SimMachine -import org.opendc.simulator.compute.SimMachineContext import org.opendc.simulator.compute.kernel.cpufreq.ScalingGovernor import org.opendc.simulator.compute.kernel.interference.VmInterferenceDomain import org.opendc.simulator.compute.model.MachineModel @@ -38,64 +37,20 @@ import org.opendc.simulator.flow.mux.MaxMinFlowMultiplexer * concurrently using weighted fair sharing. * * @param engine The [FlowEngine] to manage the machine's resources. - * @param parent The parent simulation system. + * @param listener The listener for the convergence of the system. * @param scalingGovernor The CPU frequency scaling governor to use for the hypervisor. * @param interferenceDomain The resource interference domain to which the hypervisor belongs. - * @param listener The hypervisor listener to use. */ public class SimFairShareHypervisor( engine: FlowEngine, - private val parent: FlowConvergenceListener? = null, - scalingGovernor: ScalingGovernor? = null, - interferenceDomain: VmInterferenceDomain? = null, - private val listener: SimHypervisor.Listener? = null -) : SimAbstractHypervisor(engine, scalingGovernor, interferenceDomain) { - - override fun canFit(model: MachineModel, switch: FlowMultiplexer): Boolean = true - - override fun createMultiplexer(ctx: SimMachineContext): FlowMultiplexer { - return SwitchSystem(ctx).switch - } - - private inner class SwitchSystem(private val ctx: SimMachineContext) : FlowConvergenceListener { - val switch = MaxMinFlowMultiplexer(engine, this, interferenceDomain) - - private var lastCpuUsage = 0.0 - private var lastCpuDemand = 0.0 - private var lastDemand = 0.0 - private var lastActual = 0.0 - private var lastOvercommit = 0.0 - private var lastInterference = 0.0 - private var lastReport = Long.MIN_VALUE - - override fun onConverge(now: Long, delta: Long) { - val listener = listener ?: return - val counters = switch.counters - - if (now > lastReport) { - listener.onSliceFinish( - this@SimFairShareHypervisor, - counters.demand - lastDemand, - counters.actual - lastActual, - counters.overcommit - lastOvercommit, - counters.interference - lastInterference, - lastCpuUsage, - lastCpuDemand - ) - } - lastReport = now - - lastCpuDemand = switch.outputs.sumOf { it.demand } - lastCpuUsage = switch.outputs.sumOf { it.rate } - lastDemand = counters.demand - lastActual = counters.actual - lastOvercommit = counters.overcommit - lastInterference = counters.interference - - val load = lastCpuDemand / ctx.cpus.sumOf { it.model.frequency } - triggerGovernors(load) - - parent?.onConverge(now, delta) - } - } + listener: FlowConvergenceListener?, + scalingGovernor: ScalingGovernor?, + interferenceDomain: VmInterferenceDomain?, +) : SimAbstractHypervisor(engine, listener, scalingGovernor, interferenceDomain) { + /** + * The multiplexer that distributes the computing capacity. + */ + override val mux: FlowMultiplexer = MaxMinFlowMultiplexer(engine, this, interferenceDomain) + + override fun canFit(model: MachineModel): Boolean = true } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt index e0a70926..3136f4c8 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorProvider.kt @@ -35,15 +35,8 @@ public class SimFairShareHypervisorProvider : SimHypervisorProvider { override fun create( engine: FlowEngine, - parent: FlowConvergenceListener?, + listener: FlowConvergenceListener?, scalingGovernor: ScalingGovernor?, interferenceDomain: VmInterferenceDomain?, - listener: SimHypervisor.Listener? - ): SimHypervisor = SimFairShareHypervisor( - engine, - parent, - scalingGovernor = scalingGovernor, - interferenceDomain = interferenceDomain, - listener = listener - ) + ): SimHypervisor = SimFairShareHypervisor(engine, listener, scalingGovernor, interferenceDomain) } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisor.kt index 1b11ca6b..57d4cf20 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisor.kt @@ -25,7 +25,6 @@ package org.opendc.simulator.compute.kernel import org.opendc.simulator.compute.SimMachine import org.opendc.simulator.compute.model.MachineModel import org.opendc.simulator.compute.workload.SimWorkload -import org.opendc.simulator.flow.FlowCounters /** * A SimHypervisor facilitates the execution of multiple concurrent [SimWorkload]s, while acting as a single workload @@ -40,7 +39,22 @@ public interface SimHypervisor : SimWorkload { /** * The resource counters associated with the hypervisor. */ - public val counters: FlowCounters + public val counters: SimHypervisorCounters + + /** + * The CPU usage of the hypervisor in MHz. + */ + public val cpuUsage: Double + + /** + * The CPU usage of the hypervisor in MHz. + */ + public val cpuDemand: Double + + /** + * The CPU capacity of the hypervisor in MHz. + */ + public val cpuCapacity: Double /** * Determine whether the specified machine characterized by [model] can fit on this hypervisor at this moment. @@ -53,23 +67,5 @@ public interface SimHypervisor : SimWorkload { * @param model The machine to create. * @param interferenceId An identifier for the interference model. */ - public fun createMachine(model: MachineModel, interferenceId: String? = null): SimMachine - - /** - * Event listener for hypervisor events. - */ - public interface Listener { - /** - * This method is invoked when a slice is finished. - */ - public fun onSliceFinish( - hypervisor: SimHypervisor, - totalWork: Double, - grantedWork: Double, - overcommittedWork: Double, - interferedWork: Double, - cpuUsage: Double, - cpuDemand: Double - ) - } + public fun createMachine(model: MachineModel, interferenceId: String? = null): SimVirtualMachine } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorCounters.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorCounters.kt new file mode 100644 index 00000000..030d9c5f --- /dev/null +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorCounters.kt @@ -0,0 +1,48 @@ +/* + * 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.kernel + +/** + * Performance counters of a [SimHypervisor]. + */ +public interface SimHypervisorCounters { + /** + * The amount of time (in milliseconds) the CPUs of the hypervisor were actively running. + */ + public val cpuActiveTime: Long + + /** + * The amount of time (in milliseconds) the CPUs of the hypervisor were idle. + */ + public val cpuIdleTime: Long + + /** + * The amount of CPU time (in milliseconds) that virtual machines were ready to run, but were not able to. + */ + public val cpuStealTime: Long + + /** + * The amount of CPU time (in milliseconds) that was lost due to interference between virtual machines. + */ + public val cpuLostTime: Long +} diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt index dad2cc3b..483217af 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorProvider.kt @@ -40,13 +40,12 @@ public interface SimHypervisorProvider { public val id: String /** - * Create a [SimHypervisor] instance with the specified [listener]. + * Create a new [SimHypervisor] instance. */ public fun create( engine: FlowEngine, - parent: FlowConvergenceListener? = null, + listener: FlowConvergenceListener? = null, scalingGovernor: ScalingGovernor? = null, interferenceDomain: VmInterferenceDomain? = null, - listener: SimHypervisor.Listener? = null ): SimHypervisor } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisor.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisor.kt index 883e0d82..82f8df38 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisor.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisor.kt @@ -22,8 +22,9 @@ package org.opendc.simulator.compute.kernel -import org.opendc.simulator.compute.SimMachineContext +import org.opendc.simulator.compute.kernel.cpufreq.ScalingGovernor import org.opendc.simulator.compute.model.MachineModel +import org.opendc.simulator.flow.FlowConvergenceListener import org.opendc.simulator.flow.FlowEngine import org.opendc.simulator.flow.mux.FlowMultiplexer import org.opendc.simulator.flow.mux.ForwardingFlowMultiplexer @@ -31,12 +32,14 @@ import org.opendc.simulator.flow.mux.ForwardingFlowMultiplexer /** * A [SimHypervisor] that allocates its sub-resources exclusively for the virtual machine that it hosts. */ -public class SimSpaceSharedHypervisor(engine: FlowEngine) : SimAbstractHypervisor(engine) { - override fun canFit(model: MachineModel, switch: FlowMultiplexer): Boolean { - return switch.outputs.size - switch.inputs.size >= model.cpus.size - } +public class SimSpaceSharedHypervisor( + engine: FlowEngine, + listener: FlowConvergenceListener?, + scalingGovernor: ScalingGovernor?, +) : SimAbstractHypervisor(engine, listener, scalingGovernor) { + override val mux: FlowMultiplexer = ForwardingFlowMultiplexer(engine) - override fun createMultiplexer(ctx: SimMachineContext): FlowMultiplexer { - return ForwardingFlowMultiplexer(engine) + override fun canFit(model: MachineModel): Boolean { + return mux.outputs.size - mux.inputs.size >= model.cpus.size } } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt index 93921eb9..dd6fb0b1 100644 --- a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorProvider.kt @@ -35,9 +35,8 @@ public class SimSpaceSharedHypervisorProvider : SimHypervisorProvider { override fun create( engine: FlowEngine, - parent: FlowConvergenceListener?, + listener: FlowConvergenceListener?, scalingGovernor: ScalingGovernor?, interferenceDomain: VmInterferenceDomain?, - listener: SimHypervisor.Listener? - ): SimHypervisor = SimSpaceSharedHypervisor(engine) + ): SimHypervisor = SimSpaceSharedHypervisor(engine, listener, scalingGovernor) } diff --git a/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimVirtualMachine.kt b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimVirtualMachine.kt new file mode 100644 index 00000000..36219ef2 --- /dev/null +++ b/opendc-simulator/opendc-simulator-compute/src/main/kotlin/org/opendc/simulator/compute/kernel/SimVirtualMachine.kt @@ -0,0 +1,50 @@ +/* + * 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.kernel + +import org.opendc.simulator.compute.SimMachine + +/** + * A virtual [SimMachine] running on top of another [SimMachine]. + */ +public interface SimVirtualMachine : SimMachine { + /** + * The resource counters associated with the virtual machine. + */ + public val counters: SimHypervisorCounters + + /** + * The CPU usage of the VM in MHz. + */ + public val cpuUsage: Double + + /** + * The CPU usage of the VM in MHz. + */ + public val cpuDemand: Double + + /** + * The CPU capacity of the VM in MHz. + */ + public val cpuCapacity: Double +} diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorTest.kt new file mode 100644 index 00000000..9db2e6ec --- /dev/null +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimFairShareHypervisorTest.kt @@ -0,0 +1,238 @@ +/* + * 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.kernel + +import kotlinx.coroutines.* +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 +import org.junit.jupiter.api.assertDoesNotThrow +import org.opendc.simulator.compute.SimBareMetalMachine +import org.opendc.simulator.compute.kernel.cpufreq.PerformanceScalingGovernor +import org.opendc.simulator.compute.kernel.interference.VmInterferenceGroup +import org.opendc.simulator.compute.kernel.interference.VmInterferenceModel +import org.opendc.simulator.compute.model.MachineModel +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.power.ConstantPowerModel +import org.opendc.simulator.compute.power.SimplePowerDriver +import org.opendc.simulator.compute.workload.SimTraceWorkload +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.FlowEngine + +/** + * Test suite for the [SimHypervisor] class. + */ +@OptIn(ExperimentalCoroutinesApi::class) +internal class SimFairShareHypervisorTest { + private lateinit var model: MachineModel + + @BeforeEach + fun setUp() { + val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 1) + model = MachineModel( + cpus = List(cpuNode.coreCount) { ProcessingUnit(cpuNode, it, 3200.0) }, + memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } + ) + } + + /** + * Test overcommitting of resources via the hypervisor with a single VM. + */ + @Test + fun testOvercommittedSingle() = runBlockingSimulation { + val duration = 5 * 60L + val workloadA = + SimTraceWorkload( + sequenceOf( + SimTraceWorkload.Fragment(0, duration * 1000, 28.0, 1), + SimTraceWorkload.Fragment(duration * 1000, duration * 1000, 3500.0, 1), + SimTraceWorkload.Fragment(duration * 2000, duration * 1000, 0.0, 1), + SimTraceWorkload.Fragment(duration * 3000, duration * 1000, 183.0, 1) + ), + ) + + val platform = FlowEngine(coroutineContext, clock) + val machine = SimBareMetalMachine(platform, model, SimplePowerDriver(ConstantPowerModel(0.0))) + val hypervisor = SimFairShareHypervisor(platform, null, PerformanceScalingGovernor(), null) + + launch { + machine.run(hypervisor) + println("Hypervisor finished") + } + yield() + + val vm = hypervisor.createMachine(model) + vm.run(workloadA) + + yield() + machine.close() + + assertAll( + { assertEquals(319781, hypervisor.counters.cpuActiveTime, "Active time does not match") }, + { assertEquals(880219, hypervisor.counters.cpuIdleTime, "Idle time does not match") }, + { assertEquals(28125, hypervisor.counters.cpuStealTime, "Steal time does not match") }, + { assertEquals(1200000, clock.millis()) { "Current time is correct" } } + ) + } + + /** + * Test overcommitting of resources via the hypervisor with two VMs. + */ + @Test + fun testOvercommittedDual() = runBlockingSimulation { + val duration = 5 * 60L + val workloadA = + SimTraceWorkload( + sequenceOf( + SimTraceWorkload.Fragment(0, duration * 1000, 28.0, 1), + SimTraceWorkload.Fragment(duration * 1000, duration * 1000, 3500.0, 1), + SimTraceWorkload.Fragment(duration * 2000, duration * 1000, 0.0, 1), + SimTraceWorkload.Fragment(duration * 3000, duration * 1000, 183.0, 1) + ), + ) + val workloadB = + SimTraceWorkload( + sequenceOf( + SimTraceWorkload.Fragment(0, duration * 1000, 28.0, 1), + SimTraceWorkload.Fragment(duration * 1000, duration * 1000, 3100.0, 1), + SimTraceWorkload.Fragment(duration * 2000, duration * 1000, 0.0, 1), + SimTraceWorkload.Fragment(duration * 3000, duration * 1000, 73.0, 1) + ) + ) + + val platform = FlowEngine(coroutineContext, clock) + val machine = SimBareMetalMachine( + platform, model, SimplePowerDriver(ConstantPowerModel(0.0)) + ) + val hypervisor = SimFairShareHypervisor(platform, null, null, null) + + launch { + machine.run(hypervisor) + } + + yield() + coroutineScope { + launch { + val vm = hypervisor.createMachine(model) + vm.run(workloadA) + vm.close() + } + val vm = hypervisor.createMachine(model) + vm.run(workloadB) + vm.close() + } + yield() + machine.close() + yield() + + assertAll( + { assertEquals(329250, hypervisor.counters.cpuActiveTime, "Active time does not match") }, + { assertEquals(870750, hypervisor.counters.cpuIdleTime, "Idle time does not match") }, + { assertEquals(318750, hypervisor.counters.cpuStealTime, "Steal time does not match") }, + { assertEquals(1200000, clock.millis()) } + ) + } + + @Test + fun testMultipleCPUs() = runBlockingSimulation { + val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 2) + val model = MachineModel( + cpus = List(cpuNode.coreCount) { ProcessingUnit(cpuNode, it, 3200.0) }, + memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } + ) + + val platform = FlowEngine(coroutineContext, clock) + val machine = SimBareMetalMachine(platform, model, SimplePowerDriver(ConstantPowerModel(0.0))) + val hypervisor = SimFairShareHypervisor(platform, null, null, null) + + assertDoesNotThrow { + launch { + machine.run(hypervisor) + } + } + + machine.close() + } + + @Test + fun testInterference() = runBlockingSimulation { + val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 2) + val model = MachineModel( + cpus = List(cpuNode.coreCount) { ProcessingUnit(cpuNode, it, 3200.0) }, + memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } + ) + + val groups = listOf( + VmInterferenceGroup(targetLoad = 0.0, score = 0.9, members = setOf("a", "b")), + VmInterferenceGroup(targetLoad = 0.0, score = 0.6, members = setOf("a", "c")), + VmInterferenceGroup(targetLoad = 0.1, score = 0.8, members = setOf("a", "n")) + ) + val interferenceModel = VmInterferenceModel(groups) + + val platform = FlowEngine(coroutineContext, clock) + val machine = SimBareMetalMachine( + platform, model, SimplePowerDriver(ConstantPowerModel(0.0)) + ) + val hypervisor = SimFairShareHypervisor(platform, null, null, interferenceModel.newDomain()) + + val duration = 5 * 60L + val workloadA = + SimTraceWorkload( + sequenceOf( + SimTraceWorkload.Fragment(0, duration * 1000, 0.0, 1), + SimTraceWorkload.Fragment(duration * 1000, duration * 1000, 28.0, 1), + SimTraceWorkload.Fragment(duration * 2000, duration * 1000, 3500.0, 1), + SimTraceWorkload.Fragment(duration * 3000, duration * 1000, 183.0, 1) + ), + ) + val workloadB = + SimTraceWorkload( + sequenceOf( + SimTraceWorkload.Fragment(0, duration * 1000, 0.0, 1), + SimTraceWorkload.Fragment(duration * 1000, duration * 1000, 28.0, 1), + SimTraceWorkload.Fragment(duration * 2000, duration * 1000, 3100.0, 1), + SimTraceWorkload.Fragment(duration * 3000, duration * 1000, 73.0, 1) + ) + ) + + launch { + machine.run(hypervisor) + } + + coroutineScope { + launch { + val vm = hypervisor.createMachine(model, "a") + vm.run(workloadA) + vm.close() + } + val vm = hypervisor.createMachine(model, "b") + vm.run(workloadB) + vm.close() + } + + machine.close() + } +} diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt deleted file mode 100644 index 058d5d28..00000000 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimHypervisorTest.kt +++ /dev/null @@ -1,280 +0,0 @@ -/* - * 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.kernel - -import kotlinx.coroutines.* -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 -import org.junit.jupiter.api.assertDoesNotThrow -import org.opendc.simulator.compute.SimBareMetalMachine -import org.opendc.simulator.compute.kernel.cpufreq.PerformanceScalingGovernor -import org.opendc.simulator.compute.kernel.interference.VmInterferenceGroup -import org.opendc.simulator.compute.kernel.interference.VmInterferenceModel -import org.opendc.simulator.compute.model.MachineModel -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.power.ConstantPowerModel -import org.opendc.simulator.compute.power.SimplePowerDriver -import org.opendc.simulator.compute.workload.SimTraceWorkload -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.flow.FlowEngine - -/** - * Test suite for the [SimHypervisor] class. - */ -@OptIn(ExperimentalCoroutinesApi::class) -internal class SimHypervisorTest { - private lateinit var model: MachineModel - - @BeforeEach - fun setUp() { - val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 1) - model = MachineModel( - cpus = List(cpuNode.coreCount) { ProcessingUnit(cpuNode, it, 3200.0) }, - memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } - ) - } - - /** - * Test overcommitting of resources via the hypervisor with a single VM. - */ - @Test - fun testOvercommittedSingle() = runBlockingSimulation { - val listener = object : SimHypervisor.Listener { - var totalRequestedWork = 0.0 - var totalGrantedWork = 0.0 - var totalOvercommittedWork = 0.0 - - override fun onSliceFinish( - hypervisor: SimHypervisor, - totalWork: Double, - grantedWork: Double, - overcommittedWork: Double, - interferedWork: Double, - cpuUsage: Double, - cpuDemand: Double - ) { - totalRequestedWork += totalWork - totalGrantedWork += grantedWork - totalOvercommittedWork += overcommittedWork - } - } - - val duration = 5 * 60L - val workloadA = - SimTraceWorkload( - sequenceOf( - SimTraceWorkload.Fragment(0, duration * 1000, 28.0, 1), - SimTraceWorkload.Fragment(duration * 1000, duration * 1000, 3500.0, 1), - SimTraceWorkload.Fragment(duration * 2000, duration * 1000, 0.0, 1), - SimTraceWorkload.Fragment(duration * 3000, duration * 1000, 183.0, 1) - ), - ) - - val platform = FlowEngine(coroutineContext, clock) - val machine = SimBareMetalMachine(platform, model, SimplePowerDriver(ConstantPowerModel(0.0))) - val hypervisor = SimFairShareHypervisor(platform, scalingGovernor = PerformanceScalingGovernor(), listener = listener) - - launch { - machine.run(hypervisor) - println("Hypervisor finished") - } - yield() - - val vm = hypervisor.createMachine(model) - vm.run(workloadA) - - yield() - machine.close() - - assertAll( - { assertEquals(1113300.0, listener.totalRequestedWork, "Requested Burst does not match") }, - { assertEquals(1023300.0, listener.totalGrantedWork, "Granted Burst does not match") }, - { assertEquals(90000.0, listener.totalOvercommittedWork, "Overcommissioned Burst does not match") }, - { assertEquals(1200000, clock.millis()) { "Current time is correct" } } - ) - } - - /** - * Test overcommitting of resources via the hypervisor with two VMs. - */ - @Test - fun testOvercommittedDual() = runBlockingSimulation { - val listener = object : SimHypervisor.Listener { - var totalRequestedWork = 0.0 - var totalGrantedWork = 0.0 - var totalOvercommittedWork = 0.0 - - override fun onSliceFinish( - hypervisor: SimHypervisor, - totalWork: Double, - grantedWork: Double, - overcommittedWork: Double, - interferedWork: Double, - cpuUsage: Double, - cpuDemand: Double - ) { - totalRequestedWork += totalWork - totalGrantedWork += grantedWork - totalOvercommittedWork += overcommittedWork - } - } - - val duration = 5 * 60L - val workloadA = - SimTraceWorkload( - sequenceOf( - SimTraceWorkload.Fragment(0, duration * 1000, 28.0, 1), - SimTraceWorkload.Fragment(duration * 1000, duration * 1000, 3500.0, 1), - SimTraceWorkload.Fragment(duration * 2000, duration * 1000, 0.0, 1), - SimTraceWorkload.Fragment(duration * 3000, duration * 1000, 183.0, 1) - ), - ) - val workloadB = - SimTraceWorkload( - sequenceOf( - SimTraceWorkload.Fragment(0, duration * 1000, 28.0, 1), - SimTraceWorkload.Fragment(duration * 1000, duration * 1000, 3100.0, 1), - SimTraceWorkload.Fragment(duration * 2000, duration * 1000, 0.0, 1), - SimTraceWorkload.Fragment(duration * 3000, duration * 1000, 73.0, 1) - ) - ) - - val platform = FlowEngine(coroutineContext, clock) - val machine = SimBareMetalMachine( - platform, model, SimplePowerDriver(ConstantPowerModel(0.0)) - ) - val hypervisor = SimFairShareHypervisor(platform, listener = listener) - - launch { - machine.run(hypervisor) - } - - yield() - coroutineScope { - launch { - val vm = hypervisor.createMachine(model) - vm.run(workloadA) - vm.close() - } - val vm = hypervisor.createMachine(model) - vm.run(workloadB) - vm.close() - } - yield() - machine.close() - yield() - - assertAll( - { assertEquals(2073600.0, listener.totalRequestedWork, "Requested Burst does not match") }, - { assertEquals(1053600.0, listener.totalGrantedWork, "Granted Burst does not match") }, - { assertEquals(1020000.0, listener.totalOvercommittedWork, "Overcommissioned Burst does not match") }, - { assertEquals(1200000, clock.millis()) } - ) - } - - @Test - fun testMultipleCPUs() = runBlockingSimulation { - val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 2) - val model = MachineModel( - cpus = List(cpuNode.coreCount) { ProcessingUnit(cpuNode, it, 3200.0) }, - memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } - ) - - val platform = FlowEngine(coroutineContext, clock) - val machine = SimBareMetalMachine( - platform, model, SimplePowerDriver(ConstantPowerModel(0.0)) - ) - val hypervisor = SimFairShareHypervisor(platform) - - assertDoesNotThrow { - launch { - machine.run(hypervisor) - } - } - - machine.close() - } - - @Test - fun testInterference() = runBlockingSimulation { - val cpuNode = ProcessingNode("Intel", "Xeon", "amd64", 2) - val model = MachineModel( - cpus = List(cpuNode.coreCount) { ProcessingUnit(cpuNode, it, 3200.0) }, - memory = List(4) { MemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } - ) - - val groups = listOf( - VmInterferenceGroup(targetLoad = 0.0, score = 0.9, members = setOf("a", "b")), - VmInterferenceGroup(targetLoad = 0.0, score = 0.6, members = setOf("a", "c")), - VmInterferenceGroup(targetLoad = 0.1, score = 0.8, members = setOf("a", "n")) - ) - val interferenceModel = VmInterferenceModel(groups) - - val platform = FlowEngine(coroutineContext, clock) - val machine = SimBareMetalMachine( - platform, model, SimplePowerDriver(ConstantPowerModel(0.0)) - ) - val hypervisor = SimFairShareHypervisor(platform, interferenceDomain = interferenceModel.newDomain()) - - val duration = 5 * 60L - val workloadA = - SimTraceWorkload( - sequenceOf( - SimTraceWorkload.Fragment(0, duration * 1000, 0.0, 1), - SimTraceWorkload.Fragment(duration * 1000, duration * 1000, 28.0, 1), - SimTraceWorkload.Fragment(duration * 2000, duration * 1000, 3500.0, 1), - SimTraceWorkload.Fragment(duration * 3000, duration * 1000, 183.0, 1) - ), - ) - val workloadB = - SimTraceWorkload( - sequenceOf( - SimTraceWorkload.Fragment(0, duration * 1000, 0.0, 1), - SimTraceWorkload.Fragment(duration * 1000, duration * 1000, 28.0, 1), - SimTraceWorkload.Fragment(duration * 2000, duration * 1000, 3100.0, 1), - SimTraceWorkload.Fragment(duration * 3000, duration * 1000, 73.0, 1) - ) - ) - - launch { - machine.run(hypervisor) - } - - coroutineScope { - launch { - val vm = hypervisor.createMachine(model, "a") - vm.run(workloadA) - vm.close() - } - val vm = hypervisor.createMachine(model, "b") - vm.run(workloadB) - vm.close() - } - - machine.close() - } -} diff --git a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt index 95fb6679..b05ffd22 100644 --- a/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt +++ b/opendc-simulator/opendc-simulator-compute/src/test/kotlin/org/opendc/simulator/compute/kernel/SimSpaceSharedHypervisorTest.kt @@ -75,10 +75,8 @@ internal class SimSpaceSharedHypervisorTest { ) val engine = FlowEngine(coroutineContext, clock) - val machine = SimBareMetalMachine( - FlowEngine(coroutineContext, clock), machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) - ) - val hypervisor = SimSpaceSharedHypervisor(engine) + val machine = SimBareMetalMachine(engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0))) + val hypervisor = SimSpaceSharedHypervisor(engine, null, null) launch { machine.run(hypervisor) } val vm = hypervisor.createMachine(machineModel) @@ -99,10 +97,8 @@ internal class SimSpaceSharedHypervisorTest { val duration = 5 * 60L * 1000 val workload = SimRuntimeWorkload(duration) val engine = FlowEngine(coroutineContext, clock) - val machine = SimBareMetalMachine( - engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) - ) - val hypervisor = SimSpaceSharedHypervisor(engine) + val machine = SimBareMetalMachine(engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0))) + val hypervisor = SimSpaceSharedHypervisor(engine, null, null) launch { machine.run(hypervisor) } yield() @@ -125,7 +121,7 @@ internal class SimSpaceSharedHypervisorTest { val machine = SimBareMetalMachine( engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimSpaceSharedHypervisor(engine) + val hypervisor = SimSpaceSharedHypervisor(engine, null, null) launch { machine.run(hypervisor) } yield() @@ -146,7 +142,7 @@ internal class SimSpaceSharedHypervisorTest { val machine = SimBareMetalMachine( engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimSpaceSharedHypervisor(engine) + val hypervisor = SimSpaceSharedHypervisor(engine, null, null) launch { machine.run(hypervisor) } yield() @@ -172,7 +168,7 @@ internal class SimSpaceSharedHypervisorTest { fun testConcurrentWorkloadFails() = runBlockingSimulation { val engine = FlowEngine(coroutineContext, clock) val machine = SimBareMetalMachine(engine, machineModel, SimplePowerDriver(ConstantPowerModel(0.0))) - val hypervisor = SimSpaceSharedHypervisor(engine) + val hypervisor = SimSpaceSharedHypervisor(engine, null, null) launch { machine.run(hypervisor) } yield() @@ -196,7 +192,7 @@ internal class SimSpaceSharedHypervisorTest { val machine = SimBareMetalMachine( interpreter, machineModel, SimplePowerDriver(ConstantPowerModel(0.0)) ) - val hypervisor = SimSpaceSharedHypervisor(interpreter) + val hypervisor = SimSpaceSharedHypervisor(interpreter, null, null) launch { machine.run(hypervisor) } yield() diff --git a/opendc-simulator/opendc-simulator-flow/src/jmh/kotlin/org/opendc/simulator/flow/FlowBenchmarks.kt b/opendc-simulator/opendc-simulator-flow/src/jmh/kotlin/org/opendc/simulator/flow/FlowBenchmarks.kt index 4834f10f..e927f81d 100644 --- a/opendc-simulator/opendc-simulator-flow/src/jmh/kotlin/org/opendc/simulator/flow/FlowBenchmarks.kt +++ b/opendc-simulator/opendc-simulator-flow/src/jmh/kotlin/org/opendc/simulator/flow/FlowBenchmarks.kt @@ -83,8 +83,8 @@ class FlowBenchmarks { return scope.runBlockingSimulation { val switch = MaxMinFlowMultiplexer(engine) - switch.addOutput(FlowSink(engine, 3000.0)) - switch.addOutput(FlowSink(engine, 3000.0)) + FlowSink(engine, 3000.0).startConsumer(switch.newOutput()) + FlowSink(engine, 3000.0).startConsumer(switch.newOutput()) val provider = switch.newInput() return@runBlockingSimulation provider.consume(TraceFlowSource(state.trace)) @@ -96,8 +96,8 @@ class FlowBenchmarks { return scope.runBlockingSimulation { val switch = MaxMinFlowMultiplexer(engine) - switch.addOutput(FlowSink(engine, 3000.0)) - switch.addOutput(FlowSink(engine, 3000.0)) + FlowSink(engine, 3000.0).startConsumer(switch.newOutput()) + FlowSink(engine, 3000.0).startConsumer(switch.newOutput()) repeat(3) { launch { @@ -113,8 +113,8 @@ class FlowBenchmarks { return scope.runBlockingSimulation { val switch = ForwardingFlowMultiplexer(engine) - switch.addOutput(FlowSink(engine, 3000.0)) - switch.addOutput(FlowSink(engine, 3000.0)) + FlowSink(engine, 3000.0).startConsumer(switch.newOutput()) + FlowSink(engine, 3000.0).startConsumer(switch.newOutput()) val provider = switch.newInput() return@runBlockingSimulation provider.consume(TraceFlowSource(state.trace)) @@ -126,8 +126,8 @@ class FlowBenchmarks { return scope.runBlockingSimulation { val switch = ForwardingFlowMultiplexer(engine) - switch.addOutput(FlowSink(engine, 3000.0)) - switch.addOutput(FlowSink(engine, 3000.0)) + FlowSink(engine, 3000.0).startConsumer(switch.newOutput()) + FlowSink(engine, 3000.0).startConsumer(switch.newOutput()) repeat(2) { launch { diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/AbstractFlowConsumer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/AbstractFlowConsumer.kt index c8092082..b02426e3 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/AbstractFlowConsumer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/AbstractFlowConsumer.kt @@ -83,14 +83,18 @@ public abstract class AbstractFlowConsumer(private val engine: FlowEngine, initi /** * The previous demand for the consumer. */ - private var previousDemand = 0.0 + private var _previousDemand = 0.0 + private var _previousCapacity = 0.0 /** * Update the counters of the flow consumer. */ protected fun updateCounters(ctx: FlowConnection, delta: Long) { - val demand = previousDemand - previousDemand = ctx.demand + val demand = _previousDemand + val capacity = _previousCapacity + + _previousDemand = ctx.demand + _previousCapacity = ctx.capacity if (delta <= 0) { return @@ -98,23 +102,23 @@ public abstract class AbstractFlowConsumer(private val engine: FlowEngine, initi val counters = _counters val deltaS = delta / 1000.0 - val work = demand * deltaS + val total = demand * deltaS + val work = capacity * deltaS val actualWork = ctx.rate * deltaS - val remainingWork = work - actualWork counters.demand += work counters.actual += actualWork - counters.overcommit += remainingWork + counters.remaining += (total - actualWork) } /** * Update the counters of the flow consumer. */ - protected fun updateCounters(demand: Double, actual: Double, overcommit: Double) { + protected fun updateCounters(demand: Double, actual: Double, remaining: Double) { val counters = _counters counters.demand += demand counters.actual += actual - counters.overcommit += overcommit + counters.remaining += remaining } final override fun startConsumer(source: FlowSource) { diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowCounters.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowCounters.kt index e15d7643..a717ae6e 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowCounters.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowCounters.kt @@ -37,9 +37,9 @@ public interface FlowCounters { public val actual: Double /** - * The accumulated flow that could not be transferred over the connection. + * The amount of capacity that was not utilized. */ - public val overcommit: Double + public val remaining: Double /** * The accumulated flow lost due to interference between sources. diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt index 17de601a..7eaaf6c2 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/FlowForwarder.kt @@ -242,10 +242,11 @@ public class FlowForwarder(private val engine: FlowEngine, private val isCoupled val counters = _counters val deltaS = delta / 1000.0 + val total = ctx.capacity * deltaS val work = _demand * deltaS val actualWork = ctx.rate * deltaS counters.demand += work counters.actual += actualWork - counters.overcommit += (work - actualWork) + counters.remaining += (total - actualWork) } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowCountersImpl.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowCountersImpl.kt index 141d335d..d2fa5228 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowCountersImpl.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/internal/FlowCountersImpl.kt @@ -30,17 +30,17 @@ import org.opendc.simulator.flow.FlowCounters internal class FlowCountersImpl : FlowCounters { override var demand: Double = 0.0 override var actual: Double = 0.0 - override var overcommit: Double = 0.0 + override var remaining: Double = 0.0 override var interference: Double = 0.0 override fun reset() { demand = 0.0 actual = 0.0 - overcommit = 0.0 + remaining = 0.0 interference = 0.0 } override fun toString(): String { - return "FlowCounters[demand=$demand,actual=$actual,overcommit=$overcommit,interference=$interference]" + return "FlowCounters[demand=$demand,actual=$actual,remaining=$remaining,interference=$interference]" } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/FlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/FlowMultiplexer.kt index 17b82391..04ba7f21 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/FlowMultiplexer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/FlowMultiplexer.kt @@ -39,7 +39,22 @@ public interface FlowMultiplexer { /** * The outputs of the multiplexer over which the flows will be distributed. */ - public val outputs: Set + public val outputs: Set + + /** + * The actual processing rate of the multiplexer. + */ + public val rate: Double + + /** + * The demanded processing rate of the input. + */ + public val demand: Double + + /** + * The capacity of the outputs. + */ + public val capacity: Double /** * The flow counters to track the flow metrics of all multiplexer inputs. @@ -59,12 +74,27 @@ public interface FlowMultiplexer { public fun removeInput(input: FlowConsumer) /** - * Add the specified [output] to the multiplexer. + * Create a new output on this multiplexer. */ - public fun addOutput(output: FlowConsumer) + public fun newOutput(): FlowSource /** - * Clear all inputs and outputs from the switch. + * Remove [output] from this multiplexer. + */ + public fun removeOutput(output: FlowSource) + + /** + * Clear all inputs and outputs from the multiplexer. */ public fun clear() + + /** + * Clear the inputs of the multiplexer. + */ + public fun clearInputs() + + /** + * Clear the outputs of the multiplexer. + */ + public fun clearOutputs() } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt index 6dd9dcfb..125d10fe 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexer.kt @@ -38,35 +38,44 @@ public class ForwardingFlowMultiplexer(private val engine: FlowEngine) : FlowMul get() = _inputs private val _inputs = mutableSetOf() - override val outputs: Set + override val outputs: Set get() = _outputs - private val _outputs = mutableSetOf() - private val _availableOutputs = ArrayDeque() + private val _outputs = mutableSetOf() + private val _availableOutputs = ArrayDeque() override val counters: FlowCounters = object : FlowCounters { override val demand: Double - get() = _outputs.sumOf { it.counters.demand } + get() = _outputs.sumOf { it.forwarder.counters.demand } override val actual: Double - get() = _outputs.sumOf { it.counters.actual } - override val overcommit: Double - get() = _outputs.sumOf { it.counters.overcommit } + get() = _outputs.sumOf { it.forwarder.counters.actual } + override val remaining: Double + get() = _outputs.sumOf { it.forwarder.counters.remaining } override val interference: Double - get() = _outputs.sumOf { it.counters.interference } + get() = _outputs.sumOf { it.forwarder.counters.interference } override fun reset() { - for (input in _outputs) { - input.counters.reset() + for (output in _outputs) { + output.forwarder.counters.reset() } } - override fun toString(): String = "FlowCounters[demand=$demand,actual=$actual,overcommit=$overcommit]" + override fun toString(): String = "FlowCounters[demand=$demand,actual=$actual,remaining=$remaining]" } + override val rate: Double + get() = _outputs.sumOf { it.forwarder.rate } + + override val demand: Double + get() = _outputs.sumOf { it.forwarder.demand } + + override val capacity: Double + get() = _outputs.sumOf { it.forwarder.capacity } + override fun newInput(key: InterferenceKey?): FlowConsumer { - val forwarder = checkNotNull(_availableOutputs.poll()) { "No capacity to serve request" } - val output = Input(forwarder) - _inputs += output - return output + val output = checkNotNull(_availableOutputs.poll()) { "No capacity to serve request" } + val input = Input(output) + _inputs += input + return input } override fun removeInput(input: FlowConsumer) { @@ -74,51 +83,72 @@ public class ForwardingFlowMultiplexer(private val engine: FlowEngine) : FlowMul return } - (input as Input).close() + val output = (input as Input).output + output.forwarder.cancel() + _availableOutputs += output } - override fun addOutput(output: FlowConsumer) { - if (output in outputs) { - return - } - + override fun newOutput(): FlowSource { val forwarder = FlowForwarder(engine) + val output = Output(forwarder) _outputs += output - _availableOutputs += forwarder + return output + } - output.startConsumer(object : FlowSource by forwarder { - override fun onStop(conn: FlowConnection, now: Long, delta: Long) { - _outputs -= output + override fun removeOutput(output: FlowSource) { + if (!_outputs.remove(output)) { + return + } - forwarder.onStop(conn, now, delta) - } - }) + val forwarder = (output as Output).forwarder + forwarder.close() } - override fun clear() { - for (input in _outputs) { - input.cancel() + override fun clearInputs() { + for (input in _inputs) { + val output = input.output + output.forwarder.cancel() + _availableOutputs += output } - _outputs.clear() - // Inputs are implicitly cancelled by the output forwarders _inputs.clear() } + override fun clearOutputs() { + for (output in _outputs) { + output.forwarder.cancel() + } + _outputs.clear() + _availableOutputs.clear() + } + + override fun clear() { + clearOutputs() + clearInputs() + } + /** * An input on the multiplexer. */ - private inner class Input(private val forwarder: FlowForwarder) : FlowConsumer by forwarder { - /** - * Close the input. - */ - fun close() { - // We explicitly do not close the forwarder here in order to re-use it across input resources. - _inputs -= this - _availableOutputs += forwarder + private inner class Input(@JvmField val output: Output) : FlowConsumer by output.forwarder { + override fun toString(): String = "ForwardingFlowMultiplexer.Input" + } + + /** + * An output on the multiplexer. + */ + private inner class Output(@JvmField val forwarder: FlowForwarder) : FlowSource by forwarder { + override fun onStart(conn: FlowConnection, now: Long) { + _availableOutputs += this + forwarder.onStart(conn, now) } - override fun toString(): String = "ForwardingFlowMultiplexer.Input" + override fun onStop(conn: FlowConnection, now: Long, delta: Long) { + forwarder.cancel() + forwarder.onStop(conn, now, delta) + } + + override fun toString(): String = "ForwardingFlowMultiplexer.Output" } } diff --git a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt index 7232df35..5ff0fb8d 100644 --- a/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt +++ b/opendc-simulator/opendc-simulator-flow/src/main/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexer.kt @@ -52,9 +52,9 @@ public class MaxMinFlowMultiplexer( /** * The outputs of the multiplexer. */ - override val outputs: Set + override val outputs: Set get() = _outputs - private val _outputs = mutableSetOf() + private val _outputs = mutableSetOf() private val _activeOutputs = mutableListOf() /** @@ -67,22 +67,35 @@ public class MaxMinFlowMultiplexer( /** * The actual processing rate of the multiplexer. */ + public override val rate: Double + get() = _rate private var _rate = 0.0 /** * The demanded processing rate of the input. */ + public override val demand: Double + get() = _demand private var _demand = 0.0 /** * The capacity of the outputs. */ + public override val capacity: Double + get() = _capacity private var _capacity = 0.0 /** * Flag to indicate that the scheduler is active. */ private var _schedulerActive = false + private var _lastSchedulerCycle = Long.MAX_VALUE + + /** + * The last convergence timestamp and the input. + */ + private var _lastConverge: Long = Long.MIN_VALUE + private var _lastConvergeInput: Input? = null override fun newInput(key: InterferenceKey?): FlowConsumer { val provider = Input(_capacity, key) @@ -90,14 +103,6 @@ public class MaxMinFlowMultiplexer( return provider } - override fun addOutput(output: FlowConsumer) { - val consumer = Output(output) - if (_outputs.add(output)) { - _activeOutputs.add(consumer) - output.startConsumer(consumer) - } - } - override fun removeInput(input: FlowConsumer) { if (!_inputs.remove(input)) { return @@ -106,16 +111,38 @@ public class MaxMinFlowMultiplexer( (input as Input).close() } - override fun clear() { - for (input in _activeOutputs) { + override fun newOutput(): FlowSource { + val output = Output() + _outputs.add(output) + return output + } + + override fun removeOutput(output: FlowSource) { + if (!_outputs.remove(output)) { + return + } + + // This cast should always succeed since only `Output` instances should be added to `_outputs` + (output as Output).cancel() + } + + override fun clearInputs() { + for (input in _inputs) { input.cancel() } - _activeOutputs.clear() + _inputs.clear() + } - for (output in _activeInputs) { + override fun clearOutputs() { + for (output in _outputs) { output.cancel() } - _activeInputs.clear() + _outputs.clear() + } + + override fun clear() { + clearOutputs() + clearInputs() } /** @@ -125,10 +152,13 @@ public class MaxMinFlowMultiplexer( if (_schedulerActive) { return } - + val lastSchedulerCycle = _lastSchedulerCycle + val delta = max(0, now - lastSchedulerCycle) _schedulerActive = true + _lastSchedulerCycle = now + try { - doSchedule(now) + doSchedule(now, delta) } finally { _schedulerActive = false } @@ -137,12 +167,17 @@ public class MaxMinFlowMultiplexer( /** * Schedule the inputs over the outputs. */ - private fun doSchedule(now: Long) { + private fun doSchedule(now: Long, delta: Long) { val activeInputs = _activeInputs val activeOutputs = _activeOutputs + // Update the counters of the scheduler + updateCounters(delta) + // If there is no work yet, mark the inputs as idle. if (activeInputs.isEmpty()) { + _demand = 0.0 + _rate = 0.0 return } @@ -156,6 +191,7 @@ public class MaxMinFlowMultiplexer( // Remove outputs that have finished if (!input.isActive) { + input.actualRate = 0.0 inputIterator.remove() } } @@ -168,7 +204,8 @@ public class MaxMinFlowMultiplexer( // Divide the available output capacity fairly over the inputs using max-min fair sharing var remaining = activeInputs.size - for (input in activeInputs) { + for (i in activeInputs.indices) { + val input = activeInputs[i] val availableShare = availableCapacity / remaining-- val grantedRate = min(input.allowedRate, availableShare) @@ -192,7 +229,8 @@ public class MaxMinFlowMultiplexer( activeOutputs.sort() // Divide the requests over the available capacity of the input resources fairly - for (output in activeOutputs) { + for (i in activeOutputs.indices) { + val output = activeOutputs[i] val inputCapacity = output.capacity val fraction = inputCapacity / capacity val grantedSpeed = rate * fraction @@ -219,6 +257,29 @@ public class MaxMinFlowMultiplexer( } } + /** + * The previous capacity of the multiplexer. + */ + private var _previousCapacity = 0.0 + + /** + * Update the counters of the scheduler. + */ + private fun updateCounters(delta: Long) { + val previousCapacity = _previousCapacity + _previousCapacity = _capacity + + if (delta <= 0) { + return + } + + val deltaS = delta / 1000.0 + + _counters.demand += _demand * deltaS + _counters.actual += _rate * deltaS + _counters.remaining += (previousCapacity - _rate) * deltaS + } + /** * An internal [FlowConsumer] implementation for multiplexer inputs. */ @@ -252,6 +313,11 @@ public class MaxMinFlowMultiplexer( */ private var _lastPull: Long = Long.MIN_VALUE + /** + * The interference domain this input belongs to. + */ + private val interferenceDomain = this@MaxMinFlowMultiplexer.interferenceDomain + /** * Close the input. * @@ -269,7 +335,6 @@ public class MaxMinFlowMultiplexer( check(!_isClosed) { "Cannot re-use closed input" } _activeInputs += this - if (parent != null) { ctx.shouldConsumerConverge = true } @@ -287,14 +352,22 @@ public class MaxMinFlowMultiplexer( doUpdateCounters(delta) actualRate = 0.0 - this.limit = rate + limit = rate _lastPull = now runScheduler(now) } override fun onConverge(ctx: FlowConsumerContext, now: Long, delta: Long) { - parent?.onConverge(now, delta) + val lastConverge = _lastConverge + val parent = parent + + if (parent != null && (lastConverge < now || _lastConvergeInput == null)) { + _lastConverge = now + _lastConvergeInput = this + + parent.onConverge(now, max(0, now - lastConverge)) + } } override fun onFinish(ctx: FlowConsumerContext, now: Long, delta: Long, cause: Throwable?) { @@ -303,6 +376,14 @@ public class MaxMinFlowMultiplexer( limit = 0.0 actualRate = 0.0 _lastPull = now + + // Assign a new input responsible for handling the convergence events + if (_lastConvergeInput == this) { + _lastConvergeInput = null + } + + // Re-run scheduler to distribute new load + runScheduler(now) } /* Comparable */ @@ -328,35 +409,31 @@ public class MaxMinFlowMultiplexer( // Compute the performance penalty due to flow interference val perfScore = if (interferenceDomain != null) { - val load = _rate / capacity + val load = _rate / _capacity interferenceDomain.apply(key, load) } else { 1.0 } val deltaS = delta / 1000.0 - val work = limit * deltaS - val actualWork = actualRate * deltaS - val remainingWork = work - actualWork + val demand = limit * deltaS + val actual = actualRate * deltaS + val remaining = (capacity - actualRate) * deltaS - updateCounters(work, actualWork, remainingWork) + updateCounters(demand, actual, remaining) - val distCounters = _counters - distCounters.demand += work - distCounters.actual += actualWork - distCounters.overcommit += remainingWork - distCounters.interference += actualWork * max(0.0, 1 - perfScore) + _counters.interference += actual * max(0.0, 1 - perfScore) } } /** * An internal [FlowSource] implementation for multiplexer outputs. */ - private inner class Output(private val provider: FlowConsumer) : FlowSource, Comparable { + private inner class Output : FlowSource, Comparable { /** * The active [FlowConnection] of this source. */ - private var _ctx: FlowConnection? = null + private var _conn: FlowConnection? = null /** * The capacity of this output. @@ -367,27 +444,33 @@ public class MaxMinFlowMultiplexer( * Push the specified rate to the consumer. */ fun push(rate: Double) { - _ctx?.push(rate) + _conn?.push(rate) } /** * Cancel this output. */ fun cancel() { - provider.cancel() + _conn?.close() } override fun onStart(conn: FlowConnection, now: Long) { - assert(_ctx == null) { "Source running concurrently" } - _ctx = conn + assert(_conn == null) { "Source running concurrently" } + _conn = conn capacity = conn.capacity + _activeOutputs.add(this) + updateCapacity() } override fun onStop(conn: FlowConnection, now: Long, delta: Long) { - _ctx = null + _conn = null capacity = 0.0 + _activeOutputs.remove(this) + updateCapacity() + + runScheduler(now) } override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { @@ -397,6 +480,7 @@ public class MaxMinFlowMultiplexer( updateCapacity() } + // Re-run scheduler to distribute new load runScheduler(now) return Long.MAX_VALUE } diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt index d548451f..12e72b8f 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/FlowForwarderTest.kt @@ -217,7 +217,7 @@ internal class FlowForwarderTest { assertEquals(2.0, source.counters.actual) assertEquals(source.counters.actual, forwarder.counters.actual) { "Actual work" } assertEquals(source.counters.demand, forwarder.counters.demand) { "Work demand" } - assertEquals(source.counters.overcommit, forwarder.counters.overcommit) { "Overcommitted work" } + assertEquals(source.counters.remaining, forwarder.counters.remaining) { "Overcommitted work" } assertEquals(2000, clock.millis()) } diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt deleted file mode 100644 index 3475f027..00000000 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ExclusiveFlowMultiplexerTest.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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.flow.mux - -import kotlinx.coroutines.yield -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertAll -import org.junit.jupiter.api.assertThrows -import org.opendc.simulator.core.runBlockingSimulation -import org.opendc.simulator.flow.* -import org.opendc.simulator.flow.internal.FlowEngineImpl -import org.opendc.simulator.flow.source.FixedFlowSource -import org.opendc.simulator.flow.source.FlowSourceRateAdapter -import org.opendc.simulator.flow.source.TraceFlowSource - -/** - * Test suite for the [ForwardingFlowMultiplexer] class. - */ -internal class ExclusiveFlowMultiplexerTest { - /** - * Test a trace workload. - */ - @Test - fun testTrace() = runBlockingSimulation { - val engine = FlowEngineImpl(coroutineContext, clock) - - val speed = mutableListOf() - - val duration = 5 * 60L - val workload = - TraceFlowSource( - sequenceOf( - TraceFlowSource.Fragment(duration * 1000, 28.0), - TraceFlowSource.Fragment(duration * 1000, 3500.0), - TraceFlowSource.Fragment(duration * 1000, 0.0), - TraceFlowSource.Fragment(duration * 1000, 183.0) - ), - ) - - val switch = ForwardingFlowMultiplexer(engine) - val source = FlowSink(engine, 3200.0) - val forwarder = FlowForwarder(engine) - val adapter = FlowSourceRateAdapter(forwarder, speed::add) - source.startConsumer(adapter) - switch.addOutput(forwarder) - - val provider = switch.newInput() - provider.consume(workload) - yield() - - assertAll( - { assertEquals(listOf(0.0, 28.0, 3200.0, 0.0, 183.0, 0.0), speed) { "Correct speed" } }, - { assertEquals(5 * 60L * 4000, clock.millis()) { "Took enough time" } } - ) - } - - /** - * Test runtime workload on hypervisor. - */ - @Test - fun testRuntimeWorkload() = runBlockingSimulation { - val engine = FlowEngineImpl(coroutineContext, clock) - - val duration = 5 * 60L * 1000 - val workload = FixedFlowSource(duration * 3.2, 1.0) - - val switch = ForwardingFlowMultiplexer(engine) - val source = FlowSink(engine, 3200.0) - - switch.addOutput(source) - - val provider = switch.newInput() - provider.consume(workload) - yield() - - assertEquals(duration, clock.millis()) { "Took enough time" } - } - - /** - * Test two workloads running sequentially. - */ - @Test - fun testTwoWorkloads() = runBlockingSimulation { - val engine = FlowEngineImpl(coroutineContext, clock) - - val duration = 5 * 60L * 1000 - val workload = object : FlowSource { - var isFirst = true - - override fun onStart(conn: FlowConnection, now: Long) { - isFirst = true - } - - override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { - return if (isFirst) { - isFirst = false - conn.push(1.0) - duration - } else { - conn.close() - Long.MAX_VALUE - } - } - } - - val switch = ForwardingFlowMultiplexer(engine) - val source = FlowSink(engine, 3200.0) - - switch.addOutput(source) - - val provider = switch.newInput() - provider.consume(workload) - yield() - provider.consume(workload) - assertEquals(duration * 2, clock.millis()) { "Took enough time" } - } - - /** - * Test concurrent workloads on the machine. - */ - @Test - fun testConcurrentWorkloadFails() = runBlockingSimulation { - val engine = FlowEngineImpl(coroutineContext, clock) - - val switch = ForwardingFlowMultiplexer(engine) - val source = FlowSink(engine, 3200.0) - - switch.addOutput(source) - - switch.newInput() - assertThrows { switch.newInput() } - } -} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexerTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexerTest.kt new file mode 100644 index 00000000..187dacd9 --- /dev/null +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/ForwardingFlowMultiplexerTest.kt @@ -0,0 +1,154 @@ +/* + * 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.flow.mux + +import kotlinx.coroutines.yield +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.internal.FlowEngineImpl +import org.opendc.simulator.flow.source.FixedFlowSource +import org.opendc.simulator.flow.source.FlowSourceRateAdapter +import org.opendc.simulator.flow.source.TraceFlowSource + +/** + * Test suite for the [ForwardingFlowMultiplexer] class. + */ +internal class ForwardingFlowMultiplexerTest { + /** + * Test a trace workload. + */ + @Test + fun testTrace() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val speed = mutableListOf() + + val duration = 5 * 60L + val workload = + TraceFlowSource( + sequenceOf( + TraceFlowSource.Fragment(duration * 1000, 28.0), + TraceFlowSource.Fragment(duration * 1000, 3500.0), + TraceFlowSource.Fragment(duration * 1000, 0.0), + TraceFlowSource.Fragment(duration * 1000, 183.0) + ), + ) + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + val forwarder = FlowForwarder(engine) + val adapter = FlowSourceRateAdapter(forwarder, speed::add) + source.startConsumer(adapter) + forwarder.startConsumer(switch.newOutput()) + + val provider = switch.newInput() + provider.consume(workload) + yield() + + assertAll( + { assertEquals(listOf(0.0, 28.0, 3200.0, 0.0, 183.0, 0.0), speed) { "Correct speed" } }, + { assertEquals(5 * 60L * 4000, clock.millis()) { "Took enough time" } } + ) + } + + /** + * Test runtime workload on hypervisor. + */ + @Test + fun testRuntimeWorkload() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val duration = 5 * 60L * 1000 + val workload = FixedFlowSource(duration * 3.2, 1.0) + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + + source.startConsumer(switch.newOutput()) + + val provider = switch.newInput() + provider.consume(workload) + yield() + + assertEquals(duration, clock.millis()) { "Took enough time" } + } + + /** + * Test two workloads running sequentially. + */ + @Test + fun testTwoWorkloads() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val duration = 5 * 60L * 1000 + val workload = object : FlowSource { + var isFirst = true + + override fun onStart(conn: FlowConnection, now: Long) { + isFirst = true + } + + override fun onPull(conn: FlowConnection, now: Long, delta: Long): Long { + return if (isFirst) { + isFirst = false + conn.push(1.0) + duration + } else { + conn.close() + Long.MAX_VALUE + } + } + } + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + + source.startConsumer(switch.newOutput()) + + val provider = switch.newInput() + provider.consume(workload) + yield() + provider.consume(workload) + assertEquals(duration * 2, clock.millis()) { "Took enough time" } + } + + /** + * Test concurrent workloads on the machine. + */ + @Test + fun testConcurrentWorkloadFails() = runBlockingSimulation { + val engine = FlowEngineImpl(coroutineContext, clock) + + val switch = ForwardingFlowMultiplexer(engine) + val source = FlowSink(engine, 3200.0) + + source.startConsumer(switch.newOutput()) + + switch.newInput() + assertThrows { switch.newInput() } + } +} diff --git a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexerTest.kt b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexerTest.kt index 9f6b8a2c..6e2cdb98 100644 --- a/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexerTest.kt +++ b/opendc-simulator/opendc-simulator-flow/src/test/kotlin/org/opendc/simulator/flow/mux/MaxMinFlowMultiplexerTest.kt @@ -44,7 +44,7 @@ internal class MaxMinFlowMultiplexerTest { val switch = MaxMinFlowMultiplexer(scheduler) val sources = List(2) { FlowSink(scheduler, 2000.0) } - sources.forEach { switch.addOutput(it) } + sources.forEach { it.startConsumer(switch.newOutput()) } val provider = switch.newInput() val consumer = FixedFlowSource(2000.0, 1.0) @@ -76,10 +76,11 @@ internal class MaxMinFlowMultiplexerTest { ) val switch = MaxMinFlowMultiplexer(scheduler) + val sink = FlowSink(scheduler, 3200.0) val provider = switch.newInput() try { - switch.addOutput(FlowSink(scheduler, 3200.0)) + sink.startConsumer(switch.newOutput()) provider.consume(workload) yield() } finally { @@ -89,7 +90,7 @@ internal class MaxMinFlowMultiplexerTest { assertAll( { assertEquals(1113300.0, switch.counters.demand, "Requested work does not match") }, { assertEquals(1023300.0, switch.counters.actual, "Actual work does not match") }, - { assertEquals(90000.0, switch.counters.overcommit, "Overcommitted work does not match") }, + { assertEquals(2816700.0, switch.counters.remaining, "Remaining capacity does not match") }, { assertEquals(1200000, clock.millis()) } ) } @@ -122,11 +123,12 @@ internal class MaxMinFlowMultiplexerTest { ) val switch = MaxMinFlowMultiplexer(scheduler) + val sink = FlowSink(scheduler, 3200.0) val providerA = switch.newInput() val providerB = switch.newInput() try { - switch.addOutput(FlowSink(scheduler, 3200.0)) + sink.startConsumer(switch.newOutput()) coroutineScope { launch { providerA.consume(workloadA) } @@ -140,7 +142,7 @@ internal class MaxMinFlowMultiplexerTest { assertAll( { assertEquals(2073600.0, switch.counters.demand, "Requested work does not match") }, { assertEquals(1053600.0, switch.counters.actual, "Granted work does not match") }, - { assertEquals(1020000.0, switch.counters.overcommit, "Overcommitted work does not match") }, + { assertEquals(2786400.0, switch.counters.remaining, "Remaining capacity does not match") }, { assertEquals(1200000, clock.millis()) } ) } diff --git a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt index 2b7c1ad7..6667c80c 100644 --- a/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt +++ b/opendc-simulator/opendc-simulator-network/src/main/kotlin/org/opendc/simulator/network/SimNetworkSwitchVirtual.kt @@ -63,11 +63,9 @@ public class SimNetworkSwitchVirtual(private val engine: FlowEngine) : SimNetwor get() = _provider private val _provider = mux.newInput() - override fun createConsumer(): FlowSource { - val forwarder = FlowForwarder(engine, isCoupled = true) - mux.addOutput(forwarder) - return forwarder - } + private val _source = mux.newOutput() + + override fun createConsumer(): FlowSource = _source override fun close() { isClosed = true diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt index d536f22d..9f88fecc 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt @@ -46,18 +46,14 @@ public class SimPdu( /** * The [FlowForwarder] that represents the input of the PDU. */ - private val forwarder = FlowForwarder(engine) + private val output = mux.newOutput() /** * Create a new PDU outlet. */ public fun newOutlet(): Outlet = Outlet(mux, mux.newInput()) - init { - mux.addOutput(forwarder) - } - - override fun createSource(): FlowSource = FlowMapper(forwarder) { _, rate -> + override fun createSource(): FlowSource = FlowMapper(output) { _, rate -> val loss = computePowerLoss(rate) rate + loss } diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt index 312f1d0f..46d659f8 100644 --- a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt @@ -42,19 +42,19 @@ public class SimUps( /** * The resource aggregator used to combine the input sources. */ - private val switch = MaxMinFlowMultiplexer(engine) + private val mux = MaxMinFlowMultiplexer(engine) /** * The [FlowConsumer] that represents the output of the UPS. */ - private val provider = switch.newInput() + private val provider = mux.newInput() /** * Create a new UPS outlet. */ public fun newInlet(): SimPowerInlet { val forward = FlowForwarder(engine, isCoupled = true) - switch.addOutput(forward) + forward.startConsumer(mux.newOutput()) return Inlet(forward) } -- cgit v1.2.3