From fcdba86fec97314910b795fa135f29b5fc576b05 Mon Sep 17 00:00:00 2001 From: Jas Singh Date: Thu, 24 Oct 2024 02:26:39 -0700 Subject: [PATCH] Updated the logic for Stat/Metric tracking in the dashboard more robust. (#365) * Updated the logic for Stat/Metric tracking in the dashboard more robust. * Show used/total for stats. * Added the ability to configure the update interval of metrics in the dashboard. --- customModules/cpu/computeCPU.ts | 1 + customModules/ram/computeRam.ts | 1 + customModules/storage/computeStorage.ts | 1 + lib/types/customModules/generic.d.ts | 2 +- modules/menus/dashboard/stats/index.ts | 126 +++++------------- options.ts | 1 + services/Cpu.ts | 41 ++++++ services/Ram.ts | 73 ++++++++++ services/Storage.ts | 61 +++++++++ .../settings/pages/config/menus/dashboard.ts | 11 ++ 10 files changed, 223 insertions(+), 95 deletions(-) create mode 100644 services/Cpu.ts create mode 100644 services/Ram.ts create mode 100644 services/Storage.ts diff --git a/customModules/cpu/computeCPU.ts b/customModules/cpu/computeCPU.ts index 460b1db..dad5e69 100644 --- a/customModules/cpu/computeCPU.ts +++ b/customModules/cpu/computeCPU.ts @@ -4,6 +4,7 @@ import GTop from 'gi://GTop'; let previousCpuData = new GTop.glibtop_cpu(); GTop.glibtop_get_cpu(previousCpuData); +// FIX: Consolidate with Cpu service class export const computeCPU = (): number => { const currentCpuData = new GTop.glibtop_cpu(); GTop.glibtop_get_cpu(currentCpuData); diff --git a/customModules/ram/computeRam.ts b/customModules/ram/computeRam.ts index b8026c2..e435d40 100644 --- a/customModules/ram/computeRam.ts +++ b/customModules/ram/computeRam.ts @@ -4,6 +4,7 @@ import { divide } from 'customModules/utils'; import { GenericResourceData } from 'lib/types/customModules/generic'; import { Variable as VariableType } from 'types/variable'; +// FIX: Consolidate with Ram service class export const calculateRamUsage = (round: VariableType): GenericResourceData => { try { const [success, meminfoBytes] = GLib.file_get_contents('/proc/meminfo'); diff --git a/customModules/storage/computeStorage.ts b/customModules/storage/computeStorage.ts index 49cbc14..f30ede5 100644 --- a/customModules/storage/computeStorage.ts +++ b/customModules/storage/computeStorage.ts @@ -5,6 +5,7 @@ import { divide } from 'customModules/utils'; import { Variable as VariableType } from 'types/variable'; import { GenericResourceData } from 'lib/types/customModules/generic'; +// FIX: Consolidate with Storage service class export const computeStorage = (round: VariableType): GenericResourceData => { try { const currentFsUsage = new GTop.glibtop_fsusage(); diff --git a/lib/types/customModules/generic.d.ts b/lib/types/customModules/generic.d.ts index 839e04a..22610e2 100644 --- a/lib/types/customModules/generic.d.ts +++ b/lib/types/customModules/generic.d.ts @@ -6,7 +6,7 @@ export type GenericResourceMetrics = { percentage: number; }; -type GenericResourceData = ResourceUsage & { +export type GenericResourceData = GenericResourceMetrics & { free: number; }; diff --git a/modules/menus/dashboard/stats/index.ts b/modules/menus/dashboard/stats/index.ts index 43e8457..f10e0cc 100644 --- a/modules/menus/dashboard/stats/index.ts +++ b/modules/menus/dashboard/stats/index.ts @@ -1,67 +1,31 @@ import options from 'options'; +import Ram from 'services/Ram'; import { GPU_Stat } from 'lib/types/gpustat'; import { dependencies } from 'lib/utils'; import { BoxWidget } from 'lib/types/widget'; -import { GenericResourceMetrics } from 'lib/types/customModules/generic'; +import Cpu from 'services/Cpu'; +import Storage from 'services/Storage'; +import { renderResourceLabel } from 'customModules/utils'; const { terminal } = options; -const { enable_gpu } = options.menus.dashboard.stats; +const { enable_gpu, interval } = options.menus.dashboard.stats; + +const ramService = new Ram(); +const cpuService = new Cpu(); +const storageService = new Storage(); + +ramService.setShouldRound(true); +storageService.setShouldRound(true); + +interval.connect('changed', () => { + ramService.updateTimer(interval.value); + cpuService.updateTimer(interval.value); + storageService.updateTimer(interval.value); +}); const Stats = (): BoxWidget => { const divide = ([total, free]: number[]): number => free / total; - const formatSizeInGB = (sizeInKB: number): number => Number((sizeInKB / 1024 ** 2).toFixed(2)); - - const cpu = Variable(0, { - poll: [ - 2000, - 'top -b -n 1', - (out): number => { - if (typeof out !== 'string') { - return 0; - } - - const cpuOut = out.split('\n').find((line) => line.includes('Cpu(s)')); - - if (cpuOut === undefined) { - return 0; - } - - const freeCpu = parseFloat(cpuOut.split(/\s+/)[1].replace(',', '.')); - return divide([100, freeCpu]); - }, - ], - }); - - const ram = Variable( - { total: 0, used: 0, percentage: 0 }, - { - poll: [ - 2000, - 'free', - (out): GenericResourceMetrics => { - if (typeof out !== 'string') { - return { total: 0, used: 0, percentage: 0 }; - } - - const ramOut = out.split('\n').find((line) => line.includes('Mem:')); - - if (ramOut === undefined) { - return { total: 0, used: 0, percentage: 0 }; - } - - const [totalRam, usedRam] = ramOut.split(/\s+/).splice(1, 2).map(Number); - - return { - percentage: divide([totalRam, usedRam]), - total: formatSizeInGB(totalRam), - used: formatSizeInGB(usedRam), - }; - }, - ], - }, - ); - const gpu = Variable(0); const GPUStat = Widget.Box({ @@ -170,40 +134,6 @@ const Stats = (): BoxWidget => { }), }); - const storage = Variable( - { total: 0, used: 0, percentage: 0 }, - { - poll: [ - 2000, - 'df -B1 /', - (out): GenericResourceMetrics => { - if (typeof out !== 'string') { - return { total: 0, used: 0, percentage: 0 }; - } - - const dfOut = out.split('\n').find((line) => line.startsWith('/')); - - if (dfOut === undefined) { - return { total: 0, used: 0, percentage: 0 }; - } - - const parts = dfOut.split(/\s+/); - const size = parseInt(parts[1], 10); - const used = parseInt(parts[2], 10); - - const sizeInGB = formatSizeInGB(size); - const usedInGB = formatSizeInGB(used); - - return { - total: Math.floor(sizeInGB / 1000), - used: Math.floor(usedInGB / 1000), - percentage: divide([size, used]), - }; - }, - ], - }, - ); - return Widget.Box({ class_name: 'dashboard-card stats-container', vertical: true, @@ -248,7 +178,7 @@ const Stats = (): BoxWidget => { vpack: 'center', bar_mode: 'continuous', max_value: 1, - value: cpu.bind('value'), + value: cpuService.cpu.bind('value').as((cpuUsage) => Math.round(cpuUsage) / 100), }), }), ], @@ -256,7 +186,7 @@ const Stats = (): BoxWidget => { Widget.Label({ hpack: 'end', class_name: 'stat-value cpu', - label: cpu.bind('value').as((v) => `${Math.floor(v * 100)}%`), + label: cpuService.cpu.bind('value').as((cpuUsage) => `${Math.round(cpuUsage)}%`), }), ], }), @@ -295,7 +225,9 @@ const Stats = (): BoxWidget => { class_name: 'stats-bar', hexpand: true, vpack: 'center', - value: ram.bind('value').as((v) => v.percentage), + value: ramService.ram.bind('value').as((ramUsage) => { + return ramUsage.percentage / 100; + }), }), }), ], @@ -303,7 +235,9 @@ const Stats = (): BoxWidget => { Widget.Label({ hpack: 'end', class_name: 'stat-value ram', - label: ram.bind('value').as((v) => `${v.used}/${v.total} GB`), + label: ramService.ram + .bind('value') + .as((ramUsage) => `${renderResourceLabel('used/total', ramUsage, true)}`), }), ], }), @@ -343,7 +277,9 @@ const Stats = (): BoxWidget => { class_name: 'stats-bar', hexpand: true, vpack: 'center', - value: storage.bind('value').as((v) => v.percentage), + value: storageService.storage + .bind('value') + .as((storageUsage) => storageUsage.percentage / 100), }), }), ], @@ -351,7 +287,9 @@ const Stats = (): BoxWidget => { Widget.Label({ hpack: 'end', class_name: 'stat-value storage', - label: storage.bind('value').as((v) => `${v.used}/${v.total} GB`), + label: storageService.storage + .bind('value') + .as((storageUsage) => `${renderResourceLabel('used/total', storageUsage, true)}`), }), ], }), diff --git a/options.ts b/options.ts index 9ed6e3a..1e8dce8 100644 --- a/options.ts +++ b/options.ts @@ -1066,6 +1066,7 @@ const options = mkOptions(OPTIONS, { }, stats: { enabled: opt(true), + interval: opt(2000), enable_gpu: opt(false), }, controls: { diff --git a/services/Cpu.ts b/services/Cpu.ts new file mode 100644 index 0000000..9a4c555 --- /dev/null +++ b/services/Cpu.ts @@ -0,0 +1,41 @@ +// TODO: Convert to a real service + +// @ts-expect-error: This import is a special directive that tells the compiler to use the GTop library +import GTop from 'gi://GTop'; + +import { pollVariable } from 'customModules/PollVar'; + +class Cpu { + private updateFrequency = Variable(2000); + public cpu = Variable(0); + + private previousCpuData = new GTop.glibtop_cpu(); + + constructor() { + GTop.glibtop_get_cpu(this.previousCpuData); + + this.calculateUsage = this.calculateUsage.bind(this); + pollVariable(this.cpu, [], this.updateFrequency.bind('value'), this.calculateUsage); + } + + public calculateUsage(): number { + const currentCpuData = new GTop.glibtop_cpu(); + GTop.glibtop_get_cpu(currentCpuData); + + // Calculate the differences from the previous to current data + const totalDiff = currentCpuData.total - this.previousCpuData.total; + const idleDiff = currentCpuData.idle - this.previousCpuData.idle; + + const cpuUsagePercentage = totalDiff > 0 ? ((totalDiff - idleDiff) / totalDiff) * 100 : 0; + + this.previousCpuData = currentCpuData; + + return cpuUsagePercentage; + } + + public updateTimer(timerInMs: number): void { + this.updateFrequency.value = timerInMs; + } +} + +export default Cpu; diff --git a/services/Ram.ts b/services/Ram.ts new file mode 100644 index 0000000..2454c02 --- /dev/null +++ b/services/Ram.ts @@ -0,0 +1,73 @@ +// TODO: Convert to a real service + +const GLib = imports.gi.GLib; + +import { pollVariable } from 'customModules/PollVar'; +import { GenericResourceData } from 'lib/types/customModules/generic'; + +class Ram { + private updateFrequency = Variable(2000); + private shouldRound = false; + + public ram = Variable({ total: 0, used: 0, percentage: 0, free: 0 }); + + constructor() { + this.calculateUsage = this.calculateUsage.bind(this); + pollVariable(this.ram, [], this.updateFrequency.bind('value'), this.calculateUsage); + } + + public calculateUsage(): GenericResourceData { + try { + const [success, meminfoBytes] = GLib.file_get_contents('/proc/meminfo'); + + if (!success || !meminfoBytes) { + throw new Error('Failed to read /proc/meminfo or file content is null.'); + } + + const meminfo = new TextDecoder('utf-8').decode(meminfoBytes); + + const totalMatch = meminfo.match(/MemTotal:\s+(\d+)/); + const availableMatch = meminfo.match(/MemAvailable:\s+(\d+)/); + + if (!totalMatch || !availableMatch) { + throw new Error('Failed to parse /proc/meminfo for memory values.'); + } + + const totalRamInBytes = parseInt(totalMatch[1], 10) * 1024; + const availableRamInBytes = parseInt(availableMatch[1], 10) * 1024; + + let usedRam = totalRamInBytes - availableRamInBytes; + usedRam = isNaN(usedRam) || usedRam < 0 ? 0 : usedRam; + + return { + percentage: this.divide([totalRamInBytes, usedRam]), + total: totalRamInBytes, + used: usedRam, + free: availableRamInBytes, + }; + } catch (error) { + console.error('Error calculating RAM usage:', error); + return { total: 0, used: 0, percentage: 0, free: 0 }; + } + } + + public setShouldRound(round: boolean): void { + this.shouldRound = round; + } + + private divide([total, used]: number[]): number { + const percentageTotal = (used / total) * 100; + + if (this.shouldRound) { + return total > 0 ? Math.round(percentageTotal) : 0; + } + + return total > 0 ? parseFloat(percentageTotal.toFixed(2)) : 0; + } + + updateTimer(timerInMs: number): void { + this.updateFrequency.value = timerInMs; + } +} + +export default Ram; diff --git a/services/Storage.ts b/services/Storage.ts new file mode 100644 index 0000000..466d5ec --- /dev/null +++ b/services/Storage.ts @@ -0,0 +1,61 @@ +// TODO: Convert to a real service + +// @ts-expect-error: This import is a special directive that tells the compiler to use the GTop library +import GTop from 'gi://GTop'; + +import { pollVariable } from 'customModules/PollVar'; +import { GenericResourceData } from 'lib/types/customModules/generic'; + +class Storage { + private updateFrequency = Variable(2000); + private shouldRound = false; + + public storage = Variable({ total: 0, used: 0, percentage: 0, free: 0 }); + + constructor() { + this.calculateUsage = this.calculateUsage.bind(this); + pollVariable(this.storage, [], this.updateFrequency.bind('value'), this.calculateUsage); + } + + public calculateUsage(): GenericResourceData { + try { + const currentFsUsage = new GTop.glibtop_fsusage(); + + GTop.glibtop_get_fsusage(currentFsUsage, '/'); + + const total = currentFsUsage.blocks * currentFsUsage.block_size; + const available = currentFsUsage.bavail * currentFsUsage.block_size; + const used = total - available; + + return { + total, + used, + free: available, + percentage: this.divide([total, used]), + }; + } catch (error) { + console.error('Error calculating Storage usage:', error); + return { total: 0, used: 0, percentage: 0, free: 0 }; + } + } + + public setShouldRound(round: boolean): void { + this.shouldRound = round; + } + + private divide([total, used]: number[]): number { + const percentageTotal = (used / total) * 100; + + if (this.shouldRound) { + return total > 0 ? Math.round(percentageTotal) : 0; + } + + return total > 0 ? parseFloat(percentageTotal.toFixed(2)) : 0; + } + + public updateTimer(timerInMs: number): void { + this.updateFrequency.value = timerInMs; + } +} + +export default Storage; diff --git a/widget/settings/pages/config/menus/dashboard.ts b/widget/settings/pages/config/menus/dashboard.ts index e387d0b..8abe579 100644 --- a/widget/settings/pages/config/menus/dashboard.ts +++ b/widget/settings/pages/config/menus/dashboard.ts @@ -48,8 +48,10 @@ export const DashboardMenuSettings = (): Scrollable => { Option({ opt: options.menus.dashboard.powermenu.reboot, title: 'Reboot Command', type: 'string' }), Option({ opt: options.menus.dashboard.powermenu.logout, title: 'Logout Command', type: 'string' }), Option({ opt: options.menus.dashboard.powermenu.sleep, title: 'Sleep Command', type: 'string' }), + Header('Controls'), Option({ opt: options.menus.dashboard.controls.enabled, title: 'Enabled', type: 'boolean' }), + Header('Resource Usage Metrics'), Option({ opt: options.menus.dashboard.stats.enabled, title: 'Enabled', type: 'boolean' }), Option({ @@ -58,6 +60,15 @@ export const DashboardMenuSettings = (): Scrollable => { subtitle: "NOTE: This is currently only available for NVidia GPUs and requires 'python-gpustat'.", type: 'boolean', }), + Option({ + opt: options.menus.dashboard.stats.interval, + title: 'Update Interval', + subtitle: 'The frequency at which to poll system metrics.', + type: 'number', + min: 100, + increment: 500, + }), + Header('Shortcuts'), Option({ opt: options.menus.dashboard.shortcuts.enabled, title: 'Enabled', type: 'boolean' }), Option({