Skip to content

Commit 480bcac

Browse files
motiz88facebook-github-bot
authored andcommitted
Add RuntimeTargetDelegate::serializeStackTrace API (#54048)
Summary: Changelog: [Internal] Adds an engine-agnostic mechanism for serialising a previously captured stack trace as a CDP [`Runtime.StackTrace`](https://cdpstatus.reactnative.dev/devtools-protocol/tot/Runtime#type-StackTrace). This complements the existing `RuntimeTargetDelegate::captureStackTrace` method, which returns an opaque, engine-specific representation of a stack trace. This can be used as a building block for implementing higher-level CDP message types like [`Network.Initiator`](https://cdpstatus.reactnative.dev/devtools-protocol/tot/Network#type-Initiator) within React Native, while keeping the underlying stack trace representation private to each engine. NOTE: This diff includes an implementation for Hermes that duplicates logic from the Hermes codebase. Further up the stack, I have diffs to replace this with a new API to be provided by Hermes. Differential Revision: D83754142
1 parent 38f52a5 commit 480bcac

File tree

8 files changed

+169
-0
lines changed

8 files changed

+169
-0
lines changed

packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate {
7878
return &hermesStackTrace_;
7979
}
8080

81+
const HermesStackTrace& operator*() const {
82+
return hermesStackTrace_;
83+
}
84+
85+
const HermesStackTrace* operator->() const {
86+
return &hermesStackTrace_;
87+
}
88+
8189
private:
8290
HermesStackTrace hermesStackTrace_;
8391
};
@@ -216,6 +224,47 @@ class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate {
216224
return samplingProfileDelegate_->collectSamplingProfile();
217225
}
218226

227+
std::optional<folly::dynamic> serializeStackTrace(
228+
const StackTrace& stackTrace) override {
229+
if (auto* hermesStackTraceWrapper =
230+
dynamic_cast<const HermesStackTraceWrapper*>(&stackTrace)) {
231+
// The logic below is duplicated from
232+
// facebook::hermes::cdp::message::makeCallFrames in
233+
// hermes/cdp/MessageConverters.cpp (and rewritten to use Folly).
234+
// TODO: Use a suitable Hermes API (D83560910 / D83560972 / D83562078) to
235+
// serialize the stack trace to CDP-formatted JSON.
236+
folly::dynamic stackTrace = folly::dynamic::object();
237+
auto& hermesStackTrace = **hermesStackTraceWrapper;
238+
if (hermesStackTrace.callFrameCount() > 0) {
239+
folly::dynamic callFrames = folly::dynamic::array();
240+
callFrames.reserve(hermesStackTrace.callFrameCount());
241+
for (size_t i = 0, n = hermesStackTrace.callFrameCount(); i != n; i++) {
242+
auto callFrame = hermesStackTrace.callFrameForIndex(i);
243+
if (callFrame.location.fileId ==
244+
facebook::hermes::debugger::kInvalidLocation) {
245+
continue;
246+
}
247+
folly::dynamic callFrameObj = folly::dynamic::object();
248+
callFrameObj["functionName"] = callFrame.functionName;
249+
callFrameObj["scriptId"] = std::to_string(callFrame.location.fileId);
250+
callFrameObj["url"] = callFrame.location.fileName;
251+
if (callFrame.location.line !=
252+
facebook::hermes::debugger::kInvalidLocation) {
253+
callFrameObj["lineNumber"] = callFrame.location.line - 1;
254+
}
255+
if (callFrame.location.column !=
256+
facebook::hermes::debugger::kInvalidLocation) {
257+
callFrameObj["columnNumber"] = callFrame.location.column - 1;
258+
}
259+
callFrames.push_back(std::move(callFrameObj));
260+
}
261+
stackTrace["callFrames"] = std::move(callFrames);
262+
}
263+
return stackTrace;
264+
}
265+
return std::nullopt;
266+
}
267+
219268
private:
220269
HermesRuntimeTargetDelegate& delegate_;
221270
std::shared_ptr<HermesRuntime> runtime_;
@@ -311,6 +360,11 @@ HermesRuntimeTargetDelegate::collectSamplingProfile() {
311360
return impl_->collectSamplingProfile();
312361
}
313362

363+
std::optional<folly::dynamic> HermesRuntimeTargetDelegate::serializeStackTrace(
364+
const StackTrace& stackTrace) {
365+
return impl_->serializeStackTrace(stackTrace);
366+
}
367+
314368
#ifdef HERMES_ENABLE_DEBUGGER
315369
CDPDebugAPI& HermesRuntimeTargetDelegate::getCDPDebugAPI() {
316370
return impl_->getCDPDebugAPI();

packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ class HermesRuntimeTargetDelegate : public RuntimeTargetDelegate {
6060

6161
tracing::RuntimeSamplingProfile collectSamplingProfile() override;
6262

63+
std::optional<folly::dynamic> serializeStackTrace(
64+
const StackTrace& stackTrace) override;
65+
6366
private:
6467
// We use the private implementation idiom to ensure this class has the same
6568
// layout regardless of whether HERMES_ENABLE_DEBUGGER is defined. The net

packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,10 @@ FallbackRuntimeTargetDelegate::collectSamplingProfile() {
5858
"Sampling Profiler capabilities are not supported for Runtime fallback");
5959
}
6060

61+
std::optional<folly::dynamic>
62+
FallbackRuntimeTargetDelegate::serializeStackTrace(
63+
const StackTrace& /*stackTrace*/) {
64+
return std::nullopt;
65+
}
66+
6167
} // namespace facebook::react::jsinspector_modern

packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ class FallbackRuntimeTargetDelegate : public RuntimeTargetDelegate {
4646

4747
tracing::RuntimeSamplingProfile collectSamplingProfile() override;
4848

49+
std::optional<folly::dynamic> serializeStackTrace(
50+
const StackTrace& stackTrace) override;
51+
4952
private:
5053
std::string engineDescription_;
5154
};

packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ class RuntimeTargetDelegate {
108108
* Return recorded sampling profile for the previous sampling session.
109109
*/
110110
virtual tracing::RuntimeSamplingProfile collectSamplingProfile() = 0;
111+
112+
/**
113+
* \returns a JSON representation of the given stack trace, conforming to the
114+
* @cdp Runtime.StackTrace type, if the runtime supports it. Otherwise,
115+
* returns std::nullopt.
116+
*/
117+
virtual std::optional<folly::dynamic> serializeStackTrace(
118+
const StackTrace& stackTrace) = 0;
111119
};
112120

113121
/**

packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ class MockRuntimeTargetDelegate : public RuntimeTargetDelegate {
169169
collectSamplingProfile,
170170
(),
171171
(override));
172+
MOCK_METHOD(
173+
std::optional<folly::dynamic>,
174+
serializeStackTrace,
175+
(const StackTrace& stackTrace),
176+
(override));
172177

173178
inline MockRuntimeTargetDelegate() {
174179
using namespace testing;

packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.cpp

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,88 @@ TYPED_TEST(JsiIntegrationHermesTest, ReleaseRemoteObjectGroup) {
893893
})");
894894
}
895895

896+
// A low-level test for captureStackTrace and serializeStackTrace in
897+
// HermesRuntimeTargetDelegate. This functionality is not directly exposed
898+
// to user code, but serves as a building block for higher-level CDP domains.
899+
TYPED_TEST(JsiIntegrationHermesTest, testCaptureAndSerializeStackTrace) {
900+
auto& runtimeTargetDelegate = this->dangerouslyGetRuntimeTargetDelegate();
901+
auto& runtime = this->dangerouslyGetRuntime();
902+
runtime.global().setProperty(
903+
runtime,
904+
"captureCdpStackTrace",
905+
jsi::Function::createFromHostFunction(
906+
runtime,
907+
jsi::PropNameID::forAscii(runtime, "captureCdpStackTrace"),
908+
0,
909+
[&runtimeTargetDelegate](
910+
jsi::Runtime& rt,
911+
const jsi::Value& /* thisVal */,
912+
const jsi::Value* /* args */,
913+
size_t /* count */) -> jsi::Value {
914+
auto stackTraceDynamic = runtimeTargetDelegate.serializeStackTrace(
915+
*runtimeTargetDelegate.captureStackTrace(rt));
916+
if (!stackTraceDynamic.has_value()) {
917+
return jsi::Value::undefined();
918+
}
919+
return jsi::String::createFromUtf8(
920+
rt, folly::toJson(*stackTraceDynamic));
921+
}));
922+
923+
this->connect();
924+
925+
InSequence s;
926+
927+
this->expectMessageFromPage(JsonEq(R"({
928+
"id": 1,
929+
"result": {}
930+
})"));
931+
this->toPage_->sendMessage(R"({
932+
"id": 1,
933+
"method": "Debugger.enable"
934+
})");
935+
936+
auto scriptInfo = this->expectMessageFromPage(JsonParsed(AllOf(
937+
AtJsonPtr("/method", "Debugger.scriptParsed"),
938+
AtJsonPtr("/params/url", "stackTraceTest.js"))));
939+
940+
auto stackTrace = this->eval(R"( // line 0
941+
function inner() { // line 1
942+
return globalThis.captureCdpStackTrace(); // line 2
943+
} // line 3
944+
function outer() { // line 4
945+
return inner(); // line 5
946+
} // line 6
947+
outer(); // line 7
948+
//# sourceURL=stackTraceTest.js
949+
)")
950+
.getString(runtime)
951+
.utf8(runtime);
952+
953+
ASSERT_TRUE(scriptInfo->has_value());
954+
955+
EXPECT_THAT(
956+
stackTrace,
957+
JsonParsed(AllOf(
958+
AtJsonPtr("/callFrames/0/functionName", "inner"),
959+
AtJsonPtr(
960+
"/callFrames/0/scriptId",
961+
scriptInfo->value()["params"]["scriptId"]),
962+
AtJsonPtr("/callFrames/0/lineNumber", 2),
963+
AtJsonPtr("/callFrames/0/columnNumber", 44),
964+
AtJsonPtr("/callFrames/1/functionName", "outer"),
965+
AtJsonPtr(
966+
"/callFrames/1/scriptId",
967+
scriptInfo->value()["params"]["scriptId"]),
968+
AtJsonPtr("/callFrames/1/lineNumber", 5),
969+
AtJsonPtr("/callFrames/1/columnNumber", 18),
970+
AtJsonPtr("/callFrames/2/functionName", "global"),
971+
AtJsonPtr(
972+
"/callFrames/2/scriptId",
973+
scriptInfo->value()["params"]["scriptId"]),
974+
AtJsonPtr("/callFrames/2/lineNumber", 7),
975+
AtJsonPtr("/callFrames/2/columnNumber", 9))));
976+
}
977+
896978
#pragma endregion // AllHermesVariants
897979

898980
} // namespace facebook::react::jsinspector_modern

packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ class JsiIntegrationPortableTestBase : public ::testing::Test,
156156
return result;
157157
}
158158

159+
RuntimeTargetDelegate& dangerouslyGetRuntimeTargetDelegate() {
160+
return engineAdapter_->getRuntimeTargetDelegate();
161+
}
162+
163+
jsi::Runtime& dangerouslyGetRuntime() {
164+
return engineAdapter_->getRuntime();
165+
}
166+
159167
std::shared_ptr<HostTarget> page_;
160168
InstanceTarget* instance_{};
161169
RuntimeTarget* runtimeTarget_{};

0 commit comments

Comments
 (0)