ui/app/components/info-multi-stats/component.js

709 lines
15 KiB
JavaScript

import Ember from 'ember';
import MultiStatsSocket from 'ui/utils/multi-stats';
import { formatPercent, formatMib, formatKbps, pluralize } from 'ui/utils/util';
const MAX_POINTS = 60;
const TICK_COUNT = 6;
const GRADIENT_COLORS = [
{
type: 'cpu',
colors: ['#2ecc71', '#DBE8B1']
},
{
type: 'memory',
colors: ['#00558b', '#AED6F1']
},
{
type: 'network',
colors: ['#e49701', '#f1c40f']
},
{
type: 'storage',
colors: ['#3a6f81', '#ABCED3']
},
];
export default Ember.Component.extend({
model: null,
linkName: 'containerStats',
single: true,
showGraphs: true,
showMultiStat: true,
renderSeconds: null,
statsSocket: null,
available: Ember.computed.alias('statsSocket.available'),
active: Ember.computed.alias('statsSocket.active'),
loading: Ember.computed.alias('statsSocket.loading'),
cpuCanvas: '#cpuGraph',
cpuGraph: null,
cpuData: null,
setCpuScale: false,
cpuD3Data: null,
memoryCanvas: '#memoryGraph',
memoryGraph: null,
memoryData: null,
useMemoryLimit: true,
setMemoryScale: false,
storageCanvas: '#storageGraph',
storageGraph: null,
storageData: null,
networkCanvas: '#networkGraph',
networkGraph: null,
networkData: null,
renderOk: false,
renderTimer: null,
didReceiveAttrs() {
if ( this.get('statsSocket') )
{
this.disconnect();
this.tearDown();
}
this.connect();
},
// The SVG gradients have the path name in them, so they have to be updated when the route changes.
routeChanged: function() {
Ember.run.next(() => {
let graphs = [this.get('cpuGraph'), this.get('memoryGraph'), this.get('storageGraph'), this.get('networkGraph')];
graphs.forEach((graph) => {
try {
let colors = graph.internal.config.data_colors;
Object.keys(colors).forEach((key) => {
let neu = 'url(' + window.location.pathname + colors[key].replace(/^[^#]+/,'');
colors[key] = neu;
});
} catch (e) {
// eh....
}
});
});
}.observes('application.currentRouteName'),
willDestroyElement: function() {
this._super();
this.disconnect();
this.tearDown();
},
onActiveChanged: function() {
if ( this.get('active') )
{
this.setUp();
}
else
{
this.tearDown();
}
}.observes('active'),
connect() {
Ember.run.next(() => {
try {
var stats = MultiStatsSocket.create({
resource: this.get('model'),
linkName: this.get('linkName'),
});
this.set('statsSocket',stats);
stats.on('dataPoint', (data) => { this.onDataPoint(data); });
} catch(e) {
}
});
},
disconnect() {
var stats = this.get('statsSocket');
if ( stats )
{
stats.close();
}
},
setUp() {
this.set('renderOk', false);
this.set('setMemoryScale', false);
this.set('setCpuScale', false);
if ( this.get('cpuCanvas') )
{
this.initCpuGraph();
}
if ( this.get('memoryCanvas') )
{
this.initMemoryGraph();
}
if ( this.get('storageCanvas') )
{
this.initStorageGraph();
}
if ( this.get('networkCanvas') )
{
this.initNetworkGraph();
}
this.setupMarkers();
this.startTimer();
},
startTimer() {
clearInterval(this.get('renderTimer'));
var interval = this.get('renderSeconds');
if ( isNaN(interval) || interval < 1 )
{
interval = 1;
}
this.set('renderTimer', setInterval(this.renderGraphs.bind(this), interval*1000));
},
renderSecondsChanged: function() {
this.startTimer();
}.observes('renderSeconds'),
setupMarkers: function() {
var svg = d3.select('body').append('svg:svg');
svg.attr('height','0');
svg.attr('width','0');
svg.style('position','absolute');
var defs = svg.append('svg:defs');
GRADIENT_COLORS.forEach((v) => {
var type = v.type;
v.colors.forEach((val, idx) => {
var gradient = defs.append("svg:linearGradient")
.attr('id', `${type}-${idx}-gradient`)
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '100%')
.attr('spreadMethod', 'pad');
gradient.append('svg:stop')
.attr('offset', '0%')
.attr('stop-color', val)
.attr('stop-opacity', '1');
gradient.append('svg:stop')
.attr('offset', '100%')
.attr('stop-color', val)
.attr('stop-opacity', '.4');
});
});
},
tearDown() {
['cpuGraph','memoryGraph','storageGraph','networkGraph'].forEach((key) => {
var obj = this.get(key);
if ( obj )
{
obj.destroy();
}
});
this.setProperties({
cpuGraph: null,
memoryGraph: null,
storageGraph: null,
networkGraph: null,
cpuData: null,
memoryData: null,
storageData: null,
networkData: null,
setMemoryScale: false,
setCpuScale: false,
renderSeconds: null,
});
},
onDataPoint(point) {
if ( this.isDestroyed || this.isDestroying ) {
return;
}
var didSetCpuScale = false;
var didSetMemoryScale = false;
if ( point.time_diff_ms )
{
var seconds = Math.max(1, Math.round(point.time_diff_ms/1000));
if ( this.get('renderSeconds') )
{
seconds = Math.min(seconds, this.get('renderSeconds'));
}
//console.log('point', point.time_diff_ms, seconds, this.get('renderSeconds'));
this.set('renderSeconds', seconds);
}
// CPU
var row;
var graph = this.get('cpuGraph');
var data = this.get('cpuData');
if ( data && (typeof point.cpu_total !== 'undefined') )
{
if ( this.get('single') )
{
row = getOrCreateDataRow(graph, data, 'System');
row.push(point.cpu_system);
row = getOrCreateDataRow(graph, data, 'User');
row.push(point.cpu_user);
}
else
{
row = getOrCreateDataRow(graph, data, point.key);
row.push(point.cpu_total);
}
this.set('cpuD3Data', data);
if ( point.cpu_count && this.get('renderOk') && !this.get('setCpuScale') )
{
graph.axis.max(point.cpu_count*100);
didSetCpuScale = true;
}
}
// Memory
graph = this.get('memoryGraph');
data = this.get('memoryData');
if ( data && (typeof point.mem_used_mb !== 'undefined') )
{
if ( this.get('single') )
{
row = getOrCreateDataRow(graph, data, 'Used');
}
else
{
row = getOrCreateDataRow(graph, data, point.key);
}
row.push(point.mem_used_mb);
var max = Math.ceil(point.mem_total_mb || this.get('model.info.memoryInfo.memTotal'));
if ( max && this.get('renderOk') && !this.get('setMemoryScale') )
{
graph.axis.max(max);
didSetMemoryScale = true;
}
}
// Network
graph = this.get('networkGraph');
data = this.get('networkData');
if ( data && (typeof point.net_rx_kb !== 'undefined') )
{
if ( this.get('single') )
{
row = getOrCreateDataRow(graph, data, 'Transmit');
row.push(point.net_tx_kb*8);
row = getOrCreateDataRow(graph, data, 'Receive');
row.push(point.net_rx_kb*8);
}
else
{
row = getOrCreateDataRow(graph, data, point.key);
row.push(point.net_rx_kb + point.net_tx_kb);
}
}
// Storage
graph = this.get('storageGraph');
data = this.get('storageData');
if ( data && (typeof point.disk_read_kb !== 'undefined') )
{
if ( this.get('single') )
{
row = getOrCreateDataRow(graph, data, 'Write');
row.push(point.disk_write_kb*8);
row = getOrCreateDataRow(graph, data, 'Read');
row.push(point.disk_read_kb*8);
}
else
{
row = getOrCreateDataRow(graph, data, point.key);
row.push(point.disk_read_kb + point.disk_write_kb);
}
}
if ( didSetMemoryScale )
{
this.set('setMemoryScale', true);
}
if ( didSetCpuScale )
{
this.set('setCpuScale', true);
}
this.set('renderOk', true);
},
initCpuGraph() {
var store = this.get('store');
var single = this.get('single');
//console.log('Init CPU');
var x = ['x'];
for ( var i = 0 ; i < MAX_POINTS ; i++ )
{
x.push(i);
}
this.set('cpuData', [x]);
this.set('cpuD3Data', [x]);
var cpuGraph = c3.generate({
padding: {
top: 5,
left: 75
},
bindto: this.get('cpuCanvas'),
size: {
height: 110,
},
data: {
type: 'area-step',
x: 'x',
columns: this.get('cpuData'),
groups: [[]], // Stacked graph, populated by getOrCreateDataRow...
order: null,
colors: {
System: `url(${window.location.pathname}#cpu-0-gradient)`,
User: `url(${window.location.pathname}#cpu-1-gradient)`,
},
},
transition: { duration: 0 },
legend: { show: false },
tooltip: {
show: true,
format: {
title: formatSecondsAgo.bind(this),
name: formatKey.bind(this,single,store),
value: formatPercent,
}
},
axis: {
x: {
show: false,
},
y: {
min: 0,
max: 100,
padding: {
top: 0,
left: 0,
bottom: 0,
right: 0
},
tick: {
count: TICK_COUNT,
format: formatPercent,
},
},
},
});
this.set('cpuGraph', cpuGraph);
},
initMemoryGraph() {
var store = this.get('store');
var single = this.get('single');
//console.log('Init Memory');
var x = ['x'];
for ( var i = 0 ; i < MAX_POINTS ; i++ )
{
x.push(i);
}
this.set('memoryData', [x]);
var memoryGraph = c3.generate({
padding: {
top: 5,
left: 75
},
bindto: this.get('memoryCanvas'),
size: {
height: 110,
},
data: {
type: 'area-step',
x: 'x',
columns: this.get('memoryData'),
groups: [[]], // Stacked graph, populated by getOrCreateDataRow...
colors: {
Used: `url(${window.location.pathname}#memory-0-gradient)`
},
},
transition: { duration: 0 },
legend: { show: false },
tooltip: {
show: true,
format: {
title: formatSecondsAgo.bind(this),
name: formatKey.bind(this,single,store),
value: formatMib,
}
},
axis: {
x: {
show: false,
},
y: {
min: 0,
padding: {
top: 0,
left: 0,
bottom: 0,
right: 0
},
tick: {
count: TICK_COUNT,
format: formatMib,
},
},
}
});
this.set('memoryGraph', memoryGraph);
},
initStorageGraph() {
var store = this.get('store');
var single = this.get('single');
//console.log('Init Storage');
var x = ['x'];
for ( var i = 0 ; i < MAX_POINTS ; i++ )
{
x.push(i);
}
this.set('storageData', [x]);
var storageGraph = c3.generate({
padding: {
top: 5,
left: 75
},
bindto: this.get('storageCanvas'),
size: {
height: 110,
},
data: {
type: 'area-step',
x: 'x',
columns: this.get('storageData'),
groups: [[]], // Stacked graph, populated by getOrCreateDataRow...
order: null,
colors: {
Write: `url(${window.location.pathname}#storage-0-gradient)`,
Read: `url(${window.location.pathname}#storage-1-gradient)`
},
},
transition: { duration: 0 },
legend: { show: false },
tooltip: {
show: true,
format: {
title: formatSecondsAgo.bind(this),
name: formatKey.bind(this,single,store),
value: formatKbps,
},
},
axis: {
x: {
show: false,
},
y: {
padding: {
top: 0,
left: 0,
bottom: 0,
right: 0
},
tick: {
count: TICK_COUNT,
format: formatKbps,
},
},
}
});
this.set('storageGraph', storageGraph);
},
initNetworkGraph() {
var store = this.get('store');
var single = this.get('single');
//console.log('Init Network');
var x = ['x'];
var z = [];
for ( var i = 0 ; i < MAX_POINTS ; i++ )
{
x.push(i);
z.push(i);
}
this.set('networkData', [x]);
this.set('networkD3Data', z);
var networkGraph = c3.generate({
padding: {
top: 5,
left: 75
},
bindto: this.get('networkCanvas'),
size: {
height: 110,
},
data: {
type: 'area-step',
x: 'x',
columns: this.get('networkData'),
groups: [[]], // Stacked graph, populated by getOrCreateDataRow...
order: null,
colors: {
Transmit: `url(${window.location.pathname}#network-0-gradient)`,
Receive: `url(${window.location.pathname}#network-1-gradient)`
},
},
transition: { duration: 0 },
legend: { show: false },
tooltip: {
show: true,
format: {
title: formatSecondsAgo.bind(this),
name: formatKey.bind(this,single,store),
value: formatKbps,
},
},
axis: {
x: {
show: false,
},
y: {
padding: {
top: 0,
left: 0,
bottom: 0,
right: 0
},
tick: {
count: TICK_COUNT,
format: formatKbps,
},
},
}
});
this.set('networkGraph', networkGraph);
},
renderGraphs() {
['cpu','memory','storage','network'].forEach((key) => {
var graph = this.get(key+'Graph');
var data = this.get(key+'Data');
if ( graph && data )
{
graph.load({columns: data});
// Remove the oldest point
for ( var i = 1 ; i < data.length ; i++ )
{
data[i].splice(1, Math.max(0, data[i].length - 1 - MAX_POINTS));
}
}
});
},
});
function getOrCreateDataRow(graph, data, key) {
var i;
for ( i = 0 ; i < data.length ; i++ )
{
if ( data[i][0] === key )
{
return data[i];
}
}
// Create a new row and backfill with 0's
var newRow = [key];
for ( i = 1 ; i < data[0].length ; i++ )
{
newRow.push(0);
}
data.push(newRow);
// Add the new key to the graph stack
var groups = graph.groups();
if ( groups.length )
{
groups[0].push(key);
graph.groups(groups);
}
return newRow;
}
function formatSecondsAgo(d) {
var ago = Math.max(0,MAX_POINTS - d - 1) * this.get('renderSeconds');
if ( ago === 0 )
{
return 'Now';
}
if ( ago >= 60 )
{
var min = Math.floor(ago/60);
var sec = ago - 60*min;
if ( sec > 0 )
{
return `${min} min, ${sec} sec ago`;
}
else
{
return pluralize(min,'minute') + ' ago';
}
}
return pluralize(ago,'second') + ' ago';
}
function formatKey(single, store, key /*, ratio, _id, index*/) {
if ( single )
{
return key;
}
var [type, id] = key.split('/');
var obj = store.getById(type, id);
if ( obj )
{
return obj.get('displayName');
}
else
{
return key;
}
}