diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index c926772..275e432 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -147,8 +147,11 @@ slots, then runs relocations, constructors, and request-pointer binding. This removes repeated Wasmtime import-type traversal without sharing request state or changing core-first, unit-load-order symbol resolution. Each FastCGI child initializes its process-local Wasmtime engine after fork and -before entering the accept loop. This preserves Wasmtime's fork boundary while -preventing the first request assigned to each worker from paying engine startup. +before entering the accept loop. It also creates the linker, resolves the core's +host imports into a store-independent Wasmtime `InstancePre`, and births then +drops one empty workspace. This preserves Wasmtime's fork boundary while +preventing the first request assigned to each worker from paying engine, linker, +or pre-instantiation startup. Startup duration or failure is written to the service log. The serialized core module lives in the configured writable cache root rather than beside the possibly root-owned deployed `core.wasm`; freshness still uses the deployed @@ -227,7 +230,15 @@ Each request gets a fresh **workspace** — a per-request wasm instance tree wit the membrane wired in. `wasm_worker_serve(worker, ctx, entry_unit, handler)` (`src/wasm/worker.cpp`) is the single entry point for *every* mode: -1. Birth a request-scoped workspace with fresh per-request state. Current production workers instantiate a fresh Store, table, core instance, memory, globals, constructors, and request tree for each request. The worker retains only Wasmtime's store-independent host-function definitions and their fixed import order, avoiding reconstruction of 73 identical callback definitions. Every callback resolves the one active request workspace at invocation. Any future snapshot/CoW optimization must preserve that request-isolation contract. +1. Birth a request-scoped workspace with fresh per-request state. Current + production workers create a fresh Store and instantiate the store-independent + `InstancePre`; the resulting core instance owns a fresh exported, growable + function table, memory, globals, constructors, and request tree. The worker + retains only Wasmtime's compiled module, linker, pre-instantiation plan, and + store-independent host-function definitions. Every callback resolves the one + active request workspace at invocation. Legacy cores that import their table + remain supported through the prior per-request import path. Any future + snapshot/CoW optimization must preserve this request-isolation contract. 2. Resolve `entry_unit` + `handler` to an export; components referenced at runtime are resolved on demand via the `uce_host_component_resolve` hostcall (`component_resolve()` → `__uce_<...>` slot), loading dependency modules @@ -254,7 +265,7 @@ not fork into a private transport type. Validated decode trees move into the request rather than being deep-copied twice; the historical by-value `DValue::operator=(DValue)` symbol remains exported so warmed side-module artifacts stay ABI-compatible. `request_perf()` subdivides birth into policy, -import materialization, core instantiation, export lookup, and initialization, +import materialization, core instantiation, export/table lookup, and initialization, and context transfer into bytes, host encode, guest allocation/write, guest decode/application, and free. @@ -453,6 +464,10 @@ header free-functions are `inline`. The wasm backend exposes only declarations the generated C++ snapshot has been written. The compiler must either retry and serve the post-edit source or fail closed; it must never stamp current source metadata onto wasm produced from an older snapshot. +- **Core compatibility**: the production core owns and exports its growable + function table, enabling `InstancePre`. A separately built legacy + `--import-table` core must still pass the demo and 64-request pool-isolation + groups before the compatibility fallback changes. `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. The focused shell gates create temporary diff --git a/scripts/build_core_wasm.sh b/scripts/build_core_wasm.sh index 18c2d48..8d11160 100755 --- a/scripts/build_core_wasm.sh +++ b/scripts/build_core_wasm.sh @@ -21,7 +21,8 @@ fi -Wl,--export-all \ -Wl,--export=__heap_base \ -Wl,--export=__stack_pointer \ - -Wl,--import-table \ + -Wl,--export-table \ + -Wl,--growable-table \ -Wl,-z,stack-size=8388608 \ -Wl,--allow-undefined-file=src/wasm/core_hostcalls.syms \ -Wl,--no-entry \ diff --git a/src/wasm/backend.cpp b/src/wasm/backend.cpp index 3ba239e..df64661 100644 --- a/src/wasm/backend.cpp +++ b/src/wasm/backend.cpp @@ -7,8 +7,7 @@ #include "../lib/wasm_trace.h" // The native server TU has the native connectors (sqlite/mysql) compiled in, so -// the worker's host-side connector hostcalls are available here. The W3 CLI -// driver does not define this and gets a named-trap stub for those imports. +// the worker's host-side connector hostcalls are available here. #define UCE_WASM_HOST_CONNECTORS 1 #include "worker.cpp" @@ -70,6 +69,8 @@ static String wasm_backend_ensure_started(Request* context) g_wasm_worker = new WasmWorker(wc); g_wasm_init_error = g_wasm_worker->init(); + if(g_wasm_init_error == "") + g_wasm_init_error = wasm_worker_prepare(*g_wasm_worker); if(g_wasm_init_error != "") { delete g_wasm_worker; diff --git a/src/wasm/w3_driver.cpp b/src/wasm/w3_driver.cpp deleted file mode 100644 index 84db293..0000000 --- a/src/wasm/w3_driver.cpp +++ /dev/null @@ -1,184 +0,0 @@ -// W3 CLI driver — the exit gate for WASM-PROPOSAL §9.1 W3. -// -// Serves requests through the production workspace runtime -// (src/wasm/worker.cpp): UCEB2 context in → core + lazily loaded -// generated units → body/response-meta out. Each --repeat gets a fresh -// workspace, proving birth/drop. An epoch ticker thread enforces the CPU -// budget; the store limiter enforces memory; traps come back as collapsed -// wasm_trace summaries. -// -// Amalgamation TU (project style, like uce_lib.cpp): native types + DValue -// codec first, then the worker. - -// same order as uce_lib.cpp, minus the native compiler/connector block -#include "../lib/types.cpp" -#include "../lib/dvalue.cpp" -#include "../lib/functionlib.cpp" -#include "../lib/hash.cpp" -#include "../lib/sys.cpp" -#include "../lib/uri.cpp" -#include "../lib/cli.cpp" -#include "../lib/wasm_trace.h" -#include "worker.cpp" - -#include -#include - -static String arg_value(int argc, char** argv, int& i, const char* flag) -{ - if(i + 1 >= argc) - { - fprintf(stderr, "missing value for %s\n", flag); - exit(2); - } - return(String(argv[++i])); -} - -static String absolute_path(String path) -{ - if(path.rfind("/", 0) == 0) - return(path); - char buf[4096]; - if(!getcwd(buf, sizeof(buf))) - return(path); - return(String(buf) + "/" + path); -} - -int main(int argc, char** argv) -{ - WasmWorkerConfig cfg; - cfg.site_root = absolute_path("site"); - String page; - String route; - StringMap params; - StringMap get_params; - std::vector expects; - String expect_status; - int repeat = 1; - u64 epoch_period_ms = 50; - bool show_body = true; - - for(int i = 1; i < argc; i++) - { - String arg = argv[i]; - if(arg == "--core") cfg.core_wasm_path = arg_value(argc, argv, i, "--core"); - else if(arg == "--site") cfg.site_root = absolute_path(arg_value(argc, argv, i, "--site")); - else if(arg == "--cache") cfg.cache_root = arg_value(argc, argv, i, "--cache"); - else if(arg == "--page") page = arg_value(argc, argv, i, "--page"); - else if(arg == "--route") route = arg_value(argc, argv, i, "--route"); - else if(arg == "--repeat") repeat = atoi(arg_value(argc, argv, i, "--repeat").c_str()); - else if(arg == "--expect") expects.push_back(arg_value(argc, argv, i, "--expect")); - else if(arg == "--expect-status") expect_status = arg_value(argc, argv, i, "--expect-status"); - else if(arg == "--epoch-ticks") cfg.epoch_deadline_ticks = strtoull(arg_value(argc, argv, i, "--epoch-ticks").c_str(), 0, 10); - else if(arg == "--epoch-ms") epoch_period_ms = strtoull(arg_value(argc, argv, i, "--epoch-ms").c_str(), 0, 10); - else if(arg == "--mem-limit") cfg.memory_limit = strtoll(arg_value(argc, argv, i, "--mem-limit").c_str(), 0, 10); - else if(arg == "--table-headroom") cfg.table_headroom = (u32)atoi(arg_value(argc, argv, i, "--table-headroom").c_str()); - else if(arg == "--quiet") show_body = false; - else if(arg == "--verbose") cfg.verbose = true; - else if(arg == "--param" || arg == "--get") - { - String pair = arg_value(argc, argv, i, arg.c_str()); - auto eq = pair.find("="); - if(eq == String::npos) - { - fprintf(stderr, "%s expects K=V\n", arg.c_str()); - return(2); - } - (arg == "--param" ? params : get_params)[pair.substr(0, eq)] = pair.substr(eq + 1); - } - else - { - fprintf(stderr, "unknown argument: %s\n", arg.c_str()); - return(2); - } - } - - if(page == "") - { - fprintf(stderr, "usage: w3_driver --page /demo/hello.uce [--site site] [--cache /tmp/uce/work]\n" - " [--core bin/wasm/core.wasm] [--param K=V ...] [--get K=V ...] [--route path]\n" - " [--repeat N] [--expect STR ...] [--expect-status STR] [--quiet] [--verbose]\n" - " [--epoch-ticks N] [--epoch-ms N] [--mem-limit BYTES]\n"); - return(2); - } - - String entry_unit = cfg.site_root + page; - WasmWorker worker(cfg); - String init_error = worker.init(); - if(init_error != "") - { - fprintf(stderr, "FAIL: %s\n", init_error.c_str()); - return(1); - } - - // the epoch only advances through this ticker; period × deadline-ticks - // is the per-request CPU budget - std::atomic running(true); - std::thread ticker([&] { - while(running.load()) - { - std::this_thread::sleep_for(std::chrono::milliseconds(epoch_period_ms)); - worker.engine.increment_epoch(); - } - }); - - DValue context_tree; - for(auto& entry : params) - context_tree["params"][entry.first] = entry.second; - if(context_tree.key("params") == 0 || !params.count("HTTP_HOST")) - context_tree["params"]["HTTP_HOST"] = "w3.test"; - context_tree["params"]["SCRIPT_URL"] = page; - context_tree["params"]["REQUEST_METHOD"] = "GET"; - for(auto& entry : get_params) - context_tree["get"][entry.first] = entry.second; - if(route != "") - { - context_tree["route"]["l_path"] = route; - context_tree["params"]["ROUTE_PATH"] = route; - } - context_tree["entry_unit"] = entry_unit; - - bool all_ok = true; - WasmResponse last; - for(int request = 0; request < repeat && all_ok; request++) - { - f64 started = (f64)clock() / CLOCKS_PER_SEC; - last = wasm_worker_serve(worker, context_tree, entry_unit); - f64 elapsed_ms = ((f64)clock() / CLOCKS_PER_SEC - started) * 1000.0; - printf("==== request %d/%d (%.1f ms cpu) ====\n", request + 1, repeat, elapsed_ms); - if(!last.ok) - { - printf("ERROR:\n%s\n", last.error.c_str()); - all_ok = false; - break; - } - printf("status: %s\n", last.meta["status"].to_string().c_str()); - if(last.meta.key("headers")) - last.meta["headers"].each([](const DValue& value, String key) { - printf("header: %s: %s\n", key.c_str(), value.to_string().c_str()); - }); - if(show_body) - printf("---- body (%zu bytes) ----\n%.*s\n----\n", - last.body.size(), (int)last.body.size(), last.body.data()); - else - printf("body: %zu bytes\n", last.body.size()); - } - - running.store(false); - ticker.join(); - - if(all_ok && expect_status != "" && last.meta["status"].to_string().find(expect_status) == String::npos) - { - printf("FAIL: status %s does not contain %s\n", last.meta["status"].to_string().c_str(), expect_status.c_str()); - all_ok = false; - } - for(auto& expect : expects) - if(all_ok && last.body.find(expect) == String::npos) - { - printf("FAIL: body does not contain %s\n", expect.c_str()); - all_ok = false; - } - - printf(all_ok ? "W3 RESULT: PASS\n" : "W3 RESULT: FAIL\n"); - return(all_ok ? 0 : 1); -} diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index ba4b2f5..03b5f2c 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -8,8 +8,7 @@ // // Designed for amalgamation-include (like uce_lib.cpp): the including TU must // provide String/DValue/ucb_encode/ucb_decode (types.cpp + dvalue.cpp) and -// wasm_trace.h. W4 includes this from the FastCGI worker build; the W3 CLI -// driver (w3_driver.cpp) is the first consumer. +// wasm_trace.h. The production FastCGI worker includes this through backend.cpp. // // Loader rules carried from the spike phases, now enforced as policy: // - import discipline: units may import only env / GOT.mem / GOT.func @@ -117,6 +116,14 @@ struct WasmUnitModuleLoadProfile u64 classify_us = 0; }; +struct WasmInstancePreDeleter +{ + void operator()(wasmtime_instance_pre_t* instance_pre) const + { + wasmtime_instance_pre_delete(instance_pre); + } +}; + struct WasmWorkerConfig { String core_wasm_path = "bin/wasm/core.wasm"; @@ -602,8 +609,8 @@ static bool uce_job_cancel_value(u64 id) // ---- module byte parsing (hardened; carried from the phase 3 spike) ------- -// included into both w3_driver.cpp and the native server TU (via backend.cpp); -// file-scope helpers are static so each TU gets its own copy with no clash +// Included into the native server TU through backend.cpp. File-scope helpers +// remain static to keep their linkage local to the wasm backend object. static bool wasm_read_uleb(const std::vector& buf, size_t& pos, size_t end, u64& out) { out = 0; @@ -965,6 +972,7 @@ public: wasmtime::Engine engine; std::optional core_module; std::optional core_linker; + std::unique_ptr core_instance_pre; struct CoreImport { String module; @@ -1411,38 +1419,75 @@ public: return(error); worker.core_imports.push_back({ mod, name, false }); } - } - if(!worker.core_table_imported) - return("core does not import __indirect_function_table — rebuild core with --import-table"); - std::vector imports; - u32 total = worker.core_table_min + worker.cfg.table_headroom; - wasmtime::TableType table_type(wasmtime::ValType::funcref(), total, total); - auto table_created = wasmtime::Table::create(cx, table_type, wasmtime::Val(std::optional())); - if(!table_created) - return("table create failed: " + String(table_created.err().message())); - table.emplace(table_created.ok()); - table_next_free = worker.core_table_min; - for(auto& import : worker.core_imports) - { - if(import.table) + if(!worker.core_table_imported) { - imports.push_back(*table); - continue; + wasmtime_instance_pre_t* instance_pre = 0; + wasmtime_error_t* pre_error = wasmtime_linker_instantiate_pre( + worker.core_linker->capi(), module.capi(), &instance_pre); + if(pre_error) + return("core pre-instantiation failed: " + String(wasmtime::Error(pre_error).message())); + worker.core_instance_pre.reset(instance_pre); + } + } + std::vector imports; + if(worker.core_table_imported) + { + u32 total = worker.core_table_min + worker.cfg.table_headroom; + wasmtime::TableType table_type(wasmtime::ValType::funcref(), total, total); + auto table_created = wasmtime::Table::create(cx, table_type, wasmtime::Val(std::optional())); + if(!table_created) + return("table create failed: " + String(table_created.err().message())); + table.emplace(table_created.ok()); + table_next_free = worker.core_table_min; + for(auto& import : worker.core_imports) + { + if(import.table) + { + imports.push_back(*table); + continue; + } + auto host_import = worker.core_linker->get(cx, import.module, import.name); + if(!host_import) + return("core host import unavailable: " + import.module + "." + import.name); + imports.push_back(*host_import); } - auto host_import = worker.core_linker->get(cx, import.module, import.name); - if(!host_import) - return("core host import unavailable: " + import.module + "." + import.name); - imports.push_back(*host_import); } hostcall_operation_slots = worker.hostcall_operation_names.size(); birth_import_us = phase_us(); - auto created = wasmtime::Instance::create(cx, module, imports); - if(!created) - return("core instantiation failed: " + trap_text(created.err())); - core.emplace(created.ok()); + if(worker.core_instance_pre) + { + wasmtime_instance_t instance; + wasm_trap_t* trap = 0; + wasmtime_error_t* instantiate_error = wasmtime_instance_pre_instantiate( + worker.core_instance_pre.get(), cx.capi(), &instance, &trap); + if(instantiate_error) + return("core instantiation failed: " + String(wasmtime::Error(instantiate_error).message())); + if(trap) + return("core instantiation trapped: " + String(wasmtime::Trap(trap).message())); + core.emplace(instance); + } + else + { + auto created = wasmtime::Instance::create(cx, module, imports); + if(!created) + return("core instantiation failed: " + trap_text(created.err())); + core.emplace(created.ok()); + } birth_instantiate_us = phase_us(); + if(!table) + { + auto exported_table = core->get(cx, "__indirect_function_table"); + if(!exported_table || !std::get_if(&*exported_table)) + return("core does not export __indirect_function_table"); + table.emplace(std::get(*exported_table)); + auto grown = table->grow(cx, worker.cfg.table_headroom, wasmtime::Val(std::optional())); + if(!grown) + return("table grow failed: " + String(grown.err().message())); + table_next_free = (u32)grown.ok(); + } + auto exported_memory = core->get(cx, "memory"); if(!exported_memory || !std::get_if(&*exported_memory)) return("core does not export memory"); @@ -3706,6 +3751,12 @@ private: } }; +inline String wasm_worker_prepare(WasmWorker& worker) +{ + WasmWorkspace workspace(worker); + return(workspace.birth()); +} + // ---- public entry: one request through one workspace ----------------------- inline WasmResponse wasm_worker_serve(WasmWorker& worker, const DValue& context_tree, const String& entry_source_path,