W5
This commit is contained in:
+77
-25
@@ -10,6 +10,10 @@
|
||||
// handle_complete() changes. CLI, serve_http, and websocket stay native.
|
||||
|
||||
#include "../lib/wasm_trace.h"
|
||||
// The native server TU has the real connectors (sqlite/mysql) compiled in, so
|
||||
// the worker's host-side connector hostcalls are available here. The W3 CLI
|
||||
// driver does not define this and gets a named-trap stub for those imports.
|
||||
#define UCE_WASM_HOST_CONNECTORS 1
|
||||
#include "worker.cpp"
|
||||
|
||||
#include <atomic>
|
||||
@@ -44,6 +48,12 @@ static String wasm_backend_ensure_started(Request* context)
|
||||
path_join(cfg["COMPILER_SYS_PATH"], "bin/wasm/core.wasm"));
|
||||
wc.site_root = path_join(cfg["COMPILER_SYS_PATH"], cfg["SITE_DIRECTORY"]);
|
||||
wc.cache_root = cfg["BIN_DIRECTORY"];
|
||||
// write membrane allowlist: the site tree plus the runtime scratch dirs
|
||||
// pages legitimately write to (matches native reachable write targets).
|
||||
wc.write_roots = { wc.site_root, "/tmp" };
|
||||
for(const char* key : { "BIN_DIRECTORY", "SESSION_PATH", "TMP_UPLOAD_PATH" })
|
||||
if(cfg[key] != "")
|
||||
wc.write_roots.push_back(cfg[key]);
|
||||
wc.memory_limit = (int64_t)config_u64("WASM_MEMORY_LIMIT_BYTES", 512ull * 1024 * 1024);
|
||||
wc.epoch_deadline_ticks = config_u64("WASM_EPOCH_DEADLINE_TICKS", 200);
|
||||
wc.verbose = config_bool("WASM_BACKEND_VERBOSE", false);
|
||||
@@ -75,31 +85,68 @@ static bool wasm_artifact_exists(Request* context, const String& entry_unit)
|
||||
if(entry_unit == "")
|
||||
return(false);
|
||||
String wasm_path = context->server->config["BIN_DIRECTORY"] + entry_unit + ".wasm";
|
||||
struct stat st;
|
||||
return(stat(wasm_path.c_str(), &st) == 0 && S_ISREG(st.st_mode));
|
||||
struct stat wasm_st;
|
||||
if(stat(wasm_path.c_str(), &wasm_st) != 0 || !S_ISREG(wasm_st.st_mode))
|
||||
return(false);
|
||||
// The wasm path bypasses the native JIT recompile, so a source edit would
|
||||
// otherwise be served from a stale .wasm. Require the artifact to be newer
|
||||
// than the source; if it is stale, fall back to native — which JIT-rebuilds
|
||||
// both .so and .wasm — so the next request gets a fresh artifact.
|
||||
struct stat src_st;
|
||||
if(stat(entry_unit.c_str(), &src_st) == 0 && wasm_st.st_mtime < src_st.st_mtime)
|
||||
return(false);
|
||||
return(true);
|
||||
}
|
||||
|
||||
// Decide, by source inspection, whether a page must stay on the native
|
||||
// backend for now (W5 surfaces not yet behind the membrane).
|
||||
//
|
||||
// Caveats this deliberately accepts:
|
||||
// - ENTRY-UNIT ONLY: a clean entry page that component()s into a native-only
|
||||
// unit is still served by wasm. With signals_based_traps off (see
|
||||
// make_engine) the worst case is a clean wasm error or a stubbed-empty
|
||||
// result, never a worker crash — so this is a best-effort routing hint,
|
||||
// not a guarantee. Full coverage is a W5 concern (un-stub the APIs).
|
||||
// - SUBSTRING match, intentionally conservative: a token like "xml_" also
|
||||
// matches an identifier or doc string containing it. That only ever
|
||||
// over-routes to native (always correct), never the reverse.
|
||||
static bool wasm_backend_native_fallback_uncached(Request* context, const String& entry_unit)
|
||||
{
|
||||
String source = file_get_contents(entry_unit);
|
||||
// Supported by the wasm core, intentionally NOT in this list:
|
||||
// - regex_* (host PCRE2 hostcall), xml_*/yaml_*/markdown_* (compiled in),
|
||||
// - unit_render()/component() (host resolver).
|
||||
// What remains is genuinely host-owned / native-only for now:
|
||||
// - zip, sqlite, background tasks, filesystem writes, sleeps;
|
||||
// - unit_call + compiler/unit introspection (need the native toolchain).
|
||||
StringList native_only_tokens = {
|
||||
"zip_", "task(", "task_repeat(",
|
||||
"task_pid(", "task_kill(", "unit_call(",
|
||||
"unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit(",
|
||||
"usleep(", "sleep("
|
||||
};
|
||||
for(auto& token : native_only_tokens)
|
||||
if(source.find(token) != String::npos)
|
||||
return(true);
|
||||
return(false);
|
||||
}
|
||||
|
||||
static bool wasm_backend_native_fallback_needed(Request* context, const String& entry_unit)
|
||||
{
|
||||
if(!context || !context->server || entry_unit == "")
|
||||
return(true);
|
||||
String site_root = path_join(context->server->config["COMPILER_SYS_PATH"], context->server->config["SITE_DIRECTORY"]);
|
||||
// W5 keeps native as the reference backend for compiler/docs pages and the
|
||||
// host-owned service surfaces that are not membrane APIs yet. This makes the
|
||||
// default backend safe while W6 decides which fallbacks to retire vs keep.
|
||||
if(entry_unit.rfind(path_join(site_root, "doc") + "/", 0) == 0)
|
||||
return(true);
|
||||
String source = file_get_contents(entry_unit);
|
||||
StringList native_only_tokens = {
|
||||
"markdown_to_", "zip_", "sqlite_", "regex_", "xml_", "yaml_", "task(", "task_repeat(",
|
||||
"task_pid(", "task_kill(", "unit_call(", "unit_render(",
|
||||
"unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit(",
|
||||
"file_put_contents(", "file_append(", "usleep(", "sleep("
|
||||
};
|
||||
for(auto& token : native_only_tokens)
|
||||
if(source.find(token) != String::npos)
|
||||
return(true);
|
||||
return(false);
|
||||
// Cache the verdict per process, keyed on path + mtime: the source scan is
|
||||
// otherwise a file read + 20 substring searches on every page request.
|
||||
struct CachedVerdict { time_t mtime; bool fallback; };
|
||||
static std::map<String, CachedVerdict> cache;
|
||||
struct stat st;
|
||||
time_t mtime = (stat(entry_unit.c_str(), &st) == 0) ? st.st_mtime : 0;
|
||||
auto it = cache.find(entry_unit);
|
||||
if(it != cache.end() && it->second.mtime == mtime)
|
||||
return(it->second.fallback);
|
||||
bool fallback = wasm_backend_native_fallback_uncached(context, entry_unit);
|
||||
cache[entry_unit] = { mtime, fallback };
|
||||
return(fallback);
|
||||
}
|
||||
|
||||
// True if this request should be served by the wasm backend. Falls through to
|
||||
@@ -142,12 +189,17 @@ String wasm_backend_serve(Request& request, const String& entry_unit)
|
||||
if(!response.ok)
|
||||
return(response.error == "" ? String("wasm workspace failed") : response.error);
|
||||
|
||||
request.header["X-UCE-Backend"] = "wasm";
|
||||
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Count"] = std::to_string(response.component_resolve_count);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Total-Us"] = std::to_string(response.component_resolve_total_us);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Avg-Us"] = std::to_string(
|
||||
response.component_resolve_count ? response.component_resolve_total_us / response.component_resolve_count : 0);
|
||||
// Diagnostic timing headers are opt-in: they leak workspace internals and
|
||||
// belong to the W5 benchmark harness, not public responses.
|
||||
if(config_bool("WASM_BACKEND_VERBOSE", false))
|
||||
{
|
||||
request.header["X-UCE-Backend"] = "wasm";
|
||||
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Count"] = std::to_string(response.component_resolve_count);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Total-Us"] = std::to_string(response.component_resolve_total_us);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Avg-Us"] = std::to_string(
|
||||
response.component_resolve_count ? response.component_resolve_total_us / response.component_resolve_count : 0);
|
||||
}
|
||||
|
||||
// status line: keep the native default unless the unit set one
|
||||
String status = response.meta["status"].to_string();
|
||||
|
||||
Reference in New Issue
Block a user