diff --git a/openhands/runtime/utils/runtime_templates/Dockerfile.j2 b/openhands/runtime/utils/runtime_templates/Dockerfile.j2 index c0d915f75e..233e292097 100644 --- a/openhands/runtime/utils/runtime_templates/Dockerfile.j2 +++ b/openhands/runtime/utils/runtime_templates/Dockerfile.j2 @@ -64,11 +64,17 @@ RUN if [ -z "${RELEASE_TAG}" ]; then \ if [ -d "${OPENVSCODE_SERVER_ROOT}" ]; then rm -rf "${OPENVSCODE_SERVER_ROOT}"; fi && \ mv ${RELEASE_TAG}-linux-${arch} ${OPENVSCODE_SERVER_ROOT} && \ cp ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/openvscode-server ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/code && \ - rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz && \ - # Install our custom extension - mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world && \ + rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz + +{% endmacro %} + +{% macro install_vscode_extensions() %} +# Install our custom extension +RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world && \ cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/hello-world/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world/ +RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor && \ + cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/memory-monitor/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor/ {% endmacro %} {% macro install_dependencies() %} @@ -147,6 +153,7 @@ RUN chmod a+rwx /openhands/code/openhands/__init__.py # ================================================================ {% if build_from_versioned %} {{ install_dependencies() }} +{{ install_vscode_extensions() }} {% endif %} # Install extra dependencies if specified diff --git a/openhands/runtime/utils/vscode-extensions/memory-monitor/README.md b/openhands/runtime/utils/vscode-extensions/memory-monitor/README.md new file mode 100644 index 0000000000..d66b746696 --- /dev/null +++ b/openhands/runtime/utils/vscode-extensions/memory-monitor/README.md @@ -0,0 +1,48 @@ +# OpenHands Memory Monitor + +A VSCode extension for monitoring system and process memory usage in real-time. + +## Features + +- **Real-time Memory Monitoring**: Displays current memory usage in the status bar +- **Detailed Memory Information**: View detailed memory statistics in a graphical interface +- **Process Monitoring**: See top processes by memory usage +- **Memory Usage History**: Track memory usage over time with interactive charts +- **Cross-Platform Support**: Works on Windows, macOS, and Linux + +## Usage + +The extension automatically starts monitoring memory usage when VSCode is launched. You can interact with it in the following ways: + +### Status Bar Indicator + +A memory usage indicator is displayed in the status bar showing the current system memory usage percentage. Click on this indicator to open the detailed memory view. + +### Commands + +The following commands are available in the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P`): + +- **Start Memory Monitor**: Start monitoring memory usage +- **Stop Memory Monitor**: Stop monitoring memory usage +- **Show Memory Details**: Open the detailed memory view + +## Detailed Memory View + +The detailed memory view provides comprehensive information about: + +1. **System Memory**: Total, used, and free memory +2. **Process Memory**: Memory usage of the VSCode extension host process +3. **Memory History**: Chart showing memory usage over time +4. **Top Processes**: List of processes using the most memory + +## Development + +This extension is part of the OpenHands project. To modify or extend it: + +1. Make changes to the source files +2. Test the extension in a development VSCode instance +3. Package the extension for distribution + +## License + +This extension is licensed under the MIT license. \ No newline at end of file diff --git a/openhands/runtime/utils/vscode-extensions/memory-monitor/extension.js b/openhands/runtime/utils/vscode-extensions/memory-monitor/extension.js new file mode 100644 index 0000000000..a0ba135c7c --- /dev/null +++ b/openhands/runtime/utils/vscode-extensions/memory-monitor/extension.js @@ -0,0 +1,42 @@ +const vscode = require('vscode'); +const MemoryMonitor = require('./memory_monitor'); + +function activate(context) { + // Create memory monitor instance + const memoryMonitor = new MemoryMonitor(); + + // Store the context in the memory monitor + memoryMonitor.context = context; + + // Register memory monitor start command + let startMonitorCommand = vscode.commands.registerCommand('openhands-memory-monitor.startMemoryMonitor', function () { + memoryMonitor.start(); + }); + + // Register memory monitor stop command + let stopMonitorCommand = vscode.commands.registerCommand('openhands-memory-monitor.stopMemoryMonitor', function () { + memoryMonitor.stop(); + }); + + // Register memory details command + let showMemoryDetailsCommand = vscode.commands.registerCommand('openhands-memory-monitor.showMemoryDetails', function () { + memoryMonitor.showDetails(); + }); + + // Add all commands to subscriptions + context.subscriptions.push(startMonitorCommand); + context.subscriptions.push(stopMonitorCommand); + context.subscriptions.push(showMemoryDetailsCommand); + + // Start memory monitoring by default + memoryMonitor.start(); +} + +function deactivate() { + // Clean up resources if needed +} + +module.exports = { + activate, + deactivate +} \ No newline at end of file diff --git a/openhands/runtime/utils/vscode-extensions/memory-monitor/memory_monitor.js b/openhands/runtime/utils/vscode-extensions/memory-monitor/memory_monitor.js new file mode 100644 index 0000000000..b8ebe21546 --- /dev/null +++ b/openhands/runtime/utils/vscode-extensions/memory-monitor/memory_monitor.js @@ -0,0 +1,343 @@ +const os = require('os'); +const vscode = require('vscode'); +const ProcessMonitor = require('./process_monitor'); + +class MemoryMonitor { + constructor() { + this.isMonitoring = false; + this.intervalId = null; + this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); + this.statusBarItem.command = 'openhands-memory-monitor.showMemoryDetails'; + this.processMonitor = new ProcessMonitor(); + this.memoryHistory = []; + this.maxHistoryLength = 60; // Keep 5 minutes of data with 5-second intervals + this.context = null; // Will be set in activate + } + + start(interval = 5000) { + if (this.isMonitoring) { + return; + } + + this.isMonitoring = true; + this.statusBarItem.show(); + + // Initial update + this.updateMemoryInfo(); + + // Set interval for updates + this.intervalId = setInterval(() => { + this.updateMemoryInfo(); + }, interval); + + vscode.window.showInformationMessage('Memory monitoring started'); + } + + stop() { + if (!this.isMonitoring) { + return; + } + + this.isMonitoring = false; + clearInterval(this.intervalId); + this.statusBarItem.hide(); + + vscode.window.showInformationMessage('Memory monitoring stopped'); + } + + updateMemoryInfo() { + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const usedMem = totalMem - freeMem; + + // Calculate memory usage percentage + const memUsagePercent = Math.round((usedMem / totalMem) * 100); + + // Format memory values to MB + const usedMemMB = Math.round(usedMem / (1024 * 1024)); + const totalMemMB = Math.round(totalMem / (1024 * 1024)); + + // Update status bar + this.statusBarItem.text = `$(pulse) Mem: ${memUsagePercent}%`; + this.statusBarItem.tooltip = `Memory Usage: ${usedMemMB}MB / ${totalMemMB}MB`; + + // Store memory data in history + this.memoryHistory.push({ + timestamp: new Date(), + usedMemMB, + totalMemMB, + memUsagePercent, + processMemory: process.memoryUsage() + }); + + // Limit history length + if (this.memoryHistory.length > this.maxHistoryLength) { + this.memoryHistory.shift(); + } + } + + showDetails() { + // Create and show a webview panel with detailed memory information + const panel = vscode.window.createWebviewPanel( + 'memoryMonitor', + 'Memory Monitor', + vscode.ViewColumn.One, + { + enableScripts: true + } + ); + + // Set up message handler for real-time updates + panel.webview.onDidReceiveMessage( + message => { + if (message.command === 'requestUpdate') { + this.updateWebviewContent(panel); + } + }, + undefined, + this.context ? this.context.subscriptions : [] + ); + + // Initial update + this.updateWebviewContent(panel); + + // Handle panel disposal + panel.onDidDispose(() => { + // Clean up any resources if needed + }, null, this.context ? this.context.subscriptions : []); + } + + updateWebviewContent(panel) { + // Get system memory info + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const usedMem = totalMem - freeMem; + + // Format memory values + const usedMemMB = Math.round(usedMem / (1024 * 1024)); + const freeMemMB = Math.round(freeMem / (1024 * 1024)); + const totalMemMB = Math.round(totalMem / (1024 * 1024)); + + // Get process memory usage + const processMemory = process.memoryUsage(); + const rss = Math.round(processMemory.rss / (1024 * 1024)); + const heapTotal = Math.round(processMemory.heapTotal / (1024 * 1024)); + const heapUsed = Math.round(processMemory.heapUsed / (1024 * 1024)); + + // Get process information + this.processMonitor.getProcessInfo((error, processInfo) => { + if (error) { + console.error('Error getting process info:', error); + return; + } + + // Create HTML content for the webview + const htmlContent = this.generateHtmlReport( + usedMemMB, freeMemMB, totalMemMB, + rss, heapTotal, heapUsed, + processInfo + ); + + // Set the webview's HTML content + panel.webview.html = htmlContent; + }); + } + + generateHtmlReport(usedMemMB, freeMemMB, totalMemMB, rss, heapTotal, heapUsed, processInfo) { + // Create memory usage history data for chart + const memoryLabels = this.memoryHistory.map((entry, index) => index); + const memoryData = this.memoryHistory.map(entry => entry.memUsagePercent); + const heapData = this.memoryHistory.map(entry => + Math.round(entry.processMemory.heapUsed / (1024 * 1024)) + ); + + // Format process info table + let processTable = ''; + if (processInfo && processInfo.processes) { + processTable = ` +

