Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/native-runtime-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,15 @@ jobs:
--upstream-v8 \
--partition-alloc \
--output "$GITHUB_WORKSPACE/artifacts/nuget-packages"
- name: Upload exact macOS native symbols
if: matrix.rid == 'osx-arm64'
uses: actions/upload-artifact@v4
with:
name: native-symbols-${{ matrix.rid }}-${{ needs.metadata.outputs.package-version }}
path: artifacts/native-engine-runtime-build/**/*.dSYM/**
if-no-files-found: error
compression-level: 9
retention-days: 3
- name: Build, pack, and test Linux runtime
if: matrix.rid == 'linux-x64'
shell: bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@
pending_programmatic_scroll_events.clear();
pending_interop_promises.clear();
pending_callback_promises.clear();
// Native host and file completions can still be pending when a window
// closes. Their persistent context/resolver handles must be released
// while the isolate is alive; member destruction happens after this
// body and therefore after isolate disposal below.
host_promise_targets.clear();
file_targets.clear();
pending_fetches.clear();
interop_handles.clear();
pending_promise_rejections.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3180,6 +3180,127 @@ void test_native_clipboard_shortcut_events(webscene_engine*)
webscene_engine_destroy(engine);
}

void test_native_legacy_clipboard_completion_stress(webscene_engine*)
{
auto* engine = webscene_engine_create(0);
require(engine != nullptr,
"legacy clipboard completion stress engine creation failed");
execute_and_wait(engine, R"JS(
document.body.innerHTML = '<textarea id="legacy-copy-stress"></textarea>';
const editor = document.getElementById('legacy-copy-stress');
globalThis.__legacyCopyStressEvents = 0;
globalThis.__legacyCopyStressResults = [];
editor.addEventListener('keydown', event => {
if (event.metaKey && event.key.toLowerCase() === 'c') {
event.preventDefault();
__legacyCopyStressResults.push(document.execCommand('copy'));
}
});
editor.addEventListener('copy', event => {
event.clipboardData.setData('text/plain', 'legacy stress');
event.preventDefault();
++__legacyCopyStressEvents;
});
editor.focus();
)JS", "native-legacy-clipboard-stress-setup.js");

constexpr unsigned operation_count = 10000U;
constexpr unsigned batch_size = 16U;
constexpr auto modifier = WEBSCENE_INPUT_MODIFIER_META;
uint64_t sequence = 20000U;
const auto started = std::chrono::steady_clock::now();
for (unsigned offset = 0; offset < operation_count; offset += batch_size) {
for (unsigned index = 0; index < batch_size; ++index) {
keyboard_input(
engine, WEBSCENE_INPUT_KEY_DOWN, 'C', sequence++, modifier);
}
webscene_engine_metrics metrics{};
webscene_engine_get_metrics(engine, &metrics);
wait_for_consumed_inputs(
engine, metrics.enqueued_inputs,
"legacy clipboard stress input was not consumed");
for (unsigned index = 0; index < batch_size; ++index) {
const auto request = take_typed_host_request(engine);
require(
request.kind == WEBSCENE_HOST_REQUEST_CLIPBOARD_WRITE_V1
&& request.flags
== WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1
&& request.content_type == "text/plain"
&& std::string(request.bytes.begin(), request.bytes.end())
== "legacy stress",
"legacy clipboard stress request changed");
require(webscene_engine_complete_host_request_v1(
engine, request.id, 0U, "text/plain",
nullptr, 0U, nullptr) != 0,
"legacy clipboard stress completion was rejected");
}
const auto expected = offset + batch_size;
require(evaluate(
engine,
"__legacyCopyStressEvents === " + std::to_string(expected)
+ " && __legacyCopyStressResults.length === "
+ std::to_string(expected)
+ " && __legacyCopyStressResults.every(Boolean)",
"native-legacy-clipboard-stress-barrier.js") == "true",
"legacy clipboard stress did not drain completions in order");
}
require(!take_typed_host_request(engine),
"legacy clipboard stress retained a host request");
const auto elapsed = std::chrono::duration<double>(
std::chrono::steady_clock::now() - started).count();
require(elapsed < 15.0,
"10,000 legacy clipboard completions exceeded fifteen seconds");
std::cout << "Legacy clipboard completion gate: operations="
<< operation_count << " pendingHighWater=" << batch_size
<< " elapsed=" << elapsed << "s\n";
// Reproduce the packaged close boundary: the worker destroys all resolved
// promise/microtask state immediately after the last public-ABI barrier.
webscene_engine_destroy(engine);
}

void test_native_pending_legacy_clipboard_shutdown(webscene_engine*)
{
auto* engine = webscene_engine_create(0);
require(engine != nullptr,
"pending legacy clipboard shutdown engine creation failed");
execute_and_wait(engine, R"JS(
document.body.innerHTML = '<textarea id="pending-copy"></textarea>';
const editor = document.getElementById('pending-copy');
editor.addEventListener('keydown', event => {
if (event.metaKey && event.key.toLowerCase() === 'c') {
event.preventDefault();
document.execCommand('copy');
}
});
editor.addEventListener('copy', event => {
event.clipboardData.setData('text/plain', 'pending close');
event.preventDefault();
});
editor.focus();
)JS", "native-pending-legacy-clipboard-shutdown-setup.js");

keyboard_input(
engine, WEBSCENE_INPUT_KEY_DOWN, 'C', 31001U,
WEBSCENE_INPUT_MODIFIER_META);
webscene_engine_metrics metrics{};
webscene_engine_get_metrics(engine, &metrics);
wait_for_consumed_inputs(
engine, metrics.enqueued_inputs,
"pending legacy clipboard shutdown input was not consumed");
const auto request = take_typed_host_request(engine);
require(
request.kind == WEBSCENE_HOST_REQUEST_CLIPBOARD_WRITE_V1
&& request.content_type == "text/plain"
&& std::string(request.bytes.begin(), request.bytes.end())
== "pending close",
"pending legacy clipboard shutdown did not retain its host promise");

// A native window may close before the operating-system host completes a
// request. Destruction must release the pending persistent resolver while
// its V8 isolate is still alive.
webscene_engine_destroy(engine);
}

void test_clipboard_maximum_payload_gate(webscene_engine* engine)
{
constexpr size_t maximum_bytes = 16U * 1024U * 1024U;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ int main()
webscene_engine_destroy(focused_engine);
return 0;
}
if (selected == "input-diagnostics-pointer-focus") {
test_document_direction_and_visibility_are_native_properties();
test_native_legacy_clipboard_completion_stress(nullptr);
test_native_pending_legacy_clipboard_shutdown(nullptr);
return 0;
}
if (selected == "media-query-list") {
auto* focused_engine = webscene_engine_create(0);
require(focused_engine != nullptr, "focused engine creation failed");
Expand Down Expand Up @@ -925,6 +931,8 @@ int main()
test_clipboard_write_text_host_handoff(engine);
test_clipboard_read_host_completion(engine);
test_native_clipboard_shortcut_events(engine);
test_native_legacy_clipboard_completion_stress(engine);
test_native_pending_legacy_clipboard_shutdown(engine);
test_clipboard_maximum_payload_gate(engine);
test_clipboard_small_round_trip_performance(engine);
test_fullscreen_host_completion(engine);
Expand Down
11 changes: 9 additions & 2 deletions scripts/build-native-engine-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,12 @@ elif [[ "$expected_kernel" == Linux ]]; then
-DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=lld
)
fi
if [[ "$expected_kernel" == Darwin && "$cmake_build_type" == Release ]]; then
# Keep line tables only until dsymutil has emitted the exact shipped
# binary's external symbols. strip removes them from the runtime before
# packaging, so diagnostics do not increase the installed footprint.
cmake_args+=("-DCMAKE_CXX_FLAGS_RELEASE=-O3 -DNDEBUG -gline-tables-only")
fi
cmake "${cmake_args[@]}"
cmake --build "$build_dir" --config "$cmake_build_type" --parallel
cmake -E copy_if_different "$icu_data" "$build_dir/icudtl.dat"
Expand All @@ -408,15 +414,16 @@ if [[ "$expected_kernel" == Darwin ]]; then
echo "Native engine deployment target is '$actual_macos_deployment_target'; expected '$macos_deployment_target'." >&2
exit 1
fi
fi
if [[ "$expected_kernel" == Darwin && "$cmake_build_type" == RelWithDebInfo ]]; then
native_dsym_path="$native_path.dSYM"
cmake -E remove_directory "$native_dsym_path"
dsymutil "$native_path" -o "$native_dsym_path"
if [[ ! -d "$native_dsym_path" ]]; then
echo "Native engine build did not produce '$native_dsym_path'." >&2
exit 1
fi
if [[ "$cmake_build_type" == Release ]]; then
strip -S "$native_path"
fi
fi
snapshot_path="$build_dir/webscene_bootstrap_snapshot.bin"
snapshot_metadata_path="$build_dir/webscene_bootstrap_snapshot.meta"
Expand Down
34 changes: 34 additions & 0 deletions tests/WebScene.Architecture.Tests/ReleaseCompatibilityGateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,40 @@ public void RuntimeBuildersRunTheCompleteRequiredCompatibilityProfile()
AssertBuilderRunsCompleteRequiredProfile("build-native-engine-runtime.ps1");
}

[Fact]
public void MacosReleaseRuntimePublishesExactBoundedNativeSymbols()
{
var builder = File.ReadAllText(Path.Combine(
s_repositoryRoot,
"scripts",
"build-native-engine-runtime.sh"));
Assert.Contains("-gline-tables-only", builder, StringComparison.Ordinal);
Assert.Contains("dsymutil \"$native_path\"", builder, StringComparison.Ordinal);
Assert.Contains("strip -S \"$native_path\"", builder, StringComparison.Ordinal);
var releaseOnlyGuard = builder.IndexOf(
"if [[ \"$expected_kernel\" == Darwin && \"$cmake_build_type\" == Release ]]; then",
StringComparison.Ordinal);
var lineTables = builder.IndexOf("-gline-tables-only", StringComparison.Ordinal);
var configure = builder.IndexOf("cmake \"${cmake_args[@]}\"", StringComparison.Ordinal);
Assert.True(
releaseOnlyGuard >= 0 && releaseOnlyGuard < lineTables && lineTables < configure,
"External symbol generation must be limited to the macOS Release package build.");
Assert.True(
builder.IndexOf("dsymutil \"$native_path\"", StringComparison.Ordinal)
< builder.IndexOf("strip -S \"$native_path\"", StringComparison.Ordinal),
"The exact runtime must be symbolized before its package copy is stripped, leaving no normal-path runtime cost.");

var workflow = File.ReadAllText(Path.Combine(
s_repositoryRoot,
".github",
"workflows",
"native-runtime-packages.yml"));
Assert.Contains("name: Upload exact macOS native symbols", workflow, StringComparison.Ordinal);
Assert.Contains("path: artifacts/native-engine-runtime-build/**/*.dSYM/**", workflow, StringComparison.Ordinal);
Assert.Contains("compression-level: 9", workflow, StringComparison.Ordinal);
Assert.Contains("retention-days: 3", workflow, StringComparison.Ordinal);
}

[Fact]
public void RequiredProfileContainsTheEstablishedReleaseDenominator()
{
Expand Down
Loading