From 9cbfa2983829d0ac252afa7cd3952827cbd3aa3e Mon Sep 17 00:00:00 2001 From: Nick Wittwer <6360459+nwittwer@users.noreply.github.com> Date: Wed, 21 Jun 2023 21:04:00 +0200 Subject: [PATCH] Adds system performance monitoring in UI, warning during high load (#255) * Added simple performance monitoring * Simplified changeArtboardVisibility() to only require ID * Improved UI * Increased warning threshold to 85% for CPU * Added `will-change` property to Panzoom class * Show UI in packaged app, not just dev * Added border to container of gauges to visually group them * Moved performance to start before browser downloads; browser downloads no longer top-level blocking await --- app/components/PerfMonitor.vue | 277 +++++++++++++++++++++++++++++ app/components/ToolBar/index.vue | 9 +- app/components/panzoom/Panzoom.vue | 44 +++-- app/electron/main.ts | 34 ++-- app/electron/mainWindow.ts | 16 +- app/electron/usage-monitoring.ts | 82 +++++++++ app/mixins/rightClickMenu.ts | 2 +- app/package.json | 1 + app/store/artboards.ts | 15 +- yarn.lock | 11 ++ 10 files changed, 447 insertions(+), 44 deletions(-) create mode 100644 app/components/PerfMonitor.vue create mode 100644 app/electron/usage-monitoring.ts diff --git a/app/components/PerfMonitor.vue b/app/components/PerfMonitor.vue new file mode 100644 index 00000000..220dd3bc --- /dev/null +++ b/app/components/PerfMonitor.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/app/components/ToolBar/index.vue b/app/components/ToolBar/index.vue index 25a3ce3b..afdf54c5 100644 --- a/app/components/ToolBar/index.vue +++ b/app/components/ToolBar/index.vue @@ -53,10 +53,13 @@ --> +
+ + +
-
@@ -70,6 +73,9 @@ import InstallUpdateButton from '@/components/ToolBar/InstallUpdateButton.vue' import UpdateChannel from '@/components/Settings/UpdateChannel.vue' import Artboard from '@/components/Screens/Artboard.vue' +const runtimeConfig = useRuntimeConfig() +const isDev = runtimeConfig.public.DEV + // Pinia import { useArtboardsStore } from '~/store/artboards' import { useHistoryStore } from '~/store/history' @@ -77,6 +83,7 @@ import { useGuiStore } from '~/store/gui' // TODO: Re-connect with Electron import * as remote from '@electron/remote' +import PerfMonitor from '../PerfMonitor.vue' const artboards = useArtboardsStore() const history = useHistoryStore() diff --git a/app/components/panzoom/Panzoom.vue b/app/components/panzoom/Panzoom.vue index 2506d9d9..5d256afe 100644 --- a/app/components/panzoom/Panzoom.vue +++ b/app/components/panzoom/Panzoom.vue @@ -2,21 +2,32 @@
-
+
-
+
-
+
@@ -44,6 +55,7 @@ import useEventHandler from '../Screens/useEventHandler' import { useDevStore } from '~/store/dev' import { initialPanZoom } from './panzoomFns' + // Connect w/ Electron const interactions = useInteractionStore() @@ -104,6 +116,7 @@ onMounted(async () => { // Init Panzoom globally // We use the 'canvas' option to enable interactions on the parent DOM Node as well // Docs: https://github.com/timmywil/panzoom#canvas + $root.$panzoom = Panzoom(innerPanArea.value, { canvas: true, // Allows parent to control child cursor: 'grab', @@ -216,6 +229,8 @@ function enableEventListeners() { } } + + /** * Handles wheel events (i.e. mousewheel) * @param DOMElement DOM element that Panzoom is on @@ -323,6 +338,7 @@ function fitToScreen() { position: relative; // Important display: inline-block; overflow: visible; + will-change: auto; } #parent { @@ -341,7 +357,6 @@ function fitToScreen() { } .dev-visual-debugger { - &:before, &:after { position: absolute; @@ -367,7 +382,6 @@ function fitToScreen() { // Nested debugger .dev-visual-debugger { - // Vertical line &:before { position: absolute; diff --git a/app/electron/main.ts b/app/electron/main.ts index a2701a24..9dbd7ad6 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -7,8 +7,11 @@ import path from 'path' import { init as initIpcHandlers } from './ipcHandlers' import mainWindowInit from './mainWindow' import { getPackageJson } from './util' - ; import { RuntimeConfig } from './config' -(async () => { +import { RuntimeConfig } from './config' +import startPerfMonitoring from './usage-monitoring' +import { installBrowsers } from './cross-browser/playwright-browser-manager' +import enableCrossBrowserScreenshots from './cross-browser/screenshots/api' +;(async () => { const version = await getPackageJson().then((data) => data.version) // Set the version @@ -30,19 +33,19 @@ import { getPackageJson } from './util' // Check if asar is enabled const runtimeConfig = RuntimeConfig.getInstance() - if (!runtimeConfig.dev.appFilesPath || !runtimeConfig.packaged.appFilesPath) { + if ( + !runtimeConfig.dev.appFilesPath || + !runtimeConfig.packaged.appFilesPath + ) { throw new Error('RuntimeConfig.packaged.appFilesPath is not set') } webPreferences.preload = isDev - ? path.join( - runtimeConfig.dev.appFilesPath, - 'extraResources/inject.js' - ) + ? path.join(runtimeConfig.dev.appFilesPath, 'extraResources/inject.js') : path.join( - runtimeConfig.packaged.appFilesPath, - 'dist-electron/extraResources/inject.js' - ) + runtimeConfig.packaged.appFilesPath, + 'dist-electron/extraResources/inject.js' + ) // webPreferences.nodeIntegration = false // Disable Node.js integration inside // webPreferences.webSecurity = false // Disable web security @@ -72,5 +75,14 @@ import { getPackageJson } from './util' initIpcHandlers() // Load here all startup windows - mainWindowInit() + const mainWindow = await mainWindowInit() + + // Start monitoring CPU/Memory usage + startPerfMonitoring() + + // Check for browser installations + installBrowsers().then(() => { + // Screenshot worker + enableCrossBrowserScreenshots() + }) })() diff --git a/app/electron/mainWindow.ts b/app/electron/mainWindow.ts index 9fa50938..6062553b 100644 --- a/app/electron/mainWindow.ts +++ b/app/electron/mainWindow.ts @@ -11,18 +11,15 @@ import windowPosition from './windowPosition' // import browserInstaller from './browser-installer' import { setMenu } from './menu' import { init as initUpdates } from './updates' -import browserInstaller from './browser-installer' -import { installBrowsers } from './cross-browser/playwright-browser-manager' import log from 'electron-log' import isDev from 'electron-is-dev' -import enableCrossBrowserScreenshots from './cross-browser/screenshots/api' import { RuntimeConfig } from './config' const INDEX_PATH = path.join(__dirname, '..', 'renderer', 'index.html') const DEV_SERVER_URL = process.env.DEV_SERVER_URL // eslint-disable-line prefer-destructuring -export default function init() { +export default async function init(): Promise { // Get saved window position state windowPosition.onAppLoad() const initialWindowPos = windowPosition.getState() @@ -43,7 +40,7 @@ export default function init() { titleBarStyle: 'hiddenInset', // Hide the bar }) - winHandler.onCreated(async (browserWindow: BrowserWindow) => { + return await winHandler.onCreated(async (browserWindow: BrowserWindow) => { // Initialize the runtime config // Exposes file paths and other dynamic properties const appConfig = RuntimeConfig.getInstance() @@ -77,11 +74,7 @@ export default function init() { initUpdates(browserWindow) }) - // Check for browser installations - await installBrowsers() - - // Screenshot worker test - enableCrossBrowserScreenshots() + // Reload when requested by the renderer process ipcMain.handle('reload-window', () => { @@ -100,5 +93,8 @@ export default function init() { browserWindow.on('closed', () => { browserWindow = null }) + + // Return the window instance + return browserWindow }) } diff --git a/app/electron/usage-monitoring.ts b/app/electron/usage-monitoring.ts new file mode 100644 index 00000000..16506efe --- /dev/null +++ b/app/electron/usage-monitoring.ts @@ -0,0 +1,82 @@ +import si from 'systeminformation' +import { app, BrowserWindow } from 'electron' +import { RuntimeConfig } from './config' + +let cpuInterval: string | number | NodeJS.Timeout | undefined +let memoryInterval: string | number | NodeJS.Timeout | undefined + +const cpuThreshold = 85 // 85% CPU usage threshold +const memoryThreshold = 90 // 90% memory usage threshold + +let currentWindow: BrowserWindow | null = null + +const emitToRenderer = (channel: string, ...args: any[]) => { + if (currentWindow) { + currentWindow.webContents.send(channel, ...args) + } else { + throw new Error('No focused window found') + } +} + +export default function startMonitoring() { + const appConfig = RuntimeConfig.getInstance() + currentWindow = appConfig.window + + monitorCPUUsage() + monitorMemoryUsage() + + // Register before-quit event listener + app.on('before-quit', () => { + // Clear the monitoring intervals + clearInterval(cpuInterval) + clearInterval(memoryInterval) + }) +} + +function monitorCPUUsage() { + cpuInterval = setInterval(async () => { + try { + const cpuData = await si.currentLoad() + const cpuUsagePercent = cpuData.currentLoad + + emitToRenderer('perf-cpu', Math.round(cpuUsagePercent)) // Send CPU usage to renderer process + + if (cpuUsagePercent >= cpuThreshold) { + const warningMessage = `High memory usage: ${cpuUsagePercent.toFixed( + 2 + )}%` + console.warn(warningMessage) + emitToRenderer('perf-warning', warningMessage) + } else { + console.info('CPU usage:', cpuUsagePercent.toFixed(2), '%') + } + } catch (err) { + console.debug('Error retrieving CPU usage:', err) + } + }, 1000) +} + +function monitorMemoryUsage() { + memoryInterval = setInterval(async () => { + try { + const memData = await si.mem() + const totalMemory = memData.total + const usedMemory = memData.active + const memoryUsagePercent = (usedMemory / totalMemory) * 100 + + emitToRenderer('perf-memory', Math.round(memoryUsagePercent)) // Send memory usage to renderer process + + if (memoryUsagePercent >= memoryThreshold) { + const warningMessage = `High memory usage: ${memoryUsagePercent.toFixed( + 2 + )}%` + console.warn(warningMessage) + emitToRenderer('perf-warning', warningMessage) + } else { + console.info('Memory usage:', memoryUsagePercent.toFixed(2), '%') + } + } catch (err) { + console.error('Error retrieving memory usage:', err) + } + }, 1000) +} diff --git a/app/mixins/rightClickMenu.ts b/app/mixins/rightClickMenu.ts index 9bc58959..b3d6a982 100644 --- a/app/mixins/rightClickMenu.ts +++ b/app/mixins/rightClickMenu.ts @@ -46,7 +46,7 @@ export default function (artboard: Artboard) { label: artboard.isVisible ? 'Hide' : 'Show', click() { const store = useArtboardsStore() - store.changeArtboardVisibility(artboard) + store.changeArtboardVisibility(artboard.id) }, }) ) diff --git a/app/package.json b/app/package.json index 13422a66..5a07b7d6 100644 --- a/app/package.json +++ b/app/package.json @@ -40,6 +40,7 @@ "electron-store": "^8.1.0", "electron-updater": "5.3.0", "html-to-image": "^1.11.11", + "systeminformation": "^5.18.3", "uuid": "8.3.2", "vue": "^3.2.47", "vuetify": "^3.0.6" diff --git a/app/store/artboards.ts b/app/store/artboards.ts index 8cd53dfd..081ef120 100644 --- a/app/store/artboards.ts +++ b/app/store/artboards.ts @@ -87,12 +87,15 @@ export const useArtboardsStore = defineStore('artboards', { ? (artboard.isInViewport = isVisible) : console.warn('No artboard found') }, - changeArtboardVisibility(artboard: Artboard) { - // 1. Get the artboard.id - const id = artboard.id - const index = this.list.findIndex((obj) => obj.id === id) - // 2. Change the visibility of just that artboard's is property - this.list[index].isVisible = !artboard.isVisible + changeArtboardVisibility(id: Artboard['id']) { + const match = this.list.find((obj) => obj.id === id) + + if (!match) { + console.error('Artboard not found') + return + } + + match.isVisible = !match.isVisible }, updateArtboardAtIndex(artboard: Partial) { // 1. Get the artboard.id diff --git a/yarn.lock b/yarn.lock index 17ce493c..705d68f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2841,6 +2841,7 @@ __metadata: sass: 1.60.0 sortablejs: ^1.15.0 sortablejs-vue3: ^1.2.9 + systeminformation: ^5.18.3 tailwindcss: ^3.3.2 typescript: ^5.0.0 uuid: 8.3.2 @@ -20303,6 +20304,16 @@ __metadata: languageName: node linkType: hard +"systeminformation@npm:^5.18.3": + version: 5.18.3 + resolution: "systeminformation@npm:5.18.3" + bin: + systeminformation: lib/cli.js + checksum: dac4d0b92c102d4f5766692ea275c54bf234012a989e3383e43511ada7193957843e5ad391df3dfea89d3579ae0d9f9e7ca1e555b48f30c173374dcb8f645ce3 + conditions: (os=darwin | os=linux | os=win32 | os=freebsd | os=openbsd | os=netbsd | os=sunos | os=android) + languageName: node + linkType: hard + "table@npm:^6.0.9": version: 6.8.1 resolution: "table@npm:6.8.1"