Top Processes by Memory Usage

+ + + + + + + + ${processInfo.processes.map(proc => ` + + + + + + + `).join('')} +
PIDMemory %CPU %Command
${proc.pid}${proc.memPercent}%${proc.cpuPercent || 'N/A'}${proc.cmd}
+ `; + } + + return ` + + + + + + Memory Monitor + + + + +

Memory Monitor

+ +
+

System Memory

+
+
+
Used Memory
+
${usedMemMB} MB
+
+
+
Free Memory
+
${freeMemMB} MB
+
+
+
Total Memory
+
${totalMemMB} MB
+
+
+
Usage
+
${Math.round(usedMemMB / totalMemMB * 100)}%
+
+
+
+ +
+

Process Memory (VSCode Extension Host)

+
+
+
RSS
+
${rss} MB
+
+
+
Heap Used
+
${heapUsed} MB
+
+
+
Heap Total
+
${heapTotal} MB
+
+
+
+ +
+

Memory Usage History

+
+ +
+
+ +
+ ${processTable} +
+ + + + + `; + } +} + +module.exports = MemoryMonitor; \ No newline at end of file diff --git a/openhands/runtime/utils/vscode-extensions/memory-monitor/package.json b/openhands/runtime/utils/vscode-extensions/memory-monitor/package.json new file mode 100644 index 0000000000..eef3d2d25e --- /dev/null +++ b/openhands/runtime/utils/vscode-extensions/memory-monitor/package.json @@ -0,0 +1,50 @@ +{ + "name": "openhands-memory-monitor", + "displayName": "OpenHands Memory Monitor", + "description": "A VSCode extension for monitoring system and process memory usage", + "version": "0.1.0", + "publisher": "openhands", + "engines": { + "vscode": "^1.94.0" + }, + "categories": [ + "Other", + "Monitoring" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./extension.js", + "contributes": { + "commands": [ + { + "command": "openhands-memory-monitor.startMemoryMonitor", + "title": "Start Memory Monitor" + }, + { + "command": "openhands-memory-monitor.stopMemoryMonitor", + "title": "Stop Memory Monitor" + }, + { + "command": "openhands-memory-monitor.showMemoryDetails", + "title": "Show Memory Details" + } + ], + "menus": { + "commandPalette": [ + { + "command": "openhands-memory-monitor.startMemoryMonitor", + "group": "OpenHands" + }, + { + "command": "openhands-memory-monitor.stopMemoryMonitor", + "group": "OpenHands" + }, + { + "command": "openhands-memory-monitor.showMemoryDetails", + "group": "OpenHands" + } + ] + } + } +} \ No newline at end of file diff --git a/openhands/runtime/utils/vscode-extensions/memory-monitor/process_monitor.js b/openhands/runtime/utils/vscode-extensions/memory-monitor/process_monitor.js new file mode 100644 index 0000000000..c0ea86faf9 --- /dev/null +++ b/openhands/runtime/utils/vscode-extensions/memory-monitor/process_monitor.js @@ -0,0 +1,143 @@ +const { exec } = require('child_process'); +const os = require('os'); + +class ProcessMonitor { + constructor() { + this.platform = os.platform(); + } + + /** + * Get detailed process information based on the platform + * @param {Function} callback - Callback function to receive process data + */ + getProcessInfo(callback) { + if (this.platform === 'linux') { + this.getLinuxProcessInfo(callback); + } else if (this.platform === 'darwin') { + this.getMacProcessInfo(callback); + } else if (this.platform === 'win32') { + this.getWindowsProcessInfo(callback); + } else { + callback(new Error(`Unsupported platform: ${this.platform}`), null); + } + } + + /** + * Get process information on Linux + * @param {Function} callback - Callback function to receive process data + */ + getLinuxProcessInfo(callback) { + // Get top processes by memory usage + exec('ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%mem | head -n 11', (error, stdout) => { + if (error) { + callback(error, null); + return; + } + + // Parse the output + const lines = stdout.trim().split('\n'); + const header = lines[0]; + const processes = lines.slice(1).map(line => { + const parts = line.trim().split(/\s+/); + const pid = parts[0]; + const ppid = parts[1]; + const memPercent = parseFloat(parts[parts.length - 2]); + const cpuPercent = parseFloat(parts[parts.length - 1]); + const cmd = parts.slice(2, parts.length - 2).join(' '); + + return { + pid, + ppid, + cmd, + memPercent, + cpuPercent + }; + }); + + callback(null, { header, processes }); + }); + } + + /** + * Get process information on macOS + * @param {Function} callback - Callback function to receive process data + */ + getMacProcessInfo(callback) { + // Similar to Linux but with slightly different ps command + exec('ps -eo pid,ppid,command,%mem,%cpu -r | head -n 11', (error, stdout) => { + if (error) { + callback(error, null); + return; + } + + // Parse the output + const lines = stdout.trim().split('\n'); + const header = lines[0]; + const processes = lines.slice(1).map(line => { + const parts = line.trim().split(/\s+/); + const pid = parts[0]; + const ppid = parts[1]; + const memPercent = parseFloat(parts[parts.length - 2]); + const cpuPercent = parseFloat(parts[parts.length - 1]); + const cmd = parts.slice(2, parts.length - 2).join(' '); + + return { + pid, + ppid, + cmd, + memPercent, + cpuPercent + }; + }); + + callback(null, { header, processes }); + }); + } + + /** + * Get process information on Windows + * @param {Function} callback - Callback function to receive process data + */ + getWindowsProcessInfo(callback) { + // Windows command to get process info + exec('wmic process get ProcessId,ParentProcessId,CommandLine,WorkingSetSize /format:csv', (error, stdout) => { + if (error) { + callback(error, null); + return; + } + + // Parse the CSV output + const lines = stdout.trim().split('\n'); + const header = "PID,PPID,Command,Memory (bytes)"; + + // Skip empty lines and the header + const dataLines = lines.filter(line => line.trim() !== '' && !line.includes('Node,')); + + const processes = dataLines.map(line => { + const parts = line.split(','); + if (parts.length < 4) return null; + + // Last part is the node name, then ProcessId, ParentProcessId, CommandLine, WorkingSetSize + const pid = parts[parts.length - 4]; + const ppid = parts[parts.length - 3]; + const cmd = parts[parts.length - 2]; + const memBytes = parseInt(parts[parts.length - 1], 10); + const memPercent = (memBytes / os.totalmem() * 100).toFixed(1); + + return { + pid, + ppid, + cmd, + memPercent, + memBytes + }; + }).filter(Boolean) + .sort((a, b) => b.memBytes - a.memBytes) + .slice(0, 10); + + callback(null, { header, processes }); + }); + } +} + +module.exports = ProcessMonitor; \ No newline at end of file