Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/vscode-extension/assets/react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 38 additions & 4 deletions packages/vscode-extension/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions packages/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,18 @@
"description": "Get up-to-date answers related to React Native",
"isSticky": true
}
],
"customEditors": [
{
"viewType": "RadonIDE.reactDevtoolsProfiler",
"displayName": "React Profiler",
"selector": [
{
"filenamePattern": "*.reactprofile"
}
],
"priority": "default"
}
]
},
"scripts": {
Expand Down Expand Up @@ -780,8 +792,10 @@
"prettier": "3.5.3",
"re-resizable": "^6.11.2",
"react": "^19.1.0",
"react-devtools-inline": "^6.1.1",
"react-dom": "^19.1.0",
"react-hook-form": "^7.55.0",
"react-is": "^19.1.0",
"react-scan": "https://github.com/software-mansion-labs/react-scan/releases/download/0.1.3-radon-2/react-scan-0.1.3-radon-2.tgz",
"rollup": "^4.40.0",
"rollup-plugin-license": "^3.6.0",
Expand Down
5 changes: 5 additions & 0 deletions packages/vscode-extension/src/common/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export interface ProjectEventMap {
replayDataCreated: MultimediaData;
isRecording: boolean;
isProfilingCPU: boolean;
isProfilingReact: boolean;
isSavingReactProfile: boolean;
}

export interface ProjectEventListener<T> {
Expand Down Expand Up @@ -189,6 +191,9 @@ export interface ProjectInterface {
startProfilingCPU(): void;
stopProfilingCPU(): void;

startProfilingReact(): void;
stopProfilingReact(): void;

dispatchTouches(touches: Array<TouchPoint>, type: "Up" | "Move" | "Down"): void;
dispatchKeyPress(keyCode: number, direction: "Up" | "Down"): void;
dispatchWheel(point: TouchPoint, deltaX: number, deltaY: number): void;
Expand Down
2 changes: 2 additions & 0 deletions packages/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { IDE } from "./project/ide";
import { registerChat } from "./chat";
import { ProxyDebugSessionAdapterDescriptorFactory } from "./debugging/ProxyDebugAdapter";
import { Connector } from "./connect/Connector";
import { ReactDevtoolsEditorProvider } from "./react-devtools-profiler/ReactDevtoolsEditorProvider";

const OPEN_PANEL_ON_ACTIVATION = "open_panel_on_activation";
const CHAT_ONBOARDING_COMPLETED = "chat_onboarding_completed";
Expand Down Expand Up @@ -147,6 +148,7 @@ export async function activate(context: ExtensionContext) {
{ webviewOptions: { retainContextWhenHidden: true } }
)
);
context.subscriptions.push(ReactDevtoolsEditorProvider.register(context));
context.subscriptions.push(
commands.registerCommand("RNIDE.performBiometricAuthorization", performBiometricAuthorization)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export function generateWebviewContent(
extensionUri: Uri,
webviewName: string,
webviewDevPath: string,
wsEndpoint?: string
wsEndpoint?: string,
allowUnsafeEval?: boolean
) {
const config = workspace.getConfiguration("RadonIDE");
const useCodeTheme = config.get("themeType") === "vscode";
Expand Down Expand Up @@ -65,11 +66,11 @@ export function generateWebviewContent(
img-src vscode-resource: http: https: data:;
media-src vscode-resource: http: https:;
style-src ${webview.cspSource} 'unsafe-inline';
script-src 'nonce-${nonce}';
script-src 'nonce-${nonce}' ${allowUnsafeEval ? "'unsafe-eval'" : ""};
worker-src 'self' blob:;
${wsEndpoint ? `connect-src ws://${wsEndpoint} ${webview.cspSource};` : ""}
font-src vscode-resource: https:;" />
<link rel="stylesheet" type="text/css" href="theme.css" />
<link rel="stylesheet" type="text/css" href="${webviewName}.css" />
<link rel="stylesheet" type="text/css" href="style.css" />
`
}
</head>
Expand Down
5 changes: 5 additions & 0 deletions packages/vscode-extension/src/project/deviceSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export type AppEvent = {
navigationChanged: { displayName: string; id: string };
fastRefreshStarted: undefined;
fastRefreshComplete: undefined;
isProfilingReact: boolean;
isSavingReactProfile: boolean;
};

export type DeviceSessionDelegate = {
Expand Down Expand Up @@ -154,6 +156,9 @@ export class DeviceSession implements Disposable {
devtools.onEvent("RNIDE_fastRefreshComplete", () => {
this.deviceSessionDelegate.onAppEvent("fastRefreshComplete", undefined);
});
devtools.onEvent("RNIDE_isProfilingReact", (isProfiling) => {
this.deviceSessionDelegate.onAppEvent("isProfilingReact", isProfiling);
});
return devtools;
}

Expand Down
56 changes: 54 additions & 2 deletions packages/vscode-extension/src/project/devtools.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import http from "http";
import { Disposable } from "vscode";
import { commands, Disposable, Uri } from "vscode";
import { WebSocketServer, WebSocket } from "ws";
import { Logger } from "../Logger";
import {
createBridge,
createStore,
FrontendBridge,
prepareProfilingDataExport,
Store,
Wall,
} from "../../third-party/react-devtools/headless";
import path from "path";
import fs from "fs";
import os from "os";

// Define event names as a const array to avoid duplication
Expand All @@ -23,6 +25,7 @@ export const DEVTOOLS_EVENTS = [
"RNIDE_devtoolPluginsChanged",
"RNIDE_rendersReported",
"RNIDE_pluginMessage",
"RNIDE_isProfilingReact",
] as const;

// Define the payload types for each event
Expand All @@ -36,6 +39,13 @@ export interface DevtoolsEvents {
RNIDE_devtoolPluginsChanged: [{ plugins: string[] }];
RNIDE_rendersReported: [any];
RNIDE_pluginMessage: [{ scope: string; type: string; data: any }];
RNIDE_isProfilingReact: [boolean];
}

function filePathForProfile() {
const fileName = `profile-${Date.now()}.reactprofile`;
const filePath = path.join(os.tmpdir(), fileName);
return filePath;
}

export class Devtools implements Disposable {
Expand All @@ -44,6 +54,7 @@ export class Devtools implements Disposable {
private socket?: WebSocket;
private startPromise: Promise<void> | undefined;
private listeners: Map<keyof DevtoolsEvents, Array<(...payload: any) => void>> = new Map();
private store: Store | undefined;

public get port() {
return this._port;
Expand Down Expand Up @@ -107,10 +118,16 @@ export class Devtools implements Disposable {

const bridge = createBridge(wall);
const store = createStore(bridge);
this.store = store;

ws.on("close", () => {
this.socket = undefined;
if (this.socket === ws) {
this.socket = undefined;
}
bridge.shutdown();
if (this.store === store) {
this.store = undefined;
}
});

// Register bridge listeners for ALL custom event types
Expand All @@ -119,6 +136,14 @@ export class Devtools implements Disposable {
this.listeners.get(event)?.forEach((listener) => listener(payload));
});
}

// Register for isProfiling event on the profiler store
store.profilerStore.addListener("isProfiling", () => {
this.listeners
.get("RNIDE_isProfilingReact")
// @ts-ignore - isProfilingBasedOnUserInput exists but types are outdated
?.forEach((listener) => listener(store.profilerStore.isProfilingBasedOnUserInput));
});
});

return new Promise<void>((resolve) => {
Expand All @@ -130,6 +155,33 @@ export class Devtools implements Disposable {
});
}

public async startProfilingReact() {
this.store?.profilerStore.startProfiling();
}

public async stopProfilingReact() {
const { resolve, reject, promise } = Promise.withResolvers<Uri>();
const saveProfileListener = async () => {
const isProcessingData = this.store?.profilerStore.isProcessingData;
if (!isProcessingData) {
this.store?.profilerStore.removeListener("isProcessingData", saveProfileListener);
const profilingData = this.store?.profilerStore.profilingData;
if (profilingData) {
const exportData = prepareProfilingDataExport(profilingData);
const filePath = filePathForProfile();
await fs.promises.writeFile(filePath, JSON.stringify(exportData));
resolve(Uri.file(filePath));
} else {
reject(new Error("No profiling data available"));
}
}
};

this.store?.profilerStore.addListener("isProcessingData", saveProfileListener);
this.store?.profilerStore.stopProfiling();
return promise;
}

public dispose() {
this.server.close();
}
Expand Down
23 changes: 23 additions & 0 deletions packages/vscode-extension/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ export class Project implements Disposable, ProjectInterface, DeviceSessionsMana
}
this.updateProjectState({ status: "running" });
break;
case "isProfilingReact":
this.eventEmitter.emit("isProfilingReact", payload);
break;
case "isSavingReactProfile":
this.eventEmitter.emit("isSavingReactProfile", payload);
break;
}
};
//#endregion
Expand Down Expand Up @@ -291,6 +297,23 @@ export class Project implements Disposable, ProjectInterface, DeviceSessionsMana
}
}

async startProfilingReact() {
await this.deviceSession?.devtools.startProfilingReact();
}

async stopProfilingReact() {
try {
this.eventEmitter.emit("isSavingReactProfile", true);
const uri = await this.deviceSession?.devtools.stopProfilingReact();
if (uri) {
// open profile file in vscode using our custom editor
commands.executeCommand("vscode.open", uri);
}
} finally {
this.eventEmitter.emit("isSavingReactProfile", false);
}
}

onProfilingCPUStarted(event: DebugSessionCustomEvent): void {
this.eventEmitter.emit("isProfilingCPU", true);
}
Expand Down
10 changes: 10 additions & 0 deletions packages/vscode-extension/src/react-devtools-profiler/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
html,
body,
#root {
box-sizing: border-box;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
background-color: var(--background);
}
Loading