Add test showcasing memory leak in RemoteObjectsTable in Hermes (#44936)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/44936

# Changelog: [Internal]

Added a small test that uses `folly:ManualExecutor`, which reproduces the memory leak issue in Hermes' RemoteObjectsTable:
1. Send `Runtime.enable`
2. Evaluate `console.log(<object>);` to populate `RemoteObjectsTable`
3. Send `Page.reload` to reload VM

This test is expected to fail, because by the time it is published, the D58398254 hasn't landed.

Reviewed By: motiz88

Differential Revision: D58531763

fbshipit-source-id: 99af3bfce0a31fe905d5bf2bf433f62cfbc34897
This commit is contained in:
Ruslan Lesiutin
2024-06-14 04:05:10 -07:00
committed by Facebook GitHub Bot
parent 6584b408fb
commit 7bd98eecd0
3 changed files with 99 additions and 17 deletions
@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
#include <folly/executors/QueuedImmediateExecutor.h>
#include "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
@@ -44,12 +45,13 @@ struct Params {
/**
* A test fixture for the Console API.
*/
class ConsoleApiTest
: public JsiIntegrationPortableTest<JsiIntegrationTestHermesEngineAdapter>,
public WithParamInterface<Params> {
class ConsoleApiTest : public JsiIntegrationPortableTestBase<
JsiIntegrationTestHermesEngineAdapter,
folly::QueuedImmediateExecutor>,
public WithParamInterface<Params> {
protected:
void SetUp() override {
JsiIntegrationPortableTest::SetUp();
JsiIntegrationPortableTestBase::SetUp();
connect();
EXPECT_CALL(
fromPage(),
@@ -81,7 +83,7 @@ class ConsoleApiTest
if (!GetParam().runtimeEnabledAtStart) {
enableRuntimeDomain();
}
JsiIntegrationPortableTest::TearDown();
JsiIntegrationPortableTestBase::TearDown();
}
/**
@@ -6,6 +6,8 @@
*/
#include <folly/Format.h>
#include <folly/executors/ManualExecutor.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include "JsiIntegrationTest.h"
#include "engines/JsiIntegrationTestGenericEngineAdapter.h"
@@ -31,11 +33,40 @@ using AllEngines = Types<
using AllHermesVariants = Types<JsiIntegrationTestHermesEngineAdapter>;
template <typename EngineAdapter>
using JsiIntegrationPortableTest = JsiIntegrationPortableTestBase<
EngineAdapter,
folly::QueuedImmediateExecutor>;
TYPED_TEST_SUITE(JsiIntegrationPortableTest, AllEngines);
template <typename EngineAdapter>
using JsiIntegrationHermesTest = JsiIntegrationPortableTest<EngineAdapter>;
using JsiIntegrationHermesTest = JsiIntegrationPortableTestBase<
EngineAdapter,
folly::QueuedImmediateExecutor>;
/**
* Fixture class for tests that run on a ManualExecutor. Work scheduled
* on the executor is *not* run automatically; it must be manually advanced
* in the body of the test.
*/
template <typename EngineAdapter>
class JsiIntegrationHermesTestAsync : public JsiIntegrationPortableTestBase<
EngineAdapter,
folly::ManualExecutor> {
public:
void TearDown() override {
// Assert there are no pending tasks on the ManualExecutor.
auto tasksCleared = this->executor_.clear();
EXPECT_EQ(tasksCleared, 0)
<< "There were still pending tasks on executor_ at the end of the test. Use advance() or run() as needed.";
JsiIntegrationPortableTestBase<EngineAdapter, folly::ManualExecutor>::
TearDown();
}
};
TYPED_TEST_SUITE(JsiIntegrationHermesTest, AllHermesVariants);
TYPED_TEST_SUITE(JsiIntegrationHermesTestAsync, AllHermesVariants);
#pragma region AllEngines
@@ -371,6 +402,56 @@ TYPED_TEST(JsiIntegrationPortableTest, ReactNativeApplicationDisable) {
#pragma endregion // AllEngines
#pragma region AllHermesVariants
TYPED_TEST(JsiIntegrationHermesTestAsync, HermesObjectsTableDoesNotMemoryLeak) {
// This is a regression test for T186157855 (CDPAgent leaking JSI data in
// RemoteObjectsTable past the Runtime's lifetime)
this->connect();
this->executor_.run();
InSequence s;
this->expectMessageFromPage(JsonParsed(
AllOf(AtJsonPtr("/method", "Runtime.executionContextCreated"))));
this->expectMessageFromPage(JsonEq(R"({
"id": 1,
"result": {}
})"));
this->toPage_->sendMessage(R"({
"id": 1,
"method": "Runtime.enable"
})");
this->executor_.run();
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Runtime.consoleAPICalled"),
AtJsonPtr("/params/args/0/objectId", "1"))));
this->eval(R"(console.log({a: 1});)");
this->executor_.run();
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextDestroyed",
"params": {
"executionContextId": 1
}
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextsCleared"
})"));
this->expectMessageFromPage(JsonEq(R"({
"method": "Runtime.executionContextCreated",
"params": {
"context": {
"id": 2,
"origin": "",
"name": "main"
}
}
})"));
// NOTE: Doesn't crash when Hermes checks for JSI value leaks
this->reload();
this->executor_.run();
}
TYPED_TEST(JsiIntegrationHermesTest, EvaluateExpression) {
this->connect();
@@ -8,7 +8,6 @@
#pragma once
#include <folly/dynamic.h>
#include <folly/executors/QueuedImmediateExecutor.h>
#include <folly/json.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@@ -42,15 +41,15 @@ namespace facebook::react::jsinspector_modern {
* for a particular engine, plus exposes access to a RuntimeExecutor (based on
* the provided folly::Executor) and the corresponding jsi::Runtime.
*/
template <typename EngineAdapter>
class JsiIntegrationPortableTest : public ::testing::Test,
private HostTargetDelegate {
folly::QueuedImmediateExecutor immediateExecutor_;
template <typename EngineAdapter, typename Executor>
class JsiIntegrationPortableTestBase : public ::testing::Test,
private HostTargetDelegate {
protected:
JsiIntegrationPortableTest()
Executor executor_;
JsiIntegrationPortableTestBase()
: inspectorFlagsGuard_{EngineAdapter::getInspectorFlagOverrides()},
engineAdapter_{immediateExecutor_} {}
engineAdapter_{executor_} {}
void SetUp() override {
// NOTE: Using SetUp() so we can call virtual methods like
@@ -63,7 +62,7 @@ class JsiIntegrationPortableTest : public ::testing::Test,
loadMainBundle();
}
~JsiIntegrationPortableTest() override {
~JsiIntegrationPortableTestBase() override {
toPage_.reset();
if (runtimeTarget_) {
EXPECT_TRUE(instance_);
@@ -118,7 +117,7 @@ class JsiIntegrationPortableTest : public ::testing::Test,
instance_ = nullptr;
}
// Recreate the engine (e.g. to wipe any state in the inner jsi::Runtime)
engineAdapter_.emplace(immediateExecutor_);
engineAdapter_.emplace(executor_);
instance_ = &page_->registerInstance(instanceTargetDelegate_);
setupRuntimeBeforeRegistration(engineAdapter_->getRuntime());
runtimeTarget_ = &instance_->registerRuntime(
@@ -133,7 +132,7 @@ class JsiIntegrationPortableTest : public ::testing::Test,
}
VoidExecutor inspectorExecutor_ = [this](auto callback) {
immediateExecutor_.add(callback);
executor_.add(callback);
};
jsi::Value eval(std::string_view code) {