From 7f15aa06404dc8b47c72afaa6d44baeae8814de1 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Wed, 24 Jun 2026 14:01:51 -0700 Subject: [PATCH 1/3] Fix PCA dynamic metrics instrumentation for rec groups and other realloc shapes --- .github/workflows/benchmarks.yml | 2 +- crates/cli/src/pca_metrics/dynamic_metrics.rs | 99 ++++++++++++++----- 2 files changed, 77 insertions(+), 24 deletions(-) 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/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 Date: Wed, 24 Jun 2026 14:54:00 -0700 Subject: [PATCH 2/3] New benchmark: a port of Richards to Kotlin/Wasm This adds 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. --- benchmarks/build.sh | 10 +- benchmarks/kotlin-richards/Dockerfile | 76 +++++ benchmarks/kotlin-richards/README.md | 29 ++ benchmarks/kotlin-richards/build.gradle.kts | 31 ++ benchmarks/kotlin-richards/default.input | 1 + benchmarks/kotlin-richards/gradle.properties | 2 + .../kotlin-richards.stderr.expected | 0 .../kotlin-richards.stdout.expected | 4 + .../kotlin-richards/kotlin-richards.wasm | Bin 0 -> 44136 bytes .../kotlin-richards/settings.gradle.kts | 8 + .../src/wasmWasiMain/kotlin/Main.kt | 147 +++++++++ .../src/wasmWasiMain/kotlin/Richards.kt | 283 ++++++++++++++++++ benchmarks/kotlin-richards/wit/bench.wit | 12 + 13 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 benchmarks/kotlin-richards/Dockerfile create mode 100644 benchmarks/kotlin-richards/README.md create mode 100644 benchmarks/kotlin-richards/build.gradle.kts create mode 100644 benchmarks/kotlin-richards/default.input create mode 100644 benchmarks/kotlin-richards/gradle.properties create mode 100644 benchmarks/kotlin-richards/kotlin-richards.stderr.expected create mode 100644 benchmarks/kotlin-richards/kotlin-richards.stdout.expected create mode 100644 benchmarks/kotlin-richards/kotlin-richards.wasm create mode 100644 benchmarks/kotlin-richards/settings.gradle.kts create mode 100644 benchmarks/kotlin-richards/src/wasmWasiMain/kotlin/Main.kt create mode 100644 benchmarks/kotlin-richards/src/wasmWasiMain/kotlin/Richards.kt create mode 100644 benchmarks/kotlin-richards/wit/bench.wit 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 0000000000000000000000000000000000000000..2f7a7c699507bd7584344596f0180a8bfdc4462b GIT binary patch literal 44136 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 + // 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(); + } +} From b89561d3b3e301a1628cd5ae665de0a62a871605 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Wed, 24 Jun 2026 14:58:53 -0700 Subject: [PATCH 3/3] add kotlin-richards to all.suite --- benchmarks/all.suite | 1 + 1 file changed, 1 insertion(+) 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