From a5ad5042b71e7ecfa8bea0fb567d81cb493043d3 Mon Sep 17 00:00:00 2001 From: udo Date: Sat, 18 Jul 2026 05:01:27 +0000 Subject: [PATCH] Reuse loaded component units per request --- docs/wasm-runtime-architecture.md | 8 +- scripts/run_cli_tests.sh | 1 + scripts/test_relative_component_cache.sh | 38 +++++++ site/tests/components.uce | 2 +- site/tests/io.uce | 5 +- src/wasm/backend.cpp | 1 + src/wasm/core.cpp | 4 +- src/wasm/worker.cpp | 136 ++++++++++++++--------- 8 files changed, 133 insertions(+), 62 deletions(-) create mode 100755 scripts/test_relative_component_cache.sh diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index a1f6053..914670e 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -139,8 +139,12 @@ The request entry unit is already loaded by the host, so its selected handler and optional `ONCE` export are placed directly into the workspace table and passed to `uce_wasm_invoke_loaded_entry()`. Dynamic component and unit calls continue through `wasm_resolve_target()`, where relative-path resolution is -required. Opt-in verbose response headers split entry invocation into load, -presence lookup, table linking, and core dispatch time. +required. Handler cache keys include the calling unit so identically named +relative targets in different directories cannot alias. Once a unit has passed +freshness checks and loaded into a request workspace, later handler lookups +reuse that immutable request-local instance; `component_loaded_reuse_count` +reports those lookups. Opt-in verbose response headers split entry invocation +into load, presence lookup, table linking, and core dispatch time. `wasm_backend_should_handle(request, entry_unit)` checks whether the wasm backend is initialized and the requested artifact/handler is currently diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index 79d9750..56a27ca 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -69,6 +69,7 @@ if [[ "$action" == "run" ]]; then scripts/test_dependency_invalidation.sh scripts/test_cold_component_deadline.sh scripts/test_nested_component_props.sh + scripts/test_relative_component_cache.sh scripts/test_password_hashing.sh scripts/test_mysql_epoch_refresh.sh scripts/test_mysql_persistent_pool.sh diff --git a/scripts/test_relative_component_cache.sh b/scripts/test_relative_component_cache.sh new file mode 100755 index 0000000..5c2d555 --- /dev/null +++ b/scripts/test_relative_component_cache.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +test_name="relative-component-cache-test-$$" +site_directory="${UCE_TEST_SITE_DIRECTORY:-site}" +if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then + configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg) + if [[ -n "${configured_site_directory:-}" ]]; then + site_directory="$configured_site_directory" + fi +fi +source_dir="$site_directory/$test_name" + +cleanup() { + rm -rf "$source_dir" +} +trap cleanup EXIT +mkdir -p "$source_dir/a" "$source_dir/b" + +printf '%s\n' \ + 'CLI(Request& context) {' \ + ' print(component("a/parent", context));' \ + ' print("/");' \ + ' print(component("b/parent", context));' \ + '}' >"$source_dir/entry.uce" +printf '%s\n' 'COMPONENT(Request& context) { print(component("child", context)); }' >"$source_dir/a/parent.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("relative-a"); }' >"$source_dir/a/child.uce" +printf '%s\n' 'COMPONENT(Request& context) { print(component("child", context)); }' >"$source_dir/b/parent.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("relative-b"); }' >"$source_dir/b/child.uce" + +output=$(scripts/uce-cli "/$test_name/entry.uce") +if [[ "$output" != "relative-a/relative-b" ]]; then + echo "relative component cache crossed caller boundaries: $output" >&2 + exit 1 +fi + +echo "relative component cache passed" diff --git a/site/tests/components.uce b/site/tests/components.uce index a651958..39c73e9 100644 --- a/site/tests/components.uce +++ b/site/tests/components.uce @@ -59,7 +59,7 @@ RENDER(Request& context) String unit_operations = json_encode(perf["unit_module_operations"]); check( "request_perf() separates entry code from dynamic component materialization", - perf["entry_unit_load_count"].to_u64() == 1 && perf["entry_unit_materialize_us"].to_u64() > 0 && perf["dynamic_include_load_count"].to_u64() > 0 && perf["dynamic_include_materialize_us"].to_u64() > 0 && unit_operations.find("\"kind\": \"entry\"") != String::npos && unit_operations.find("\"kind\": \"component\"") != String::npos && unit_operations.find("\"materialize_us\"") != String::npos, + perf["entry_unit_load_count"].to_u64() == 1 && perf["entry_unit_materialize_us"].to_u64() > 0 && perf["dynamic_include_load_count"].to_u64() > 0 && perf["dynamic_include_materialize_us"].to_u64() > 0 && perf["component_loaded_reuse_count"].to_u64() > 0 && unit_operations.find("\"kind\": \"entry\"") != String::npos && unit_operations.find("\"kind\": \"component\"") != String::npos && unit_operations.find("\"materialize_us\"") != String::npos, unit_operations ); diff --git a/site/tests/io.uce b/site/tests/io.uce index 3270d36..cdb9bb5 100644 --- a/site/tests/io.uce +++ b/site/tests/io.uce @@ -181,11 +181,12 @@ RENDER(Request& context) u64 dropped_hostcall_operations = perf["hostcall_operations_dropped"].to_u64(); hostcall_operation_profile_valid = hostcall_operation_profile_valid && perf["hostcall_cpu_profiled"].type == 'B' && perf["hostcall_cpu_us"].to_u64() <= perf["hostcall_us"].to_u64() + 2 && operation_kinds > 0 && operation_kinds <= 32 && ((dropped_hostcall_operations == 0 && operation_hostcalls == perf["hostcall_count"].to_u64() && operation_hostcall_us == perf["hostcall_us"].to_u64() && operation_hostcall_cpu_us == perf["hostcall_cpu_us"].to_u64()) || (dropped_hostcall_operations > 0 && operation_kinds == 32 && operation_hostcalls < perf["hostcall_count"].to_u64() && operation_hostcall_us <= perf["hostcall_us"].to_u64() && operation_hostcall_cpu_us <= perf["hostcall_cpu_us"].to_u64())); bool thread_runtime_profile_valid = perf["thread_runtime_profiled"].type == 'B' && (!perf["thread_runtime_profiled"].to_bool() || (perf["thread_cpu_start"].to_f64() >= 0 && perf["thread_cpu_end"].to_f64() >= 0 && perf["thread_cpu_migrated"].type == 'B' && perf["thread_user_cpu_us"].to_u64() + perf["thread_system_cpu_us"].to_u64() <= perf["workspace_wall_us"].to_u64() + 5 && perf["thread_voluntary_context_switches"].type != 'S' && perf["thread_involuntary_context_switches"].type != 'S' && perf["thread_minor_faults"].type != 'S' && perf["thread_major_faults"].type != 'S')); - bool perf_stable = transport_profile_valid && workspace_cpu_profile_valid && unit_module_profile_valid && hostcall_operation_profile_valid && thread_runtime_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'; + bool perf_stable = transport_profile_valid && workspace_cpu_profile_valid && unit_module_profile_valid && hostcall_operation_profile_valid && thread_runtime_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_loaded_reuse_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(); u64 profiled_hostcall_cpu_us = perf["hostcall_cpu_us"].to_u64(); u64 profiled_components = perf["component_resolve_count"].to_u64(); + u64 profiled_component_loaded_reuses = perf["component_loaded_reuse_count"].to_u64(); u64 profiled_component_path = perf["component_path_us"].to_u64(); u64 profiled_component_artifact = perf["component_artifact_us"].to_u64(); u64 profiled_component_load = perf["component_load_us"].to_u64(); @@ -203,7 +204,7 @@ RENDER(Request& context) for(u64 i = 0; i < 512 && perf_stable; i++) { perf = request_perf(); - perf_stable = 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() == profiled_hostcalls && perf["hostcall_us"].to_f64() == profiled_hostcall_us && perf["hostcall_cpu_us"].to_u64() == profiled_hostcall_cpu_us && perf["guest_us"].to_f64() > 0 && perf["component_resolve_count"].to_u64() == profiled_components && perf["component_path_us"].to_u64() == profiled_component_path && perf["component_artifact_us"].to_u64() == profiled_component_artifact && perf["component_load_us"].to_u64() == profiled_component_load && perf["component_link_us"].to_u64() == profiled_component_link && perf["unit_load_count"].to_u64() == profiled_unit_loads && perf["unit_module_us"].to_u64() == profiled_unit_module && perf["unit_allocate_us"].to_u64() == profiled_unit_allocate && perf["unit_import_us"].to_u64() == profiled_unit_import && perf["unit_symbol_resolve_count"].to_u64() == profiled_unit_symbol_resolve_count && perf["unit_symbol_resolve_us"].to_u64() == profiled_unit_symbol_resolve && perf["unit_instantiate_us"].to_u64() == profiled_unit_instantiate && perf["unit_initialize_us"].to_u64() == profiled_unit_initialize && json_encode(perf["mysql_operations"]) == profiled_mysql_operations && json_encode(perf["hostcall_operations"]) == profiled_hostcall_operations; + perf_stable = 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() == profiled_hostcalls && perf["hostcall_us"].to_f64() == profiled_hostcall_us && perf["hostcall_cpu_us"].to_u64() == profiled_hostcall_cpu_us && perf["guest_us"].to_f64() > 0 && perf["component_resolve_count"].to_u64() == profiled_components && perf["component_loaded_reuse_count"].to_u64() == profiled_component_loaded_reuses && perf["component_path_us"].to_u64() == profiled_component_path && perf["component_artifact_us"].to_u64() == profiled_component_artifact && perf["component_load_us"].to_u64() == profiled_component_load && perf["component_link_us"].to_u64() == profiled_component_link && perf["unit_load_count"].to_u64() == profiled_unit_loads && perf["unit_module_us"].to_u64() == profiled_unit_module && perf["unit_allocate_us"].to_u64() == profiled_unit_allocate && perf["unit_import_us"].to_u64() == profiled_unit_import && perf["unit_symbol_resolve_count"].to_u64() == profiled_unit_symbol_resolve_count && perf["unit_symbol_resolve_us"].to_u64() == profiled_unit_symbol_resolve && perf["unit_instantiate_us"].to_u64() == profiled_unit_instantiate && perf["unit_initialize_us"].to_u64() == profiled_unit_initialize && json_encode(perf["mysql_operations"]) == profiled_mysql_operations && json_encode(perf["hostcall_operations"]) == profiled_hostcall_operations; } check("request_perf() stages stable repeated snapshots", perf_stable, json_encode(perf)); diff --git a/src/wasm/backend.cpp b/src/wasm/backend.cpp index 4510068..5eb0b2d 100644 --- a/src/wasm/backend.cpp +++ b/src/wasm/backend.cpp @@ -221,6 +221,7 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri request.header["X-UCE-Wasm-Entry-Dispatch-Us"] = std::to_string(response.entry_dispatch_us); request.header["X-UCE-Wasm-Output-Collect-Us"] = std::to_string(response.output_collect_us); request.header["X-UCE-Wasm-Component-Resolve-Count"] = std::to_string(response.component_resolve_count); + request.header["X-UCE-Wasm-Component-Loaded-Reuse-Count"] = std::to_string(response.component_loaded_reuse_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); diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index 62e4687..2fdc6f5 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -543,7 +543,8 @@ struct RequestPropsScope // probe that loads nothing). No per-mode kinds. static s32 wasm_resolve_target(String unit_target, String handler, String* resolved_out = 0) { - String cache_key = handler + "\t" + unit_target; + String current = context ? context->resources.current_unit_file : ""; + String cache_key = current + "\t" + handler + "\t" + unit_target; bool is_exists = (handler == "exists"); auto cached = wasm_component_slots.find(cache_key); if(cached != wasm_component_slots.end() && !is_exists) @@ -553,7 +554,6 @@ static s32 wasm_resolve_target(String unit_target, String handler, String* resol return(cached->second); } char resolved[512]; - String current = context ? context->resources.current_unit_file : ""; s32 slot = uce_host_component_resolve( unit_target.data(), unit_target.size(), handler.data(), handler.size(), current.data(), current.size(), diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 99e9c99..d529315 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -201,6 +201,7 @@ struct WasmRequestProfile u64 output_collect_us = 0; u64 workspace_complete_us = 0; u64 component_resolve_count = 0; + u64 component_loaded_reuse_count = 0; u64 component_resolve_total_us = 0; u64 component_path_total_us = 0; u64 component_artifact_total_us = 0; @@ -1732,6 +1733,7 @@ private: }; std::vector units; std::map units_by_source; + std::map component_loaded_paths; // caller + target → request-loaded canonical source std::map handler_slots; // source + ":" + symbol → table slot bool stale_component_mutation_blocked = false; String stale_component_mutation_status; @@ -2386,7 +2388,27 @@ private: return(0); } - String resolved = resolve_source_path(file_name, current_unit); + String resolve_key = current_unit + "\t" + file_name; + String resolved; + String error; + size_t unit_index = 0; + bool loaded_reuse = false; + auto loaded = units_by_source.find(file_name); + if(loaded == units_by_source.end()) + { + auto known_path = component_loaded_paths.find(resolve_key); + if(known_path != component_loaded_paths.end()) + loaded = units_by_source.find(known_path->second); + } + if(loaded != units_by_source.end()) + { + resolved = loaded->first; + unit_index = loaded->second; + loaded_reuse = true; + component_loaded_reuse_count++; + } + else + resolved = resolve_source_path(file_name, current_unit); component_path_total_us += (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - probe_start).count(); if(resolved == "") @@ -2401,67 +2423,70 @@ private: return(1); } - auto artifact_start = std::chrono::steady_clock::now(); - bool artifact_exists = file_exists_host(worker.unit_wasm_path(resolved)); - bool can_serve_stale = compiler_request_can_serve_stale_artifact(context); - String method = context ? to_upper(trim(context->params["REQUEST_METHOD"])) : String(""); - bool read_request = method == "GET" || method == "HEAD" || method == "OPTIONS"; - if(can_serve_stale && read_request && !component_source_generation_checked) + if(!loaded_reuse) { - component_source_generation = compiler_source_generation(context); - component_source_generation_checked = true; - } - bool stale = false; - if(artifact_exists) - { - auto now = std::chrono::steady_clock::now(); - auto cached_freshness = worker.component_freshness.find(resolved); - // HTTP reads may serve a complete stale artifact, so bound their graph - // stat work; CLI and mutations always check the current graph. - bool generation_available = component_source_generation != ""; - bool check_freshness = !can_serve_stale || !read_request || - cached_freshness == worker.component_freshness.end() || - (generation_available ? - cached_freshness->second.source_generation != component_source_generation : - std::chrono::duration_cast( - now - cached_freshness->second.checked_at).count() >= 1000); - if(check_freshness) + auto artifact_start = std::chrono::steady_clock::now(); + bool artifact_exists = file_exists_host(worker.unit_wasm_path(resolved)); + bool can_serve_stale = compiler_request_can_serve_stale_artifact(context); + String method = context ? to_upper(trim(context->params["REQUEST_METHOD"])) : String(""); + bool read_request = method == "GET" || method == "HEAD" || method == "OPTIONS"; + if(can_serve_stale && read_request && !component_source_generation_checked) { - stale = compiler_unit_needs_recompile(context, resolved, 0, can_serve_stale && read_request, true); - worker.component_freshness[resolved] = { now, stale, component_source_generation }; + component_source_generation = compiler_source_generation(context); + component_source_generation_checked = true; } - else - stale = cached_freshness->second.stale; - } - if(stale && can_serve_stale) - { - compiler_prioritize_unit(context, resolved); - if(!read_request) + bool stale = false; + if(artifact_exists) { - stale_component_mutation_blocked = true; - stale_component_mutation_status = context->params["GATEWAY_INTERFACE"] != "" ? - "Status: 503 Service Unavailable" : "HTTP/1.1 503 Service Unavailable"; - component_artifact_total_us += (u64)std::chrono::duration_cast( - std::chrono::steady_clock::now() - artifact_start).count(); + auto now = std::chrono::steady_clock::now(); + auto cached_freshness = worker.component_freshness.find(resolved); + // HTTP reads may serve a complete stale artifact, so bound their graph + // stat work; CLI and mutations always check the current graph. + bool generation_available = component_source_generation != ""; + bool check_freshness = !can_serve_stale || !read_request || + cached_freshness == worker.component_freshness.end() || + (generation_available ? + cached_freshness->second.source_generation != component_source_generation : + std::chrono::duration_cast( + now - cached_freshness->second.checked_at).count() >= 1000); + if(check_freshness) + { + stale = compiler_unit_needs_recompile(context, resolved, 0, can_serve_stale && read_request, true); + worker.component_freshness[resolved] = { now, stale, component_source_generation }; + } + else + stale = cached_freshness->second.stale; + } + if(stale && can_serve_stale) + { + compiler_prioritize_unit(context, resolved); + if(!read_request) + { + stale_component_mutation_blocked = true; + stale_component_mutation_status = context->params["GATEWAY_INTERFACE"] != "" ? + "Status: 503 Service Unavailable" : "HTTP/1.1 503 Service Unavailable"; + component_artifact_total_us += (u64)std::chrono::duration_cast( + std::chrono::steady_clock::now() - artifact_start).count(); + record_probe(); + return(0); + } + } + if(!artifact_exists || (stale && !can_serve_stale)) + get_shared_unit(context, resolved); + component_artifact_total_us += (u64)std::chrono::duration_cast( + std::chrono::steady_clock::now() - artifact_start).count(); + + auto load_start = std::chrono::steady_clock::now(); + error = load_unit(resolved, "component", unit_index); + component_load_total_us += (u64)std::chrono::duration_cast( + std::chrono::steady_clock::now() - load_start).count(); + if(error != "") + { + fprintf(stderr, "[wasm] component load failed: %s\n", error.c_str()); record_probe(); return(0); } - } - if(!artifact_exists || (stale && !can_serve_stale)) - get_shared_unit(context, resolved); - component_artifact_total_us += (u64)std::chrono::duration_cast( - std::chrono::steady_clock::now() - artifact_start).count(); - - auto load_start = std::chrono::steady_clock::now(); - size_t unit_index = 0; - String error = load_unit(resolved, "component", unit_index); - component_load_total_us += (u64)std::chrono::duration_cast( - std::chrono::steady_clock::now() - load_start).count(); - if(error != "") - { - fprintf(stderr, "[wasm] component load failed: %s\n", error.c_str()); - record_probe(); - return(0); + component_loaded_paths[resolve_key] = resolved; } auto link_start = std::chrono::steady_clock::now(); auto record_link = [&]() { @@ -2723,6 +2748,7 @@ private: response["memcache_hostcall_count"] = (f64)self->memcache_hostcall_count; response["memcache_hostcall_us"] = (f64)self->memcache_hostcall_total_us; response["component_resolve_count"] = (f64)self->component_resolve_count; + response["component_loaded_reuse_count"] = (f64)self->component_loaded_reuse_count; response["component_resolve_us"] = (f64)self->component_resolve_total_us; response["component_path_us"] = (f64)self->component_path_total_us; response["component_artifact_us"] = (f64)self->component_artifact_total_us;