From 6e4ef8ed7a8ab9779c80a0f474f2907abbb1bf3a Mon Sep 17 00:00:00 2001 From: udo Date: Sat, 18 Jul 2026 00:55:29 +0000 Subject: [PATCH] Reuse core host definitions across requests --- docs/wasm-runtime-architecture.md | 11 +- site/doc/pages/request_perf.txt | 2 + site/tests/core.uce | 10 ++ site/tests/io.uce | 4 +- src/lib/dvalue.cpp | 44 +++++++- src/lib/dvalue.h | 6 +- src/wasm/core.cpp | 29 +++--- src/wasm/worker.cpp | 162 +++++++++++++++++++++++------- 8 files changed, 212 insertions(+), 56 deletions(-) diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 2fb23b8..b40bb50 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -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)` (`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 runtime are resolved on demand via the `uce_host_component_resolve` hostcall (`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; 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()` and `task_repeat()` are fork-backed. The `uce_host_task_spawn` hostcall diff --git a/site/doc/pages/request_perf.txt b/site/doc/pages/request_perf.txt index 0baf240..8a96825 100644 --- a/site/doc/pages/request_perf.txt +++ b/site/doc/pages/request_perf.txt @@ -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. +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. :example diff --git a/site/tests/core.uce b/site/tests/core.uce index 5a4e777..eb1d0be 100644 --- a/site/tests/core.uce +++ b/site/tests/core.uce @@ -86,6 +86,16 @@ RENDER(Request& context) 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)); + 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; conv["name"] = "ada"; conv["count"] = "12"; diff --git a/site/tests/io.uce b/site/tests/io.uce index ec6a5e8..3270d36 100644 --- a/site/tests/io.uce +++ b/site/tests/io.uce @@ -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 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(); - 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_hostcall_us = 0; u64 operation_hostcall_cpu_us = 0; diff --git a/src/lib/dvalue.cpp b/src/lib/dvalue.cpp index bbd0b69..22d61cb 100644 --- a/src/lib/dvalue.cpp +++ b/src/lib/dvalue.cpp @@ -652,7 +652,7 @@ void DValue::set_bool(bool b) _list_mode = false; } -void DValue::set(DValue source) +void DValue::set(const DValue& source) { DValue* target = reference_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) { DValue* target = reference_target(); @@ -782,7 +818,7 @@ DValue& DValue::operator [] (String s) { void DValue::operator = (String v) { set(v); } void DValue::operator = (f64 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::push(const DValue& child) @@ -1076,7 +1112,7 @@ bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& err DValue child; if(!ucb_decode_node(src, offset, child, error, depth + 1)) return(false); - out[key] = child; + out[key] = std::move(child); } return(true); } @@ -1126,7 +1162,7 @@ bool ucb_decode(const String& encoded, DValue& out, String* error_out) DValue decoded; if(ucb_decode_node(encoded, offset, decoded, error) && offset == encoded.size()) { - out = decoded; + out = std::move(decoded); if(error_out) *error_out = ""; return(true); diff --git a/src/lib/dvalue.h b/src/lib/dvalue.h index 3c92dc8..98c544e 100644 --- a/src/lib/dvalue.h +++ b/src/lib/dvalue.h @@ -8,6 +8,9 @@ String json_escape(String s, char quote_char = '"'); // JSON-decoded values, and metadata trees can be consumed without repetitive // manual parsing at each call site. struct DValue { + DValue() = default; + DValue(const DValue&) = default; + DValue(DValue&&) = default; char type = 'S'; @@ -49,7 +52,8 @@ struct DValue { void set(void* p); void set(f64 f); void set_bool(bool b); - void set(DValue source); + void set(const DValue& source); + void set(DValue&& source); void set(StringMap source); void set_array(); void set_reference(DValue* target); diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index e4299b1..0f43bda 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -818,7 +818,8 @@ int uce_wasm_apply_context(const char* buf, size_t len) uce_host_log(3, error.data(), error.size()); return(1); } - wasm_request.call = decoded; + wasm_request.call = std::move(decoded); + DValue& applied = wasm_request.call; auto apply_map = [](DValue* source, StringMap& dest) { dest.clear(); if(source) @@ -826,23 +827,23 @@ int uce_wasm_apply_context(const char* buf, size_t len) dest[key] = item.to_string(); }); }; - if(decoded.key("server_config")) - apply_map(decoded.key("server_config"), wasm_server.config); - apply_map(decoded.key("params"), wasm_request.params); - apply_map(decoded.key("get"), wasm_request.get); - apply_map(decoded.key("post"), wasm_request.post); - apply_map(decoded.key("cookies"), wasm_request.cookies); - apply_map(decoded.key("session"), wasm_request.session); - if(DValue* session_id = decoded.key("session_id")) + if(applied.key("server_config")) + apply_map(applied.key("server_config"), wasm_server.config); + apply_map(applied.key("params"), wasm_request.params); + apply_map(applied.key("get"), wasm_request.get); + apply_map(applied.key("post"), wasm_request.post); + apply_map(applied.key("cookies"), wasm_request.cookies); + apply_map(applied.key("session"), wasm_request.session); + if(DValue* session_id = applied.key("session_id")) 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(); - 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(); - DValue* entry = decoded.key("entry_unit"); + DValue* entry = applied.key("entry_unit"); if(entry) 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() : ""; // websocket event context: ws_send()/ws_close() capture into the dispatch // 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_dispatch_commands = DValue(); wasm_request.resources.websocket_dispatch_capture = false; - DValue* ws = decoded.key("ws"); + DValue* ws = applied.key("ws"); if(ws) { wasm_request.resources.websocket_connection_id = (*ws)["connection_id"].to_string(); diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index d60a4a8..ba4b2f5 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -172,8 +172,19 @@ struct WasmRequestProfile u64 workspace_setup_cpu_us = 0; u64 workspace_birth_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_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 output_collect_us = 0; u64 workspace_complete_us = 0; @@ -862,6 +873,8 @@ static bool wasm_read_metadata_file(const String& path, std::vector& metadat // ---- worker (per process): engine + compiled module caches ---------------- +class WasmWorkspace; + class WasmWorker { public: @@ -951,6 +964,21 @@ public: wasmtime::Engine engine; std::optional core_module; + std::optional core_linker; + struct CoreImport + { + String module; + String name; + bool table = false; + }; + std::vector 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; // unit artifact path in the W2 cache: cache_root + + /.wasm @@ -1257,6 +1285,7 @@ public: 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, @@ -1287,6 +1316,8 @@ public: #endif ~WasmWorkspace() { + if(worker.active_workspace == this) + worker.active_workspace = 0; for(auto& h : file_handles) { if(h.fd >= 0) @@ -1341,50 +1372,82 @@ public: 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(now - phase_start).count(); + phase_start = now; + return(elapsed); + }; auto cx = ctx(); store.limiter(worker.cfg.memory_limit, -1, -1, -1, -1); cx.set_epoch_deadline(worker.cfg.epoch_deadline_ticks); + birth_policy_us = phase_us(); auto& module = *worker.core_module; - std::vector imports; - for(auto import_type : module.imports()) + if(!worker.core_linker) { - 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(&extern_type)) + worker.core_linker.emplace(worker.engine); + for(auto import_type : module.imports()) + { + 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(&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(&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 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(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())); - if(!created) - return("table create failed: " + String(created.err().message())); - table.emplace(created.ok()); - table_next_free = core_min; imports.push_back(*table); continue; } - auto* func_ref = std::get_if(&extern_type); - if(!func_ref) - return("core has unexpected non-func import " + mod + "." + name); - wasmtime::FuncType func_type(*func_ref); - imports.push_back(make_host_import(cx, mod, name, func_type)); + 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); } - if(!table) - return("core does not import __indirect_function_table — rebuild core with --import-table"); + 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()); + birth_instantiate_us = phase_us(); auto exported_memory = core->get(cx, "memory"); if(!exported_memory || !std::get_if(&*exported_memory)) return("core does not export memory"); memory.emplace(std::get(*exported_memory)); + birth_exports_us = phase_us(); String error = call_core("_initialize", {}, 0); if(error != "") @@ -1403,6 +1466,7 @@ public: error = call_core("uce_wasm_request", {}, &request_ptr); if(error != "") return(error); + birth_initialize_us = phase_us(); if(request_ptr == 0) return("core returned null Request*"); return(""); @@ -1410,21 +1474,34 @@ public: 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(now - phase_start).count(); + phase_start = now; + return(elapsed); + }; String encoded = ucb_encode(context_tree); + context_bytes = encoded.size(); + context_encode_us = phase_us(); int32_t guest_ptr = 0; String error = call_core("uce_alloc", { (int32_t)encoded.size() }, &guest_ptr); + context_allocate_us = phase_us(); if(error != "") return(error); if(guest_ptr == 0) return("guest uce_alloc failed for context buffer"); error = guest_write((u32)guest_ptr, encoded); + context_write_us = phase_us(); if(error != "") return(error); int32_t rc = 0; error = call_core("uce_wasm_apply_context", { guest_ptr, (int32_t)encoded.size() }, &rc); + context_guest_apply_us = phase_us(); if(error != "") return(error); call_core("uce_free", { guest_ptr }, 0); + context_free_us = phase_us(); if(rc != 0) return("uce_wasm_apply_context returned " + std::to_string(rc)); return(""); @@ -1532,7 +1609,6 @@ private: std::vector units; std::map units_by_source; std::map handler_slots; // source + ":" + symbol → table slot - std::vector host_funcs; // keep host imports alive bool stale_component_mutation_blocked = false; String stale_component_mutation_status; bool component_source_generation_checked = false; @@ -2315,18 +2391,22 @@ private: // ---- 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; - WasmWorkspace* self = this; + struct ActiveWorkspace + { + WasmWorker* worker; + WasmWorkspace* operator->() const { return(worker->active_workspace); } + }; + ActiveWorkspace self = { &worker }; - auto add = [&](auto&& callback) -> Extern { - u64 profile_index = self->hostcall_operation_slots; + auto add = [&](auto&& callback) -> String { + u64 profile_index = worker.core_host_import_count++; if(profile_index < WasmRequestProfile::HOSTCALL_OPERATION_MAX) { - self->hostcall_operation_slots++; - if(self->worker.hostcall_operation_names.size() <= profile_index) - self->worker.hostcall_operation_names.push_back(name); + if(worker.hostcall_operation_names.size() <= profile_index) + worker.hostcall_operation_names.push_back(name); } bool profile_enabled = name != "uce_host_request_perf"; bool profile_mysql = name == "uce_host_mysql"; @@ -2368,15 +2448,16 @@ private: } return(result); }; - Func func(cx, func_type, profiled); - host_funcs.push_back(func); - return(host_funcs.back()); + auto defined = linker.func_new(mod, name, func_type, profiled); + if(!defined) + return("core host import failed: " + mod + "." + name + ": " + String(defined.err().message())); + return(""); }; // Hostcall blocklist (UCE_HOSTCALL_BLOCKLIST): a sysadmin-disabled hostcall // resolves to a trap stub instead of its real implementation, so a unit // 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 // the runtime itself cannot be bricked. 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_birth_us"] = (f64)self->workspace_birth_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_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; 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;