diff --git a/dashboard/src/components/SystemMetrics.vue b/dashboard/src/components/SystemMetrics.vue index ebff92a6d..029da8cba 100644 --- a/dashboard/src/components/SystemMetrics.vue +++ b/dashboard/src/components/SystemMetrics.vue @@ -34,7 +34,7 @@ const periods = [ ]; const busy = ref(false); -const period = ref(6); +const period = ref(0); const cpuGraphNode = useTemplateRef('cpuGraphNode'); const memoryGraphNode = useTemplateRef('memoryGraphNode'); @@ -56,11 +56,11 @@ async function liveRefresh() { cpuGraph.update('none'); } - if (data.memory[0]) { - memoryGraph.data.labels.push(moment(data.memory[1]*1000).format('hh:mm')); - memoryGraph.data.datasets[0].data.push((data.memory[0] / 1024 / 1024 / 1024).toFixed(2)); - memoryGraph.update('none'); - } + memoryGraph.data.labels.push(moment(data.memory[1]*1000).format('hh:mm')); + memoryGraph.data.datasets[0].data.push((data.memory[0] / 1024 / 1024 / 1024).toFixed(2)); + memoryGraph.data.datasets[1].data.push((data.swap[0] / 1024 / 1024 / 1024).toFixed(2) + 2); + + memoryGraph.update('none'); }; } @@ -106,9 +106,7 @@ async function refresh() { }, y: { ticks: { - callback: (value) => { - return `${value}%`; - }, + callback: (value) => `${value}%`, maxTicksLimit: 6 // max tick labels to show }, min: 0, @@ -126,7 +124,6 @@ async function refresh() { if (cpuGraph) cpuGraph.destroy(); cpuGraph = new Chart(cpuGraphNode.value, { type: 'line', data: cpuGraphData, options: cpuGraphOptions }); - // memory const memoryLabels = result.memory.map(v => { return moment(v[1]*1000).format('hh:mm'); }); @@ -135,28 +132,49 @@ async function refresh() { return (v[0] / 1024 / 1024 / 1024).toFixed(2); }); + const swapData = result.swap.map(v => { // assume that there is 1:1 timeline for swap and memory + return (v[0] / 1024 / 1024 / 1024).toFixed(2); + }); + const giB = 1024 * 1024 * 1024; const quarterGiB = 0.25 * giB; const roundedMemory = Math.round(systemMemory.memory / quarterGiB) * quarterGiB; - const roundedMemoryGiB = (roundedMemory / giB).toFixed(2); + const roundedSwap = Math.round(systemMemory.swap / quarterGiB) * quarterGiB; const memoryGraphData = { labels: memoryLabels, datasets: [{ - label: 'Memory', + label: 'RAM', data: memoryData, + 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 + 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' }] }; const memoryGraphOptions = { plugins: { - legend: false + legend: { + display: true, + position: 'bottom', + align: 'center' + } }, scales: { x: { @@ -169,14 +187,18 @@ async function refresh() { }, y: { ticks: { - callback: (value) => { - return `${value} GiB`; + callback: (value) => `${value} GiB`, + color: (value /* ,index, ticks */) => { + const tickValue = parseFloat(value['tick']['value']); + console.log(value); + return ((tickValue * 1024 * 1024 * 1024) > roundedMemory) ? '#ff7d98' : '#46a9ec'; }, maxTicksLimit: 6 // max tick labels to show }, min: 0, - max: roundedMemoryGiB, + max: ((roundedMemory + roundedSwap)/ giB).toFixed(2), // string beginAtZero: true, + stacked: true, } }, interaction: { diff --git a/src/metrics.js b/src/metrics.js index 9bdd1c957..d5f26ec9f 100644 --- a/src/metrics.js +++ b/src/metrics.js @@ -97,11 +97,18 @@ async function getMemoryMetrics() { // we can also read /proc/meminfo but complicated to match the 'used' output of free const output = execSync('free --bytes --wide', { encoding: 'utf8' }).trim(); // --line is not in older ubuntu const memoryRe = /Mem:\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)/; + const swapRe = /Swap:\s+(?\d+)\s+(?\d+)\s+(?\d+)/; const memory = output.match(memoryRe); if (!memory) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not find memory used'); - return { used: parseInt(memory.groups.used, 10) }; + const swap = output.match(swapRe); + if (!swap) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not find swap used'); + + return { + memoryUsed: parseInt(memory.groups.used, 10), + swapUsed: parseInt(swap.groups.used, 10) + }; } async function getCpuMetrics() { @@ -117,7 +124,8 @@ async function sendToGraphite() { const graphiteMetrics = []; const memoryMetrics = await getMemoryMetrics(); - graphiteMetrics.push({ path: `cloudron.system.memory-used`, value: memoryMetrics.used }); + graphiteMetrics.push({ path: `cloudron.system.memory-used`, value: memoryMetrics.memoryUsed }); + graphiteMetrics.push({ path: `cloudron.system.swap-used`, value: memoryMetrics.swapUsed }); const cpuMetrics = await getCpuMetrics(); graphiteMetrics.push({ path: `cloudron.system.cpu-user`, value: cpuMetrics.userMsecs }); @@ -242,7 +250,8 @@ async function readSystemFromGraphite(options) { // (cpu usage msecs) / (cpus * 1000) is the percent but over all cpus. times 100 is the percent. // but the y-scale is cpus times 100. so, we only need to scale by 0.1 `scale(perSecond(sumSeries(cloudron.system.cpu-user,cloudron.system.cpu-sys)),0.1)`, - `summarize(cloudron.system.memory-used, "${intervalSecs}s", "avg")` + `summarize(cloudron.system.memory-used, "${intervalSecs}s", "avg")`, + `summarize(cloudron.system.swap-used, "${intervalSecs}s", "avg")`, ]; const results = []; @@ -265,7 +274,8 @@ async function readSystemFromGraphite(options) { return { cpu: results[0], - memory: results[1] + memory: results[1], + swap: results[2] }; } @@ -287,6 +297,7 @@ async function getSystem(options) { return { cpu: systemStats.cpu, memory: systemStats.memory, + swap: systemStats.swap, apps: appStats, services: serviceStats, cpuCount: os.cpus().length @@ -315,7 +326,8 @@ async function getSystemStream() { const now = Date.now(); metricsStream.push(JSON.stringify({ cpu: [ cpuPercent, now ], - memory: [ memoryMetrics.used, now ] + memory: [ memoryMetrics.memoryUsed, now ], + swap: [ memoryMetrics.swapUsed, now ], })); }, INTERVAL_SECS*1000);