Cache immutable Wasm server configuration encoding

This commit is contained in:
udo
2026-07-18 03:43:44 +00:00
parent a043fb8fc7
commit 516ed94032
6 changed files with 134 additions and 23 deletions
+12 -8
View File
@@ -87,9 +87,11 @@ the boundary).
children yielding `const DValue&`. children yielding `const DValue&`.
- The request context (`params`/`get`/`post`/`cookies`/`session`, the raw body - The request context (`params`/`get`/`post`/`cookies`/`session`, the raw body
`in`, and—for WS—the connection context) is marshalled into a single `ctx` `in`, and—for WS—the connection context) is marshalled into a single `ctx`
DValue, UCEB-encoded, and handed to the workspace. The response (body, DValue and UCEB-encoded. Immutable server configuration is UCEB-encoded once
headers, status, and any `meta` such as `ws_commands`) comes back the same per worker as a flat string map; both byte ranges are written into one guest
way. 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 See [`docs/wasm-phase1-dvalue-abi.md`](wasm-phase1-dvalue-abi.md) for the wire
format details. 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, preventing the first request assigned to each worker from paying engine, linker,
or pre-instantiation startup. or pre-instantiation startup.
Server configuration is immutable by that point. The worker therefore retains Server configuration is immutable by that point. The worker therefore retains
one native `DValue` view of it; each request's temporary context tree references one native `DValue` view and one UCEB encoding of it. Each request transfers
that view while UCEB is encoded, avoiding a repeated native tree copy. The guest those cached bytes and the guest decodes the flat scalar map directly into its
still decodes the bytes into its fresh request tree. Request parameters, body, fresh `Server`, avoiding both repeated native encoding and an intermediate guest
cookies, session, call data, and response state are never retained this way. `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 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 module lives in the configured writable cache root rather than beside the
possibly root-owned deployed `core.wasm`; freshness still uses the deployed 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, artifacts stay ABI-compatible. `request_perf()` subdivides birth into policy,
import materialization, core instantiation, export/table lookup, and initialization, import materialization, core instantiation, export/table lookup, and initialization,
and context transfer into bytes, host encode, guest allocation/write, 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 ### Task callbacks and workspace lifetime
+1
View File
@@ -51,6 +51,7 @@ RENDER(Request& context)
check("str_ends_with()", str_ends_with("component.uce", ".uce"), "component.uce ends with .uce"); 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("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 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"]; String saved_query_string = context.params["QUERY_STRING"];
context.params["QUERY_STRING"] = "workspace/projects&theme=dark"; 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))); 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)));
+81
View File
@@ -1175,6 +1175,87 @@ bool ucb_decode(const String& encoded, DValue& out, String* error_out)
return(false); 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 ucb_decode(const String& encoded)
{ {
DValue out; DValue out;
+17 -5
View File
@@ -68,10 +68,10 @@ static String wasm_backend_ensure_started(Request* context)
} }
g_wasm_worker = new WasmWorker(wc); g_wasm_worker = new WasmWorker(wc);
// Server configuration is finalized before render workers start. Retain one // Server configuration is finalized before render workers start. Encode it
// native tree so per-request UCEB encoding can reference it without rebuilding // once; each fresh guest decodes the cached flat map into its own Server.
// dozens of identical nodes; the guest still decodes it into fresh state.
g_wasm_worker->server_config_context = cfg; 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(); g_wasm_init_error = g_wasm_worker->init();
if(g_wasm_init_error == "") if(g_wasm_init_error == "")
g_wasm_init_error = wasm_worker_prepare(*g_wasm_worker); 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) for(auto& entry : source)
ctx[key][entry.first] = entry.second; 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.params, "params");
copy_map(request.get, "get"); copy_map(request.get, "get");
copy_map(request.post, "post"); 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)) if(to_bool(request.server->config["WASM_BACKEND_VERBOSE"], false))
{ {
request.header["X-UCE-Backend"] = "wasm"; 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-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-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-Invoke-Us"] = std::to_string(response.entry_invoke_us);
request.header["X-UCE-Wasm-Entry-Load-Us"] = std::to_string(response.entry_load_us); request.header["X-UCE-Wasm-Entry-Load-Us"] = std::to_string(response.entry_load_us);
+12 -6
View File
@@ -7,6 +7,7 @@
#define __UCE_WASM_CORE__ 1 #define __UCE_WASM_CORE__ 1
#include "../lib/uce_lib.cpp" #include "../lib/uce_lib.cpp"
#include "../lib/mysql-connector.h" #include "../lib/mysql-connector.h"
#include "../lib/sqlite-connector.h" #include "../lib/sqlite-connector.h"
@@ -799,19 +800,26 @@ void uce_wasm_core_reset_request()
wasm_component_paths.clear(); wasm_component_paths.clear();
} }
// Host pushes the UCEB2-encoded request context into a guest buffer // Host pushes the worker-cached immutable configuration followed by the
// (uce_alloc) and applies it here; mirrors the native param population. // dynamic UCEB2 request context into one guest buffer.
int uce_wasm_apply_context(const char* buf, size_t len) int uce_wasm_apply_context(const char* config_buf, size_t config_len, const char* context_buf, size_t context_len)
{ {
if(context == 0) if(context == 0)
uce_wasm_core_init(); uce_wasm_core_init();
StringMap decoded_config;
DValue decoded; DValue decoded;
String error; 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()); uce_host_log(3, error.data(), error.size());
return(1); 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); wasm_request.call = std::move(decoded);
DValue& applied = wasm_request.call; DValue& applied = wasm_request.call;
auto apply_map = [](DValue* source, StringMap& dest) { 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(); 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); apply_map(applied.key("params"), wasm_request.params);
wasm_request.response_code = wasm_request.params["GATEWAY_INTERFACE"] != "" ? wasm_request.response_code = wasm_request.params["GATEWAY_INTERFACE"] != "" ?
"Status: 200 OK" : "HTTP/1.1 200 OK"; "Status: 200 OK" : "HTTP/1.1 200 OK";
+11 -4
View File
@@ -187,6 +187,7 @@ struct WasmRequestProfile
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_bytes = 0;
u64 server_config_bytes = 0;
u64 context_encode_us = 0; u64 context_encode_us = 0;
u64 context_allocate_us = 0; u64 context_allocate_us = 0;
u64 context_write_us = 0; u64 context_write_us = 0;
@@ -978,6 +979,7 @@ public:
std::optional<wasmtime::Linker> core_linker; std::optional<wasmtime::Linker> core_linker;
std::unique_ptr<wasmtime_instance_pre_t, WasmInstancePreDeleter> core_instance_pre; std::unique_ptr<wasmtime_instance_pre_t, WasmInstancePreDeleter> core_instance_pre;
DValue server_config_context; DValue server_config_context;
String server_config_encoded;
struct CoreImport struct CoreImport
{ {
String module; String module;
@@ -1532,21 +1534,25 @@ public:
return(elapsed); return(elapsed);
}; };
String encoded = ucb_encode(context_tree); 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(); 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)context_bytes }, &guest_ptr);
context_allocate_us = phase_us(); 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, worker.server_config_encoded);
if(error == "")
error = guest_write((u32)guest_ptr + (u32)server_config_bytes, encoded);
context_write_us = phase_us(); 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)server_config_bytes,
guest_ptr + (int32_t)server_config_bytes, (int32_t)encoded.size() }, &rc);
context_guest_apply_us = phase_us(); context_guest_apply_us = phase_us();
if(error != "") if(error != "")
return(error); return(error);
@@ -2592,6 +2598,7 @@ private:
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_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_encode_us"] = (f64)self->context_encode_us;
response["context_allocate_us"] = (f64)self->context_allocate_us; response["context_allocate_us"] = (f64)self->context_allocate_us;
response["context_write_us"] = (f64)self->context_write_us; response["context_write_us"] = (f64)self->context_write_us;