From 53c4338cc0956f795c720b39ea72b89168cdae9c Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Wed, 19 Feb 2025 15:10:42 +0100 Subject: [PATCH 1/8] Prototype impl --- .../vscode-extension/src/common/Project.ts | 4 ++++ .../src/debugging/DebugAdapter.ts | 22 ++++++++++++++--- .../src/debugging/DebugSession.ts | 16 +++++++++++++ .../src/project/deviceSession.ts | 16 +++++++++++++ .../vscode-extension/src/project/project.ts | 24 +++++++++++++++++++ .../src/webview/components/ToolsDropdown.tsx | 9 ++++++- .../src/webview/providers/ProjectProvider.tsx | 7 ++++++ 7 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/vscode-extension/src/common/Project.ts b/packages/vscode-extension/src/common/Project.ts index 8df381ccd..8ac24d969 100644 --- a/packages/vscode-extension/src/common/Project.ts +++ b/packages/vscode-extension/src/common/Project.ts @@ -125,6 +125,7 @@ export interface ProjectEventMap { needsNativeRebuild: void; replayDataCreated: MultimediaData; isRecording: boolean; + isProfilingCPU: boolean; } export interface ProjectEventListener { @@ -175,6 +176,9 @@ export interface ProjectInterface { captureReplay(): void; captureScreenshot(): void; + startProfilingCPU(): void; + stopProfilingCPU(): void; + dispatchTouches(touches: Array, type: "Up" | "Move" | "Down"): void; dispatchKeyPress(keyCode: number, direction: "Up" | "Down"): void; dispatchWheel(point: TouchPoint, deltaX: number, deltaY: number): void; diff --git a/packages/vscode-extension/src/debugging/DebugAdapter.ts b/packages/vscode-extension/src/debugging/DebugAdapter.ts index db0c42ff1..20f26830b 100644 --- a/packages/vscode-extension/src/debugging/DebugAdapter.ts +++ b/packages/vscode-extension/src/debugging/DebugAdapter.ts @@ -1,4 +1,7 @@ import { DebugConfiguration } from "vscode"; +import fs from "fs"; +import path from "path"; +import os from "os"; import { DebugSession, InitializedEvent, @@ -617,12 +620,25 @@ export class DebugAdapter extends DebugSession { this.sendResponse(response); } - protected customRequest( + protected async customRequest( command: string, response: DebugProtocol.Response, args: any, request?: DebugProtocol.Request | undefined - ): void { - Logger.debug(`Custom req ${command} ${args}`); + ) { + if (command === "startProfiling") { + await this.cdpSession.sendCDPMessage("Profiler.start", {}); + this.sendEvent(new Event("RNIDE_profilingCPUStarted")); + this.sendResponse(response); + } else if (command === "stopProfiling") { + const result = await this.cdpSession.sendCDPMessage("Profiler.stop", {}); + const fileName = `profile-${Date.now()}.cpuprofile`; + const filePath = path.join(os.tmpdir(), fileName); + await fs.promises.writeFile(filePath, JSON.stringify(result.profile)); + this.sendEvent(new Event("RNIDE_profilingCPUStopped", { filePath })); + this.sendResponse(response); + } else { + Logger.debug(`Custom req ${command} ${args}`); + } } } diff --git a/packages/vscode-extension/src/debugging/DebugSession.ts b/packages/vscode-extension/src/debugging/DebugSession.ts index 9f350c65f..6659d5a89 100644 --- a/packages/vscode-extension/src/debugging/DebugSession.ts +++ b/packages/vscode-extension/src/debugging/DebugSession.ts @@ -10,6 +10,8 @@ export type DebugSessionDelegate = { onConsoleLog(event: DebugSessionCustomEvent): void; onDebuggerPaused(event: DebugSessionCustomEvent): void; onDebuggerResumed(event: DebugSessionCustomEvent): void; + onProfilingCPUStarted(event: DebugSessionCustomEvent): void; + onProfilingCPUStopped(event: DebugSessionCustomEvent): void; }; export class DebugSession implements Disposable { @@ -28,6 +30,12 @@ export class DebugSession implements Disposable { case "RNIDE_continued": this.delegate.onDebuggerResumed(event); break; + case "RNIDE_profilingCPUStarted": + this.delegate.onProfilingCPUStarted(event); + break; + case "RNIDE_profilingCPUStopped": + this.delegate.onProfilingCPUStopped(event); + break; default: // ignore other events break; @@ -98,6 +106,14 @@ export class DebugSession implements Disposable { this.session.customRequest("next"); } + public async startProfilingCPU() { + await this.session.customRequest("startProfiling"); + } + + public async stopProfilingCPU() { + await this.session.customRequest("stopProfiling"); + } + private get session() { if (!this.vscSession) { throw new Error("Debugger not started"); diff --git a/packages/vscode-extension/src/project/deviceSession.ts b/packages/vscode-extension/src/project/deviceSession.ts index 92de715a2..4a33b7f22 100644 --- a/packages/vscode-extension/src/project/deviceSession.ts +++ b/packages/vscode-extension/src/project/deviceSession.ts @@ -313,6 +313,22 @@ export class DeviceSession implements Disposable { return this.device.captureScreenshot(); } + public async startProfilingCPU() { + if (this.debugSession) { + await this.debugSession.startProfilingCPU(); + } else { + throw new Error("Debug session not started"); + } + } + + public async stopProfilingCPU() { + if (this.debugSession) { + await this.debugSession.stopProfilingCPU(); + } else { + throw new Error("Debug session not started"); + } + } + public sendTouches(touches: Array, type: "Up" | "Move" | "Down") { this.device.sendTouches(touches, type); } diff --git a/packages/vscode-extension/src/project/project.ts b/packages/vscode-extension/src/project/project.ts index 9449cea37..c680c2bfb 100644 --- a/packages/vscode-extension/src/project/project.ts +++ b/packages/vscode-extension/src/project/project.ts @@ -215,6 +215,30 @@ export class Project return this.deviceSession.captureAndStopRecording(); } + async startProfilingCPU() { + if (this.deviceSession) { + await this.deviceSession.startProfilingCPU(); + } else { + throw new Error("No device session available"); + } + } + + async stopProfilingCPU() { + if (this.deviceSession) { + await this.deviceSession.stopProfilingCPU(); + } else { + throw new Error("No device session available"); + } + } + + onProfilingCPUStarted(event: DebugSessionCustomEvent): void { + this.eventEmitter.emit("isProfilingCPU", true); + } + + onProfilingCPUStopped(event: DebugSessionCustomEvent): void { + this.eventEmitter.emit("isProfilingCPU", false); + } + async captureAndStopRecording() { const recording = await this.stopRecording(); await this.utils.saveMultimedia(recording); diff --git a/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx b/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx index 09da27f40..aa9432d62 100644 --- a/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx +++ b/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx @@ -45,7 +45,7 @@ function DevToolCheckbox({ } function ToolsDropdown({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) { - const { project, toolsState } = useProject(); + const { project, toolsState, isProfilingCPU } = useProject(); const toolEntries = Object.entries(toolsState).map(([key, tool]) => { return ( @@ -82,6 +82,13 @@ function ToolsDropdown({ children, disabled }: { children: React.ReactNode; disa Learn more )} + + isProfilingCPU ? project.stopProfilingCPU() : project.startProfilingCPU() + }> + {isProfilingCPU ? "Stop JS Profiler" : "Start JS Profiler"} + diff --git a/packages/vscode-extension/src/webview/providers/ProjectProvider.tsx b/packages/vscode-extension/src/webview/providers/ProjectProvider.tsx index 8b5311f34..0d7f49070 100644 --- a/packages/vscode-extension/src/webview/providers/ProjectProvider.tsx +++ b/packages/vscode-extension/src/webview/providers/ProjectProvider.tsx @@ -28,6 +28,7 @@ interface ProjectContextProps { replayData: MultimediaData | undefined; setReplayData: Dispatch>; isRecording: boolean; + isProfilingCPU: boolean; } const defaultProjectState: ProjectState = { @@ -61,6 +62,7 @@ const ProjectContext = createContext({ replayData: undefined, setReplayData: () => {}, isRecording: false, + isProfilingCPU: false, }); export default function ProjectProvider({ children }: PropsWithChildren) { @@ -69,6 +71,7 @@ export default function ProjectProvider({ children }: PropsWithChildren) { const [toolsState, setToolsState] = useState({}); const [hasActiveLicense, setHasActiveLicense] = useState(true); const [isRecording, setIsRecording] = useState(false); + const [isProfilingCPU, setIsProfilingCPU] = useState(false); const [replayData, setReplayData] = useState(undefined); useEffect(() => { @@ -87,6 +90,8 @@ export default function ProjectProvider({ children }: PropsWithChildren) { project.addListener("isRecording", setIsRecording); project.addListener("replayDataCreated", setReplayData); + project.addListener("isProfilingCPU", setIsProfilingCPU); + return () => { project.removeListener("projectStateChanged", setProjectState); project.removeListener("deviceSettingsChanged", setDeviceSettings); @@ -105,6 +110,7 @@ export default function ProjectProvider({ children }: PropsWithChildren) { replayData, setReplayData, isRecording, + isProfilingCPU, }; }, [ projectState, @@ -115,6 +121,7 @@ export default function ProjectProvider({ children }: PropsWithChildren) { replayData, setReplayData, isRecording, + isProfilingCPU, ]); return {children}; From feab8b7a31823c51665652baaa6f2c766307c1f6 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Wed, 26 Feb 2025 23:21:54 +0100 Subject: [PATCH 2/8] Add CPU profiling support with profile file handling and extension recommendation --- .../vscode-extension/src/project/project.ts | 59 ++++++++- .../src/webview/components/ToolsDropdown.tsx | 18 +-- .../src/webview/components/UrlSelect.css | 6 +- .../src/webview/styles/theme.css | 3 +- .../src/webview/views/PreviewView.css | 36 +++++- .../src/webview/views/PreviewView.tsx | 119 ++++++++++-------- 6 files changed, 176 insertions(+), 65 deletions(-) diff --git a/packages/vscode-extension/src/project/project.ts b/packages/vscode-extension/src/project/project.ts index c680c2bfb..9bad349ac 100644 --- a/packages/vscode-extension/src/project/project.ts +++ b/packages/vscode-extension/src/project/project.ts @@ -1,6 +1,15 @@ import { EventEmitter } from "stream"; import os from "os"; -import { env, Disposable, commands, workspace, window, DebugSessionCustomEvent } from "vscode"; +import { + env, + Disposable, + commands, + workspace, + window, + DebugSessionCustomEvent, + Uri, + extensions, +} from "vscode"; import _ from "lodash"; import stripAnsi from "strip-ansi"; import { minimatch } from "minimatch"; @@ -41,6 +50,8 @@ import { import { getTelemetryReporter } from "../utilities/telemetry"; import { ToolKey, ToolsManager } from "./tools"; import { UtilsInterface } from "../common/utils"; +import path from "path"; +import fs from "fs"; const DEVICE_SETTINGS_KEY = "device_settings_v4"; @@ -235,8 +246,52 @@ export class Project this.eventEmitter.emit("isProfilingCPU", true); } - onProfilingCPUStopped(event: DebugSessionCustomEvent): void { + async onProfilingCPUStopped(event: DebugSessionCustomEvent) { this.eventEmitter.emit("isProfilingCPU", false); + + // Handle the profile file if a file path is provided + if (event.body && event.body.filePath) { + const tempFilePath = event.body.filePath; + + // Show save dialog to save the profile file to the workspace folder: + let defaultUri = Uri.file(tempFilePath); + const workspaceFolder = workspace.workspaceFolders?.[0]; + if (workspaceFolder) { + defaultUri = Uri.file(path.join(workspaceFolder.uri.fsPath, path.basename(tempFilePath))); + } + + const saveDialog = await window.showSaveDialog({ + defaultUri, + filters: { + "CPU Profile": ["cpuprofile"], + }, + }); + + if (saveDialog) { + await fs.promises.copyFile(tempFilePath, saveDialog.fsPath); + commands.executeCommand("vscode.open", Uri.file(saveDialog.fsPath)); + + // verify whether flame chart visualizer extension is installed + // flame chart visualizer is not necessary to open the cpuprofile file, but when it is installed, + // the user can use the flame button from cpuprofile view to visualize it differently + const flameChartExtension = extensions.getExtension("ms-vscode.vscode-js-profile-flame"); + if (!flameChartExtension) { + window + .showInformationMessage( + "Flame Chart Visualizer extension is not installed. It is recommended to install it for better profiling insights.", + "Install Now" + ) + .then((action) => { + if (action === "Install Now") { + commands.executeCommand( + "workbench.extensions.search", + "ms-vscode.vscode-js-profile-flame" + ); + } + }); + } + } + } } async captureAndStopRecording() { diff --git a/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx b/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx index aa9432d62..d66046775 100644 --- a/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx +++ b/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx @@ -9,6 +9,7 @@ import "./ToolsDropdown.css"; import { useProject } from "../providers/ProjectProvider"; import IconButton from "./shared/IconButton"; import { DropdownMenuRoot } from "./DropdownMenuRoot"; +import Label from "./shared/Label"; interface DevToolCheckboxProps { label: string; @@ -75,6 +76,16 @@ function ToolsDropdown({ children, disabled }: { children: React.ReactNode; disa className="dropdown-menu-content device-settings-content" onCloseAutoFocus={(e) => e.preventDefault()}>

Tools

+ + + isProfilingCPU ? project.stopProfilingCPU() : project.startProfilingCPU() + }> + + {isProfilingCPU ? "Stop JS CPU Profiler" : "Start JS CPU Profiler"} + + {toolEntries} {toolEntries.length === 0 && (
@@ -82,13 +93,6 @@ function ToolsDropdown({ children, disabled }: { children: React.ReactNode; disa Learn more
)} - - isProfilingCPU ? project.stopProfilingCPU() : project.startProfilingCPU() - }> - {isProfilingCPU ? "Stop JS Profiler" : "Start JS Profiler"} - diff --git a/packages/vscode-extension/src/webview/components/UrlSelect.css b/packages/vscode-extension/src/webview/components/UrlSelect.css index 416b280ed..b2609303c 100644 --- a/packages/vscode-extension/src/webview/components/UrlSelect.css +++ b/packages/vscode-extension/src/webview/components/UrlSelect.css @@ -1,6 +1,5 @@ :root { - --url-select-min-width: 130px; - --url-select-max-width: 300px; + --url-select-max-width: 200px; } .url-select-trigger { @@ -19,7 +18,8 @@ color: var(--swm-url-select); background-color: var(--swm-url-select-background); user-select: none; - min-width: var(--url-select-min-width); + max-width: var(--url-select-max-width); + flex: 1; } .url-select-trigger:hover { background-color: var(--swm-url-select-hover-background); diff --git a/packages/vscode-extension/src/webview/styles/theme.css b/packages/vscode-extension/src/webview/styles/theme.css index 542afcdd7..7c4bdb516 100644 --- a/packages/vscode-extension/src/webview/styles/theme.css +++ b/packages/vscode-extension/src/webview/styles/theme.css @@ -570,7 +570,8 @@ body[data-use-code-theme="true"] { --swm-button-counter-background: var(--vscode-activityBarBadge-background); --swm-button-counter-border: var(--vscode-contrastBorder); - --swm-button-recording-on-background: var(--navy-light-60); + --swm-button-recording-on: var(--vscode-activityBarBadge-foreground); + --swm-button-recording-on-background: var(--vscode-activityBarBadge-background); --swm-button-replay-hover: var(--navy-light-transparent); /* Tooltip */ diff --git a/packages/vscode-extension/src/webview/views/PreviewView.css b/packages/vscode-extension/src/webview/views/PreviewView.css index b3d513bc5..0949ddb90 100644 --- a/packages/vscode-extension/src/webview/views/PreviewView.css +++ b/packages/vscode-extension/src/webview/views/PreviewView.css @@ -1,12 +1,22 @@ .button-group-top, .button-group-bottom { display: flex; - align-items: center; + justify-content: space-between; width: 100%; margin: 8px; gap: 4px; } +.button-group-top-left, +.button-group-top-right { + display: flex; + flex-direction: row; +} + +.button-group-top-left { + flex: 1; +} + .icons-rewind:before { width: 0px; margin: -4px; @@ -71,12 +81,33 @@ .recording-rec-indicator { display: flex; align-items: center; - color: white; + color: var(--swm-button-recording-on); font-size: 120%; filter: blur(0.4px); font-family: "Courier new", fixed; } +.icon-red { + color: red; + font-size: 24px; + scale: 2.8; +} + +@keyframes pulse { + 0% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.6; + transform: scale(0.85); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + .recording-rec-dot { width: 13px; height: 13px; @@ -84,6 +115,7 @@ align-self: center; border-radius: 50%; margin-right: 6px; + animation: pulse 1.5s ease-in-out infinite; } @media (max-width: 385px) { diff --git a/packages/vscode-extension/src/webview/views/PreviewView.tsx b/packages/vscode-extension/src/webview/views/PreviewView.tsx index fa6aba21f..aeb4bc1a5 100644 --- a/packages/vscode-extension/src/webview/views/PreviewView.tsx +++ b/packages/vscode-extension/src/webview/views/PreviewView.tsx @@ -42,6 +42,7 @@ function PreviewView() { hasActiveLicense, replayData, isRecording, + isProfilingCPU, setReplayData, } = useProject(); const { showDismissableError } = useUtils(); @@ -136,6 +137,10 @@ function PreviewView() { } } + function stopProfilingCPU() { + project.stopProfilingCPU(); + } + async function handleReplay() { try { await project.captureReplay(); @@ -166,64 +171,78 @@ function PreviewView() { return (
- -
- - - - - - - {isRecording ? ( -
+
+ +
+
+ {isProfilingCPU && ( +
- {recordingTimeFormat} -
- ) : ( - + Profiling CPU  +
)} - - {showReplayButton && ( + + + + + - + {isRecording ? ( +
+
+ {recordingTimeFormat} +
+ ) : ( + + )} - )} - - - - { - setLogCounter(0); - project.focusDebugConsole(); - }} - tooltip={{ - label: "Open logs panel", - }} - disabled={hasNoDevices}> - - - - - + {showReplayButton && ( + + + + )} + + - + { + setLogCounter(0); + project.focusDebugConsole(); + }} + tooltip={{ + label: "Open logs panel", + }} + disabled={hasNoDevices}> + + + + + + + +
{selectedDevice && initialized ? ( From 32079a341e727b2d1d3d90fe04982b4d773f10f0 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Wed, 26 Feb 2025 23:56:00 +0100 Subject: [PATCH 3/8] Improve CPU profiling button styling and interaction --- .../src/webview/views/PreviewView.css | 6 ++--- .../src/webview/views/PreviewView.tsx | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/vscode-extension/src/webview/views/PreviewView.css b/packages/vscode-extension/src/webview/views/PreviewView.css index 0949ddb90..205614caa 100644 --- a/packages/vscode-extension/src/webview/views/PreviewView.css +++ b/packages/vscode-extension/src/webview/views/PreviewView.css @@ -73,17 +73,17 @@ .button-recording-on, .button-recording-on:hover { background-color: var(--swm-button-recording-on-background); + color: var(--swm-button-recording-on); + border: 1px solid var(--swm-button-counter-border); border-radius: 10px; width: auto; - padding: 0 5px 0 8px; + padding: 0 8px; } .recording-rec-indicator { display: flex; align-items: center; - color: var(--swm-button-recording-on); font-size: 120%; - filter: blur(0.4px); font-family: "Courier new", fixed; } diff --git a/packages/vscode-extension/src/webview/views/PreviewView.tsx b/packages/vscode-extension/src/webview/views/PreviewView.tsx index aeb4bc1a5..88a7d5bb9 100644 --- a/packages/vscode-extension/src/webview/views/PreviewView.tsx +++ b/packages/vscode-extension/src/webview/views/PreviewView.tsx @@ -175,17 +175,20 @@ function PreviewView() {
- {isProfilingCPU && ( - -
- Profiling CPU  - - )} + + {isProfilingCPU && ( + <> +
+ Profiling CPU + + )} + From 8f6e296b2fcefe1c63f00c24d0325cee45ae1923 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Thu, 27 Feb 2025 00:21:49 +0100 Subject: [PATCH 4/8] Organize tools --- .../vscode-extension/src/common/Project.ts | 8 ++- .../src/webview/components/ToolsDropdown.tsx | 54 ++++++++++--------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/packages/vscode-extension/src/common/Project.ts b/packages/vscode-extension/src/common/Project.ts index 8ac24d969..598867f10 100644 --- a/packages/vscode-extension/src/common/Project.ts +++ b/packages/vscode-extension/src/common/Project.ts @@ -16,8 +16,14 @@ export type DeviceSettings = { showTouches: boolean; }; +export type ToolState = { + enabled: boolean; + panelAvailable: boolean; + label: string; +}; + export type ToolsState = { - [key: string]: { enabled: boolean; panelAvailable: boolean; label: string }; + [key: string]: ToolState; }; export type ProjectState = { diff --git a/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx b/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx index d66046775..1e8601f03 100644 --- a/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx +++ b/packages/vscode-extension/src/webview/components/ToolsDropdown.tsx @@ -10,6 +10,7 @@ import { useProject } from "../providers/ProjectProvider"; import IconButton from "./shared/IconButton"; import { DropdownMenuRoot } from "./DropdownMenuRoot"; import Label from "./shared/Label"; +import { ProjectInterface, ToolState } from "../../common/Project"; interface DevToolCheckboxProps { label: string; @@ -45,26 +46,36 @@ function DevToolCheckbox({ ); } +function ToolsList({ + project, + tools, +}: { + project: ProjectInterface; + tools: [string, ToolState][]; +}) { + return tools.map(([key, tool]) => ( + { + await project.updateToolEnabledState(key, checked); + if (checked) { + project.openTool(key); + } + }} + onSelect={() => project.openTool(key)} + /> + )); +} + function ToolsDropdown({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) { const { project, toolsState, isProfilingCPU } = useProject(); - const toolEntries = Object.entries(toolsState).map(([key, tool]) => { - return ( - { - await project.updateToolEnabledState(key, checked); - if (checked) { - project.openTool(key); - } - }} - onSelect={() => project.openTool(key)} - /> - ); - }); + const allTools = Object.entries(toolsState); + const panelTools = allTools.filter(([key, tool]) => tool.panelAvailable); + const nonPanelTools = allTools.filter(([key, tool]) => !tool.panelAvailable); return ( @@ -85,14 +96,9 @@ function ToolsDropdown({ children, disabled }: { children: React.ReactNode; disa {isProfilingCPU ? "Stop JS CPU Profiler" : "Start JS CPU Profiler"} + - {toolEntries} - {toolEntries.length === 0 && ( -
- Your app doesn't have any supported dev tools configured.  - Learn more -
- )} +
From eacc6307e249816c101bd5bbda9904ad462c8a15 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Fri, 28 Feb 2025 16:34:08 +0100 Subject: [PATCH 5/8] Reviwe comments --- packages/vscode-extension/src/project/project.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vscode-extension/src/project/project.ts b/packages/vscode-extension/src/project/project.ts index 3c389f2e5..da1ccffd6 100644 --- a/packages/vscode-extension/src/project/project.ts +++ b/packages/vscode-extension/src/project/project.ts @@ -332,13 +332,14 @@ export class Project // the user can use the flame button from cpuprofile view to visualize it differently const flameChartExtension = extensions.getExtension("ms-vscode.vscode-js-profile-flame"); if (!flameChartExtension) { + const GO_TO_EXTENSION_BUTTON = "Go to Extension"; window .showInformationMessage( "Flame Chart Visualizer extension is not installed. It is recommended to install it for better profiling insights.", - "Install Now" + GO_TO_EXTENSION_BUTTON ) .then((action) => { - if (action === "Install Now") { + if (action === GO_TO_EXTENSION_BUTTON) { commands.executeCommand( "workbench.extensions.search", "ms-vscode.vscode-js-profile-flame" From 374cfee799633047b171099711aaf891275a3257 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Mon, 3 Mar 2025 20:27:50 +0100 Subject: [PATCH 6/8] Stashing work on source maps --- .../src/debugging/DebugAdapter.ts | 6 +- .../vscode-extension/src/debugging/cdp.ts | 30 +++++++ .../src/debugging/cpuProfiler.ts | 80 +++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 packages/vscode-extension/src/debugging/cpuProfiler.ts diff --git a/packages/vscode-extension/src/debugging/DebugAdapter.ts b/packages/vscode-extension/src/debugging/DebugAdapter.ts index 20f26830b..10bd885c0 100644 --- a/packages/vscode-extension/src/debugging/DebugAdapter.ts +++ b/packages/vscode-extension/src/debugging/DebugAdapter.ts @@ -1,7 +1,7 @@ -import { DebugConfiguration } from "vscode"; import fs from "fs"; import path from "path"; import os from "os"; +import { DebugConfiguration } from "vscode"; import { DebugSession, InitializedEvent, @@ -37,6 +37,7 @@ import { SourceMapsRegistry } from "./SourceMapsRegistry"; import { BreakpointsController } from "./BreakpointsController"; import { CDPSession } from "./CDPSession"; import getArraySlots from "./templates/getArraySlots"; +import { annotateLocations } from "./cpuProfiler"; function typeToCategory(type: string) { switch (type) { @@ -634,7 +635,8 @@ export class DebugAdapter extends DebugSession { const result = await this.cdpSession.sendCDPMessage("Profiler.stop", {}); const fileName = `profile-${Date.now()}.cpuprofile`; const filePath = path.join(os.tmpdir(), fileName); - await fs.promises.writeFile(filePath, JSON.stringify(result.profile)); + const annotatedProfile = annotateLocations(result.profile, this.sourceMapRegistry); + await fs.promises.writeFile(filePath, JSON.stringify(annotatedProfile)); this.sendEvent(new Event("RNIDE_profilingCPUStopped", { filePath })); this.sendResponse(response); } else { diff --git a/packages/vscode-extension/src/debugging/cdp.ts b/packages/vscode-extension/src/debugging/cdp.ts index cd67b3764..eddae3b68 100644 --- a/packages/vscode-extension/src/debugging/cdp.ts +++ b/packages/vscode-extension/src/debugging/cdp.ts @@ -31,6 +31,36 @@ export type CDPDebuggerScope = { object: CDPRemoteObject & { type: "object" }; }; +export type CDPProfile = { + nodes: CDPProfileNode[]; + startTime: number; + endTime: number; + samples?: number[]; + timeDeltas?: number[]; +}; + +export type CDPProfileNode = { + id: number; + callFrame: CDPCallFrame; + hitCount?: number; + children?: number[]; + deoptReason?: string; + positionTicks?: CDPPositionTick[]; +}; + +export type CDPPositionTick = { + line: number; // 1-based according to the spec, WTF! + ticks: number; +}; + +export type CDPCallFrame = { + functionName: string; + scriptId: string; + url: string; + lineNumber: number; // 0-based + columnNumber: number; // 0-based +}; + export function inferDAPVariableValueForCDPRemoteObject(cdpValue: CDPRemoteObject): string { switch (cdpValue.type) { case "undefined": diff --git a/packages/vscode-extension/src/debugging/cpuProfiler.ts b/packages/vscode-extension/src/debugging/cpuProfiler.ts new file mode 100644 index 000000000..56d1f39ff --- /dev/null +++ b/packages/vscode-extension/src/debugging/cpuProfiler.ts @@ -0,0 +1,80 @@ +import { CDPCallFrame, CDPProfile } from "./cdp"; +import { SourceMapsRegistry } from "./SourceMapsRegistry"; +import { Source } from "@vscode/debugadapter"; + +type FrameKey = string; + +type DAPAnnotationLocation = { + callFrame: CDPCallFrame; + locations: DAPSourceLocation[]; +}; + +type DAPSourceLocation = { + lineNumber: number; + columnNumber: number; + source: Source; + relativePath?: string; +}; + +function callFrameKey(callFrame: CDPCallFrame): FrameKey { + return [callFrame.scriptId, callFrame.lineNumber, callFrame.columnNumber].join(":") as FrameKey; +} + +/** + * Expands the providing CPU Profile data with VSCode specific fields that + * are used by the VSCode CPU Profile Visualizer extension. The additional fields + * are used to provide the original source location for each node in the profile. + */ +export function annotateLocations(profile: CDPProfile, sourceMapsRegistry: SourceMapsRegistry) { + let locationIdCounter = 0; + const locationIds = new Map(); + const locations: DAPAnnotationLocation[] = []; + + const nodes = profile.nodes.map((node) => { + const key = callFrameKey(node.callFrame); + let locationId = locationIds.get(key); + + if (!locationId) { + locationId = locationIdCounter++; + locationIds.set(key, locationId); + + const origPosition = sourceMapsRegistry.findOriginalPosition( + node.callFrame.scriptId, + node.callFrame.lineNumber + 1, + node.callFrame.columnNumber + ); + + if (origPosition.sourceURL !== "__source__") { + // TODO: we should find a better way to communicate that the source location cannot be resolved + locations.push({ + callFrame: node.callFrame, + locations: [ + { + lineNumber: origPosition.lineNumber1Based - 1, + columnNumber: origPosition.columnNumber0Based, + source: new Source(origPosition.sourceURL, origPosition.sourceURL), + }, + ], + }); + } else { + locations.push({ + callFrame: node.callFrame, + locations: [], + }); + } + } + return { + ...node, + locationId, // VScode specific field: https://github.com/microsoft/vscode-js-profile-visualizer/blob/main/packages/vscode-js-profile-core/src/cpu/types.ts#L12 + }; + }); + + return { + ...profile, + nodes, + $vscode: { + // VSCode especific fields: https://github.com/microsoft/vscode-js-profile-visualizer/blob/main/packages/vscode-js-profile-core/src/cpu/types.ts#L20 + locations: locations, + }, + }; +} From 6874a40349c8d50e565af4887a1da023b200fe1c Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Mon, 3 Mar 2025 21:59:16 +0100 Subject: [PATCH 7/8] Use url's instead of scriptId for proper frame translation --- packages/vscode-extension/src/debugging/cpuProfiler.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vscode-extension/src/debugging/cpuProfiler.ts b/packages/vscode-extension/src/debugging/cpuProfiler.ts index 56d1f39ff..527858fd8 100644 --- a/packages/vscode-extension/src/debugging/cpuProfiler.ts +++ b/packages/vscode-extension/src/debugging/cpuProfiler.ts @@ -39,7 +39,11 @@ export function annotateLocations(profile: CDPProfile, sourceMapsRegistry: Sourc locationIds.set(key, locationId); const origPosition = sourceMapsRegistry.findOriginalPosition( - node.callFrame.scriptId, + // Apparently, hermes seems to report a different scriptId here – for a fresh app that just started, + // we get scriptId "2" for the main bundle, but in profile node's callFrame, "0" is reported. + // for this reasone, we rely on the script URL rather than scriptIde here, but we should revisit this + // to investigate if this is a bug in hermes or something that can be addressed. + node.callFrame.url, node.callFrame.lineNumber + 1, node.callFrame.columnNumber ); From 8ff9320a11df56c8595e13333524fcce872f81c9 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Mon, 3 Mar 2025 22:03:40 +0100 Subject: [PATCH 8/8] lint --- packages/vscode-extension/src/debugging/cpuProfiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode-extension/src/debugging/cpuProfiler.ts b/packages/vscode-extension/src/debugging/cpuProfiler.ts index 527858fd8..3c4e5dc9c 100644 --- a/packages/vscode-extension/src/debugging/cpuProfiler.ts +++ b/packages/vscode-extension/src/debugging/cpuProfiler.ts @@ -1,6 +1,6 @@ +import { Source } from "@vscode/debugadapter"; import { CDPCallFrame, CDPProfile } from "./cdp"; import { SourceMapsRegistry } from "./SourceMapsRegistry"; -import { Source } from "@vscode/debugadapter"; type FrameKey = string;