Reuse core host definitions across requests

This commit is contained in:
udo
2026-07-18 00:55:29 +00:00
parent 1ebf94b135
commit 6e4ef8ed7a
8 changed files with 212 additions and 56 deletions
+10 -1
View File
@@ -218,7 +218,7 @@ 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)` the membrane wired in. `wasm_worker_serve(worker, ctx, entry_unit, handler)`
(`src/wasm/worker.cpp`) is the single entry point for *every* mode: (`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 workspace for each request; 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 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.
2. Resolve `entry_unit` + `handler` to an export; components referenced at 2. Resolve `entry_unit` + `handler` to an export; components referenced at
runtime are resolved on demand via the `uce_host_component_resolve` hostcall runtime are resolved on demand via the `uce_host_component_resolve` hostcall
(`component_resolve()``__uce_<...>` slot), loading dependency modules (`component_resolve()``__uce_<...>` slot), loading dependency modules
@@ -240,6 +240,15 @@ module cache, adding a visible cold-start request. Request state remains bounded
by the fresh workspace and the normal per-request database/resource cleanup; by the fresh workspace and the normal per-request database/resource cleanup;
faulted workers still exit and are replaced by the parent. faulted workers still exit and are replaced by the parent.
The request context remains UCEB2 so application `context.call` semantics do
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,
and context transfer into bytes, host encode, guest allocation/write,
guest decode/application, and free.
### Task callbacks and workspace lifetime ### Task callbacks and workspace lifetime
`task()` and `task_repeat()` are fork-backed. The `uce_host_task_spawn` hostcall `task()` and `task_repeat()` are fork-backed. The `uce_host_task_spawn` hostcall
+2
View File
@@ -23,6 +23,8 @@ Successful first loads within the request are counted by `unit_load_count` and d
The request log's `wasm-ready`, `wasm`, `workspace`, `invoke`, `collect`, and `post` fields complete the timing boundary after this in-page snapshot: compiled-artifact readiness before backend entry, native backend wall time, complete workspace wall time, entry invocation, guest-output/meta collection, and native response assembly after the backend. They do not expose request data. This distinction is useful when a completed FastCGI request takes longer than the page's mid-render `request_perf()` snapshot. The request log's `wasm-ready`, `wasm`, `workspace`, `invoke`, `collect`, and `post` fields complete the timing boundary after this in-page snapshot: compiled-artifact readiness before backend entry, native backend wall time, complete workspace wall time, entry invocation, guest-output/meta collection, and native response assembly after the backend. They do not expose request data. This distinction is useful when a completed FastCGI request takes longer than the page's mid-render `request_perf()` snapshot.
Workspace birth is subdivided into `birth_policy_us`, `birth_import_us`, `birth_instantiate_us`, `birth_exports_us`, and `birth_initialize_us`. Request-context transfer reports `context_bytes`, `context_encode_us`, `context_allocate_us`, `context_write_us`, `context_guest_apply_us`, and `context_free_us`. These bounded aggregate fields expose sizes and timing only, never request values.
Wasm FastCGI workers retain up to `MYSQL_PERSISTENT_POOL_SIZE` credential-keyed MySQL connections (default `8`; set `0` to disable). UCE calls the client library's connection-reset operation before another request receives a cached connection, clearing transactions, temporary tables, session variables, and selected databases while avoiding a new authentication handshake. Same-request leases continue to share state until request cleanup. Wasm FastCGI workers retain up to `MYSQL_PERSISTENT_POOL_SIZE` credential-keyed MySQL connections (default `8`; set `0` to disable). UCE calls the client library's connection-reset operation before another request receives a cached connection, clearing transactions, temporary tables, session variables, and selected databases while avoiding a new authentication handshake. Same-request leases continue to share state until request cleanup.
:example :example
+10
View File
@@ -86,6 +86,16 @@ RENDER(Request& context)
DValue nav_dash_summary = nav_dash.filter({"title"}); DValue nav_dash_summary = nav_dash.filter({"title"});
check("DValue collection helpers", empty_pop.to_string() == "" && app_nav.is_list() && nav_titles["0"].to_string() == "Dashboard" && join(nav_dash_summary.keys(), ",") == "title" && nav_dash.values()["0"].to_string() == "app" && nav_dash.values()["1"].to_string() == "Dashboard", json_encode(nav_titles)); check("DValue collection helpers", empty_pop.to_string() == "" && app_nav.is_list() && nav_titles["0"].to_string() == "Dashboard" && join(nav_dash_summary.keys(), ",") == "title" && nav_dash.values()["0"].to_string() == "app" && nav_dash.values()["1"].to_string() == "Dashboard", json_encode(nav_titles));
DValue copy_source; copy_source["nested"]["value"] = "original";
DValue copied = copy_source;
copied["nested"]["value"] = "copy";
DValue move_source; move_source["nested"]["value"] = "moved";
DValue moved = std::move(move_source);
DValue reference_target; DValue reference; reference.set_reference(&reference_target);
DValue reference_source; reference_source["nested"]["value"] = "target";
reference = reference_source;
check("DValue copy/move assignment preserves value and reference semantics", copy_source["nested"]["value"].to_string() == "original" && copied["nested"]["value"].to_string() == "copy" && moved["nested"]["value"].to_string() == "moved" && reference_target["nested"]["value"].to_string() == "target", json_encode(moved) + " / " + json_encode(reference_target));
DValue conv; DValue conv;
conv["name"] = "ada"; conv["name"] = "ada";
conv["count"] = "12"; conv["count"] = "12";
+3 -1
View File
@@ -162,7 +162,9 @@ RENDER(Request& context)
bool unit_module_profile_valid = serialized_reads_bounded && unit_operation_profile_valid && perf["unit_module_cache_hit_count"].to_u64() + module_misses == perf["unit_load_count"].to_u64() && serialized_hits + perf["unit_module_compile_count"].to_u64() == module_misses && perf["unit_module_lookup_us"].type != 'S' && perf["unit_module_read_us"].type != 'S' && perf["unit_module_read_bytes"].type != 'S' && perf["unit_module_parse_us"].type != 'S' && perf["unit_module_compile_us"].type != 'S' && perf["unit_module_classify_us"].type != 'S'; bool unit_module_profile_valid = serialized_reads_bounded && unit_operation_profile_valid && perf["unit_module_cache_hit_count"].to_u64() + module_misses == perf["unit_load_count"].to_u64() && serialized_hits + perf["unit_module_compile_count"].to_u64() == module_misses && perf["unit_module_lookup_us"].type != 'S' && perf["unit_module_read_us"].type != 'S' && perf["unit_module_read_bytes"].type != 'S' && perf["unit_module_parse_us"].type != 'S' && perf["unit_module_compile_us"].type != 'S' && perf["unit_module_classify_us"].type != 'S';
bool transport_profile_valid = perf["transport_params_us"].type != 'S' && perf["transport_input_us"].type != 'S' && perf["handler_queue_us"].type != 'S' && perf["accept_us"].to_f64() + 2 >= perf["transport_params_us"].to_f64() + perf["transport_input_us"].to_f64() + perf["handler_queue_us"].to_f64(); bool transport_profile_valid = perf["transport_params_us"].type != 'S' && perf["transport_input_us"].type != 'S' && perf["handler_queue_us"].type != 'S' && perf["accept_us"].to_f64() + 2 >= perf["transport_params_us"].to_f64() + perf["transport_input_us"].to_f64() + perf["handler_queue_us"].to_f64();
u64 phase_cpu_us = perf["workspace_setup_cpu_us"].to_u64() + perf["workspace_birth_cpu_us"].to_u64() + perf["context_apply_cpu_us"].to_u64(); u64 phase_cpu_us = perf["workspace_setup_cpu_us"].to_u64() + perf["workspace_birth_cpu_us"].to_u64() + perf["context_apply_cpu_us"].to_u64();
bool workspace_cpu_profile_valid = perf["workspace_wall_us"].to_u64() > 0 && perf["workspace_cpu_us"].to_u64() > 0 && perf["workspace_cpu_us"].to_u64() <= perf["workspace_wall_us"].to_u64() + 2 && (perf["workspace_cpu_us"].to_u64() >= perf["workspace_wall_us"].to_u64() ? perf["workspace_wait_us"].to_u64() == 0 : perf["workspace_wait_us"].to_u64() + perf["workspace_cpu_us"].to_u64() == perf["workspace_wall_us"].to_u64()) && perf["workspace_setup_cpu_us"].to_u64() <= perf["workspace_setup_us"].to_u64() + 2 && perf["workspace_birth_cpu_us"].to_u64() <= perf["workspace_birth_us"].to_u64() + 2 && perf["context_apply_cpu_us"].to_u64() <= perf["context_apply_us"].to_u64() + 2 && phase_cpu_us + perf["execution_cpu_us"].to_u64() == perf["workspace_cpu_us"].to_u64(); u64 birth_profile_us = perf["birth_policy_us"].to_u64() + perf["birth_import_us"].to_u64() + perf["birth_instantiate_us"].to_u64() + perf["birth_exports_us"].to_u64() + perf["birth_initialize_us"].to_u64();
u64 context_profile_us = perf["context_encode_us"].to_u64() + perf["context_allocate_us"].to_u64() + perf["context_write_us"].to_u64() + perf["context_guest_apply_us"].to_u64() + perf["context_free_us"].to_u64();
bool workspace_cpu_profile_valid = perf["workspace_wall_us"].to_u64() > 0 && perf["workspace_cpu_us"].to_u64() > 0 && perf["workspace_cpu_us"].to_u64() <= perf["workspace_wall_us"].to_u64() + 2 && (perf["workspace_cpu_us"].to_u64() >= perf["workspace_wall_us"].to_u64() ? perf["workspace_wait_us"].to_u64() == 0 : perf["workspace_wait_us"].to_u64() + perf["workspace_cpu_us"].to_u64() == perf["workspace_wall_us"].to_u64()) && perf["workspace_setup_cpu_us"].to_u64() <= perf["workspace_setup_us"].to_u64() + 2 && perf["workspace_birth_cpu_us"].to_u64() <= perf["workspace_birth_us"].to_u64() + 2 && perf["context_apply_cpu_us"].to_u64() <= perf["context_apply_us"].to_u64() + 2 && birth_profile_us <= perf["workspace_birth_us"].to_u64() && perf["context_bytes"].to_u64() > 0 && context_profile_us <= perf["context_apply_us"].to_u64() && phase_cpu_us + perf["execution_cpu_us"].to_u64() == perf["workspace_cpu_us"].to_u64();
u64 operation_hostcalls = 0; u64 operation_hostcalls = 0;
u64 operation_hostcall_us = 0; u64 operation_hostcall_us = 0;
u64 operation_hostcall_cpu_us = 0; u64 operation_hostcall_cpu_us = 0;
+40 -4
View File
@@ -652,7 +652,7 @@ void DValue::set_bool(bool b)
_list_mode = false; _list_mode = false;
} }
void DValue::set(DValue source) void DValue::set(const DValue& source)
{ {
DValue* target = reference_target(); DValue* target = reference_target();
if(target) if(target)
@@ -691,6 +691,42 @@ void DValue::set(DValue source)
} }
} }
void DValue::set(DValue&& source)
{
DValue* target = reference_target();
if(target)
{
target->set(std::move(source));
return;
}
set_type(source.type);
switch(type)
{
case('S'):
_String = std::move(source._String);
_list_mode = false;
break;
case('F'):
_float = source._float;
_list_mode = false;
break;
case('B'):
_bool = source._bool;
_list_mode = false;
break;
case('M'):
_map = std::move(source._map);
_array_index = source._array_index;
_list_mode = source._list_mode;
break;
case('P'):
case('R'):
_ptr = source._ptr;
_list_mode = false;
break;
}
}
void DValue::set(StringMap source) void DValue::set(StringMap source)
{ {
DValue* target = reference_target(); DValue* target = reference_target();
@@ -782,7 +818,7 @@ DValue& DValue::operator [] (String s) {
void DValue::operator = (String v) { set(v); } void DValue::operator = (String v) { set(v); }
void DValue::operator = (f64 v) { set(v); } void DValue::operator = (f64 v) { set(v); }
void DValue::operator = (void* v) { set(v); } void DValue::operator = (void* v) { set(v); }
void DValue::operator = (DValue v) { set(v); } void DValue::operator = (DValue v) { set(std::move(v)); }
void DValue::operator = (StringMap v) { set(v); } void DValue::operator = (StringMap v) { set(v); }
void DValue::push(const DValue& child) void DValue::push(const DValue& child)
@@ -1076,7 +1112,7 @@ bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& err
DValue child; DValue child;
if(!ucb_decode_node(src, offset, child, error, depth + 1)) if(!ucb_decode_node(src, offset, child, error, depth + 1))
return(false); return(false);
out[key] = child; out[key] = std::move(child);
} }
return(true); return(true);
} }
@@ -1126,7 +1162,7 @@ bool ucb_decode(const String& encoded, DValue& out, String* error_out)
DValue decoded; DValue decoded;
if(ucb_decode_node(encoded, offset, decoded, error) && offset == encoded.size()) if(ucb_decode_node(encoded, offset, decoded, error) && offset == encoded.size())
{ {
out = decoded; out = std::move(decoded);
if(error_out) if(error_out)
*error_out = ""; *error_out = "";
return(true); return(true);
+5 -1
View File
@@ -8,6 +8,9 @@ String json_escape(String s, char quote_char = '"');
// JSON-decoded values, and metadata trees can be consumed without repetitive // JSON-decoded values, and metadata trees can be consumed without repetitive
// manual parsing at each call site. // manual parsing at each call site.
struct DValue { struct DValue {
DValue() = default;
DValue(const DValue&) = default;
DValue(DValue&&) = default;
char type = 'S'; char type = 'S';
@@ -49,7 +52,8 @@ struct DValue {
void set(void* p); void set(void* p);
void set(f64 f); void set(f64 f);
void set_bool(bool b); void set_bool(bool b);
void set(DValue source); void set(const DValue& source);
void set(DValue&& source);
void set(StringMap source); void set(StringMap source);
void set_array(); void set_array();
void set_reference(DValue* target); void set_reference(DValue* target);
+15 -14
View File
@@ -818,7 +818,8 @@ int uce_wasm_apply_context(const char* buf, size_t len)
uce_host_log(3, error.data(), error.size()); uce_host_log(3, error.data(), error.size());
return(1); return(1);
} }
wasm_request.call = decoded; wasm_request.call = std::move(decoded);
DValue& applied = wasm_request.call;
auto apply_map = [](DValue* source, StringMap& dest) { auto apply_map = [](DValue* source, StringMap& dest) {
dest.clear(); dest.clear();
if(source) if(source)
@@ -826,23 +827,23 @@ int uce_wasm_apply_context(const char* buf, size_t len)
dest[key] = item.to_string(); dest[key] = item.to_string();
}); });
}; };
if(decoded.key("server_config")) if(applied.key("server_config"))
apply_map(decoded.key("server_config"), wasm_server.config); apply_map(applied.key("server_config"), wasm_server.config);
apply_map(decoded.key("params"), wasm_request.params); apply_map(applied.key("params"), wasm_request.params);
apply_map(decoded.key("get"), wasm_request.get); apply_map(applied.key("get"), wasm_request.get);
apply_map(decoded.key("post"), wasm_request.post); apply_map(applied.key("post"), wasm_request.post);
apply_map(decoded.key("cookies"), wasm_request.cookies); apply_map(applied.key("cookies"), wasm_request.cookies);
apply_map(decoded.key("session"), wasm_request.session); apply_map(applied.key("session"), wasm_request.session);
if(DValue* session_id = decoded.key("session_id")) if(DValue* session_id = applied.key("session_id"))
wasm_request.session_id = session_id->to_string(); wasm_request.session_id = session_id->to_string();
if(DValue* session_name = decoded.key("session_name")) if(DValue* session_name = applied.key("session_name"))
wasm_request.session_name = session_name->to_string(); wasm_request.session_name = session_name->to_string();
if(DValue* session_loaded_hash = decoded.key("session_loaded_hash")) if(DValue* session_loaded_hash = applied.key("session_loaded_hash"))
wasm_request.session_loaded_hash = session_loaded_hash->to_string(); wasm_request.session_loaded_hash = session_loaded_hash->to_string();
DValue* entry = decoded.key("entry_unit"); DValue* entry = applied.key("entry_unit");
if(entry) if(entry)
wasm_request.resources.current_unit_file = entry->to_string(); wasm_request.resources.current_unit_file = entry->to_string();
DValue* raw_in = decoded.key("in"); DValue* raw_in = applied.key("in");
wasm_request.in = raw_in ? raw_in->to_string() : ""; wasm_request.in = raw_in ? raw_in->to_string() : "";
// websocket event context: ws_send()/ws_close() capture into the dispatch // websocket event context: ws_send()/ws_close() capture into the dispatch
// list (the workspace owns no connections), which collect() carries back to // list (the workspace owns no connections), which collect() carries back to
@@ -851,7 +852,7 @@ int uce_wasm_apply_context(const char* buf, size_t len)
wasm_request.resources.websocket_connection_state_before = DValue(); wasm_request.resources.websocket_connection_state_before = DValue();
wasm_request.resources.websocket_dispatch_commands = DValue(); wasm_request.resources.websocket_dispatch_commands = DValue();
wasm_request.resources.websocket_dispatch_capture = false; wasm_request.resources.websocket_dispatch_capture = false;
DValue* ws = decoded.key("ws"); DValue* ws = applied.key("ws");
if(ws) if(ws)
{ {
wasm_request.resources.websocket_connection_id = (*ws)["connection_id"].to_string(); wasm_request.resources.websocket_connection_id = (*ws)["connection_id"].to_string();
+127 -35
View File
@@ -172,8 +172,19 @@ struct WasmRequestProfile
u64 workspace_setup_cpu_us = 0; u64 workspace_setup_cpu_us = 0;
u64 workspace_birth_us = 0; u64 workspace_birth_us = 0;
u64 workspace_birth_cpu_us = 0; u64 workspace_birth_cpu_us = 0;
u64 birth_policy_us = 0;
u64 birth_import_us = 0;
u64 birth_instantiate_us = 0;
u64 birth_exports_us = 0;
u64 birth_initialize_us = 0;
u64 context_apply_us = 0; u64 context_apply_us = 0;
u64 context_apply_cpu_us = 0; u64 context_apply_cpu_us = 0;
u64 context_bytes = 0;
u64 context_encode_us = 0;
u64 context_allocate_us = 0;
u64 context_write_us = 0;
u64 context_guest_apply_us = 0;
u64 context_free_us = 0;
u64 entry_invoke_us = 0; u64 entry_invoke_us = 0;
u64 output_collect_us = 0; u64 output_collect_us = 0;
u64 workspace_complete_us = 0; u64 workspace_complete_us = 0;
@@ -862,6 +873,8 @@ static bool wasm_read_metadata_file(const String& path, std::vector<u8>& metadat
// ---- worker (per process): engine + compiled module caches ---------------- // ---- worker (per process): engine + compiled module caches ----------------
class WasmWorkspace;
class WasmWorker class WasmWorker
{ {
public: public:
@@ -951,6 +964,21 @@ public:
wasmtime::Engine engine; wasmtime::Engine engine;
std::optional<wasmtime::Module> core_module; std::optional<wasmtime::Module> core_module;
std::optional<wasmtime::Linker> core_linker;
struct CoreImport
{
String module;
String name;
bool table = false;
};
std::vector<CoreImport> core_imports;
u32 core_table_min = 0;
bool core_table_imported = false;
u64 core_host_import_count = 0;
// A render worker dispatches one workspace at a time. Persistent host Func
// definitions resolve their request-owned state through this pointer; forked
// task children inherit the live workspace copy before the parent drops it.
WasmWorkspace* active_workspace = 0;
WasmDylinkInfo core_dylink; WasmDylinkInfo core_dylink;
// unit artifact path in the W2 cache: cache_root + <abs source dir> + /<file>.wasm // unit artifact path in the W2 cache: cache_root + <abs source dir> + /<file>.wasm
@@ -1257,6 +1285,7 @@ public:
explicit WasmWorkspace(WasmWorker& w) : worker(w), store(w.engine) explicit WasmWorkspace(WasmWorker& w) : worker(w), store(w.engine)
{ {
worker.active_workspace = this;
} }
void set_perf_snapshot(u64 worker_pid, u64 parent_pid, u64 request_count, void set_perf_snapshot(u64 worker_pid, u64 parent_pid, u64 request_count,
@@ -1287,6 +1316,8 @@ public:
#endif #endif
~WasmWorkspace() ~WasmWorkspace()
{ {
if(worker.active_workspace == this)
worker.active_workspace = 0;
for(auto& h : file_handles) for(auto& h : file_handles)
{ {
if(h.fd >= 0) if(h.fd >= 0)
@@ -1341,50 +1372,82 @@ public:
String birth() String birth()
{ {
auto phase_start = std::chrono::steady_clock::now();
auto phase_us = [&]() -> u64 {
auto now = std::chrono::steady_clock::now();
u64 elapsed = (u64)std::chrono::duration_cast<std::chrono::microseconds>(now - phase_start).count();
phase_start = now;
return(elapsed);
};
auto cx = ctx(); auto cx = ctx();
store.limiter(worker.cfg.memory_limit, -1, -1, -1, -1); store.limiter(worker.cfg.memory_limit, -1, -1, -1, -1);
cx.set_epoch_deadline(worker.cfg.epoch_deadline_ticks); cx.set_epoch_deadline(worker.cfg.epoch_deadline_ticks);
birth_policy_us = phase_us();
auto& module = *worker.core_module; auto& module = *worker.core_module;
std::vector<wasmtime::Extern> imports; if(!worker.core_linker)
for(auto import_type : module.imports())
{ {
String mod(import_type.module()); worker.core_linker.emplace(worker.engine);
String name(import_type.name()); for(auto import_type : module.imports())
auto extern_type = wasmtime::ExternType::from_import(import_type); {
if(auto* table_ref = std::get_if<wasmtime::TableType::Ref>(&extern_type)) String mod(import_type.module());
String name(import_type.name());
auto extern_type = wasmtime::ExternType::from_import(import_type);
if(auto* table_ref = std::get_if<wasmtime::TableType::Ref>(&extern_type))
{
if(name.rfind("__indirect_function_table", 0) != 0)
return("core imports unexpected table " + mod + "." + name);
worker.core_table_min = table_ref->min();
worker.core_table_imported = true;
worker.core_imports.push_back({ mod, name, true });
continue;
}
auto* func_ref = std::get_if<wasmtime::FuncType::Ref>(&extern_type);
if(!func_ref)
return("core has unexpected non-func import " + mod + "." + name);
wasmtime::FuncType func_type(*func_ref);
String error = make_host_import(*worker.core_linker, mod, name, func_type);
if(error != "")
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(name.rfind("__indirect_function_table", 0) != 0)
return("core imports unexpected table " + mod + "." + name);
u32 core_min = table_ref->min();
u32 total = core_min + worker.cfg.table_headroom;
wasmtime::TableType table_type(wasmtime::ValType::funcref(), total, total);
auto created = wasmtime::Table::create(cx, table_type, wasmtime::Val(std::optional<wasmtime::Func>()));
if(!created)
return("table create failed: " + String(created.err().message()));
table.emplace(created.ok());
table_next_free = core_min;
imports.push_back(*table); imports.push_back(*table);
continue; continue;
} }
auto* func_ref = std::get_if<wasmtime::FuncType::Ref>(&extern_type); auto host_import = worker.core_linker->get(cx, import.module, import.name);
if(!func_ref) if(!host_import)
return("core has unexpected non-func import " + mod + "." + name); return("core host import unavailable: " + import.module + "." + import.name);
wasmtime::FuncType func_type(*func_ref); imports.push_back(*host_import);
imports.push_back(make_host_import(cx, mod, name, func_type));
} }
if(!table) hostcall_operation_slots = worker.hostcall_operation_names.size();
return("core does not import __indirect_function_table — rebuild core with --import-table"); birth_import_us = phase_us();
auto created = wasmtime::Instance::create(cx, module, imports); auto created = wasmtime::Instance::create(cx, module, imports);
if(!created) if(!created)
return("core instantiation failed: " + trap_text(created.err())); return("core instantiation failed: " + trap_text(created.err()));
core.emplace(created.ok()); core.emplace(created.ok());
birth_instantiate_us = phase_us();
auto exported_memory = core->get(cx, "memory"); auto exported_memory = core->get(cx, "memory");
if(!exported_memory || !std::get_if<wasmtime::Memory>(&*exported_memory)) if(!exported_memory || !std::get_if<wasmtime::Memory>(&*exported_memory))
return("core does not export memory"); return("core does not export memory");
memory.emplace(std::get<wasmtime::Memory>(*exported_memory)); memory.emplace(std::get<wasmtime::Memory>(*exported_memory));
birth_exports_us = phase_us();
String error = call_core("_initialize", {}, 0); String error = call_core("_initialize", {}, 0);
if(error != "") if(error != "")
@@ -1403,6 +1466,7 @@ public:
error = call_core("uce_wasm_request", {}, &request_ptr); error = call_core("uce_wasm_request", {}, &request_ptr);
if(error != "") if(error != "")
return(error); return(error);
birth_initialize_us = phase_us();
if(request_ptr == 0) if(request_ptr == 0)
return("core returned null Request*"); return("core returned null Request*");
return(""); return("");
@@ -1410,21 +1474,34 @@ public:
String apply_context(const DValue& context_tree) String apply_context(const DValue& context_tree)
{ {
auto phase_start = std::chrono::steady_clock::now();
auto phase_us = [&]() -> u64 {
auto now = std::chrono::steady_clock::now();
u64 elapsed = (u64)std::chrono::duration_cast<std::chrono::microseconds>(now - phase_start).count();
phase_start = now;
return(elapsed);
};
String encoded = ucb_encode(context_tree); String encoded = ucb_encode(context_tree);
context_bytes = encoded.size();
context_encode_us = phase_us();
int32_t guest_ptr = 0; int32_t guest_ptr = 0;
String error = call_core("uce_alloc", { (int32_t)encoded.size() }, &guest_ptr); String error = call_core("uce_alloc", { (int32_t)encoded.size() }, &guest_ptr);
context_allocate_us = phase_us();
if(error != "") if(error != "")
return(error); return(error);
if(guest_ptr == 0) if(guest_ptr == 0)
return("guest uce_alloc failed for context buffer"); return("guest uce_alloc failed for context buffer");
error = guest_write((u32)guest_ptr, encoded); error = guest_write((u32)guest_ptr, encoded);
context_write_us = phase_us();
if(error != "") if(error != "")
return(error); return(error);
int32_t rc = 0; int32_t rc = 0;
error = call_core("uce_wasm_apply_context", { guest_ptr, (int32_t)encoded.size() }, &rc); error = call_core("uce_wasm_apply_context", { guest_ptr, (int32_t)encoded.size() }, &rc);
context_guest_apply_us = phase_us();
if(error != "") if(error != "")
return(error); return(error);
call_core("uce_free", { guest_ptr }, 0); call_core("uce_free", { guest_ptr }, 0);
context_free_us = phase_us();
if(rc != 0) if(rc != 0)
return("uce_wasm_apply_context returned " + std::to_string(rc)); return("uce_wasm_apply_context returned " + std::to_string(rc));
return(""); return("");
@@ -1532,7 +1609,6 @@ private:
std::vector<LoadedUnit> units; std::vector<LoadedUnit> units;
std::map<String, size_t> units_by_source; std::map<String, size_t> units_by_source;
std::map<String, u32> handler_slots; // source + ":" + symbol → table slot std::map<String, u32> handler_slots; // source + ":" + symbol → table slot
std::vector<wasmtime::Func> host_funcs; // keep host imports alive
bool stale_component_mutation_blocked = false; bool stale_component_mutation_blocked = false;
String stale_component_mutation_status; String stale_component_mutation_status;
bool component_source_generation_checked = false; bool component_source_generation_checked = false;
@@ -2315,18 +2391,22 @@ private:
// ---- host imports for the core ----------------------------------------- // ---- host imports for the core -----------------------------------------
wasmtime::Extern make_host_import(wasmtime::Store::Context cx, const String& mod, const String& name, const wasmtime::FuncType& func_type) String make_host_import(wasmtime::Linker& linker, const String& mod, const String& name, const wasmtime::FuncType& func_type)
{ {
using namespace wasmtime; using namespace wasmtime;
WasmWorkspace* self = this; struct ActiveWorkspace
{
WasmWorker* worker;
WasmWorkspace* operator->() const { return(worker->active_workspace); }
};
ActiveWorkspace self = { &worker };
auto add = [&](auto&& callback) -> Extern { auto add = [&](auto&& callback) -> String {
u64 profile_index = self->hostcall_operation_slots; u64 profile_index = worker.core_host_import_count++;
if(profile_index < WasmRequestProfile::HOSTCALL_OPERATION_MAX) if(profile_index < WasmRequestProfile::HOSTCALL_OPERATION_MAX)
{ {
self->hostcall_operation_slots++; if(worker.hostcall_operation_names.size() <= profile_index)
if(self->worker.hostcall_operation_names.size() <= profile_index) worker.hostcall_operation_names.push_back(name);
self->worker.hostcall_operation_names.push_back(name);
} }
bool profile_enabled = name != "uce_host_request_perf"; bool profile_enabled = name != "uce_host_request_perf";
bool profile_mysql = name == "uce_host_mysql"; bool profile_mysql = name == "uce_host_mysql";
@@ -2368,15 +2448,16 @@ private:
} }
return(result); return(result);
}; };
Func func(cx, func_type, profiled); auto defined = linker.func_new(mod, name, func_type, profiled);
host_funcs.push_back(func); if(!defined)
return(host_funcs.back()); return("core host import failed: " + mod + "." + name + ": " + String(defined.err().message()));
return("");
}; };
// Hostcall blocklist (UCE_HOSTCALL_BLOCKLIST): a sysadmin-disabled hostcall // Hostcall blocklist (UCE_HOSTCALL_BLOCKLIST): a sysadmin-disabled hostcall
// resolves to a trap stub instead of its real implementation, so a unit // resolves to a trap stub instead of its real implementation, so a unit
// invoking it fails at runtime into the configurable error page. The // invoking it fails at runtime into the configurable error page. The
// decision is made once per import at workspace birth — no per-call cost, // decision is made once per worker when imports are defined — no per-call cost,
// and zero cost when nothing is blocked. A small core set stays exempt so // and zero cost when nothing is blocked. A small core set stays exempt so
// the runtime itself cannot be bricked. // the runtime itself cannot be bricked.
if(mod == "env" && !worker.cfg.hostcall_blocklist.empty() && name.rfind("uce_host_", 0) == 0) if(mod == "env" && !worker.cfg.hostcall_blocklist.empty() && name.rfind("uce_host_", 0) == 0)
@@ -2437,8 +2518,19 @@ private:
response["workspace_setup_cpu_us"] = (f64)self->workspace_setup_cpu_us; response["workspace_setup_cpu_us"] = (f64)self->workspace_setup_cpu_us;
response["workspace_birth_us"] = (f64)self->workspace_birth_us; response["workspace_birth_us"] = (f64)self->workspace_birth_us;
response["workspace_birth_cpu_us"] = (f64)self->workspace_birth_cpu_us; response["workspace_birth_cpu_us"] = (f64)self->workspace_birth_cpu_us;
response["birth_policy_us"] = (f64)self->birth_policy_us;
response["birth_import_us"] = (f64)self->birth_import_us;
response["birth_instantiate_us"] = (f64)self->birth_instantiate_us;
response["birth_exports_us"] = (f64)self->birth_exports_us;
response["birth_initialize_us"] = (f64)self->birth_initialize_us;
response["context_apply_us"] = (f64)self->context_apply_us; response["context_apply_us"] = (f64)self->context_apply_us;
response["context_apply_cpu_us"] = (f64)self->context_apply_cpu_us; response["context_apply_cpu_us"] = (f64)self->context_apply_cpu_us;
response["context_bytes"] = (f64)self->context_bytes;
response["context_encode_us"] = (f64)self->context_encode_us;
response["context_allocate_us"] = (f64)self->context_allocate_us;
response["context_write_us"] = (f64)self->context_write_us;
response["context_guest_apply_us"] = (f64)self->context_guest_apply_us;
response["context_free_us"] = (f64)self->context_free_us;
u64 phase_cpu_us = self->workspace_setup_cpu_us + self->workspace_birth_cpu_us + self->context_apply_cpu_us; u64 phase_cpu_us = self->workspace_setup_cpu_us + self->workspace_birth_cpu_us + self->context_apply_cpu_us;
response["execution_cpu_us"] = (f64)(workspace_cpu_us > phase_cpu_us ? workspace_cpu_us - phase_cpu_us : 0); response["execution_cpu_us"] = (f64)(workspace_cpu_us > phase_cpu_us ? workspace_cpu_us - phase_cpu_us : 0);
response["hostcall_count"] = (f64)self->hostcall_count; response["hostcall_count"] = (f64)self->hostcall_count;