diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 51940bcd..292fc528 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -75,7 +75,7 @@ jobs: run: | cargo run benchmark \ --engine=artifacts/wasmtime-main/libengine.so \ - --engine-flags "-Wgc -Wfunction-references" \ + --engine-flags "-Wgc -Wfunction-references -Wexceptions" \ --processes=4 \ --iterations-per-process=1 \ -- "${PARTITION_SUITE}" diff --git a/benchmarks/all.suite b/benchmarks/all.suite index faaf1e61..7af79b4b 100644 --- a/benchmarks/all.suite +++ b/benchmarks/all.suite @@ -13,6 +13,7 @@ cm-online-stats/cm-online-stats.wasm hex-simd/benchmark.wasm # image-classification/image-classification-benchmark.wasm intgemm-simd/benchmark.wasm +kotlin-richards/kotlin-richards.wasm libsodium/libsodium-aead_aegis128l.wasm libsodium/libsodium-aead_aegis256.wasm libsodium/libsodium-aead_aes256gcm.wasm diff --git a/benchmarks/build.sh b/benchmarks/build.sh index acf7e437..9e670848 100755 --- a/benchmarks/build.sh +++ b/benchmarks/build.sh @@ -31,7 +31,15 @@ print_header() { # directory and `--dereference` (i.e., follow) all symlinks provided. print_header "Create build context" TMP_TAR=$(mktemp /tmp/sightglass-benchmark-dir-XXXXXX.tar) -(set -x; cd $BENCHMARK_DIR && tar --create --file $TMP_TAR --dereference --verbose .) +# macOS's bsdtar bundles extended attributes (notably `com.apple.provenance`) that a Linux Docker +# daemon rejects when unpacking the build context (`lsetxattr ... operation not supported`); exclude +# them. GNU tar (used on CI) omits xattrs by default and lacks `--no-mac-metadata`, so only pass +# these flags when the local `tar` is bsdtar. +TAR_XATTR_FLAGS=() +if tar --version 2>&1 | grep -qi bsdtar; then + TAR_XATTR_FLAGS=(--no-xattrs --no-mac-metadata) +fi +(set -x; cd $BENCHMARK_DIR && tar "${TAR_XATTR_FLAGS[@]}" --create --file $TMP_TAR --dereference --verbose .) # Build the benchmark image and extract the generated `benchmark.wasm` file from its container. print_header "Build benchmarks" diff --git a/benchmarks/kotlin-richards/Dockerfile b/benchmarks/kotlin-richards/Dockerfile new file mode 100644 index 00000000..3005ffb6 --- /dev/null +++ b/benchmarks/kotlin-richards/Dockerfile @@ -0,0 +1,76 @@ +# Reproducible build of the kotlin-richards benchmark. +# +# Stage 1 compiles the Kotlin sources to an optimized WasmGC core module via the +# Kotlin/Wasm `wasmWasi` target. Stage 2 turns that core module into a WASI +# *component* with wasm-tools and the WASI preview1 command adapter. + +# --------------------------------------------------------------------------- +# Stage 1: Kotlin -> optimized WasmGC core module. +# --------------------------------------------------------------------------- +FROM gradle:8.13-jdk21 AS kotlin-build +WORKDIR /src + +# Warm the Kotlin/Native + Binaryen toolchain and dependency caches in a layer +# that does not depend on our sources, so editing the Kotlin only re-runs the +# (fast) compile below. +COPY settings.gradle.kts gradle.properties build.gradle.kts ./ +RUN mkdir -p src/wasmWasiMain/kotlin \ + && printf 'fun main() {}\n' > src/wasmWasiMain/kotlin/Stub.kt \ + && gradle --no-daemon compileProductionExecutableKotlinWasmWasiOptimize \ + && rm -rf src + +# Compile the real sources. Produces, with Binaryen already applied: +# build/compileSync/wasmWasi/main/productionExecutable/optimized/kotlin-richards.wasm +COPY src ./src +RUN gradle --no-daemon compileProductionExecutableKotlinWasmWasiOptimize + +# --------------------------------------------------------------------------- +# Stage 2: core module -> WASI component. +# --------------------------------------------------------------------------- +FROM debian:bookworm-slim AS component-build + +# Pinned tool versions. The adapter targets WASI 0.2.6 (matches sightglass). +ARG WASM_TOOLS_VERSION=1.247.0 +ARG ADAPTER_WASMTIME_VERSION=37.0.0 +WORKDIR /work + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends curl xz-utils ca-certificates; \ + rm -rf /var/lib/apt/lists/*; \ + case "$(uname -m)" in \ + x86_64) WT_ARCH=x86_64-linux ;; \ + aarch64|arm64) WT_ARCH=aarch64-linux ;; \ + *) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;; \ + esac; \ + curl -sSL -o wasm-tools.tar.gz \ + "https://github.com/bytecodealliance/wasm-tools/releases/download/v${WASM_TOOLS_VERSION}/wasm-tools-${WASM_TOOLS_VERSION}-${WT_ARCH}.tar.gz"; \ + tar -xzf wasm-tools.tar.gz; \ + cp "wasm-tools-${WASM_TOOLS_VERSION}-${WT_ARCH}/wasm-tools" /usr/local/bin/; \ + wasm-tools --version; \ + curl -sSL -o wasi_snapshot_preview1.command.wasm \ + "https://github.com/bytecodealliance/wasmtime/releases/download/v${ADAPTER_WASMTIME_VERSION}/wasi_snapshot_preview1.command.wasm" + +COPY --from=kotlin-build \ + /src/build/compileSync/wasmWasi/main/productionExecutable/optimized/kotlin-richards.wasm \ + core.wasm +COPY wit ./wit + +# The Kotlin WASI executable exports `_initialize` (a WASI "reactor" entry that +# runs `fun main()`). Rename that export to `_start` so the preview1 *command* +# adapter binds it to `wasi:cli/run`'s `run`, which is how sightglass invokes a +# component benchmark. Then embed the world describing the `bench` import and +# create the component (the adapter supplies the WASI imports + `run` export). +RUN set -eux; \ + wasm-tools print core.wasm \ + | sed -E 's/\(export "_initialize" \(func /(export "_start" (func /' \ + | wasm-tools parse -o command.wasm; \ + wasm-tools component embed wit/bench.wit --world richards command.wasm -o embedded.wasm; \ + wasm-tools component new embedded.wasm \ + --adapt wasi_snapshot_preview1=wasi_snapshot_preview1.command.wasm \ + -o kotlin-richards.wasm; \ + wasm-tools validate --features all kotlin-richards.wasm; \ + mkdir -p /benchmark; \ + cp kotlin-richards.wasm /benchmark/ +# We output the Wasm file to the `/benchmark` directory, where the client +# expects it. diff --git a/benchmarks/kotlin-richards/README.md b/benchmarks/kotlin-richards/README.md new file mode 100644 index 00000000..7097cc30 --- /dev/null +++ b/benchmarks/kotlin-richards/README.md @@ -0,0 +1,29 @@ +# kotlin-richards + +A Kotlin port of the classic Richards benchmark: a simulation of an operating +system's scheduler. This version is a Wasm component produced via Kotlin/Wasm's +`wasmWasi` target and `wasm-tools component new`. + +Richards is an integer/pointer-heavy workload built around linked lists of +"packets" routed between cooperating tasks (idle, worker, handler, device). It +exercises object allocation, virtual dispatch, and pointer chasing, and here it +also exercises the WasmGC, typed-function-references, and exception-handling +proposals that Kotlin/Wasm relies on. + +## Origin + +The port follows the JavaScript implementation distributed with JetStream 2: + +* Source: + (also published in Google's Octane benchmark suite as `richards.js`). + +That JavaScript is itself a port of Martin Richards' original BCPL benchmark +(). `Richards.kt` is a faithful, +class-for-class translation of the JavaScript. + +### License + +The original JavaScript `richards.js` is copyright 2006-2008 the V8 project +authors and is distributed under a BSD 3-clause license (see the header of the +source linked above). This Kotlin port is made available under the same licenses +as the rest of sightglass (Apache-2.0 WITH LLVM-exception, or MIT). diff --git a/benchmarks/kotlin-richards/build.gradle.kts b/benchmarks/kotlin-richards/build.gradle.kts new file mode 100644 index 00000000..a076a290 --- /dev/null +++ b/benchmarks/kotlin-richards/build.gradle.kts @@ -0,0 +1,31 @@ +@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + +plugins { + kotlin("multiplatform") version "2.3.0" +} + +repositories { + mavenCentral() +} + +kotlin { + wasmWasi { + // A WASI "command": emits a core module exporting `_start` (whose body is + // `fun main()`) plus linear `memory`, ready for the preview1->component + // command adapter. + nodejs() + binaries.executable() + } +} + +tasks.withType>().configureEach { + compilerOptions.freeCompilerArgs.addAll( + // Opt in to the experimental host-import and unsafe-linear-memory APIs we + // use for the `bench` hooks and WASI file reading. + "-opt-in=kotlin.wasm.ExperimentalWasmInterop", + "-opt-in=kotlin.wasm.unsafe.UnsafeWasmMemoryApi", + ) +} diff --git a/benchmarks/kotlin-richards/default.input b/benchmarks/kotlin-richards/default.input new file mode 100644 index 00000000..52bd8e43 --- /dev/null +++ b/benchmarks/kotlin-richards/default.input @@ -0,0 +1 @@ +120 diff --git a/benchmarks/kotlin-richards/gradle.properties b/benchmarks/kotlin-richards/gradle.properties new file mode 100644 index 00000000..1983578f --- /dev/null +++ b/benchmarks/kotlin-richards/gradle.properties @@ -0,0 +1,2 @@ +kotlin.code.style=official +org.gradle.jvmargs=-Xmx2g diff --git a/benchmarks/kotlin-richards/kotlin-richards.stderr.expected b/benchmarks/kotlin-richards/kotlin-richards.stderr.expected new file mode 100644 index 00000000..e69de29b diff --git a/benchmarks/kotlin-richards/kotlin-richards.stdout.expected b/benchmarks/kotlin-richards/kotlin-richards.stdout.expected new file mode 100644 index 00000000..51fa8334 --- /dev/null +++ b/benchmarks/kotlin-richards/kotlin-richards.stdout.expected @@ -0,0 +1,4 @@ +[kotlin-richards] iterations: 120 +[kotlin-richards] queue count: 278640 +[kotlin-richards] hold count: 111360 +[kotlin-richards] verified diff --git a/benchmarks/kotlin-richards/kotlin-richards.wasm b/benchmarks/kotlin-richards/kotlin-richards.wasm new file mode 100644 index 00000000..2f7a7c69 Binary files /dev/null and b/benchmarks/kotlin-richards/kotlin-richards.wasm differ diff --git a/benchmarks/kotlin-richards/settings.gradle.kts b/benchmarks/kotlin-richards/settings.gradle.kts new file mode 100644 index 00000000..68ad2f3d --- /dev/null +++ b/benchmarks/kotlin-richards/settings.gradle.kts @@ -0,0 +1,8 @@ +rootProject.name = "kotlin-richards" + +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} diff --git a/benchmarks/kotlin-richards/src/wasmWasiMain/kotlin/Main.kt b/benchmarks/kotlin-richards/src/wasmWasiMain/kotlin/Main.kt new file mode 100644 index 00000000..e9ae2b9c --- /dev/null +++ b/benchmarks/kotlin-richards/src/wasmWasiMain/kotlin/Main.kt @@ -0,0 +1,147 @@ +import kotlin.wasm.WasmImport +import kotlin.wasm.unsafe.withScopedMemoryAllocator + +// =========================================================================== +// Sightglass benchmarking hooks. +// +// The harness records execution time / performance counters between these two +// calls. As core-module imports they are `(import "bench" "start" (func))` and +// `(import "bench" "end" (func))`; after `wasm-tools component new` they surface +// as an imported `bench` instance exporting `start`/`end`, which is exactly what +// the sightglass component runner supplies. +// =========================================================================== + +@WasmImport("bench", "start") +private external fun benchStart() + +@WasmImport("bench", "end") +private external fun benchEnd() + +// =========================================================================== +// Minimal WASI preview1 bindings for reading the workload file. +// +// Kotlin/Wasm's `wasmWasi` standard library has no general file API, so we bind +// the handful of `wasi_snapshot_preview1` functions we need directly. The +// WASI-preview1-to-component adapter lowers these to `wasi:filesystem` when the +// component is created. +// =========================================================================== + +@WasmImport("wasi_snapshot_preview1", "path_open") +private external fun pathOpen( + fd: Int, + dirflags: Int, + pathPtr: Int, + pathLen: Int, + oflags: Int, + rightsBase: Long, + rightsInheriting: Long, + fdflags: Int, + resultFdPtr: Int, +): Int + +@WasmImport("wasi_snapshot_preview1", "fd_read") +private external fun fdRead(fd: Int, iovsPtr: Int, iovsLen: Int, nreadPtr: Int): Int + +@WasmImport("wasi_snapshot_preview1", "fd_close") +private external fun fdClose(fd: Int): Int + +/** + * The benchmark's working directory is preopened by the harness as the first + * (and only) WASI preopen, which is conventionally file descriptor 3. + */ +private const val PREOPEN_FD = 3 + +/** `__WASI_RIGHTS_FD_READ`: the right to `fd_read` the opened file. */ +private const val RIGHTS_FD_READ = 2L + +/** + * Open [path] in the preopened working directory, read it, and return the + * leading decimal integer it contains (the Richards iteration count). + */ +private fun readIterationCount(path: String): Int = withScopedMemoryAllocator { allocator -> + // Write the path bytes into linear memory for `path_open`. + val pathBytes = path.encodeToByteArray() + val pathPtr = allocator.allocate(pathBytes.size) + for (i in pathBytes.indices) { + (pathPtr + i).storeByte(pathBytes[i]) + } + + // Open the file relative to the preopened directory. + val fdResult = allocator.allocate(4) + val openRc = pathOpen( + PREOPEN_FD, + 0, + pathPtr.address.toInt(), + pathBytes.size, + 0, + RIGHTS_FD_READ, + 0L, + 0, + fdResult.address.toInt(), + ) + check(openRc == 0) { "path_open(\"$path\") failed with errno $openRc" } + val fd = fdResult.loadInt() + + // Read the file into a buffer via a single iovec { ptr, len }. + val capacity = 64 + val buffer = allocator.allocate(capacity) + val iovec = allocator.allocate(8) + iovec.storeInt(buffer.address.toInt()) + (iovec + 4).storeInt(capacity) + val nreadPtr = allocator.allocate(4) + val readRc = fdRead(fd, iovec.address.toInt(), 1, nreadPtr.address.toInt()) + check(readRc == 0) { "fd_read failed with errno $readRc" } + val bytesRead = nreadPtr.loadInt() + fdClose(fd) + + // Parse the leading decimal integer out of the bytes read. + var value = 0 + var sawDigit = false + for (i in 0 until bytesRead) { + val c = (buffer + i).loadByte().toInt() + if (c in '0'.code..'9'.code) { + value = value * 10 + (c - '0'.code) + sawDigit = true + } else if (sawDigit) { + break + } + } + check(sawDigit) { "no integer found in \"$path\"" } + value +} + +fun main() { + // 1. Read the workload (the number of Richards iterations) from the + // filesystem via WASI. + val iterations = readIterationCount("default.input") + + // 2. Begin recording. + benchStart() + + // 3. The main work: run the Richards scheduler simulation `iterations` + // times, accumulating the packet-queue and task-hold counts each run + // produces. + var totalQueueCount = 0L + var totalHoldCount = 0L + var allValid = true + for (i in 0 until iterations) { + val scheduler = runRichards() + totalQueueCount += scheduler.queueCount + totalHoldCount += scheduler.holdCount + if (scheduler.queueCount != EXPECTED_QUEUE_COUNT || + scheduler.holdCount != EXPECTED_HOLD_COUNT + ) { + allValid = false + } + } + + // 4. Stop recording. + benchEnd() + + // 5. Print results derived from the work so an optimizing compiler cannot + // elide the simulation. + println("[kotlin-richards] iterations: $iterations") + println("[kotlin-richards] queue count: $totalQueueCount") + println("[kotlin-richards] hold count: $totalHoldCount") + println("[kotlin-richards] " + if (allValid) "verified" else "INVALID") +} diff --git a/benchmarks/kotlin-richards/src/wasmWasiMain/kotlin/Richards.kt b/benchmarks/kotlin-richards/src/wasmWasiMain/kotlin/Richards.kt new file mode 100644 index 00000000..ea4382dd --- /dev/null +++ b/benchmarks/kotlin-richards/src/wasmWasiMain/kotlin/Richards.kt @@ -0,0 +1,283 @@ +/* + * Richards: a faithful Kotlin port of the JetStream2 Octane `richards.js` + * benchmark, which simulates the task dispatcher of an operating system. + * + * The original JavaScript (and this port's structure, constants, and the + * EXPECTED_QUEUE_COUNT / EXPECTED_HOLD_COUNT validation values) come from the V8 + * project's Richards benchmark; see README.md for full provenance and license. + */ + +const val ID_IDLE = 0 +const val ID_WORKER = 1 +const val ID_HANDLER_A = 2 +const val ID_HANDLER_B = 3 +const val ID_DEVICE_A = 4 +const val ID_DEVICE_B = 5 +const val NUMBER_OF_IDS = 6 + +const val KIND_DEVICE = 0 +const val KIND_WORK = 1 + +const val STATE_RUNNING = 0 +const val STATE_RUNNABLE = 1 +const val STATE_SUSPENDED = 2 +const val STATE_HELD = 4 +const val STATE_SUSPENDED_RUNNABLE = STATE_SUSPENDED or STATE_RUNNABLE // 3 +const val STATE_NOT_HELD = STATE_HELD.inv() // ~4 + +const val DATA_SIZE = 4 +const val COUNT = 1000 + +/** + * These two constants are characteristic of a correct run of Richards: they + * specify how many times a packet is queued and how many times a task is put on + * hold. If the actual counts differ, the implementation has a bug. + */ +const val EXPECTED_QUEUE_COUNT = 2322 +const val EXPECTED_HOLD_COUNT = 928 + +/** + * A simple package of data manipulated by the tasks. Besides carrying data, + * packets form linked lists and are hence used both as data and worklists. + */ +class Packet(var link: Packet?, var id: Int, val kind: Int) { + var a1: Int = 0 + val a2: IntArray = IntArray(DATA_SIZE) + + /** Add this packet to the end of [queue], and return the worklist. */ + fun addTo(queue: Packet?): Packet { + link = null + if (queue == null) return this + var next: Packet = queue + var peek = next.link + while (peek != null) { + next = peek + peek = next.link + } + next.link = this + return queue + } +} + +/** The work a [TaskControlBlock] performs when scheduled. */ +abstract class Task { + abstract fun run(packet: Packet?): TaskControlBlock? +} + +/** Cycles control between the two device tasks; ends the run when [count] hits 0. */ +class IdleTask(val scheduler: Scheduler, var v1: Int, var count: Int) : Task() { + override fun run(packet: Packet?): TaskControlBlock? { + count-- + if (count == 0) return scheduler.holdCurrent() + return if ((v1 and 1) == 0) { + v1 = v1 shr 1 + scheduler.release(ID_DEVICE_A) + } else { + v1 = (v1 shr 1) xor 0xD008 + scheduler.release(ID_DEVICE_B) + } + } +} + +/** Suspends itself after each run to simulate waiting on an external device. */ +class DeviceTask(val scheduler: Scheduler) : Task() { + var v1: Packet? = null + override fun run(packet: Packet?): TaskControlBlock? { + if (packet == null) { + val v = v1 ?: return scheduler.suspendCurrent() + v1 = null + return scheduler.queue(v) + } else { + v1 = packet + return scheduler.holdCurrent() + } + } +} + +/** Manipulates work packets. */ +class WorkerTask(val scheduler: Scheduler, var v1: Int, var v2: Int) : Task() { + override fun run(packet: Packet?): TaskControlBlock? { + if (packet == null) return scheduler.suspendCurrent() + v1 = if (v1 == ID_HANDLER_A) ID_HANDLER_B else ID_HANDLER_A + packet.id = v1 + packet.a1 = 0 + for (i in 0 until DATA_SIZE) { + v2++ + if (v2 > 26) v2 = 1 + packet.a2[i] = v2 + } + return scheduler.queue(packet) + } +} + +/** Manipulates work packets and then suspends itself. */ +class HandlerTask(val scheduler: Scheduler) : Task() { + var v1: Packet? = null + var v2: Packet? = null + override fun run(packet: Packet?): TaskControlBlock? { + if (packet != null) { + if (packet.kind == KIND_WORK) v1 = packet.addTo(v1) else v2 = packet.addTo(v2) + } + val work = v1 + if (work != null) { + val count = work.a1 + if (count < DATA_SIZE) { + val dev = v2 + if (dev != null) { + v2 = dev.link + dev.a1 = work.a2[count] + work.a1 = count + 1 + return scheduler.queue(dev) + } + } else { + v1 = work.link + return scheduler.queue(work) + } + } + return scheduler.suspendCurrent() + } +} + +/** Manages a task and the queue of work packets associated with it. */ +class TaskControlBlock( + val link: TaskControlBlock?, + val id: Int, + val priority: Int, + var queue: Packet?, + val task: Task, +) { + var state: Int = if (queue == null) STATE_SUSPENDED else STATE_SUSPENDED_RUNNABLE + + fun setRunning() { state = STATE_RUNNING } + fun markAsNotHeld() { state = state and STATE_NOT_HELD } + fun markAsHeld() { state = state or STATE_HELD } + fun isHeldOrSuspended(): Boolean = (state and STATE_HELD) != 0 || state == STATE_SUSPENDED + fun markAsSuspended() { state = state or STATE_SUSPENDED } + fun markAsRunnable() { state = state or STATE_RUNNABLE } + + /** Run this task, if ready, and return the next task to run. */ + fun run(): TaskControlBlock? { + val packet: Packet? + if (state == STATE_SUSPENDED_RUNNABLE) { + packet = queue + queue = packet!!.link + state = if (queue == null) STATE_RUNNING else STATE_RUNNABLE + } else { + packet = null + } + return task.run(packet) + } + + /** + * Add [packet] to this block's worklist, mark it runnable if necessary, and + * return the next runnable task (the highest-priority one). + */ + fun checkPriorityAdd(tcb: TaskControlBlock, packet: Packet): TaskControlBlock { + if (queue == null) { + queue = packet + markAsRunnable() + if (priority > tcb.priority) return this + } else { + queue = packet.addTo(queue) + } + return tcb + } +} + +/** Schedules a set of tasks based on their relative priorities. */ +class Scheduler { + var queueCount = 0 + var holdCount = 0 + val blocks = arrayOfNulls(NUMBER_OF_IDS) + var list: TaskControlBlock? = null + var currentTcb: TaskControlBlock? = null + var currentId: Int = 0 + + fun addIdleTask(id: Int, priority: Int, queue: Packet?, count: Int) = + addRunningTask(id, priority, queue, IdleTask(this, 1, count)) + + fun addWorkerTask(id: Int, priority: Int, queue: Packet?) = + addTask(id, priority, queue, WorkerTask(this, ID_HANDLER_A, 0)) + + fun addHandlerTask(id: Int, priority: Int, queue: Packet?) = + addTask(id, priority, queue, HandlerTask(this)) + + fun addDeviceTask(id: Int, priority: Int, queue: Packet?) = + addTask(id, priority, queue, DeviceTask(this)) + + fun addRunningTask(id: Int, priority: Int, queue: Packet?, task: Task) { + addTask(id, priority, queue, task) + currentTcb!!.setRunning() + } + + fun addTask(id: Int, priority: Int, queue: Packet?, task: Task) { + val tcb = TaskControlBlock(list, id, priority, queue, task) + currentTcb = tcb + list = tcb + blocks[id] = tcb + } + + fun schedule() { + currentTcb = list + while (currentTcb != null) { + val tcb = currentTcb!! + if (tcb.isHeldOrSuspended()) { + currentTcb = tcb.link + } else { + currentId = tcb.id + currentTcb = tcb.run() + } + } + } + + fun release(id: Int): TaskControlBlock? { + val tcb = blocks[id] ?: return null + tcb.markAsNotHeld() + return if (tcb.priority > currentTcb!!.priority) tcb else currentTcb + } + + fun holdCurrent(): TaskControlBlock? { + holdCount++ + currentTcb!!.markAsHeld() + return currentTcb!!.link + } + + fun suspendCurrent(): TaskControlBlock? { + currentTcb!!.markAsSuspended() + return currentTcb + } + + fun queue(packet: Packet): TaskControlBlock? { + val t = blocks[packet.id] ?: return null + queueCount++ + packet.link = null + packet.id = currentId + return t.checkPriorityAdd(currentTcb!!, packet) + } +} + +/** Run one Richards simulation; returns the scheduler holding the run's counts. */ +fun runRichards(): Scheduler { + val scheduler = Scheduler() + scheduler.addIdleTask(ID_IDLE, 0, null, COUNT) + + var queue: Packet? = Packet(null, ID_WORKER, KIND_WORK) + queue = Packet(queue, ID_WORKER, KIND_WORK) + scheduler.addWorkerTask(ID_WORKER, 1000, queue) + + queue = Packet(null, ID_DEVICE_A, KIND_DEVICE) + queue = Packet(queue, ID_DEVICE_A, KIND_DEVICE) + queue = Packet(queue, ID_DEVICE_A, KIND_DEVICE) + scheduler.addHandlerTask(ID_HANDLER_A, 2000, queue) + + queue = Packet(null, ID_DEVICE_B, KIND_DEVICE) + queue = Packet(queue, ID_DEVICE_B, KIND_DEVICE) + queue = Packet(queue, ID_DEVICE_B, KIND_DEVICE) + scheduler.addHandlerTask(ID_HANDLER_B, 3000, queue) + + scheduler.addDeviceTask(ID_DEVICE_A, 4000, null) + scheduler.addDeviceTask(ID_DEVICE_B, 5000, null) + + scheduler.schedule() + return scheduler +} diff --git a/benchmarks/kotlin-richards/wit/bench.wit b/benchmarks/kotlin-richards/wit/bench.wit new file mode 100644 index 00000000..aa9c8164 --- /dev/null +++ b/benchmarks/kotlin-richards/wit/bench.wit @@ -0,0 +1,12 @@ +package sightglass:richards; + +// The world the Kotlin core module targets. We only need to describe the custom +// `bench` timing-hook import here; `wasm-tools component new` discovers the WASI +// imports (filesystem, stdout, ...) and the `wasi:cli/run` export from the +// preview1 command adapter automatically. +world richards { + import bench: interface { + start: func(); + end: func(); + } +} diff --git a/crates/cli/src/pca_metrics/dynamic_metrics.rs b/crates/cli/src/pca_metrics/dynamic_metrics.rs index 15476e1b..75f452f0 100644 --- a/crates/cli/src/pca_metrics/dynamic_metrics.rs +++ b/crates/cli/src/pca_metrics/dynamic_metrics.rs @@ -4,9 +4,9 @@ mod component; -use super::category::{Category, NUM_CATEGORIES}; use super::Counts; -use anyhow::{bail, Context, Result}; +use super::category::{Category, NUM_CATEGORIES}; +use anyhow::{Context, Result, bail}; use sightglass_data::{Measurement, Phase}; use std::path::Path; #[cfg(all(target_os = "linux", feature = "callgrind"))] @@ -23,12 +23,16 @@ const COUNTER_PREFIX: &str = "__pca_count_"; const PCA_INSTANCE: &str = "sightglass-pca"; const INCREMENT_FN: &str = "increment-instruction-count"; -/// Functions exported under these conventional names are the canonical ABI -/// `realloc`, which runs with the component's "may leave" flag cleared and so -/// cannot call the host `increment-instruction-count`. When flushing, we still -/// count their instructions into the per-category globals but never flush from -/// inside them; a later, ordinary function flushes the accumulated counts. -const NO_FLUSH_EXPORTS: &[&str] = &["realloc", "cabi_realloc"]; +/// Functions exported under these conventional names are canonical ABI +/// `realloc`s, which (along with everything they call) run with the component's +/// "may leave" flag cleared and so cannot call the host +/// `increment-instruction-count`. When flushing, we raise an in-realloc guard on +/// entry to these functions and gate every flush on it (see +/// `instrument_core_module`): their instructions still accumulate into the +/// per-category globals, but the host call is deferred to the next flush point +/// outside the realloc. `cabi_import_realloc` is the realloc the WASI preview1 +/// component adapter exports. +const NO_FLUSH_EXPORTS: &[&str] = &["realloc", "cabi_realloc", "cabi_import_realloc"]; /// Compute the dynamic instruction mix for `wasm`, accumulating it into /// `counts`, by instrumenting and running the benchmark once. @@ -96,7 +100,7 @@ fn callgrind_metrics( .arg("--working-dir") .arg(working_dir) .arg("--engine-flags") - .arg("-Wgc,function-references") + .arg("-Wgc,function-references,exceptions") .arg("--") .arg(benchmark) .output() @@ -289,6 +293,7 @@ pub(crate) fn make_engine(consume_fuel: bool) -> Result { // Required for running some of our benchmarks. config.wasm_gc(true); config.wasm_function_references(true); + config.wasm_exceptions(true); // Enable Wasmtime's compilation cache. if let Ok(cache) = wasmtime::Cache::new(wasmtime::CacheConfig::default()) { @@ -406,10 +411,10 @@ fn basic_blocks(body: &wasmparser::FunctionBody<'_>) -> Result> fn instrument_core_module(wasm: &[u8], flush: bool) -> Result { use wasm_encoder::reencode::{Error as ReencodeError, Reencode}; use wasm_encoder::{ - CodeSection, ConstExpr, DataCountSection, DataSection, ElementSection, EntityType, - ExportKind, ExportSection, Function, FunctionSection, GlobalSection, GlobalType, - ImportSection, Instruction, MemorySection, Module, StartSection, TableSection, TagSection, - TypeSection, ValType, + BlockType, CodeSection, ConstExpr, DataCountSection, DataSection, ElementSection, + EntityType, ExportKind, ExportSection, Function, FunctionSection, GlobalSection, + GlobalType, ImportSection, Instruction, MemorySection, Module, StartSection, TableSection, + TagSection, TypeSection, ValType, }; // Shift function references by one only when flushing prepends its import. @@ -467,7 +472,9 @@ fn instrument_core_module(wasm: &[u8], flush: bool) -> Result { - num_types += s.count(); + for rec_group in s.clone() { + num_types += rec_group?.types().count() as u32; + } let sec = types.get_or_insert_with(TypeSection::new); reencoder.parse_type_section(sec, s)?; } @@ -543,8 +550,14 @@ fn instrument_core_module(wasm: &[u8], flush: bool) -> Result { let base_global = imported_globals + defined_globals; - let active_global = base_global + NUM_CATEGORIES as u32; - // `realloc`-style functions accumulate counts but never flush. + // The global just past the per-category counters. When flushing + // (components) it is the in-realloc guard: non-zero while a + // canonical ABI `realloc` (and anything it calls) is on the + // stack. When not flushing (standalone core modules) it is the + // `is-benchmarking-active` flag instead. + let extra_global = base_global + NUM_CATEGORIES as u32; + // `realloc`-style functions (and their callees) run with the + // component's "may leave" flag cleared, so they must not flush. let no_flush = flush && no_flush_funcs.contains(&(imported_funcs + code_entry)); code_entry += 1; @@ -557,6 +570,17 @@ fn instrument_core_module(wasm: &[u8], flush: bool) -> Result Result Result Result Result Result Result