diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ModelConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ModelConstructor.kt
index 5a664fe..5cc2f36 100644
--- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ModelConstructor.kt
+++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ModelConstructor.kt
@@ -3,14 +3,14 @@ package space.kscience.controls.constructor
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.newCoroutineContext
-import space.kscience.controls.time.AsyncTimeProvider
+import space.kscience.controls.time.clock
 import space.kscience.dataforge.context.Context
 import kotlin.coroutines.CoroutineContext
 
 public abstract class ModelConstructor(
     final override val context: Context,
     vararg dependencies: DeviceState<*>,
-) : StateContainer, CoroutineScope, AsyncTimeProvider{
+) : StateContainer, CoroutineScope{
 
     @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
     override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob())
@@ -31,4 +31,6 @@ public abstract class ModelConstructor(
     override fun unregisterElement(constructorElement: ConstructorElement) {
         _constructorElements.remove(constructorElement)
     }
-}
\ No newline at end of file
+}
+
+public val ModelConstructor.clock get() = context.clock
\ No newline at end of file
diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt
index 47b74ca..24c36e8 100644
--- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt
+++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt
@@ -25,7 +25,7 @@ public class TimerState(
 
     private val clock = MutableStateFlow(initialValue)
 
-    private val updateJob = clockManager.context.launch(clockManager.asDispatcher()) {
+    private val updateJob = clockManager.context.launch(clockManager.dispatcher) {
         while (isActive) {
             clock.value = clockManager.clock.now()
             delay(tick)
diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt
index 0f7b9de..1e941bb 100644
--- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt
@@ -5,9 +5,6 @@ import kotlinx.coroutines.Job
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.*
 import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
-import space.kscience.controls.time.AsyncClock
-import space.kscience.controls.time.AsyncTimeProvider
-import space.kscience.controls.time.clock
 import space.kscience.dataforge.context.ContextAware
 import space.kscience.dataforge.context.info
 import space.kscience.dataforge.context.logger
@@ -23,7 +20,7 @@ import space.kscience.dataforge.names.parseAsName
  *  When canceled, cancels all running processes.
  */
 @DfType(DEVICE_TARGET)
-public interface Device : ContextAware, WithLifeCycle, CoroutineScope, AsyncTimeProvider {
+public interface Device : ContextAware, WithLifeCycle, CoroutineScope {
 
     /**
      * Initial configuration meta for the device
@@ -71,8 +68,6 @@ public interface Device : ContextAware, WithLifeCycle, CoroutineScope, AsyncTime
      */
     override suspend fun start(): Unit = Unit
 
-    override val clock: AsyncClock get() = context.clock
-
     /**
      * Close and terminate the device. This function does not wait for the device to be closed.
      */
diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/converters.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/converters.kt
index 656be10..1b3e0bf 100644
--- a/controls-core/src/commonMain/kotlin/space/kscience/controls/converters.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/converters.kt
@@ -51,7 +51,7 @@ public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = Insta
 
 public fun Instant.toMeta(): Meta = Meta(toString())
 
-public val Meta.instant: Instant? get() = value?.string?.let { Instant.parse(it) }
+public val Meta?.instant: Instant? get() = this?.value?.string?.let { Instant.parse(it) }
 
 /**
  * An [IOFormat] for [Instant]
diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt
index 1d064e0..2cedab7 100644
--- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt
@@ -4,7 +4,7 @@ import kotlinx.coroutines.*
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.flow
 import space.kscience.controls.api.Device
-import space.kscience.controls.time.getCoroutineDispatcher
+import space.kscience.controls.time.coroutineDispatcher
 import kotlin.time.Duration
 
 /**
@@ -16,7 +16,7 @@ public fun <D : Device> D.doRecurring(
     task: suspend D.() -> Unit,
 ): Job {
     val taskName = debugTaskName ?: "task[${task.hashCode().toString(16)}]"
-    val dispatcher = getCoroutineDispatcher()
+    val dispatcher = coroutineDispatcher
     return launch(CoroutineName(taskName) + dispatcher) {
         while (isActive) {
             delay(interval)
diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/time/ClockManager.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/time/ClockManager.kt
index 674a3b8..823d8a8 100644
--- a/controls-core/src/commonMain/kotlin/space/kscience/controls/time/ClockManager.kt
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/time/ClockManager.kt
@@ -4,20 +4,24 @@ import kotlinx.coroutines.*
 import kotlinx.datetime.Clock
 import kotlinx.datetime.Instant
 import space.kscience.controls.api.Device
+import space.kscience.controls.instant
 import space.kscience.dataforge.context.*
 import space.kscience.dataforge.meta.Meta
 import space.kscience.dataforge.meta.double
+import space.kscience.dataforge.meta.get
+import space.kscience.dataforge.meta.string
 import kotlin.coroutines.CoroutineContext
 import kotlin.math.roundToLong
 import kotlin.time.Duration
 
 @OptIn(InternalCoroutinesApi::class)
 private class CompressedTimeDispatcher(
-    val clockManager: ClockManager,
-    val dispatcher: CoroutineDispatcher,
+    val coroutineContext: CoroutineContext,
     val compression: Double,
 ) : CoroutineDispatcher(), Delay {
 
+    val dispatcher = coroutineContext[CoroutineDispatcher] ?: Dispatchers.Default
+
     @InternalCoroutinesApi
     override fun dispatchYield(context: CoroutineContext, block: Runnable) {
         dispatcher.dispatchYield(context, block)
@@ -25,14 +29,6 @@ private class CompressedTimeDispatcher(
 
     override fun isDispatchNeeded(context: CoroutineContext): Boolean = dispatcher.isDispatchNeeded(context)
 
-//    @Deprecated(
-//        "Deprecated for good. Override 'limitedParallelism(parallelism: Int, name: String?)' instead",
-//        replaceWith = ReplaceWith("limitedParallelism(parallelism, null)"),
-//        level = DeprecationLevel.HIDDEN
-//    )
-//    @ExperimentalCoroutinesApi
-//    override fun limitedParallelism(parallelism: Int): CoroutineDispatcher = dispatcher.limitedParallelism(parallelism)
-
     override fun limitedParallelism(parallelism: Int, name: String?): CoroutineDispatcher =
         dispatcher.limitedParallelism(parallelism, name)
 
@@ -40,22 +36,21 @@ private class CompressedTimeDispatcher(
         dispatcher.dispatch(context, block)
     }
 
-    private val delay = ((dispatcher as? Delay) ?: (Dispatchers.Default as Delay))
+    private val parentDelay = ((dispatcher as? Delay) ?: (Dispatchers.Default as Delay))
 
     override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
-        delay.scheduleResumeAfterDelay((timeMillis / compression).roundToLong(), continuation)
+        parentDelay.scheduleResumeAfterDelay((timeMillis / compression).roundToLong(), continuation)
     }
 
 
-    override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
-        return delay.invokeOnTimeout((timeMillis / compression).roundToLong(), block, context)
-    }
+    override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
+        parentDelay.invokeOnTimeout((timeMillis / compression).roundToLong(), block, context)
 }
 
 private class CompressedClock(
-    val start: Instant,
-    val compression: Double,
     val baseClock: Clock = Clock.System,
+    val compression: Double,
+    val start: Instant = baseClock.now(),
 ) : Clock {
     override fun now(): Instant {
         val elapsed = (baseClock.now() - start)
@@ -63,38 +58,44 @@ private class CompressedClock(
     }
 }
 
-public class ClockManager : AbstractPlugin(), AsyncTimeProvider {
+public sealed interface ClockMode {
+    public data object System : ClockMode
+    public data class Compressed(val compression: Double) : ClockMode
+    public data class Virtual(val manager: VirtualTimeManager) : ClockMode
+}
+
+public class ClockManager : AbstractPlugin() {
     override val tag: PluginTag get() = Companion.tag
 
-    public val timeCompression: Double by meta.double(1.0)
-
-    override val clock: AsyncClock by lazy {
-        if (timeCompression == 1.0) {
-            AsyncClock.real(Clock.System)
-        } else {
-            AsyncClock.real(CompressedClock(Clock.System.now(), timeCompression))
-        }
+    public val clockMode: ClockMode = when (meta["clock.mode"].string) {
+        null, "system" -> ClockMode.System
+        "virtual" -> ClockMode.Virtual(VirtualTimeManager(meta["clock.start"]?.instant ?: Clock.System.now()))
+        else -> ClockMode.Compressed(meta["clock.compression"].double ?: 1.0)
     }
 
+    public val clock: Clock = when (clockMode) {
+        is ClockMode.Compressed -> CompressedClock(Clock.System, clockMode.compression)
+        ClockMode.System -> Clock.System
+        is ClockMode.Virtual -> clockMode.manager
+    }
+
+
     /**
-     * Provide a [CoroutineDispatcher] with compressed time based on given [dispatcher]
+     * Provide a [CoroutineDispatcher] with compressed time based on context dispatcher
      */
-    public fun asDispatcher(
-        dispatcher: CoroutineDispatcher = Dispatchers.Default,
-    ): CoroutineDispatcher = if (timeCompression == 1.0) {
-        dispatcher
-    } else {
-        CompressedTimeDispatcher(this, dispatcher, timeCompression)
+    public val dispatcher: CoroutineDispatcher = when (clockMode) {
+        ClockMode.System -> context.coroutineContext[CoroutineDispatcher] ?: Dispatchers.Default
+        is ClockMode.Compressed -> CompressedTimeDispatcher(context.coroutineContext, clockMode.compression)
+        is ClockMode.Virtual -> VirtualTimeDispatcher(context.coroutineContext, clockMode.manager)
     }
 
-    public fun scheduleWithFixedDelay(tick: Duration, block: suspend () -> Unit): Job = context.launch(asDispatcher()) {
+    public fun scheduleWithFixedDelay(tick: Duration, block: suspend () -> Unit): Job = context.launch(dispatcher) {
         while (isActive) {
             delay(tick)
             block()
         }
     }
 
-
     public companion object : PluginFactory<ClockManager> {
         override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)
 
@@ -102,10 +103,14 @@ public class ClockManager : AbstractPlugin(), AsyncTimeProvider {
     }
 }
 
-public val Context.clock: AsyncClock get() = plugins[ClockManager]?.clock ?: AsyncClock.real(Clock.System)
+public val Context.clock: Clock get() = plugins[ClockManager]?.clock ?: Clock.System
 
-public fun Device.getCoroutineDispatcher(dispatcher: CoroutineDispatcher = Dispatchers.Default): CoroutineDispatcher =
-    context.plugins[ClockManager]?.asDispatcher(dispatcher) ?: dispatcher
+public val Device.clock: Clock get() = context.clock
+
+public val Device.coroutineDispatcher: CoroutineDispatcher
+    get() = context.plugins[ClockManager]?.dispatcher
+        ?: context.coroutineContext[CoroutineDispatcher]
+        ?: Dispatchers.Default
 
 public fun ContextBuilder.withTimeCompression(compression: Double) {
     require(compression > 0.0) { "Time compression must be greater than zero." }
diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/time/VirtualTimeManager.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/time/VirtualTimeManager.kt
new file mode 100644
index 0000000..7e0fef7
--- /dev/null
+++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/time/VirtualTimeManager.kt
@@ -0,0 +1,100 @@
+package space.kscience.controls.time
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import kotlin.coroutines.CoroutineContext
+import kotlin.time.Duration.Companion.milliseconds
+
+public class VirtualTimeManager(
+    startTime: Instant,
+) : Clock {
+    private val _time = MutableStateFlow(startTime)
+    public val time: StateFlow<Instant> get() = _time
+
+    override fun now(): Instant = _time.value
+
+    private val markerTimes = mutableMapOf<Any, Instant>()
+
+    /**
+     * Set target of [handle] timeline to [to] and wait for it to happen
+     */
+    public suspend fun advanceTime(handle: Any, to: Instant) {
+        val currentMarkerTime = markerTimes[handle] ?: now()
+        require(to > currentMarkerTime) { "The advanced time for marker `$handle` $to is less that current marker time $currentMarkerTime" }
+        markerTimes[handle] = to
+        // advance time if necessary
+        _time.emit(markerTimes.values.min())
+        // wait for time to exceed marker time
+        time.first { it >= to }
+    }
+
+}
+
+@OptIn(InternalCoroutinesApi::class)
+public class VirtualTimeDispatcher internal constructor(
+    private val coroutineContext: CoroutineContext,
+    private val virtualTimeManager: VirtualTimeManager
+) : CoroutineDispatcher(), Delay {
+
+    private val scope = CoroutineScope(coroutineContext)
+
+    public val dispatcher: CoroutineDispatcher =
+        coroutineContext[CoroutineDispatcher] ?: Dispatchers.Default
+
+    override fun dispatch(context: CoroutineContext, block: Runnable): Unit = dispatcher.dispatch(context, block)
+
+    override fun limitedParallelism(
+        parallelism: Int,
+        name: String?
+    ): CoroutineDispatcher = VirtualTimeDispatcher(
+        coroutineContext = coroutineContext + dispatcher.limitedParallelism(parallelism, name),
+        virtualTimeManager = virtualTimeManager
+    )
+
+    override fun isDispatchNeeded(context: CoroutineContext): Boolean = dispatcher.isDispatchNeeded(context)
+
+    @InternalCoroutinesApi
+    override fun dispatchYield(context: CoroutineContext, block: Runnable) {
+        dispatcher.dispatchYield(context, block)
+    }
+
+    override fun toString(): String = dispatcher.toString()
+
+    override fun <E : CoroutineContext.Element> get(key: CoroutineContext.Key<E>): E? = dispatcher[key]
+
+    override fun scheduleResumeAfterDelay(
+        timeMillis: Long,
+        continuation: CancellableContinuation<Unit>
+    ) {
+        val handle = continuation.context[Job] ?: error("Can't use VirtualTimeDispatcher without Job")
+
+        val scheduledJob = scope.launch {
+            virtualTimeManager.advanceTime(handle, virtualTimeManager.time.value + timeMillis.milliseconds)
+            dispatcher.dispatch(
+                continuation.context,
+                Runnable {
+                    @OptIn(ExperimentalCoroutinesApi::class)
+                    with(dispatcher) { with(continuation) { resumeUndispatched(Unit) } }
+                }
+            )
+        }
+
+        continuation.disposeOnCancellation {
+            scheduledJob.cancel()
+        }
+
+    }
+}
+
+public fun CoroutineContext.withVirtualTime(
+    virtualTimeManager: VirtualTimeManager
+): CoroutineContext = if (this[Job] != null) {
+    this
+} else {
+    //add job if it is not present
+    plus(Job(null))
+}.plus(VirtualTimeDispatcher(this, virtualTimeManager))
\ No newline at end of file
diff --git a/controls-jupyter/build.gradle.kts b/controls-jupyter/build.gradle.kts
index ec3972f..a5790c0 100644
--- a/controls-jupyter/build.gradle.kts
+++ b/controls-jupyter/build.gradle.kts
@@ -5,7 +5,6 @@ plugins {
 
 kscience {
     fullStack("js/controls-jupyter.js")
-    useKtor()
     useContextReceivers()
     jupyterLibrary("space.kscience.controls.jupyter.ControlsJupyter")
     dependencies {
@@ -17,6 +16,7 @@ kscience {
         //FIXME remove after VisionForge 0.5
         api("org.jetbrains.kotlin-wrappers:kotlin-extensions:1.0.1-pre.823")
     }
+
     jvmMain {
         implementation(spclibs.logback.classic)
     }
diff --git a/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt b/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt
index 388e1ab..c3138cc 100644
--- a/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt
+++ b/controls-jupyter/src/jsMain/kotlin/commonJupyter.kt
@@ -1,7 +1,7 @@
+import space.kscience.plotly.PlotlyPlugin
 import space.kscience.visionforge.html.runVisionClient
 import space.kscience.visionforge.jupyter.VFNotebookClient
 import space.kscience.visionforge.markup.MarkupPlugin
-import space.kscience.visionforge.plotly.PlotlyPlugin
 
 public fun main(): Unit = runVisionClient {
 //    plugin(DeviceManager)
diff --git a/controls-vision/build.gradle.kts b/controls-vision/build.gradle.kts
index 9d68b22..39a6162 100644
--- a/controls-vision/build.gradle.kts
+++ b/controls-vision/build.gradle.kts
@@ -9,13 +9,12 @@ description = """
 
 kscience {
     fullStack("js/controls-vision.js")
-    useKtor()
     useSerialization()
     useContextReceivers()
     commonMain {
         api(projects.controlsCore)
         api(projects.controlsConstructor)
-        api(libs.visionforge.plotly)
+        api(libs.plotlykt.core)
         api(libs.visionforge.markdown)
 //        api("space.kscience:tables-kt:0.2.1")
 //        api("space.kscience:visionforge-tables:$visionforgeVersion")
diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt
index 0fee031..997ae6f 100644
--- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt
+++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt
@@ -19,12 +19,7 @@ import space.kscience.dataforge.context.Context
 import space.kscience.dataforge.meta.*
 import space.kscience.dataforge.misc.DFExperimental
 import space.kscience.plotly.Plot
-import space.kscience.plotly.bar
-import space.kscience.plotly.models.Bar
-import space.kscience.plotly.models.Scatter
-import space.kscience.plotly.models.Trace
-import space.kscience.plotly.models.TraceValues
-import space.kscience.plotly.scatter
+import space.kscience.plotly.models.*
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.minutes
 import kotlin.time.Duration.Companion.seconds
diff --git a/controls-vision/src/jsMain/kotlin/client.kt b/controls-vision/src/jsMain/kotlin/client.kt
index 78da2e8..918de86 100644
--- a/controls-vision/src/jsMain/kotlin/client.kt
+++ b/controls-vision/src/jsMain/kotlin/client.kt
@@ -1,8 +1,8 @@
 package space.kscience.controls.vision
 
+import space.kscience.plotly.PlotlyPlugin
 import space.kscience.visionforge.html.runVisionClient
 import space.kscience.visionforge.markup.MarkupPlugin
-import space.kscience.visionforge.plotly.PlotlyPlugin
 
 public fun main(): Unit = runVisionClient {
     plugin(PlotlyPlugin)
diff --git a/controls-vision/src/jvmMain/kotlin/dashboard.kt b/controls-vision/src/jvmMain/kotlin/dashboard.kt
index 03186db..43c42f5 100644
--- a/controls-vision/src/jvmMain/kotlin/dashboard.kt
+++ b/controls-vision/src/jvmMain/kotlin/dashboard.kt
@@ -6,22 +6,17 @@ import io.ktor.server.engine.embeddedServer
 import io.ktor.server.http.content.staticResources
 import io.ktor.server.routing.Routing
 import io.ktor.server.routing.routing
-import kotlinx.html.TagConsumer
 import space.kscience.dataforge.context.Context
-import space.kscience.plotly.Plot
-import space.kscience.plotly.PlotlyConfig
+import space.kscience.plotly.PlotlyPlugin
 import space.kscience.visionforge.html.HtmlVisionFragment
 import space.kscience.visionforge.html.VisionPage
-import space.kscience.visionforge.html.VisionTagConsumer
 import space.kscience.visionforge.markup.MarkupPlugin
-import space.kscience.visionforge.plotly.PlotlyPlugin
-import space.kscience.visionforge.plotly.plotly
 import space.kscience.visionforge.server.VisionRoute
 import space.kscience.visionforge.server.openInBrowser
 import space.kscience.visionforge.server.visionPage
 import space.kscience.visionforge.visionManager
 
-public fun Context.showDashboard(
+public suspend fun Context.showDashboard(
     port: Int = 7777,
     routes: Routing.() -> Unit = {},
     configurationBuilder: VisionRoute.() -> Unit = {},
@@ -43,7 +38,7 @@ public fun Context.showDashboard(
         visionPage(
             visualisationContext.visionManager,
             VisionPage.scriptHeader("js/controls-vision.js"),
-            configurationBuilder = configurationBuilder,
+            routeConfiguration = configurationBuilder,
             visionFragment = visionFragment
         )
     }.also {
@@ -60,12 +55,12 @@ public fun Context.showDashboard(
     }
 }
 
-context(VisionTagConsumer<*>)
-public fun TagConsumer<*>.plot(
-    config: PlotlyConfig = PlotlyConfig(),
-    block: Plot.() -> Unit,
-) {
-    vision {
-        plotly(config, block)
-    }
-}
+//context(consumer: VisionTagConsumer<*>)
+//public fun TagConsumer<*>.plot(
+//    config: PlotlyConfig = PlotlyConfig(),
+//    block: Plot.() -> Unit,
+//) {
+//    vision {
+//        plotly(config, block)
+//    }
+//}
diff --git a/controls-visualisation-compose/build.gradle.kts b/controls-visualisation-compose/build.gradle.kts
index 0c68988..66897c5 100644
--- a/controls-visualisation-compose/build.gradle.kts
+++ b/controls-visualisation-compose/build.gradle.kts
@@ -13,7 +13,6 @@ description = """
 
 kscience {
     jvm()
-    useKtor()
     useSerialization()
     useContextReceivers()
     commonMain {
diff --git a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt
index da6507f..e6e3d0a 100644
--- a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt
+++ b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt
@@ -10,6 +10,7 @@ import io.github.koalaplot.core.xygraph.DefaultPoint
 import io.github.koalaplot.core.xygraph.XYGraphScope
 import kotlinx.coroutines.*
 import kotlinx.coroutines.flow.*
+import kotlinx.datetime.Clock
 import kotlinx.datetime.Instant
 import space.kscience.controls.api.Device
 import space.kscience.controls.api.PropertyChangedMessage
@@ -19,7 +20,6 @@ import space.kscience.controls.constructor.units.NumericalValue
 import space.kscience.controls.constructor.values
 import space.kscience.controls.spec.DevicePropertySpec
 import space.kscience.controls.spec.name
-import space.kscience.controls.time.AsyncClock
 import space.kscience.controls.time.ValueWithTime
 import space.kscience.controls.time.clock
 import space.kscience.dataforge.context.Context
@@ -41,7 +41,7 @@ internal fun <T> Flow<ValueWithTime<T>>.collectAndTrim(
     maxAge: Duration = defaultMaxAge,
     maxPoints: Int = defaultMaxPoints,
     minPoints: Int = defaultMinPoints,
-    clock: AsyncClock = Global.clock,
+    clock: Clock = Global.clock,
 ): Flow<List<ValueWithTime<T>>> {
     require(maxPoints > 2)
     require(minPoints > 0)
@@ -222,7 +222,7 @@ public fun XYGraphScope<Instant, Double>.PlotAveragedDeviceProperty(
     var points by remember { mutableStateOf<List<ValueWithTime<Double>>>(emptyList()) }
 
     LaunchedEffect(device, propertyName, startValue, maxAge, maxPoints, minPoints, averagingInterval) {
-        val clock: AsyncClock = device.clock
+        val clock: Clock = device.clock
         var lastValue = startValue
         device.propertyMessageFlow(propertyName)
             .chunkedByPeriod(averagingInterval)
diff --git a/demo/all-things/build.gradle.kts b/demo/all-things/build.gradle.kts
index 077e55d..53ee2e6 100644
--- a/demo/all-things/build.gradle.kts
+++ b/demo/all-things/build.gradle.kts
@@ -36,7 +36,7 @@ dependencies {
 kotlin{
     jvmToolchain(17)
     compilerOptions {
-        freeCompilerArgs.addAll("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
+        freeCompilerArgs.addAll("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn", "-Xcontext-parameters")
     }
 }
 
diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt
index fc72520..9c603d1 100644
--- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt
+++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt
@@ -9,7 +9,8 @@ import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Window
 import androidx.compose.ui.window.application
 import androidx.compose.ui.window.rememberWindowState
-import io.ktor.server.engine.ApplicationEngine
+import io.ktor.server.application.port
+import io.ktor.server.engine.EmbeddedServer
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -46,8 +47,8 @@ private val json = Json { prettyPrint = true }
 class DemoController : ContextAware {
 
     var device: DemoDevice? = null
-    var magixServer: ApplicationEngine? = null
-    var visualizer: ApplicationEngine? = null
+    var magixServer: EmbeddedServer<*,*>? = null
+    var visualizer: EmbeddedServer<*,*>? = null
     val opcUaServer: OpcUaServer = OpcUaServer {
         setApplicationName(LocalizedText.english("space.kscience.controls.opcua"))
 
@@ -174,7 +175,7 @@ fun DemoControls(controller: DemoController) {
                     onClick = {
                         controller.visualizer?.run {
                             val host = "localhost"//environment.connectors.first().host
-                            val port = environment.connectors.first().port
+                            val port = environment.config.port
                             val uri = URI("http", null, host, port, "/", null, null)
                             Desktop.getDesktop().browse(uri)
                         }
diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt
index 5ff1287..971ac6f 100644
--- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt
+++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt
@@ -1,6 +1,6 @@
 package space.kscience.controls.demo
 
-import io.ktor.server.engine.ApplicationEngine
+import io.ktor.server.engine.EmbeddedServer
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.*
 import kotlinx.coroutines.launch
@@ -18,9 +18,8 @@ import space.kscience.plotly.Plotly
 import space.kscience.plotly.layout
 import space.kscience.plotly.models.Trace
 import space.kscience.plotly.plot
-import space.kscience.plotly.server.PlotlyUpdateMode
-import space.kscience.plotly.server.serve
 import space.kscience.plotly.trace
+import space.kscience.visionforge.plotly.serveSinglePage
 import java.util.concurrent.ConcurrentLinkedQueue
 
 /**
@@ -53,7 +52,7 @@ suspend fun Trace.updateXYFrom(flow: Flow<Iterable<Pair<Double, Double>>>) {
 }
 
 
-fun CoroutineScope.startDemoDeviceServer(magixEndpoint: MagixEndpoint): ApplicationEngine {
+fun CoroutineScope.startDemoDeviceServer(magixEndpoint: MagixEndpoint): EmbeddedServer<*, *> {
     //share subscription to a parse message only once
     val subscription = magixEndpoint.subscribe(DeviceManager.magixFormat).shareIn(this, SharingStarted.Lazily)
 
@@ -69,70 +68,68 @@ fun CoroutineScope.startDemoDeviceServer(magixEndpoint: MagixEndpoint): Applicat
         (payload as? PropertyChangedMessage)?.takeIf { it.property == DemoDevice.coordinates.name }
     }.map { it.value }
 
-    return Plotly.serve(port = 9091, scope = this) {
-        updateMode = PlotlyUpdateMode.PUSH
+    return Plotly.serveSinglePage(port = 9091, routeConfiguration = {
         updateInterval = 100
-        page { container ->
-            link {
-                rel = "stylesheet"
-                href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
-                attributes["integrity"] = "sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
-                attributes["crossorigin"] = "anonymous"
-            }
-            div("row") {
-                div("col-6") {
-                    plot(renderer = container) {
-                        layout {
-                            title = "sin property"
-                            xaxis.title = "point index"
-                            yaxis.title = "sin"
-                        }
-                        trace {
-                            launch {
-                                val flow: Flow<Iterable<Double>> = sinFlow.mapNotNull { it.double }.windowed(100)
-                                updateFrom(Trace.Y_AXIS, flow)
-                            }
-                        }
-                    }
-                }
-                div("col-6") {
-                    plot(renderer = container) {
-                        layout {
-                            title = "cos property"
-                            xaxis.title = "point index"
-                            yaxis.title = "cos"
-                        }
-                        trace {
-                            launch {
-                                val flow: Flow<Iterable<Double>> = cosFlow.mapNotNull { it.double }.windowed(100)
-                                updateFrom(Trace.Y_AXIS, flow)
-                            }
-                        }
-                    }
-                }
-            }
-            div("row") {
-                div("col-12") {
-                    plot(renderer = container) {
-                        layout {
-                            title = "cos vs sin"
-                            xaxis.title = "sin"
-                            yaxis.title = "cos"
-                        }
-                        trace {
-                            name = "non-synchronized"
-                            launch {
-                                val flow: Flow<Iterable<Pair<Double, Double>>> = sinCosFlow.mapNotNull {
-                                    it["x"].double!! to it["y"].double!!
-                                }.windowed(30)
-                                updateXYFrom(flow)
-                            }
-                        }
-                    }
-                }
-            }
-
+    }) {
+        link {
+            rel = "stylesheet"
+            href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
+            attributes["integrity"] = "sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
+            attributes["crossorigin"] = "anonymous"
         }
+        div("row") {
+            div("col-6") {
+                plot{
+                    layout {
+                        title = "sin property"
+                        xaxis.title = "point index"
+                        yaxis.title = "sin"
+                    }
+                    trace {
+                        launch {
+                            val flow: Flow<Iterable<Double>> = sinFlow.mapNotNull { it.double }.windowed(100)
+                            updateFrom(Trace.Y_AXIS, flow)
+                        }
+                    }
+                }
+            }
+            div("col-6") {
+                plot{
+                    layout {
+                        title = "cos property"
+                        xaxis.title = "point index"
+                        yaxis.title = "cos"
+                    }
+                    trace {
+                        launch {
+                            val flow: Flow<Iterable<Double>> = cosFlow.mapNotNull { it.double }.windowed(100)
+                            updateFrom(Trace.Y_AXIS, flow)
+                        }
+                    }
+                }
+            }
+        }
+        div("row") {
+            div("col-12") {
+                plot{
+                    layout {
+                        title = "cos vs sin"
+                        xaxis.title = "sin"
+                        yaxis.title = "cos"
+                    }
+                    trace {
+                        name = "non-synchronized"
+                        launch {
+                            val flow: Flow<Iterable<Pair<Double, Double>>> = sinCosFlow.mapNotNull {
+                                it["x"].double!! to it["y"].double!!
+                            }.windowed(30)
+                            updateXYFrom(flow)
+                        }
+                    }
+                }
+            }
+        }
+
     }
 
 }
diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts
index 6d6d7f0..d1bda0e 100644
--- a/demo/constructor/build.gradle.kts
+++ b/demo/constructor/build.gradle.kts
@@ -9,7 +9,6 @@ plugins {
 
 kscience {
     jvm()
-    useKtor()
     useSerialization()
     useContextReceivers()
     commonMain {
diff --git a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt
index b7ae7fd..ab0d833 100644
--- a/demo/constructor/src/jvmMain/kotlin/PidDemo.kt
+++ b/demo/constructor/src/jvmMain/kotlin/PidDemo.kt
@@ -271,12 +271,12 @@ fun main() = application {
                 second(400.dp) {
                     ChartLayout {
                         XYGraph<Instant, Double>(
-                            xAxisModel = remember { TimeAxisModel.recent(maxAge, clock) },
+                            xAxisModel = remember { TimeAxisModel.recent(maxAge, context.clock) },
                             yAxisModel = rememberDoubleLinearAxisModel((range.start - 1.0)..(range.endInclusive + 1.0)),
                             xAxisTitle = { Text("Time in seconds relative to current") },
                             xAxisLabels = { it: Instant ->
                                 Text(
-                                    (clock.now() - it).toDouble(
+                                    (context.clock.now() - it).toDouble(
                                         DurationUnit.SECONDS
                                     ).toString(2)
                                 )
diff --git a/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt b/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt
index 53c4c7f..4855ba0 100644
--- a/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt
+++ b/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt
@@ -29,10 +29,10 @@ import space.kscience.plotly.Plotly
 import space.kscience.plotly.PlotlyConfig
 import space.kscience.plotly.layout
 import space.kscience.plotly.models.Bar
+import space.kscience.plotly.models.invoke
 import space.kscience.plotly.plot
-import space.kscience.plotly.server.PlotlyUpdateMode
-import space.kscience.plotly.server.serve
-import space.kscience.plotly.server.show
+import space.kscience.visionforge.plotly.serveSinglePage
+import space.kscience.visionforge.server.openInBrowser
 import space.kscince.magix.zmq.ZmqMagixFlowPlugin
 import space.kscince.magix.zmq.zmq
 import kotlin.random.Random
@@ -117,24 +117,22 @@ suspend fun main() {
         }
     }
 
-    val application = Plotly.serve(port = 9091) {
-        updateMode = PlotlyUpdateMode.PUSH
+    val application = Plotly.serveSinglePage(port = 9091, routeConfiguration = {
         updateInterval = 1000
-
-        page { container ->
-            plot(renderer = container, config = PlotlyConfig { saveAsSvg() }) {
-                layout {
+    }) {
+        plot(config = PlotlyConfig { saveAsSvg() }) {
+            layout {
 //                    title = "Latest event"
 
-                    xaxis.title = "Device number"
-                    yaxis.title = "Maximum latency in ms"
-                }
-                traces(trace)
+                xaxis.title = "Device number"
+                yaxis.title = "Maximum latency in ms"
             }
+            traces(trace)
         }
     }
 
-    application.show()
+
+    application.openInBrowser()
 
     while (readlnOrNull().isNullOrBlank()) {
 
diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt
index 4c2b350..0c68570 100644
--- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt
+++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt
@@ -4,9 +4,9 @@ import io.ktor.network.selector.ActorSelectorManager
 import io.ktor.network.sockets.aSocket
 import io.ktor.network.sockets.openReadChannel
 import io.ktor.network.sockets.openWriteChannel
-import io.ktor.util.InternalAPI
 import io.ktor.util.moveToByteArray
-import io.ktor.utils.io.writeAvailable
+import io.ktor.utils.io.read
+import io.ktor.utils.io.writeByteArray
 import kotlinx.coroutines.*
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -17,7 +17,6 @@ val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
     throwable.printStackTrace()
 }
 
-@OptIn(InternalAPI::class)
 fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) {
     val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes)
     aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port).use { server ->
@@ -32,7 +31,7 @@ fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exc
 
                 val sendJob = virtualDevice.subscribe().onEach {
                     //println("Sending: ${it.decodeToString()}")
-                    output.writeAvailable(it)
+                    output.writeByteArray(it)
                     output.flush()
                 }.launchIn(this)
 
diff --git a/gradle.properties b/gradle.properties
index 423d87f..4480e5a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -9,4 +9,4 @@ org.gradle.jvmargs=-Xmx4096m
 
 org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
 
-toolsVersion=0.16.1-kotlin-2.1.0
\ No newline at end of file
+toolsVersion=0.17.1-kotlin-2.1.20
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 06df103..22c0534 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
 [versions]
 
-dataforge = "0.10.0"
+dataforge = "0.10.1"
 rsocket = "0.16.0"
 xodus = "2.0.1"
 
@@ -10,8 +10,6 @@ fazecast = "2.10.3"
 
 tornadofx = "1.7.20"
 
-plotlykt = "0.7.2"
-
 logback = "1.2.11"
 
 hivemq = "1.3.1"
@@ -29,7 +27,7 @@ pi4j-ktx = "2.4.0"
 
 plc4j = "0.12.0"
 
-visionforge = "0.4.2"
+visionforge = "0.5.0"
 
 [libraries]
 
@@ -50,7 +48,9 @@ jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "fazecast" }
 
 tornadofx = { module = "no.tornado:tornadofx", version.ref = "tornadofx" }
 
-plotlykt-server = { module = "space.kscience:plotlykt-server", version.ref = "plotlykt" }
+plotlykt-core = { module = "space.kscience:plotly-kt-core", version.ref = "visionforge" }
+
+plotlykt-server = { module = "space.kscience:plotly-kt-server", version.ref = "visionforge" }
 
 logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
 
@@ -74,7 +74,6 @@ pi4j-plugin-pigpio = { module = "com.pi4j:pi4j-plugin-pigpio", version.ref = "pi
 plc4j-spi = { module = "org.apache.plc4x:plc4j-spi", version.ref = "plc4j" }
 
 visionforge-jupiter = { module = "space.kscience:visionforge-jupyter", version.ref = "visionforge" }
-visionforge-plotly = { module = "space.kscience:visionforge-plotly", version.ref = "visionforge" }
 visionforge-markdown = { module = "space.kscience:visionforge-markdown", version.ref = "visionforge" }
 visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" }
 visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" }
diff --git a/simulation-kt/src/commonMain/kotlin/TreeTimeline.kt b/simulation-kt/src/commonMain/kotlin/TreeTimeline.kt
index 97789aa..e5243df 100644
--- a/simulation-kt/src/commonMain/kotlin/TreeTimeline.kt
+++ b/simulation-kt/src/commonMain/kotlin/TreeTimeline.kt
@@ -9,6 +9,9 @@ import kotlinx.coroutines.sync.withLock
 import kotlinx.datetime.Instant
 import kotlin.coroutines.CoroutineContext
 
+/**
+ * A timeline that could be forked. The events from the fork appear in parent timeline events, but not vise versa.
+ */
 public interface ForkingTimeline<E : Any> : CollectingTimeline<E> {
     public suspend fun fork(): ForkingTimeline<E>
 }
@@ -84,11 +87,12 @@ public class TreeTimeline<E : Any>(
 
             override suspend fun collect(upTo: Instant) = mutex.withLock {
                 require(upTo >= time.value) { "Requested time $upTo is lower than observed ${time.value}" }
-                events().takeWhile {
-                    timeOf(it) <= upTo
-                }.collect {
-                    channel.send(it)
-                }
+                TODO("Not yet implemented")
+//                events().takeWhile {
+//                    timeOf(it) <= upTo
+//                }.collect {
+//                    channel.send(it)
+//                }
             }
 
             override fun close() {
diff --git a/simulation-kt/src/commonTest/kotlin/TimelineTests.kt b/simulation-kt/src/commonTest/kotlin/TimelineTests.kt
index 4710802..7ec7079 100644
--- a/simulation-kt/src/commonTest/kotlin/TimelineTests.kt
+++ b/simulation-kt/src/commonTest/kotlin/TimelineTests.kt
@@ -1,6 +1,7 @@
 package space.kscience.simulation
 
 import kotlinx.coroutines.isActive
+import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.runTest
 import kotlinx.datetime.Instant
 import kotlin.test.Test
@@ -14,6 +15,8 @@ class TimelineTests {
     fun testGeneration() = runTest(timeout = 1.seconds) {
         val startTime = Instant.parse("2020-01-01T00:00:00.000Z")
 
+        StandardTestDispatcher()
+
         val generation = GeneratingTimeline(
             origin = TimelineEvent(startTime, Unit),
             lookaheadInterval = 1.seconds,