Advanced
Clock Synchronization
In simulation a clear distinction is made between real time and simulation time. With real time we refer to the wall-clock time. It represents the execution time of the experiment. The simulation time is an attribute of the simulator.
To support use cases where a simulation may drive a demonstration or system check, the kalasim
API allows to run a simulation at a defined clock speed. Such real-time simulations may be necessary
- If you have hardware-in-the-loop
- If the intent of the simulation is to drive a visualization of a process
- If there is human interaction with your simulation, or
- If you want to analyze the real-time behavior of an algorithm
import org.kalasim.*
import kotlin.time.Duration.Companion.seconds
val timeBefore = System.currentTimeMillis()
createSimulation {
enableComponentLogger()
// enable real-time clock synchronization
ClockSync(tickDuration = 1.seconds)
run(10)
}
println("time passed ${System.currentTimeMillis() - timeBefore})")
To enable clock synchronization, we need to add a ClockSync
to our simulation. We need to define what one tick in simulation time corresponds to in wall time. In the example, one tick equals to one second wall time. This is configured with the parameter tickDuration
. It defines the duration of a simulation tick in wall clock coordinates. It can be created with Duration.ofSeconds(1)
, Duration.ofMinutes(10)
and so on.
ClockSync
also provides settings for more advanced uses-cases
- To run simulations, in more than realtime, the user can specify
speedUp
to run a simulation faster (speedUp
> 1) or slower (speedUp
< 1) than realtime. It defaults to1
, that is no speed-up will be applied. - The argument
syncsPerTick
defines how often a clock synchronization should happen. Per default it synchronizes once per tick (i.e. an 1-increment of simulation time).
It may happen that a simulation is too complex to run at a defined clock. In such a situation, it (i.e. Environment.run()
) will throw a ClockOverloadException
if the user has specified a maximum delay maxDelay
parameter between simulation and wall clock coordinates.
Operational Control
Even if kalasim
tries to provide a simplistic, efficient, declarative approach to define a simulation, it may come along with computational demands simulation. To allow introspection into time-complexity of the underlying computations, the user may want to enable the built-in env.tickMetrics
monitor to analyze how much time is spent per time unit (aka tick). This monitor can be enabled by calling enableTickMetrics()
when configuring the simulation.
import org.kalasim.*
import org.kalasim.plot.letsplot.display
createSimulation {
enableTickMetrics()
object : Component() {
override fun process() = sequence {
while(true) {
// create some artificial non-linear compute load
if(nowTT.value < 7)
Thread.sleep((nowTT.value * 100).toLong())
else {
Thread.sleep(100)
}
hold(1.minutes)
}
}
}
run(10.hours)
tickMetrics.display().show()
}
Performance tuning
There are multiple ways to improve the performance of a simulation.
- Disable internal event logging: The interaction model is configured by default to provide insights into the simulation via the event log. However, to optimize performance of a simulation a user may want to consume only custom event-types. If so, internal interaction logging can be adjusted by setting a logging policy.
- Disable component statistics: Components and queues log various component statistics with built-in monitors which can be adjusted by setting a logging policy to reduce compute and memory footprint of a simulation.
- Set the correct
AssertMode
: The assertion mode determines which internal consistency checks are being performed. The mode can be set toFull
(Slowest),Light
(default) orOff
(Fastest). Depending on simulation logic and complexity, this will improve performance by ~20%.
To further fine-tune and optimize simulation performance and to reveal bottlenecks, a JVM profiler (such as yourkit or the built-in profiler of Intellij IDEA Ultimate) can be used. Both call-counts and spent-time analysis have been proven useful here.
Continuous Simulation
For some use-cases, simulations may run for a very long simulation and wall time. To prevent internal metrics gathering from consuming all available memory, it needs to be disabled or at least configured carefully. This can be achieved, but either disabling timelines and monitors manually on a per-entity basis, or by setting a sensible default policy via Environment.entityTrackingDefaults
For each entity type a corresponding tracking-policy TrackingConfig
can be provisioned along with an entity matcher to narrow down its scope. A tracking-policy allows to change
- How events are logged
- How internal metrics are gathered
There are different default implementations, but the user can also implement and register custom tracking-configurations.
- ComponentTrackingConfig
- ResourceTrackingConfig
- StateTrackingConfig
- ComponentCollectionTrackingConfig
//import org.kalasim.*
import org.kalasim.misc.*
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
class Driver : Resource(trackingConfig = ResourceTrackingConfig(trackUtilization = false))
class TrafficLight : State<String>("red", trackingConfig = StateTrackingConfig(logCreation = false))
class Car : Component(
trackingConfig = ComponentTrackingConfig(logInteractionEvents = false)
) {
val trafficLight = get<TrafficLight>()
val driver = get<Driver>()
override fun process() = sequence {
request(driver) {
hold(30.minutes, description = "driving")
wait(trafficLight, "green")
}
}
}
createSimulation {
enableComponentLogger()
// in addition or alternatively we can also change the environment defaults
entityTrackingDefaults.DefaultComponentConfig =
ComponentTrackingConfig(logStateChangeEvents = false)
// create simulation entities
dependency { TrafficLight() }
dependency { Driver() }
Car()
}.run(5.hours)
Note
Tracking configuration policies defaults must be set before instantiating simulation entities to be used
To disable all metrics and to minimize internal event logging, the user can run env.entityTrackingDefaults.disableAll()
The same mechanism applies also fine-tune the internal event logging. By disabling some - not-needed for production - events, simulation performance can be improved significantly.
Save and Load Simulations
kalasim
does not include a default mechanism to serialize and deserialize simulations yet. However, it seems that with xstream that Environment
can be saved including its current simulation state across all included entities. It can be restored from the xml snapshot and continued with run()
.
We have not succeeded to do the same with gson yet. Also, some experiments with kotlinx.serialization were not that successful.
Internal State Validation
The simulation engine provides different levels of internal consistency checks. As these are partially computationally expensive these can be be/disabled. There are 3 modes
OFF
- Productive mode, where asserts that may impact performance are disabled.LIGHT
- Disables compute-intensive asserts. This will have a minimal to moderate performance impact on simulations.FULL
- Full introspection, this will have a measurable performance impact on simulations. E.g. it will validate that passive components are not scheduled, and queued components have unique names.
Switching off asserts, will typically optimize performance by another ~20% (depending on simulation logic).