diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc index b224a77a..22dcbdcd 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc @@ -596,6 +596,22 @@ if (!valid_priority) return; if (name.starts_with("--")) { if (!valid_custom_property_name(name)) return; + if (!value.empty()) { + const auto& authored = node->authored_style(); + const auto existing = authored.declarations.find(name); + if (existing != authored.declarations.end() + && existing->second == value + && authored.important_declarations.contains(name) == important) { +#if defined(WEBSCENE_NATIVE_ENGINE_CERTIFICATION) + if (self->resize_profile_active) { + ++self->last_resize_redundant_style_writes; + } +#endif + self->notify_custom_element_attribute( + *node, "style", old_value, node->attributes.at("style")); + return; + } + } if (value.empty()) { if (!has_inline_style_declaration(*node, name)) { #if defined(WEBSCENE_NATIVE_ENGINE_CERTIFICATION) diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_browser_dom_tests.inc b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_browser_dom_tests.inc index a783fe40..66328bd2 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_browser_dom_tests.inc +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_browser_dom_tests.inc @@ -4311,6 +4311,159 @@ void test_async_save_acknowledgement_publishes_without_pointer_input() webscene_engine_destroy(engine); } +void test_identical_custom_property_writes_do_not_schedule_visual_work() +{ + auto* engine = webscene_engine_create(0); + require(engine != nullptr, "custom-property no-op engine creation failed"); + + const webscene_scene_view* initial = nullptr; + for (auto attempt = 0; attempt < 100 && initial == nullptr; ++attempt) { + initial = webscene_engine_acquire_latest_scene(engine); + if (initial == nullptr) { + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + } + require(initial != nullptr, "custom-property no-op fixture had no initial scene"); + require( + webscene_scene_acknowledge(initial) != 0, + "custom-property no-op fixture could not acknowledge its initial scene"); + webscene_scene_release(initial); + + execute_and_wait( + engine, + R"JS( + const style = document.createElement('style'); + style.textContent = '#custom-property-noop { width: var(--editor-width); height: 24px; }'; + document.head.appendChild(style); + const target = document.createElement('div'); + target.id = 'custom-property-noop'; + target.style.setProperty('--editor-width', '120px'); + document.body.appendChild(target); + )JS", + "custom-property-noop-setup.js"); + require( + evaluate( + engine, + "document.getElementById('custom-property-noop').offsetWidth", + "custom-property-noop-ready.js") == "120", + "custom-property no-op fixture did not complete initial layout"); + + auto setup_scene_acknowledged = false; + for (auto attempt = 0; attempt < 100; ++attempt) { + const auto* scene = webscene_engine_acquire_latest_scene(engine); + if (scene != nullptr) { + require( + webscene_scene_acknowledge(scene) != 0, + "custom-property no-op setup scene could not be acknowledged"); + webscene_scene_release(scene); + setup_scene_acknowledged = true; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + require( + setup_scene_acknowledged, + "custom-property no-op setup scene was not published"); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + + webscene_engine_metrics connected_before{}; + webscene_engine_get_metrics(engine, &connected_before); + execute_and_wait( + engine, + R"JS( + const target = document.getElementById('custom-property-noop'); + for (let index = 0; index < 4096; ++index) { + target.style.setProperty('--editor-width', '120px'); + } + )JS", + "custom-property-noop-connected.js"); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + webscene_engine_metrics connected_after{}; + webscene_engine_get_metrics(engine, &connected_after); + require( + connected_after.layout_passes == connected_before.layout_passes + && connected_after.published_scenes == connected_before.published_scenes, + "identical connected custom-property writes scheduled layout or publication"); + + execute_and_wait( + engine, + R"JS( + globalThis.__customPropertyNoopTarget = + document.getElementById('custom-property-noop'); + globalThis.__customPropertyNoopTarget.remove(); + )JS", + "custom-property-noop-detach.js"); + auto detach_scene_acknowledged = false; + for (auto attempt = 0; attempt < 100; ++attempt) { + const auto* scene = webscene_engine_acquire_latest_scene(engine); + if (scene != nullptr) { + require( + webscene_scene_acknowledge(scene) != 0, + "custom-property no-op detach scene could not be acknowledged"); + webscene_scene_release(scene); + detach_scene_acknowledged = true; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + require( + detach_scene_acknowledged, + "custom-property no-op detach scene was not published"); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + + webscene_engine_metrics detached_before{}; + webscene_engine_get_metrics(engine, &detached_before); + execute_and_wait( + engine, + R"JS( + for (let index = 0; index < 4096; ++index) { + __customPropertyNoopTarget.style.setProperty('--editor-width', '120px'); + } + )JS", + "custom-property-noop-detached.js"); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + webscene_engine_metrics detached_after{}; + webscene_engine_get_metrics(engine, &detached_after); + require( + detached_after.layout_passes == detached_before.layout_passes + && detached_after.published_scenes == detached_before.published_scenes, + "identical detached custom-property writes scheduled visual work"); + + execute_and_wait( + engine, + R"JS( + document.body.appendChild(__customPropertyNoopTarget); + delete globalThis.__customPropertyNoopTarget; + )JS", + "custom-property-noop-reattach.js"); + webscene_engine_metrics reattached{}; + for (auto attempt = 0; attempt < 100; ++attempt) { + webscene_engine_get_metrics(engine, &reattached); + if (reattached.published_scenes > detached_after.published_scenes) break; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + require( + evaluate( + engine, + R"JS((() => { + const target = document.getElementById('custom-property-noop'); + return { + specified: target.style.getPropertyValue('--editor-width'), + width: target.offsetWidth, + connected: target.isConnected + }; + })())JS", + "custom-property-noop-reattach-result.js") + == R"({"specified":"120px","width":120,"connected":true})", + "custom-property no-op state did not survive detach and reconnect"); + webscene_engine_get_metrics(engine, &reattached); + require( + reattached.layout_passes == detached_after.layout_passes + 1U + && reattached.published_scenes == detached_after.published_scenes + 1U, + "custom-property no-op reconnect escaped one layout/publication"); + webscene_engine_destroy(engine); +} + void test_ordered_scene_consumer_preserves_two_diff_chain() { auto* engine = webscene_engine_create(64); diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp index 8fa28c59..78e49fd5 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp @@ -1525,6 +1525,7 @@ int main() test_runtime_work_is_attributed(); test_scene_flow_is_attributed(); test_read_only_evaluation_does_not_publish_scene(); + test_identical_custom_property_writes_do_not_schedule_visual_work(); test_async_save_acknowledgement_publishes_without_pointer_input(); test_ordered_scene_consumer_preserves_two_diff_chain(); test_keyboard_and_pointer_focus_modality();