diff --git a/dashboard/src/components/SystemMetrics.vue b/dashboard/src/components/SystemMetrics.vue index cb75a0a0b..6a92fc4b1 100644 --- a/dashboard/src/components/SystemMetrics.vue +++ b/dashboard/src/components/SystemMetrics.vue @@ -38,7 +38,7 @@ const periods = [ ]; const busy = ref(false); -const period = ref(6); +const period = ref(0); const cpuGraphNode = useTemplateRef('cpuGraphNode'); const memoryGraphNode = useTemplateRef('memoryGraphNode'); @@ -48,16 +48,21 @@ let cpuGraph = null; let memoryGraph = null; let metricStream = null; +const LIVE_REFRESH_INTERVAL_MSECS = 500; +const LIVE_REFRESH_HISTORY_MSECS = 5*60*1000; // last 5 mins + async function liveRefresh() { - metricStream = await systemModel.getMetricStream(); + + metricStream = await systemModel.getMetricStream(LIVE_REFRESH_INTERVAL_MSECS); metricStream.onerror = (error) => console.log('event stream error:', error); metricStream.onmessage = (message) => { const data = JSON.parse(message.data); - // value can be null if no previous value - if (data.cpu[0]) { - cpuGraph.data.labels.push(moment(data.cpu[1]*1000).format('hh:mm')); - cpuGraph.data.datasets[0].data.push(data.cpu[0]); + if (data.cpu[0]) { // value can be null if no previous value + cpuGraph.data.datasets[0].data.push({ + x: cpuGraph.options.scales.x.max, // add to edge of our window. we could also trust server timestamp and use data.cpu[1] * 1000 + y: data.cpu[0] + }); cpuGraph.update('none'); } @@ -67,12 +72,18 @@ async function liveRefresh() { memoryGraph.update('none'); }; + // advances the window by 500ms. this is independent of incoming data + 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'); + }, LIVE_REFRESH_INTERVAL_MSECS); } -async function getMetrics(fromSecs) { - if (fromSecs === 0) return { cpuData: [], memoryData: [], swapData: [] }; +async function getMetrics(hours) { + if (hours === 0) return { cpuData: [], memoryData: [], swapData: [] }; - const [error, result] = await systemModel.getMetrics({ fromSecs: fromSecs * 60 * 60, intervalSecs: 300 }); + const [error, result] = await systemModel.getMetrics({ fromSecs: hours * 60 * 60, intervalSecs: 300 }); if (error) return console.error(error); // time is converted to msecs . cpu is already scaled to cpu*100 @@ -121,9 +132,10 @@ async function refresh() { }, scales: { x: { - type: 'time', - bounds: 'ticks', // otherwise data bound. https://www.chartjs.org/docs/latest/axes/cartesian/time.html#changing-the-scale-type-from-time-scale-to-logarithmic-linear-scale - min: now - period.value*60*60*1000, + // the 'time' type optimizes the x ticks text based on the data + type: period.value === 0 ? 'linear' : 'time', + bounds: 'ticks', // for 'time' type. otherwise data bound. https://www.chartjs.org/docs/latest/axes/cartesian/time.html#changing-the-scale-type-from-time-scale-to-logarithmic-linear-scale + min: now - (period.value === 0 ? LIVE_REFRESH_HISTORY_MSECS : period.value*60*60*1000), max: now, ticks: { autoSkip: true, // skip tick labels as needed @@ -153,6 +165,19 @@ async function refresh() { } }; + if (period.value === 0) { + // for realtime graph, generate steps of 1min and appropriate tick text + cpuGraphOptions.scales.x.ticks.stepSize = 60*1000; // 1min + cpuGraphOptions.scales.x.ticks.callback = function (value) { return `${5-(value-this.min)/60000}min`; }; + + // fix the hover tooltip to show timestamp + cpuGraphOptions.plugins.tooltip = { + callbacks: { + title: (tooltipItem) => moment(tooltipItem[0].raw.x).format('hh:mm:ss') + } + }; + } + if (cpuGraph) cpuGraph.destroy(); cpuGraph = new Chart(cpuGraphNode.value, { type: 'line', data: cpuGraphData, options: cpuGraphOptions }); @@ -235,6 +260,7 @@ async function refresh() { busy.value = false; if (metricStream) { + clearInterval(metricStream.intervalId); metricStream.close(); metricStream = null; } @@ -258,7 +284,10 @@ onMounted(async () => { }); onUnmounted(async () => { - if (metricStream) metricStream.close(); + if (metricStream) { + clearInterval(metricStream.intervalId); + metricStream.close(); + } }); diff --git a/dashboard/src/models/SystemModel.js b/dashboard/src/models/SystemModel.js index 736a5140b..41bf4a42c 100644 --- a/dashboard/src/models/SystemModel.js +++ b/dashboard/src/models/SystemModel.js @@ -94,8 +94,8 @@ function create() { if (error || result.status !== 200) return [error || result]; return [null, result.body]; }, - async getMetricStream() { - return new EventSource(`${API_ORIGIN}/api/v1/system/metricstream?access_token=${accessToken}&intervalMsecs=500`); + async getMetricStream(intervalMsecs) { + return new EventSource(`${API_ORIGIN}/api/v1/system/metricstream?access_token=${accessToken}&intervalMsecs=${intervalMsecs}`); } }; }