2025-05-21 18:35:55 +02:00
< script setup >
import { useI18n } from 'vue-i18n' ;
const i18n = useI18n ( ) ;
const t = i18n . t ;
2025-05-22 12:26:30 +02:00
import { ref , onMounted , onUnmounted , useTemplateRef } from 'vue' ;
2025-05-21 18:35:55 +02:00
import Chart from 'chart.js/auto' ;
import moment from 'moment-timezone' ;
import { SingleSelect , Spinner } from 'pankow' ;
import Section from './Section.vue' ;
import SystemModel from '../models/SystemModel.js' ;
const systemModel = SystemModel . create ( ) ;
const periods = [
2025-05-23 15:08:01 +02:00
{ id : 0 , label : t ( 'app.graphs.period.live' ) , format : 'hh:mm A' , tooltipFormat : 'hh:mm A' } ,
{ id : 1 , label : t ( 'app.graphs.period.1h' ) , format : 'hh:mm A' , tooltipFormat : 'hh:mm A' } ,
{ id : 6 , label : t ( 'app.graphs.period.6h' ) , format : 'hh:mm A' , tooltipFormat : 'hh:mm A' } ,
{ id : 12 , label : t ( 'app.graphs.period.12h' ) , format : 'hh:mm A' , tooltipFormat : 'hh:mm A' } ,
{ id : 24 , label : t ( 'app.graphs.period.24h' ) , format : 'hh:mm A' , tooltipFormat : 'hh:mm A' } ,
{ id : 24 * 7 , label : t ( 'app.graphs.period.7d' ) , format : 'DD MMM' , tooltipFormat : 'DD MMM hh:mm A' } ,
{ id : 24 * 30 , label : t ( 'app.graphs.period.30d' ) , format : 'DD MMM' , tooltipFormat : 'DD MMM hh:mm A' } ,
2025-05-21 18:35:55 +02:00
] ;
const busy = ref ( false ) ;
2025-05-23 11:40:25 +02:00
const period = ref ( 0 ) ;
2025-05-21 18:35:55 +02:00
const cpuGraphNode = useTemplateRef ( 'cpuGraphNode' ) ;
const memoryGraphNode = useTemplateRef ( 'memoryGraphNode' ) ;
let systemMemory = { } ;
2025-05-22 18:45:05 +02:00
let systemCpus = { } ;
2025-05-21 18:35:55 +02:00
let cpuGraph = null ;
let memoryGraph = null ;
let metricStream = null ;
2025-05-23 11:40:25 +02:00
const LIVE _REFRESH _INTERVAL _MSECS = 500 ;
const LIVE _REFRESH _HISTORY _MSECS = 5 * 60 * 1000 ; // last 5 mins
2025-05-23 16:11:48 +02:00
function pruneGraphData ( dataset , options ) {
while ( dataset . data . length && ( dataset . data [ 0 ] . x < options . scales . x . min ) ) { // remove elements beyond our tme window
dataset . data . shift ( ) ;
}
}
2025-05-21 18:35:55 +02:00
async function liveRefresh ( ) {
2025-05-23 11:40:25 +02:00
metricStream = await systemModel . getMetricStream ( LIVE _REFRESH _INTERVAL _MSECS ) ;
2025-05-21 18:35:55 +02:00
metricStream . onerror = ( error ) => console . log ( 'event stream error:' , error ) ;
metricStream . onmessage = ( message ) => {
const data = JSON . parse ( message . data ) ;
2025-05-23 16:11:48 +02:00
if ( data . cpu [ 0 ] ) { // since cpu% is relative, value can be null if no previous value
2025-05-23 11:40:25 +02:00
cpuGraph . data . datasets [ 0 ] . data . push ( {
2025-05-23 13:24:45 +02:00
x : data . cpu [ 1 ] * 1000 , // cpuGraph.options.scales.x.max can be used for window edge, if we don't trust server timestamps . but using server timestamps handles network lags better
2025-05-23 11:40:25 +02:00
y : data . cpu [ 0 ]
} ) ;
2025-05-23 15:08:01 +02:00
2025-05-23 16:11:48 +02:00
pruneGraphData ( cpuGraph . data . datasets [ 0 ] , cpuGraph . options ) ;
2025-05-21 18:35:55 +02:00
cpuGraph . update ( 'none' ) ;
}
2025-05-23 16:11:48 +02:00
memoryGraph . data . datasets [ 0 ] . data . push ( {
x : data . memory [ 1 ] * 1000 ,
y : ( data . memory [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 )
} ) ;
pruneGraphData ( memoryGraph . data . datasets [ 0 ] , memoryGraph . options ) ;
memoryGraph . data . datasets [ 1 ] . data . push ( {
x : data . swap [ 1 ] * 1000 ,
y : ( data . swap [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 )
} ) ;
pruneGraphData ( memoryGraph . data . datasets [ 1 ] , memoryGraph . options ) ;
2025-05-22 10:21:21 +02:00
memoryGraph . update ( 'none' ) ;
2025-05-21 18:35:55 +02:00
} ;
2025-05-23 15:08:01 +02:00
// advances the time window by 500ms. this is independent of incoming data
2025-05-23 11:40:25 +02:00
metricStream . intervalId = setInterval ( function ( ) {
cpuGraph . options . scales . x . min += LIVE _REFRESH _INTERVAL _MSECS ;
cpuGraph . options . scales . x . max += LIVE _REFRESH _INTERVAL _MSECS ;
cpuGraph . update ( 'none' ) ;
2025-05-23 16:11:48 +02:00
memoryGraph . options . scales . x . min += LIVE _REFRESH _INTERVAL _MSECS ;
memoryGraph . options . scales . x . max += LIVE _REFRESH _INTERVAL _MSECS ;
memoryGraph . update ( 'none' ) ;
2025-05-23 11:40:25 +02:00
} , LIVE _REFRESH _INTERVAL _MSECS ) ;
2025-05-21 18:35:55 +02:00
}
2025-05-23 11:40:25 +02:00
async function getMetrics ( hours ) {
if ( hours === 0 ) return { cpuData : [ ] , memoryData : [ ] , swapData : [ ] } ;
2025-05-21 18:35:55 +02:00
2025-05-23 11:40:25 +02:00
const [ error , result ] = await systemModel . getMetrics ( { fromSecs : hours * 60 * 60 , intervalSecs : 300 } ) ;
2025-05-21 18:35:55 +02:00
if ( error ) return console . error ( error ) ;
2025-05-22 20:54:42 +02:00
// time is converted to msecs . cpu is already scaled to cpu*100
const cpuData = result . cpu . map ( v => { return { x : v [ 1 ] * 1000 , y : v [ 0 ] } ; } ) ;
2025-05-21 18:35:55 +02:00
2025-05-22 20:54:42 +02:00
const memoryData = result . memory . map ( v => {
return {
x : v [ 1 ] * 1000 ,
y : ( v [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 )
} ;
} ) ;
2025-05-23 16:11:48 +02:00
2025-05-22 20:54:42 +02:00
const swapData = result . swap . map ( v => {
return {
x : v [ 1 ] * 1000 ,
y : ( v [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 )
} ;
} ) ;
return { cpuData , memoryData , swapData } ;
2025-05-22 18:45:05 +02:00
}
2025-07-01 22:15:13 +02:00
function createGraphOptions ( { yscale , realtime } ) {
2025-05-22 18:45:05 +02:00
const now = Date . now ( ) ;
2025-07-01 22:15:13 +02:00
return {
2025-05-22 19:51:41 +02:00
maintainAspectRatio : false ,
2025-05-21 18:35:55 +02:00
plugins : {
2025-05-23 15:08:01 +02:00
legend : {
display : false
} ,
tooltip : {
callbacks : {
title : ( tooltipItem ) => moment ( tooltipItem [ 0 ] . raw . x ) . format ( periods . find ( ( p ) => p . id === period . value ) . tooltipFormat )
}
}
2025-05-21 18:35:55 +02:00
} ,
scales : {
x : {
2025-05-23 15:08:01 +02:00
// we used to use 'time' type but it relies on the data to generate ticks. we may not have data for our time periods
type : 'linear' ,
2025-05-23 11:40:25 +02:00
min : now - ( period . value === 0 ? LIVE _REFRESH _HISTORY _MSECS : period . value * 60 * 60 * 1000 ) ,
2025-05-22 18:10:46 +02:00
max : now ,
2025-05-21 18:35:55 +02:00
ticks : {
autoSkip : true , // skip tick labels as needed
autoSkipPadding : 20 , // padding between ticks
maxRotation : 0 , // don't rotate the labels
2025-05-23 15:08:01 +02:00
count : 7 , // tick labels to show. anything more than 7 will not work for "7 days"
callback : function ( value ) {
if ( period . value === 0 ) return ` ${ 5 - ( value - this . min ) / 60000 } min ` ;
return moment ( value ) . format ( periods . find ( ( p ) => p . id === period . value ) . format ) ;
2025-07-01 22:15:13 +02:00
} ,
stepSize : realtime ? 60 * 1000 : null // // for realtime graph, generate steps of 1min and appropriate tick text
2025-05-22 18:17:42 +02:00
} ,
grid : {
drawOnChartArea : false ,
} ,
2025-05-21 18:35:55 +02:00
} ,
2025-07-01 22:15:13 +02:00
y : yscale ,
2025-05-21 18:35:55 +02:00
} ,
interaction : {
intersect : false ,
mode : 'nearest' ,
axis : 'x'
}
} ;
2025-07-01 22:15:13 +02:00
}
2025-05-21 18:35:55 +02:00
2025-07-01 22:15:13 +02:00
async function refresh ( ) {
const { cpuData , memoryData , swapData } = await getMetrics ( period . value ) ;
const cpuGraphData = {
datasets : [ {
label : 'CPU' ,
data : cpuData ,
pointRadius : 0 ,
// https://www.chartjs.org/docs/latest/charts/line.html#line-styling
borderWidth : 1 ,
tension : 0.4 ,
showLine : true ,
fill : true
} ]
} ;
const cpuYscale = {
type : 'linear' ,
min : 0 ,
max : systemCpus . length * 100 ,
ticks : {
callback : ( value ) => ` ${ value } % ` ,
maxTicksLimit : 6 // max tick labels to show
} ,
beginAtZero : true ,
} ;
const cpuGraphOptions = createGraphOptions ( { yscale : cpuYscale , realtime : period . value === 0 } ) ;
2025-05-23 11:40:25 +02:00
2025-05-23 13:24:45 +02:00
if ( ! cpuGraph ) {
cpuGraph = new Chart ( cpuGraphNode . value , { type : 'line' , data : cpuGraphData , options : cpuGraphOptions } ) ;
} else {
cpuGraph . data = cpuGraphData ;
cpuGraph . options = cpuGraphOptions ;
cpuGraph . update ( 'none' ) ;
}
2025-05-21 18:35:55 +02:00
const giB = 1024 * 1024 * 1024 ;
2025-05-22 18:10:46 +02:00
const roundedMemory = Math . ceil ( systemMemory . memory / giB ) * giB ; // we have to scale up so that the graph can show the data!
const roundedSwap = Math . ceil ( systemMemory . swap / giB ) * giB ;
2025-05-21 18:35:55 +02:00
const memoryGraphData = {
datasets : [ {
2025-05-22 10:21:21 +02:00
label : 'RAM' ,
2025-05-21 18:35:55 +02:00
data : memoryData ,
2025-05-22 10:21:21 +02:00
stack : 'memory+swap' ,
2025-05-21 18:35:55 +02:00
pointRadius : 0 ,
// https://www.chartjs.org/docs/latest/charts/line.html#line-styling
borderWidth : 1 ,
tension : 0.4 ,
showLine : true ,
2025-05-22 10:21:21 +02:00
fill : true ,
color : '#9ad0f5'
} , {
label : 'Swap' ,
data : swapData ,
stack : 'memory+swap' ,
pointRadius : 0 ,
// https://www.chartjs.org/docs/latest/charts/line.html#line-styling
borderWidth : 1 ,
tension : 0.4 ,
showLine : true ,
fill : true ,
color : '#ffb1c1'
2025-05-21 18:35:55 +02:00
} ]
} ;
2025-07-01 22:15:13 +02:00
const memoryYscale = {
type : 'linear' ,
min : 0 ,
max : ( roundedMemory + roundedSwap ) / giB ,
ticks : {
stepSize : 1 ,
autoSkip : true , // skip tick labels as needed
autoSkipPadding : 20 , // padding between ticks
callback : ( value ) => ` ${ value } GiB ` ,
maxTicksLimit : 8 // max tick labels to show
2025-05-21 18:35:55 +02:00
} ,
2025-07-01 22:15:13 +02:00
beginAtZero : true ,
stacked : true ,
2025-05-21 18:35:55 +02:00
} ;
2025-07-01 22:15:13 +02:00
const memoryGraphOptions = createGraphOptions ( { yscale : memoryYscale , realtime : period . value === 0 } ) ;
2025-05-23 16:11:48 +02:00
if ( ! memoryGraph ) {
memoryGraph = new Chart ( memoryGraphNode . value , { type : 'line' , data : memoryGraphData , options : memoryGraphOptions } ) ;
} else {
memoryGraph . data = memoryGraphData ;
memoryGraph . options = memoryGraphOptions ;
memoryGraph . update ( 'none' ) ;
}
2025-05-21 18:35:55 +02:00
if ( metricStream ) {
2025-05-23 11:40:25 +02:00
clearInterval ( metricStream . intervalId ) ;
2025-05-21 18:35:55 +02:00
metricStream . close ( ) ;
metricStream = null ;
}
if ( period . value === 0 ) liveRefresh ( ) ;
}
onMounted ( async ( ) => {
2025-05-22 18:45:05 +02:00
let error , result ;
[ error , result ] = await systemModel . memory ( ) ;
2025-05-21 18:35:55 +02:00
if ( error ) return console . error ( error ) ;
systemMemory = result ;
2025-05-22 18:45:05 +02:00
[ error , result ] = await systemModel . cpus ( ) ;
if ( error ) return console . error ( error ) ;
systemCpus = result ;
2025-05-21 18:35:55 +02:00
await refresh ( ) ;
} ) ;
2025-05-22 12:26:30 +02:00
onUnmounted ( async ( ) => {
2025-05-23 11:40:25 +02:00
if ( metricStream ) {
clearInterval ( metricStream . intervalId ) ;
metricStream . close ( ) ;
}
2025-05-22 12:26:30 +02:00
} ) ;
2025-05-21 18:35:55 +02:00
< / script >
< template >
< Section :title = "$t('system.graphs.title')" >
< template # header -buttons >
< SingleSelect @select ="refresh()" v-model = "period" :options="periods" option-key="id" option-label="label" />
< / template >
2025-05-22 19:51:41 +02:00
2025-05-21 18:35:55 +02:00
< div class = "graphs" >
2025-05-22 19:51:41 +02:00
< label > { { $t ( 'system.cpuUsage.title' ) } } < / label >
< div style = "text-align: center" v-if = "busy" > < Spinner / > < / div >
< div class = "graph" >
2025-05-21 18:35:55 +02:00
< canvas v-show = "!busy" ref="cpuGraphNode" > < / canvas >
< / div >
2025-05-22 19:51:41 +02:00
< label > { { $t ( 'system.systemMemory.title' ) } } < / label >
< div style = "text-align: center" v-if = "busy" > < Spinner / > < / div >
< div class = "graph" >
2025-05-21 18:35:55 +02:00
< canvas v-show = "!busy" ref="memoryGraphNode" > < / canvas >
< / div >
< / div >
< / Section >
< / template >
< style scoped >
2025-05-22 19:51:41 +02:00
. graphs label {
margin : 16 px 0 ;
}
. graph {
position : relative ;
width : 100 % ;
height : 160 px ;
2025-05-21 18:35:55 +02:00
}
< / style >