diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 9108591..ea677fb 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -131,6 +131,10 @@ available. If an artifact is cold or stale, dispatch compiles it on demand via Workers identify cached unit artifacts by nanosecond mtime, ctime, and size. Whole-second mtime alone is insufficient because a dependency-triggered rebuild can replace a wasm artifact within the same second as its prior build. +Cold module compilation and deserialization are host work, so `load_unit()` +refreshes the epoch deadline before its first guest call. Otherwise a component +whose compilation outlasted the guest CPU budget would immediately trap in the +following allocator/relocation call even though no guest loop consumed it. --- @@ -330,9 +334,11 @@ header free-functions are `inline`. The wasm backend exposes only declarations `scripts/test_dependency_invalidation.sh`. The latter changes a transitive `#load`, then replaces a warmed worker artifact while preserving its whole-second mtime to prove both compiler and worker caches invalidate it. + `scripts/test_cold_component_deadline.sh` separately compiles a deliberately + cold component that exceeds the development epoch window and proves the + parent request still renders it. - **WebSocket end-to-end**: a headless client performs a raw WS handshake to `:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving `SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full broker → worker → broker → client chain across process boundaries. - diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index ed4483c..9230316 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -51,4 +51,5 @@ url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${inclu curl -sS --fail-with-body --unix-socket "$socket_path" "$url" if [[ "$action" == "run" ]]; then scripts/test_dependency_invalidation.sh + scripts/test_cold_component_deadline.sh fi diff --git a/scripts/test_cold_component_deadline.sh b/scripts/test_cold_component_deadline.sh new file mode 100755 index 0000000..70e322f --- /dev/null +++ b/scripts/test_cold_component_deadline.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +test_name="component-deadline-test-$$" +site_directory="${UCE_TEST_SITE_DIRECTORY:-site}" +source_dir="$site_directory/$test_name" +bin_directory="${BIN_DIRECTORY:-}" +if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then + bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg) +fi +bin_directory="${bin_directory:-/tmp/uce/work}" +cache_dir="" + +cleanup() { + rm -rf "$source_dir" + if [[ -n "$cache_dir" ]]; then + rm -rf "$cache_dir" + fi +} +trap cleanup EXIT +mkdir -p "$source_dir" +cache_dir="$bin_directory$(realpath "$source_dir")" + +printf '%s\n' \ + 'CLI(Request& context) { DValue props; print(component("child", props, context)); }' >"$source_dir/parent.uce" +for i in $(seq 1 800); do + printf 'String cold_component_pad_%s() { return("%s"); }\n' "$i" "$i" +done >"$source_dir/child.uce" +printf '%s\n' \ + 'COMPONENT(Request& context) { <>cold-component-deadline-ok }' >>"$source_dir/child.uce" +rm -rf "$cache_dir" + +output=$(scripts/uce-cli "/$test_name/parent.uce") +if [[ "$output" != *cold-component-deadline-ok* ]]; then + echo "cold component compilation consumed the guest epoch budget: $output" >&2 + exit 1 +fi + +echo "cold component deadline passed" diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index fef30af..1c86c32 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -1190,6 +1190,10 @@ private: auto mod = worker.unit_module(source_path, error); if(!mod) return(error); + // Compiling/deserializing a cold module is host work. Refresh the guest + // watchdog before the first core call so that wall time cannot make the + // otherwise harmless malloc/reloc sequence trap immediately. + ctx().set_epoch_deadline(worker.cfg.epoch_deadline_ticks); if(mod->abi.version != abi_version) return(mod->wasm_path + ": uce.abi version " + std::to_string(mod->abi.version) + " does not match core ABI " + std::to_string(abi_version)); @@ -1347,12 +1351,8 @@ private: if(worker.cfg.verbose) fprintf(stderr, "[wasm] loaded %s (mem_base=%u table_base=%u)\n", source_path.c_str(), memory_base, table_base); - // The epoch budget is a guest-CPU watchdog, but the ticker is wall-clock - // and host-side module compilation here (lazy, mid-render, possibly many - // units) burns it without the guest running. Reset the deadline after a - // load so the budget measures guest execution between membrane crossings, - // not our compile time. A genuine runaway loop makes no loads, so it - // still trips the deadline. + // Exclude the rest of host-side loading as well. A genuine runaway loop + // makes no loads, so it still trips the deadline. ctx().set_epoch_deadline(worker.cfg.epoch_deadline_ticks); return(""); }