diff --git a/change/react-native-windows-2f7f8ddd-d029-440c-903f-a66737129e31.json b/change/react-native-windows-2f7f8ddd-d029-440c-903f-a66737129e31.json new file mode 100644 index 00000000000..9611274fc58 --- /dev/null +++ b/change/react-native-windows-2f7f8ddd-d029-440c-903f-a66737129e31.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Revert TurboModule property caching and add opt-in native lifetime integration tests", + "packageName": "react-native-windows", + "email": "email not defined", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj index e3689a7623c..674cc7658fc 100644 --- a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj +++ b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj @@ -150,6 +150,9 @@ Create + + $(ReactNativeWindowsDir)Microsoft.ReactNative;$(ReactNativeWindowsDir)Common;$(IntDir)..\React.Windows.Desktop\Generated Files;%(AdditionalIncludeDirectories) + diff --git a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj index 3f206877319..2fc983c47e7 100644 --- a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj +++ b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj @@ -113,6 +113,9 @@ + + $(V8LifetimeTestOutput)\build\native\include;%(AdditionalIncludeDirectories) + @@ -157,6 +160,15 @@ + + + + + + + + + diff --git a/vnext/Desktop.IntegrationTests/TurboModuleLifetimeTestHooks.cpp b/vnext/Desktop.IntegrationTests/TurboModuleLifetimeTestHooks.cpp new file mode 100644 index 00000000000..5df76f80d94 --- /dev/null +++ b/vnext/Desktop.IntegrationTests/TurboModuleLifetimeTestHooks.cpp @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include +#include +#include + +// Compiled into react-native-win32.dll only for EnableV8LifetimeTests builds. +extern "C" __declspec(dllexport) void __cdecl RNWCreateTurboModuleForLifetimeTest( + facebook::jsi::Runtime *runtime, + facebook::jsi::Object *result) { + namespace React = winrt::Microsoft::ReactNative; + React::TurboModulesProvider provider; + provider.AddModuleProvider( + L"LifetimeTest", + [](React::IReactModuleBuilder const &builder) -> winrt::Windows::Foundation::IInspectable { + builder.AddSyncMethod( + L"value", [](React::IJSValueReader const &, React::IJSValueWriter const &writer) { writer.WriteInt64(42); }); + return winrt::box_value(true); + }, + false); + *result = facebook::jsi::Object::createFromHostObject(*runtime, provider.getModule("LifetimeTest", nullptr)); +} diff --git a/vnext/Desktop.IntegrationTests/TurboModuleLifetimeTests.cpp b/vnext/Desktop.IntegrationTests/TurboModuleLifetimeTests.cpp new file mode 100644 index 00000000000..fd8061c8dea --- /dev/null +++ b/vnext/Desktop.IntegrationTests/TurboModuleLifetimeTests.cpp @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +namespace jsi = facebook::jsi; + +namespace Microsoft::React::Test { +namespace { + +using Stats = v8runtime::HostProxyLifetimeStats; +using CreateModule = void(__cdecl *)(jsi::Runtime *, jsi::Object *); +using CreateRuntime = jsi::Runtime *(__cdecl *)(); +using GetStats = void(__cdecl *)(Stats *) noexcept; + +template +T GetExport(HMODULE library, char const *name) { + auto address = GetProcAddress(library, name); + Assert::IsTrue(address != nullptr, L"Missing lifetime-test export; build both DLLs with lifetime tests enabled."); + return reinterpret_cast(address); +} + +class LifetimeTestRuntime { + public: + LifetimeTestRuntime() { + auto rnw = GetModuleHandleW(L"react-native-win32.dll"); + Assert::IsNotNull(rnw, L"The test must load react-native-win32.dll."); + wchar_t path[MAX_PATH]; + auto length = GetModuleFileNameW(rnw, path, MAX_PATH); + Assert::IsTrue(length != 0 && length < MAX_PATH, L"Cannot locate the RNW DLL."); + auto v8Path = std::filesystem::path(path).parent_path() / L"v8jsi.dll"; + m_v8.reset( + LoadLibraryExW(v8Path.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)); + Assert::IsNotNull(m_v8.get(), L"Cannot load the instrumented v8jsi.dll beside react-native-win32.dll."); +#if defined(_M_IX86) + constexpr char createModuleName[] = "_RNWCreateTurboModuleForLifetimeTest"; + constexpr char createRuntimeName[] = "_v8jsi_create_runtime_for_lifetime_test"; + constexpr char getStatsName[] = "_v8jsi_get_host_proxy_lifetime_stats"; +#else + constexpr char createModuleName[] = "RNWCreateTurboModuleForLifetimeTest"; + constexpr char createRuntimeName[] = "v8jsi_create_runtime_for_lifetime_test"; + constexpr char getStatsName[] = "v8jsi_get_host_proxy_lifetime_stats"; +#endif + m_createModule = GetExport(rnw, createModuleName); + m_createRuntime = GetExport(m_v8.get(), createRuntimeName); + m_getStats = GetExport(m_v8.get(), getStatsName); + } + + std::unique_ptr Create() const { + return std::unique_ptr{m_createRuntime()}; + } + + jsi::Object Module(jsi::Runtime &runtime) const { + jsi::Object module(runtime); + m_createModule(&runtime, &module); + return module; + } + + Stats Snapshot() const { + Stats stats{}; + m_getStats(&stats); + return stats; + } + + private: + std::unique_ptr, decltype(&FreeLibrary)> m_v8{nullptr, &FreeLibrary}; + CreateModule m_createModule{}; + CreateRuntime m_createRuntime{}; + GetStats m_getStats{}; +}; + +void ExpectReclaimed(Stats before, Stats after) { + Assert::AreEqual( + after.proxiesAllocated - before.proxiesAllocated, + after.proxiesFreed - before.proxiesFreed, + L"Native proxy allocations were not freed."); + Assert::AreEqual( + after.trackersAllocated - before.trackersAllocated, + after.trackersFreed - before.trackersFreed, + L"Lifetime trackers were not freed."); +} + +} // namespace + +// Run this class alone in a test process: V8 counters are process-wide. +TEST_CLASS (TurboModuleLifetimeTests) { + TEST_METHOD_INITIALIZE(Initialize) { + winrt::uninit_apartment(); + winrt::init_apartment(winrt::apartment_type::multi_threaded); + } + + TEST_METHOD_CLEANUP(Cleanup) { + winrt::uninit_apartment(); + } + + TEST_METHOD(PropertyReadsReclaimProxiesAndTrackers) { + LifetimeTestRuntime factory; + auto baseline = factory.Snapshot(); + auto runtime = factory.Create(); + { + auto module = factory.Module(*runtime); + auto retained = module.getPropertyAsFunction(*runtime, "value"); + { + auto other = module.getPropertyAsFunction(*runtime, "value"); + Assert::IsFalse( + jsi::Object::strictEquals(*runtime, retained, other), L"The property cache must not mask this test."); + } + runtime->instrumentation().collectGarbage("lifetime test warmup"); + + constexpr uint64_t batchSize = 10000; + for (size_t batch = 0; batch != 10; ++batch) { + auto before = factory.Snapshot(); + for (uint64_t i = 0; i != batchSize; ++i) { + auto method = module.getProperty(*runtime, "value"); + Assert::IsTrue(method.isObject()); + } + runtime->instrumentation().collectGarbage("TurboModule property read batch"); + auto after = factory.Snapshot(); + Assert::AreEqual( + batchSize, after.proxiesAllocated - before.proxiesAllocated, L"Reads did not allocate distinct proxies."); + Assert::AreEqual(batchSize, after.trackersAllocated - before.trackersAllocated); + ExpectReclaimed(before, after); + Assert::AreEqual(42.0, retained.call(*runtime).getNumber(), L"GC reclaimed a retained function prematurely."); + } + } + runtime->instrumentation().collectGarbage("release retained module and function"); + ExpectReclaimed(baseline, factory.Snapshot()); + runtime.reset(); + ExpectReclaimed(baseline, factory.Snapshot()); + } + + TEST_METHOD(RuntimeTeardownReclaimsReachableWrappers) { + LifetimeTestRuntime factory; + for (size_t iteration = 0; iteration != 5; ++iteration) { + auto baseline = factory.Snapshot(); + auto runtime = factory.Create(); + { + auto module = factory.Module(*runtime); + runtime->global().setProperty(*runtime, "retainedModule", module); + runtime->global().setProperty(*runtime, "retainedMethod", module.getProperty(*runtime, "value")); + } + auto live = factory.Snapshot(); + Assert::AreEqual(uint64_t{2}, live.proxiesAllocated - baseline.proxiesAllocated); + runtime.reset(); + ExpectReclaimed(baseline, factory.Snapshot()); + } + } +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp b/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp index 28991974022..86620f93277 100644 --- a/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp +++ b/vnext/Microsoft.ReactNative/TurboModulesProvider.cpp @@ -166,9 +166,7 @@ class TurboModuleImpl : public facebook::react::TurboModule { return m_hostObjectWrapper->get(runtime, propName); } - // it is not safe to assume that "runtime" never changes, so members are not cached here std::string key = propName.utf8(runtime); - if (key == "getConstants" && !m_moduleBuilder->ConstantProviders().empty()) { // try to find getConstants if there is any constant return facebook::jsi::Function::createFromHostFunction(