From 945ccd4ae2166d3885b2d57a02792e48c8077eb7 Mon Sep 17 00:00:00 2001 From: Anandraj Date: Fri, 14 May 2021 14:24:07 -0700 Subject: [PATCH] Hermes Inspector (#7322) * Hermes Inspector Integration redesign * Improving the change file text * Resolving review comments * Reverting the unintentional change to enable hermes by default and some formatting fixes * Fixing x86 builds by ensuring the right calling convention prefix with inspector APIs * Fixing issues in instance reload * Fixing last incomplete commit by uploading JSEngine.props * yarn format * Some cleanups * yarn format * Adding markdown documentation * Reporting bundle download status to package and resolving review feedbacks * Fixing build --- ...-557f8ae4-d109-4a29-b048-8b6e88123a35.json | 7 + docs/inspector.md | 45 ++++ packages/playground/packages.config | 2 +- .../windows/playground-win32/packages.config | 2 +- .../windows/playground/Playground.vcxproj | 4 +- .../windows/playground/packages.config | 2 +- .../ReactHost/ReactInstanceWin.cpp | 3 +- vnext/Microsoft.ReactNative/packages.config | 2 +- vnext/PropertySheets/JSEngine.props | 3 +- vnext/ReactCommon/ReactCommon.vcxproj | 2 + vnext/Shared/DevServerHelper.h | 13 + vnext/Shared/DevSupportManager.cpp | 42 +++ vnext/Shared/DevSupportManager.h | 27 ++ vnext/Shared/HermesRuntimeHolder.cpp | 103 +++++++- vnext/Shared/HermesRuntimeHolder.h | 15 +- vnext/Shared/IDevSupportManager.h | 4 + vnext/Shared/InspectorPackagerConnection.cpp | 240 ++++++++++++++++++ vnext/Shared/InspectorPackagerConnection.h | 58 +++++ vnext/Shared/OInstance.cpp | 20 +- vnext/Shared/Shared.vcxitems | 5 + vnext/Shared/Shared.vcxitems.filters | 9 + vnext/Shared/tracing/tracing.cpp | 11 + vnext/Shared/tracing/tracing.h | 20 ++ 23 files changed, 608 insertions(+), 31 deletions(-) create mode 100644 change/react-native-windows-557f8ae4-d109-4a29-b048-8b6e88123a35.json create mode 100644 docs/inspector.md create mode 100644 vnext/Shared/InspectorPackagerConnection.cpp create mode 100644 vnext/Shared/InspectorPackagerConnection.h create mode 100644 vnext/Shared/tracing/tracing.h diff --git a/change/react-native-windows-557f8ae4-d109-4a29-b048-8b6e88123a35.json b/change/react-native-windows-557f8ae4-d109-4a29-b048-8b6e88123a35.json new file mode 100644 index 0000000000..43bc30fd81 --- /dev/null +++ b/change/react-native-windows-557f8ae4-d109-4a29-b048-8b6e88123a35.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Enabling Hermes Inspector", + "packageName": "react-native-windows", + "email": "anandrag@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/docs/inspector.md b/docs/inspector.md new file mode 100644 index 0000000000..44f94748ab --- /dev/null +++ b/docs/inspector.md @@ -0,0 +1,45 @@ +# Hermes Inspector + +*React Native for Windows* with *Hermes engine* now supports direct JavaScript runtime inspection using tools such as Chrome or Edge devtools, VSCode debugger, Flipper etc. by implementing an in-process Chrome Debug Protocol server. +Please note that it is fundamentally different from "Remote JS Debugging", which loads the JavaScript bundle into a remote Chrome browser session with duplex communication over IPC channels. + +We share the implementation (code and design) with other platforms wherever possible. All the external endpoints, APIs and protocols should be identical to *React Native* environments on other platforms. +Hence, we expect most tooling available on other platforms to just work on Windows. But, as of now, we have tested only with Chrome and Edge devtools. + +## Steps to enable direct debugging + +1. Initialize React Native Host, + - Turn on `DeveloperSupport` + - Turn on `FastRefresh` + - Turn off `WebDebugger` + - Turn on `Direct Debugging` +2. Ensure Dev-Server is running +3. Start the application + +After the app has booted, + +4. Navigate to `edge://inspect` in Edge browser or `chrome://inspect` in Chrome browser +5. Enable **Discover network targets** and **configure** the target discovery settings to include `localhost:8081` (or wherever the metro server is running) +6. Within a few seconds "Hermes React Native" should appear on the page as a remote target +7. Click on the **inspect** link to launch the devtools page +8. Click `Ctrl+P` to open source files and set break points +9. Alternatively, you can insert `debugger` statements in source code to break on specific locations + +In order to break on locations during boot, you can either + +- `debugger` statements during boot sequence will pause the runtime waiting for debugger to connect +- Set break point and refresh the bundle through the Dev Server. The runtime will wait for debugger to attach. + +## Steps to enable heap profiling + +Follow steps 1-7 from above, and then +1. Click on the "Memory" tab in the inspector +2. Heap snapshots and instrumented allocations with callstacks should be working. + +## Enable debugging/profiling on release builds + +We keep the inspector turned off on release builds by default. If you want to debug or profile release builds, set the MSBuild property `EnableHermesInspectorInReleaseFlavor` to `'true'` when building the platform. + +## Known Issues + +1. CPU Sampling profiler current don't work \ No newline at end of file diff --git a/packages/playground/packages.config b/packages/playground/packages.config index a82fad74dd..d2f5de8a20 100644 --- a/packages/playground/packages.config +++ b/packages/playground/packages.config @@ -2,5 +2,5 @@ - + diff --git a/packages/playground/windows/playground-win32/packages.config b/packages/playground/windows/playground-win32/packages.config index 20e60d1e01..86932ea33f 100644 --- a/packages/playground/windows/playground-win32/packages.config +++ b/packages/playground/windows/playground-win32/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/packages/playground/windows/playground/Playground.vcxproj b/packages/playground/windows/playground/Playground.vcxproj index 1fc4698d99..df6d9651bd 100644 --- a/packages/playground/windows/playground/Playground.vcxproj +++ b/packages/playground/windows/playground/Playground.vcxproj @@ -186,7 +186,7 @@ - + @@ -195,7 +195,7 @@ - + diff --git a/packages/playground/windows/playground/packages.config b/packages/playground/windows/playground/packages.config index e1666ae6bb..448898d2d9 100644 --- a/packages/playground/windows/playground/packages.config +++ b/packages/playground/windows/playground/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp index fc8592149b..e888063735 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp @@ -401,7 +401,8 @@ void ReactInstanceWin::Initialize() noexcept { switch (m_options.JsiEngine) { case JSIEngine::Hermes: #if defined(USE_HERMES) - devSettings->jsiRuntimeHolder = std::make_shared(); + devSettings->jsiRuntimeHolder = + std::make_shared(devSettings, m_jsMessageThread.Load()); devSettings->inlineSourceMap = false; break; #endif diff --git a/vnext/Microsoft.ReactNative/packages.config b/vnext/Microsoft.ReactNative/packages.config index bd025f006f..47ff3bbb5e 100644 --- a/vnext/Microsoft.ReactNative/packages.config +++ b/vnext/Microsoft.ReactNative/packages.config @@ -7,6 +7,6 @@ - + \ No newline at end of file diff --git a/vnext/PropertySheets/JSEngine.props b/vnext/PropertySheets/JSEngine.props index 36d025b16a..e3a6431e11 100644 --- a/vnext/PropertySheets/JSEngine.props +++ b/vnext/PropertySheets/JSEngine.props @@ -3,10 +3,11 @@ false - 0.7.2-microsoft.5 + 0.7.2-microsoft.11 $(SolutionDir)packages\ReactNative.Hermes.Windows.$(HermesVersion) uwp + false false 0.64.8 diff --git a/vnext/ReactCommon/ReactCommon.vcxproj b/vnext/ReactCommon/ReactCommon.vcxproj index 0ec60a97b7..0bd8c9dc99 100644 --- a/vnext/ReactCommon/ReactCommon.vcxproj +++ b/vnext/ReactCommon/ReactCommon.vcxproj @@ -10,6 +10,7 @@ 10.0 + @@ -85,6 +86,7 @@ + diff --git a/vnext/Shared/DevServerHelper.h b/vnext/Shared/DevServerHelper.h index 3473e4ee4e..4d8330a90e 100644 --- a/vnext/Shared/DevServerHelper.h +++ b/vnext/Shared/DevServerHelper.h @@ -59,6 +59,18 @@ class DevServerHelper { PackagerOpenStackFrameUrlFormat, GetDeviceLocalHost(sourceBundleHost, sourceBundlePort).c_str()); } + static std::string get_InspectorDeviceUrl( + const std::string &packagerHost, + const uint16_t packagerPort, + const std::string &deviceName, + const std::string &packageName) { + return string_format( + InspectorDeviceUrlFormat, + GetDeviceLocalHost(packagerHost, packagerPort).c_str(), + deviceName.c_str(), + packageName.c_str()); + } + static constexpr const char DefaultPackagerHost[] = "localhost"; static const uint16_t DefaultPackagerPort = 8081; @@ -79,6 +91,7 @@ class DevServerHelper { static constexpr const char PackagerConnectionUrlFormat[] = "ws://%s/message"; static constexpr const char PackagerStatusUrlFormat[] = "http://%s/status"; static constexpr const char PackagerOpenStackFrameUrlFormat[] = "https://%s/open-stack-frame"; + static constexpr const char InspectorDeviceUrlFormat[] = "ws://%s/inspector/device?name=%s&app=%s"; static constexpr const char PackagerOkStatus[] = "packager-status:running"; const int LongPollFailureDelayMs = 5000; diff --git a/vnext/Shared/DevSupportManager.cpp b/vnext/Shared/DevSupportManager.cpp index d4159ec24f..9825ecc817 100644 --- a/vnext/Shared/DevSupportManager.cpp +++ b/vnext/Shared/DevSupportManager.cpp @@ -21,6 +21,11 @@ #include #include +#ifdef HERMES_ENABLE_DEBUGGER +#include +#include +#endif + #pragma warning(push) #pragma warning(disable : 4068 4251 4101 4804 4309) #include @@ -232,6 +237,43 @@ void DevSupportManager::StopPollingLiveReload() { m_cancellation_token = true; } +void DevSupportManager::StartInspector( + [[maybe_unused]] const std::string &packagerHost, + [[maybe_unused]] const uint16_t packagerPort) noexcept { +#ifdef HERMES_ENABLE_DEBUGGER + std::string packageName("RNW"); + if (auto currentPackage = winrt::Windows::ApplicationModel::Package::Current()) { + packageName = winrt::to_string(currentPackage.DisplayName()); + } + + std::string deviceName("RNWHost"); + auto hostNames = winrt::Windows::Networking::Connectivity::NetworkInformation::GetHostNames(); + if (hostNames && hostNames.First() && hostNames.First().Current()) { + deviceName = winrt::to_string(hostNames.First().Current().DisplayName()); + } + + m_inspectorPackagerConnection = std::make_shared( + facebook::react::DevServerHelper::get_InspectorDeviceUrl(packagerHost, packagerPort, deviceName, packageName), + m_BundleStatusProvider); + m_inspectorPackagerConnection->connectAsync(); +#endif +} + +void DevSupportManager::StopInspector() noexcept { +#ifdef HERMES_ENABLE_DEBUGGER + if (m_inspectorPackagerConnection) { + m_inspectorPackagerConnection->disconnectAsync(); + m_inspectorPackagerConnection = nullptr; + } +#endif +} + +void DevSupportManager::UpdateBundleStatus(bool isLastDownloadSucess, int64_t updateTimestamp) noexcept { +#ifdef HERMES_ENABLE_DEBUGGER + m_BundleStatusProvider->updateBundleStatus(isLastDownloadSucess, updateTimestamp); +#endif +} + std::pair GetJavaScriptFromServer( const std::string &sourceBundleHost, const uint16_t sourceBundlePort, diff --git a/vnext/Shared/DevSupportManager.h b/vnext/Shared/DevSupportManager.h index c33c06736f..6bb19ace4e 100644 --- a/vnext/Shared/DevSupportManager.h +++ b/vnext/Shared/DevSupportManager.h @@ -14,6 +14,10 @@ #include #include +#if defined(HERMES_ENABLE_DEBUGGER) +#include +#endif + namespace facebook { namespace react { struct DevSettings; @@ -43,8 +47,31 @@ class DevSupportManager final : public facebook::react::IDevSupportManager { std::function onChangeCallback) override; virtual void StopPollingLiveReload() override; + virtual void StartInspector(const std::string &packagerHost, const uint16_t packagerPort) noexcept override; + virtual void StopInspector() noexcept override; + virtual void UpdateBundleStatus(bool isLastDownloadSucess, int64_t updateTimestamp) noexcept override; + private: std::atomic_bool m_cancellation_token; + +#if defined(HERMES_ENABLE_DEBUGGER) + std::shared_ptr m_inspectorPackagerConnection; + + struct BundleStatusProvider : public InspectorPackagerConnection::IBundleStatusProvider { + virtual InspectorPackagerConnection::BundleStatus getBundleStatus() { + return m_bundleStatus; + } + + void updateBundleStatus(bool isLastDownloadSucess, int64_t updateTimestamp) { + m_bundleStatus.m_isLastDownloadSucess = isLastDownloadSucess; + m_bundleStatus.m_updateTimestamp = updateTimestamp; + } + + private: + InspectorPackagerConnection::BundleStatus m_bundleStatus; + }; + std::shared_ptr m_BundleStatusProvider = std::make_shared(); +#endif }; } // namespace Microsoft::ReactNative diff --git a/vnext/Shared/HermesRuntimeHolder.cpp b/vnext/Shared/HermesRuntimeHolder.cpp index 64fd73ebe2..0f5c3d580d 100644 --- a/vnext/Shared/HermesRuntimeHolder.cpp +++ b/vnext/Shared/HermesRuntimeHolder.cpp @@ -3,31 +3,108 @@ #include "pch.h" -#include +#include #include + +#include +#include +#include +#include #include "HermesRuntimeHolder.h" +#if defined(HERMES_ENABLE_DEBUGGER) +#include +#endif + using namespace facebook; namespace facebook { namespace react { -std::shared_ptr HermesRuntimeHolder::getRuntime() noexcept { - std::call_once(once_flag_, [this]() { initRuntime(); }); +namespace { - if (!runtime_) - std::terminate(); - - // ChakraJsiRuntime is not thread safe as of now. - if (own_thread_id_ != std::this_thread::get_id()) - std::terminate(); - - return runtime_; +std::unique_ptr makeHermesRuntimeSystraced( + const ::hermes::vm::RuntimeConfig &runtimeConfig) { + SystraceSection s("HermesExecutorFactory::makeHermesRuntimeSystraced"); + return hermes::makeHermesRuntime(runtimeConfig); } +#ifdef HERMES_ENABLE_DEBUGGER +class HermesExecutorRuntimeAdapter final : public facebook::hermes::inspector::RuntimeAdapter { + public: + HermesExecutorRuntimeAdapter( + std::shared_ptr runtime, + facebook::hermes::HermesRuntime &hermesRuntime, + std::shared_ptr thread) + : m_runtime(runtime), m_hermesRuntime(hermesRuntime), m_thread(std::move(thread)) {} + + virtual ~HermesExecutorRuntimeAdapter() = default; + + jsi::Runtime &getRuntime() override { + return *m_runtime; + } + + facebook::hermes::debugger::Debugger &getDebugger() override { + return m_hermesRuntime.getDebugger(); + } + + void tickleJs() override { + // The queue will ensure that runtime_ is still valid when this + // gets invoked. + m_thread->runOnQueue([&runtime = m_runtime]() { + auto func = runtime->global().getPropertyAsFunction(*runtime, "__tickleJs"); + func.call(*runtime); + }); + } + + private: + std::shared_ptr m_runtime; + facebook::hermes::HermesRuntime &m_hermesRuntime; + + std::shared_ptr m_thread; +}; +#endif + +} // namespace + +std::shared_ptr HermesRuntimeHolder::getRuntime() noexcept { + std::call_once(m_once_flag, [this]() { initRuntime(); }); + + if (!m_runtime) + std::terminate(); + + // Make sure that the runtime instance is not consumed from multiple threads. + if (m_own_thread_id != std::this_thread::get_id()) + std::terminate(); + + return m_runtime; +} + +HermesRuntimeHolder::HermesRuntimeHolder( + std::shared_ptr devSettings, + std::shared_ptr jsQueue) noexcept + : m_devSettings(std::move(devSettings)), m_jsQueue(std::move(jsQueue)) {} + void HermesRuntimeHolder::initRuntime() noexcept { - runtime_ = facebook::hermes::makeHermesRuntime(); - own_thread_id_ = std::this_thread::get_id(); + auto runtimeConfig = ::hermes::vm::RuntimeConfig(); + auto hermesRuntime = makeHermesRuntimeSystraced(runtimeConfig); + facebook::hermes::HermesRuntime &hermesRuntimeRef = *hermesRuntime; + + m_runtime = std::move(hermesRuntime); + m_own_thread_id = std::this_thread::get_id(); + +#ifdef HERMES_ENABLE_DEBUGGER + if (m_devSettings->useDirectDebugger) { + auto adapter = std::make_unique(m_runtime, hermesRuntimeRef, m_jsQueue); + facebook::hermes::inspector::chrome::enableDebugging(std::move(adapter), "Hermes React Native"); + } +#endif + + // Add js engine information to Error.prototype so in error reporting we + // can send this information. + auto errorPrototype = + m_runtime->global().getPropertyAsObject(*m_runtime, "Error").getPropertyAsObject(*m_runtime, "prototype"); + errorPrototype.setProperty(*m_runtime, "jsEngine", "hermes"); } } // namespace react diff --git a/vnext/Shared/HermesRuntimeHolder.h b/vnext/Shared/HermesRuntimeHolder.h index 74791971d1..120d824396 100644 --- a/vnext/Shared/HermesRuntimeHolder.h +++ b/vnext/Shared/HermesRuntimeHolder.h @@ -7,6 +7,8 @@ #include #include +#include + namespace facebook { namespace react { @@ -14,12 +16,19 @@ class HermesRuntimeHolder : public facebook::jsi::RuntimeHolderLazyInit { public: std::shared_ptr getRuntime() noexcept override; + HermesRuntimeHolder( + std::shared_ptr devSettings, + std::shared_ptr jsQueue) noexcept; + private: void initRuntime() noexcept; - std::shared_ptr runtime_; + std::shared_ptr m_runtime; - std::once_flag once_flag_; - std::thread::id own_thread_id_; + std::once_flag m_once_flag; + std::thread::id m_own_thread_id; + + std::shared_ptr m_devSettings; + std::shared_ptr m_jsQueue; }; } // namespace react diff --git a/vnext/Shared/IDevSupportManager.h b/vnext/Shared/IDevSupportManager.h index b50ba6cca4..3aa61ce8a7 100644 --- a/vnext/Shared/IDevSupportManager.h +++ b/vnext/Shared/IDevSupportManager.h @@ -21,6 +21,10 @@ struct IDevSupportManager { const uint16_t sourceBundlePort, std::function onChangeCallback) = 0; virtual void StopPollingLiveReload() = 0; + + virtual void StartInspector(const std::string &packagerHost, const uint16_t packagerPort) noexcept = 0; + virtual void StopInspector() noexcept = 0; + virtual void UpdateBundleStatus(bool isLastDownloadSucess, int64_t updateTimestamp) noexcept = 0; }; std::shared_ptr CreateDevSupportManager(); diff --git a/vnext/Shared/InspectorPackagerConnection.cpp b/vnext/Shared/InspectorPackagerConnection.cpp new file mode 100644 index 0000000000..8b3779fc7c --- /dev/null +++ b/vnext/Shared/InspectorPackagerConnection.cpp @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" + +#ifdef HERMES_ENABLE_DEBUGGER + +#include +#include +#include "InspectorPackagerConnection.h" + +namespace facebook { +namespace react { +IDestructible::~IDestructible() {} +} // namespace react +} // namespace facebook + +namespace Microsoft::ReactNative { + +namespace { + +struct InspectorProtocol { + static constexpr std::string_view Message_PAGEID = "pageId"; + static constexpr std::string_view Message_PAYLOAD = "payload"; + + static constexpr std::string_view Message_eventName_wrappedEvent = "wrappedEvent"; + static constexpr std::string_view Message_eventName_getPages = "getPages"; + static constexpr std::string_view Message_eventName_connect = "connect"; + static constexpr std::string_view Message_eventName_disconnect = "disconnect"; + + static constexpr std::string_view Message_EVENT = "event"; + + enum class EventType { GetPages, WrappedEvent, Connect, Disconnect }; + + static EventType getEventType(const folly::dynamic &messageFromPackager) { + std::string event = messageFromPackager.at(InspectorProtocol::Message_EVENT).getString(); + if (event == Message_eventName_getPages) { + return EventType::GetPages; + } + + if (event == Message_eventName_wrappedEvent) { + return EventType::WrappedEvent; + } + + if (event == Message_eventName_connect) { + return EventType::Connect; + } + + if (event == Message_eventName_disconnect) { + return EventType::Disconnect; + } + + assert(false && "Unknown event!"); + std::abort(); + } + + static folly::dynamic constructResponseForPackager(EventType eventType, folly::dynamic &&payload) { + folly::dynamic response = folly::dynamic::object; + + switch (eventType) { + case EventType::GetPages: + response[InspectorProtocol::Message_EVENT] = InspectorProtocol::Message_eventName_getPages; + break; + case EventType::WrappedEvent: + response[InspectorProtocol::Message_EVENT] = InspectorProtocol::Message_eventName_wrappedEvent; + break; + case EventType::Connect: + response[InspectorProtocol::Message_EVENT] = InspectorProtocol::Message_eventName_connect; + break; + case EventType::Disconnect: + response[InspectorProtocol::Message_EVENT] = InspectorProtocol::Message_eventName_disconnect; + break; + default: + assert(false && "Unknown event Type."); + std::abort(); + } + + response[InspectorProtocol::Message_PAYLOAD] = std::move(payload); + return response; + } + + static folly::dynamic constructGetPagesResponsePayloadForPackager( + const std::vector &pages, + InspectorPackagerConnection::BundleStatus bundleStatus) { + folly::dynamic payload = folly::dynamic::array; + for (const facebook::react::InspectorPage &page : pages) { + folly::dynamic pageDyn = folly::dynamic::object; + pageDyn["id"] = page.id; + pageDyn["title"] = page.title; + pageDyn["vm"] = page.vm; + + pageDyn["isLastBundleDownloadSuccess"] = bundleStatus.m_isLastDownloadSucess; + pageDyn["bundleUpdateTimestamp"] = bundleStatus.m_updateTimestamp; + + payload.push_back(pageDyn); + } + return payload; + } + + static folly::dynamic constructVMResponsePayloadForPackager(int64_t pageId, std::string &&messageFromVM) { + folly::dynamic payload = folly::dynamic::object; + payload[InspectorProtocol::Message_eventName_wrappedEvent] = messageFromVM; + payload[InspectorProtocol::Message_PAGEID] = pageId; + return payload; + } + + static folly::dynamic constructVMResponsePayloadOnDisconnectForPackager(int64_t pageId) { + folly::dynamic payload = folly::dynamic::object; + payload[InspectorProtocol::Message_PAGEID] = pageId; + return payload; + } +}; + +} // namespace + +RemoteConnection::RemoteConnection(int64_t pageId, const InspectorPackagerConnection &packagerConnection) + : m_packagerConnection(packagerConnection), m_pageId(pageId) {} + +void RemoteConnection::onMessage(std::string message) { + folly::dynamic response = InspectorProtocol::constructResponseForPackager( + InspectorProtocol::EventType::WrappedEvent, + InspectorProtocol::constructVMResponsePayloadForPackager(m_pageId, std::move(message))); + std::string responsestr = folly::toJson(response); + m_packagerConnection.sendMessageToPackager(std::move(responsestr)); +} + +void RemoteConnection::onDisconnect() { + folly::dynamic response = InspectorProtocol::constructResponseForPackager( + InspectorProtocol::EventType::Disconnect, + InspectorProtocol::constructVMResponsePayloadOnDisconnectForPackager(m_pageId)); + + std::string responsestr = folly::toJson(response); + m_packagerConnection.sendMessageToPackager(std::move(responsestr)); +} + +winrt::fire_and_forget InspectorPackagerConnection::sendMessageToPackagerAsync(std::string &&message) const { + std::string message_(std::move(message)); + co_await winrt::resume_background(); + m_packagerWebSocketConnection->Send(std::move(message_)); + co_return; +} + +void InspectorPackagerConnection::sendMessageToPackager(std::string &&message) const { + sendMessageToPackagerAsync(std::move(message)); +} + +void InspectorPackagerConnection::sendMessageToVM(int64_t pageId, std::string &&message) { + m_localConnections[pageId]->sendMessage(std::move(message)); +} + +InspectorPackagerConnection::InspectorPackagerConnection( + std::string &&url, + std::shared_ptr bundleStatusProvider) + : m_url(std::move(url)), m_bundleStatusProvider(std::move(bundleStatusProvider)) {} + +winrt::fire_and_forget InspectorPackagerConnection::disconnectAsync() { + co_await winrt::resume_background(); + std::string reason("Explicit close"); + m_packagerWebSocketConnection->Close(Microsoft::React::WinRTWebSocketResource::CloseCode::GoingAway, reason); + co_return; +} + +winrt::fire_and_forget InspectorPackagerConnection::connectAsync() { + co_await winrt::resume_background(); + + std::vector certExceptions; + m_packagerWebSocketConnection = + std::make_shared(m_url, std::move(certExceptions)); + + m_packagerWebSocketConnection->SetOnError([](const Microsoft::React::IWebSocketResource::Error &err) { + facebook::react::tracing::error(err.Message.c_str()); + }); + + m_packagerWebSocketConnection->SetOnConnect( + []() { facebook::react::tracing::log("Inspector: Websocket connection succeeded."); }); + + m_packagerWebSocketConnection->SetOnMessage([self = shared_from_this()]( + size_t /*length*/, const std::string &message, bool isBinary) { + assert(!isBinary && "We don't expect any binary messages !"); + folly::dynamic messageDyn = folly::parseJson(message); + + InspectorProtocol::EventType eventType = InspectorProtocol::getEventType(messageDyn); + switch (eventType) { + case InspectorProtocol::EventType::GetPages: { + std::vector inspetorPages = facebook::react::getInspectorInstance().getPages(); + folly::dynamic response = InspectorProtocol::constructResponseForPackager( + InspectorProtocol::EventType::GetPages, + InspectorProtocol::constructGetPagesResponsePayloadForPackager( + inspetorPages, self->m_bundleStatusProvider->getBundleStatus())); + + std::string responsestr = folly::toJson(response); + self->sendMessageToPackager(std::move(responsestr)); + } break; + + case InspectorProtocol::EventType::WrappedEvent: { + folly::dynamic payload = messageDyn[InspectorProtocol::Message_PAYLOAD]; + int64_t pageId = payload[InspectorProtocol::Message_PAGEID].asInt(); + + if (self->m_localConnections.find(pageId) == self->m_localConnections.end()) { + break; + } + + std::string wrappedEvent = payload[InspectorProtocol::Message_eventName_wrappedEvent].getString(); + self->sendMessageToVM(pageId, std::move(wrappedEvent)); + } break; + + case InspectorProtocol::EventType::Connect: { + folly::dynamic payload = messageDyn[InspectorProtocol::Message_PAYLOAD]; + int64_t pageId = payload[InspectorProtocol::Message_PAGEID].asInt(); + + if (self->m_localConnections.find(pageId) != self->m_localConnections.end()) { + break; + } + + self->m_localConnections[pageId] = facebook::react::getInspectorInstance().connect( + static_cast(pageId), std::make_unique(pageId, *self)); + } break; + + case InspectorProtocol::EventType::Disconnect: { + folly::dynamic payload = messageDyn[InspectorProtocol::Message_PAYLOAD]; + int64_t pageId = payload[InspectorProtocol::Message_PAGEID].asInt(); + if (self->m_localConnections.find(pageId) != self->m_localConnections.end()) { + self->m_localConnections[pageId]->disconnect(); + self->m_localConnections.erase(pageId); + } + + } break; + } + }); + + Microsoft::React::IWebSocketResource::Protocols protocols; + Microsoft::React::IWebSocketResource::Options options; + m_packagerWebSocketConnection->Connect(protocols, options); + + co_return; +} + +} // namespace Microsoft::ReactNative + +#endif diff --git a/vnext/Shared/InspectorPackagerConnection.h b/vnext/Shared/InspectorPackagerConnection.h new file mode 100644 index 0000000000..fdb56a97a5 --- /dev/null +++ b/vnext/Shared/InspectorPackagerConnection.h @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +namespace Microsoft::ReactNative { + +class InspectorPackagerConnection final : public std::enable_shared_from_this { + public: + winrt::fire_and_forget connectAsync(); + winrt::fire_and_forget disconnectAsync(); + + class BundleStatus { + public: + bool m_isLastDownloadSucess; + int64_t m_updateTimestamp = -1; + + BundleStatus(bool isLastDownloadSucess, long updateTimestamp) + : m_isLastDownloadSucess(isLastDownloadSucess), m_updateTimestamp(updateTimestamp) {} + BundleStatus() : m_isLastDownloadSucess(false), m_updateTimestamp(-1) {} + }; + + struct IBundleStatusProvider { + virtual BundleStatus getBundleStatus() = 0; + }; + + InspectorPackagerConnection(std::string &&url, std::shared_ptr bundleStatusProvider); + + private: + friend class RemoteConnection; + + winrt::fire_and_forget sendMessageToPackagerAsync(std::string &&message) const; + void sendMessageToPackager(std::string &&message) const; + + // Note:: VM side Inspector processes the messages asynchronousely in a sequential executor with dedicated thread. + // Hence, we don't bother invoking the inspector asynchronously. + void sendMessageToVM(int64_t pageId, std::string &&message); + + std::unordered_map> m_localConnections; + std::shared_ptr m_packagerWebSocketConnection; + std::shared_ptr m_bundleStatusProvider; + std::string m_url; +}; + +class RemoteConnection final : public facebook::react::IRemoteConnection { + public: + RemoteConnection(int64_t pageId, const InspectorPackagerConnection &packagerConnection); + void onMessage(std::string message) override; + void onDisconnect() override; + + private: + int64_t m_pageId; + const InspectorPackagerConnection &m_packagerConnection; +}; +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index 023cd49e0d..7a43666857 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -53,14 +53,9 @@ #include #include "ChakraRuntimeHolder.h" +#include namespace fs = std::filesystem; -// forward declaration. -namespace facebook::react::tracing { -void initializeETW(); -void initializeJSHooks(facebook::jsi::Runtime &runtime); -} // namespace facebook::react::tracing - namespace { #if (defined(_MSC_VER) && !defined(WINRT)) @@ -297,6 +292,10 @@ InstanceImpl::InstanceImpl( facebook::react::tracing::initializeETW(); #endif + if (m_devSettings->useDirectDebugger && !m_devSettings->useWebDebugger) { + m_devManager->StartInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); + } + // Default (common) NativeModules auto modules = GetDefaultNativeModules(nativeQueue); @@ -345,7 +344,7 @@ InstanceImpl::InstanceImpl( switch (m_devSettings->jsiEngineOverride) { case JSIEngineOverride::Hermes: #if defined(USE_HERMES) - m_devSettings->jsiRuntimeHolder = std::make_shared(); + m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread); m_devSettings->inlineSourceMap = false; break; #else @@ -433,10 +432,16 @@ void InstanceImpl::loadBundleInternal(std::string &&jsBundleRelativePath, bool s m_devSettings->inlineSourceMap); if (!success) { + m_devManager->UpdateBundleStatus(false, -1); m_devSettings->errorCallback(jsBundleString); return; } + int64_t currentTimeInMilliSeconds = + std::chrono::duration_cast(std::chrono ::system_clock::now().time_since_epoch()) + .count(); + m_devManager->UpdateBundleStatus(true, currentTimeInMilliSeconds); + auto bundleUrl = DevServerHelper::get_BundleUrl( m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, @@ -484,6 +489,7 @@ void InstanceImpl::loadBundleInternal(std::string &&jsBundleRelativePath, bool s } InstanceImpl::~InstanceImpl() { + m_devManager->StopInspector(); m_nativeQueue->quitSynchronous(); } diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index 2d21f0d58a..473ea243cd 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -29,6 +29,9 @@ true + + true + @@ -91,6 +94,7 @@ + @@ -120,6 +124,7 @@ + diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index 8ae923cda7..4bc0d9eceb 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -124,6 +124,9 @@ Source Files\JSI + + Source Files + @@ -363,6 +366,12 @@ Header Files\JSI + + Header Files + + + Header Files\tracing + diff --git a/vnext/Shared/tracing/tracing.cpp b/vnext/Shared/tracing/tracing.cpp index 50f169fa30..71abbfc5cb 100644 --- a/vnext/Shared/tracing/tracing.cpp +++ b/vnext/Shared/tracing/tracing.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "tracing/fbsystrace.h" #include @@ -383,6 +384,16 @@ void initializeETW() { } } +void log(const char *msg) { + TraceLoggingWrite( + g_hTraceLoggingProvider, "Trace", TraceLoggingLevel(WINEVENT_LEVEL_INFO), TraceLoggingString(msg, "message")); +} + +void error(const char *msg) { + TraceLoggingWrite( + g_hTraceLoggingProvider, "Trace", TraceLoggingLevel(WINEVENT_LEVEL_ERROR), TraceLoggingString(msg, "message")); +} + } // namespace tracing } // namespace react } // namespace facebook diff --git a/vnext/Shared/tracing/tracing.h b/vnext/Shared/tracing/tracing.h new file mode 100644 index 0000000000..d5cf919490 --- /dev/null +++ b/vnext/Shared/tracing/tracing.h @@ -0,0 +1,20 @@ +#pragma once + +// forward declaration. +namespace facebook { +namespace jsi { +class Runtime; +} +} // namespace facebook + +namespace facebook { +namespace react { +namespace tracing { +void initializeETW(); +void initializeJSHooks(facebook::jsi::Runtime &runtime); + +void log(const char *msg); +void error(const char *msg); +} // namespace tracing +} // namespace react +} // namespace facebook