From 6b185875817c8e171a751bee4fd0340bbbcadd27 Mon Sep 17 00:00:00 2001 From: udo Date: Tue, 14 Jul 2026 22:41:55 +0000 Subject: [PATCH] scope nested component props without deep copies --- docs/wasm-runtime-architecture.md | 5 ++ scripts/run_cli_tests.sh | 1 + scripts/test_nested_component_props.sh | 65 ++++++++++++++++++++++++++ src/lib/dvalue.h | 6 +-- src/wasm/core.cpp | 34 ++++++++++---- 5 files changed, 98 insertions(+), 13 deletions(-) create mode 100755 scripts/test_nested_component_props.sh diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 06c6ea4..1287c1f 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -401,6 +401,11 @@ header free-functions are `inline`. The wasm backend exposes only declarations share one source, compile, artifact, and module-cache identity. The compiler independently enforces the same canonical identity, and persisted failures are reused only when their recorded source path matches the current request. + `scripts/test_nested_component_props.sh` passes a large prop tree through an + outer component, invokes 300 nested components, and verifies both inner and + caller props are restored. Its warm-request ceiling guards the request-scope + invariant that entering a nested component swaps the active prop trees in + constant time instead of deep-copying the outer tree for every child. - **Log timeliness**: the base process line-buffers stdout before forking workers, the proactive compiler, and the WebSocket broker. This keeps each diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index b82b23c..7407492 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -68,6 +68,7 @@ if [[ "$action" == "run" ]]; then done scripts/test_dependency_invalidation.sh scripts/test_cold_component_deadline.sh + scripts/test_nested_component_props.sh scripts/test_password_hashing.sh scripts/test_log_timeliness.sh fi diff --git a/scripts/test_nested_component_props.sh b/scripts/test_nested_component_props.sh new file mode 100755 index 0000000..6175c6a --- /dev/null +++ b/scripts/test_nested_component_props.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +test_name="nested-component-props-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" +threshold_ms="${UCE_NESTED_COMPONENT_PROPS_MAX_MS:-1500}" + +cleanup() { + rm -rf "$source_dir" +} +trap cleanup EXIT +mkdir -p "$source_dir" + +printf '%s\n' \ + 'CLI(Request& context) {' \ + ' context.props["sentinel"] = "caller";' \ + ' String payload; for(u64 i = 0; i < 4096; ++i) payload += "x";' \ + ' DValue props; for(u64 i = 0; i < 300; ++i) { DValue item; item = payload; props["items"].push(item); }' \ + ' String output = component("outer", props, context);' \ + ' if(context.props["sentinel"].to_string() != "caller") { print("caller props not restored"); return; }' \ + ' print(output);' \ + '}' >"$source_dir/parent.uce" + +printf '%s\n' \ + 'COMPONENT(Request& context) {' \ + ' if(context.props["items"]._map.size() != 300) { print("outer props missing"); return; }' \ + ' for(u64 i = 0; i < 300; ++i) {' \ + ' DValue props; props["index"] = std::to_string(i); component("leaf", props, context);' \ + ' if(context.props["items"]._map.size() != 300) { print("outer props not restored"); return; }' \ + ' }' \ + ' print("nested-component-props-ok");' \ + '}' >"$source_dir/outer.uce" + +printf '%s\n' \ + 'COMPONENT(Request& context) {' \ + ' if(context.props["index"].to_string() == "") print("leaf props missing");' \ + '}' >"$source_dir/leaf.uce" + +warm_output=$(scripts/uce-cli "/$test_name/parent.uce") +if [[ "$warm_output" != "nested-component-props-ok" ]]; then + echo "nested component props warmup failed: $warm_output" >&2 + exit 1 +fi + +start_ns=$(date +%s%N) +output=$(scripts/uce-cli "/$test_name/parent.uce") +elapsed_ms=$(( ($(date +%s%N) - start_ns) / 1000000 )) +if [[ "$output" != "nested-component-props-ok" ]]; then + echo "nested component props failed: $output" >&2 + exit 1 +fi +if (( elapsed_ms > threshold_ms )); then + echo "nested component props took ${elapsed_ms}ms (limit ${threshold_ms}ms)" >&2 + exit 1 +fi + +echo "nested component props passed in ${elapsed_ms}ms" diff --git a/src/lib/dvalue.h b/src/lib/dvalue.h index 4f0c2c4..3c92dc8 100644 --- a/src/lib/dvalue.h +++ b/src/lib/dvalue.h @@ -12,11 +12,11 @@ struct DValue { char type = 'S'; String _String; - f64 _float; + f64 _float = 0; s64 _array_index = 0; - bool _bool; + bool _bool = false; bool _list_mode = false; - void* _ptr; + void* _ptr = 0; std::map _map; // Read accessors are const and never create or modify nodes. The to_* diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index 64a7b7a..7c9c563 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -494,20 +494,32 @@ struct RequestPropsScope Request* context = 0; DValue previous_props; - RequestPropsScope(Request* context, const DValue& props) + static void swap_value(DValue& left, DValue& right) + { + std::swap(left.type, right.type); + left._String.swap(right._String); + std::swap(left._float, right._float); + std::swap(left._array_index, right._array_index); + std::swap(left._bool, right._bool); + std::swap(left._list_mode, right._list_mode); + std::swap(left._ptr, right._ptr); + left._map.swap(right._map); + } + + RequestPropsScope(Request* context, DValue& props) { this->context = context; if(this->context) { - previous_props = this->context->props; - this->context->props = props; + swap_value(previous_props, this->context->props); + swap_value(this->context->props, props); } } ~RequestPropsScope() { if(context) - context->props = previous_props; + swap_value(context->props, previous_props); } }; @@ -600,7 +612,8 @@ DValue* unit_call(String file_name, String function_name, DValue* call_param) print("Error: unit_call() function '", function_name, "' not found"); return(0); } - RequestPropsScope props_scope(context, (call_param ? *call_param : DValue())); + DValue props = call_param ? *call_param : DValue(); + RequestPropsScope props_scope(context, props); if((handler == "render" || handler.rfind("render:", 0) == 0 || handler == "component" || handler.rfind("component:", 0) == 0) && resolved != "") wasm_run_once(resolved, *context); String previous_unit = context->resources.current_unit_file; @@ -634,7 +647,7 @@ DValue* unit_call(String file_name, String function_name, DValue* call_param) return(0); } -void component_render(String name, DValue props, Request& request) +static void component_render_with_props(String name, DValue& props, Request& request) { String file_name, render_name; component_parse_target(trim(name), file_name, render_name); @@ -658,14 +671,15 @@ void component_render(String name, DValue props, Request& request) request.resources.current_unit_file = previous_unit; } -void component_render(String name) { DValue props; component_render(name, props, *context); } -void component_render(String name, Request& request) { DValue props; component_render(name, props, request); } -void component_render(String name, DValue props) { component_render(name, props, *context); } +void component_render(String name, DValue props, Request& request) { component_render_with_props(name, props, request); } +void component_render(String name) { DValue props; component_render_with_props(name, props, *context); } +void component_render(String name, Request& request) { DValue props; component_render_with_props(name, props, request); } +void component_render(String name, DValue props) { component_render_with_props(name, props, *context); } String component(String name, DValue props, Request& request) { ob_start(); - component_render(name, props, request); + component_render_with_props(name, props, request); return(ob_get_close()); }