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(