Pre-instantiate WASM core workers
This commit is contained in:
parent
f89e33e241
commit
72fd0dd53a
@ -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
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 <atomic>
|
||||
#include <thread>
|
||||
|
||||
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<String> 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<bool> 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);
|
||||
}
|
||||
@ -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<u8>& buf, size_t& pos, size_t end, u64& out)
|
||||
{
|
||||
out = 0;
|
||||
@ -965,6 +972,7 @@ public:
|
||||
wasmtime::Engine engine;
|
||||
std::optional<wasmtime::Module> core_module;
|
||||
std::optional<wasmtime::Linker> core_linker;
|
||||
std::unique_ptr<wasmtime_instance_pre_t, WasmInstancePreDeleter> 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<wasmtime::Extern> 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<wasmtime::Func>()));
|
||||
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<wasmtime::Extern> 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<wasmtime::Func>()));
|
||||
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<wasmtime::Table>(&*exported_table))
|
||||
return("core does not export __indirect_function_table");
|
||||
table.emplace(std::get<wasmtime::Table>(*exported_table));
|
||||
auto grown = table->grow(cx, worker.cfg.table_headroom, wasmtime::Val(std::optional<wasmtime::Func>()));
|
||||
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<wasmtime::Memory>(&*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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user