stackviz/app/js/directives/timeline-dstat.js

451 lines
14 KiB
JavaScript

'use strict';
var directivesModule = require('./_index.js');
var d3Ease = require('d3-ease');
var d3Interpolate = require('d3-interpolate');
var d3Scale = require('d3-scale');
var arrayUtil = require('../util/array-util');
var parseDstat = require('../util/dstat-parse');
var getDstatLanes = function(data, mins, maxes) {
if (!data || !data.length) {
return [];
}
var row = data[0];
var lanes = [];
if ('total_cpu_usage_usr' in row && 'total_cpu_usage_sys' in row) {
lanes.push([{
scale: d3Scale.scaleLinear().domain([0, 100]),
value: function(d) {
return d.total_cpu_usage_wai;
},
color: "rgba(224, 188, 188, 1)",
text: "CPU wait"
}, {
scale: d3Scale.scaleLinear().domain([0, 100]),
value: function(d) {
return d.total_cpu_usage_usr + d.total_cpu_usage_sys;
},
color: "rgba(102, 140, 178, 0.75)",
text: "CPU (user+sys)"
}]);
}
if ('memory_usage_used' in row) {
lanes.push([{
scale: d3Scale.scaleLinear().domain([0, maxes.memory_usage_used]),
value: function(d) { return d.memory_usage_used; },
color: "rgba(102, 140, 178, 0.75)",
text: "Memory"
}]);
}
if ('net_total_recv' in row && 'net_total_send' in row) {
lanes.push([{
scale: d3Scale.scaleLinear().domain([0, maxes.net_total_recv]),
value: function(d) { return d.net_total_recv; },
color: "rgba(224, 188, 188, 1)",
text: "Net Down"
}, {
scale: d3Scale.scaleLinear().domain([0, maxes.net_total_send]),
value: function(d) { return d.net_total_send; },
color: "rgba(102, 140, 178, 0.75)",
text: "Net Up",
type: "line"
}]);
}
if ('dsk_total_read' in row && 'dsk_total_writ' in row) {
lanes.push([{
scale: d3Scale.scaleLinear().domain([0, maxes.dsk_total_read]),
value: function(d) { return d.dsk_total_read; },
color: "rgba(224, 188, 188, 1)",
text: "Disk Read",
type: "line"
}, {
scale: d3Scale.scaleLinear().domain([0, maxes.dsk_total_writ]),
value: function(d) { return d.dsk_total_writ; },
color: "rgba(102, 140, 178, 0.75)",
text: "Disk Write",
type: "line"
}]);
}
return lanes;
};
function timelineDstat($document, $window) {
var link = function(scope, el, attrs, timelineController) {
// local display variables
var margin = timelineController.margin;
var height = 140;
var laneDefs = [];
var laneHeight = 30;
var loaded = false;
// axes and dstat-global variables
var absolute = timelineController.axes.absolute;
var xSelected = timelineController.axes.selection;
var y = d3Scale.scaleLinear();
// animation variables
var currentViewExtents = null;
var viewInterpolator = null;
var easeOutCubic = d3Ease.easeCubicOut;
var easeStartTimestamp = null;
var easeDuration = 500;
// canvases and layers
var regions = [];
var lanes = timelineController.createCanvas(null, height);
var main = timelineController.createCanvas(null, height, false);
el.append(main.canvas);
/**
* Generate the list of active regions or "chunks". These regions span a
* fixed area of the timeline's full "virtual" width, and contain only a
* small subset of data points that fall within the area. This function only
* initializes a list of regions, but does not actually attempt to draw
* anything. Drawing can be handled lazily and will only occur when a
* region's 'dirty' property is set. If a list of regions already exists,
* it will be thrown away and replaced with a new list; this should occur
* any time the full "virtual" timeline width changes (such as a extent
* resize), or if the view extents no longer fall within the generated list
* of regions.
*
* This function will limit the number of generated regions. If this is not
* sufficient to cover the entire area spanned by the timeline's virtual
* width, regions will be generated around the user's current viewport.
*
* Note that individual data points will exist within multiple regions if
* they span region borders. In this case, each containing region will have
* a unique rect instance pointing to the same data point.
*/
function createRegions() {
regions = [];
var fullWidth = absolute(timelineController.timeExtents[1]);
var chunkWidth = 500;
var chunks = Math.ceil(fullWidth / chunkWidth);
var offset = 0;
// avoid creating lots of chunks - cap and only generate around the
// current view
// if we scroll out of bounds of the chunks we *do* have, we can throw
// away our regions + purge regions in memory
if (chunks > 30) {
var startX = absolute(timelineController.viewExtents[0]);
var endX = absolute(timelineController.viewExtents[1]);
var midX = startX + (endX - startX) / 2;
chunks = 50;
offset = Math.max(0, midX - (chunkWidth * 15));
}
for (var i = 0; i < chunks; i++) {
// for each desired chunk, find the bounds and managed data points
// then, calculate positions for each data point
var w = Math.min(fullWidth - offset, chunkWidth);
var min = absolute.invert(offset);
var max = absolute.invert(offset + w);
var data = timelineController.dstatInBounds(min, max);
regions.push({
x: offset, width: w, min: min, max: max,
data: data,
c: null,
dirty: true,
index: regions.length
});
offset += w;
}
}
/**
* Finds all regions falling within the given minimum and maximum absolute
* x coordinates.
* @param {number} minX the minimum x coordinate (exclusive)
* @param {number} maxX the maximum x coording (exclusive)
* @return {object[]} a list of matching regions
*/
function getContainedRegions(minX, maxX) {
return regions.filter(function(region) {
return (region.x + region.width) > minX && region.x < maxX;
});
}
/**
* Draw lane labels into the offscreen lanes canvas.
*/
function drawLanes() {
// make sure the canvas is the correct size and clear it
lanes.resize(timelineController.width + margin.left + margin.right);
lanes.ctx.clearRect(0, 0, lanes.canvas.width, lanes.canvas.height);
lanes.ctx.strokeStyle = 'lightgray';
lanes.ctx.textAlign = 'end';
lanes.ctx.textBaseline = 'middle';
lanes.ctx.font = '10px sans-serif';
// draw lanes for each worker
var laneHeight = 0.8 * y(1);
for (var i = 0; i < laneDefs.length; i++) {
var laneDef = laneDefs[i];
var yPos = y(i + 0.5);
var dy = 0;
for (var pathIndex = 0; pathIndex < laneDef.length; pathIndex++) {
var pathDef = laneDef[pathIndex];
pathDef.scale.range([laneHeight, 0]);
// draw labels right-aligned to the left of each lane
if ('text' in pathDef) {
lanes.ctx.fillStyle = pathDef.color;
lanes.ctx.fillText(
pathDef.text,
margin.left - margin.right, yPos + dy,
margin.left - 10);
dy += 10;
}
}
}
}
/**
* Draw the given region into its own canvas. The region will only be drawn
* if it is marked as dirty. If its canvas has not yet been created, it will
* be initialized automatically. Note that this does not actually draw
* anything to the screen (i.e. main canvas), as this result only populates
* each region's local offscreen image with content. drawAll() will actually
* draw to the screen (and implicitly calls this function as well).
* @param {object} region the region to draw
*/
function drawRegion(region) {
if (!region.dirty) {
// only redraw if dirty
return;
}
if (!region.c) {
// create the actual image buffer lazily - don't waste memory if it will
// never be seen
region.c = timelineController.createCanvas(region.width, height);
}
var ctx = region.c.ctx;
ctx.clearRect(0, 0, region.width, height);
ctx.strokeStyle = 'rgb(175, 175, 175)';
ctx.lineWidth = 1;
for (var laneIndex = 0; laneIndex < laneDefs.length; laneIndex++) {
var laneDef = laneDefs[laneIndex];
var bottom = y(laneIndex) + laneHeight;
for (var pathIndex = 0; pathIndex < laneDef.length; pathIndex++) {
if (!region.data.length) {
continue;
}
var pathDef = laneDef[pathIndex];
var line = pathDef.type === 'line';
ctx.strokeStyle = pathDef.color;
ctx.fillStyle = pathDef.color;
var first = region.data[0];
ctx.beginPath();
ctx.moveTo(
absolute(+first.system_time) - region.x,
y(laneIndex) + pathDef.scale(pathDef.value(first)));
for (var i = 1; i < region.data.length; i++) {
var d = region.data[i];
ctx.lineTo(
absolute(+d.system_time) - region.x,
y(laneIndex) + pathDef.scale(pathDef.value(d)));
}
if (line) {
ctx.stroke();
} else {
var last = region.data[region.data.length - 1];
ctx.lineTo(absolute(+last.system_time) - region.x, bottom);
ctx.lineTo(absolute(+first.system_time) - region.x, bottom);
ctx.fill();
}
}
}
region.dirty = false;
}
/**
* Draw all layers and visible regions on the screen.
*/
function drawAll() {
if (!currentViewExtents) {
currentViewExtents = timelineController.viewExtents;
}
// update size of main canvas
var w = timelineController.width + margin.left + margin.right;
var e = angular.element(main.canvas);
main.resize(w);
var s = function(v) {
return v * main.ratio;
};
main.ctx.clearRect(0, 0, main.canvas.width, main.canvas.height);
main.ctx.drawImage(lanes.canvas, 0, 0);
// draw all visible regions
var startX = absolute(currentViewExtents[0]);
var endX = absolute(currentViewExtents[1]);
var viewRegions = getContainedRegions(startX, endX);
var effectiveWidth = 0;
viewRegions.forEach(function(region) {
effectiveWidth += region.width;
});
if (effectiveWidth < timelineController.width) {
// we had to cap the region generation previously, but moved outside of
// the generated area, so regenerate regions around the current view
createRegions();
viewRegions = getContainedRegions(startX, endX);
}
viewRegions.forEach(function(region) {
drawRegion(region);
// calculate the cropping area and offsets needed to place the region
// in the main canvas
var sx1 = Math.max(0, startX - region.x);
var sx2 = Math.min(region.width, endX - region.x);
var sw = sx2 - sx1;
var dx = Math.max(0, startX - region.x);
if (Math.floor(sw) === 0) {
return;
}
main.ctx.drawImage(
region.c.canvas,
s(sx1), 0, Math.floor(s(sw)), s(height),
s(margin.left + region.x - startX + sx1), 0, s(sw), s(height));
});
}
timelineController.animateCallbacks.push(function(timestamp) {
if (!loaded) {
return false;
}
if (viewInterpolator) {
// start the animation
var currentSize = currentViewExtents[1] - currentViewExtents[0];
var newSize = timelineController.viewExtents[1] - timelineController.viewExtents[0];
var diffSize = currentSize - newSize;
var diffTime = timestamp - easeStartTimestamp;
var pct = diffTime / easeDuration;
// interpolate the current view bounds according to the easing method
currentViewExtents = viewInterpolator(easeOutCubic(pct));
if (Math.abs(diffSize) > 1) {
// size has changed, recalculate regions
createRegions();
}
drawAll();
if (pct >= 1) {
// finished, clear the state vars
easeStartTimestamp = null;
viewInterpolator = null;
return false;
} else {
// request more frames until finished
return true;
}
} else {
// if there is no view interpolator function, just do a plain redraw
drawAll();
return false;
}
});
scope.$on('dstatLoaded', function(event, dstat) {
laneDefs = getDstatLanes(dstat.entries, dstat.minimums, dstat.maximums);
laneHeight = height / (laneDefs.length + 1);
y.domain([0, laneDefs.length]).range([0, height]);
drawLanes();
createRegions();
loaded = true;
});
scope.$on('update', function() {
if (!loaded) {
return;
}
drawLanes();
createRegions();
timelineController.animate();
});
scope.$on('updateViewSize', function() {
if (!loaded) {
return;
}
if (currentViewExtents) {
// if we know where the view is already, try to animate the transition
viewInterpolator = d3Interpolate.interpolateArray(
currentViewExtents,
timelineController.viewExtents);
easeStartTimestamp = performance.now();
} else {
// otherwise, move directly to the new location/size (we will need to
// rebuild regions)
createRegions();
}
timelineController.animate();
});
scope.$on('updateViewPosition', function() {
if (!loaded) {
return;
}
if (currentViewExtents) {
// if we know where the view is already, try to animate the transition
viewInterpolator = d3Interpolate.interpolateArray(
currentViewExtents,
timelineController.viewExtents);
easeStartTimestamp = performance.now();
}
timelineController.animate();
});
};
return {
restrict: 'E',
require: '^timeline',
scope: true,
link: link
};
}
directivesModule.directive('timelineDstat', timelineDstat);