From 516ed94032a28f953677124d333a95e42aaec0d6 Mon Sep 17 00:00:00 2001 From: udo Date: Sat, 18 Jul 2026 03:43:44 +0000 Subject: [PATCH] Cache immutable Wasm server configuration encoding --- docs/wasm-runtime-architecture.md | 20 +++++--- site/tests/core.uce | 1 + src/lib/dvalue.cpp | 81 +++++++++++++++++++++++++++++++ src/wasm/backend.cpp | 22 +++++++-- src/wasm/core.cpp | 18 ++++--- src/wasm/worker.cpp | 15 ++++-- 6 files changed, 134 insertions(+), 23 deletions(-) diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index ef4bd36..359fb74 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -87,9 +87,11 @@ the boundary). children yielding `const DValue&`. - The request context (`params`/`get`/`post`/`cookies`/`session`, the raw body `in`, and—for WS—the connection context) is marshalled into a single `ctx` - DValue, UCEB-encoded, and handed to the workspace. The response (body, - headers, status, and any `meta` such as `ws_commands`) comes back the same - way. + DValue and UCEB-encoded. Immutable server configuration is UCEB-encoded once + per worker as a flat string map; both byte ranges are written into one guest + buffer. The guest decodes the flat map directly into its fresh `Server` and + the dynamic tree into its fresh `Request`. The response (body, headers, + status, and any `meta` such as `ws_commands`) comes back as UCEB. See [`docs/wasm-phase1-dvalue-abi.md`](wasm-phase1-dvalue-abi.md) for the wire format details. @@ -160,10 +162,11 @@ 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. Server configuration is immutable by that point. The worker therefore retains -one native `DValue` view of it; each request's temporary context tree references -that view while UCEB is encoded, avoiding a repeated native tree copy. The guest -still decodes the bytes into its fresh request tree. Request parameters, body, -cookies, session, call data, and response state are never retained this way. +one native `DValue` view and one UCEB encoding of it. Each request transfers +those cached bytes and the guest decodes the flat scalar map directly into its +fresh `Server`, avoiding both repeated native encoding and an intermediate guest +`DValue` tree. Request parameters, body, cookies, session, call data, and +response state are never retained this way. 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 @@ -285,7 +288,8 @@ request rather than being deep-copied twice; the historical by-value artifacts stay ABI-compatible. `request_perf()` subdivides birth into policy, import materialization, core instantiation, export/table lookup, and initialization, and context transfer into bytes, host encode, guest allocation/write, -guest decode/application, and free. +guest decode/application, and free. The byte profile separately reports the +worker-cached server-configuration portion. ### Task callbacks and workspace lifetime diff --git a/site/tests/core.uce b/site/tests/core.uce index eb1d0be..2385080 100644 --- a/site/tests/core.uce +++ b/site/tests/core.uce @@ -51,6 +51,7 @@ RENDER(Request& context) check("str_ends_with()", str_ends_with("component.uce", ".uce"), "component.uce ends with .uce"); check("to_lower() / to_upper()", to_lower("MiXeD") == "mixed" && to_upper("MiXeD") == "MIXED", to_lower("MiXeD") + " / " + to_upper("MiXeD")); check("request context params", context.params["SCRIPT_URL"] != "" && context.params["BASE_URL"] != "" && context.params["ROUTE_PATH"] != "" && context.params["ROUTE_PAGE"] != "" && context.params["ROUTE_VALID"] == "1", "script=" + context.params["SCRIPT_URL"] + " base=" + context.params["BASE_URL"] + " route=" + context.params["ROUTE_PATH"] + " page=" + context.params["ROUTE_PAGE"] + " valid=" + context.params["ROUTE_VALID"]); + check("request server configuration", context.server && context.server->config["BIN_DIRECTORY"] != "" && context.server->config["SITE_DIRECTORY"] != "" && context.server->config["SESSION_PATH"] != "" && context.server->config["WASM_CORE_PATH"] != "", "bin=" + context.server->config["BIN_DIRECTORY"] + " site=" + context.server->config["SITE_DIRECTORY"] + " session=" + context.server->config["SESSION_PATH"] + " core=" + context.server->config["WASM_CORE_PATH"]); String saved_query_string = context.params["QUERY_STRING"]; context.params["QUERY_STRING"] = "workspace/projects&theme=dark"; check("request_query_path() delegates to request_query_route()", request_query_path(context) == request_query_route(context)["l_path"].to_string() && request_query_path(context) == "workspace/projects", request_query_path(context) + " / " + json_encode(request_query_route(context))); diff --git a/src/lib/dvalue.cpp b/src/lib/dvalue.cpp index 22d61cb..aeed17d 100644 --- a/src/lib/dvalue.cpp +++ b/src/lib/dvalue.cpp @@ -1175,6 +1175,87 @@ bool ucb_decode(const String& encoded, DValue& out, String* error_out) return(false); } +#ifdef __UCE_WASM_CORE__ +static bool ucb_decode_flat_string_map(const String& encoded, StringMap& out, String* error_out) +{ + String error; + StringMap decoded; + size_t offset = 0; + auto fail = [&](String message) { + error = message; + return(false); + }; + if(encoded.size() < 5 || encoded.compare(0, 4, UCEB_MAGIC) != 0) + fail("missing UCEB magic header"); + else if((u8)encoded[4] != UCEB_VERSION) + fail("unsupported UCEB version"); + else + { + offset = 5; + if(encoded.size() - offset < 2) + fail("unexpected end of UCEB2 string map"); + else + { + u8 flags = (u8)encoded[offset++]; + char type = encoded[offset++]; + u64 scalar_len = 0, child_count = 0; + if(flags != 0 || type != 'M') + fail("UCEB2 string map root must be a map"); + else if(!ucb_read_varint(encoded, offset, scalar_len) || scalar_len != 0) + fail("UCEB2 string map root must have no scalar"); + else if(!ucb_read_varint(encoded, offset, child_count)) + fail("invalid UCEB2 string map child count"); + else + { + for(u64 i = 0; error == "" && i < child_count; i++) + { + u64 key_len = 0, value_len = 0, value_children = 0; + if(!ucb_read_varint(encoded, offset, key_len) || key_len > encoded.size() - offset) + { + fail("invalid UCEB2 string map key"); + break; + } + String key(encoded.data() + offset, (size_t)key_len); + offset += (size_t)key_len; + if(encoded.size() - offset < 2) + { + fail("unexpected end of UCEB2 string map value"); + break; + } + u8 value_flags = (u8)encoded[offset++]; + char value_type = encoded[offset++]; + if(value_flags != 0 || value_type != 'S' || !ucb_read_varint(encoded, offset, value_len) || value_len > encoded.size() - offset) + { + fail("UCEB2 string map value must be a string scalar"); + break; + } + String value(encoded.data() + offset, (size_t)value_len); + offset += (size_t)value_len; + if(!ucb_read_varint(encoded, offset, value_children) || value_children != 0) + { + fail("UCEB2 string map value cannot have children"); + break; + } + decoded[std::move(key)] = std::move(value); + } + if(error == "" && offset != encoded.size()) + fail("trailing bytes after UCEB2 string map"); + } + } + } + if(error != "") + { + if(error_out) + *error_out = error; + return(false); + } + out = std::move(decoded); + if(error_out) + *error_out = ""; + return(true); +} +#endif + DValue ucb_decode(const String& encoded) { DValue out; diff --git a/src/wasm/backend.cpp b/src/wasm/backend.cpp index ab6dc4b..e638eb8 100644 --- a/src/wasm/backend.cpp +++ b/src/wasm/backend.cpp @@ -68,10 +68,10 @@ static String wasm_backend_ensure_started(Request* context) } g_wasm_worker = new WasmWorker(wc); - // Server configuration is finalized before render workers start. Retain one - // native tree so per-request UCEB encoding can reference it without rebuilding - // dozens of identical nodes; the guest still decodes it into fresh state. + // Server configuration is finalized before render workers start. Encode it + // once; each fresh guest decodes the cached flat map into its own Server. g_wasm_worker->server_config_context = cfg; + g_wasm_worker->server_config_encoded = ucb_encode(g_wasm_worker->server_config_context); g_wasm_init_error = g_wasm_worker->init(); if(g_wasm_init_error == "") g_wasm_init_error = wasm_worker_prepare(*g_wasm_worker); @@ -159,8 +159,6 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri for(auto& entry : source) ctx[key][entry.first] = entry.second; }; - if(request.server) - ctx["server_config"].set_reference(&g_wasm_worker->server_config_context); copy_map(request.params, "params"); copy_map(request.get, "get"); copy_map(request.post, "post"); @@ -240,7 +238,21 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri if(to_bool(request.server->config["WASM_BACKEND_VERBOSE"], false)) { request.header["X-UCE-Backend"] = "wasm"; + request.header["X-UCE-Wasm-Workspace-Setup-Us"] = std::to_string(response.workspace_setup_us); request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us); + request.header["X-UCE-Wasm-Birth-Policy-Us"] = std::to_string(response.birth_policy_us); + request.header["X-UCE-Wasm-Birth-Import-Us"] = std::to_string(response.birth_import_us); + request.header["X-UCE-Wasm-Birth-Instantiate-Us"] = std::to_string(response.birth_instantiate_us); + request.header["X-UCE-Wasm-Birth-Exports-Us"] = std::to_string(response.birth_exports_us); + request.header["X-UCE-Wasm-Birth-Initialize-Us"] = std::to_string(response.birth_initialize_us); + request.header["X-UCE-Wasm-Context-Apply-Us"] = std::to_string(response.context_apply_us); + request.header["X-UCE-Wasm-Context-Bytes"] = std::to_string(response.context_bytes); + request.header["X-UCE-Wasm-Server-Config-Bytes"] = std::to_string(response.server_config_bytes); + request.header["X-UCE-Wasm-Context-Encode-Us"] = std::to_string(response.context_encode_us); + request.header["X-UCE-Wasm-Context-Allocate-Us"] = std::to_string(response.context_allocate_us); + request.header["X-UCE-Wasm-Context-Write-Us"] = std::to_string(response.context_write_us); + request.header["X-UCE-Wasm-Context-Guest-Apply-Us"] = std::to_string(response.context_guest_apply_us); + request.header["X-UCE-Wasm-Context-Free-Us"] = std::to_string(response.context_free_us); request.header["X-UCE-Wasm-Workspace-Complete-Us"] = std::to_string(response.workspace_complete_us); request.header["X-UCE-Wasm-Entry-Invoke-Us"] = std::to_string(response.entry_invoke_us); request.header["X-UCE-Wasm-Entry-Load-Us"] = std::to_string(response.entry_load_us); diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index eb237c0..2936202 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -7,6 +7,7 @@ #define __UCE_WASM_CORE__ 1 #include "../lib/uce_lib.cpp" + #include "../lib/mysql-connector.h" #include "../lib/sqlite-connector.h" @@ -799,19 +800,26 @@ void uce_wasm_core_reset_request() wasm_component_paths.clear(); } -// Host pushes the UCEB2-encoded request context into a guest buffer -// (uce_alloc) and applies it here; mirrors the native param population. -int uce_wasm_apply_context(const char* buf, size_t len) +// Host pushes the worker-cached immutable configuration followed by the +// dynamic UCEB2 request context into one guest buffer. +int uce_wasm_apply_context(const char* config_buf, size_t config_len, const char* context_buf, size_t context_len) { if(context == 0) uce_wasm_core_init(); + StringMap decoded_config; DValue decoded; String error; - if(!ucb_decode(String(buf, len), decoded, &error)) + if(!ucb_decode_flat_string_map(String(config_buf, config_len), decoded_config, &error)) { uce_host_log(3, error.data(), error.size()); return(1); } + if(!ucb_decode(String(context_buf, context_len), decoded, &error)) + { + uce_host_log(3, error.data(), error.size()); + return(2); + } + wasm_server.config = std::move(decoded_config); wasm_request.call = std::move(decoded); DValue& applied = wasm_request.call; auto apply_map = [](DValue* source, StringMap& dest) { @@ -821,8 +829,6 @@ int uce_wasm_apply_context(const char* buf, size_t len) dest[key] = item.to_string(); }); }; - if(applied.key("server_config")) - apply_map(applied.key("server_config"), wasm_server.config); apply_map(applied.key("params"), wasm_request.params); wasm_request.response_code = wasm_request.params["GATEWAY_INTERFACE"] != "" ? "Status: 200 OK" : "HTTP/1.1 200 OK"; diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 59557cf..5a8d72c 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -187,6 +187,7 @@ struct WasmRequestProfile u64 context_apply_us = 0; u64 context_apply_cpu_us = 0; u64 context_bytes = 0; + u64 server_config_bytes = 0; u64 context_encode_us = 0; u64 context_allocate_us = 0; u64 context_write_us = 0; @@ -978,6 +979,7 @@ public: std::optional core_linker; std::unique_ptr core_instance_pre; DValue server_config_context; + String server_config_encoded; struct CoreImport { String module; @@ -1532,21 +1534,25 @@ public: return(elapsed); }; String encoded = ucb_encode(context_tree); - context_bytes = encoded.size(); + server_config_bytes = worker.server_config_encoded.size(); + context_bytes = server_config_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); + String error = call_core("uce_alloc", { (int32_t)context_bytes }, &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); + error = guest_write((u32)guest_ptr, worker.server_config_encoded); + if(error == "") + error = guest_write((u32)guest_ptr + (u32)server_config_bytes, 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); + error = call_core("uce_wasm_apply_context", { guest_ptr, (int32_t)server_config_bytes, + guest_ptr + (int32_t)server_config_bytes, (int32_t)encoded.size() }, &rc); context_guest_apply_us = phase_us(); if(error != "") return(error); @@ -2592,6 +2598,7 @@ private: 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["server_config_bytes"] = (f64)self->server_config_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;