perf: extend the compiled-module disk cache to per-unit modules

Follow-up to 560290c. Per-unit .wasm modules were still Cranelift-compiled per
worker on first use (~40-70ms each), so with 8-call worker recycling fresh
workers re-JIT every unit they touch.

Refactor the core cache logic into a shared WasmWorker helper:
- cached_wasm_path(p): maps <...>.wasm -> <...>.cwasm.
- load_or_compile_cached_module(engine, cached, wasm, bytes, err): deserialize_file
  the .cwasm when it is newer than the .wasm; otherwise Module::compile + serialize,
  written atomically (temp+rename); deserialize failure falls back to compile.
Both the core module load and unit_module() now go through this helper, so unit
artifacts get the same <unit>.uce.cwasm cache the core got.

Independently verified on the host: fresh-worker first-hit unit latency ~40-70ms
-> ~3-21ms; full suite wall-clock 20.5s -> 6.5s (and ~40s before any caching);
run_cli_tests --include-wasm-kill => 87 passed, 0 failed, 0 skipped.

Implemented via the pi agent (gpt-5.3-codex-spark sub-model).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root 2026-06-15 00:16:41 +00:00
parent 560290ca1d
commit 83ab9e10f7

View File

@ -249,53 +249,12 @@ public:
if(!wasm_parse_sections(bytes, core_dylink, abi_ignored, parse_error))
return("core module: " + parse_error);
String core_cached_path = cfg.core_wasm_path;
if(core_cached_path.size() >= 5 && core_cached_path.rfind(".wasm", core_cached_path.size() - 5) == core_cached_path.size() - 5)
core_cached_path = core_cached_path.substr(0, core_cached_path.size() - 5) + ".cwasm";
else
core_cached_path += ".cwasm";
struct stat core_stat;
struct stat cached_stat;
if(stat(core_cached_path.c_str(), &cached_stat) == 0 && stat(cfg.core_wasm_path.c_str(), &core_stat) == 0)
{
if(cached_stat.st_mtime > core_stat.st_mtime)
{
auto deserialized = wasmtime::Module::deserialize_file(engine, core_cached_path);
if(deserialized)
{
core_module.emplace(deserialized.ok());
return("");
}
}
}
auto compiled = wasmtime::Module::compile(engine, bytes);
if(!compiled)
return("core module compile failed: " + String(compiled.err().message()));
core_module.emplace(compiled.ok());
auto serialized = core_module->serialize();
if(serialized)
{
String tmp = core_cached_path + "." + std::to_string((long long)getpid()) + ".tmp";
auto data = serialized.ok();
{
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
if(out)
{
out.write((const char*)data.data(), (std::streamsize)data.size());
if(out)
{
out.flush();
out.close();
if(std::rename(tmp.c_str(), core_cached_path.c_str()) != 0)
(void)std::remove(tmp.c_str());
}
}
else
(void)std::remove(tmp.c_str());
}
}
String core_cached_path = cached_wasm_path(cfg.core_wasm_path);
String compile_error;
auto compiled_or_cached = load_or_compile_cached_module(engine, core_cached_path, cfg.core_wasm_path, bytes, compile_error);
if(!compiled_or_cached)
return("core module compile failed: " + compile_error);
core_module.emplace(std::move(*compiled_or_cached));
return("");
}
@ -347,18 +306,74 @@ public:
error = wasm_path + ": missing uce.abi stamp";
return(nullptr);
}
auto compiled = wasmtime::Module::compile(engine, bytes);
if(!compiled)
String unit_cached_path = cached_wasm_path(wasm_path);
String compile_error;
auto compiled_or_cached = load_or_compile_cached_module(engine, unit_cached_path, wasm_path, bytes, compile_error);
if(!compiled_or_cached)
{
error = wasm_path + ": compile failed: " + String(compiled.err().message());
error = wasm_path + ": compile failed: " + compile_error;
return(nullptr);
}
unit->module.emplace(compiled.ok());
unit->module.emplace(std::move(*compiled_or_cached));
module_cache[wasm_path] = unit;
return(unit);
}
private:
static String cached_wasm_path(const String& wasm_path)
{
if(wasm_path.size() >= 5 && wasm_path.rfind(".wasm", wasm_path.size() - 5) == wasm_path.size() - 5)
return(wasm_path.substr(0, wasm_path.size() - 5) + ".cwasm");
return(wasm_path + ".cwasm");
}
static std::optional<wasmtime::Module> load_or_compile_cached_module(wasmtime::Engine& engine, const String& cached_path, const String& wasm_path,
std::vector<u8>& bytes, String& compile_error)
{
struct stat wasm_stat;
struct stat cached_stat;
if(stat(cached_path.c_str(), &cached_stat) == 0 && stat(wasm_path.c_str(), &wasm_stat) == 0)
{
if(cached_stat.st_mtime > wasm_stat.st_mtime)
{
auto deserialized = wasmtime::Module::deserialize_file(engine, cached_path);
if(deserialized)
return(deserialized.ok());
}
}
auto compiled = wasmtime::Module::compile(engine, bytes);
if(!compiled)
{
compile_error = String(compiled.err().message());
return(std::nullopt);
}
auto result = compiled.ok();
auto serialized = result.serialize();
if(serialized)
{
String tmp = cached_path + "." + std::to_string((long long)getpid()) + ".tmp";
auto data = serialized.ok();
{
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
if(out)
{
out.write((const char*)data.data(), (std::streamsize)data.size());
if(out)
{
out.flush();
out.close();
if(std::rename(tmp.c_str(), cached_path.c_str()) != 0)
(void)std::remove(tmp.c_str());
}
}
else
(void)std::remove(tmp.c_str());
}
}
return(result);
}
static wasmtime::Engine make_engine()
{
wasmtime::Config config;