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

View File

@ -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

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("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)));

View File

@ -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;

View File

@ -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);

View File

@ -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";

View File

@ -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<wasmtime::Linker> core_linker;
std::unique_ptr<wasmtime_instance_pre_t, WasmInstancePreDeleter> 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;