Reuse loaded component units per request

This commit is contained in:
udo 2026-07-18 05:01:27 +00:00
parent 5e140d6a5a
commit a5ad5042b7
8 changed files with 133 additions and 62 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(),

View File

@ -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<LoadedUnit> units;
std::map<String, size_t> units_by_source;
std::map<String, String> component_loaded_paths; // caller + target → request-loaded canonical source
std::map<String, u32> 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::microseconds>(
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<std::chrono::milliseconds>(
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::microseconds>(
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<std::chrono::milliseconds>(
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::microseconds>(
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::microseconds>(
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::microseconds>(
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::microseconds>(
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::microseconds>(
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;