diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index abf7149..e3c4a9a 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -141,6 +141,10 @@ changing core-first, unit-load-order symbol resolution. into artifact lookup, wasm read, custom-section parse, serialized-module deserialization or wasm compilation, and immutable import classification. This keeps cold-worker module latency distinguishable without exposing source paths. +When a current serialized module exists, the worker scans wasm section headers +and reads only `dylink.0` and `uce.abi`; it does not fault the multi-megabyte code +and data bodies into every new worker. A missing/stale/invalid serialized module +still reads, validates, compiles, and republishes the complete wasm artifact. Cold module compilation and deserialization are host work, so `load_unit()` refreshes the epoch deadline before its first guest call. Otherwise a component whose compilation outlasted the guest CPU budget would immediately trap in the diff --git a/site/demo/index.uce b/site/demo/index.uce index 379d551..6355efc 100644 --- a/site/demo/index.uce +++ b/site/demo/index.uce @@ -105,7 +105,7 @@ RENDER(Request& context) print("Component phases: path ", (u64)perf["component_path_us"].to_u64(), " / artifact ", (u64)perf["component_artifact_us"].to_u64(), " / load ", (u64)perf["component_load_us"].to_u64(), " / link ", (u64)perf["component_link_us"].to_u64(), " us\n"); print("Unit loads: ", (u64)perf["unit_load_count"].to_u64(), " / module ", (u64)perf["unit_module_us"].to_u64(), " / allocate ", (u64)perf["unit_allocate_us"].to_u64(), " / imports ", (u64)perf["unit_import_us"].to_u64(), " / instantiate ", (u64)perf["unit_instantiate_us"].to_u64(), " / initialize ", (u64)perf["unit_initialize_us"].to_u64(), " us\n"); print("Unit module cache: ", (u64)perf["unit_module_cache_hit_count"].to_u64(), " worker hits / ", (u64)perf["unit_module_cache_miss_count"].to_u64(), " misses / ", (u64)perf["unit_module_serialized_cache_hit_count"].to_u64(), " serialized hits / ", (u64)perf["unit_module_compile_count"].to_u64(), " wasm compiles\n"); - print("Unit module phases: lookup ", (u64)perf["unit_module_lookup_us"].to_u64(), " / read ", (u64)perf["unit_module_read_us"].to_u64(), " / parse ", (u64)perf["unit_module_parse_us"].to_u64(), " / deserialize/compile ", (u64)perf["unit_module_compile_us"].to_u64(), " / classify ", (u64)perf["unit_module_classify_us"].to_u64(), " us\n"); + print("Unit module phases: lookup ", (u64)perf["unit_module_lookup_us"].to_u64(), " / read ", (u64)perf["unit_module_read_us"].to_u64(), " us / ", (u64)perf["unit_module_read_bytes"].to_u64(), " bytes / parse ", (u64)perf["unit_module_parse_us"].to_u64(), " / deserialize/compile ", (u64)perf["unit_module_compile_us"].to_u64(), " / classify ", (u64)perf["unit_module_classify_us"].to_u64(), " us\n"); print("Unit symbol resolves: ", (u64)perf["unit_symbol_resolve_count"].to_u64(), " / ", (u64)perf["unit_symbol_resolve_us"].to_u64(), " us\n"); print("Output buffer size: ", context.ob->str().length(), "\n"); ?> diff --git a/site/doc/pages/request_perf.txt b/site/doc/pages/request_perf.txt index e1c743c..75d1c35 100644 --- a/site/doc/pages/request_perf.txt +++ b/site/doc/pages/request_perf.txt @@ -15,7 +15,7 @@ Component resolution is divided into `component_path_us`, `component_artifact_us Successful first loads within the request are counted by `unit_load_count` and divided into `unit_module_us`, `unit_allocate_us`, `unit_import_us`, `unit_instantiate_us`, and `unit_initialize_us`. These cover compiled-module lookup, guest memory/table allocation, import construction, Wasmtime instantiation, and relocations/constructors/request binding. Repeated handlers from an already loaded unit are excluded. -`unit_module_cache_hit_count` and `unit_module_cache_miss_count` divide module loads by the worker's compiled-module cache. A miss is further identified by `unit_module_serialized_cache_hit_count` when Wasmtime deserializes the current `.cwasm`; `unit_module_compile_count` means it fell back to compiling the `.wasm`. `unit_module_lookup_us`, `unit_module_read_us`, `unit_module_parse_us`, `unit_module_compile_us`, and `unit_module_classify_us` divide `unit_module_us` into artifact stat/cache lookup, wasm read, custom-section parse, deserialize-or-compile, and immutable import classification. The phase sum can be below the total because allocation and cache publication overhead remain in the aggregate. +`unit_module_cache_hit_count` and `unit_module_cache_miss_count` divide module loads by the worker's compiled-module cache. A miss is further identified by `unit_module_serialized_cache_hit_count` when Wasmtime deserializes the current `.cwasm`; `unit_module_compile_count` means it fell back to compiling the `.wasm`. `unit_module_lookup_us`, `unit_module_read_us`, `unit_module_read_bytes`, `unit_module_parse_us`, `unit_module_compile_us`, and `unit_module_classify_us` divide `unit_module_us` into artifact stat/cache lookup, wasm metadata/full-artifact read volume, custom-section parse, deserialize-or-compile, and immutable import classification. A current serialized-module hit scans only section headers and the `dylink.0`/`uce.abi` payloads; compilation fallback reads the complete wasm. The phase sum can be below the total because allocation and cache publication overhead remain in the aggregate. `unit_symbol_resolve_count` and `unit_symbol_resolve_us` isolate function and data symbol lookup within `unit_import_us`. The remainder of import time covers import-vector construction, Wasmtime Globals, GOT function table placement, and related bindings. diff --git a/site/tests/io.uce b/site/tests/io.uce index f1af42d..0669539 100644 --- a/site/tests/io.uce +++ b/site/tests/io.uce @@ -151,7 +151,11 @@ RENDER(Request& context) check("to_u64() / to_s64() / to_f64() / to_bool()", to_u64(cfg["u64"], 7) == 42 && to_u64(cfg["bad"], 7) == 7 && to_s64(cfg["s64"], 5) == -12 && to_s64(cfg["bad"], 5) == 5 && to_f64(cfg["f64"], 1.0) > 3.49 && to_f64(cfg["bad"], 1.25) == 1.25 && to_bool(cfg["yes"], false) && !to_bool(cfg["no"], true) && !to_bool("unknown", false), var_dump(cfg)); DValue perf = request_perf(); - bool unit_module_profile_valid = perf["unit_module_cache_hit_count"].to_u64() + perf["unit_module_cache_miss_count"].to_u64() == perf["unit_load_count"].to_u64() && perf["unit_module_serialized_cache_hit_count"].to_u64() + perf["unit_module_compile_count"].to_u64() == perf["unit_module_cache_miss_count"].to_u64() && perf["unit_module_lookup_us"].type != 'S' && perf["unit_module_read_us"].type != 'S' && perf["unit_module_parse_us"].type != 'S' && perf["unit_module_compile_us"].type != 'S' && perf["unit_module_classify_us"].type != 'S'; + u64 module_misses = perf["unit_module_cache_miss_count"].to_u64(); + u64 serialized_hits = perf["unit_module_serialized_cache_hit_count"].to_u64(); + u64 module_read_bytes = perf["unit_module_read_bytes"].to_u64(); + bool serialized_reads_bounded = module_misses == 0 || serialized_hits != module_misses || (module_read_bytes > 0 && module_read_bytes < module_misses * 1024 * 1024); + bool unit_module_profile_valid = serialized_reads_bounded && perf["unit_module_cache_hit_count"].to_u64() + module_misses == perf["unit_load_count"].to_u64() && serialized_hits + perf["unit_module_compile_count"].to_u64() == module_misses && perf["unit_module_lookup_us"].type != 'S' && perf["unit_module_read_us"].type != 'S' && perf["unit_module_read_bytes"].type != 'S' && perf["unit_module_parse_us"].type != 'S' && perf["unit_module_compile_us"].type != 'S' && perf["unit_module_classify_us"].type != 'S'; bool perf_stable = unit_module_profile_valid && perf["worker_pid"].to_u64() > 0 && perf["running_us"].to_f64() > 0 && perf["dispatch_us"].type != 'S' && perf["workspace_setup_us"].type != 'S' && perf["workspace_birth_us"].type != 'S' && perf["context_apply_us"].type != 'S' && perf["hostcall_count"].to_u64() > 0 && perf["hostcall_us"].type != 'S' && perf["guest_us"].to_f64() > 0 && perf["component_resolve_count"].type != 'S' && perf["component_path_us"].type != 'S' && perf["component_artifact_us"].type != 'S' && perf["component_load_us"].type != 'S' && perf["component_link_us"].type != 'S' && perf["unit_load_count"].to_u64() > 0 && perf["unit_module_us"].type != 'S' && perf["unit_allocate_us"].type != 'S' && perf["unit_import_us"].type != 'S' && perf["unit_symbol_resolve_count"].type != 'S' && perf["unit_symbol_resolve_us"].type != 'S' && perf["unit_instantiate_us"].type != 'S' && perf["unit_initialize_us"].type != 'S'; u64 profiled_hostcalls = perf["hostcall_count"].to_u64(); f64 profiled_hostcall_us = perf["hostcall_us"].to_f64(); diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 1327830..e861598 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -109,6 +109,7 @@ struct WasmUnitModuleLoadProfile bool serialized_cache_hit = false; u64 lookup_us = 0; u64 read_us = 0; + u64 read_bytes = 0; u64 parse_us = 0; u64 compile_us = 0; u64 classify_us = 0; @@ -158,6 +159,7 @@ struct WasmRequestProfile u64 unit_module_compile_count = 0; u64 unit_module_lookup_total_us = 0; u64 unit_module_read_total_us = 0; + u64 unit_module_read_bytes = 0; u64 unit_module_parse_total_us = 0; u64 unit_module_compile_total_us = 0; u64 unit_module_classify_total_us = 0; @@ -671,6 +673,142 @@ static bool wasm_read_file(const String& path, std::vector& out) return((bool)in); } +static bool wasm_pread_all(int fd, u64 offset, u8* out, size_t size, u64& bytes_read) +{ + size_t done = 0; + while(done < size) + { + ssize_t n = pread(fd, out + done, size - done, (off_t)(offset + done)); + if(n < 0 && errno == EINTR) + continue; + if(n <= 0) + return(false); + done += (size_t)n; + bytes_read += (u64)n; + } + return(true); +} + +static bool wasm_read_uleb_fd(int fd, u64& pos, u64 end, u64& out, u64& bytes_read) +{ + out = 0; + u32 shift = 0; + while(pos < end && shift < 64) + { + u8 byte = 0; + if(!wasm_pread_all(fd, pos++, &byte, 1, bytes_read)) + return(false); + out |= ((u64)(byte & 0x7f)) << shift; + if((byte & 0x80) == 0) + return(true); + shift += 7; + } + return(false); +} + +static void wasm_write_uleb(std::vector& out, u64 value) +{ + do + { + u8 byte = (u8)(value & 0x7f); + value >>= 7; + out.push_back(value ? byte | 0x80 : byte); + } + while(value); +} + +static bool wasm_read_metadata_file(const String& path, std::vector& metadata, String& error, u64& bytes_read, + u64 expected_modified_ns, u64 expected_changed_ns, u64 expected_size) +{ + bytes_read = 0; + int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if(fd < 0) + return(false); + struct stat st; + if(fstat(fd, &st) != 0 || st.st_size < 8) + { + close(fd); + error = "not a wasm module"; + return(false); + } + u64 modified_ns = (u64)st.st_mtim.tv_sec * 1000000000ull + (u64)st.st_mtim.tv_nsec; + u64 changed_ns = (u64)st.st_ctim.tv_sec * 1000000000ull + (u64)st.st_ctim.tv_nsec; + if(modified_ns != expected_modified_ns || changed_ns != expected_changed_ns || (u64)st.st_size != expected_size) + { + close(fd); + error = "wasm artifact changed while loading metadata"; + return(false); + } + u64 file_size = (u64)st.st_size; + u8 header[8]; + if(!wasm_pread_all(fd, 0, header, sizeof(header), bytes_read) || memcmp(header, "\0asm\1\0\0\0", sizeof(header)) != 0) + { + close(fd); + error = "not a supported wasm module"; + return(false); + } + metadata.assign(header, header + sizeof(header)); + u64 pos = sizeof(header); + while(pos < file_size) + { + u8 section_id = 0; + if(!wasm_pread_all(fd, pos++, §ion_id, 1, bytes_read)) + { + error = "malformed wasm section header"; + break; + } + u64 section_size = 0; + if(!wasm_read_uleb_fd(fd, pos, file_size, section_size, bytes_read) || section_size > file_size - pos) + { + error = "malformed wasm section header"; + break; + } + u64 section_end = pos + section_size; + if(section_id == 0) + { + u64 cursor = pos; + u64 name_len = 0; + if(!wasm_read_uleb_fd(fd, cursor, section_end, name_len, bytes_read) || name_len > section_end - cursor) + { + error = "malformed custom section name"; + break; + } + String name; + if(name_len <= 64) + { + name.resize((size_t)name_len); + if(name_len && !wasm_pread_all(fd, cursor, (u8*)&name[0], (size_t)name_len, bytes_read)) + { + error = "malformed custom section name"; + break; + } + } + if(name == "dylink.0" || name == "uce.abi") + { + if(section_size > 1024 * 1024) + { + error = "oversized wasm metadata section"; + break; + } + std::vector section((size_t)section_size); + if(section_size && !wasm_pread_all(fd, pos, section.data(), section.size(), bytes_read)) + { + error = "cannot read wasm metadata section"; + break; + } + metadata.push_back(0); + wasm_write_uleb(metadata, section_size); + metadata.insert(metadata.end(), section.begin(), section.end()); + } + } + pos = section_end; + } + close(fd); + if(error != "") + return(false); + return(true); +} + // ---- worker (per process): engine + compiled module caches ---------------- class WasmWorker @@ -797,13 +935,25 @@ public: unit->modified_ns = modified_ns; unit->changed_ns = changed_ns; unit->size = (u64)st.st_size; + String unit_cached_path = cached_wasm_path(wasm_path); + auto compile_start = std::chrono::steady_clock::now(); + auto compiled_or_cached = load_current_serialized_module(engine, unit_cached_path, wasm_path); + profile.compile_us = (u64)std::chrono::duration_cast( + std::chrono::steady_clock::now() - compile_start).count(); + profile.serialized_cache_hit = compiled_or_cached.has_value(); std::vector bytes; auto read_start = std::chrono::steady_clock::now(); - if(!wasm_read_file(wasm_path, bytes)) + bool read_ok = profile.serialized_cache_hit + ? wasm_read_metadata_file(wasm_path, bytes, error, profile.read_bytes, modified_ns, changed_ns, unit->size) + : wasm_read_file(wasm_path, bytes); + if(!profile.serialized_cache_hit) + profile.read_bytes = bytes.size(); + if(!read_ok) { profile.read_us = (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - read_start).count(); - error = "cannot read " + wasm_path; + if(error == "") + error = "cannot read " + wasm_path; return(nullptr); } profile.read_us = (u64)std::chrono::duration_cast( @@ -828,12 +978,14 @@ public: error = wasm_path + ": missing uce.abi stamp"; return(nullptr); } - String unit_cached_path = cached_wasm_path(wasm_path); String compile_error; - auto compile_start = std::chrono::steady_clock::now(); - auto compiled_or_cached = load_or_compile_cached_module(engine, unit_cached_path, wasm_path, bytes, compile_error, profile.serialized_cache_hit); - profile.compile_us = (u64)std::chrono::duration_cast( - std::chrono::steady_clock::now() - compile_start).count(); + if(!compiled_or_cached) + { + compile_start = std::chrono::steady_clock::now(); + compiled_or_cached = compile_and_cache_module(engine, unit_cached_path, bytes, compile_error); + profile.compile_us += (u64)std::chrono::duration_cast( + std::chrono::steady_clock::now() - compile_start).count(); + } if(!compiled_or_cached) { error = wasm_path + ": compile failed: " + compile_error; @@ -888,10 +1040,8 @@ private: return(wasm_path + ".cwasm"); } - static std::optional load_or_compile_cached_module(wasmtime::Engine& engine, const String& cached_path, const String& wasm_path, - std::vector& bytes, String& compile_error, bool& serialized_cache_hit) + static std::optional load_current_serialized_module(wasmtime::Engine& engine, const String& cached_path, const String& wasm_path) { - serialized_cache_hit = false; 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) @@ -901,13 +1051,15 @@ private: { auto deserialized = wasmtime::Module::deserialize_file(engine, cached_path); if(deserialized) - { - serialized_cache_hit = true; return(deserialized.ok()); - } } } + return(std::nullopt); + } + static std::optional compile_and_cache_module(wasmtime::Engine& engine, const String& cached_path, + std::vector& bytes, String& compile_error) + { auto compiled = wasmtime::Module::compile(engine, bytes); if(!compiled) { @@ -940,6 +1092,19 @@ private: return(result); } + static std::optional load_or_compile_cached_module(wasmtime::Engine& engine, const String& cached_path, const String& wasm_path, + std::vector& bytes, String& compile_error, bool& serialized_cache_hit) + { + serialized_cache_hit = false; + auto cached = load_current_serialized_module(engine, cached_path, wasm_path); + if(cached) + { + serialized_cache_hit = true; + return(cached); + } + return(compile_and_cache_module(engine, cached_path, bytes, compile_error)); + } + static wasmtime::Engine make_engine() { wasmtime::Config config; @@ -1397,6 +1562,7 @@ private: unit_module_compile_count += !module_profile.cache_hit && !module_profile.serialized_cache_hit ? 1 : 0; unit_module_lookup_total_us += module_profile.lookup_us; unit_module_read_total_us += module_profile.read_us; + unit_module_read_bytes += module_profile.read_bytes; unit_module_parse_total_us += module_profile.parse_us; unit_module_compile_total_us += module_profile.compile_us; unit_module_classify_total_us += module_profile.classify_us; @@ -2042,6 +2208,7 @@ private: response["unit_module_compile_count"] = (f64)self->unit_module_compile_count; response["unit_module_lookup_us"] = (f64)self->unit_module_lookup_total_us; response["unit_module_read_us"] = (f64)self->unit_module_read_total_us; + response["unit_module_read_bytes"] = (f64)self->unit_module_read_bytes; response["unit_module_parse_us"] = (f64)self->unit_module_parse_total_us; response["unit_module_compile_us"] = (f64)self->unit_module_compile_total_us; response["unit_module_classify_us"] = (f64)self->unit_module_classify_total_us;