Skip to content

Commit c4bc29f

Browse files
authored
Add support for modern inspector (#15328)
1 parent 5837f91 commit c4bc29f

File tree

57 files changed

+2391
-1982
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2391
-1982
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Added support for modern inspector",
4+
"packageName": "react-native-windows",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

docs/modern-inspector-details.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Modern Inspector Support in React Native for Windows
2+
3+
## Overview
4+
The modern inspector is the Chrome DevTools–based debugging experience that ships with the latest
5+
versions of React Native. This experience now works end-to-end for React Native for Windows (RNW)
6+
applications, enabling parity with Android and iOS. The integration provides a unified way to inspect
7+
JavaScript execution, evaluate console expressions, profile CPU and memory usage, and visualize the
8+
component hierarchy for both the Paper and Composition UI stacks.
9+
10+
## Modern Inspector Building Blocks
11+
- **Host Target** – One per `ReactHost`; surfaces metadata, reload requests, pause overlays, and
12+
implements the CDP-facing delegate (`ReactInspectorHostTargetDelegate`).
13+
- **Instance Target** – Created for each React instance; registers runtime targets, tracks instance
14+
lifecycle, and unregisters cleanly on reload.
15+
- **Runtime Target & Agent** – Runtime targets map to JavaScript VMs; agents are per-session objects
16+
that translate Chrome DevTools Protocol (CDP) messages into engine calls. RNW mirrors upstream
17+
lifetimes so reloads tear everything down deterministically.
18+
- **Frontend Channel** – Delivers JSON CDP payloads between the RNW host and DevTools.
19+
- **Inspector Thread** – A single `ReactInspectorThread` ensures CDP work is serialized away from
20+
the UI and JS queues. (iOS and Andrtoid use UI thread.)
21+
- **Debugger Notifications**`DebuggerNotifications.h` broadcasts pause/resume events so view
22+
hosts can react (e.g., showing overlays or resuming when the debugger continues).
23+
24+
## Windows Integration Points
25+
- **ReactHost & ReactOptions**`ReactHost` creates the `HostTarget`, exposes it through
26+
`ReactOptions::InspectorHostTarget`, and implements reload/pause hooks. This is the jump-off point
27+
for all inspector traffic. The inspector supported only if the `UseDirectDebugger` is true.
28+
- **ReactInstanceWin / OInstance** – Register and unregister instance/runtime targets around runtime
29+
creation, keeping the inspector aligned with bridgeless and legacy bridge lifecycles.
30+
- **DevSupportManager & Packager**`DevSupportManager` now spins up
31+
`ReactInspectorPackagerConnectionDelegate`, allowing Metro to broker modern inspector connections
32+
and reuse the existing websocket infrastructure.
33+
- **Dev Console Shortcut** – Metro’s `J` shortcut launches the inspector for Windows apps, matching
34+
upstream behavior.
35+
36+
## UI Overlays
37+
- **Composition**`DebuggerUIIsland` renders pause overlays, focus chrome, and selection adorners
38+
whenever the runtime is paused.
39+
- **Paper**`ReactRootView` updates provide the same pause/selection affordances.
40+
41+
## Hermes Runtime Integration
42+
- `HermesRuntimeTargetDelegate` and `HermesRuntimeAgentDelegate` wrap the hermes-windows C debug API
43+
so we can re-use upstream modern inspector code.
44+
- `RuntimeHolder`/`HermesRuntimeHolder` surface a `createRuntimeTargetDelegate` hook that instantiates
45+
delegates only when the inspector is enabled, and safely tears them down during reloads.
46+
47+
## Packager & Console Integration
48+
- `ReactInspectorPackagerConnectionDelegate` maps the Metro websocket APIs to the modern inspector
49+
handshake.
50+
- Console output, CPU sampling, and memory profiling are forwarded through the Hermes inspector
51+
plumbing automatically once a session is active.
52+
53+
## Using the Modern Inspector with RNW
54+
1. Start your Metro bundler (`npx react-native start` or `yarn start`).
55+
2. Launch your RNW app (Paper or Composition).
56+
3. In the Metro console, press `J` to open the modern inspector URL in a browser.
57+
4. Chrome DevTools will connect to the Hermes runtime. Pause execution, explore the component tree,
58+
and capture profiles as needed.
59+
5. When execution is paused, the corresponding overlay is rendered in the app window; resume to clear
60+
the overlay.
61+
62+
## Known Limitations & Follow-Up Work
63+
- **Network profiling** – The `NetworkIOAgent` is not wired up yet for Windows. The integration point
64+
is the `ReactInspectorHostTargetDelegate` in
65+
`vnext/Microsoft.ReactNative/ReactHost/ReactHost.cpp`; override `loadNetworkResource` there to
66+
forward requests through a Windows HTTP helper (similar to `GetJavaScriptFromServerAsync`) and
67+
stream results back via the provided `NetworkRequestListener`. Until this happens, the Network tab
68+
in DevTools stays empty.
69+
- **Legacy Chakra runtime** – Modern inspector support currently targets Hermes. Chakra-based apps
70+
continue to rely on legacy debugging flows.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
#include "pch.h"
4+
#include "DebuggerUIIsland.h"
5+
6+
#include <AutoDraw.h>
7+
#include <react/renderer/attributedstring/AttributedStringBox.h>
8+
#include <react/renderer/textlayoutmanager/WindowsTextLayoutManager.h>
9+
#include <winrt/Microsoft.UI.Composition.h>
10+
#include <winrt/Microsoft.UI.Content.h>
11+
#include <winrt/Microsoft.UI.Input.h>
12+
#include "CompositionContextHelper.h"
13+
#include "TextDrawing.h"
14+
15+
namespace winrt::Microsoft::ReactNative::implementation {
16+
17+
constexpr float debuggerUIFontSize = 10.0f;
18+
constexpr float debuggerTextMargin = 4.0f;
19+
20+
DebuggerUIIsland::DebuggerUIIsland(
21+
const winrt::Microsoft::UI::Composition::Compositor &compositor,
22+
winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext,
23+
winrt::Microsoft::ReactNative::Composition::Theme theme) noexcept
24+
: m_compositor(compositor), m_compContext(compContext), m_theme(theme) {
25+
m_backgroundVisual = m_compositor.CreateSpriteVisual();
26+
m_backgroundVisual.RelativeSizeAdjustment({1.0f, 1.0f});
27+
28+
auto backgroundBrush = m_compositor.CreateColorBrush({100, 0, 0, 0});
29+
m_backgroundVisual.Brush(backgroundBrush);
30+
31+
m_TextVisual = m_compositor.CreateSpriteVisual();
32+
m_TextVisual.IsPixelSnappingEnabled(true);
33+
34+
m_backgroundVisual.Children().InsertAtTop(m_TextVisual);
35+
}
36+
37+
DebuggerUIIsland::~DebuggerUIIsland() noexcept {
38+
m_island.StateChanged(m_islandStateChangedToken);
39+
}
40+
41+
void DebuggerUIIsland::Redraw() noexcept {
42+
if (!m_island)
43+
return;
44+
45+
if (m_island.ActualSize().x == 0 || m_island.ActualSize().y == 0)
46+
return;
47+
48+
auto scaleFactor = m_island.Environment().DisplayScale();
49+
50+
auto attributedString = facebook::react::AttributedString{};
51+
auto fragment = facebook::react::AttributedString::Fragment{};
52+
fragment.string = m_message;
53+
fragment.textAttributes.fontSize = debuggerUIFontSize;
54+
attributedString.appendFragment(std::move(fragment));
55+
56+
// Resume Icon
57+
auto iconFragment = facebook::react::AttributedString::Fragment{};
58+
iconFragment.string = " \uF08F";
59+
iconFragment.textAttributes.fontFamily = "Segoe Fluent Icons";
60+
iconFragment.textAttributes.fontSize = debuggerUIFontSize;
61+
attributedString.appendFragment(std::move(iconFragment));
62+
63+
auto attributedStringBox = facebook::react::AttributedStringBox{attributedString};
64+
65+
facebook::react::LayoutConstraints constraints;
66+
constraints.maximumSize.width = std::max(0.0f, m_island.ActualSize().x - debuggerTextMargin * 2 * scaleFactor);
67+
constraints.maximumSize.height = std::max(0.0f, m_island.ActualSize().y - debuggerTextMargin * 2 * scaleFactor);
68+
69+
auto textAttributes = facebook::react::TextAttributes{};
70+
textAttributes.foregroundColor = facebook::react::blackColor();
71+
72+
winrt::com_ptr<::IDWriteTextLayout> textLayout;
73+
facebook::react::WindowsTextLayoutManager::GetTextLayout(attributedStringBox, {}, constraints, textLayout);
74+
75+
DWRITE_TEXT_METRICS tm;
76+
textLayout->GetMetrics(&tm);
77+
78+
winrt::Windows::Foundation::Size surfaceSize = {
79+
std::ceilf(std::min(constraints.maximumSize.width, tm.width + debuggerTextMargin * 2 * scaleFactor)),
80+
std::ceilf(std::min(constraints.maximumSize.height, tm.height + debuggerTextMargin * 2 * scaleFactor))};
81+
auto drawingSurface = m_compContext.CreateDrawingSurfaceBrush(
82+
surfaceSize,
83+
winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
84+
winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied);
85+
86+
POINT offset;
87+
{
88+
::Microsoft::ReactNative::Composition::AutoDrawDrawingSurface autoDraw(drawingSurface, scaleFactor, &offset);
89+
if (auto d2dDeviceContext = autoDraw.GetRenderTarget()) {
90+
d2dDeviceContext->Clear(D2D1::ColorF{1.0f, 1.0f, 0.76f, 1.0f});
91+
92+
auto theme = winrt::get_self<winrt::Microsoft::ReactNative::Composition::implementation::Theme>(m_theme);
93+
94+
Composition::RenderText(
95+
*d2dDeviceContext,
96+
*textLayout,
97+
attributedStringBox.getValue(),
98+
textAttributes,
99+
{static_cast<float>(offset.x + std::floorf(debuggerTextMargin * scaleFactor)),
100+
static_cast<float>(offset.y + std::floorf(debuggerTextMargin * scaleFactor))},
101+
scaleFactor,
102+
*theme);
103+
}
104+
105+
drawingSurface.HorizontalAlignmentRatio(0.0f);
106+
drawingSurface.Stretch(winrt::Microsoft::ReactNative::Composition::Experimental::CompositionStretch::None);
107+
108+
m_TextVisual.Brush(winrt::Microsoft::ReactNative::Composition::Experimental::implementation::
109+
MicrosoftCompositionContextHelper::InnerBrush(drawingSurface));
110+
m_TextVisual.Size({surfaceSize.Width, surfaceSize.Height});
111+
112+
m_debuggerHitRect = {
113+
m_island.ActualSize().x / 2 - tm.width / 2 + debuggerTextMargin * scaleFactor,
114+
debuggerTextMargin * scaleFactor,
115+
surfaceSize.Width,
116+
surfaceSize.Height};
117+
118+
m_TextVisual.Offset({m_debuggerHitRect.X, m_debuggerHitRect.Y, 0.0f});
119+
}
120+
}
121+
122+
void DebuggerUIIsland::Message(std::string &&value) noexcept {
123+
m_message = value;
124+
Redraw();
125+
}
126+
127+
winrt::Microsoft::UI::Content::ContentIsland DebuggerUIIsland::Island() noexcept {
128+
if (!m_island) {
129+
m_island = winrt::Microsoft::UI::Content::ContentIsland::Create(m_backgroundVisual);
130+
131+
m_islandStateChangedToken =
132+
m_island.StateChanged([weakThis = weak_from_this()](
133+
winrt::Microsoft::UI::Content::ContentIsland const &island,
134+
winrt::Microsoft::UI::Content::ContentIslandStateChangedEventArgs const &args) {
135+
if (auto pThis = weakThis.lock()) {
136+
if (args.DidRasterizationScaleChange() || args.DidActualSizeChange()) {
137+
pThis->Redraw();
138+
}
139+
}
140+
});
141+
142+
auto pointerSource = winrt::Microsoft::UI::Input::InputPointerSource::GetForIsland(m_island);
143+
144+
m_islandPointerUpToken =
145+
pointerSource.PointerReleased([weakThis = weak_from_this()](
146+
winrt::Microsoft::UI::Input::InputPointerSource const &,
147+
winrt::Microsoft::UI::Input::PointerEventArgs const &args) {
148+
if (auto pThis = weakThis.lock()) {
149+
auto position = args.CurrentPoint().Position();
150+
if (position.X >= pThis->m_debuggerHitRect.X && position.Y >= pThis->m_debuggerHitRect.Y &&
151+
position.X <= pThis->m_debuggerHitRect.X + pThis->m_debuggerHitRect.Width &&
152+
position.Y <= pThis->m_debuggerHitRect.Y + pThis->m_debuggerHitRect.Height) {
153+
pThis->m_resumedEvent(nullptr, nullptr);
154+
}
155+
}
156+
});
157+
}
158+
return m_island;
159+
}
160+
161+
winrt::event_token DebuggerUIIsland::Resumed(
162+
winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable> const &handler) noexcept {
163+
return m_resumedEvent.add(handler);
164+
}
165+
void DebuggerUIIsland::Resumed(winrt::event_token const &token) noexcept {
166+
m_resumedEvent.remove(token);
167+
}
168+
169+
} // namespace winrt::Microsoft::ReactNative::implementation
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
#pragma once
4+
5+
#include <winrt/Microsoft.ReactNative.Composition.Experimental.h>
6+
#include <winrt/Microsoft.ReactNative.Composition.h>
7+
8+
namespace winrt::Microsoft::ReactNative::implementation {
9+
10+
struct DebuggerUIIsland : std::enable_shared_from_this<DebuggerUIIsland> {
11+
DebuggerUIIsland(
12+
const winrt::Microsoft::UI::Composition::Compositor &compositor,
13+
winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext,
14+
winrt::Microsoft::ReactNative::Composition::Theme theme) noexcept;
15+
~DebuggerUIIsland() noexcept;
16+
winrt::Microsoft::UI::Content::ContentIsland Island() noexcept;
17+
18+
void Message(std::string &&value) noexcept;
19+
20+
winrt::event_token Resumed(
21+
winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable> const &handler) noexcept;
22+
void Resumed(winrt::event_token const &token) noexcept;
23+
24+
private:
25+
void Redraw() noexcept;
26+
27+
winrt::event_token m_islandStateChangedToken;
28+
winrt::event_token m_islandPointerUpToken;
29+
30+
winrt::Microsoft::UI::Composition::SpriteVisual m_backgroundVisual{nullptr};
31+
winrt::Microsoft::UI::Composition::SpriteVisual m_TextVisual{nullptr};
32+
winrt::Windows::Foundation::Rect m_debuggerHitRect{0, 0, 0, 0};
33+
winrt::Microsoft::ReactNative::Composition::Theme m_theme{nullptr};
34+
std::string m_message;
35+
36+
winrt::event<winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable>> m_resumedEvent;
37+
winrt::Microsoft::UI::Composition::Compositor m_compositor{nullptr};
38+
winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext m_compContext{nullptr};
39+
winrt::Microsoft::UI::Content::ContentIsland m_island{nullptr};
40+
};
41+
42+
} // namespace winrt::Microsoft::ReactNative::implementation

vnext/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ void DebuggingOverlayComponentView::HandleCommand(
103103
auto rootVisual = root->OuterVisual();
104104

105105
while (m_activeOverlays != 0) {
106+
--m_activeOverlays;
106107
auto visual = rootVisual.GetAt(root->overlayIndex() + m_activeOverlays);
107108
rootVisual.Remove(visual);
108-
--m_activeOverlays;
109109
}
110110
}
111111
return;

0 commit comments

Comments
 (0)