diff --git a/change/react-native-windows-96f33c2e-d47a-4ff4-bcfc-20490d838ad9.json b/change/react-native-windows-96f33c2e-d47a-4ff4-bcfc-20490d838ad9.json new file mode 100644 index 00000000000..833fab8f9d5 --- /dev/null +++ b/change/react-native-windows-96f33c2e-d47a-4ff4-bcfc-20490d838ad9.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Added support for modern inspector", + "packageName": "react-native-windows", + "email": "vmorozov@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/docs/modern-inspector-details.md b/docs/modern-inspector-details.md new file mode 100644 index 00000000000..bf55e0611d8 --- /dev/null +++ b/docs/modern-inspector-details.md @@ -0,0 +1,70 @@ +# Modern Inspector Support in React Native for Windows + +## Overview +The modern inspector is the Chrome DevTools–based debugging experience that ships with the latest +versions of React Native. This experience now works end-to-end for React Native for Windows (RNW) +applications, enabling parity with Android and iOS. The integration provides a unified way to inspect +JavaScript execution, evaluate console expressions, profile CPU and memory usage, and visualize the +component hierarchy for both the Paper and Composition UI stacks. + +## Modern Inspector Building Blocks +- **Host Target** – One per `ReactHost`; surfaces metadata, reload requests, pause overlays, and + implements the CDP-facing delegate (`ReactInspectorHostTargetDelegate`). +- **Instance Target** – Created for each React instance; registers runtime targets, tracks instance + lifecycle, and unregisters cleanly on reload. +- **Runtime Target & Agent** – Runtime targets map to JavaScript VMs; agents are per-session objects + that translate Chrome DevTools Protocol (CDP) messages into engine calls. RNW mirrors upstream + lifetimes so reloads tear everything down deterministically. +- **Frontend Channel** – Delivers JSON CDP payloads between the RNW host and DevTools. +- **Inspector Thread** – A single `ReactInspectorThread` ensures CDP work is serialized away from + the UI and JS queues. (iOS and Andrtoid use UI thread.) +- **Debugger Notifications** – `DebuggerNotifications.h` broadcasts pause/resume events so view + hosts can react (e.g., showing overlays or resuming when the debugger continues). + +## Windows Integration Points +- **ReactHost & ReactOptions** – `ReactHost` creates the `HostTarget`, exposes it through + `ReactOptions::InspectorHostTarget`, and implements reload/pause hooks. This is the jump-off point + for all inspector traffic. The inspector supported only if the `UseDirectDebugger` is true. +- **ReactInstanceWin / OInstance** – Register and unregister instance/runtime targets around runtime + creation, keeping the inspector aligned with bridgeless and legacy bridge lifecycles. +- **DevSupportManager & Packager** – `DevSupportManager` now spins up + `ReactInspectorPackagerConnectionDelegate`, allowing Metro to broker modern inspector connections + and reuse the existing websocket infrastructure. +- **Dev Console Shortcut** – Metro’s `J` shortcut launches the inspector for Windows apps, matching + upstream behavior. + +## UI Overlays +- **Composition** – `DebuggerUIIsland` renders pause overlays, focus chrome, and selection adorners + whenever the runtime is paused. +- **Paper** – `ReactRootView` updates provide the same pause/selection affordances. + +## Hermes Runtime Integration +- `HermesRuntimeTargetDelegate` and `HermesRuntimeAgentDelegate` wrap the hermes-windows C debug API + so we can re-use upstream modern inspector code. +- `RuntimeHolder`/`HermesRuntimeHolder` surface a `createRuntimeTargetDelegate` hook that instantiates + delegates only when the inspector is enabled, and safely tears them down during reloads. + +## Packager & Console Integration +- `ReactInspectorPackagerConnectionDelegate` maps the Metro websocket APIs to the modern inspector + handshake. +- Console output, CPU sampling, and memory profiling are forwarded through the Hermes inspector + plumbing automatically once a session is active. + +## Using the Modern Inspector with RNW +1. Start your Metro bundler (`npx react-native start` or `yarn start`). +2. Launch your RNW app (Paper or Composition). +3. In the Metro console, press `J` to open the modern inspector URL in a browser. +4. Chrome DevTools will connect to the Hermes runtime. Pause execution, explore the component tree, + and capture profiles as needed. +5. When execution is paused, the corresponding overlay is rendered in the app window; resume to clear + the overlay. + +## Known Limitations & Follow-Up Work +- **Network profiling** – The `NetworkIOAgent` is not wired up yet for Windows. The integration point + is the `ReactInspectorHostTargetDelegate` in + `vnext/Microsoft.ReactNative/ReactHost/ReactHost.cpp`; override `loadNetworkResource` there to + forward requests through a Windows HTTP helper (similar to `GetJavaScriptFromServerAsync`) and + stream results back via the provided `NetworkRequestListener`. Until this happens, the Network tab + in DevTools stays empty. +- **Legacy Chakra runtime** – Modern inspector support currently targets Hermes. Chakra-based apps + continue to rely on legacy debugging flows. diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggerUIIsland.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggerUIIsland.cpp new file mode 100644 index 00000000000..eb23179c9ad --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggerUIIsland.cpp @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DebuggerUIIsland.h" + +#include +#include +#include +#include +#include +#include +#include "CompositionContextHelper.h" +#include "TextDrawing.h" + +namespace winrt::Microsoft::ReactNative::implementation { + +constexpr float debuggerUIFontSize = 10.0f; +constexpr float debuggerTextMargin = 4.0f; + +DebuggerUIIsland::DebuggerUIIsland( + const winrt::Microsoft::UI::Composition::Compositor &compositor, + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, + winrt::Microsoft::ReactNative::Composition::Theme theme) noexcept + : m_compositor(compositor), m_compContext(compContext), m_theme(theme) { + m_backgroundVisual = m_compositor.CreateSpriteVisual(); + m_backgroundVisual.RelativeSizeAdjustment({1.0f, 1.0f}); + + auto backgroundBrush = m_compositor.CreateColorBrush({100, 0, 0, 0}); + m_backgroundVisual.Brush(backgroundBrush); + + m_TextVisual = m_compositor.CreateSpriteVisual(); + m_TextVisual.IsPixelSnappingEnabled(true); + + m_backgroundVisual.Children().InsertAtTop(m_TextVisual); +} + +DebuggerUIIsland::~DebuggerUIIsland() noexcept { + m_island.StateChanged(m_islandStateChangedToken); +} + +void DebuggerUIIsland::Redraw() noexcept { + if (!m_island) + return; + + if (m_island.ActualSize().x == 0 || m_island.ActualSize().y == 0) + return; + + auto scaleFactor = m_island.Environment().DisplayScale(); + + auto attributedString = facebook::react::AttributedString{}; + auto fragment = facebook::react::AttributedString::Fragment{}; + fragment.string = m_message; + fragment.textAttributes.fontSize = debuggerUIFontSize; + attributedString.appendFragment(std::move(fragment)); + + // Resume Icon + auto iconFragment = facebook::react::AttributedString::Fragment{}; + iconFragment.string = " \uF08F"; + iconFragment.textAttributes.fontFamily = "Segoe Fluent Icons"; + iconFragment.textAttributes.fontSize = debuggerUIFontSize; + attributedString.appendFragment(std::move(iconFragment)); + + auto attributedStringBox = facebook::react::AttributedStringBox{attributedString}; + + facebook::react::LayoutConstraints constraints; + constraints.maximumSize.width = std::max(0.0f, m_island.ActualSize().x - debuggerTextMargin * 2 * scaleFactor); + constraints.maximumSize.height = std::max(0.0f, m_island.ActualSize().y - debuggerTextMargin * 2 * scaleFactor); + + auto textAttributes = facebook::react::TextAttributes{}; + textAttributes.foregroundColor = facebook::react::blackColor(); + + winrt::com_ptr<::IDWriteTextLayout> textLayout; + facebook::react::WindowsTextLayoutManager::GetTextLayout(attributedStringBox, {}, constraints, textLayout); + + DWRITE_TEXT_METRICS tm; + textLayout->GetMetrics(&tm); + + winrt::Windows::Foundation::Size surfaceSize = { + std::ceilf(std::min(constraints.maximumSize.width, tm.width + debuggerTextMargin * 2 * scaleFactor)), + std::ceilf(std::min(constraints.maximumSize.height, tm.height + debuggerTextMargin * 2 * scaleFactor))}; + auto drawingSurface = m_compContext.CreateDrawingSurfaceBrush( + surfaceSize, + winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, + winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied); + + POINT offset; + { + ::Microsoft::ReactNative::Composition::AutoDrawDrawingSurface autoDraw(drawingSurface, scaleFactor, &offset); + if (auto d2dDeviceContext = autoDraw.GetRenderTarget()) { + d2dDeviceContext->Clear(D2D1::ColorF{1.0f, 1.0f, 0.76f, 1.0f}); + + auto theme = winrt::get_self(m_theme); + + Composition::RenderText( + *d2dDeviceContext, + *textLayout, + attributedStringBox.getValue(), + textAttributes, + {static_cast(offset.x + std::floorf(debuggerTextMargin * scaleFactor)), + static_cast(offset.y + std::floorf(debuggerTextMargin * scaleFactor))}, + scaleFactor, + *theme); + } + + drawingSurface.HorizontalAlignmentRatio(0.0f); + drawingSurface.Stretch(winrt::Microsoft::ReactNative::Composition::Experimental::CompositionStretch::None); + + m_TextVisual.Brush(winrt::Microsoft::ReactNative::Composition::Experimental::implementation:: + MicrosoftCompositionContextHelper::InnerBrush(drawingSurface)); + m_TextVisual.Size({surfaceSize.Width, surfaceSize.Height}); + + m_debuggerHitRect = { + m_island.ActualSize().x / 2 - tm.width / 2 + debuggerTextMargin * scaleFactor, + debuggerTextMargin * scaleFactor, + surfaceSize.Width, + surfaceSize.Height}; + + m_TextVisual.Offset({m_debuggerHitRect.X, m_debuggerHitRect.Y, 0.0f}); + } +} + +void DebuggerUIIsland::Message(std::string &&value) noexcept { + m_message = value; + Redraw(); +} + +winrt::Microsoft::UI::Content::ContentIsland DebuggerUIIsland::Island() noexcept { + if (!m_island) { + m_island = winrt::Microsoft::UI::Content::ContentIsland::Create(m_backgroundVisual); + + m_islandStateChangedToken = + m_island.StateChanged([weakThis = weak_from_this()]( + winrt::Microsoft::UI::Content::ContentIsland const &island, + winrt::Microsoft::UI::Content::ContentIslandStateChangedEventArgs const &args) { + if (auto pThis = weakThis.lock()) { + if (args.DidRasterizationScaleChange() || args.DidActualSizeChange()) { + pThis->Redraw(); + } + } + }); + + auto pointerSource = winrt::Microsoft::UI::Input::InputPointerSource::GetForIsland(m_island); + + m_islandPointerUpToken = + pointerSource.PointerReleased([weakThis = weak_from_this()]( + winrt::Microsoft::UI::Input::InputPointerSource const &, + winrt::Microsoft::UI::Input::PointerEventArgs const &args) { + if (auto pThis = weakThis.lock()) { + auto position = args.CurrentPoint().Position(); + if (position.X >= pThis->m_debuggerHitRect.X && position.Y >= pThis->m_debuggerHitRect.Y && + position.X <= pThis->m_debuggerHitRect.X + pThis->m_debuggerHitRect.Width && + position.Y <= pThis->m_debuggerHitRect.Y + pThis->m_debuggerHitRect.Height) { + pThis->m_resumedEvent(nullptr, nullptr); + } + } + }); + } + return m_island; +} + +winrt::event_token DebuggerUIIsland::Resumed( + winrt::Windows::Foundation::EventHandler const &handler) noexcept { + return m_resumedEvent.add(handler); +} +void DebuggerUIIsland::Resumed(winrt::event_token const &token) noexcept { + m_resumedEvent.remove(token); +} + +} // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggerUIIsland.h b/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggerUIIsland.h new file mode 100644 index 00000000000..9fc719ad0d1 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggerUIIsland.h @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#include +#include + +namespace winrt::Microsoft::ReactNative::implementation { + +struct DebuggerUIIsland : std::enable_shared_from_this { + DebuggerUIIsland( + const winrt::Microsoft::UI::Composition::Compositor &compositor, + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext, + winrt::Microsoft::ReactNative::Composition::Theme theme) noexcept; + ~DebuggerUIIsland() noexcept; + winrt::Microsoft::UI::Content::ContentIsland Island() noexcept; + + void Message(std::string &&value) noexcept; + + winrt::event_token Resumed( + winrt::Windows::Foundation::EventHandler const &handler) noexcept; + void Resumed(winrt::event_token const &token) noexcept; + + private: + void Redraw() noexcept; + + winrt::event_token m_islandStateChangedToken; + winrt::event_token m_islandPointerUpToken; + + winrt::Microsoft::UI::Composition::SpriteVisual m_backgroundVisual{nullptr}; + winrt::Microsoft::UI::Composition::SpriteVisual m_TextVisual{nullptr}; + winrt::Windows::Foundation::Rect m_debuggerHitRect{0, 0, 0, 0}; + winrt::Microsoft::ReactNative::Composition::Theme m_theme{nullptr}; + std::string m_message; + + winrt::event> m_resumedEvent; + winrt::Microsoft::UI::Composition::Compositor m_compositor{nullptr}; + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext m_compContext{nullptr}; + winrt::Microsoft::UI::Content::ContentIsland m_island{nullptr}; +}; + +} // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp index fd84303f8e1..84452bf67aa 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp @@ -103,9 +103,9 @@ void DebuggingOverlayComponentView::HandleCommand( auto rootVisual = root->OuterVisual(); while (m_activeOverlays != 0) { + --m_activeOverlays; auto visual = rootVisual.GetAt(root->overlayIndex() + m_activeOverlays); rootVisual.Remove(visual); - --m_activeOverlays; } } return; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp index d1424fa0ebd..89f1a8b097f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp @@ -284,6 +284,7 @@ void ReactNativeIsland::UpdateRootVisualSize() noexcept { m_rootVisual.Size({m_size.Width * m_scaleFactor, m_size.Height * m_scaleFactor}); UpdateLoadingVisualSize(); + UpdateDebuggerVisualSize(); } void ReactNativeIsland::UpdateLoadingVisualSize() noexcept { @@ -300,6 +301,13 @@ void ReactNativeIsland::UpdateLoadingVisualSize() noexcept { } } +void ReactNativeIsland::UpdateDebuggerVisualSize() noexcept { + if (!m_debuggerChildSiteLink) + return; + + m_debuggerChildSiteLink.ActualSize(m_size); +} + float ReactNativeIsland::ScaleFactor() noexcept { return m_scaleFactor; } @@ -484,6 +492,20 @@ void ReactNativeIsland::InitRootView( m_CompositionEventHandler = std::make_shared<::Microsoft::ReactNative::CompositionEventHandler>(m_context, *this); m_CompositionEventHandler->Initialize(); + ::Microsoft::ReactNative::DebuggerNotifications::SubscribeShowDebuggerPausedOverlay( + m_context.Notifications().Handle(), + m_context.UIDispatcher().Handle(), + [weakThis = get_weak()](std::string message, std::function onResume) { + if (auto strongThis = weakThis.get()) { + strongThis->ShowDebuggerUI(message, onResume); + } + }, + [weakThis = get_weak()]() { + if (auto strongThis = weakThis.get()) { + strongThis->HideDebuggerUI(); + } + }); + UpdateRootViewInternal(); m_isInitialized = true; @@ -746,7 +768,10 @@ void ReactNativeIsland::ShowInstanceLoading() noexcept { NotifySizeChanged(); UpdateLoadingVisualSize(); - InternalRootVisual().InsertAt(m_loadingVisual, m_hasRenderedVisual ? 1 : 0); + // ShowDebuggerUI(); // TEMP + + InternalRootVisual().InsertAt( + m_loadingVisual, m_hasRenderedVisual ? (m_debuggerVisual ? 2 : 1) : (m_debuggerVisual ? 1 : 0)); } void ReactNativeIsland::InitTextScaleMultiplier() noexcept { @@ -767,6 +792,48 @@ void ReactNativeIsland::InitTextScaleMultiplier() noexcept { }); } +void ReactNativeIsland::ShowDebuggerUI(std::string message, const std::function &onResume) noexcept { + if (!m_debuggerVisual) { + auto compContext = + winrt::Microsoft::ReactNative::Composition::implementation::CompositionUIService::GetCompositionContext( + m_context.Properties().Handle()); + m_debuggerVisual = compContext.CreateSpriteVisual(); + + m_debuggerChildSiteLink = winrt::Microsoft::UI::Content::ChildSiteLink::Create( + Island(), + winrt::Microsoft::ReactNative::Composition::Experimental::MicrosoftCompositionContextHelper::InnerVisual( + m_debuggerVisual) + .as()); + + m_debuggerUIIsland = std::make_shared(m_compositor, compContext, Theme()); + m_debuggerUIIsland->Message(std::string(message)); + m_debuggerVisual.RelativeSizeWithOffset({0.0f, 0.0f}, {1.0f, 1.0f}); + + m_debuggerUIIsland->Resumed( + [wkThis = get_weak(), onResume]( + const winrt::Windows::Foundation::IInspectable &, const winrt::Windows::Foundation::IInspectable &) { + if (auto pThis = wkThis.get()) { + // pThis->HideDebuggerUI(); + onResume(); + } + }); + + InternalRootVisual().InsertAt(m_debuggerVisual, m_hasRenderedVisual ? 1 : 0); + m_debuggerUIIsland->Island().IsHitTestVisibleWhenTransparent(false); + m_debuggerChildSiteLink.Connect(m_debuggerUIIsland->Island()); + } + + m_debuggerVisual.IsVisible(true); + + UpdateRootVisualSize(); +} + +void ReactNativeIsland::HideDebuggerUI() noexcept { + if (m_debuggerVisual) { + m_debuggerVisual.IsVisible(false); + } +} + winrt::Windows::Foundation::Size ReactNativeIsland::Measure( const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraints, const winrt::Windows::Foundation::Point &viewportOffset) const { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h index 2b959123387..bfdae77958e 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h @@ -7,11 +7,14 @@ #include #include +#include #include #include #include +#include #include #include "CompositionEventHandler.h" +#include "DebuggerUIIsland.h" #include "PortalComponentView.h" #include "ReactHost/React.h" @@ -181,6 +184,9 @@ struct ReactNativeIsland std::shared_ptr<::Microsoft::ReactNative::CompositionEventHandler> m_CompositionEventHandler; winrt::Microsoft::ReactNative::Composition::Experimental::IVisual m_rootVisual{nullptr}; winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual m_loadingVisual{nullptr}; + winrt::Microsoft::UI::Content::ChildSiteLink m_debuggerChildSiteLink{nullptr}; + std::shared_ptr m_debuggerUIIsland; + winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual m_debuggerVisual{nullptr}; winrt::Microsoft::ReactNative::Composition::Experimental::IActivityVisual m_loadingActivityVisual{nullptr}; winrt::Microsoft::ReactNative::Composition::ICustomResourceLoader m_resources{nullptr}; winrt::Microsoft::ReactNative::Composition::Theme m_theme{nullptr}; @@ -196,8 +202,11 @@ struct ReactNativeIsland void ShowInstanceLoaded() noexcept; void ShowInstanceError() noexcept; void ShowInstanceLoading() noexcept; + void ShowDebuggerUI(std::string message, const std::function &onResume) noexcept; + void HideDebuggerUI() noexcept; void UpdateRootVisualSize() noexcept; void UpdateLoadingVisualSize() noexcept; + void UpdateDebuggerVisualSize() noexcept; Composition::Experimental::IDrawingSurfaceBrush CreateLoadingVisualBrush() noexcept; void ApplyConstraints( const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraintsIn, diff --git a/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp b/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp index 7860f83c047..13c9d242388 100644 --- a/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp @@ -167,6 +167,9 @@ void FabricUIManager::setProps(facebook::react::SurfaceId surfaceId, const folly } void FabricUIManager::stopSurface(facebook::react::SurfaceId surfaceId) noexcept { + if (surfaceId == -1) { + return; + } visit(surfaceId, [&](const facebook::react::SurfaceHandler &surfaceHandler) { surfaceHandler.stop(); m_scheduler->unregisterSurface(surfaceHandler); @@ -176,7 +179,9 @@ void FabricUIManager::stopSurface(facebook::react::SurfaceId surfaceId) noexcept std::unique_lock lock(m_handlerMutex); auto iterator = m_handlerRegistry.find(surfaceId); - m_handlerRegistry.erase(iterator); + if (iterator != m_handlerRegistry.end()) { + m_handlerRegistry.erase(iterator); + } } auto &rootDescriptor = m_registry.componentViewDescriptorWithTag(surfaceId); diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.cpp index 9c7318103bd..945d28ee97e 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.cpp @@ -12,8 +12,11 @@ #include #include -#include +#include #include +#include +#include +#include #include #include @@ -25,40 +28,88 @@ namespace facebook::react { -TaskDispatchThread::TaskDispatchThread(std::string threadName, int priorityOffset) noexcept - : threadName_(std::move(threadName)) { -#ifdef ANDROID - // Attaches the thread to JVM just in case anything calls out to Java - thread_ = std::thread([&]() { - facebook::jni::ThreadScope::WithClassLoader([&]() { - int result = setpriority(PRIO_PROCESS, static_cast(::syscall(SYS_gettid)), priorityOffset); +class TaskDispatchThread::Impl : public std::enable_shared_from_this { + public: + Impl(std::string &&threadName) noexcept; + ~Impl() noexcept; + + void start() noexcept; + bool isOnThread() noexcept; + bool isRunning() noexcept; + void runAsync(TaskFn &&task, std::chrono::milliseconds delayMs = std::chrono::milliseconds::zero()) noexcept; + void runSync(TaskFn &&task) noexcept; + void quit() noexcept; + void loop() noexcept; + + private: + struct Task { + TimePoint dispatchTime; + TaskFn fn; + + Task(TimePoint dispatchTime, TaskFn &&fn) : dispatchTime(dispatchTime), fn(std::move(fn)) {} + + bool operator<(const Task &other) const { + // Have the earliest tasks be at the front of the queue. + return dispatchTime > other.dispatchTime; + } + }; + + std::mutex queueLock_; + std::condition_variable loopCv_; + std::priority_queue queue_; + std::atomic running_{true}; + std::string threadName_; + std::thread thread_; +}; + +TaskDispatchThread::TaskDispatchThread(std::string threadName, int /*priorityOffset*/) noexcept + : impl_(std::make_shared(std::move(threadName))) { + impl_->start(); +} - if (result != 0) { - LOG(INFO) << " setCurrentThreadPriority failed with pri errno: " << errno; - } +TaskDispatchThread::~TaskDispatchThread() noexcept { + impl_->quit(); +} - loop(); - }); - }); +bool TaskDispatchThread::isOnThread() noexcept { + return impl_->isOnThread(); +} -#else - thread_ = std::thread(&TaskDispatchThread::loop, this); -#endif +bool TaskDispatchThread::isRunning() noexcept { + return impl_->isRunning(); } -TaskDispatchThread::~TaskDispatchThread() noexcept { +void TaskDispatchThread::runAsync(TaskFn &&task, std::chrono::milliseconds delayMs) noexcept { + impl_->runAsync(std::move(task), delayMs); +} + +void TaskDispatchThread::runSync(TaskFn &&task) noexcept { + impl_->runSync(std::move(task)); +} + +void TaskDispatchThread::quit() noexcept { + impl_->quit(); +} + +TaskDispatchThread::Impl::Impl(std::string &&threadName) noexcept : threadName_(std::move(threadName)) {} + +TaskDispatchThread::Impl::~Impl() noexcept { quit(); } -bool TaskDispatchThread::isOnThread() noexcept { +void TaskDispatchThread::Impl::start() noexcept { + thread_ = std::thread([self = shared_from_this()]() { self->loop(); }); +} + +bool TaskDispatchThread::Impl::isOnThread() noexcept { return std::this_thread::get_id() == thread_.get_id(); } -bool TaskDispatchThread::isRunning() noexcept { +bool TaskDispatchThread::Impl::isRunning() noexcept { return running_; } -void TaskDispatchThread::runAsync(TaskFn &&task, std::chrono::milliseconds delayMs) noexcept { +void TaskDispatchThread::Impl::runAsync(TaskFn &&task, std::chrono::milliseconds delayMs) noexcept { if (!running_) { return; } @@ -68,7 +119,7 @@ void TaskDispatchThread::runAsync(TaskFn &&task, std::chrono::milliseconds delay loopCv_.notify_one(); } -void TaskDispatchThread::runSync(TaskFn &&task) noexcept { +void TaskDispatchThread::Impl::runSync(TaskFn &&task) noexcept { std::promise promise; runAsync([&]() { if (running_) { @@ -79,7 +130,7 @@ void TaskDispatchThread::runSync(TaskFn &&task) noexcept { promise.get_future().wait(); } -void TaskDispatchThread::quit() noexcept { +void TaskDispatchThread::Impl::quit() noexcept { if (!running_) { return; } @@ -94,7 +145,7 @@ void TaskDispatchThread::quit() noexcept { } } -void TaskDispatchThread::loop() noexcept { +void TaskDispatchThread::Impl::loop() noexcept { if (!threadName_.empty()) { folly::setThreadName(threadName_); } diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.h b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.h index 1f161ca6997..38fd14aca49 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.h +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/threading/TaskDispatchThread.h @@ -11,11 +11,8 @@ #pragma once #include -#include #include -#include -#include -#include +#include namespace facebook::react { @@ -47,27 +44,9 @@ class TaskDispatchThread { /** Shut down and clean up the thread. */ void quit() noexcept; - protected: - struct Task { - TimePoint dispatchTime; - TaskFn fn; - - Task(TimePoint dispatchTime, TaskFn &&fn) : dispatchTime(dispatchTime), fn(std::move(fn)) {} - - bool operator<(const Task &other) const { - // Have the earliest tasks be at the front of the queue. - return dispatchTime > other.dispatchTime; - } - }; - - void loop() noexcept; - - std::mutex queueLock_; - std::condition_variable loopCv_; - std::priority_queue queue_; - std::atomic running_{true}; - std::string threadName_; - std::thread thread_; + private: + class Impl; + std::shared_ptr impl_; }; } // namespace facebook::react diff --git a/vnext/Microsoft.ReactNative/JsiApi.cpp b/vnext/Microsoft.ReactNative/JsiApi.cpp index d074fff39f7..3408bd28163 100644 --- a/vnext/Microsoft.ReactNative/JsiApi.cpp +++ b/vnext/Microsoft.ReactNative/JsiApi.cpp @@ -479,7 +479,7 @@ facebook::jsi::JSError const &jsError) { \ }(); )JS"); // TODO: consider implementing this script as a resource file and loading it with the resource URL. - jsiRuntime->evaluateJavaScript(jsiPalBuffer, "Form_JSI_API_not_a_real_file"); + jsiRuntime->evaluateJavaScript(jsiPalBuffer, "jsi-internal://host-function-manager.js"); ReactNative::JsiRuntime abiJsiResult{make(Mso::Copy(jsiRuntimeHolder), Mso::Copy(jsiRuntime))}; std::scoped_lock lock{s_mutex}; auto it = s_jsiRuntimeMap.try_emplace(reinterpret_cast(jsiRuntime.get()), abiJsiResult); diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj index b50fab3cadb..f8097e89ed4 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj @@ -268,6 +268,7 @@ Code + diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters index ad0c2b22282..e2480686490 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters @@ -313,6 +313,9 @@ ReactHost + + ReactHost + ReactHost diff --git a/vnext/Microsoft.ReactNative/ReactHost/DebuggerNotifications.h b/vnext/Microsoft.ReactNative/ReactHost/DebuggerNotifications.h new file mode 100644 index 00000000000..6a083140a34 --- /dev/null +++ b/vnext/Microsoft.ReactNative/ReactHost/DebuggerNotifications.h @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace Microsoft::ReactNative { + +struct DebuggerNotifications { + static winrt::Microsoft::ReactNative::IReactPropertyName ShowDebuggerPausedOverlayEventName() noexcept { + static winrt::Microsoft::ReactNative::IReactPropertyName propertyName{ + winrt::Microsoft::ReactNative::ReactPropertyBagHelper::GetName( + winrt::Microsoft::ReactNative::ReactPropertyBagHelper::GetNamespace(L"ReactNative.Debugger"), + L"ShowDebuggerPausedOverlay")}; + return propertyName; + } + + static void OnShowDebuggerPausedOverlay( + winrt::Microsoft::ReactNative::IReactNotificationService const &service, + std::string message, + std::function onResume) { + const winrt::Microsoft::ReactNative::ReactNonAbiValue>> nonAbiValue{ + std::in_place, std::tie(message, onResume)}; + service.SendNotification(ShowDebuggerPausedOverlayEventName(), nullptr, nonAbiValue); + } + + static void OnHideDebuggerPausedOverlay(winrt::Microsoft::ReactNative::IReactNotificationService const &service) { + service.SendNotification(ShowDebuggerPausedOverlayEventName(), nullptr, nullptr); + } + + static winrt::Microsoft::ReactNative::IReactNotificationSubscription SubscribeShowDebuggerPausedOverlay( + winrt::Microsoft::ReactNative::IReactNotificationService const &service, + winrt::Microsoft::ReactNative::IReactDispatcher const &dispatcher, + std::function)> showCallback, + std::function hideCallback) { + return service.Subscribe( + ShowDebuggerPausedOverlayEventName(), + dispatcher, + [showCallback, hideCallback](auto &&, winrt::Microsoft::ReactNative::IReactNotificationArgs const &args) { + if (args.Data()) { + const auto [message, onResume] = args.Data() + .as>>>() + .Value(); + showCallback(message, onResume); + } else { + hideCallback(); + } + }); + } +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/ReactHost/React.h b/vnext/Microsoft.ReactNative/ReactHost/React.h index dc7867f39b8..c7b8f8a15f1 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/React.h +++ b/vnext/Microsoft.ReactNative/ReactHost/React.h @@ -33,6 +33,10 @@ #include #endif +namespace facebook::react::jsinspector_modern { +class HostTarget; +} // namespace facebook::react::jsinspector_modern + namespace Mso::React { // Forward declarations @@ -216,9 +220,9 @@ struct ReactOptions { //! Base path of the SDX. The absolute path of the SDX can be constructed from this and the Identity. std::string BundleRootPath; - //! Javascript Bundles - //! This List includes both Platform and User Javascript Bundles - //! Bundles are loaded into Javascript engine in the same order + //! JavaScript Bundles + //! This List includes both Platform and User JavaScript Bundles + //! Bundles are loaded into JavaScript engine in the same order //! as they are specified in this list. std::vector> JSBundles; @@ -237,7 +241,7 @@ struct ReactOptions { //! during development to report JavaScript errors to users std::shared_ptr RedBoxHandler; - //! Flag to suggest sdx owner's preference on enabling Bytecode caching in Javascript Engine for corresponding SDX. + //! Flag to suggest sdx owner's preference on enabling Bytecode caching in JavaScript Engine for corresponding SDX. bool EnableBytecode{true}; //! Flag controlling whether the JavaScript engine uses JIT compilation. @@ -347,6 +351,9 @@ struct ReactOptions { //! The callback is called when IReactInstance is destroyed and must not be used anymore. //! It is called from the native queue. OnReactInstanceDestroyedCallback OnInstanceDestroyed; + + //! The HostTarget instance for modern inspector integration. + facebook::react::jsinspector_modern::HostTarget *InspectorHostTarget; }; //! IReactHost manages a ReactNative instance. diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactHost.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactHost.cpp index 56ce0f97635..cb7737ae6ed 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactHost.cpp +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactHost.cpp @@ -8,9 +8,16 @@ #include +#include +#include #include #include +#include "Inspector/ReactInspectorThread.h" +#include "ReactHost/DebuggerNotifications.h" + +using namespace facebook::react; + namespace Mso::React { //============================================================================================= @@ -282,6 +289,10 @@ bool ReactOptions::EnableDefaultCrashHandler() const noexcept { return winrt::unbox_value_or(properties.Get(EnableDefaultCrashHandlerProperty()), false); } +//============================================================================================= +// ReactNativeWindowsFeatureFlags implementation +//============================================================================================= + class ReactNativeWindowsFeatureFlags : public facebook::react::ReactNativeFeatureFlagsDefaults { public: bool enableBridgelessArchitecture() override { @@ -295,9 +306,59 @@ class ReactNativeWindowsFeatureFlags : public facebook::react::ReactNativeFeatur bool enableCppPropsIteratorSetter() override { return true; } + + bool fuseboxEnabledRelease() override { + return true; // Enable Fusebox (modern CDP backend) by default for React Native Windows + } + + bool fuseboxNetworkInspectionEnabled() override { + return true; // Enable network inspection support in Fusebox + } +}; + +//============================================================================================= +// ReactInspectorHostTargetDelegate implementation +//============================================================================================= + +class ReactInspectorHostTargetDelegate : public jsinspector_modern::HostTargetDelegate, + public std::enable_shared_from_this { + public: + ReactInspectorHostTargetDelegate(Mso::WeakPtr &&reactHost) noexcept : m_reactHost(std::move(reactHost)) {} + + jsinspector_modern::HostTargetMetadata getMetadata() override { + // TODO: (vmoroz) provide more info + return { + .integrationName = "React Native Windows (Host)", + }; + } + + void onReload(jsinspector_modern::HostTargetDelegate::PageReloadRequest const &request) override { + if (Mso::CntPtr reactHost = m_reactHost.GetStrongPtr()) { + reactHost->ReloadInstance(); + } + } + + void onSetPausedInDebuggerMessage( + jsinspector_modern::HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest const &request) override { + if (Mso::CntPtr reactHost = m_reactHost.GetStrongPtr()) { + auto notifications = reactHost->Options().Notifications; + if (request.message.has_value()) { + ::Microsoft::ReactNative::DebuggerNotifications::OnShowDebuggerPausedOverlay( + notifications, request.message.value(), [weakReactHost = m_reactHost]() { + if (Mso::CntPtr strongReactHost = weakReactHost.GetStrongPtr()) { + strongReactHost->OnDebuggerResume(); + } + }); + } else { + ::Microsoft::ReactNative::DebuggerNotifications::OnHideDebuggerPausedOverlay(notifications); + } + } + } + + private: + Mso::WeakPtr m_reactHost; }; -std::once_flag g_FlagInitFeatureFlags; //============================================================================================= // ReactHost implementation //============================================================================================= @@ -305,9 +366,16 @@ std::once_flag g_FlagInitFeatureFlags; ReactHost::ReactHost(Mso::DispatchQueue const &queue) noexcept : Super{EnsureSerialQueue(queue)}, m_options{Queue(), m_mutex}, - m_notifyWhenClosed{ReactHostRegistry::Register(*this), Queue(), m_mutex} { - std::call_once(g_FlagInitFeatureFlags, []() noexcept { - facebook::react::ReactNativeFeatureFlags::override(std::make_unique()); + m_notifyWhenClosed{ReactHostRegistry::Register(*this), Queue(), m_mutex}, + m_inspectorHostTargetDelegate{std::make_shared(this)}, + m_inspectorHostTarget{ + jsinspector_modern::HostTarget::create(*m_inspectorHostTargetDelegate, [](std::function &&callback) { + ::Microsoft::ReactNative::ReactInspectorThread::Instance().Post( + [callback = std::move(callback)]() { callback(); }); + })} { + static std::once_flag initFeatureFlagsOnce; + std::call_once(initFeatureFlagsOnce, []() noexcept { + ReactNativeFeatureFlags::override(std::make_unique()); }); } @@ -319,15 +387,16 @@ void ReactHost::Finalize() noexcept { // Since each AsyncAction has a strong ref count to ReactHost, the AsyncActionQueue must be empty. // Thus, we only need to call UnloadInQueue to unload ReactInstance if the ReactHost is not closed yet. if (Mso::Promise notifyWhenClosed = m_notifyWhenClosed.Exchange(nullptr)) { - UnloadInQueue(0).Then( - [notifyWhenClosed = std::move(notifyWhenClosed)]() noexcept { notifyWhenClosed.TrySetValue(); }); + UnloadInQueue(UnloadReason::CloseHost, 0) + .Then( + [notifyWhenClosed = std::move(notifyWhenClosed)]() noexcept { notifyWhenClosed.TrySetValue(); }); } } void ReactHost::Close() noexcept { InvokeInQueue([this]() noexcept { // Put the ReactHost to the closed state, unload ReactInstance, and notify the closing Promise. - auto whenClosed = m_actionQueue.Load()->PostAction(MakeUnloadInstanceAction()); + auto whenClosed = m_actionQueue.Load()->PostAction(MakeUnloadInstanceAction(UnloadReason::CloseHost)); // After we set the m_notifyWhenClosed to null, the ReactHost is considered to be closed. Mso::SetPromiseValue(m_notifyWhenClosed.Exchange(nullptr), std::move(whenClosed)); @@ -379,12 +448,14 @@ Mso::Future ReactHost::ReloadInstance() noexcept { Mso::Future ReactHost::ReloadInstanceWithOptions(ReactOptions &&options) noexcept { return PostInQueue([this, options = std::move(options)]() mutable noexcept { - return m_actionQueue.Load()->PostActions({MakeUnloadInstanceAction(), MakeLoadInstanceAction(std::move(options))}); + return m_actionQueue.Load()->PostActions( + {MakeUnloadInstanceAction(UnloadReason::Unload), MakeLoadInstanceAction(std::move(options))}); }); } Mso::Future ReactHost::UnloadInstance() noexcept { - return PostInQueue([this]() noexcept { return m_actionQueue.Load()->PostAction(MakeUnloadInstanceAction()); }); + return PostInQueue( + [this]() noexcept { return m_actionQueue.Load()->PostAction(MakeUnloadInstanceAction(UnloadReason::Unload)); }); } AsyncAction ReactHost::MakeLoadInstanceAction(ReactOptions &&options) noexcept { @@ -393,11 +464,13 @@ AsyncAction ReactHost::MakeLoadInstanceAction(ReactOptions &&options) noexcept { }; } -AsyncAction ReactHost::MakeUnloadInstanceAction() noexcept { +AsyncAction ReactHost::MakeUnloadInstanceAction(UnloadReason reason) noexcept { Mso::Internal::VerifyIsInQueueElseCrash(Queue()); size_t unloadActionId = ++m_nextUnloadActionId; m_pendingUnloadActionId = unloadActionId; - return [spThis = Mso::CntPtr{this}, unloadActionId]() noexcept { return spThis->UnloadInQueue(unloadActionId); }; + return [spThis = Mso::CntPtr{this}, reason, unloadActionId]() noexcept { + return spThis->UnloadInQueue(reason, unloadActionId); + }; } Mso::CntPtr ReactHost::MakeViewHost(ReactViewOptions &&options) noexcept { @@ -428,6 +501,18 @@ Mso::Future ReactHost::LoadInQueue(ReactOptions &&options) noexcept { return Mso::MakeCanceledFuture(); } + // Start or stop inspector page if needed. + // Make sure to update the both copies of options. + if (IsInspectable()) { + AddInspectorPage(); + options.InspectorHostTarget = m_inspectorHostTarget.get(); + m_options.Load().InspectorHostTarget = m_inspectorHostTarget.get(); + } else { + RemoveInspectorPage(); + options.InspectorHostTarget = nullptr; + m_options.Load().InspectorHostTarget = nullptr; + } + Mso::Promise whenCreated; Mso::Promise whenLoaded; @@ -463,7 +548,7 @@ Mso::Future ReactHost::LoadInQueue(ReactOptions &&options) noexcept { }); } -Mso::Future ReactHost::UnloadInQueue(size_t unloadActionId) noexcept { +Mso::Future ReactHost::UnloadInQueue(UnloadReason reason, size_t unloadActionId) noexcept { Mso::Internal::VerifyIsInQueueElseCrash(Queue()); // If the pending unload action Id does not match, then we have newer unload action, @@ -485,21 +570,25 @@ Mso::Future ReactHost::UnloadInQueue(size_t unloadActionId) noexcept { // We unload ReactInstance after all view instances are unloaded. // It is safe to capture 'this' because the Unload action keeps a strong reference to ReactHost. - return Mso::WhenAllCompleted(unloadCompletionList).Then(m_executor, [this](Mso::Maybe && /*value*/) noexcept { - Mso::Future onUnloaded; - if (auto reactInstance = m_reactInstance.Exchange(nullptr)) { - onUnloaded = reactInstance->Destroy(); - } - - m_isInstanceUnloading.Store(false); - m_lastError.Store({}); - - if (!onUnloaded) { - onUnloaded = Mso::MakeSucceededFuture(); - } - - return onUnloaded; - }); + return Mso::WhenAllCompleted(unloadCompletionList) + .Then(m_executor, [this, reason](Mso::Maybe && /*value*/) noexcept { + Mso::Future onUnloaded; + if (auto reactInstance = m_reactInstance.Exchange(nullptr)) { + onUnloaded = reactInstance->Destroy(); + } + + m_isInstanceUnloading.Store(false); + m_lastError.Store({}); + + if (!onUnloaded) { + onUnloaded = Mso::MakeSucceededFuture(); + } + + if (reason == UnloadReason::CloseHost) { + RemoveInspectorPage(); + } + return onUnloaded; + }); } void ReactHost::ForEachViewHost(const Mso::FunctorRef &action) noexcept { @@ -528,6 +617,54 @@ void ReactHost::DetachViewHost(ReactViewHost &viewHost) noexcept { viewHosts.erase(it); } +bool ReactHost::IsInspectable() noexcept { + ReactOptions &options = m_options.Load(); + return options.JsiEngine() == JSIEngine::Hermes && options.UseDirectDebugger(); +} + +void ReactHost::AddInspectorPage() noexcept { + std::optional &inspectorPageId = m_inspectorPageId.Load(); + if (inspectorPageId.has_value()) + return; + + jsinspector_modern::InspectorTargetCapabilities capabilities; + capabilities.nativePageReloads = true; + capabilities.prefersFuseboxFrontend = true; + // TODO: (vmoroz) improve the page name + inspectorPageId = jsinspector_modern::getInspectorInstance().addPage( + "React Native Windows (Experimental)", + "Hermes", + [weakInspectorHostTarget = + std::weak_ptr(m_inspectorHostTarget)](std::unique_ptr remote) + -> std::unique_ptr { + if (std::shared_ptr inspectorHostTarget = weakInspectorHostTarget.lock()) { + return inspectorHostTarget->connect(std::move(remote)); + } + + // This can happen if we're about to shut down. Reject the connection. + return nullptr; + }, + capabilities); +} + +void ReactHost::RemoveInspectorPage() noexcept { + std::optional &inspectorPageId = m_inspectorPageId.Load(); + if (!inspectorPageId.has_value()) + return; + + jsinspector_modern::getInspectorInstance().removePage(*inspectorPageId); + inspectorPageId.reset(); +} + +void ReactHost::OnDebuggerResume() noexcept { + ::Microsoft::ReactNative::ReactInspectorThread::Instance().Post( + [weakInspectorHostTarget = std::weak_ptr(m_inspectorHostTarget)]() { + if (std::shared_ptr inspectorHostTarget = weakInspectorHostTarget.lock()) { + inspectorHostTarget->sendCommand(jsinspector_modern::HostCommand::DebuggerResume); + } + }); +} + //============================================================================================= // ReactViewHost implementation //============================================================================================= @@ -579,8 +716,8 @@ Mso::Future ReactViewHost::AttachViewInstance(IReactViewInstance &viewInst m_reactHost->AttachViewHost(*this); return InitViewInstanceInQueue(); - //// Schedule the viewInstance load in the action queue since there can be other load actions in the queue that need - //// to be consolidated. + // Schedule the viewInstance load in the action queue since there can be other load actions in the queue that need + // to be consolidated. // return m_actionQueue.Load()->PostAction(MakeInitViewInstanceAction()); }); } diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactHost.h b/vnext/Microsoft.ReactNative/ReactHost/ReactHost.h index 765039d9542..b786772b233 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactHost.h +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactHost.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include "AsyncActionQueue.h" @@ -15,6 +16,7 @@ namespace Mso::React { +class ReactInspectorHostTargetDelegate; class ReactViewHost; //! ReactHost manages lifetime of ReactNative instance. @@ -52,9 +54,6 @@ class ReactHost final : public Mso::ActiveObject { Mso::CntPtr ActionQueue() const noexcept; - Mso::Future LoadInQueue(ReactOptions &&options) noexcept; - Mso::Future UnloadInQueue(size_t unloadActionId) noexcept; - void Close() noexcept; bool IsClosed() const noexcept; @@ -64,6 +63,12 @@ class ReactHost final : public Mso::ActiveObject { template Mso::Future PostInQueue(TCallback &&callback) noexcept; + private: + enum class UnloadReason { + Unload, + CloseHost, + }; + private: friend MakePolicy; ReactHost(Mso::DispatchQueue const &queue) noexcept; @@ -75,9 +80,18 @@ class ReactHost final : public Mso::ActiveObject { void ForEachViewHost(const Mso::FunctorRef &action) noexcept; AsyncAction MakeLoadInstanceAction(ReactOptions &&options) noexcept; - AsyncAction MakeUnloadInstanceAction() noexcept; + AsyncAction MakeUnloadInstanceAction(UnloadReason reason) noexcept; + + Mso::Future LoadInQueue(ReactOptions &&options) noexcept; + Mso::Future UnloadInQueue(UnloadReason reason, size_t unloadActionId) noexcept; + + void OnDebuggerResume() noexcept; + bool IsInspectable() noexcept; + void AddInspectorPage() noexcept; + void RemoveInspectorPage() noexcept; private: + friend class ReactInspectorHostTargetDelegate; mutable std::mutex m_mutex; const Mso::InvokeElsePostExecutor m_executor{Queue()}; const Mso::ActiveReadableField> m_actionQueue{ @@ -92,6 +106,10 @@ class ReactHost final : public Mso::ActiveObject { size_t m_pendingUnloadActionId{0}; size_t m_nextUnloadActionId{0}; const Mso::ActiveField m_isInstanceUnloading{false, Queue()}; + + const std::shared_ptr m_inspectorHostTargetDelegate; + const std::shared_ptr m_inspectorHostTarget; + const Mso::ActiveField> m_inspectorPageId{Queue()}; }; //! Implements a cross-platform host for a React view diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp index 35a4874ae9b..2bb37e363d9 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp @@ -66,6 +66,7 @@ #include #include #include +#include "Inspector/ReactInspectorThread.h" #endif #if !defined(CORE_ABI) && !defined(USE_FABRIC) @@ -546,6 +547,8 @@ std::shared_ptr ReactInstanceWin::CreateDevSetting devSettings->useRuntimeScheduler = useRuntimeScheduler; + devSettings->inspectorHostTarget = m_options.InspectorHostTarget; + return devSettings; } @@ -656,16 +659,21 @@ void ReactInstanceWin::InitializeBridgeless() noexcept { }; if (devSettings->useDirectDebugger) { - ::Microsoft::ReactNative::GetSharedDevManager()->EnsureHermesInspector( - devSettings->sourceBundleHost, devSettings->sourceBundlePort); + ::Microsoft::ReactNative::GetSharedDevManager()->EnsureInspectorPackagerConnection( + devSettings->sourceBundleHost, devSettings->sourceBundlePort, devSettings->bundleAppId); } m_jsiRuntimeHolder = std::make_shared( devSettings, jsMessageThread, CreatePreparedScriptStore()); auto jsRuntime = std::make_unique(m_jsiRuntimeHolder); jsRuntime->getRuntime(); - m_bridgelessReactInstance = std::make_unique( - std::move(jsRuntime), jsMessageThread, timerManager, jsErrorHandlingFunc); + + m_bridgelessReactInstance = std::make_shared( + std::move(jsRuntime), + jsMessageThread, + timerManager, + jsErrorHandlingFunc, + m_options.InspectorHostTarget); auto bufferedRuntimeExecutor = m_bridgelessReactInstance->getBufferedRuntimeExecutor(); timerManager->setRuntimeExecutor(bufferedRuntimeExecutor); @@ -687,6 +695,7 @@ void ReactInstanceWin::InitializeBridgeless() noexcept { winrt::make(Mso::Copy(m_reactContext))); facebook::react::ReactInstance::JSRuntimeFlags options; + m_bridgelessReactInstance->initializeRuntime( options, [=, onCreated = m_options.OnInstanceCreated, reactContext = m_reactContext]( @@ -740,7 +749,6 @@ void ReactInstanceWin::InitializeBridgeless() noexcept { LoadJSBundlesBridgeless(devSettings); SetupHMRClient(); - } catch (std::exception &e) { OnErrorWithMessage(e.what()); OnErrorWithMessage("ReactInstanceWin: Failed to create React Instance."); @@ -1082,6 +1090,17 @@ Mso::Future ReactInstanceWin::Destroy() noexcept { if (m_bridgelessReactInstance) { if (auto jsMessageThread = m_jsMessageThread.Exchange(nullptr)) { jsMessageThread->runOnQueueSync([&]() noexcept { + // Unregister from inspector BEFORE shutting down JS thread + if (m_bridgelessReactInstance && m_options.InspectorHostTarget) { + Mso::React::MessageDispatchQueue messageDispatchQueue{ + ::Microsoft::ReactNative::ReactInspectorThread::Instance(), nullptr}; + messageDispatchQueue.runOnQueueSync( + [weakBridgelessReactInstance = std::weak_ptr(m_bridgelessReactInstance)]() { + if (auto bridgelessReactInstance = weakBridgelessReactInstance.lock()) { + bridgelessReactInstance->unregisterFromInspector(); + } + }); + } { // Release the JSI runtime std::scoped_lock lock{m_mutex}; diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h index 36c153df4e9..4d5125bc166 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h @@ -205,7 +205,7 @@ class ReactInstanceWin final : public Mso::ActiveObject #ifdef USE_FABRIC // Bridgeless - std::unique_ptr m_bridgelessReactInstance; + std::shared_ptr m_bridgelessReactInstance; #endif std::atomic m_state{ReactInstanceState::Loading}; diff --git a/vnext/Microsoft.ReactNative/ReactRootView.cpp b/vnext/Microsoft.ReactNative/ReactRootView.cpp index a697f76aa39..e04b0b813c2 100644 --- a/vnext/Microsoft.ReactNative/ReactRootView.cpp +++ b/vnext/Microsoft.ReactNative/ReactRootView.cpp @@ -5,14 +5,19 @@ #include "ReactRootView.g.cpp" #include +#include #include +#include +#include #include #include #include #include #include +#include "InstanceManager.h" #include "ReactNativeHost.h" #include "ReactViewInstance.h" +#include "Utils/KeyboardUtils.h" #include "XamlUtils.h" #include @@ -34,6 +39,7 @@ ReactRootView::ReactRootView() noexcept : m_uiQueue(Mso::DispatchQueue::GetCurre UpdatePerspective(); Loaded([this](auto &&, auto &&) { ::Microsoft::ReactNative::SetCompositor(::Microsoft::ReactNative::GetCompositor(*this)); + SetupDevToolsShortcut(); }); } @@ -45,6 +51,20 @@ void ReactRootView::ReactNativeHost(ReactNative::ReactNativeHost const &value) n if (m_reactNativeHost != value) { ReactViewHost(nullptr); m_reactNativeHost = value; + const auto weakThis = this->get_weak(); + ::Microsoft::ReactNative::DebuggerNotifications::SubscribeShowDebuggerPausedOverlay( + m_reactNativeHost.InstanceSettings().Notifications(), + m_reactNativeHost.InstanceSettings().UIDispatcher(), + [weakThis](std::string message, std::function onResume) { + if (auto strongThis = weakThis.get()) { + strongThis->ShowDebuggerPausedOverlay(message, onResume); + } + }, + [weakThis]() { + if (auto strongThis = weakThis.get()) { + strongThis->HideDebuggerPausedOverlay(); + } + }); ReloadView(); } } @@ -283,6 +303,65 @@ void ReactRootView::EnsureLoadingUI() noexcept { } } +void ReactRootView::HideDebuggerPausedOverlay() noexcept { + m_isDebuggerPausedOverlayOpen = false; + if (m_debuggerPausedFlyout) { + m_debuggerPausedFlyout.Hide(); + m_debuggerPausedFlyout = nullptr; + } +} + +void ReactRootView::ShowDebuggerPausedOverlay( + const std::string &message, + const std::function &onResume) noexcept { + // Initialize content + const xaml::Controls::Grid contentGrid; + xaml::Controls::ColumnDefinition messageColumnDefinition; + xaml::Controls::ColumnDefinition buttonColumnDefinition; + messageColumnDefinition.MinWidth(60); + buttonColumnDefinition.MinWidth(36); + contentGrid.ColumnDefinitions().Append(messageColumnDefinition); + contentGrid.ColumnDefinitions().Append(buttonColumnDefinition); + xaml::Controls::TextBlock messageBlock; + messageBlock.Text(winrt::to_hstring(message)); + messageBlock.FontWeight(winrt::Windows::UI::Text::FontWeights::SemiBold()); + xaml::Controls::FontIcon resumeGlyph; + resumeGlyph.FontFamily(xaml::Media::FontFamily(L"Segoe MDL2 Assets")); + resumeGlyph.Foreground(xaml::Media::SolidColorBrush(winrt::Colors::Green())); + resumeGlyph.Glyph(L"\uF5B0"); + resumeGlyph.HorizontalAlignment(xaml::HorizontalAlignment::Right); + resumeGlyph.PointerReleased([onResume](auto &&...) { onResume(); }); + xaml::Controls::Grid::SetColumn(resumeGlyph, 1); + contentGrid.Children().Append(messageBlock); + contentGrid.Children().Append(resumeGlyph); + + // Configure flyout + m_isDebuggerPausedOverlayOpen = true; + xaml::Style flyoutStyle( + {XAML_NAMESPACE_STR L".Controls.FlyoutPresenter", winrt::Windows::UI::Xaml::Interop::TypeKind::Metadata}); + flyoutStyle.Setters().Append(winrt::Setter( + xaml::Controls::Control::CornerRadiusProperty(), winrt::box_value(xaml::CornerRadius{12, 12, 12, 12}))); + flyoutStyle.Setters().Append(winrt::Setter( + xaml::Controls::Control::BackgroundProperty(), + winrt::box_value(xaml::Media::SolidColorBrush{FromArgb(255, 255, 255, 193)}))); + flyoutStyle.Setters().Append( + winrt::Setter(xaml::FrameworkElement::MarginProperty(), winrt::box_value(xaml::Thickness{0, 12, 0, 0}))); + m_debuggerPausedFlyout = xaml::Controls::Flyout{}; + m_debuggerPausedFlyout.FlyoutPresenterStyle(flyoutStyle); + m_debuggerPausedFlyout.LightDismissOverlayMode(xaml::Controls::LightDismissOverlayMode::On); + m_debuggerPausedFlyout.Content(contentGrid); + + // Disable light dismiss + m_debuggerPausedFlyout.Closing([weakThis = this->get_weak()](auto &&, const auto &args) { + if (auto strongThis = weakThis.get()) { + args.Cancel(strongThis->m_isDebuggerPausedOverlayOpen); + } + }); + + // Show flyout + m_debuggerPausedFlyout.ShowAt(*this); +} + void ReactRootView::ShowInstanceLoaded() noexcept { if (m_xamlRootView) { ClearLoadingUI(); @@ -481,4 +560,33 @@ void ReactRootView::RemoveChildAt(uint32_t index) { Children().RemoveAt(RNIndexToXamlIndex(index)); } +bool IsCtrlShiftI(winrt::Windows::System::VirtualKey key) noexcept { + return ( + key == winrt::Windows::System::VirtualKey::I && + ::Microsoft::ReactNative::IsModifiedKeyPressed( + winrt::CoreWindow::GetForCurrentThread(), winrt::Windows::System::VirtualKey::Shift) && + ::Microsoft::ReactNative::IsModifiedKeyPressed( + winrt::CoreWindow::GetForCurrentThread(), winrt::Windows::System::VirtualKey::Control)); +} + +void ReactRootView::SetupDevToolsShortcut() noexcept { + if (auto xamlRoot = XamlRoot()) { + if (std::find(m_subscribedDebuggerRoots.begin(), m_subscribedDebuggerRoots.end(), xamlRoot) == + m_subscribedDebuggerRoots.end()) { + if (auto rootContent = xamlRoot.Content()) { + m_subscribedDebuggerRoots.push_back(xamlRoot); + rootContent.KeyDown( + [weakThis = this->get_weak()](const auto & /*sender*/, const xaml::Input::KeyRoutedEventArgs &args) { + if (const auto strongThis = weakThis.get()) { + if (IsCtrlShiftI(args.Key())) { + ::Microsoft::ReactNative::GetSharedDevManager()->OpenDevTools( + winrt::to_string(strongThis->m_reactNativeHost.InstanceSettings().BundleAppId())); + } + }; + }); + } + } + } +} + } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/ReactRootView.h b/vnext/Microsoft.ReactNative/ReactRootView.h index f35a3c3fd60..876ac7e38cb 100644 --- a/vnext/Microsoft.ReactNative/ReactRootView.h +++ b/vnext/Microsoft.ReactNative/ReactRootView.h @@ -72,6 +72,7 @@ struct ReactRootView : ReactRootViewT, ::Microsoft::ReactNative:: bool m_isPerspectiveEnabled{true}; bool m_isInitialized{false}; bool m_isJSViewAttached{false}; + bool m_isDebuggerPausedOverlayOpen{false}; Mso::DispatchQueue m_uiQueue; int64_t m_rootTag{-1}; std::unique_ptr m_reactOptions; @@ -84,9 +85,11 @@ struct ReactRootView : ReactRootViewT, ::Microsoft::ReactNative:: std::shared_ptr<::Microsoft::ReactNative::PreviewKeyboardEventHandlerOnRoot> m_previewKeyboardEventHandlerOnRoot; xaml::Controls::ContentControl m_focusSafeHarbor{nullptr}; xaml::Controls::ContentControl::LosingFocus_revoker m_focusSafeHarborLosingFocusRevoker{}; + xaml::Controls::Flyout m_debuggerPausedFlyout{nullptr}; winrt::Grid m_greenBoxGrid{nullptr}; winrt::TextBlock m_waitingTextBlock{nullptr}; winrt::SystemNavigationManager::BackRequested_revoker m_backRequestedRevoker{}; + std::vector m_subscribedDebuggerRoots{}; // Visual tree to support safe harbor // this @@ -102,6 +105,8 @@ struct ReactRootView : ReactRootViewT, ::Microsoft::ReactNative:: void UpdateRootViewInternal() noexcept; void ClearLoadingUI() noexcept; void EnsureLoadingUI() noexcept; + void HideDebuggerPausedOverlay() noexcept; + void ShowDebuggerPausedOverlay(const std::string &message, const std::function &onResume) noexcept; void ShowInstanceLoaded() noexcept; void ShowInstanceError() noexcept; void ShowInstanceWaiting() noexcept; @@ -112,6 +117,7 @@ struct ReactRootView : ReactRootViewT, ::Microsoft::ReactNative:: bool OnBackRequested() noexcept; Mso::React::IReactViewHost *ReactViewHost() noexcept; void ReactViewHost(Mso::React::IReactViewHost *viewHost) noexcept; + void SetupDevToolsShortcut() noexcept; }; } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Views/DevMenu.cpp b/vnext/Microsoft.ReactNative/Views/DevMenu.cpp index 2d59627e885..38d1f17fcbd 100644 --- a/vnext/Microsoft.ReactNative/Views/DevMenu.cpp +++ b/vnext/Microsoft.ReactNative/Views/DevMenu.cpp @@ -6,7 +6,7 @@ #include "DevMenu.h" #include -#include "HermesSamplingProfiler.h" +#include "Hermes/HermesSamplingProfiler.h" #include "IReactDispatcher.h" #include "Modules/DevSettingsModule.h" diff --git a/vnext/PropertySheets/React.Cpp.props b/vnext/PropertySheets/React.Cpp.props index c7979cdaa5e..a726cec3201 100644 --- a/vnext/PropertySheets/React.Cpp.props +++ b/vnext/PropertySheets/React.Cpp.props @@ -61,7 +61,7 @@ - USE_HERMES;%(PreprocessorDefinitions) + USE_HERMES;REACT_NATIVE_DEBUGGER_ENABLED;%(PreprocessorDefinitions) ENABLE_DEVSERVER_HBCBUNDLES;%(PreprocessorDefinitions) USE_V8;%(PreprocessorDefinitions) USE_FABRIC;%(PreprocessorDefinitions) diff --git a/vnext/ReactCommon/ReactCommon.vcxproj b/vnext/ReactCommon/ReactCommon.vcxproj index f02b3127bdb..939d810f849 100644 --- a/vnext/ReactCommon/ReactCommon.vcxproj +++ b/vnext/ReactCommon/ReactCommon.vcxproj @@ -114,6 +114,13 @@ + + + + + + + @@ -145,6 +152,14 @@ + + + + + + + + @@ -186,6 +201,8 @@ + + Create diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/NativeToJsBridge.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/NativeToJsBridge.cpp index 93062866e3c..b7faf36aadd 100644 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/NativeToJsBridge.cpp +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/cxxreact/NativeToJsBridge.cpp @@ -344,7 +344,7 @@ NativeToJsBridge::getDecoratedNativeMethodCallInvoker( jsinspector_modern::RuntimeTargetDelegate& NativeToJsBridge::getInspectorTargetDelegate() { - return m_executor->getRuntimeTargetDelegate(); + return m_executor->getRuntimeTargetDelegate(); } } // namespace facebook::react diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jserrorhandler/JsErrorHandler.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jserrorhandler/JsErrorHandler.cpp deleted file mode 100644 index fc5ff6cfabc..00000000000 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jserrorhandler/JsErrorHandler.cpp +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#include "JsErrorHandler.h" -#include -#include -#include -#include -#include -#include "StackTraceParser.h" - -using namespace facebook; - -namespace { -std::string quote(const std::string& view) { - return "\"" + view + "\""; -} - -int nextExceptionId() { - static int exceptionId = 0; - return exceptionId++; -} - -bool isLooselyNull(const jsi::Value& value) { - return value.isNull() || value.isUndefined(); -} - -bool isEqualTo( - jsi::Runtime& runtime, - const jsi::Value& value, - const std::string& str) { - return jsi::Value::strictEquals( - runtime, value, jsi::String::createFromUtf8(runtime, str)); -} - -std::string stringifyToCpp(jsi::Runtime& runtime, const jsi::Value& value) { - return value.toString(runtime).utf8(runtime); -} - -bool isTruthy(jsi::Runtime& runtime, const jsi::Value& value) { - auto Boolean = runtime.global().getPropertyAsFunction(runtime, "Boolean"); - return Boolean.call(runtime, value).getBool(); -} - -void objectAssign( - jsi::Runtime& runtime, - jsi::Object& target, - const jsi::Object& value) { - auto Object = runtime.global().getPropertyAsObject(runtime, "Object"); - auto assign = Object.getPropertyAsFunction(runtime, "assign"); - assign.callWithThis(runtime, Object, target, value); -} - -jsi::Object wrapInErrorIfNecessary( - jsi::Runtime& runtime, - const jsi::Value& value) { - auto Error = runtime.global().getPropertyAsFunction(runtime, "Error"); - auto isError = - value.isObject() && value.asObject(runtime).instanceOf(runtime, Error); - auto error = isError - ? value.getObject(runtime) - : Error.callAsConstructor(runtime, value).getObject(runtime); - return error; -} - -class SetFalseOnDestruct { - std::shared_ptr _value; - - public: - SetFalseOnDestruct(const SetFalseOnDestruct&) = delete; - SetFalseOnDestruct& operator=(const SetFalseOnDestruct&) = delete; - SetFalseOnDestruct(SetFalseOnDestruct&&) = delete; - SetFalseOnDestruct& operator=(SetFalseOnDestruct&&) = delete; - explicit SetFalseOnDestruct(std::shared_ptr value) - : _value(std::move(value)) {} - ~SetFalseOnDestruct() { - *_value = false; - } -}; - -void logErrorWhileReporting( - std::string message, - jsi::JSError& error, - jsi::JSError& originalError) { - LOG(ERROR) << "JsErrorHandler::" << message << std::endl - << "Js error message: " << error.getMessage() << std::endl - << "Original js error message: " << originalError.getMessage() - << std::endl; -} - -jsi::Value getBundleMetadata(jsi::Runtime& runtime, jsi::JSError& error) { - auto jsGetBundleMetadataValue = - runtime.global().getProperty(runtime, "__getBundleMetadata"); - - if (!jsGetBundleMetadataValue.isObject() || - !jsGetBundleMetadataValue.asObject(runtime).isFunction(runtime)) { - return jsi::Value::null(); - } - - auto jsGetBundleMetadataValueFn = - jsGetBundleMetadataValue.asObject(runtime).asFunction(runtime); - - try { - auto bundleMetadataValue = jsGetBundleMetadataValueFn.call(runtime); - if (bundleMetadataValue.isObject()) { - return bundleMetadataValue; - } - return bundleMetadataValue; - } catch (jsi::JSError& ex) { - logErrorWhileReporting( - "getBundleMetadata(): Error raised while calling __getBundleMetadata(). Returning null.", - ex, - error); - } - - return jsi::Value::null(); -} -} // namespace - -namespace facebook::react { - -template <> -struct Bridging { - static jsi::Value toJs( - jsi::Runtime& runtime, - const JsErrorHandler::ProcessedError::StackFrame& frame) { - auto stackFrame = jsi::Object(runtime); - auto file = bridging::toJs(runtime, frame.file, nullptr); - auto lineNumber = bridging::toJs(runtime, frame.lineNumber, nullptr); - auto column = bridging::toJs(runtime, frame.column, nullptr); - - stackFrame.setProperty(runtime, "file", file); - stackFrame.setProperty(runtime, "methodName", frame.methodName); - stackFrame.setProperty(runtime, "lineNumber", lineNumber); - stackFrame.setProperty(runtime, "column", column); - return stackFrame; - } -}; - -template <> -struct Bridging { - static jsi::Value toJs( - jsi::Runtime& runtime, - const JsErrorHandler::ProcessedError& error) { - auto data = jsi::Object(runtime); - data.setProperty(runtime, "message", error.message); - data.setProperty( - runtime, - "originalMessage", - bridging::toJs(runtime, error.originalMessage, nullptr)); - data.setProperty( - runtime, "name", bridging::toJs(runtime, error.name, nullptr)); - data.setProperty( - runtime, - "componentStack", - bridging::toJs(runtime, error.componentStack, nullptr)); - - auto stack = jsi::Array(runtime, error.stack.size()); - for (size_t i = 0; i < error.stack.size(); i++) { - auto& frame = error.stack[i]; - stack.setValueAtIndex(runtime, i, bridging::toJs(runtime, frame)); - } - - data.setProperty(runtime, "stack", stack); - data.setProperty(runtime, "id", error.id); - data.setProperty(runtime, "isFatal", error.isFatal); - data.setProperty(runtime, "extraData", error.extraData); - return data; - } -}; - -std::ostream& operator<<( - std::ostream& os, - const JsErrorHandler::ProcessedError::StackFrame& frame) { - auto file = frame.file ? quote(*frame.file) : "nil"; - auto methodName = quote(frame.methodName); - auto lineNumber = - frame.lineNumber ? std::to_string(*frame.lineNumber) : "nil"; - auto column = frame.column ? std::to_string(*frame.column) : "nil"; - - os << "StackFrame { .file = " << file << ", .methodName = " << methodName - << ", .lineNumber = " << lineNumber << ", .column = " << column << " }"; - return os; -} -std::ostream& operator<<( - std::ostream& os, - const JsErrorHandler::ProcessedError& error) { - auto message = quote(error.message); - auto originalMessage = - error.originalMessage ? quote(*error.originalMessage) : "nil"; - auto name = error.name ? quote(*error.name) : "nil"; - auto componentStack = - error.componentStack ? quote(*error.componentStack) : "nil"; - auto id = std::to_string(error.id); - auto isFatal = std::to_string(static_cast(error.isFatal)); - auto extraData = "jsi::Object{ } "; - - os << "ProcessedError {\n" - << " .message = " << message << "\n" - << " .originalMessage = " << originalMessage << "\n" - << " .name = " << name << "\n" - << " .componentStack = " << componentStack << "\n" - << " .stack = [\n"; - - for (const auto& frame : error.stack) { - os << " " << frame << ", \n"; - } - os << " ]\n" - << " .id = " << id << "\n" - << " .isFatal " << isFatal << "\n" - << " .extraData = " << extraData << "\n" - << "}"; - return os; -} - -JsErrorHandler::JsErrorHandler(JsErrorHandler::OnJsError onJsError) - : _onJsError(std::move(onJsError)), - _inErrorHandler(std::make_shared(false)){ - - }; - -JsErrorHandler::~JsErrorHandler() {} - -void JsErrorHandler::handleError( - jsi::Runtime& runtime, - jsi::JSError& error, - bool isFatal, - bool logToConsole) { - // TODO: Current error parsing works and is stable. Can investigate using - // REGEX_HERMES to get additional Hermes data, though it requires JS setup - - if (!ReactNativeFeatureFlags::useAlwaysAvailableJSErrorHandling() && - _isRuntimeReady) { - try { - handleJSError(runtime, error, isFatal); - return; - } catch (jsi::JSError& ex) { - logErrorWhileReporting( - "handleError(): Error raised while reporting using js pipeline. Using c++ pipeline instead.", - ex, - error); - - // Re-try reporting using the c++ pipeline - _hasHandledFatalError = false; - } - } - - handleErrorWithCppPipeline(runtime, error, isFatal, logToConsole); -} - -void JsErrorHandler::handleErrorWithCppPipeline( - jsi::Runtime& runtime, - jsi::JSError& error, - bool isFatal, - bool logToConsole) { - *_inErrorHandler = true; - SetFalseOnDestruct temp{_inErrorHandler}; - - auto message = error.getMessage(); - auto errorObj = wrapInErrorIfNecessary(runtime, error.value()); - auto componentStackValue = errorObj.getProperty(runtime, "componentStack"); - if (!isLooselyNull(componentStackValue)) { - message += "\n" + stringifyToCpp(runtime, componentStackValue); - } - - auto nameValue = errorObj.getProperty(runtime, "name"); - auto name = (isLooselyNull(nameValue) || isEqualTo(runtime, nameValue, "")) - ? std::nullopt - : std::optional(stringifyToCpp(runtime, nameValue)); - - if (name && !message.starts_with(*name + ": ")) { - message = *name + ": " + message; - } - - auto jsEngineValue = errorObj.getProperty(runtime, "jsEngine"); - - if (!isLooselyNull(jsEngineValue)) { - message += ", js engine: " + stringifyToCpp(runtime, jsEngineValue); - } - - auto extraDataKey = jsi::PropNameID::forUtf8(runtime, "RN$ErrorExtraDataKey"); - auto extraDataValue = errorObj.getProperty(runtime, extraDataKey); - - auto extraData = jsi::Object(runtime); - if (extraDataValue.isObject()) { - objectAssign(runtime, extraData, extraDataValue.asObject(runtime)); - } - - auto isDEV = - isTruthy(runtime, runtime.global().getProperty(runtime, "__DEV__")); - - extraData.setProperty(runtime, "jsEngine", jsEngineValue); - extraData.setProperty(runtime, "rawStack", error.getStack()); - extraData.setProperty(runtime, "__DEV__", isDEV); - extraData.setProperty( - runtime, "bundleMetadata", getBundleMetadata(runtime, error)); - - auto cause = errorObj.getProperty(runtime, "cause"); - if (cause.isObject()) { - auto causeObj = cause.asObject(runtime); - // TODO: Consider just forwarding all properties. For now, just forward the - // stack properties to maintain symmetry with js pipeline - auto stackSymbols = causeObj.getProperty(runtime, "stackSymbols"); - extraData.setProperty(runtime, "stackSymbols", stackSymbols); - - auto stackReturnAddresses = - causeObj.getProperty(runtime, "stackReturnAddresses"); - extraData.setProperty( - runtime, "stackReturnAddresses", stackReturnAddresses); - - auto stackElements = causeObj.getProperty(runtime, "stackElements"); - extraData.setProperty(runtime, "stackElements", stackElements); - } - - auto originalMessage = message == error.getMessage() - ? std::nullopt - : std::optional(error.getMessage()); - - auto componentStack = !componentStackValue.isString() - ? std::nullopt - : std::optional(componentStackValue.asString(runtime).utf8(runtime)); - - auto isHermes = runtime.global().hasProperty(runtime, "HermesInternal"); - auto stackFrames = StackTraceParser::parse(isHermes, error.getStack()); - - auto id = nextExceptionId(); - - ProcessedError processedError = { - .message = - _isRuntimeReady ? message : ("[runtime not ready]: " + message), - .originalMessage = originalMessage, - .name = name, - .componentStack = componentStack, - .stack = stackFrames, - .id = id, - .isFatal = isFatal, - .extraData = std::move(extraData), - }; - - auto data = bridging::toJs(runtime, processedError).asObject(runtime); - - auto isComponentError = - isTruthy(runtime, errorObj.getProperty(runtime, "isComponentError")); - data.setProperty(runtime, "isComponentError", isComponentError); - - if (logToConsole && runtime.global().hasProperty(runtime, "console")) { // [Windows] Added hasProperty check - auto console = runtime.global().getPropertyAsObject(runtime, "console"); - auto errorFn = console.getPropertyAsFunction(runtime, "error"); - auto finalMessage = - jsi::String::createFromUtf8(runtime, processedError.message); - errorFn.callWithThis(runtime, console, finalMessage); - } - - std::shared_ptr shouldPreventDefault = std::make_shared(false); - auto preventDefault = jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "preventDefault"), - 0, - [shouldPreventDefault]( - jsi::Runtime& /*rt*/, - const jsi::Value& /*thisVal*/, - const jsi::Value* /*args*/, - size_t /*count*/) { - *shouldPreventDefault = true; - return jsi::Value::undefined(); - }); - - data.setProperty(runtime, "preventDefault", preventDefault); - - for (auto& errorListener : _errorListeners) { - try { - errorListener(runtime, jsi::Value(runtime, data)); - } catch (jsi::JSError& ex) { - logErrorWhileReporting( - "handleErrorWithCppPipeline(): Error raised inside an error listener. Executing next listener.", - ex, - error); - } - } - - if (*shouldPreventDefault) { - return; - } - - auto errorType = errorObj.getProperty(runtime, "type"); - auto isWarn = isEqualTo(runtime, errorType, "warn"); - - if (isFatal || !isWarn) { - if (isFatal) { - if (_hasHandledFatalError) { - return; - } - _hasHandledFatalError = true; - } - - _onJsError(runtime, processedError); - } -} - -void JsErrorHandler::registerErrorListener( - const std::function& errorListener) { - _errorListeners.push_back(errorListener); -} - -bool JsErrorHandler::hasHandledFatalError() { - return _hasHandledFatalError; -} - -void JsErrorHandler::setRuntimeReady() { - _isRuntimeReady = true; -} - -bool JsErrorHandler::isRuntimeReady() { - return _isRuntimeReady; -} - -void JsErrorHandler::notifyOfFatalError() { - _hasHandledFatalError = true; -} - -bool JsErrorHandler::inErrorHandler() { - return *_inErrorHandler; -} - -} // namespace facebook::react diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.cpp index cba0ba53f97..c5d902a5499 100644 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.cpp +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -32,7 +33,7 @@ static constexpr std::array kTextMIMETypePrefixes{ "application/javascript" // Not in Chromium but emitted by Metro }; -// namespace { [Windows #13587] +namespace { struct InitStreamResult { uint32_t httpStatusCode; @@ -46,6 +47,8 @@ using StreamInitCallback = using IOReadCallback = std::function)>; +} // namespace [Windows #13587] + /** * Private class owning state and implementing the listener for a particular * request @@ -291,8 +294,8 @@ bool NetworkIOAgent::handleRequest( // @cdp Network.getResponseBody support is experimental. if (req.method == "Network.getResponseBody") { - // TODO(T218468200) - return false; + handleGetResponseBody(req); + return true; } } @@ -479,4 +482,54 @@ void NetworkIOAgent::handleIoClose(const cdp::PreparsedRequest& req) { } } -} // namespace facebook::react::jsinspector_modern +void NetworkIOAgent::handleGetResponseBody(const cdp::PreparsedRequest& req) { + long long requestId = req.id; + if (!req.params.isObject()) { + frontendChannel_(cdp::jsonError( + requestId, + cdp::ErrorCode::InvalidParams, + "Invalid params: not an object.")); + return; + } + if ((req.params.count("requestId") == 0u) || + !req.params.at("requestId").isString()) { + frontendChannel_(cdp::jsonError( + requestId, + cdp::ErrorCode::InvalidParams, + "Invalid params: requestId is missing or not a string.")); + return; + } + + auto& networkReporter = NetworkReporter::getInstance(); + + if (!networkReporter.isDebuggingEnabled()) { + frontendChannel_(cdp::jsonError( + requestId, + cdp::ErrorCode::InvalidRequest, + "Invalid request: The \"Network\" domain is not enabled.")); + return; + } + + auto storedResponse = + networkReporter.getResponseBody(req.params.at("requestId").asString()); + + if (!storedResponse) { + frontendChannel_(cdp::jsonError( + requestId, + cdp::ErrorCode::InternalError, + "Internal error: Could not retrieve response body for the given requestId.")); + return; + } + + std::string responseBody; + bool base64Encoded = false; + std::tie(responseBody, base64Encoded) = *storedResponse; + + auto result = GetResponseBodyResult{ + .body = responseBody, + .base64Encoded = base64Encoded, + }; + + frontendChannel_(cdp::jsonResult(requestId, result.toDynamic())); +} +} // namespace facebook::react::jsinspector_modern \ No newline at end of file diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.h b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.h index bfda911715e..756c4911f4b 100644 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.h +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.h @@ -88,6 +88,17 @@ struct IOReadResult { } }; +struct GetResponseBodyResult { + std::string body; + bool base64Encoded; + folly::dynamic toDynamic() const { + folly::dynamic params = folly::dynamic::object; + params["body"] = body; + params["base64Encoded"] = base64Encoded; + return params; + } +}; + /** * Passed to `loadNetworkResource`, provides callbacks for processing incoming * data and other events. @@ -259,6 +270,11 @@ class NetworkIOAgent { * Reports CDP ok if the stream is found, or a CDP error if not. */ void handleIoClose(const cdp::PreparsedRequest& req); + + /** + * Handle a Network.getResponseBody CDP request. + */ + void handleGetResponseBody(const cdp::PreparsedRequest& req); }; -} // namespace facebook::react::jsinspector_modern +} // namespace facebook::react::jsinspector_modern \ No newline at end of file diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/network/HttpUtils.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/network/HttpUtils.cpp new file mode 100644 index 00000000000..8c49b2b8bbf --- /dev/null +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/network/HttpUtils.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "HttpUtils.h" + +#include + +namespace facebook::react::jsinspector_modern { + +std::string httpReasonPhrase(uint16_t status) { + switch (status) { + case 100: + return "Continue"; + case 101: + return "Switching Protocols"; + case 102: + return "Processing"; + case 103: + return "Early Hints"; + + case 200: + return "OK"; + case 201: + return "Created"; + case 202: + return "Accepted"; + case 203: + return "Non-Authoritative Information"; + case 204: + return "No Content"; + case 205: + return "Reset Content"; + case 206: + return "Partial Content"; + case 207: + return "Multi-Status"; + case 208: + return "Already Reported"; + case 226: + return "IM Used"; + + case 300: + return "Multiple Choices"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 303: + return "See Other"; + case 304: + return "Not Modified"; + case 305: + return "Use Proxy"; + case 307: + return "Temporary Redirect"; + case 308: + return "Permanent Redirect"; + + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + case 402: + return "Payment Required"; + case 403: + return "Forbidden"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + case 406: + return "Not Acceptable"; + case 407: + return "Proxy Authentication Required"; + case 408: + return "Request Timeout"; + case 409: + return "Conflict"; + case 410: + return "Gone"; + case 411: + return "Length Required"; + case 412: + return "Precondition Failed"; + case 413: + return "Payload Too Large"; + case 414: + return "URI Too Long"; + case 415: + return "Unsupported Media Type"; + case 416: + return "Range Not Satisfiable"; + case 417: + return "Expectation Failed"; + case 418: + return "I'm a teapot"; + case 421: + return "Misdirected Request"; + case 422: + return "Unprocessable Entity"; + case 423: + return "Locked"; + case 424: + return "Failed Dependency"; + case 425: + return "Too Early"; + case 426: + return "Upgrade Required"; + case 428: + return "Precondition Required"; + case 429: + return "Too Many Requests"; + case 431: + return "Request Header Fields Too Large"; + case 451: + return "Unavailable For Legal Reasons"; + + case 500: + return "Internal Server Error"; + case 501: + return "Not Implemented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Time-out"; + case 505: + return "HTTP Version Not Supported"; + case 506: + return "Variant Also Negotiates"; + case 507: + return "Insufficient Storage"; + case 508: + return "Loop Detected"; + case 510: + return "Not Extended"; + case 511: + return "Network Authentication Required"; + } + + return ""; +} + +std::string mimeTypeFromHeaders(const Headers& headers) { + std::string mimeType = "application/octet-stream"; + + for (const auto& [name, value] : headers) { + std::string lowerName = name; + std::transform( + lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char c) { // [Windows #13587] + return static_cast(::tolower(c)); // [Windows #13587] + }); + if (lowerName == "content-type") { + // Parse MIME type (discarding any parameters after ";") from the + // Content-Type header + // https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.1 + size_t pos = value.find(';'); + if (pos != std::string::npos) { + mimeType = value.substr(0, pos); + } else { + mimeType = value; + } + break; + } + } + + return mimeType; +} + +} // namespace facebook::react::jsinspector_modern diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp index a1bb6392a5f..87d1335e1fe 100644 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp @@ -19,10 +19,11 @@ namespace { // To capture samples timestamps Hermes is using steady_clock and returns // them in microseconds granularity since epoch. In the future we might want to // update Hermes to return timestamps in chrono type. -HighResTimeStamp getHighResTimeStampForSample(const RuntimeSamplingProfile::Sample &sample) { +HighResTimeStamp getHighResTimeStampForSample( + const RuntimeSamplingProfile::Sample& sample) { auto microsecondsSinceSteadyClockEpoch = sample.timestamp; - auto chronoTimePoint = - std::chrono::steady_clock::time_point(std::chrono::microseconds(microsecondsSinceSteadyClockEpoch)); + auto chronoTimePoint = std::chrono::steady_clock::time_point( + std::chrono::microseconds(microsecondsSinceSteadyClockEpoch)); return HighResTimeStamp::fromChronoSteadyClockTimePoint(chronoTimePoint); } @@ -36,25 +37,35 @@ constexpr std::string_view ROOT_FRAME_NAME = "(root)"; constexpr std::string_view IDLE_FRAME_NAME = "(idle)"; constexpr std::string_view PROGRAM_FRAME_NAME = "(program)"; -TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode(const ProfileTreeNode &node) { - const RuntimeSamplingProfile::SampleCallStackFrame &callFrame = node.getCallFrame(); - auto traceEventCallFrame = TraceEventProfileChunk::CPUProfile::Node::CallFrame{ - .codeType = node.getCodeType() == ProfileTreeNode::CodeType::JavaScript ? "JS" : "other", - .scriptId = callFrame.scriptId, - .functionName = std::string(callFrame.functionName), - .url = callFrame.scriptURL ? std::optional(std::string(*callFrame.scriptURL)) : std::nullopt, - .lineNumber = callFrame.lineNumber, - .columnNumber = callFrame.columnNumber, - }; +TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode( + const ProfileTreeNode& node) { + const RuntimeSamplingProfile::SampleCallStackFrame& callFrame = + node.getCallFrame(); + auto traceEventCallFrame = + TraceEventProfileChunk::CPUProfile::Node::CallFrame{ + .codeType = + node.getCodeType() == ProfileTreeNode::CodeType::JavaScript + ? "JS" + : "other", + .scriptId = callFrame.scriptId, + .functionName = std::string(callFrame.functionName), + .url = callFrame.scriptURL + ? std::optional(std::string(*callFrame.scriptURL)) + : std::nullopt, + .lineNumber = callFrame.lineNumber, + .columnNumber = callFrame.columnNumber, + }; return TraceEventProfileChunk::CPUProfile::Node{ .id = node.getId(), .callFrame = std::move(traceEventCallFrame), - .parentId = node.hasParent() ? std::optional(node.getParentId()) : std::nullopt, + .parentId = node.hasParent() ? std::optional(node.getParentId()) + : std::nullopt, }; } -RuntimeSamplingProfile::SampleCallStackFrame createArtificialCallFrame(std::string_view callFrameName) { +RuntimeSamplingProfile::SampleCallStackFrame createArtificialCallFrame( + std::string_view callFrameName) { return RuntimeSamplingProfile::SampleCallStackFrame{ .kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, .scriptId = FALLBACK_SCRIPT_ID, @@ -64,7 +75,8 @@ RuntimeSamplingProfile::SampleCallStackFrame createArtificialCallFrame(std::stri RuntimeSamplingProfile::SampleCallStackFrame createGarbageCollectorCallFrame() { return RuntimeSamplingProfile::SampleCallStackFrame{ - .kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector, + .kind = + RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector, .scriptId = FALLBACK_SCRIPT_ID, .functionName = GARBAGE_COLLECTOR_FRAME_NAME, }; @@ -73,12 +85,22 @@ RuntimeSamplingProfile::SampleCallStackFrame createGarbageCollectorCallFrame() { class ProfileTreeRootNode : public ProfileTreeNode { public: explicit ProfileTreeRootNode(uint32_t id) - : ProfileTreeNode(id, CodeType::Other, createArtificialCallFrame(ROOT_FRAME_NAME)) {} + : ProfileTreeNode( + id, + CodeType::Other, + createArtificialCallFrame(ROOT_FRAME_NAME)) {} }; struct ProfileChunk { - ProfileChunk(uint16_t chunkSize, ProcessId chunkProcessId, ThreadId chunkThreadId, HighResTimeStamp chunkTimestamp) - : size(chunkSize), processId(chunkProcessId), threadId(chunkThreadId), timestamp(chunkTimestamp) { + ProfileChunk( + uint16_t chunkSize, + ProcessId chunkProcessId, + ThreadId chunkThreadId, + HighResTimeStamp chunkTimestamp) + : size(chunkSize), + processId(chunkProcessId), + threadId(chunkThreadId), + timestamp(chunkTimestamp) { samples.reserve(size); timeDeltas.reserve(size); } @@ -106,26 +128,34 @@ void sendProfileTraceEvent( ThreadId threadId, RuntimeProfileId profileId, HighResTimeStamp profileStartTimestamp, - const std::function &dispatchCallback) { - auto traceEvent = - PerformanceTracer::constructRuntimeProfileTraceEvent(profileId, processId, threadId, profileStartTimestamp); - folly::dynamic serializedTraceEvent = TraceEventSerializer::serialize(std::move(traceEvent)); + const std::function& + dispatchCallback) { + auto traceEvent = PerformanceTracer::constructRuntimeProfileTraceEvent( + profileId, processId, threadId, profileStartTimestamp); + folly::dynamic serializedTraceEvent = + TraceEventSerializer::serialize(std::move(traceEvent)); dispatchCallback(folly::dynamic::array(std::move(serializedTraceEvent))); } // Add an empty sample to the chunk. -void chunkEmptySample(ProfileChunk &chunk, uint32_t idleNodeId, HighResDuration samplesTimeDelta) { +void chunkEmptySample( + ProfileChunk& chunk, + uint32_t idleNodeId, + HighResDuration samplesTimeDelta) { chunk.samples.push_back(idleNodeId); chunk.timeDeltas.push_back(samplesTimeDelta); } // Take the current local ProfileChunk, serialize it as "ProfileChunk" Trace // Event and buffer it. -void bufferProfileChunkTraceEvent(ProfileChunk &&chunk, RuntimeProfileId profileId, folly::dynamic &traceEventBuffer) { +void bufferProfileChunkTraceEvent( + ProfileChunk&& chunk, + RuntimeProfileId profileId, + folly::dynamic& traceEventBuffer) { std::vector traceEventNodes; traceEventNodes.reserve(chunk.nodes.size()); - for (const auto &node : chunk.nodes) { + for (const auto& node : chunk.nodes) { traceEventNodes.push_back(convertToTraceEventProfileNode(node)); } @@ -137,45 +167,50 @@ void bufferProfileChunkTraceEvent(ProfileChunk &&chunk, RuntimeProfileId profile TraceEventProfileChunk{ .cpuProfile = TraceEventProfileChunk::CPUProfile{ - .nodes = std::move(traceEventNodes), .samples = std::move(chunk.samples)}, + .nodes = std::move(traceEventNodes), + .samples = std::move(chunk.samples)}, .timeDeltas = std::move(chunk.timeDeltas), }); - auto serializedTraceEvent = TraceEventSerializer::serialize(std::move(traceEvent)); + auto serializedTraceEvent = + TraceEventSerializer::serialize(std::move(traceEvent)); traceEventBuffer.push_back(std::move(serializedTraceEvent)); } // Process a call stack of a single sample and add it to the chunk. void processCallStack( - std::vector &&callStack, - ProfileChunk &chunk, - ProfileTreeNode &rootNode, + std::vector&& callStack, + ProfileChunk& chunk, + ProfileTreeNode& rootNode, uint32_t idleNodeId, HighResDuration samplesTimeDelta, - IdGenerator &nodeIdGenerator) { + IdGenerator& nodeIdGenerator) { if (callStack.empty()) { chunkEmptySample(chunk, idleNodeId, samplesTimeDelta); return; } - ProfileTreeNode *previousNode = &rootNode; + ProfileTreeNode* previousNode = &rootNode; for (auto it = callStack.rbegin(); it != callStack.rend(); ++it) { - const RuntimeSamplingProfile::SampleCallStackFrame &callFrame = *it; - bool isGarbageCollectorFrame = - callFrame.kind == RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector; + const RuntimeSamplingProfile::SampleCallStackFrame& callFrame = *it; + bool isGarbageCollectorFrame = callFrame.kind == + RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector; - ProfileTreeNode::CodeType childCodeType = - isGarbageCollectorFrame ? ProfileTreeNode::CodeType::Other : ProfileTreeNode::CodeType::JavaScript; + ProfileTreeNode::CodeType childCodeType = isGarbageCollectorFrame + ? ProfileTreeNode::CodeType::Other + : ProfileTreeNode::CodeType::JavaScript; // We don't need real garbage collector call frame, we change it to // what Chrome DevTools expects. RuntimeSamplingProfile::SampleCallStackFrame childCallFrame = isGarbageCollectorFrame ? createGarbageCollectorCallFrame() : callFrame; - ProfileTreeNode *maybeExistingChild = previousNode->getIfAlreadyExists(childCodeType, childCallFrame); + ProfileTreeNode* maybeExistingChild = + previousNode->getIfAlreadyExists(childCodeType, childCallFrame); if (maybeExistingChild != nullptr) { previousNode = maybeExistingChild; } else { - previousNode = previousNode->addChild(nodeIdGenerator.getNext(), childCodeType, childCallFrame); + previousNode = previousNode->addChild( + nodeIdGenerator.getNext(), childCodeType, childCallFrame); chunk.nodes.push_back(*previousNode); } } @@ -186,21 +221,24 @@ void processCallStack( // Send buffered Trace Events and reset the buffer. void sendBufferedTraceEvents( - folly::dynamic &&traceEventBuffer, - const std::function &dispatchCallback) { + folly::dynamic&& traceEventBuffer, + const std::function& + dispatchCallback) { dispatchCallback(std::move(traceEventBuffer)); } } // namespace -/* static */ void RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( - std::vector &&profiles, - IdGenerator &profileIdGenerator, +/* static */ void +RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( + std::vector&& profiles, + IdGenerator& profileIdGenerator, HighResTimeStamp tracingStartTime, - const std::function &dispatchCallback, + const std::function& + dispatchCallback, uint16_t traceEventChunkSize, uint16_t profileChunkSize) { - for (auto &&profile : profiles) { + for (auto&& profile : profiles) { serializeAndDispatch( std::move(profile), profileIdGenerator, @@ -211,11 +249,13 @@ void sendBufferedTraceEvents( } } -/* static */ void RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( - RuntimeSamplingProfile &&profile, - IdGenerator &profileIdGenerator, +/* static */ void +RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( + RuntimeSamplingProfile&& profile, + IdGenerator& profileIdGenerator, HighResTimeStamp tracingStartTime, - const std::function &dispatchCallback, + const std::function& + dispatchCallback, uint16_t traceEventChunkSize, uint16_t profileChunkSize) { auto samples = std::move(profile.samples); @@ -229,28 +269,38 @@ void sendBufferedTraceEvents( ThreadId threadId = samples.front().threadId; HighResTimeStamp previousSampleTimestamp = tracingStartTime; HighResTimeStamp currentChunkTimestamp = tracingStartTime; - auto profileId = static_cast(profileIdGenerator.getNext());// [Windows #15256] + auto profileId = static_cast(profileIdGenerator.getNext()); - sendProfileTraceEvent(profile.processId, threadId, profileId, tracingStartTime, dispatchCallback); + sendProfileTraceEvent( + profile.processId, + threadId, + profileId, + tracingStartTime, + dispatchCallback); // There could be any number of new nodes in this chunk. Empty if all nodes // are already emitted in previous chunks. - ProfileChunk chunk{profileChunkSize, profile.processId, threadId, currentChunkTimestamp}; + ProfileChunk chunk{ + profileChunkSize, profile.processId, threadId, currentChunkTimestamp}; IdGenerator nodeIdGenerator{}; ProfileTreeRootNode rootNode(nodeIdGenerator.getNext()); chunk.nodes.push_back(rootNode); - ProfileTreeNode *programNode = rootNode.addChild( - nodeIdGenerator.getNext(), ProfileTreeNode::CodeType::Other, createArtificialCallFrame(PROGRAM_FRAME_NAME)); + ProfileTreeNode* programNode = rootNode.addChild( + nodeIdGenerator.getNext(), + ProfileTreeNode::CodeType::Other, + createArtificialCallFrame(PROGRAM_FRAME_NAME)); chunk.nodes.push_back(*programNode); - ProfileTreeNode *idleNode = rootNode.addChild( - nodeIdGenerator.getNext(), ProfileTreeNode::CodeType::Other, createArtificialCallFrame(IDLE_FRAME_NAME)); + ProfileTreeNode* idleNode = rootNode.addChild( + nodeIdGenerator.getNext(), + ProfileTreeNode::CodeType::Other, + createArtificialCallFrame(IDLE_FRAME_NAME)); chunk.nodes.push_back(*idleNode); uint32_t idleNodeId = idleNode->getId(); - for (auto &sample : samples) { + for (auto& sample : samples) { ThreadId currentSampleThreadId = sample.threadId; auto currentSampleTimestamp = getHighResTimeStampForSample(sample); @@ -259,8 +309,13 @@ void sendBufferedTraceEvents( // We should group samples by thread id once we support executing JavaScript // on different threads. if (currentSampleThreadId != chunk.threadId || chunk.isFull()) { - bufferProfileChunkTraceEvent(std::move(chunk), profileId, traceEventBuffer); - chunk = ProfileChunk{profileChunkSize, profile.processId, currentSampleThreadId, currentChunkTimestamp}; + bufferProfileChunkTraceEvent( + std::move(chunk), profileId, traceEventBuffer); + chunk = ProfileChunk{ + profileChunkSize, + profile.processId, + currentSampleThreadId, + currentChunkTimestamp}; } if (traceEventBuffer.size() == traceEventChunkSize) { @@ -290,4 +345,4 @@ void sendBufferedTraceEvents( } } -} // namespace facebook::react::jsinspector_modern::tracing \ No newline at end of file +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsitooling/react/runtime/JSRuntimeFactory.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsitooling/react/runtime/JSRuntimeFactory.cpp deleted file mode 100644 index c75d6d10787..00000000000 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsitooling/react/runtime/JSRuntimeFactory.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#include "JSRuntimeFactory.h" -#include -#include - -namespace facebook::react { - -jsi::Runtime& JSIRuntimeHolder::getRuntime() noexcept { - return *runtime_; -} - -JSIRuntimeHolder::JSIRuntimeHolder(std::unique_ptr runtime) - : runtime_(std::move(runtime)) { - assert(runtime_ != nullptr); -} - -void JSIRuntimeHolder::addConsoleMessage(jsi::Runtime& runtime, jsinspector_modern::ConsoleMessage message) { - return; -} - -bool JSIRuntimeHolder::supportsConsole() const{ - return false; -} - -std::unique_ptr -JSIRuntimeHolder::createAgentDelegate( - jsinspector_modern::FrontendChannel frontendChannel, - jsinspector_modern::SessionState& sessionState, - std::unique_ptr, - const jsinspector_modern::ExecutionContextDescription& - executionContextDescription, - RuntimeExecutor runtimeExecutor) { - (void)executionContextDescription; - (void)runtimeExecutor; - return std::make_unique( - std::move(frontendChannel), sessionState, runtime_->description()); -} - -} // namespace facebook::react \ No newline at end of file diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsitooling/react/runtime/JSRuntimeFactory.h b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsitooling/react/runtime/JSRuntimeFactory.h deleted file mode 100644 index 4f87670497c..00000000000 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/jsitooling/react/runtime/JSRuntimeFactory.h +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#pragma once - -#ifdef __cplusplus - -#include -#include -#include -#include -#include - - -namespace facebook::react { - -/** - * An interface that represents an instance of a JS VM - */ -class JSRuntime : public jsinspector_modern::RuntimeTargetDelegate { - public: - virtual jsi::Runtime& getRuntime() noexcept = 0; - - virtual ~JSRuntime() = default; - - /** - * Get a reference to the \c RuntimeTargetDelegate owned (or implemented) by - * this JSRuntime. This reference must remain valid for the duration of the - * JSRuntime's lifetime. - */ - // virtual jsinspector_modern::RuntimeTargetDelegate& getRuntimeTargetDelegate(); - - /** - * Run initialize work that must happen on the runtime's JS thread. Used for - * initializing TLS and registering profiling. - * - * TODO T194671568 Move the runtime constructor to the JsThread - */ - virtual void unstable_initializeOnJsThread() {} - - private: - /** - * Initialized by \c getRuntimeTargetDelegate if not overridden, and then - * never changes. - */ - std::optional - runtimeTargetDelegate_; -}; - -/** - * Interface for a class that creates instances of a JS VM - */ -class JSRuntimeFactory { - public: - virtual std::unique_ptr createJSRuntime( - std::shared_ptr msgQueueThread) noexcept = 0; - - virtual ~JSRuntimeFactory() = default; -}; - -/** - * Utility class for creating a JSRuntime from a uniquely owned jsi::Runtime. - */ -class JSIRuntimeHolder : public JSRuntime { - public: - jsi::Runtime& getRuntime() noexcept override; - void addConsoleMessage(jsi::Runtime& runtime, jsinspector_modern::ConsoleMessage message) override; - bool supportsConsole() const override; - - std::unique_ptr createAgentDelegate( - jsinspector_modern::FrontendChannel frontendChannel, - jsinspector_modern::SessionState& sessionState, - std::unique_ptr - previouslyExportedState, - const jsinspector_modern::ExecutionContextDescription& - executionContextDescription, - RuntimeExecutor runtimeExecutor) override; - - explicit JSIRuntimeHolder(std::unique_ptr runtime); - - private: - std::unique_ptr runtime_; -}; - -} // namespace facebook::react - -#endif // __cplusplus diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/nativemodule/dom/NativeDOM.h b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/nativemodule/dom/NativeDOM.h index 75b713b95b4..781f6128a3b 100644 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/nativemodule/dom/NativeDOM.h +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/nativemodule/dom/NativeDOM.h @@ -58,7 +58,9 @@ class NativeDOM : public NativeDOMCxxSpec { /* rightWidth: */ int, /* bottomWidth: */ int, /* leftWidth: */ int> - getBorderWidth(jsi::Runtime& rt, std::shared_ptr shadowNode); + getBorderWidth( + jsi::Runtime& rt, + std::shared_ptr shadowNode); std::tuple< /* x: */ double, @@ -75,15 +77,21 @@ class NativeDOM : public NativeDOMCxxSpec { std::shared_ptr shadowNode); std::tuple - getScrollPosition(jsi::Runtime& rt, std::shared_ptr shadowNode); + getScrollPosition( + jsi::Runtime& rt, + std::shared_ptr shadowNode); std::tuple getScrollSize( jsi::Runtime& rt, std::shared_ptr shadowNode); - std::string getTagName(jsi::Runtime& rt, std::shared_ptr shadowNode); + std::string getTagName( + jsi::Runtime& rt, + std::shared_ptr shadowNode); - std::string getTextContent(jsi::Runtime& rt, std::shared_ptr shadowNode); + std::string getTextContent( + jsi::Runtime& rt, + std::shared_ptr shadowNode); bool hasPointerCapture( jsi::Runtime& rt, diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/runtime/ReactInstance.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/runtime/ReactInstance.cpp deleted file mode 100644 index b9940f113d6..00000000000 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/runtime/ReactInstance.cpp +++ /dev/null @@ -1,676 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#include "ReactInstance.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace facebook::react { - -namespace { - -std::shared_ptr createRuntimeScheduler( - RuntimeExecutor runtimeExecutor, - RuntimeSchedulerTaskErrorHandler taskErrorHandler) { - std::shared_ptr scheduler = - std::make_shared( - std::move(runtimeExecutor), - HighResTimeStamp::now, - std::move(taskErrorHandler)); - scheduler->setPerformanceEntryReporter( - // FIXME: Move creation of PerformanceEntryReporter to here and - // guarantee that its lifetime is the same as the runtime. - PerformanceEntryReporter::getInstance().get()); - - return scheduler; -} - -} // namespace - -ReactInstance::ReactInstance( - std::unique_ptr runtime, - std::shared_ptr jsMessageQueueThread, - std::shared_ptr timerManager, - JsErrorHandler::OnJsError onJsError, - jsinspector_modern::HostTarget* parentInspectorTarget) - : runtime_(std::move(runtime)), - jsMessageQueueThread_(std::move(jsMessageQueueThread)), - timerManager_(std::move(timerManager)), - jsErrorHandler_(std::make_shared(std::move(onJsError))), - parentInspectorTarget_(parentInspectorTarget) { - RuntimeExecutor runtimeExecutor = - [weakRuntime = std::weak_ptr(runtime_), - weakTimerManager = std::weak_ptr(timerManager_), - weakJsThread = std::weak_ptr(jsMessageQueueThread_), - jsErrorHandler = jsErrorHandler_](auto callback) { - if (weakRuntime.expired()) { - return; - } - - if (auto jsThread = weakJsThread.lock()) { - jsThread->runOnQueue([jsErrorHandler, - weakRuntime, - weakTimerManager, - callback = std::move(callback)]() { - auto runtime = weakRuntime.lock(); - if (!runtime) { - return; - } - - jsi::Runtime& jsiRuntime = runtime->getRuntime(); - TraceSection s("ReactInstance::_runtimeExecutor[Callback]"); - try { - ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(true); - callback(jsiRuntime); - } catch (jsi::JSError& originalError) { - jsErrorHandler->handleError(jsiRuntime, originalError, true); - } catch (std::exception& ex) { - jsi::JSError error( - jsiRuntime, std::string("Non-js exception: ") + ex.what()); - jsErrorHandler->handleError(jsiRuntime, error, true); - } - }); - } - }; - - if (parentInspectorTarget_ != nullptr) { - auto executor = parentInspectorTarget_->executorFromThis(); - - auto bufferedRuntimeExecutorThatWaitsForInspectorSetup = - std::make_shared(runtimeExecutor); - auto runtimeExecutorThatExecutesAfterInspectorSetup = - [bufferedRuntimeExecutorThatWaitsForInspectorSetup]( - std::function&& callback) { - bufferedRuntimeExecutorThatWaitsForInspectorSetup->execute( - std::move(callback)); - }; - - runtimeScheduler_ = createRuntimeScheduler( - runtimeExecutorThatExecutesAfterInspectorSetup, - [jsErrorHandler = jsErrorHandler_]( - jsi::Runtime& runtime, jsi::JSError& error) { - jsErrorHandler->handleError(runtime, error, true); - }); - - auto runtimeExecutorThatGoesThroughRuntimeScheduler = - [runtimeScheduler = runtimeScheduler_.get()]( - std::function&& callback) { - runtimeScheduler->scheduleWork(std::move(callback)); - }; - - // This code can execute from any thread, so we need to make sure we set up - // the inspector logic in the right one. The callback executes immediately - // if we are already in the right thread. - executor([this, - runtimeExecutorThatGoesThroughRuntimeScheduler, - bufferedRuntimeExecutorThatWaitsForInspectorSetup]( - jsinspector_modern::HostTarget& hostTarget) { - // Callbacks scheduled through the page target executor are generally - // not guaranteed to run (e.g.: if the page target is destroyed) - // but in this case it is because the page target cannot be destroyed - // before the instance finishes its setup: - // * On iOS it's because we do the setup synchronously. - // * On Android it's because we explicitly wait for the instance - // creation task to finish before starting the destruction. - inspectorTarget_ = &hostTarget.registerInstance(*this); - runtimeInspectorTarget_ = - &inspectorTarget_->registerRuntime(*runtime_, runtimeExecutorThatGoesThroughRuntimeScheduler); // [Windows #13172] - bufferedRuntimeExecutorThatWaitsForInspectorSetup->flush(); - }); - } else { - runtimeScheduler_ = createRuntimeScheduler( - runtimeExecutor, - [jsErrorHandler = jsErrorHandler_]( - jsi::Runtime& runtime, jsi::JSError& error) { - jsErrorHandler->handleError(runtime, error, true); - }); - } - - bufferedRuntimeExecutor_ = std::make_shared( - [runtimeScheduler = runtimeScheduler_.get()]( - std::function&& callback) { - runtimeScheduler->scheduleWork(std::move(callback)); - }); -} - -void ReactInstance::unregisterFromInspector() { - if (inspectorTarget_ != nullptr) { - assert(runtimeInspectorTarget_); - inspectorTarget_->unregisterRuntime(*runtimeInspectorTarget_); - - assert(parentInspectorTarget_); - parentInspectorTarget_->unregisterInstance(*inspectorTarget_); - - inspectorTarget_ = nullptr; - } -} - -RuntimeExecutor ReactInstance::getUnbufferedRuntimeExecutor() noexcept { - return [runtimeScheduler = runtimeScheduler_.get()]( - std::function&& callback) { - runtimeScheduler->scheduleWork(std::move(callback)); - }; -} - -// This BufferedRuntimeExecutor ensures that the main JS bundle finished -// execution before any JS queued into it from C++ are executed. Use -// getUnbufferedRuntimeExecutor() instead if you do not need the main JS bundle -// to have finished. e.g. setting global variables into JS runtime. -RuntimeExecutor ReactInstance::getBufferedRuntimeExecutor() noexcept { - return [weakBufferedRuntimeExecutor_ = - std::weak_ptr(bufferedRuntimeExecutor_)]( - std::function&& callback) { - if (auto strongBufferedRuntimeExecutor_ = - weakBufferedRuntimeExecutor_.lock()) { - strongBufferedRuntimeExecutor_->execute(std::move(callback)); - } - }; -} - -// TODO(T184010230): Should the RuntimeScheduler returned from this method be -// buffered? -std::shared_ptr -ReactInstance::getRuntimeScheduler() noexcept { - return runtimeScheduler_; -} - -namespace { - -// Copied from JSIExecutor.cpp -// basename_r isn't in all iOS SDKs, so use this simple version instead. -std::string simpleBasename(const std::string& path) { - size_t pos = path.rfind('/'); - return (pos != std::string::npos) ? path.substr(pos) : path; -} - -} // namespace - -/** - * Load the JS bundle and flush buffered JS calls, future JS calls won't be - * buffered after calling this. - * Note that this method is asynchronous. However, a completion callback - * isn't needed because all calls into JS should be dispatched to the JSThread, - * preferably via the runtimeExecutor_. - */ -void ReactInstance::loadScript( - std::unique_ptr script, - const std::string& sourceURL, - std::function&& beforeLoad, - std::function&& afterLoad) { - auto buffer = std::make_shared(std::move(script)); - std::string scriptName = simpleBasename(sourceURL); - - runtimeScheduler_->scheduleWork([this, - scriptName, - sourceURL, - buffer = std::move(buffer), - weakBufferedRuntimeExecuter = - std::weak_ptr( - bufferedRuntimeExecutor_), - beforeLoad, - afterLoad](jsi::Runtime& runtime) { - if (beforeLoad) { - beforeLoad(runtime); - } - TraceSection s("ReactInstance::loadScript"); - bool hasLogger(ReactMarker::logTaggedMarkerBridgelessImpl != nullptr); - if (hasLogger) { - ReactMarker::logTaggedMarkerBridgeless( - ReactMarker::RUN_JS_BUNDLE_START, scriptName.c_str()); - } - - runtime.evaluateJavaScript(buffer, sourceURL); - - /** - * TODO(T183610671): We need a safe/reliable way to enable the js - * pipeline from javascript. Remove this after we figure that out, or - * after we just remove the js pipeline. - */ - if (!jsErrorHandler_->hasHandledFatalError()) { - jsErrorHandler_->setRuntimeReady(); - } - - if (hasLogger) { - ReactMarker::logTaggedMarkerBridgeless( - ReactMarker::RUN_JS_BUNDLE_STOP, scriptName.c_str()); - ReactMarker::logMarkerBridgeless(ReactMarker::INIT_REACT_RUNTIME_STOP); - ReactMarker::logMarkerBridgeless(ReactMarker::APP_STARTUP_STOP); - } - if (auto strongBufferedRuntimeExecuter = - weakBufferedRuntimeExecuter.lock()) { - strongBufferedRuntimeExecuter->flush(); - } - if (afterLoad) { - afterLoad(runtime); - } - }); -} - -/* - * Calls a method on a JS module that has been registered with - * `registerCallableModule`. Used to invoke a JS function from platform code. - */ -void ReactInstance::callFunctionOnModule( - const std::string& moduleName, - const std::string& methodName, - folly::dynamic&& args) { - if (bufferedRuntimeExecutor_ == nullptr) { - LOG(ERROR) - << "Calling callFunctionOnModule with null BufferedRuntimeExecutor"; - return; - } - - bufferedRuntimeExecutor_->execute([this, - moduleName = moduleName, - methodName = methodName, - args = std::move(args)]( - jsi::Runtime& runtime) { - TraceSection s( - "ReactInstance::callFunctionOnModule", - "moduleName", - moduleName, - "methodName", - methodName); - auto it = callableModules_.find(moduleName); - if (it == callableModules_.end()) { - std::ostringstream knownModules; - int i = 0; - for (it = callableModules_.begin(); it != callableModules_.end(); - it++, i++) { - const char* space = (i > 0 ? ", " : " "); - knownModules << space << it->first; - } - throw jsi::JSError( - runtime, - "Failed to call into JavaScript module method " + moduleName + "." + - methodName + - "(). Module has not been registered as callable. Registered callable JavaScript modules (n = " + - std::to_string(callableModules_.size()) + - "):" + knownModules.str() + - ". Did you forget to call `registerCallableModule`?"); - } - - if (std::holds_alternative(it->second)) { - auto module = - std::get(it->second).call(runtime).asObject(runtime); - it->second = std::move(module); - } - - auto& module = std::get(it->second); - auto method = module.getPropertyAsFunction(runtime, methodName.c_str()); - - std::vector jsArgs; - for (auto& arg : args) { - jsArgs.push_back(jsi::valueFromDynamic(runtime, arg)); - } - method.callWithThis( - runtime, module, (const jsi::Value*)jsArgs.data(), jsArgs.size()); - }); -} - -void ReactInstance::registerSegment( - uint32_t segmentId, - const std::string& segmentPath) { - LOG(WARNING) << "Starting to run ReactInstance::registerSegment with segment " - << segmentId; - runtimeScheduler_->scheduleWork([=](jsi::Runtime& runtime) { - TraceSection s("ReactInstance::registerSegment"); - auto tag = std::to_string(segmentId); - auto script = JSBigFileString::fromPath(segmentPath); - if (script->size() == 0) { - throw std::invalid_argument( - "Empty segment registered with ID " + tag + " from " + segmentPath); - } - auto buffer = std::make_shared(std::move(script)); - - bool hasLogger(ReactMarker::logTaggedMarkerBridgelessImpl != nullptr); - if (hasLogger) { - ReactMarker::logTaggedMarkerBridgeless( - ReactMarker::REGISTER_JS_SEGMENT_START, tag.c_str()); - } - LOG(WARNING) << "Starting to evaluate segment " << segmentId - << " in ReactInstance::registerSegment"; - runtime.evaluateJavaScript( - buffer, JSExecutor::getSyntheticBundlePath(segmentId, segmentPath)); - LOG(WARNING) << "Finished evaluating segment " << segmentId - << " in ReactInstance::registerSegment"; - if (hasLogger) { - ReactMarker::logTaggedMarkerBridgeless( - ReactMarker::REGISTER_JS_SEGMENT_STOP, tag.c_str()); - } - }); -} - -namespace { -void defineReactInstanceFlags( - jsi::Runtime& runtime, - const ReactInstance::JSRuntimeFlags& options) noexcept { - defineReadOnlyGlobal(runtime, "RN$Bridgeless", jsi::Value(true)); - - if (options.isProfiling) { - defineReadOnlyGlobal(runtime, "__RCTProfileIsProfiling", jsi::Value(true)); - } - - if (options.runtimeDiagnosticFlags.length() > 0) { - defineReadOnlyGlobal( - runtime, - "RN$DiagnosticFlags", - jsi::String::createFromUtf8(runtime, options.runtimeDiagnosticFlags)); - } -} - -bool isTruthy(jsi::Runtime& runtime, const jsi::Value& value) { - auto Boolean = runtime.global().getPropertyAsFunction(runtime, "Boolean"); - return Boolean.call(runtime, value).getBool(); -} - -} // namespace - -void ReactInstance::initializeRuntime( - JSRuntimeFlags options, - BindingsInstallFunc bindingsInstallFunc) noexcept { - runtimeScheduler_->scheduleWork([this, - options = std::move(options), - bindingsInstallFunc = - std::move(bindingsInstallFunc)]( - jsi::Runtime& runtime) { - TraceSection s("ReactInstance::initializeRuntime"); - - bindNativePerformanceNow(runtime); - - RuntimeSchedulerBinding::createAndInstallIfNeeded( - runtime, runtimeScheduler_); - - runtime_->unstable_initializeOnJsThread(); - - defineReactInstanceFlags(runtime, options); - - defineReadOnlyGlobal( - runtime, - "RN$useAlwaysAvailableJSErrorHandling", - jsi::Value( - ReactNativeFeatureFlags::useAlwaysAvailableJSErrorHandling())); - - defineReadOnlyGlobal( - runtime, - "RN$isRuntimeReady", - jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "isRuntimeReady"), - 0, - [jsErrorHandler = jsErrorHandler_]( - jsi::Runtime& /*runtime*/, - const jsi::Value& /*unused*/, - const jsi::Value* /*args*/, - size_t /*count*/) { - return jsErrorHandler->isRuntimeReady(); - })); - - defineReadOnlyGlobal( - runtime, - "RN$hasHandledFatalException", - jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "hasHandledFatalException"), - 0, - [jsErrorHandler = jsErrorHandler_]( - jsi::Runtime& /*runtime*/, - const jsi::Value& /*unused*/, - const jsi::Value* /*args*/, - size_t /*count*/) { - return jsErrorHandler->hasHandledFatalError(); - })); - - defineReadOnlyGlobal( - runtime, - "RN$notifyOfFatalException", - jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "notifyOfFatalException"), - 0, - [jsErrorHandler = jsErrorHandler_]( - jsi::Runtime& /*runtime*/, - const jsi::Value& /*unused*/, - const jsi::Value* /*args*/, - size_t /*count*/) { - jsErrorHandler->notifyOfFatalError(); - return jsi::Value::undefined(); - })); - - defineReadOnlyGlobal( - runtime, - "RN$inExceptionHandler", - jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "inExceptionHandler"), - 0, - [jsErrorHandler = jsErrorHandler_]( - jsi::Runtime& /*runtime*/, - const jsi::Value& /*unused*/, - const jsi::Value* /*args*/, - size_t /*count*/) { - return jsErrorHandler->inErrorHandler(); - })); - - // TODO(T196834299): We should really use a C++ turbomodule for this - defineReadOnlyGlobal( - runtime, - "RN$handleException", - jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "handleException"), - 3, - [jsErrorHandler = jsErrorHandler_]( - jsi::Runtime& runtime, - const jsi::Value& /*unused*/, - const jsi::Value* args, - size_t count) { - if (count < 2) { - throw jsi::JSError( - runtime, - "handleException requires 3 arguments: error, isFatal, logToConsole (optional)"); - } - - auto isFatal = isTruthy(runtime, args[1]); - - if (!ReactNativeFeatureFlags:: - useAlwaysAvailableJSErrorHandling()) { - if (jsErrorHandler->isRuntimeReady()) { - return jsi::Value(false); - } - } - - auto jsError = - jsi::JSError(runtime, jsi::Value(runtime, args[0])); - - if (count == 2) { - jsErrorHandler->handleError(runtime, jsError, isFatal); - } else { - auto logToConsole = isTruthy(runtime, args[2]); - jsErrorHandler->handleError( - runtime, jsError, isFatal, logToConsole); - } - - return jsi::Value(true); - })); - - defineReadOnlyGlobal( - runtime, - "RN$registerExceptionListener", - jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "registerExceptionListener"), - 1, - [errorListeners = std::vector>(), - jsErrorHandler = jsErrorHandler_]( - jsi::Runtime& runtime, - const jsi::Value& /*unused*/, - const jsi::Value* args, - size_t count) mutable { - if (count < 1) { - throw jsi::JSError( - runtime, - "registerExceptionListener: requires 1 argument: fn"); - } - - if (!args[0].isObject() || - !args[0].getObject(runtime).isFunction(runtime)) { - throw jsi::JSError( - runtime, - "registerExceptionListener: The first argument must be a function"); - } - - auto errorListener = std::make_shared( - args[0].getObject(runtime).getFunction(runtime)); - errorListeners.emplace_back(errorListener); - - jsErrorHandler->registerErrorListener( - [weakErrorListener = std::weak_ptr( - errorListener)](jsi::Runtime& runtime, jsi::Value data) { - if (auto strongErrorListener = weakErrorListener.lock()) { - strongErrorListener->call(runtime, data); - } - }); - - return jsi::Value::undefined(); - })); - - defineReadOnlyGlobal( - runtime, - "RN$registerCallableModule", - jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "registerCallableModule"), - 2, - [this]( - jsi::Runtime& runtime, - const jsi::Value& /*unused*/, - const jsi::Value* args, - size_t count) { - if (count != 2) { - throw jsi::JSError( - runtime, - "registerCallableModule requires exactly 2 arguments"); - } - if (!args[0].isString()) { - throw jsi::JSError( - runtime, - "The first argument to registerCallableModule must be a string (the name of the JS module)."); - } - auto name = args[0].asString(runtime).utf8(runtime); - if (!args[1].isObject() || - !args[1].getObject(runtime).isFunction(runtime)) { - throw jsi::JSError( - runtime, - "The second argument to registerCallableModule must be a function that returns the JS module."); - } - callableModules_.emplace( - std::move(name), - args[1].getObject(runtime).getFunction(runtime)); - return jsi::Value::undefined(); - })); - - timerManager_->attachGlobals(runtime); - - bindingsInstallFunc(runtime); - }); -} - -void ReactInstance::handleMemoryPressureJs(int pressureLevel) { - // The level is an enum value passed by the Android OS to an onTrimMemory - // event callback. Defined in ComponentCallbacks2. - enum AndroidMemoryPressure { - TRIM_MEMORY_BACKGROUND = 40, - TRIM_MEMORY_COMPLETE = 80, - TRIM_MEMORY_MODERATE = 60, - TRIM_MEMORY_RUNNING_CRITICAL = 15, - TRIM_MEMORY_RUNNING_LOW = 10, - TRIM_MEMORY_RUNNING_MODERATE = 5, - TRIM_MEMORY_UI_HIDDEN = 20, - }; - const char* levelName = nullptr; - switch (pressureLevel) { - case TRIM_MEMORY_BACKGROUND: - levelName = "TRIM_MEMORY_BACKGROUND"; - break; - case TRIM_MEMORY_COMPLETE: - levelName = "TRIM_MEMORY_COMPLETE"; - break; - case TRIM_MEMORY_MODERATE: - levelName = "TRIM_MEMORY_MODERATE"; - break; - case TRIM_MEMORY_RUNNING_CRITICAL: - levelName = "TRIM_MEMORY_RUNNING_CRITICAL"; - break; - case TRIM_MEMORY_RUNNING_LOW: - levelName = "TRIM_MEMORY_RUNNING_LOW"; - break; - case TRIM_MEMORY_RUNNING_MODERATE: - levelName = "TRIM_MEMORY_RUNNING_MODERATE"; - break; - case TRIM_MEMORY_UI_HIDDEN: - levelName = "TRIM_MEMORY_UI_HIDDEN"; - break; - default: - levelName = "UNKNOWN"; - break; - } - - switch (pressureLevel) { - case TRIM_MEMORY_RUNNING_LOW: - case TRIM_MEMORY_RUNNING_MODERATE: - case TRIM_MEMORY_UI_HIDDEN: - // For non-severe memory trims, do nothing. - LOG(INFO) << "Memory warning (pressure level: " << levelName - << ") received by JS VM, ignoring because it's non-severe"; - break; - case TRIM_MEMORY_BACKGROUND: - case TRIM_MEMORY_COMPLETE: - case TRIM_MEMORY_MODERATE: - case TRIM_MEMORY_RUNNING_CRITICAL: - // For now, pressureLevel is unused by collectGarbage. - // This may change in the future if the JS GC has different styles of - // collections. - LOG(INFO) << "Memory warning (pressure level: " << levelName - << ") received by JS VM, running a GC"; - runtimeScheduler_->scheduleWork([=](jsi::Runtime& runtime) { - TraceSection s("ReactInstance::handleMemoryPressure"); - runtime.instrumentation().collectGarbage(levelName); - }); - break; - default: - // Use the raw number instead of the name here since the name is - // meaningless. - LOG(WARNING) << "Memory warning (pressure level: " << pressureLevel - << ") received by JS VM, unrecognized pressure level"; - break; - } -} - -void* ReactInstance::getJavaScriptContext() { - return &runtime_->getRuntime(); -} - -} // namespace facebook::react diff --git a/vnext/Shared/DevServerHelper.h b/vnext/Shared/DevServerHelper.h index 384a7619810..9fc4cbcf4d9 100644 --- a/vnext/Shared/DevServerHelper.h +++ b/vnext/Shared/DevServerHelper.h @@ -76,12 +76,20 @@ class DevServerHelper { const std::string &packagerHost, const uint16_t packagerPort, const std::string &deviceName, - const std::string &packageName) { + const std::string &packageName, + const std::string &deviceId) { return string_format( InspectorDeviceUrlFormat, GetDeviceLocalHost(packagerHost, packagerPort).c_str(), deviceName.c_str(), - packageName.c_str()); + packageName.c_str(), + deviceId.c_str()); + } + + static std::string + get_OpenDebuggerUrl(const std::string &packagerHost, const uint16_t packagerPort, const std::string &deviceId) { + return string_format( + OpenDebuggerUrlFormat, GetDeviceLocalHost(packagerHost, packagerPort).c_str(), deviceId.c_str()); } static constexpr const char DefaultPackagerHost[] = "localhost"; @@ -105,11 +113,13 @@ 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 InspectorDeviceUrlFormat[] = "ws://%s/inspector/device?name=%s&app=%s&device=%s"; + static constexpr const char OpenDebuggerUrlFormat[] = "http://%s/open-debugger?device=%s"; static constexpr const char PackagerOkStatus[] = "packager-status:running"; const int LongPollFailureDelayMs = 5000; + // TODO: [vmoroz] avoid using vaiadic args for the format and move it to a utility class. template static std::string string_format(const char *format, Args... args) { size_t size = snprintf(nullptr, 0, format, args...) + 1; diff --git a/vnext/Shared/DevSettings.h b/vnext/Shared/DevSettings.h index 5a8bdd35f43..02f7d7dfdb1 100644 --- a/vnext/Shared/DevSettings.h +++ b/vnext/Shared/DevSettings.h @@ -23,6 +23,10 @@ struct RuntimeHolderLazyInit; namespace facebook { namespace react { +namespace jsinspector_modern { +class HostTarget; +} // namespace jsinspector_modern + enum class JSIEngineOverride : int32_t { Default = 0, // No JSI, will use the legacy ExecutorFactory Chakra = 1, // Use the JSIExecutorFactory with ChakraRuntime @@ -111,6 +115,9 @@ struct DevSettings { // Enable concurrent mode by installing runtimeScheduler bool useRuntimeScheduler{false}; + + // The HostTarget instance for Fusebox + facebook::react::jsinspector_modern::HostTarget *inspectorHostTarget; }; } // namespace react diff --git a/vnext/Shared/DevSupportManager.cpp b/vnext/Shared/DevSupportManager.cpp index a6935bb8aa9..70d3a2657ce 100644 --- a/vnext/Shared/DevSupportManager.cpp +++ b/vnext/Shared/DevSupportManager.cpp @@ -9,14 +9,19 @@ #include #include +#include "Inspector/ReactInspectorPackagerConnectionDelegate.h" #include "PackagerConnection.h" #include "Unicode.h" #include "Utilities.h" #include +#include #include +#include +#include #include +#include #include #include #include @@ -171,6 +176,49 @@ bool IsIgnorablePollHResult(HRESULT hr) { return hr == WININET_E_INVALID_SERVER_RESPONSE; } +std::string GetDeviceId(const std::string &packageName) { + const auto hash = winrt::Windows::Security::Cryptography::Core::HashAlgorithmProvider::OpenAlgorithm( + winrt::Windows::Security::Cryptography::Core::HashAlgorithmNames::Sha256()) + .CreateHash(); + hash.Append(winrt::Windows::System::Profile::SystemIdentification::GetSystemIdForPublisher().Id()); + winrt::Windows::Storage::Streams::InMemoryRandomAccessStream stream; + winrt::Windows::Storage::Streams::DataWriter writer; + // If an app ID is provided, we will allow reconnection to DevTools. + // Apps must supply a unique app ID to each ReactNativeHost instance settings for this to behave correctly. + if (!packageName.empty()) { + const auto packageNameBuffer = winrt::Windows::Security::Cryptography::CryptographicBuffer::ConvertStringToBinary( + winrt::to_hstring(packageName), winrt::Windows::Security::Cryptography::BinaryStringEncoding::Utf16BE); + hash.Append(packageNameBuffer); + } else { + const auto processId = GetCurrentProcessId(); + std::vector processIdBytes( + reinterpret_cast(&processId), reinterpret_cast(&processId + 1)); + winrt::array_view processIdByteArray(processIdBytes); + const auto processIdBuffer = + winrt::Windows::Security::Cryptography::CryptographicBuffer::CreateFromByteArray(processIdByteArray); + hash.Append(processIdBuffer); + } + const auto hashBuffer = hash.GetValueAndReset(); + const auto hashString = winrt::Windows::Security::Cryptography::CryptographicBuffer::EncodeToHexString(hashBuffer); + return winrt::to_string(hashString); +} + +std::string GetPackageName(const std::string &bundleAppId) { + if (!bundleAppId.empty()) { + return bundleAppId; + } + + std::string packageName{"RNW"}; + wchar_t fullName[PACKAGE_FULL_NAME_MAX_LENGTH]{}; + uint32_t size = ARRAYSIZE(fullName); + if (SUCCEEDED(GetCurrentPackageFullName(&size, fullName))) { + // we are in an unpackaged app + packageName = winrt::to_string(fullName); + } + + return packageName; +} + std::future PollForLiveReload(const std::string &url) { winrt::Windows::Web::Http::HttpClient httpClient; winrt::Windows::Foundation::Uri uri(Microsoft::Common::Unicode::Utf8ToUtf16(url)); @@ -238,37 +286,48 @@ void DevSupportManager::StopPollingLiveReload() { m_cancellation_token = true; } -void DevSupportManager::EnsureHermesInspector( +// TODO: (vmoroz) Use or delete this function +void DevSupportManager::OpenDevTools(const std::string &bundleAppId) { + winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter filter; + filter.CacheControl().ReadBehavior(winrt::Windows::Web::Http::Filters::HttpCacheReadBehavior::NoCache); + winrt::Windows::Web::Http::HttpClient httpClient(filter); + // TODO: Use currently configured dev server host + winrt::Windows::Foundation::Uri uri( + Microsoft::Common::Unicode::Utf8ToUtf16(facebook::react::DevServerHelper::get_OpenDebuggerUrl( + std::string{DevServerHelper::DefaultPackagerHost}, + DevServerHelper::DefaultPackagerPort, + GetDeviceId(GetPackageName(bundleAppId))))); + + winrt::Windows::Web::Http::HttpRequestMessage request(winrt::Windows::Web::Http::HttpMethod::Post(), uri); + httpClient.SendRequestAsync(request); +} + +void DevSupportManager::EnsureInspectorPackagerConnection( [[maybe_unused]] const std::string &packagerHost, - [[maybe_unused]] const uint16_t packagerPort) noexcept { + [[maybe_unused]] const uint16_t packagerPort, + [[maybe_unused]] const std::string &bundleAppId) noexcept { static std::once_flag once; - std::call_once(once, [this, &packagerHost, packagerPort]() { - // TODO: should we use the bundleAppId as the app param if available? - std::string packageName("RNW"); - wchar_t fullName[PACKAGE_FULL_NAME_MAX_LENGTH]{}; - UINT32 size = ARRAYSIZE(fullName); - if (SUCCEEDED(GetCurrentPackageFullName(&size, fullName))) { - // we are in an unpackaged app - packageName = winrt::to_string(fullName); - } - + std::call_once(once, [this, &packagerHost, packagerPort, &bundleAppId]() { + std::string packageName = GetPackageName(bundleAppId); 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(); + std::string deviceId = GetDeviceId(packageName); + std::string inspectorUrl = facebook::react::DevServerHelper::get_InspectorDeviceUrl( + packagerHost, packagerPort, deviceName, packageName, deviceId); + jsinspector_modern::InspectorFlags &inspectorFlags = jsinspector_modern::InspectorFlags::getInstance(); + m_inspectorPackagerConnection = std::make_unique( + inspectorUrl, + deviceName, + packageName, + std::make_unique()); + m_inspectorPackagerConnection->connect(); }); } -void DevSupportManager::UpdateBundleStatus(bool isLastDownloadSuccess, int64_t updateTimestamp) noexcept { - m_BundleStatusProvider->updateBundleStatus(isLastDownloadSuccess, updateTimestamp); -} - std::pair GetJavaScriptFromServer( const std::string &sourceBundleHost, const uint16_t sourceBundlePort, diff --git a/vnext/Shared/DevSupportManager.h b/vnext/Shared/DevSupportManager.h index 5c33ed1de02..a6dc1705af3 100644 --- a/vnext/Shared/DevSupportManager.h +++ b/vnext/Shared/DevSupportManager.h @@ -14,7 +14,7 @@ #include #include -#include +#include namespace facebook { namespace react { @@ -48,29 +48,17 @@ class DevSupportManager final : public facebook::react::IDevSupportManager { const uint16_t sourceBundlePort, std::function onChangeCallback) override; virtual void StopPollingLiveReload() override; + virtual void OpenDevTools(const std::string &bundleAppId) override; - virtual void EnsureHermesInspector(const std::string &packagerHost, const uint16_t packagerPort) noexcept override; - virtual void UpdateBundleStatus(bool isLastDownloadSuccess, int64_t updateTimestamp) noexcept override; + virtual void EnsureInspectorPackagerConnection( + const std::string &packagerHost, + const uint16_t packagerPort, + const std::string &bundleAppId) noexcept override; private: std::atomic_bool m_cancellation_token; - std::shared_ptr m_inspectorPackagerConnection; - - struct BundleStatusProvider : public InspectorPackagerConnection::IBundleStatusProvider { - virtual InspectorPackagerConnection::BundleStatus getBundleStatus() { - return m_bundleStatus; - } - - void updateBundleStatus(bool isLastDownloadSuccess, int64_t updateTimestamp) { - m_bundleStatus.m_isLastDownloadSuccess = isLastDownloadSuccess; - m_bundleStatus.m_updateTimestamp = updateTimestamp; - } - - private: - InspectorPackagerConnection::BundleStatus m_bundleStatus; - }; - std::shared_ptr m_BundleStatusProvider = std::make_shared(); + std::unique_ptr m_inspectorPackagerConnection; }; } // namespace Microsoft::ReactNative diff --git a/vnext/Shared/Hermes/HermesRuntimeAgentDelegate.cpp b/vnext/Shared/Hermes/HermesRuntimeAgentDelegate.cpp new file mode 100644 index 00000000000..6a1b20535bc --- /dev/null +++ b/vnext/Shared/Hermes/HermesRuntimeAgentDelegate.cpp @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file must match the code in React Native folder: +// ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.cpp +// Unlike the code in React Native sources, this class delegates calls to Hermes C-based API. + +#include "HermesRuntimeAgentDelegate.h" + +#include + +using namespace facebook::react::jsinspector_modern; + +namespace Microsoft::ReactNative { + +namespace { + +struct HermesStateWrapper : public RuntimeAgentDelegate::ExportedState { + explicit HermesStateWrapper(HermesUniqueCdpState &&hermesCdpState) : hermesCdpState_(std::move(hermesCdpState)) {} + + static HermesUniqueCdpState unwrapDestructively(ExportedState *wrapper) { + if (!wrapper) { + return {}; + } + if (auto *typedWrapper = dynamic_cast(wrapper)) { + return std::move(typedWrapper->hermesCdpState_); + } + return {}; + } + + private: + HermesUniqueCdpState hermesCdpState_; +}; + +} // namespace + +HermesRuntimeAgentDelegate::HermesRuntimeAgentDelegate( + FrontendChannel frontendChannel, + SessionState &sessionState, + std::unique_ptr previouslyExportedState, + const ExecutionContextDescription &executionContextDescription, + hermes_runtime runtime, + HermesRuntimeTargetDelegate &runtimeTargetDelegate, + facebook::react::RuntimeExecutor runtimeExecutor) + : hermesCdpAgent_(HermesInspectorApi::createCdpAgent( + runtimeTargetDelegate.getCdpDebugApi(), + executionContextDescription.id, + // Adapt std::function&& callback)> + // to hermes_enqueue_runtime_task_functor + AsFunctor( + [runtimeExecutor = std::move(runtimeExecutor), runtime](hermes_run_runtime_task_functor runtimeTask) { + // Adapt std::function to hermes_runtime_task_functor + runtimeExecutor( + [runtime, fn = std::make_shared>(runtimeTask)]( + facebook::jsi::Runtime &rt) { (*fn)(runtime); }); + }), + // Adapt void(const char *json_utf8, size_t json_size) to std::function + AsFunctor( + [frontendChannel = std::move(frontendChannel)](const char *json_utf8, size_t json_size) { + frontendChannel(std::string_view(json_utf8, json_size)); + }), + HermesStateWrapper::unwrapDestructively(previouslyExportedState.get()).release())) { + // Enable domains conditionally based on session state + // This matches the iOS/Android implementation pattern: + // Domains are enabled in response to Chrome DevTools sending Runtime.enable/Debugger.enable + if (sessionState.isRuntimeDomainEnabled) { + HermesInspectorApi::enableRuntimeDomain(hermesCdpAgent_.get()); + } + if (sessionState.isDebuggerDomainEnabled) { + HermesInspectorApi::enableDebuggerDomain(hermesCdpAgent_.get()); + } +} + +HermesRuntimeAgentDelegate::~HermesRuntimeAgentDelegate() = default; + +bool HermesRuntimeAgentDelegate::handleRequest(const cdp::PreparsedRequest &req) { + if (req.method.starts_with("Log.")) { + // Since we know Hermes doesn't do anything useful with Log messages, + // but our containing HostAgent will, bail out early. + return false; + } + + std::string json = req.toJson(); + HermesInspectorApi::handleCommand(hermesCdpAgent_.get(), json.c_str(), json.size()); + return true; +} + +std::unique_ptr HermesRuntimeAgentDelegate::getExportedState() { + return std::make_unique(HermesInspectorApi::getCdpState(hermesCdpAgent_.get())); +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/Hermes/HermesRuntimeAgentDelegate.h b/vnext/Shared/Hermes/HermesRuntimeAgentDelegate.h new file mode 100644 index 00000000000..149acc0c032 --- /dev/null +++ b/vnext/Shared/Hermes/HermesRuntimeAgentDelegate.h @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file must match the code in React Native folder: +// ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.h +// Unlike the code in React Native sources, this class delegates calls to Hermes C-based API. +// We use different namespace for this class comparing with the RN code. + +#pragma once + +#include "HermesRuntimeTargetDelegate.h" + +#include +#include +#include "HermesRuntimeHolder.h" + +namespace Microsoft::ReactNative { + +// A RuntimeAgentDelegate that handles requests from the Chrome DevTools +// Protocol for an instance of Hermes, using the modern CDPAgent API. +class HermesRuntimeAgentDelegate : public facebook::react::jsinspector_modern::RuntimeAgentDelegate { + public: + /** + * \param frontendChannel A channel used to send responses and events to the + * frontend. + * \param sessionState The state of the current CDP session. This will only + * be accessed on the main thread (during the constructor, in handleRequest, + * etc). + * \param previouslyExportedState The exported state from a previous instance + * of RuntimeAgentDelegate (NOT necessarily HermesRuntimeAgentDelegate). + * This may be nullptr, and if not nullptr it may be of any concrete type that + * implements RuntimeAgentDelegate::ExportedState. + * \param executionContextDescription A description of the execution context + * represented by this runtime. This is used for disambiguating the + * source/destination of CDP messages when there are multiple runtimes + * (concurrently or over the life of a Host). + * \param hermes_runtime The Hermes runtime that this agent is attached to. The caller + * is responsible for keeping this object alive for the duration of the + * \c HermesRuntimeAgentDelegate lifetime. + * \param runtimeTargetDelegate The \c HermesRuntimeTargetDelegate object + * object for the passed runtime. + * \param runtimeExecutor A callback for scheduling work on the JS thread. + * \c runtimeExecutor may drop scheduled work if the runtime is destroyed + * first. + */ + HermesRuntimeAgentDelegate( + facebook::react::jsinspector_modern::FrontendChannel frontendChannel, + facebook::react::jsinspector_modern::SessionState &sessionState, + std::unique_ptr previouslyExportedState, + const facebook::react::jsinspector_modern::ExecutionContextDescription &executionContextDescription, + hermes_runtime runtime, + HermesRuntimeTargetDelegate &runtimeTargetDelegate, + facebook::react::RuntimeExecutor runtimeExecutor); + + ~HermesRuntimeAgentDelegate() override; + + public: // RuntimeAgentDelegate implementation + /** + * Handle a CDP request. The response will be sent over the provided + * \c FrontendChannel synchronously or asynchronously. + * \param req The parsed request. + * \returns true if this agent has responded, or will respond asynchronously, + * to the request (with either a success or error message). False if the + * agent expects another agent to respond to the request instead. + */ + bool handleRequest(const facebook::react::jsinspector_modern::cdp::PreparsedRequest &req) override; + + std::unique_ptr getExportedState() override; + + private: + HermesUniqueCdpAgent hermesCdpAgent_; +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/Hermes/HermesRuntimeTargetDelegate.cpp b/vnext/Shared/Hermes/HermesRuntimeTargetDelegate.cpp new file mode 100644 index 00000000000..4e64a6b3e9f --- /dev/null +++ b/vnext/Shared/Hermes/HermesRuntimeTargetDelegate.cpp @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file must match the code in React Native folder: +// ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp +// Unlike the code in React Native sources, this class delegates calls to Hermes C-based API. +// We use different namespace for this class comparing with the RN code. + +#include "HermesRuntimeTargetDelegate.h" +#include +#include +#include "HermesRuntimeAgentDelegate.h" + +using namespace facebook::react::jsinspector_modern; + +namespace Microsoft::ReactNative { + +namespace { + +const uint16_t HERMES_SAMPLING_FREQUENCY_HZ = 10000; + +class HermesStackTraceWrapper : public StackTrace { + public: + explicit HermesStackTraceWrapper(HermesUniqueStackTrace &&hermesStackTrace) + : hermesStackTrace_{std::move(hermesStackTrace)} {} + + HermesUniqueStackTrace &operator*() { + return hermesStackTrace_; + } + + HermesUniqueStackTrace *operator->() { + return &hermesStackTrace_; + } + + private: + HermesUniqueStackTrace hermesStackTrace_; +}; + +} // namespace + +HermesRuntimeTargetDelegate::HermesRuntimeTargetDelegate(std::shared_ptr &&hermesRuntimeHolder) + : hermesRuntimeHolder_(std::move(hermesRuntimeHolder)), + hermesCdpDebugApi_(HermesInspectorApi::createCdpDebugApi(hermesRuntimeHolder_->getHermesRuntime())) {} + +HermesRuntimeTargetDelegate::~HermesRuntimeTargetDelegate() = default; + +hermes_cdp_debug_api HermesRuntimeTargetDelegate::getCdpDebugApi() { + return hermesCdpDebugApi_.get(); +} + +std::unique_ptr HermesRuntimeTargetDelegate::createAgentDelegate( + FrontendChannel frontendChannel, + SessionState &sessionState, + std::unique_ptr previouslyExportedState, + const ExecutionContextDescription &executionContextDescription, + facebook::react::RuntimeExecutor runtimeExecutor) { + return std::unique_ptr(new HermesRuntimeAgentDelegate( + frontendChannel, + sessionState, + std::move(previouslyExportedState), + executionContextDescription, + hermesRuntimeHolder_->getHermesRuntime(), + *this, + std::move(runtimeExecutor))); +} + +void HermesRuntimeTargetDelegate::addConsoleMessage(facebook::jsi::Runtime &runtime, ConsoleMessage message) { + // Convert ConsoleAPIType to hermes_console_api_type + hermes_console_api_type type{}; + switch (message.type) { + case ConsoleAPIType::kLog: + type = hermes_console_api_type_log; + break; + case ConsoleAPIType::kDebug: + type = hermes_console_api_type_debug; + break; + case ConsoleAPIType::kInfo: + type = hermes_console_api_type_info; + break; + case ConsoleAPIType::kError: + type = hermes_console_api_type_error; + break; + case ConsoleAPIType::kWarning: + type = hermes_console_api_type_warning; + break; + case ConsoleAPIType::kDir: + type = hermes_console_api_type_dir; + break; + case ConsoleAPIType::kDirXML: + type = hermes_console_api_type_dir_xml; + break; + case ConsoleAPIType::kTable: + type = hermes_console_api_type_table; + break; + case ConsoleAPIType::kTrace: + type = hermes_console_api_type_trace; + break; + case ConsoleAPIType::kStartGroup: + type = hermes_console_api_type_start_group; + break; + case ConsoleAPIType::kStartGroupCollapsed: + type = hermes_console_api_type_start_group_collapsed; + break; + case ConsoleAPIType::kEndGroup: + type = hermes_console_api_type_end_group; + break; + case ConsoleAPIType::kClear: + type = hermes_console_api_type_clear; + break; + case ConsoleAPIType::kAssert: + type = hermes_console_api_type_assert; + break; + case ConsoleAPIType::kTimeEnd: + type = hermes_console_api_type_time_end; + break; + case ConsoleAPIType::kCount: + type = hermes_console_api_type_count; + break; + default: + throw std::logic_error{"Unknown console message type"}; + } + + // Create a jsi::Array from the vector of jsi::Values + facebook::jsi::Array argsArray(runtime, message.args.size()); + for (size_t i = 0; i < message.args.size(); ++i) { + argsArray.setValueAtIndex(runtime, i, std::move(message.args[i])); + } + + // Store array as a temporary global property + // Using a property name that's unlikely to collide with user code + const char *propName = "__rnw_cdp_console_args"; + runtime.global().setProperty(runtime, propName, argsArray); + + // Convert stack trace to HermesUniqueStackTrace if available + HermesUniqueStackTrace hermesStackTrace{}; + if (auto hermesStackTraceWrapper = dynamic_cast(message.stackTrace.get())) { + hermesStackTrace = std::move(**hermesStackTraceWrapper); + } + + // Call C API with property name instead of serialized args + // The property will be cleaned up by the Hermes side + HermesInspectorApi::addConsoleMessage( + hermesCdpDebugApi_.get(), message.timestamp, type, propName, hermesStackTrace.get()); +} + +bool HermesRuntimeTargetDelegate::supportsConsole() const { + return true; +} + +std::unique_ptr HermesRuntimeTargetDelegate::captureStackTrace( + facebook::jsi::Runtime & /*runtime*/, + size_t /*framesToSkip*/) { + return std::make_unique( + HermesInspectorApi::captureStackTrace(hermesRuntimeHolder_->getHermesRuntime())); +} + +void HermesRuntimeTargetDelegate::enableSamplingProfiler() { + HermesInspectorApi::enableSamplingProfiler(hermesRuntimeHolder_->getHermesRuntime()); +} + +void HermesRuntimeTargetDelegate::disableSamplingProfiler() { + HermesInspectorApi::disableSamplingProfiler(hermesRuntimeHolder_->getHermesRuntime()); +} + +namespace { + +// Helper class to hold state while reading the sampling profile +struct SamplingProfileReaderState { + std::vector samples; + std::vector frames; + uint64_t timestamp; + uint64_t threadId; + bool hasCurrentSample; +}; + +static void NAPI_CDECL onInfo(void *cb_data, size_t sample_count) { + SamplingProfileReaderState *readerState = reinterpret_cast(cb_data); + readerState->samples.reserve(sample_count); +} + +// Callback invoked for each sample +static void NAPI_CDECL onSample(void *cb_data, uint64_t timestamp, uint64_t threadId, size_t frame_count) { + SamplingProfileReaderState *readerState = reinterpret_cast(cb_data); + if (readerState->hasCurrentSample) { + // Save the previous sample + readerState->samples.emplace_back(readerState->timestamp, readerState->threadId, std::move(readerState->frames)); + } + std::vector frames; + frames.reserve(frame_count); + readerState->frames = std::move(frames); + readerState->timestamp = timestamp; + readerState->threadId = threadId; + readerState->hasCurrentSample = true; +} + +// Callback invoked for each frame within a sample +static void NAPI_CDECL onFrame( + void *cb_data, + hermes_call_stack_frame_kind kind, + uint32_t scriptId, + const char *functionName, + size_t functionNameSize, + const char *scriptUrl, + size_t scriptUrlSize, + uint32_t lineNumber, + uint32_t columnNumber) { + SamplingProfileReaderState *readerState = reinterpret_cast(cb_data); + + using Kind = tracing::RuntimeSamplingProfile::SampleCallStackFrame::Kind; + + Kind frameKind; + switch (kind) { + case hermes_call_stack_frame_kind_js_function: + frameKind = Kind::JSFunction; + break; + case hermes_call_stack_frame_kind_native_function: + frameKind = Kind::NativeFunction; + break; + case hermes_call_stack_frame_kind_host_function: + frameKind = Kind::HostFunction; + break; + case hermes_call_stack_frame_kind_gc: + frameKind = Kind::GarbageCollector; + break; + default: + return; // Unknown frame kind, skip + } + + std::string_view funcName(functionName, functionNameSize); + std::optional url = + scriptUrl ? std::optional{std::string_view(scriptUrl, scriptUrlSize)} : std::nullopt; + std::optional line = lineNumber > 0 ? std::optional{lineNumber} : std::nullopt; + std::optional column = columnNumber > 0 ? std::optional{columnNumber} : std::nullopt; + + readerState->frames.emplace_back(frameKind, scriptId, funcName, url, line, column); +} + +class HermesRawRuntimeProfile : public tracing::RawRuntimeProfile { + public: + explicit HermesRawRuntimeProfile(HermesUniqueSamplingProfile hermesProfile) + : hermesProfile_{std::move(hermesProfile)} {} + + private: + HermesUniqueSamplingProfile hermesProfile_; +}; + +} // namespace + +tracing::RuntimeSamplingProfile HermesRuntimeTargetDelegate::collectSamplingProfile() { + // Create a readerState state to gather samples and frames + SamplingProfileReaderState readerState{}; + + // Collect the profile from Hermes + HermesUniqueSamplingProfile profile = HermesInspectorApi::collectSamplingProfile( + hermesRuntimeHolder_->getHermesRuntime(), &readerState, onInfo, onSample, onFrame); + + if (readerState.hasCurrentSample) { + // Save the last sample + readerState.samples.emplace_back(readerState.timestamp, readerState.threadId, std::move(readerState.frames)); + } + + // Return the complete profile with samples. Wrap the raw profile since it owns the strings. + return tracing::RuntimeSamplingProfile( + "Hermes", 1234, std::move(readerState.samples), std::make_unique(std::move(profile))); +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/Hermes/HermesRuntimeTargetDelegate.h b/vnext/Shared/Hermes/HermesRuntimeTargetDelegate.h new file mode 100644 index 00000000000..d27d9f279cc --- /dev/null +++ b/vnext/Shared/Hermes/HermesRuntimeTargetDelegate.h @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file must match the code in React Native folder: +// ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h +// Unlike the code in React Native sources, this class delegates calls to Hermes C-based API. +// We use different namespace for this class comparing with the RN code. + +#pragma once + +#include + +#include +#include + +#include +#include "HermesRuntimeHolder.h" + +namespace Microsoft::ReactNative { + +/** + * A RuntimeTargetDelegate that enables debugging a Hermes runtime over CDP. + */ +class HermesRuntimeTargetDelegate : public facebook::react::jsinspector_modern::RuntimeTargetDelegate { + public: + explicit HermesRuntimeTargetDelegate(std::shared_ptr &&hermesRuntimeHolder); + + ~HermesRuntimeTargetDelegate() override; + + // Our C-API specific helper method to be used internally instead of RN getCDPDebugAPI() private function. + hermes_cdp_debug_api getCdpDebugApi(); + + public: // RuntimeTargetDelegate implementation + std::unique_ptr createAgentDelegate( + facebook::react::jsinspector_modern::FrontendChannel frontendChannel, + facebook::react::jsinspector_modern::SessionState &sessionState, + std::unique_ptr previouslyExportedState, + const facebook::react::jsinspector_modern::ExecutionContextDescription &executionContextDescription, + facebook::react::RuntimeExecutor runtimeExecutor) override; + + void addConsoleMessage(facebook::jsi::Runtime &runtime, facebook::react::jsinspector_modern::ConsoleMessage message) + override; + + bool supportsConsole() const override; + + std::unique_ptr captureStackTrace( + facebook::jsi::Runtime &runtime, + size_t framesToSkip) override; + + /** + * Start sampling profiler. + */ + void enableSamplingProfiler() override; + + /** + * Stop sampling profiler. + */ + void disableSamplingProfiler() override; + + /** + * Return recorded sampling profile for the previous sampling session. + */ + facebook::react::jsinspector_modern::tracing::RuntimeSamplingProfile collectSamplingProfile() override; + + private: + std::shared_ptr hermesRuntimeHolder_; + const HermesUniqueCdpDebugApi hermesCdpDebugApi_; +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/HermesSamplingProfiler.cpp b/vnext/Shared/Hermes/HermesSamplingProfiler.cpp similarity index 100% rename from vnext/Shared/HermesSamplingProfiler.cpp rename to vnext/Shared/Hermes/HermesSamplingProfiler.cpp diff --git a/vnext/Shared/HermesSamplingProfiler.h b/vnext/Shared/Hermes/HermesSamplingProfiler.h similarity index 100% rename from vnext/Shared/HermesSamplingProfiler.h rename to vnext/Shared/Hermes/HermesSamplingProfiler.h diff --git a/vnext/Shared/HermesRuntimeHolder.cpp b/vnext/Shared/HermesRuntimeHolder.cpp index 5a2dd7285b2..8c0b45790f1 100644 --- a/vnext/Shared/HermesRuntimeHolder.cpp +++ b/vnext/Shared/HermesRuntimeHolder.cpp @@ -14,7 +14,9 @@ #include #include #include +#include #include +#include "Hermes/HermesRuntimeTargetDelegate.h" #include "SafeLoadLibrary.h" #define CRASH_ON_ERROR(result) VerifyElseCrash(result == napi_ok); @@ -27,6 +29,12 @@ using namespace Microsoft::NodeApiJsi; namespace Microsoft::ReactNative { +/*static*/ const hermes_inspector_vtable *HermesInspectorApi::vtable = nullptr; + +void setHermesInspectorVTable(const hermes_inspector_vtable *vtable) { + HermesInspectorApi::vtable = vtable; +} + React::ReactPropertyId>> HermesRuntimeHolderProperty() noexcept { static React::ReactPropertyId>> propId{ @@ -36,9 +44,6 @@ HermesRuntimeHolderProperty() noexcept { namespace { -int32_t NAPI_CDECL addInspectorPage(const char *title, const char *vm, void *connectFunc) noexcept; -void NAPI_CDECL removeInspectorPage(int32_t pageId) noexcept; - class HermesFuncResolver : public IFuncResolver { public: HermesFuncResolver() : libHandle_(LoadLibraryAsPeerFirst(L"hermes.dll")) {} @@ -55,7 +60,9 @@ HermesApi &initHermesApi() noexcept { static HermesFuncResolver funcResolver; static HermesApi s_hermesApi(&funcResolver); HermesApi::setCurrent(&s_hermesApi); - CRASH_ON_ERROR(s_hermesApi.hermes_set_inspector(&addInspectorPage, &removeInspectorPage)); + const hermes_inspector_vtable *inspectorVTable{}; + s_hermesApi.hermes_get_inspector_vtable(&inspectorVTable); + setHermesInspectorVTable(inspectorVTable); return s_hermesApi; } @@ -233,65 +240,6 @@ class HermesScriptCache { std::shared_ptr scriptStore_; }; -class HermesLocalConnection : public facebook::react::jsinspector_modern::ILocalConnection { - public: - HermesLocalConnection( - std::unique_ptr remoteConnection, - void *connectFunc) noexcept { - CRASH_ON_ERROR(getHermesApi().hermes_create_local_connection( - connectFunc, - reinterpret_cast(remoteConnection.release()), - &OnRemoteConnectionSendMessage, - &OnRemoteConnectionDisconnect, - &OnRemoteConnectionDelete, - nullptr, - &localConnection_)); - } - - ~HermesLocalConnection() override { - CRASH_ON_ERROR(getHermesApi().hermes_delete_local_connection(localConnection_)); - } - - void sendMessage(std::string message) { - CRASH_ON_ERROR(getHermesApi().hermes_local_connection_send_message(localConnection_, message.c_str())); - } - - void disconnect() { - CRASH_ON_ERROR(getHermesApi().hermes_local_connection_disconnect(localConnection_)); - } - - private: - static void NAPI_CDECL OnRemoteConnectionSendMessage(hermes_remote_connection remoteConnection, const char *message) { - reinterpret_cast(remoteConnection)->onMessage(message); - } - - static void NAPI_CDECL OnRemoteConnectionDisconnect(hermes_remote_connection remoteConnection) { - reinterpret_cast(remoteConnection)->onDisconnect(); - } - - static void NAPI_CDECL OnRemoteConnectionDelete(void *remoteConnection, void * /*deleterData*/) { - delete reinterpret_cast(remoteConnection); - } - - private: - hermes_local_connection localConnection_{}; -}; - -int32_t NAPI_CDECL addInspectorPage(const char *title, const char *vm, void *connectFunc) noexcept { - return facebook::react::jsinspector_modern::getInspectorInstance().addPage( - title, - vm, - [connectFunc, hermesApi = HermesApi::current()]( - std::unique_ptr remoteConnection) { - HermesApi::Scope apiScope(hermesApi); - return std::make_unique(std::move(remoteConnection), connectFunc); - }); -} - -void NAPI_CDECL removeInspectorPage(int32_t pageId) noexcept { - facebook::react::jsinspector_modern::getInspectorInstance().removePage(pageId); -} - } // namespace //============================================================================== @@ -307,9 +255,9 @@ HermesRuntimeHolder::HermesRuntimeHolder( m_preparedScriptStore(std::move(preparedScriptStore)) {} HermesRuntimeHolder::~HermesRuntimeHolder() { - if (m_runtime) { - CRASH_ON_ERROR(getHermesApi().jsr_delete_runtime(m_runtime)); - } + // if (m_runtime) { + // CRASH_ON_ERROR(getHermesApi().jsr_delete_runtime(m_runtime)); + // } } void HermesRuntimeHolder::initRuntime() noexcept { @@ -323,9 +271,6 @@ void HermesRuntimeHolder::initRuntime() noexcept { CRASH_ON_ERROR(api.jsr_create_config(&config)); CRASH_ON_ERROR(api.hermes_config_enable_default_crash_handler(config, devSettings->enableDefaultCrashHandler)); CRASH_ON_ERROR(api.jsr_config_enable_inspector(config, devSettings->useDirectDebugger)); - CRASH_ON_ERROR(api.jsr_config_set_inspector_runtime_name(config, devSettings->debuggerRuntimeName.c_str())); - CRASH_ON_ERROR(api.jsr_config_set_inspector_port(config, devSettings->debuggerPort)); - CRASH_ON_ERROR(api.jsr_config_set_inspector_break_on_start(config, devSettings->debuggerBreakOnNextLine)); CRASH_ON_ERROR(api.jsr_config_set_explicit_microtasks( config, facebook::react::ReactNativeFeatureFlags::enableBridgelessArchitecture())); @@ -338,6 +283,7 @@ void HermesRuntimeHolder::initRuntime() noexcept { jsr_runtime runtime{}; CRASH_ON_ERROR(api.jsr_create_runtime(config, &runtime)); CRASH_ON_ERROR(api.jsr_delete_config(config)); + m_runtime = runtime; napi_env env{}; CRASH_ON_ERROR(api.jsr_runtime_get_node_api_env(runtime, &env)); @@ -364,12 +310,13 @@ std::shared_ptr HermesRuntimeHolder::getRuntime() noexce return m_jsiRuntime; } -void HermesRuntimeHolder::crashHandler(int fileDescriptor) noexcept { - CRASH_ON_ERROR(getHermesApi().hermes_dump_crash_data(m_runtime, fileDescriptor)); +std::shared_ptr +HermesRuntimeHolder::createRuntimeTargetDelegate() { + return std::make_shared(shared_from_this()); } -void HermesRuntimeHolder::teardown() noexcept { - // TODO: (@vmoroz) Implement +void HermesRuntimeHolder::crashHandler(int fileDescriptor) noexcept { + CRASH_ON_ERROR(getHermesApi().hermes_dump_crash_data(m_runtime, fileDescriptor)); } std::shared_ptr HermesRuntimeHolder::loadFrom( @@ -403,6 +350,10 @@ void HermesRuntimeHolder::removeFromProfiling() const noexcept { CRASH_ON_ERROR(getHermesApi().hermes_sampling_profiler_dump_to_file(fileName.c_str())); } +hermes_runtime HermesRuntimeHolder::getHermesRuntime() noexcept { + return reinterpret_cast(m_runtime); +} + //============================================================================== // HermesJSRuntime implementation //============================================================================== @@ -414,47 +365,11 @@ facebook::jsi::Runtime &HermesJSRuntime::getRuntime() noexcept { return *m_holder->getRuntime(); } -void HermesJSRuntime::addConsoleMessage( - facebook::jsi::Runtime &runtime, - facebook::react::jsinspector_modern::ConsoleMessage message) { - return; -} - -bool HermesJSRuntime::supportsConsole() const { - return false; -} - -std::unique_ptr HermesJSRuntime::captureStackTrace( - facebook::jsi::Runtime &runtime, - size_t framesToSkip) { - return std::make_unique(); -} - -void HermesJSRuntime::enableSamplingProfiler() { - return; // [Windows TODO: stubbed implementation #14700] -} - -void HermesJSRuntime::disableSamplingProfiler() { - return; // [Windows TODO: stubbed implementation #14700] -} - -facebook::react::jsinspector_modern::tracing::RuntimeSamplingProfile HermesJSRuntime::collectSamplingProfile() { - return facebook::react::jsinspector_modern::tracing::RuntimeSamplingProfile( - "stubbed_impl", - 1234, - std::vector{}, - nullptr); // [Windows TODO: stubbed implementation #14700] -} - -std::unique_ptr HermesJSRuntime::createAgentDelegate( - facebook::react::jsinspector_modern::FrontendChannel frontendChannel, - facebook::react::jsinspector_modern::SessionState &sessionState, - std::unique_ptr previouslyExportedState, - const facebook::react::jsinspector_modern::ExecutionContextDescription &executionContextDescription, - facebook::react::RuntimeExecutor runtimeExecutor) { - (void)frontendChannel; - (void)sessionState; - return nullptr; +facebook::react::jsinspector_modern::RuntimeTargetDelegate &HermesJSRuntime::getRuntimeTargetDelegate() { + if (!m_runtimeTargetDelegate) { + m_runtimeTargetDelegate = m_holder->createRuntimeTargetDelegate(); + } + return *m_runtimeTargetDelegate; } } // namespace Microsoft::ReactNative diff --git a/vnext/Shared/HermesRuntimeHolder.h b/vnext/Shared/HermesRuntimeHolder.h index 4404cedbd65..d30f4b8ac23 100644 --- a/vnext/Shared/HermesRuntimeHolder.h +++ b/vnext/Shared/HermesRuntimeHolder.h @@ -8,16 +8,219 @@ #include #include #include -#include +#include namespace Microsoft::ReactNative { -class HermesRuntimeHolder : public Microsoft::JSI::RuntimeHolderLazyInit { +template +class FunctorAdapter { + static_assert(sizeof(TLambda) == -1, "Unsupported signature"); +}; + +template +class FunctorAdapter { + public: + static TResult NAPI_CDECL Invoke(void *data, TArgs... args) { + return reinterpret_cast(data)->operator()(args...); + } +}; + +template +inline TFunctor AsFunctor(TLambda &&lambda) { + using TLambdaType = std::remove_reference_t; + using TAdapter = + FunctorAdapter::invoke)>>; + return TFunctor{ + static_cast(new TLambdaType(std::forward(lambda))), &TAdapter::Invoke, [](void *data) { + delete static_cast(data); + }}; +} + +template +class FunctorWrapperBase { + static_assert(sizeof(TInvoke) == -1, "Unsupported signature"); +}; + +template +class FunctorWrapperBase { + public: + FunctorWrapperBase(TFunctor functor) : functor_(functor) {} + + ~FunctorWrapperBase() { + if (functor_.release != nullptr) { + functor_.release(functor_.data); + } + } + + TResult operator()(TArgs... args) { + return functor_.invoke(functor_.data, args...); + } + + private: + TFunctor functor_; +}; + +template +class FunctorWrapper : public FunctorWrapperBase().invoke)> { + public: + using FunctorWrapperBase().invoke)>::FunctorWrapperBase; +}; + +class HermesCdpDebugApiDeleter { + public: + void operator()(hermes_cdp_debug_api cdp_debug_api); +}; + +class HermesCdpAgentDeleter { + public: + void operator()(hermes_cdp_agent cdp_agent); +}; + +class HermesCdpStateDeleter { + public: + void operator()(hermes_cdp_state cdp_state); +}; + +class HermesStackTraceDeleter { + public: + void operator()(hermes_stack_trace stack_trace); +}; + +class HermesSamplingProfileDeleter { + public: + void operator()(hermes_sampling_profile sampling_profile); +}; + +using HermesUniqueCdpDebugApi = std::unique_ptr; +using HermesUniqueCdpAgent = std::unique_ptr; +using HermesUniqueCdpState = std::unique_ptr; +using HermesUniqueStackTrace = std::unique_ptr; +using HermesUniqueSamplingProfile = std::unique_ptr; + +class HermesInspectorApi { + public: + HermesInspectorApi() = delete; + + static void checkStatus(hermes_status status) { + if (status != hermes_status_ok) { + throw std::runtime_error("Hermes API call failed"); + } + } + + static HermesUniqueCdpDebugApi createCdpDebugApi(hermes_runtime runtime) { + hermes_cdp_debug_api cdp_debug_api{}; + checkStatus(vtable->create_cdp_debug_api(runtime, &cdp_debug_api)); + return HermesUniqueCdpDebugApi{cdp_debug_api}; + } + + static void addConsoleMessage( + hermes_cdp_debug_api cdpDebugApi, + double timestamp, + hermes_console_api_type type, + const char *argsPropertyName, + hermes_stack_trace stackTrace) { + checkStatus(vtable->add_console_message(cdpDebugApi, timestamp, type, argsPropertyName, stackTrace)); + } + + static HermesUniqueCdpAgent createCdpAgent( + hermes_cdp_debug_api cdpDebugApi, + int32_t execitionContextId, + hermes_enqueue_runtime_task_functor enqueueRuntimeTaskCallback, + hermes_enqueue_frontend_message_functor enqueueFrontendMessageCallback, + hermes_cdp_state cdp_state) { + hermes_cdp_agent cdp_agent{}; + checkStatus(vtable->create_cdp_agent( + cdpDebugApi, + execitionContextId, + enqueueRuntimeTaskCallback, + enqueueFrontendMessageCallback, + cdp_state, + &cdp_agent)); + return HermesUniqueCdpAgent{cdp_agent}; + } + + static HermesUniqueCdpState getCdpState(hermes_cdp_agent cdp_agent) { + hermes_cdp_state cdp_state{}; + checkStatus(vtable->cdp_agent_get_state(cdp_agent, &cdp_state)); + return HermesUniqueCdpState{cdp_state}; + } + + static void handleCommand(hermes_cdp_agent cdpAgent, const char *jsonUtf8, size_t jsonSize) { + checkStatus(vtable->cdp_agent_handle_command(cdpAgent, jsonUtf8, jsonSize)); + } + + static void enableRuntimeDomain(hermes_cdp_agent cdpAgent) { + checkStatus(vtable->cdp_agent_enable_runtime_domain(cdpAgent)); + } + + static void enableDebuggerDomain(hermes_cdp_agent cdpAgent) { + checkStatus(vtable->cdp_agent_enable_debugger_domain(cdpAgent)); + } + + static HermesUniqueStackTrace captureStackTrace(hermes_runtime runtime) { + hermes_stack_trace stack_trace{}; + checkStatus(vtable->capture_stack_trace(runtime, &stack_trace)); + return HermesUniqueStackTrace{stack_trace}; + } + + static void enableSamplingProfiler(hermes_runtime runtime) { + checkStatus(vtable->enable_sampling_profiler(runtime)); + } + + static void disableSamplingProfiler(hermes_runtime runtime) { + checkStatus(vtable->disable_sampling_profiler(runtime)); + } + + static HermesUniqueSamplingProfile collectSamplingProfile( + hermes_runtime runtime, + void *cb_data, + hermes_on_sampling_profile_info_callback on_info_callback, + hermes_on_sampling_profile_sample_callback on_sample_callback, + hermes_on_sampling_profile_frame_callback on_frame_callback) { + hermes_sampling_profile profile{}; + checkStatus(vtable->collect_sampling_profile( + runtime, cb_data, on_info_callback, on_sample_callback, on_frame_callback, &profile)); + return HermesUniqueSamplingProfile{profile}; + } + + private: + friend HermesCdpDebugApiDeleter; + friend HermesCdpAgentDeleter; + friend HermesCdpStateDeleter; + friend HermesStackTraceDeleter; + friend HermesSamplingProfileDeleter; + + friend void setHermesInspectorVTable(const hermes_inspector_vtable *vtable); + + static const hermes_inspector_vtable *vtable; +}; + +inline void HermesCdpDebugApiDeleter::operator()(hermes_cdp_debug_api cdp_debug_api) { + HermesInspectorApi::vtable->release_cdp_debug_api(cdp_debug_api); +} + +inline void HermesCdpAgentDeleter::operator()(hermes_cdp_agent cdp_agent) { + HermesInspectorApi::vtable->release_cdp_agent(cdp_agent); +} + +inline void HermesCdpStateDeleter::operator()(hermes_cdp_state cdp_state) { + HermesInspectorApi::vtable->release_cdp_state(cdp_state); +} + +inline void HermesStackTraceDeleter::operator()(hermes_stack_trace stack_trace) { + HermesInspectorApi::vtable->release_stack_trace(stack_trace); +} + +inline void HermesSamplingProfileDeleter::operator()(hermes_sampling_profile sampling_profile) { + HermesInspectorApi::vtable->release_sampling_profile(sampling_profile); +} + +class HermesRuntimeHolder : public Microsoft::JSI::RuntimeHolderLazyInit, + public std::enable_shared_from_this { public: // RuntimeHolderLazyInit implementation. std::shared_ptr getRuntime() noexcept override; facebook::react::JSIEngineOverride getRuntimeType() noexcept override; void crashHandler(int fileDescriptor) noexcept override; - void teardown() noexcept override; public: HermesRuntimeHolder( @@ -26,6 +229,8 @@ class HermesRuntimeHolder : public Microsoft::JSI::RuntimeHolderLazyInit { std::shared_ptr preparedScriptStore) noexcept; ~HermesRuntimeHolder(); + std::shared_ptr createRuntimeTargetDelegate() override; + static std::shared_ptr loadFrom( winrt::Microsoft::ReactNative::ReactPropertyBag const &propertyBag) noexcept; @@ -40,6 +245,8 @@ class HermesRuntimeHolder : public Microsoft::JSI::RuntimeHolderLazyInit { static void disableSamplingProfiler() noexcept; static void dumpSampledTraceToFile(const std::string &fileName) noexcept; + hermes_runtime getHermesRuntime() noexcept; + private: void initRuntime() noexcept; @@ -53,42 +260,17 @@ class HermesRuntimeHolder : public Microsoft::JSI::RuntimeHolderLazyInit { std::shared_ptr m_preparedScriptStore; }; -class HermesJSRuntime : public facebook::react::JSRuntime { +class HermesJSRuntime final : public facebook::react::JSRuntime { public: HermesJSRuntime(std::shared_ptr hermesRuntimeHolder); facebook::jsi::Runtime &getRuntime() noexcept override; - void addConsoleMessage(facebook::jsi::Runtime &runtime, facebook::react::jsinspector_modern::ConsoleMessage message) - override; - bool supportsConsole() const override; - std::unique_ptr captureStackTrace( - facebook::jsi::Runtime &runtime, - size_t framesToSkip = 0) override; - - /** - * Start sampling profiler. - */ - void enableSamplingProfiler() override; - - /** - * Stop sampling profiler. - */ - void disableSamplingProfiler() override; - - /** - * Return recorded sampling profile for the previous sampling session. - */ - facebook::react::jsinspector_modern::tracing::RuntimeSamplingProfile collectSamplingProfile() override; - - std::unique_ptr createAgentDelegate( - facebook::react::jsinspector_modern::FrontendChannel frontendChannel, - facebook::react::jsinspector_modern::SessionState &sessionState, - std::unique_ptr previouslyExportedState, - const facebook::react::jsinspector_modern::ExecutionContextDescription &executionContextDescription, - facebook::react::RuntimeExecutor runtimeExecutor) override; + + facebook::react::jsinspector_modern::RuntimeTargetDelegate &getRuntimeTargetDelegate() override; private: std::shared_ptr m_holder; + std::shared_ptr m_runtimeTargetDelegate; }; } // namespace Microsoft::ReactNative diff --git a/vnext/Shared/IDevSupportManager.h b/vnext/Shared/IDevSupportManager.h index dd90ff1bfc7..03c60ced0f5 100644 --- a/vnext/Shared/IDevSupportManager.h +++ b/vnext/Shared/IDevSupportManager.h @@ -23,9 +23,12 @@ struct IDevSupportManager { const uint16_t sourceBundlePort, std::function onChangeCallback) = 0; virtual void StopPollingLiveReload() = 0; + virtual void OpenDevTools(const std::string &bundleAppId) = 0; - virtual void EnsureHermesInspector(const std::string &packagerHost, const uint16_t packagerPort) noexcept = 0; - virtual void UpdateBundleStatus(bool isLastDownloadSuccess, int64_t updateTimestamp) noexcept = 0; + virtual void EnsureInspectorPackagerConnection( + const std::string &packagerHost, + const uint16_t packagerPort, + const std::string &bundleAppId) noexcept = 0; }; std::shared_ptr CreateDevSupportManager(); diff --git a/vnext/Shared/Inspector/ReactInspectorPackagerConnectionDelegate.cpp b/vnext/Shared/Inspector/ReactInspectorPackagerConnectionDelegate.cpp new file mode 100644 index 00000000000..65b7d6ddd0a --- /dev/null +++ b/vnext/Shared/Inspector/ReactInspectorPackagerConnectionDelegate.cpp @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "ReactInspectorPackagerConnectionDelegate.h" + +#include +#include +#include +#include "ReactInspectorThread.h" + +namespace Microsoft::ReactNative { + +namespace { + +class ReactInspectorWebSocket : public facebook::react::jsinspector_modern::IWebSocket { + public: + ReactInspectorWebSocket( + std::string const &url, + std::weak_ptr delegate); + void send(std::string_view message) override; + ~ReactInspectorWebSocket() override; + + private: + std::shared_ptr m_packagerWebSocketConnection; + std::weak_ptr m_weakDelegate; + std::atomic m_didConnect{false}; +}; + +ReactInspectorWebSocket::ReactInspectorWebSocket( + std::string const &url, + std::weak_ptr delegate) + : m_weakDelegate{delegate} { + std::vector certExceptions; + + m_packagerWebSocketConnection = + std::make_shared(std::move(certExceptions)); + + m_packagerWebSocketConnection->SetOnConnect([this, delegate]() { + ReactInspectorThread::Instance().InvokeElsePost([this, delegate]() { + m_didConnect = true; + if (const auto strongDelegate = delegate.lock()) { + strongDelegate->didOpen(); + } + }); + }); + m_packagerWebSocketConnection->SetOnMessage([delegate](auto &&, const std::string &message, bool isBinary) { + ReactInspectorThread::Instance().InvokeElsePost([delegate, message]() { + if (const auto strongDelegate = delegate.lock()) { + strongDelegate->didReceiveMessage(message); + } + }); + }); + m_packagerWebSocketConnection->SetOnError( + [delegate](const Microsoft::React::Networking::IWebSocketResource::Error &error) { + ReactInspectorThread::Instance().InvokeElsePost([delegate, error]() { + if (const auto strongDelegate = delegate.lock()) { + strongDelegate->didFailWithError(std::nullopt, error.Message); + } + }); + }); + m_packagerWebSocketConnection->SetOnClose([this, delegate](auto &&...) { + ReactInspectorThread::Instance().InvokeElsePost([this, delegate]() { + // Only call didClose() if we successfully connected first + // This prevents didClose() from being called during failed connection attempts + if (m_didConnect) { + if (const auto strongDelegate = delegate.lock()) { + strongDelegate->didClose(); + } + } + }); + }); + + Microsoft::React::Networking::IWebSocketResource::Protocols protocols; + Microsoft::React::Networking::IWebSocketResource::Options options; + m_packagerWebSocketConnection->Connect(std::string{url}, protocols, options); +} + +void ReactInspectorWebSocket::send(std::string_view message) { + m_packagerWebSocketConnection->Send(std::string{message}); +} + +ReactInspectorWebSocket::~ReactInspectorWebSocket() { + // Don't close WebSocket during shutdown - the OS will clean it up + // Attempting to close async during DLL unload causes thread pool failures + // The connection will be terminated when the process exits anyway +} + +} // namespace + +std::unique_ptr +ReactInspectorPackagerConnectionDelegate::connectWebSocket( + const std::string &url, + std::weak_ptr delegate) { + return std::make_unique(url, delegate); +} + +winrt::fire_and_forget RunWithDelayAsync(std::function callback, std::chrono::milliseconds delayMs) { + co_await winrt::resume_after(delayMs); + ReactInspectorThread::Instance().InvokeElsePost([callback]() { callback(); }); +} + +void ReactInspectorPackagerConnectionDelegate::scheduleCallback( + std::function callback, + std::chrono::milliseconds delayMs) { + RunWithDelayAsync(callback, delayMs); +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/Inspector/ReactInspectorPackagerConnectionDelegate.h b/vnext/Shared/Inspector/ReactInspectorPackagerConnectionDelegate.h new file mode 100644 index 00000000000..b7c0e16d9d5 --- /dev/null +++ b/vnext/Shared/Inspector/ReactInspectorPackagerConnectionDelegate.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include + +namespace Microsoft::ReactNative { + +class ReactInspectorPackagerConnectionDelegate final + : public facebook::react::jsinspector_modern::InspectorPackagerConnectionDelegate { + public: // InspectorPackagerConnectionDelegate implementation + std::unique_ptr connectWebSocket( + const std::string &url, + std::weak_ptr delegate) override; + + void scheduleCallback(std::function callback, std::chrono::milliseconds delayMs) override; +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/Inspector/ReactInspectorThread.h b/vnext/Shared/Inspector/ReactInspectorThread.h new file mode 100644 index 00000000000..67eec4082de --- /dev/null +++ b/vnext/Shared/Inspector/ReactInspectorThread.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace Microsoft::ReactNative { + +class ReactInspectorThread { + public: + static Mso::DispatchQueue &Instance() { + static Mso::DispatchQueue queue = Mso::DispatchQueue::MakeSerialQueue(); + return queue; + } +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/InspectorPackagerConnection.cpp b/vnext/Shared/InspectorPackagerConnection.cpp deleted file mode 100644 index 917382a5f3a..00000000000 --- a/vnext/Shared/InspectorPackagerConnection.cpp +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#include "pch.h" - -#include -#include -#include "InspectorPackagerConnection.h" - -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::jsinspector_modern::InspectorPage &page : pages) { - folly::dynamic pageDyn = folly::dynamic::object; - pageDyn["id"] = page.id; - pageDyn["title"] = page.description; - pageDyn["vm"] = page.vm; - - pageDyn["isLastBundleDownloadSuccess"] = bundleStatus.m_isLastDownloadSuccess; - pageDyn["bundleUpdateTimestamp"] = bundleStatus.m_updateTimestamp; - - payload.push_back(pageDyn); - } - return payload; - } - - static folly::dynamic constructVMResponsePayloadForPackager(int32_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(int32_t pageId) { - folly::dynamic payload = folly::dynamic::object; - payload[InspectorProtocol::Message_PAGEID] = pageId; - return payload; - } -}; - -} // namespace - -RemoteConnection::RemoteConnection(int32_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(int32_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::Networking::IWebSocketResource::CloseCode::GoingAway, reason); - co_return; -} - -winrt::fire_and_forget InspectorPackagerConnection::connectAsync() { - co_await winrt::resume_background(); - - m_packagerWebSocketConnection = Microsoft::React::Networking::IWebSocketResource::Make(); - - m_packagerWebSocketConnection->SetOnError([](const Microsoft::React::Networking::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 inspectorPages = - facebook::react::jsinspector_modern::getInspectorInstance().getPages(); - folly::dynamic response = InspectorProtocol::constructResponseForPackager( - InspectorProtocol::EventType::GetPages, - InspectorProtocol::constructGetPagesResponsePayloadForPackager( - inspectorPages, 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]; - int32_t pageId = static_cast(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]; - int32_t pageId = static_cast(payload[InspectorProtocol::Message_PAGEID].asInt()); - - if (self->m_localConnections.find(pageId) != self->m_localConnections.end()) { - break; - } - - self->m_localConnections[pageId] = facebook::react::jsinspector_modern::getInspectorInstance().connect( - pageId, std::make_unique(pageId, *self)); - break; - } - - case InspectorProtocol::EventType::Disconnect: { - folly::dynamic payload = messageDyn[InspectorProtocol::Message_PAYLOAD]; - int32_t pageId = static_cast(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::Networking::IWebSocketResource::Protocols protocols; - Microsoft::React::Networking::IWebSocketResource::Options options; - m_packagerWebSocketConnection->Connect(std::string{m_url}, protocols, options); - - co_return; -} - -} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/InspectorPackagerConnection.h b/vnext/Shared/InspectorPackagerConnection.h deleted file mode 100644 index b40b4f51e61..00000000000 --- a/vnext/Shared/InspectorPackagerConnection.h +++ /dev/null @@ -1,61 +0,0 @@ -// 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_isLastDownloadSuccess; - int64_t m_updateTimestamp = -1; - - BundleStatus(bool isLastDownloadSuccess, long updateTimestamp) - : m_isLastDownloadSuccess(isLastDownloadSuccess), m_updateTimestamp(updateTimestamp) {} - BundleStatus() : m_isLastDownloadSuccess(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 asynchronously in a sequential executor with dedicated thread. - // Hence, we don't bother invoking the inspector asynchronously. - void sendMessageToVM(int32_t pageId, std::string &&message); - - private: - 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::jsinspector_modern::IRemoteConnection { - public: - RemoteConnection(int32_t pageId, const InspectorPackagerConnection &packagerConnection); - void onMessage(std::string message) override; - void onDisconnect() override; - - private: - int32_t m_pageId; - const InspectorPackagerConnection &m_packagerConnection; -}; - -} // namespace Microsoft::ReactNative diff --git a/vnext/Shared/JSI/RuntimeHolder.h b/vnext/Shared/JSI/RuntimeHolder.h index 69a95b5a1cd..67f1bf10d64 100644 --- a/vnext/Shared/JSI/RuntimeHolder.h +++ b/vnext/Shared/JSI/RuntimeHolder.h @@ -4,6 +4,7 @@ #include #include +#include namespace Microsoft::JSI { @@ -18,11 +19,13 @@ struct RuntimeHolderLazyInit { virtual std::shared_ptr getRuntime() noexcept = 0; virtual facebook::react::JSIEngineOverride getRuntimeType() noexcept = 0; - virtual void teardown() noexcept {}; - // You can call this when a crash happens to attempt recording additional data // The fileDescriptor supplied is a raw file stream an implementation might write JSON to. virtual void crashHandler(int fileDescriptor) noexcept {}; + + virtual std::shared_ptr createRuntimeTargetDelegate() { + return nullptr; + } }; } // namespace Microsoft::JSI diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index bb5f994aa36..6a8c3bdbe7e 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -40,7 +40,9 @@ #include #include #include +#include "Inspector/ReactInspectorThread.h" #include "PackagerConnection.h" +#include "Threading/MessageDispatchQueue.h" #if defined(USE_HERMES) && defined(ENABLE_DEVSERVER_HBCBUNDLES) #include @@ -112,7 +114,6 @@ void LoadRemoteUrlScript( hermesBytecodeVersion); if (!success) { - devManager->UpdateBundleStatus(false, -1); devSettings->errorCallback(jsBundleString); return; } @@ -120,7 +121,6 @@ void LoadRemoteUrlScript( int64_t currentTimeInMilliSeconds = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count(); - devManager->UpdateBundleStatus(true, currentTimeInMilliSeconds); auto bundleUrl = facebook::react::DevServerHelper::get_BundleUrl( devSettings->sourceBundleHost, @@ -205,11 +205,33 @@ std::unique_ptr JsBigStringFromPath( } // namespace Microsoft::ReactNative -namespace facebook { -namespace react { +namespace facebook::react { namespace { +// OJSIExecutor is needed to override getRuntimeTargetDelegate to support the modern JSI inspector. +class OJSIExecutor : public JSIExecutor { + public: + OJSIExecutor( + std::shared_ptr runtime, + std::shared_ptr delegate, + const JSIScopedTimeoutInvoker &timeoutInvoker, + RuntimeInstaller runtimeInstaller, + std::shared_ptr &&targetDelegate) noexcept + : JSIExecutor(std::move(runtime), std::move(delegate), timeoutInvoker, std::move(runtimeInstaller)), + targetDelegate_(std::move(targetDelegate)) {} + jsinspector_modern::RuntimeTargetDelegate &getRuntimeTargetDelegate() override { + if (!targetDelegate_) { + // Use the fallback implementation from JSIExecutor. + return JSIExecutor::getRuntimeTargetDelegate(); + } + return *targetDelegate_; + } + + private: + std::shared_ptr targetDelegate_; +}; + class OJSIExecutorFactory : public JSExecutorFactory { public: std::unique_ptr createJSExecutor( @@ -226,7 +248,7 @@ class OJSIExecutorFactory : public JSExecutorFactory { } bindNativeLogger(*runtimeHolder_->getRuntime(), logger); - return std::make_unique( + return std::make_unique( runtimeHolder_->getRuntime(), std::move(delegate), JSIExecutor::defaultTimeoutInvoker, @@ -234,7 +256,8 @@ class OJSIExecutorFactory : public JSExecutorFactory { #ifdef ENABLE_JS_SYSTRACE_TO_ETW facebook::react::tracing::initializeJSHooks(runtime, isProfiling); #endif - }); + }, + runtimeHolder_->createRuntimeTargetDelegate()); } OJSIExecutorFactory( @@ -323,20 +346,6 @@ void InstanceImpl::SetInError() noexcept { m_isInError = true; } -namespace { -bool shouldStartHermesInspector(DevSettings &devSettings) { - bool isHermes = - ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || - (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && - devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); - - if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) - return true; - else - return false; -} -} // namespace - InstanceImpl::InstanceImpl( std::shared_ptr &&instance, std::string &&jsBundleBasePath, @@ -367,8 +376,9 @@ InstanceImpl::InstanceImpl( facebook::react::tracing::initializeETW(); #endif - if (shouldStartHermesInspector(*m_devSettings)) { - m_devManager->EnsureHermesInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); + if (m_devSettings->useDirectDebugger) { + m_devManager->EnsureInspectorPackagerConnection( + m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->bundleAppId); } std::vector> modules; @@ -477,7 +487,8 @@ InstanceImpl::InstanceImpl( } } - m_innerInstance->initializeBridge(std::move(callback), jsef, m_jsThread, m_moduleRegistry); + m_innerInstance->initializeBridge( + std::move(callback), jsef, m_jsThread, m_moduleRegistry, m_devSettings->inspectorHostTarget); // For RuntimeScheduler to work properly, we need to install TurboModuleManager with RuntimeSchedulerCallbackInvoker. // To be able to do that, we need to be able to call m_innerInstance->getRuntimeExecutor(), which we can only do after @@ -580,9 +591,16 @@ void InstanceImpl::loadBundleInternal(std::string &&jsBundleRelativePath, bool s } InstanceImpl::~InstanceImpl() { - if (shouldStartHermesInspector(*m_devSettings) && m_devSettings->jsiRuntimeHolder) { - m_devSettings->jsiRuntimeHolder->teardown(); + if (m_devSettings->inspectorHostTarget) { + Mso::React::MessageDispatchQueue messageDispatchQueue{ + ::Microsoft::ReactNative::ReactInspectorThread::Instance(), nullptr}; + messageDispatchQueue.runOnQueueSync([weakInnerInstance = std::weak_ptr(m_innerInstance)]() { + if (std::shared_ptr innerInstance = weakInnerInstance.lock()) { + innerInstance->unregisterFromInspector(); + } + }); } + m_nativeQueue->quitSynchronous(); } @@ -611,5 +629,4 @@ void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic &&par m_innerInstance->callJSCallback(callbackId, std::move(params)); } -} // namespace react -} // namespace facebook +} // namespace facebook::react diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index 8fda1683eec..3bb90af4885 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -36,7 +36,7 @@ $(ReactNativeWindowsDir)Microsoft.ReactNative\Composition.Input.idl - true + true true @@ -76,6 +76,10 @@ true + + true + Code + true $(ReactNativeWindowsDir)Microsoft.ReactNative\ReactNativeIsland.idl @@ -232,16 +236,16 @@ true - + true - + true $(ReactNativeWindowsDir)Microsoft.ReactNative\ReactNativeAppBuilder.idl Code - + true $(ReactNativeWindowsDir)Microsoft.ReactNative\ReactNativeAppBuilder.idl Code @@ -255,8 +259,10 @@ - - + + + + @@ -265,7 +271,7 @@ true - + @@ -339,10 +345,10 @@ $(MSBuildThisFileDirectory)..\Microsoft.ReactNative\JsiApi.idl - + - - true + + true $(MSBuildThisFileDirectory)..\Microsoft.ReactNative\ReactNativeAppBuilder.idl Code @@ -405,13 +411,18 @@ $(MSBuildThisFileDirectory)..\Microsoft.ReactNative\Timer.idl - + + - + + + + + @@ -456,7 +467,6 @@ - @@ -593,7 +603,7 @@ - + @@ -710,7 +720,7 @@ - + @@ -726,4 +736,4 @@ NotUsing - + \ No newline at end of file diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index f20eb89b198..c65e1f722c8 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -76,9 +76,6 @@ Source Files\JSI - - Source Files - Source Files\Modules @@ -200,6 +197,9 @@ Source Files\Fabric\Composition + + Source Files\Fabric\Composition + Source Files\Fabric\Composition @@ -212,9 +212,6 @@ Source Files\Fabric - - Source Files\Fabric - Source Files\Fabric @@ -247,9 +244,6 @@ Hermes - - Hermes - @@ -331,6 +325,18 @@ + + Hermes + + + Hermes + + + Hermes + + + Inspector + @@ -465,6 +471,9 @@ {b32590e6-ae3d-4388-ab98-767345ce38c9} + + {680511e1-15e0-48c9-a2a3-64addaeacce5} + @@ -600,9 +609,6 @@ Header Files\JSI - - Header Files - Header Files\tracing @@ -793,9 +799,6 @@ Hermes - - Hermes - @@ -804,6 +807,21 @@ + + Hermes + + + Hermes + + + Hermes + + + Inspector + + + Inspector + diff --git a/vnext/overrides.json b/vnext/overrides.json index 0560eee5d00..efa658f0b0b 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -82,6 +82,13 @@ "baseHash": "80c95f0c0635f68a77e8a61380484389d771bf9e", "issue": 12210 }, + { + "type": "patch", + "file": "ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/network/HttpUtils.cpp", + "baseFile": "packages/react-native/ReactCommon/jsinspector-modern/network/HttpUtils.cpp", + "baseHash": "fcec9bc75ff320e964dd8072dab3f0635fe935a5", + "issue": 13587 + }, { "type": "patch", "file": "ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/NetworkIOAgent.cpp", @@ -96,13 +103,6 @@ "baseHash": "d9dff538dba234600e8e0d7d7a8243891f414c29", "issue": 13587 }, - { - "type": "patch", - "file": "ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp", - "baseFile": "packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp", - "baseHash": "cba5466055f9089a93cfd7968b0001354df95e60", - "issue": 15256 - }, { "type": "patch", "file": "ReactCommon/TEMP_UntilReactCommonUpdate/jsinspector-modern/Utf8.h", @@ -110,20 +110,6 @@ "baseHash": "1d81aa08f37619a8e9e05c2cf3d76f8f878fb9df", "issue": 13587 }, - { - "type": "patch", - "file": "ReactCommon/TEMP_UntilReactCommonUpdate/jsitooling/react/runtime/JSRuntimeFactory.cpp", - "baseFile": "packages/react-native/ReactCommon/jsitooling/react/runtime/JSRuntimeFactory.cpp", - "baseHash": "c77f0b0829b850f8e2f115bfc3440ce94feb207d", - "issue": 13172 - }, - { - "type": "patch", - "file": "ReactCommon/TEMP_UntilReactCommonUpdate/jsitooling/react/runtime/JSRuntimeFactory.h", - "baseFile": "packages/react-native/ReactCommon/jsitooling/react/runtime/JSRuntimeFactory.h", - "baseHash": "3e9e300ac2c01ed2b3bfa7e5afe7b5961d3e9080", - "issue": 13172 - }, { "type": "patch", "file": "ReactCommon/TEMP_UntilReactCommonUpdate/react/bridging/Bridging.h", @@ -213,13 +199,6 @@ "baseHash": "74cee7804fa62964c9ecc3fafd475d09b0ed6375", "issue": 15263 }, - { - "type": "patch", - "file": "ReactCommon/TEMP_UntilReactCommonUpdate/react/runtime/ReactInstance.cpp", - "baseFile": "packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp", - "baseHash": "f7a058ced6474e42e0ed805a2875a99c420e8398", - "issue": 13172 - }, { "type": "copy", "directory": "ReactCopies/IntegrationTests",