Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package io.github.charlietap.chasm.benchmark.coremark
import com.goncalossilva.resources.Resource
import io.github.charlietap.chasm.benchmark.BenchmarkMode
import io.github.charlietap.chasm.benchmark.BenchmarkStabilizer
import io.github.charlietap.chasm.config.StoreConfig
import io.github.charlietap.chasm.embedding.addFuel
import io.github.charlietap.chasm.embedding.dsl.imports
import io.github.charlietap.chasm.embedding.instance
import io.github.charlietap.chasm.embedding.invoke
Expand All @@ -15,7 +17,15 @@ import io.github.charlietap.chasm.runtime.value.ExecutionValue
import io.github.charlietap.chasm.runtime.value.NumberValue
import kotlin.time.Clock

fun main() {
/** Takes an optional store configuration: `plain` (the default), `fuel`, `interrupt`, or `fuel+interrupt`. */
fun main(args: Array<String>) {
val config = when (val name = args.firstOrNull() ?: "plain") {
"plain" -> StoreConfig()
"fuel" -> StoreConfig(meterFuel = true)
"interrupt" -> StoreConfig(interruptible = true)
"fuel+interrupt" -> StoreConfig(meterFuel = true, interruptible = true)
else -> error("Unknown store configuration: $name")
}
val topology = BenchmarkStabilizer.topology()
val application = BenchmarkStabilizer.enter(BenchmarkMode.PREFER_FASTEST, topology)
val effectiveTopology = if (application.isSupported) topology else topology.copy(fastestCpuIds = emptySet())
Expand All @@ -27,7 +37,7 @@ fun main() {
"CoreMark started on logical CPU ${start.cpuId}, outside the fastest class"
}

val benchmark = CoremarkBenchmark()
val benchmark = CoremarkBenchmark(config)
benchmark.run()

val placement = BenchmarkStabilizer.finishTrial(start, effectiveTopology)
Expand All @@ -36,11 +46,12 @@ fun main() {
}
}

class CoremarkBenchmark {
class CoremarkBenchmark(private val config: StoreConfig = StoreConfig()) {

fun run() {
val bytes = Resource(FILE_DIR + "coremark.wasm").readBytes()
val store = store()
val store = store(config)
if (config.meterFuel) addFuel(store, Long.MAX_VALUE)

val imports = imports(store) {
function {
Expand Down
1 change: 1 addition & 0 deletions chasm/api/chasm.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ final fun io.github.charlietap.chasm.embedding/function(io.github.charlietap.cha
final fun io.github.charlietap.chasm.embedding/gc(io.github.charlietap.chasm.embedding.shapes/Store): io.github.charlietap.chasm.embedding.shapes/ChasmResult<kotlin/Unit, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // io.github.charlietap.chasm.embedding/gc|gc(io.github.charlietap.chasm.embedding.shapes.Store){}[0]
final fun io.github.charlietap.chasm.embedding/global(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.type/GlobalType, io.github.charlietap.chasm.runtime.value/ExecutionValue): io.github.charlietap.chasm.embedding.shapes/Global // io.github.charlietap.chasm.embedding/global|global(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.type.GlobalType;io.github.charlietap.chasm.runtime.value.ExecutionValue){}[0]
final fun io.github.charlietap.chasm.embedding/instance(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.embedding.shapes/Module, kotlin.collections/List<io.github.charlietap.chasm.embedding.shapes/Import>, io.github.charlietap.chasm.config/RuntimeConfig = ...): io.github.charlietap.chasm.embedding.shapes/ChasmResult<io.github.charlietap.chasm.embedding.shapes/Instance, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // io.github.charlietap.chasm.embedding/instance|instance(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.embedding.shapes.Module;kotlin.collections.List<io.github.charlietap.chasm.embedding.shapes.Import>;io.github.charlietap.chasm.config.RuntimeConfig){}[0]
final fun io.github.charlietap.chasm.embedding/interrupt(io.github.charlietap.chasm.embedding.shapes/Store): io.github.charlietap.chasm.embedding.shapes/ChasmResult<kotlin/Boolean, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // io.github.charlietap.chasm.embedding/interrupt|interrupt(io.github.charlietap.chasm.embedding.shapes.Store){}[0]
final fun io.github.charlietap.chasm.embedding/invoke(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.embedding.shapes/Instance, io.github.charlietap.chasm.embedding.shapes/Function, kotlin.collections/List<io.github.charlietap.chasm.runtime.value/ExecutionValue> = ...): io.github.charlietap.chasm.embedding.shapes/ChasmResult<kotlin.collections/List<io.github.charlietap.chasm.runtime.value/ExecutionValue>, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // io.github.charlietap.chasm.embedding/invoke|invoke(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.embedding.shapes.Instance;io.github.charlietap.chasm.embedding.shapes.Function;kotlin.collections.List<io.github.charlietap.chasm.runtime.value.ExecutionValue>){}[0]
final fun io.github.charlietap.chasm.embedding/invoke(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.embedding.shapes/Instance, kotlin/String, kotlin.collections/List<io.github.charlietap.chasm.runtime.value/ExecutionValue> = ...): io.github.charlietap.chasm.embedding.shapes/ChasmResult<kotlin.collections/List<io.github.charlietap.chasm.runtime.value/ExecutionValue>, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // io.github.charlietap.chasm.embedding/invoke|invoke(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.embedding.shapes.Instance;kotlin.String;kotlin.collections.List<io.github.charlietap.chasm.runtime.value.ExecutionValue>){}[0]
final fun io.github.charlietap.chasm.embedding/memory(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.type/MemoryType): io.github.charlietap.chasm.embedding.shapes/Memory // io.github.charlietap.chasm.embedding/memory|memory(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.type.MemoryType){}[0]
Expand Down
4 changes: 4 additions & 0 deletions chasm/api/jvm/chasm.api
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public final class io/github/charlietap/chasm/embedding/InstanceKt {
public static synthetic fun instance$default (Lio/github/charlietap/chasm/embedding/shapes/Store;Lio/github/charlietap/chasm/embedding/shapes/Module;Ljava/util/List;Lio/github/charlietap/chasm/config/RuntimeConfig;ILjava/lang/Object;)Lio/github/charlietap/chasm/embedding/shapes/ChasmResult;
}

public final class io/github/charlietap/chasm/embedding/InterruptKt {
public static final fun interrupt (Lio/github/charlietap/chasm/embedding/shapes/Store;)Lio/github/charlietap/chasm/embedding/shapes/ChasmResult;
}

public final class io/github/charlietap/chasm/embedding/InvokeKt {
public static final fun invoke (Lio/github/charlietap/chasm/embedding/shapes/Store;Lio/github/charlietap/chasm/embedding/shapes/Instance;Lio/github/charlietap/chasm/embedding/shapes/Function;Ljava/util/List;)Lio/github/charlietap/chasm/embedding/shapes/ChasmResult;
public static final fun invoke (Lio/github/charlietap/chasm/embedding/shapes/Store;Lio/github/charlietap/chasm/embedding/shapes/Instance;Ljava/lang/String;Ljava/util/List;)Lio/github/charlietap/chasm/embedding/shapes/ChasmResult;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.github.charlietap.chasm.embedding

import io.github.charlietap.chasm.embedding.error.ChasmError
import io.github.charlietap.chasm.embedding.shapes.ChasmResult
import io.github.charlietap.chasm.embedding.shapes.Store

/**
* Stops the call running on a store created with `StoreConfig(interruptible = true)`. Safe to call
* from any thread: Wasm code in the call traps with `Interrupted` at its next function entry or loop
* iteration. A host function the call is running is not stopped itself; Wasm code it calls back into
* traps, as does its Wasm caller once it returns.
*
* Returns `true` if a call was running to receive the interrupt, and `false` if none was, in which
* case the interrupt is dropped and the next call runs normally.
*/
fun interrupt(store: Store): ChasmResult<Boolean, ChasmError.ExecutionError> {
val interrupt = store.store.interrupt
if (!interrupt.enabled) {
return ChasmResult.Error(ChasmError.ExecutionError("Interrupts are not enabled for this store"))
}
if (interrupt.depth == 0) return ChasmResult.Success(false)

interrupt.requested = true
return ChasmResult.Success(true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ package io.github.charlietap.chasm.embedding
import io.github.charlietap.chasm.config.StoreConfig
import io.github.charlietap.chasm.embedding.shapes.Store
import io.github.charlietap.chasm.runtime.store.Fuel
import io.github.charlietap.chasm.runtime.store.Interrupt
import io.github.charlietap.chasm.runtime.store.Store as InternalStore

fun store(): Store = Store(InternalStore())

fun store(config: StoreConfig): Store = Store(InternalStore(fuel = Fuel(metered = config.meterFuel)))
fun store(config: StoreConfig): Store = Store(
InternalStore(
fuel = Fuel(metered = config.meterFuel),
interrupt = Interrupt(enabled = config.interruptible),
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package io.github.charlietap.chasm.integration

import com.goncalossilva.resources.Resource
import io.github.charlietap.chasm.config.StoreConfig
import io.github.charlietap.chasm.embedding.addFuel
import io.github.charlietap.chasm.embedding.error.ChasmError
import io.github.charlietap.chasm.embedding.fixture.publicImport
import io.github.charlietap.chasm.embedding.function
import io.github.charlietap.chasm.embedding.instance
import io.github.charlietap.chasm.embedding.interrupt
import io.github.charlietap.chasm.embedding.invoke
import io.github.charlietap.chasm.embedding.module
import io.github.charlietap.chasm.embedding.shapes.ChasmResult
import io.github.charlietap.chasm.embedding.shapes.Instance
import io.github.charlietap.chasm.embedding.shapes.Store
import io.github.charlietap.chasm.embedding.shapes.expect
import io.github.charlietap.chasm.embedding.store
import io.github.charlietap.chasm.fixture.type.functionType
import io.github.charlietap.chasm.fixture.type.i32ValueType
import io.github.charlietap.chasm.fixture.type.resultType
import io.github.charlietap.chasm.host.HostFunction
import io.github.charlietap.chasm.host.writeI32
import io.github.charlietap.chasm.runtime.error.InvocationError
import io.github.charlietap.chasm.runtime.value.NumberValue
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs

class InterruptTest {

@Test
fun `interrupting a store that is not interruptible returns an error`() {
val (store, _) = instantiate(StoreConfig())

assertIs<ChasmResult.Error<*>>(interrupt(store))
}

@Test
fun `an interrupt stops the running call at its next check`() {
val (store, instance) = instantiate(StoreConfig(interruptible = true))

assertEquals(interrupted(), invoke(store, instance, "interrupt_then_spin"))
}

@Test
fun `a call after an interrupted one runs`() {
val (store, instance) = instantiate(StoreConfig(interruptible = true))
assertEquals(interrupted(), invoke(store, instance, "interrupt_then_spin"))

assertEquals(ChasmResult.Success(listOf(NumberValue.I32(0))), count(store, instance, 3))
}

@Test
fun `an interrupt made while no call runs reports it and has no effect`() {
val (store, instance) = instantiate(StoreConfig(interruptible = true))

assertEquals(ChasmResult.Success(false), interrupt(store))
assertEquals(ChasmResult.Success(listOf(NumberValue.I32(0))), count(store, instance, 3))
}

@Test
fun `an interruptible store compiles a check per function and loop and a plain store compiles none`() {
val (interruptible, _) = instantiate(StoreConfig(interruptible = true))
val (plain, _) = instantiate(StoreConfig())

// Five functions and five loops.
assertEquals(10, interruptible.store.program.size - plain.store.program.size)
}

@Test
fun `a metered interruptible store compiles one combined check per function and loop`() {
val (both, _) = instantiate(StoreConfig(meterFuel = true, interruptible = true))
val (plain, _) = instantiate(StoreConfig())

assertEquals(10, both.store.program.size - plain.store.program.size)
}

@Test
fun `a metered interruptible store traps on an interrupt or on running out of fuel`() {
val (store, instance) = instantiate(StoreConfig(meterFuel = true, interruptible = true))
addFuel(store, Long.MAX_VALUE)
assertEquals(interrupted(), invoke(store, instance, "interrupt_then_spin"))

val (metered, meteredInstance) = instantiate(StoreConfig(meterFuel = true, interruptible = true))
addFuel(metered, 100)
assertEquals(
ChasmResult.Error(ChasmError.ExecutionError(InvocationError.FuelExhausted.toString())),
invoke(metered, meteredInstance, "spin"),
)
}

@Test
fun `a metered interruptible store reports an interrupt over running out of fuel`() {
val (store, instance) = instantiate(StoreConfig(meterFuel = true, interruptible = true))
// Enough for the interrupting function's entry check, none for its loop's.
addFuel(store, 1)

assertEquals(interrupted(), invoke(store, instance, "interrupt_then_spin"))
}

@Test
fun `an interrupt made during a call reports that it landed`() {
var interruptResult: ChasmResult<*, *>? = null
val (store, instance) = instantiate(StoreConfig(interruptible = true)) { store, _ ->
interruptResult = interrupt(store)
}

assertEquals(interrupted(), invoke(store, instance, "callback_then_spin"))
assertEquals(ChasmResult.Success(true), interruptResult)
}

@Test
fun `an interrupt raised in a call back into the store also stops its caller`() {
var callbackResult: ChasmResult<*, *>? = null
val (store, instance) = instantiate(StoreConfig(interruptible = true)) { store, instance ->
callbackResult = invoke(store, instance, "interrupt_then_spin")
}

assertEquals(interrupted(), invoke(store, instance, "callback_then_spin"))
assertEquals(interrupted(), callbackResult)
}

@Test
fun `an interrupt raised in a host function invoked directly stops its calls back into the store`() {
var callbackResult: ChasmResult<*, *>? = null
val (store, instance) = instantiate(StoreConfig(interruptible = true)) { store, instance ->
interrupt(store)
callbackResult = count(store, instance, 3)
}

assertEquals(ChasmResult.Success(emptyList()), invoke(store, instance, "callback"))
assertEquals(interrupted(), callbackResult)
}

private fun count(store: Store, instance: Instance, times: Int) =
invoke(store, instance, "count", listOf(NumberValue.I32(times)))

companion object {
private const val FIXTURE = "integration/interrupt.wasm"

internal fun interrupted() =
ChasmResult.Error(ChasmError.ExecutionError(InvocationError.Interrupted.toString()))

/**
* Instantiates the fixture. Its `env.interrupt` import interrupts the store it runs on, its
* `env.callback` import, which it also exports, runs [callback] with the store and the instance
* being created, and its `env.keep_running` import returns [keepRunning].
*/
internal fun instantiate(
config: StoreConfig,
keepRunning: () -> Boolean = { false },
callback: (Store, Instance) -> Unit = { _, _ -> },
): Pair<Store, Instance> {
val store = store(config)
lateinit var instance: Instance
val hostInterrupt = function(store, functionType(), HostFunction { _, _ -> interrupt(store) })
val hostCallback = function(store, functionType(), HostFunction { _, _ -> callback(store, instance) })
val hostKeepRunning = function(
store,
functionType(results = resultType(listOf(i32ValueType()))),
HostFunction { _, results -> results.writeI32(0, if (keepRunning()) 1 else 0) },
)
val module = module(Resource(FIXTURE).readBytes()).expect("module decodes")
val imports = listOf(
publicImport("env", "interrupt", hostInterrupt),
publicImport("env", "callback", hostCallback),
publicImport("env", "keep_running", hostKeepRunning),
)
instance = instance(store, module, imports).expect("module instantiates")
return store to instance
}
}
}
Binary file not shown.
29 changes: 29 additions & 0 deletions chasm/src/commonTest/resources/integration/interrupt.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
(module
(import "env" "interrupt" (func $interrupt))
(import "env" "callback" (func $callback))
(import "env" "keep_running" (func $keep_running (result i32)))
(export "callback" (func $callback))

(func $spin (export "spin")
(loop $continue
(br $continue)))

(func $interrupt_then_spin (export "interrupt_then_spin")
(call $interrupt)
(loop $continue
(br $continue)))

(func $callback_then_spin (export "callback_then_spin")
(call $callback)
(loop $continue
(br $continue)))

(func $spin_while_host_allows (export "spin_while_host_allows")
(loop $continue
(br_if $continue (call $keep_running))))

(func $count (export "count") (param $n i32) (result i32)
(loop $continue
(local.set $n (i32.sub (local.get $n) (i32.const 1)))
(br_if $continue (local.get $n)))
(local.get $n)))
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.github.charlietap.chasm.integration

import io.github.charlietap.chasm.config.StoreConfig
import io.github.charlietap.chasm.embedding.interrupt
import io.github.charlietap.chasm.embedding.invoke
import io.github.charlietap.chasm.embedding.shapes.ChasmResult
import io.github.charlietap.chasm.integration.InterruptTest.Companion.interrupted
import io.github.charlietap.chasm.runtime.value.NumberValue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.FutureTask
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class InterruptThreadTest {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use the host callback to signal that execution has started, then send a single interrupt and wait for completion with a timeout? That avoids the startup race and verifies that one interrupt is sufficient.

The current test can hang indefinitely if interruption breaks, a timed wait and daemon worker let it fail without keeping the test JVM alive. We can retain the assertion that the next invocation succeeds.

Roughly:

   val started = CountDownLatch(1)
        val (store, instance) = InterruptTest.instantiate(
            StoreConfig(interruptible = true),
        ) { _, _ ->
            started.countDown()
        }

        val invocation = FutureTask {
            invoke(store, instance, "callback_then_spin")
        }

        thread(isDaemon = true, name = "interrupt-test") {
            invocation.run()
        }

        assertTrue(
            started.await(5, TimeUnit.SECONDS),
            "Wasm execution did not reach the host callback",
        )

        assertEquals(ChasmResult.Success(Unit), interrupt(store))

        assertEquals(
            ChasmResult.Error(
                ChasmError.ExecutionError(InvocationError.Interrupted.toString()),
            ),
            invocation.get(5, TimeUnit.SECONDS),
        )

        assertEquals(
            ChasmResult.Success(listOf(NumberValue.I32(0))),
            invoke(store, instance, "count", listOf(NumberValue.I32(3))),
        )

note I'm using kotlin.concurrent.thread ^^^

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thanks for the sketch. I went with it almost as written. The one addition is that the loop now polls a host import, so if the interrupt ever fails the test can release the worker instead of leaving it spinning.


@Test
fun `another thread stops a running loop, and the next call runs`() {
val started = CountDownLatch(1)
val abandoned = AtomicBoolean(false)
val (store, instance) = InterruptTest.instantiate(
StoreConfig(interruptible = true),
keepRunning = {
started.countDown()
!abandoned.get()
},
)

val invocation = FutureTask {
invoke(store, instance, "spin_while_host_allows")
}
// A daemon worker and timed waits let a broken interrupt fail the test rather than hang the JVM.
thread(isDaemon = true, name = "interrupt-test") {
invocation.run()
}

try {
assertTrue(started.await(5, TimeUnit.SECONDS), "Wasm execution did not reach the host callback")

assertEquals(ChasmResult.Success(true), interrupt(store))

assertEquals(interrupted(), invocation.get(5, TimeUnit.SECONDS))
} finally {
// Ends the loop if the interrupt did not, so a failure leaves no thread spinning.
abandoned.set(true)
}

assertEquals(
ChasmResult.Success(listOf(NumberValue.I32(0))),
invoke(store, instance, "count", listOf(NumberValue.I32(3))),
)
}
}
Loading
Loading