diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index 1335513..31521fa 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_component_once_prefetch.sh scripts/test_relative_component_cache.sh scripts/test_password_hashing.sh scripts/test_mysql_epoch_refresh.sh diff --git a/scripts/test_component_once_prefetch.sh b/scripts/test_component_once_prefetch.sh new file mode 100755 index 0000000..bd6e873 --- /dev/null +++ b/scripts/test_component_once_prefetch.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +test_name="component-once-prefetch-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" + +printf '%s\n' \ + 'CLI(Request& context) {' \ + ' String first = component("child", context);' \ + ' String second = component("child", context);' \ + ' DValue perf = request_perf();' \ + ' print(first, ",", second, ",", perf["component_resolve_count"].to_u64(), ",", perf["component_loaded_reuse_count"].to_u64());' \ + '}' >"$source_dir/entry.uce" + +printf '%s\n' \ + 'ONCE(Request& context) { context.call["once"] = context.call["once"].to_u64() + 1; }' \ + 'COMPONENT(Request& context) { print(context.call["once"].to_u64()); }' >"$source_dir/child.uce" + +output=$(scripts/uce-cli "/$test_name/entry.uce") +if [[ "$output" != "1,1,1,0" ]]; then + echo "component ONCE slot was not returned with the primary handler: $output" >&2 + exit 1 +fi + +echo "component ONCE prefetch passed" diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index 2fdc6f5..e645503 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -466,7 +466,8 @@ extern "C" int32_t uce_host_component_resolve( const char* target, size_t target_len, const char* handler, size_t handler_len, const char* current_unit, size_t current_unit_len, - char* resolved_buf, size_t resolved_cap); + char* resolved_buf, size_t resolved_cap, + int32_t* once_slot_out); // target → table slot, reset per request (workspaces die with the request, // but a single workspace can render the same component many times) @@ -554,16 +555,26 @@ static s32 wasm_resolve_target(String unit_target, String handler, String* resol return(cached->second); } char resolved[512]; + s32 once_slot = 0; s32 slot = uce_host_component_resolve( unit_target.data(), unit_target.size(), handler.data(), handler.size(), current.data(), current.size(), - resolved, sizeof(resolved)); + resolved, sizeof(resolved), &once_slot); + String resolved_path = slot ? String(resolved, strnlen(resolved, sizeof(resolved))) : String(""); if(resolved_out && slot) - *resolved_out = String(resolved, strnlen(resolved, sizeof(resolved))); + *resolved_out = resolved_path; if(!is_exists) { wasm_component_slots[cache_key] = slot; - wasm_component_paths[cache_key] = slot ? String(resolved, strnlen(resolved, sizeof(resolved))) : String(""); + wasm_component_paths[cache_key] = resolved_path; + bool runs_once = handler == "render" || handler.rfind("render:", 0) == 0 || + handler == "component" || handler.rfind("component:", 0) == 0; + if(slot && runs_once) + { + String once_key = current + "\t" + "once" + "\t" + resolved_path; + wasm_component_slots[once_key] = once_slot; + wasm_component_paths[once_key] = once_slot ? resolved_path : String(""); + } } return(slot); } diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index ced3201..2006952 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -2527,8 +2527,11 @@ private: // hostcall body: uce_host_component_resolve(unit, handler, current) → slot. // `handler` names the export ("render", "component:CARD", "cli", // "serve_http:named", "once") or is "exists" (probe only, loads nothing). - int32_t component_resolve(const String& target, const String& handler, const String& current_unit, String& resolved_out) + int32_t component_resolve(const String& target, const String& handler, const String& current_unit, + String& resolved_out, int32_t* once_slot_out = 0) { + if(once_slot_out) + *once_slot_out = 0; auto probe_start = std::chrono::steady_clock::now(); auto record_probe = [&]() { component_resolve_count += 1; @@ -2675,38 +2678,38 @@ private: component_link_total_us += (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - link_start).count(); }; - String symbol = handler_export_symbol(handler); - String slot_key = resolved + ":" + symbol; - auto cached = handler_slots.find(slot_key); - if(cached != handler_slots.end()) - { - record_link(); - record_probe(); - return((int32_t)cached->second); - } - auto handler_fn = unit_func(unit_index, symbol); - if(!handler_fn) - { - // ONCE is optional per unit; a missing __uce_once is not an error - if(handler != "once") - fprintf(stderr, "[wasm] %s does not export %s\n", resolved.c_str(), symbol.c_str()); - record_link(); - record_probe(); - return(0); - } - u32 slot = 0; - error = place_funcref(*handler_fn, slot); - if(error != "") - { - fprintf(stderr, "[wasm] %s\n", error.c_str()); - record_link(); - record_probe(); - return(0); - } - handler_slots[slot_key] = slot; + auto link_handler = [&](const String& requested_handler) -> int32_t { + String symbol = handler_export_symbol(requested_handler); + String slot_key = resolved + ":" + symbol; + auto cached = handler_slots.find(slot_key); + if(cached != handler_slots.end()) + return((int32_t)cached->second); + auto handler_fn = unit_func(unit_index, symbol); + if(!handler_fn) + { + // ONCE is optional per unit; a missing __uce_once is not an error. + if(requested_handler != "once") + fprintf(stderr, "[wasm] %s does not export %s\n", resolved.c_str(), symbol.c_str()); + return(0); + } + u32 slot = 0; + error = place_funcref(*handler_fn, slot); + if(error != "") + { + fprintf(stderr, "[wasm] %s\n", error.c_str()); + return(0); + } + handler_slots[slot_key] = slot; + return((int32_t)slot); + }; + int32_t slot = link_handler(handler); + bool runs_once = handler == "render" || handler.rfind("render:", 0) == 0 || + handler == "component" || handler.rfind("component:", 0) == 0; + if(slot && once_slot_out && runs_once) + *once_slot_out = link_handler("once"); record_link(); record_probe(); - return((int32_t)slot); + return(slot); } String run_task_callback(u64 callback_id) @@ -4013,7 +4016,8 @@ private: self->hostcall_read(args[0].i32(), args[1].i32(), target); self->hostcall_read(args[2].i32(), args[3].i32(), handler); self->hostcall_read(args[4].i32(), args[5].i32(), current); - int32_t slot = self->component_resolve(target, handler, current, resolved); + int32_t once_slot = 0; + int32_t slot = self->component_resolve(target, handler, current, resolved, &once_slot); u32 cap = (u32)args[7].i32(); if(cap > 0) { @@ -4022,6 +4026,8 @@ private: resolved.push_back('\0'); self->hostcall_write(args[6].i32(), resolved); } + String once_slot_bytes((const char*)&once_slot, sizeof(once_slot)); + self->hostcall_write(args[8].i32(), once_slot_bytes); results[0] = Val(slot); return(std::monostate()); }));