From 8dd307d316937ee6d371134c5b1412828a3029bd Mon Sep 17 00:00:00 2001 From: udo Date: Thu, 16 Jul 2026 22:51:16 +0000 Subject: [PATCH] Precompile native modules off request path --- docs/wasm-runtime-architecture.md | 2 ++ scripts/test_dependency_invalidation.sh | 19 +++++++++++++++- src/linux_fastcgi.cpp | 26 +++++++++++++++------ src/wasm/backend.cpp | 10 +++++++++ src/wasm/backend.h | 5 +++++ src/wasm/worker.cpp | 30 +++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 8 deletions(-) diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 26f21cd..316f0f8 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -178,6 +178,8 @@ When a current serialized module exists, the worker scans wasm section headers and reads only `dylink.0` and `uce.abi`; it does not fault the multi-megabyte code and data bodies into every new worker. A missing/stale/invalid serialized module still reads, validates, compiles, and republishes the complete wasm artifact. +The proactive compiler also creates that serialization immediately after source +compilation, keeping first-worker native compilation off the request path. 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 diff --git a/scripts/test_dependency_invalidation.sh b/scripts/test_dependency_invalidation.sh index dc3c355..1139266 100755 --- a/scripts/test_dependency_invalidation.sh +++ b/scripts/test_dependency_invalidation.sh @@ -181,9 +181,26 @@ wait "$rebuild_lock_pid" # parent CLI request must wait and render the new child, never its stale wasm. printf '%s\n' 'COMPONENT(Request& context) { print("nested-component-marker-a"); }' >"$source_dir/nested-child.uce" printf '%s\n' \ - 'CLI(Request& context) { DValue props; print(component("nested-child", props, context)); }' >"$source_dir/nested-parent.uce" + 'CLI(Request& context) { DValue props; print(component("nested-child", props, context)); }' \ + 'RENDER(Request& context) { DValue props; print(component("nested-child", props, context)); }' >"$source_dir/nested-parent.uce" assert_marker nested-parent nested-component-marker-a nested_child_wasm="$cache_dir/nested-child.uce.wasm" + +# A proactive source rebuild must publish the native serialized module before +# any request worker has to load the changed unit. +nested_parent_cwasm="$cache_dir/nested-parent.uce.cwasm" +rm -f "$nested_parent_cwasm" +printf '%s\n' \ + 'CLI(Request& context) { DValue props; print(component("nested-child", props, context)); /* proactive-serialization-v2 */ }' \ + 'RENDER(Request& context) { DValue props; print(component("nested-child", props, context)); /* proactive-serialization-v2 */ }' >"$source_dir/nested-parent.uce" +curl -fsS -H "Host: $http_host" "http://127.0.0.1/$test_name/nested-parent.uce" >/dev/null +deadline=$((SECONDS + 10)) +while [[ ! -s "$nested_parent_cwasm" && $SECONDS -lt $deadline ]]; do sleep 0.1; done +if [[ ! -s "$nested_parent_cwasm" ]]; then + echo "proactive compiler did not serialize rebuilt module" >&2 + exit 1 +fi + ( exec 8>"$nested_child_wasm.lock" flock 8 diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 08e949a..546d132 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -1266,7 +1266,8 @@ void run_proactive_compiler() bool source_missing = false; auto retry_it = retry_after.find(file_name); bool retry_allowed = (retry_it == retry_after.end() || time_precise() >= retry_it->second); - if(compiler_unit_needs_recompile(&background_context, file_name, &source_missing) && retry_allowed) + bool needs_compile = compiler_unit_needs_recompile(&background_context, file_name, &source_missing); + if(needs_compile && retry_allowed) proactive_compile_queue_push(compile_queue, file_name); if(source_missing) { @@ -1291,14 +1292,15 @@ void run_proactive_compiler() auto retry_it = retry_after.find(file_name); if(retry_it != retry_after.end() && time_precise() < retry_it->second) continue; + bool failed = false; + String wasm_path = server_state.config["BIN_DIRECTORY"] + file_name + ".wasm"; if(compiler_unit_needs_recompile(&background_context, file_name, &source_missing)) { printf("(i) proactive compile %s\n", file_name.c_str()); auto su = get_shared_unit(&background_context, file_name); - if(su && su->compiler_messages == "") - retry_after.erase(file_name); - else - retry_after[file_name] = time_precise() + failure_retry_interval; + failed = !su || su->compiler_messages != ""; + if(su) + wasm_path = su->wasm_name; } else if(source_missing) { @@ -1306,10 +1308,20 @@ void run_proactive_compiler() compiler_untrack_known_unit(&background_context, file_name); retry_after.erase(file_name); } - else + if(!source_missing && !failed && wasm_serialized_module_needs_refresh(wasm_path)) { - retry_after.erase(file_name); + printf("(i) proactive serialize %s\n", file_name.c_str()); + String serialize_error = wasm_serialize_module_artifact(wasm_path); + if(serialize_error != "") + { + printf("(!) proactive serialize failed for %s: %s\n", file_name.c_str(), serialize_error.c_str()); + failed = true; + } } + if(!source_missing && failed) + retry_after[file_name] = time_precise() + failure_retry_interval; + else if(!source_missing) + retry_after.erase(file_name); background_context.session.clear(); background_context.session_loaded_hash = ""; clear_shared_unit_cache(server_state); diff --git a/src/wasm/backend.cpp b/src/wasm/backend.cpp index 9cd2994..3ef33eb 100644 --- a/src/wasm/backend.cpp +++ b/src/wasm/backend.cpp @@ -93,6 +93,16 @@ String wasm_backend_start(Request& request) return(wasm_backend_ensure_started(&request)); } +bool wasm_serialized_module_needs_refresh(const String& wasm_path) +{ + return(WasmWorker::serialized_module_needs_refresh(wasm_path)); +} + +String wasm_serialize_module_artifact(const String& wasm_path) +{ + return(WasmWorker::serialize_module_artifact(wasm_path)); +} + static bool wasm_artifact_exists(Request* context, const String& entry_unit) { if(entry_unit == "") diff --git a/src/wasm/backend.h b/src/wasm/backend.h index 84f3489..de20767 100644 --- a/src/wasm/backend.h +++ b/src/wasm/backend.h @@ -14,6 +14,11 @@ bool wasm_backend_should_handle(Request& request, const String& entry_unit); // requests. Returns "" on success or the retained backend initialization error. String wasm_backend_start(Request& request); +// Proactive compiler hooks for keeping Wasmtime's native serialization newer +// than a compiled unit artifact before any request worker loads it. +bool wasm_serialized_module_needs_refresh(const String& wasm_path); +String wasm_serialize_module_artifact(const String& wasm_path); + // Serve a request through a wasm workspace by invoking a named unit handler — // "render", "cli", "websocket", "serve_http", "serve_http:named" — and populate // the native Request. Returns "" on success or a collapsed error. The handler is diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index e8c8346..5604ae6 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -1049,6 +1049,36 @@ public: return(unit); } + static bool serialized_module_needs_refresh(const String& wasm_path) + { + struct stat wasm_stat; + struct stat cached_stat; + String cached_path = cached_wasm_path(wasm_path); + if(stat(wasm_path.c_str(), &wasm_stat) != 0) + return(false); + if(stat(cached_path.c_str(), &cached_stat) != 0) + return(true); + return(cached_stat.st_mtim.tv_sec < wasm_stat.st_mtim.tv_sec || + (cached_stat.st_mtim.tv_sec == wasm_stat.st_mtim.tv_sec && cached_stat.st_mtim.tv_nsec <= wasm_stat.st_mtim.tv_nsec)); + } + + static String serialize_module_artifact(const String& wasm_path) + { + if(!serialized_module_needs_refresh(wasm_path)) + return(""); + std::vector bytes; + if(!wasm_read_file(wasm_path, bytes)) + return("cannot read " + wasm_path); + wasmtime::Engine engine = make_engine(); + String error; + auto module = compile_and_cache_module(engine, cached_wasm_path(wasm_path), bytes, error); + if(!module) + return(error); + if(serialized_module_needs_refresh(wasm_path)) + return("cannot publish serialized module for " + wasm_path); + return(""); + } + private: static String cached_wasm_path(const String& wasm_path) {