diff --git a/package-lock.json b/package-lock.json index 8205bb5b2..52fd51301 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20235,6 +20235,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidusage": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-4.0.1.tgz", + "integrity": "sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/pify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", @@ -30164,6 +30176,7 @@ "lodash": "^4.17.21", "mime-types": "^2.1.35", "nanoid": "3.3.8", + "pidusage": "^4.0.1", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", "tough-cookie": "^6.0.0", diff --git a/packages/bruno-app/src/components/Devtools/Console/index.js b/packages/bruno-app/src/components/Devtools/Console/index.js index e87e38d37..5705eecb4 100644 --- a/packages/bruno-app/src/components/Devtools/Console/index.js +++ b/packages/bruno-app/src/components/Devtools/Console/index.js @@ -12,7 +12,8 @@ import { IconCode, IconChevronDown, IconTerminal2, - IconNetwork + IconNetwork, + IconDashboard, } from '@tabler/icons'; import { closeConsole, @@ -24,10 +25,12 @@ import { updateNetworkFilter, toggleAllNetworkFilters } from 'providers/ReduxStore/slices/logs'; + import NetworkTab from './NetworkTab'; import RequestDetailsPanel from './RequestDetailsPanel'; // import DebugTab from './DebugTab'; import ErrorDetailsPanel from './ErrorDetailsPanel'; +import Performance from '../Performance'; import StyledWrapper from './StyledWrapper'; const LogIcon = ({ type }) => { @@ -384,6 +387,8 @@ const Console = () => { ); case 'network': return ; + case 'performance': + return ; // case 'debug': // return ; default: @@ -484,6 +489,14 @@ const Console = () => { Network + handleTabChange('performance')} + > + + Performance + + {/* handleTabChange('debug')} diff --git a/packages/bruno-app/src/components/Devtools/Performance/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Performance/StyledWrapper.js new file mode 100644 index 000000000..a71599627 --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Performance/StyledWrapper.js @@ -0,0 +1,120 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .tab-content { + height: 100%; + display: flex; + flex-direction: column; + background: ${props => props.theme.console.bg}; + } + + .tab-content-area { + flex: 1; + overflow-y: auto; + padding: 16px; + } + + .overview-container { + max-width: 1200px; + margin: 0 auto; + } + + .overview-section { + margin-bottom: 32px; + + &:last-child { + margin-bottom: 0; + } + } + + .section-header { + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid ${props => props.theme.console.border}; + + h3 { + margin: 0 0 4px 0; + font-size: 16px; + font-weight: 600; + color: ${props => props.theme.console.titleColor}; + } + + p { + margin: 0; + font-size: 13px; + color: ${props => props.theme.console.textMuted}; + } + } + + .system-resources { + margin-bottom: 16px; + + h2 { + margin: 0 0 8px 0; + font-size: 14px; + font-weight: 600; + color: ${props => props.theme.console.titleColor}; + } + } + + .resource-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 8px; + margin-bottom: 16px; + } + + .resource-card { + background: ${props => props.theme.console.headerBg}; + border: 1px solid ${props => props.theme.console.border}; + border-radius: 4px; + padding: 8px; + } + + .resource-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; + color: ${props => props.theme.console.titleColor}; + } + + .resource-title { + font-size: 12px; + font-weight: 500; + } + + .resource-value { + font-size: 18px; + font-weight: 600; + color: ${props => props.theme.console.titleColor}; + margin-bottom: 2px; + } + + .resource-subtitle { + font-size: 11px; + color: ${props => props.theme.console.buttonColor}; + } + + .resource-trend { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; + margin-top: 8px; + + &.up { + color: #10b981; + } + + &.down { + color: #e81123; + } + + &.stable { + color: ${props => props.theme.console.buttonColor}; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Devtools/Performance/index.js b/packages/bruno-app/src/components/Devtools/Performance/index.js new file mode 100644 index 000000000..1de054b1d --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Performance/index.js @@ -0,0 +1,100 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import StyledWrapper from './StyledWrapper'; +import { + IconCpu, + IconDatabase, + IconClock, + IconServer, + IconChartLine, +} from '@tabler/icons'; + +const Performance = () => { + const { systemResources } = useSelector(state => state.performance); + + const formatBytes = bytes => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const formatUptime = seconds => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + if (hours > 0) return `${hours}h ${minutes}m ${secs}s`; + if (minutes > 0) return `${minutes}m ${secs}s`; + return `${secs}s`; + }; + + const SystemResourceCard = ({ icon: Icon, title, value, subtitle, color = 'default', trend }) => ( + + + + {title} + + {value} + {subtitle && {subtitle}} + {trend && ( + 0 ? 'up' : trend < 0 ? 'down' : 'stable'}`}> + + + {trend > 0 ? '+' : ''} + {trend.toFixed(1)} + % + + + )} + + ); + + return ( + + + + + System Resources + + 80 ? 'danger' : systemResources.cpu > 60 ? 'warning' : 'success'} + /> + + 500 * 1024 * 1024 ? 'danger' : 'default'} + /> + + + + + + + + + + ); +}; + +export default Performance; diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index 3ad176bd4..1f92f0610 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -26,6 +26,7 @@ import { isElectron } from 'utils/common/platform'; import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments'; import { collectionAddOauth2CredentialsByUrl, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index'; import { addLog } from 'providers/ReduxStore/slices/logs'; +import { updateSystemResources } from 'providers/ReduxStore/slices/performance'; const useIpcEvents = () => { const dispatch = useDispatch(); @@ -145,6 +146,10 @@ const useIpcEvents = () => { })); }); + const removeSystemResourcesListener = ipcRenderer.on('main:filesync-system-resources', resourceData => { + dispatch(updateSystemResources(resourceData)); + }); + const removeConfigUpdatesListener = ipcRenderer.on('main:bruno-config-update', (val) => dispatch(brunoConfigUpdateEvent(val)) ); @@ -209,6 +214,7 @@ const useIpcEvents = () => { removeCollectionOauth2CredentialsUpdatesListener(); removeCollectionLoadingStateListener(); removePersistentEnvVariablesUpdateListener(); + removeSystemResourcesListener(); }; }, [isElectron]); }; diff --git a/packages/bruno-app/src/providers/ReduxStore/index.js b/packages/bruno-app/src/providers/ReduxStore/index.js index 8ed528073..d5abee753 100644 --- a/packages/bruno-app/src/providers/ReduxStore/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/index.js @@ -7,6 +7,7 @@ import tabsReducer from './slices/tabs'; import notificationsReducer from './slices/notifications'; import globalEnvironmentsReducer from './slices/global-environments'; import logsReducer from './slices/logs'; +import performanceReducer from './slices/performance'; import { draftDetectMiddleware } from './middlewares/draft/middleware'; const isDevEnv = () => { @@ -25,7 +26,8 @@ export const store = configureStore({ tabs: tabsReducer, notifications: notificationsReducer, globalEnvironments: globalEnvironmentsReducer, - logs: logsReducer + logs: logsReducer, + performance: performanceReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware) }); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/performance.js b/packages/bruno-app/src/providers/ReduxStore/slices/performance.js new file mode 100644 index 000000000..efd7b01d3 --- /dev/null +++ b/packages/bruno-app/src/providers/ReduxStore/slices/performance.js @@ -0,0 +1,28 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + systemResources: { + cpu: 0, + memory: 0, + pid: null, + uptime: 0, + lastUpdated: null, + }, +}; + +export const performanceSlice = createSlice({ + name: 'performance', + initialState, + reducers: { + updateSystemResources: (state, action) => { + state.systemResources = { + ...state.systemResources, + ...action.payload, + lastUpdated: new Date().toISOString(), + }; + }, + }, +}); + +export const { updateSystemResources } = performanceSlice.actions; +export default performanceSlice.reducer; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index 3e0defb81..c8a0e118e 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -65,6 +65,7 @@ "lodash": "^4.17.21", "mime-types": "^2.1.35", "nanoid": "3.3.8", + "pidusage": "^4.0.1", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", "tough-cookie": "^6.0.0", diff --git a/packages/bruno-electron/src/app/system-monitor.js b/packages/bruno-electron/src/app/system-monitor.js new file mode 100644 index 000000000..48fc27852 --- /dev/null +++ b/packages/bruno-electron/src/app/system-monitor.js @@ -0,0 +1,71 @@ +const pidusage = require('pidusage'); + +class SystemMonitor { + constructor() { + this.intervalId = null; + this.isMonitoring = false; + this.startTime = Date.now(); + } + + start(win, intervalMs = 2000) { + if (this.isMonitoring) { + return; + } + + this.isMonitoring = true; + this.startTime = Date.now(); + + // Emit initial stats + this.emitSystemStats(win); + + // Set up periodic monitoring + this.intervalId = setInterval(() => { + this.emitSystemStats(win); + }, intervalMs); + } + + stop() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + this.isMonitoring = false; + } + + async emitSystemStats(win) { + try { + const pid = process.pid; + const stats = await pidusage(pid); + const uptime = (Date.now() - this.startTime) / 1000; + + const systemResources = { + cpu: stats.cpu || 0, + memory: stats.memory || 0, + pid: pid, + uptime: uptime, + timestamp: new Date().toISOString(), + }; + + win.webContents.send('main:filesync-system-resources', systemResources); + } catch (error) { + console.error('Error getting system stats:', error); + + // Fallback stats if pidusage fails + const fallbackStats = { + cpu: 0, + memory: process.memoryUsage().rss, + pid: process.pid, + uptime: (Date.now() - this.startTime) / 1000, + timestamp: new Date().toISOString(), + }; + + win.webContents.send('main:filesync-system-resources', fallbackStats); + } + } + + isRunning() { + return this.isMonitoring; + } +} + +module.exports = SystemMonitor; diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 267f3fb5e..c36d60ea7 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -45,8 +45,10 @@ const { safeParseJSON, safeStringifyJSON } = require('./utils/common'); const { getDomainsWithCookies } = require('./utils/cookies'); const { cookiesStore } = require('./store/cookies'); const onboardUser = require('./app/onboarding'); +const SystemMonitor = require('./app/system-monitor'); const lastOpenedCollections = new LastOpenedCollections(); +const systemMonitor = new SystemMonitor(); // Reference: https://content-security-policy.com/ const contentSecurityPolicy = [ @@ -202,6 +204,9 @@ app.on('ready', async () => { } mainWindow.webContents.send('main:app-loaded'); + + // Start system monitoring for FileSync + systemMonitor.start(mainWindow); }); // register all ipc handlers @@ -220,6 +225,9 @@ app.on('before-quit', () => { } catch (err) { console.warn('Failed to flush cookies on quit', err); } + + // Stop system monitoring + systemMonitor.stop(); }); app.on('window-all-closed', app.quit);