Add Memory Monitor VSCode Extension (#6951)

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
Xingyao Wang 2025-02-27 17:02:33 -05:00 committed by GitHub
parent 42332294a1
commit 02bc7de36d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 636 additions and 3 deletions

View File

@ -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

View File

@ -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.

View File

@ -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
}

View File

@ -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 = `
<h3>Top Processes by Memory Usage</h3>
<table>
<tr>
<th>PID</th>
<th>Memory %</th>
<th>CPU %</th>
<th>Command</th>
</tr>
${processInfo.processes.map(proc => `
<tr>
<td>${proc.pid}</td>
<td>${proc.memPercent}%</td>
<td>${proc.cpuPercent || 'N/A'}</td>
<td>${proc.cmd}</td>
</tr>
`).join('')}
</table>
`;
}
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memory Monitor</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
padding: 20px;
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
}
.memory-card {
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 5px;
padding: 15px;
margin-bottom: 20px;
}
.memory-info {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.memory-stat {
flex: 1;
min-width: 200px;
}
.memory-value {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
}
.memory-label {
font-size: 14px;
color: var(--vscode-descriptionForeground);
}
.chart-container {
height: 300px;
margin: 20px 0;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th, td {
text-align: left;
padding: 8px;
border-bottom: 1px solid var(--vscode-panel-border);
}
th {
background-color: var(--vscode-editor-background);
font-weight: bold;
}
</style>
</head>
<body>
<h1>Memory Monitor</h1>
<div class="memory-card">
<h2>System Memory</h2>
<div class="memory-info">
<div class="memory-stat">
<div class="memory-label">Used Memory</div>
<div class="memory-value">${usedMemMB} MB</div>
</div>
<div class="memory-stat">
<div class="memory-label">Free Memory</div>
<div class="memory-value">${freeMemMB} MB</div>
</div>
<div class="memory-stat">
<div class="memory-label">Total Memory</div>
<div class="memory-value">${totalMemMB} MB</div>
</div>
<div class="memory-stat">
<div class="memory-label">Usage</div>
<div class="memory-value">${Math.round(usedMemMB / totalMemMB * 100)}%</div>
</div>
</div>
</div>
<div class="memory-card">
<h2>Process Memory (VSCode Extension Host)</h2>
<div class="memory-info">
<div class="memory-stat">
<div class="memory-label">RSS</div>
<div class="memory-value">${rss} MB</div>
</div>
<div class="memory-stat">
<div class="memory-label">Heap Used</div>
<div class="memory-value">${heapUsed} MB</div>
</div>
<div class="memory-stat">
<div class="memory-label">Heap Total</div>
<div class="memory-value">${heapTotal} MB</div>
</div>
</div>
</div>
<div class="memory-card">
<h2>Memory Usage History</h2>
<div class="chart-container">
<canvas id="memoryChart"></canvas>
</div>
</div>
<div class="memory-card">
${processTable}
</div>
<script>
// Create memory usage chart
const ctx = document.getElementById('memoryChart').getContext('2d');
const memoryChart = new Chart(ctx, {
type: 'line',
data: {
labels: ${JSON.stringify(memoryLabels)},
datasets: [
{
label: 'System Memory Usage (%)',
data: ${JSON.stringify(memoryData)},
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.4
},
{
label: 'Process Heap Usage (MB)',
data: ${JSON.stringify(heapData)},
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
// Set up real-time updates
const vscode = acquireVsCodeApi();
// Request updates every 5 seconds
setInterval(() => {
vscode.postMessage({
command: 'requestUpdate'
});
}, 5000);
</script>
</body>
</html>
`;
}
}
module.exports = MemoryMonitor;

View File

@ -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"
}
]
}
}
}

View File

@ -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;