662 lines
18 KiB
JavaScript
662 lines
18 KiB
JavaScript
/*
|
|
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/*global d3:false*/
|
|
|
|
var statusColorMap = {
|
|
"success": "LightGreen",
|
|
"fail": "Crimson",
|
|
"skip": "DodgerBlue"
|
|
};
|
|
|
|
var binaryMinIndex = function(min, array, func) {
|
|
"use strict";
|
|
|
|
var left = 0;
|
|
var right = array.length - 1;
|
|
|
|
while (left < right) {
|
|
var mid = Math.floor((left + right) / 2);
|
|
|
|
if (min < func(array[mid])) {
|
|
right = mid - 1;
|
|
} else if (min > func(array[mid])) {
|
|
left = mid + 1;
|
|
} else {
|
|
right = mid;
|
|
}
|
|
}
|
|
|
|
if (left >= array.length) {
|
|
return array.length - 1;
|
|
} else if (func(array[left]) <= min) {
|
|
return left;
|
|
} else {
|
|
return left - 1;
|
|
}
|
|
};
|
|
|
|
var binaryMaxIndex = function(max, array, func) {
|
|
"use strict";
|
|
|
|
var left = 0;
|
|
var right = array.length - 1;
|
|
|
|
while (left < right) {
|
|
var mid = Math.floor((left + right) / 2);
|
|
|
|
if (max < func(array[mid])) {
|
|
right = mid - 1;
|
|
} else if (max > func(array[mid])) {
|
|
left = mid + 1;
|
|
} else {
|
|
right = mid;
|
|
}
|
|
}
|
|
|
|
if (right < 0) {
|
|
return 0;
|
|
} else if (func(array[right]) <= max) {
|
|
return right + 1; // exclusive index
|
|
} else {
|
|
return right;
|
|
}
|
|
};
|
|
|
|
var parseWorker = function(tags) {
|
|
"use strict";
|
|
|
|
for (var i = 0; i < tags.length; i++) {
|
|
if (!tags[i].startsWith("worker")) {
|
|
continue;
|
|
}
|
|
|
|
return parseInt(tags[i].split("-")[1]);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
var getDstatLanes = function(data, mins, maxes) {
|
|
if (!data) {
|
|
return [];
|
|
}
|
|
|
|
var row = data[0];
|
|
var lanes = [];
|
|
|
|
if ('total_cpu_usage_usr' in row && 'total_cpu_usage_sys' in row) {
|
|
lanes.push([{
|
|
scale: d3.scale.linear().domain([0, 100]),
|
|
value: function(d) {
|
|
return d.total_cpu_usage_wai;
|
|
},
|
|
color: "rgba(224, 188, 188, 1)",
|
|
text: "CPU wait"
|
|
}, {
|
|
scale: d3.scale.linear().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: d3.scale.linear().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: d3.scale.linear().domain([0, maxes.net_total_recv]),
|
|
value: function(d) { return d.net_total_recv; },
|
|
color: "rgba(224, 188, 188, 1)",
|
|
text: "Net Down"
|
|
}, {
|
|
scale: d3.scale.linear().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: d3.scale.linear().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: d3.scale.linear().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;
|
|
};
|
|
|
|
var initTimeline = function(options, data, timeExtents) {
|
|
"use strict";
|
|
|
|
var container = $(options.container);
|
|
|
|
// http://bl.ocks.org/bunkat/2338034
|
|
var margin = { top: 20, right: 10, bottom: 10, left: 80 };
|
|
var width = container.width() - margin.left - margin.right;
|
|
var height = 550 - margin.top - margin.bottom;
|
|
|
|
// filter dstat data immediately. if no timestamps overlap, we want to throw
|
|
// it away quickly
|
|
options.dstatData = options.dstatData.slice(
|
|
binaryMinIndex(timeExtents[0], options.dstatData, function(d) { return d.system_time; }),
|
|
binaryMaxIndex(timeExtents[1], options.dstatData, function(d) { return d.system_time; })
|
|
);
|
|
|
|
var dstatLanes;
|
|
if (options.dstatData.length > 2) {
|
|
dstatLanes = getDstatLanes(
|
|
options.dstatData,
|
|
options.dstatMinimums,
|
|
options.dstatMaximums);
|
|
} else {
|
|
dstatLanes = [];
|
|
}
|
|
|
|
var miniHeight = data.length * 12 + 30;
|
|
var dstatHeight = dstatLanes.length * 30 + 30;
|
|
var mainHeight = height - miniHeight - dstatHeight - 10;
|
|
|
|
var x = d3.time.scale()
|
|
.range([0, width])
|
|
.domain(timeExtents);
|
|
|
|
var x1 = d3.scale.linear().range([0, width]);
|
|
|
|
var y1 = d3.scale.linear()
|
|
.domain([0, data.length])
|
|
.range([0, mainHeight]);
|
|
var y2 = d3.scale.linear()
|
|
.domain([0, data.length])
|
|
.range([0, miniHeight]);
|
|
var y3 = d3.scale.linear()
|
|
.domain([0, dstatLanes.length])
|
|
.range([0, dstatHeight]);
|
|
|
|
var chart = d3.select(options.container)
|
|
.append("svg")
|
|
.attr("width", width + margin.left + margin.right)
|
|
.attr("height", height + margin.top + margin.bottom)
|
|
.attr("class", "chart");
|
|
|
|
var defs = chart.append("defs")
|
|
.append("clipPath")
|
|
.attr("id", "clip")
|
|
.append("rect")
|
|
.attr("width", width)
|
|
.attr("height", mainHeight);
|
|
|
|
var main = chart.append("g")
|
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
|
.attr("width", width)
|
|
.attr("height", mainHeight)
|
|
.attr("class", "main");
|
|
|
|
var laneLines = main.append("g");
|
|
var laneLabels = main.append("g");
|
|
|
|
var itemGroups = main.append("g");
|
|
|
|
var dstatOffset = margin.top + mainHeight;
|
|
var dstatGroup = chart.append("g")
|
|
.attr("transform", "translate(" + margin.left + "," + dstatOffset + ")")
|
|
.attr("width", width)
|
|
.attr("height", dstatHeight);
|
|
|
|
dstatLanes.forEach(function(lane, i) {
|
|
var laneGroup = dstatGroup.append("g");
|
|
|
|
var text = laneGroup.append("text")
|
|
.attr("y", function(d) { return y3(i + 0.5); })
|
|
.attr("dy", ".5ex")
|
|
.attr("text-anchor", "end")
|
|
.style("font", "10px sans-serif");
|
|
|
|
var dy = 0;
|
|
|
|
// precompute some known info for each lane's paths
|
|
lane.forEach(function(pathDef) {
|
|
var laneHeight = 0.8 * y3(1);
|
|
|
|
if ('text' in pathDef) {
|
|
text.append("tspan")
|
|
.attr("x", -margin.right)
|
|
.attr("dy", dy)
|
|
.text(pathDef.text)
|
|
.attr("fill", function(d) { return pathDef.color; });
|
|
|
|
dy += 10;
|
|
}
|
|
|
|
pathDef.scale.range([laneHeight, 0]);
|
|
|
|
pathDef.path = laneGroup.append("path");
|
|
if (pathDef.type === "line") {
|
|
pathDef.area = d3.svg.line()
|
|
.x(function(d) { return x1(d.system_time); })
|
|
.y(function(d) { return y3(i) + pathDef.scale(pathDef.value(d)); });
|
|
|
|
pathDef.path
|
|
.style("stroke", pathDef.color)
|
|
.style("stroke-width", "1.5px")
|
|
.style("fill", "none");
|
|
//.style("shape-rendering", 'crispEdges');
|
|
} else {
|
|
pathDef.area = d3.svg.area()
|
|
.x(function(d) { return x1(d.system_time); })
|
|
.y0(function(d) { return y3(i) + laneHeight; })
|
|
.y1(function(d) {
|
|
return y3(i) + pathDef.scale(pathDef.value(d));
|
|
});
|
|
|
|
pathDef.path.style("fill", pathDef.color);
|
|
}
|
|
});
|
|
});
|
|
|
|
var cursorGroup = main.append("g")
|
|
.style("opacity", 0)
|
|
.style("pointer-events", "none");
|
|
|
|
var cursor = cursorGroup.append("line")
|
|
.attr("x1", 0)
|
|
.attr("x2", 0)
|
|
.attr("y1", y1(-0.1))
|
|
.attr("stroke", "blue");
|
|
|
|
var cursorText = cursorGroup.append("text")
|
|
.attr("x", 0)
|
|
.attr("y", -10)
|
|
.attr("dy", "-.5ex")
|
|
.text("")
|
|
.style("text-anchor", "middle")
|
|
.style("font", "9px sans-serif");
|
|
|
|
var miniOffset = margin.top + mainHeight + dstatHeight;
|
|
var mini = chart.append("g")
|
|
.attr("transform", "translate(" + margin.left + "," + miniOffset + ")")
|
|
.attr("width", width)
|
|
.attr("height", mainHeight)
|
|
.attr("class", "mini");
|
|
|
|
var miniGroups = mini.append("g");
|
|
|
|
// performance hack: performance in Firefox as of 39.0 is poor due to some
|
|
// d3 bugs
|
|
// Limit the initial selection to ~1/6th of the total to make things
|
|
// bearable (user can still increase as desired)
|
|
var start = timeExtents[0];
|
|
var end = timeExtents[1];
|
|
var reducedEnd = new Date(start.getTime() + ((end - start) / 8));
|
|
|
|
var brush = d3.svg.brush()
|
|
.x(x)
|
|
.extent([start, reducedEnd]);
|
|
|
|
chart.on("mouseout", function() {
|
|
cursorGroup.style("opacity", 0);
|
|
});
|
|
|
|
chart.on("mousemove", function() {
|
|
var pos = d3.mouse(this);
|
|
var px = pos[0];
|
|
var py = pos[1];
|
|
|
|
if (px >= margin.left && px < (width + margin.left) &&
|
|
py > margin.top && py < (mainHeight + margin.top)) {
|
|
var relX = px - margin.left;
|
|
|
|
var currentTime = new Date(x1.invert(relX));
|
|
|
|
cursorGroup.style("opacity", "0.5");
|
|
cursorGroup.attr("transform", "translate(" + relX + ", 0)");
|
|
|
|
cursorText.text(d3.time.format("%X")(currentTime));
|
|
}
|
|
});
|
|
|
|
function updateLanes() {
|
|
var lines = laneLines.selectAll(".laneLine")
|
|
.data(data, function(d) { return d.key; });
|
|
|
|
lines.enter().append("line")
|
|
.attr("x1", 0)
|
|
.attr("x2", width)
|
|
.attr("stroke", "lightgray")
|
|
.attr("class", "laneLine");
|
|
|
|
lines.attr("y1", function(d, i) { return y1(i - 0.1); })
|
|
.attr("y2", function(d, i) { return y1(i - 0.1); });
|
|
|
|
lines.exit().remove();
|
|
|
|
var labels = laneLabels.selectAll(".laneLabel")
|
|
.data(data, function(d) { return d.key; });
|
|
|
|
labels.enter().append("text")
|
|
.text(function(d) { return "Worker #" + d.key; })
|
|
.attr("x", -margin.right)
|
|
.attr("dy", ".5ex")
|
|
.attr("text-anchor", "end")
|
|
.attr("class", "laneLabel");
|
|
|
|
labels.attr("y", function(d, i) { return y1(i + 0.5); });
|
|
|
|
labels.exit().remove();
|
|
|
|
cursor.attr("y2", y1(data.length - 0.1));
|
|
}
|
|
|
|
function updateItems() {
|
|
var minExtent = brush.extent()[0];
|
|
var maxExtent = brush.extent()[1];
|
|
|
|
// filter visible items to include only those within the current extent
|
|
// additionally prune extremely small values to improve performance
|
|
var visibleItems = data.map(function(group) {
|
|
return {
|
|
key: group.key,
|
|
values: group.values.filter(function(e) {
|
|
if (x1(e.end_date) - x1(e.start_date) < 2) {
|
|
return false;
|
|
}
|
|
|
|
if (e.start_date > maxExtent || e.end_date < minExtent) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
})
|
|
};
|
|
});
|
|
|
|
var groups = itemGroups.selectAll("g")
|
|
.data(visibleItems, function(d) { return d.key; });
|
|
|
|
groups.enter().append("g");
|
|
|
|
var rects = groups.selectAll("rect")
|
|
.data(function(d) { return d.values; }, function(d) { return d.name; });
|
|
|
|
rects.enter().append("rect")
|
|
.attr("y", function(d) { return y1(parseWorker(d.tags)); })
|
|
.attr("height", 0.8 * y1(1))
|
|
.attr("stroke", 'rgba(100, 100, 100, 0.25)')
|
|
.attr("clip-path", "url(#clip)");
|
|
|
|
rects
|
|
.attr("x", function(d) {
|
|
return x1(d.start_date);
|
|
})
|
|
.attr("width", function(d) {
|
|
return x1(d.end_date) - x1(d.start_date);
|
|
})
|
|
.attr("fill", function(d) { return statusColorMap[d.status]; })
|
|
.on("mouseover", options.onMouseover)
|
|
.on("mouseout", options.onMouseout)
|
|
.on("click", options.onClick);
|
|
|
|
rects.exit().remove();
|
|
groups.exit().remove();
|
|
}
|
|
|
|
function updateDstat() {
|
|
if (dstatLanes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var minExtent = brush.extent()[0];
|
|
var maxExtent = brush.extent()[1];
|
|
|
|
var dstat = options.dstatData;
|
|
var timeFunc = function(d) { return d.system_time; };
|
|
|
|
var visibleEntries = dstat.slice(
|
|
binaryMinIndex(minExtent, dstat, timeFunc),
|
|
binaryMaxIndex(maxExtent, dstat, timeFunc)
|
|
);
|
|
|
|
// apply the current dataset (visibleEntries) to each dstat path
|
|
//
|
|
dstatLanes.forEach(function(lane) {
|
|
lane.forEach(function(pathDef) {
|
|
pathDef.path
|
|
.datum(visibleEntries)
|
|
.attr("d", pathDef.area);
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateMiniItems() {
|
|
var groups = miniGroups.selectAll("g")
|
|
.data(data, function(d) { return d.key; });
|
|
|
|
groups.enter().append("g");
|
|
|
|
var rects = groups.selectAll("rect").data(
|
|
function(d) { return d.values; },
|
|
function(d) { return d.name; });
|
|
|
|
rects.enter().append("rect")
|
|
.attr("y", function(d) { return y2(parseWorker(d.tags) + 0.5) - 5; })
|
|
.attr("height", 10);
|
|
|
|
rects.attr("x", function(d) { return x(d.start_date); })
|
|
.attr("width", function(d) { return x(d.end_date) - x(d.start_date); })
|
|
.attr("stroke", 'rgba(100, 100, 100, 0.25)')
|
|
.attr("fill", function(d) { return statusColorMap[d.status]; });
|
|
|
|
rects.exit().remove();
|
|
groups.exit().remove();
|
|
}
|
|
|
|
function update() {
|
|
x1.domain(brush.extent());
|
|
|
|
updateLanes();
|
|
updateItems();
|
|
updateDstat();
|
|
}
|
|
|
|
brush.on("brush", update);
|
|
|
|
mini.append("g")
|
|
.attr("class", "x brush")
|
|
.call(brush)
|
|
.selectAll("rect")
|
|
.attr("y", 1)
|
|
.attr("height", miniHeight - 1)
|
|
.attr("fill", "dodgerblue")
|
|
.attr("fill-opacity", 0.365);
|
|
|
|
updateMiniItems();
|
|
update();
|
|
|
|
$(window).resize(function() {
|
|
var brushExtent = brush.extent();
|
|
|
|
width = container.width() - margin.left - margin.right;
|
|
x.range([0, width]);
|
|
x1.range([0, width]);
|
|
|
|
chart.attr("width", container.width());
|
|
defs.attr("width", width);
|
|
main.attr("width", width);
|
|
mini.attr("width", width);
|
|
|
|
laneLines.selectAll(".laneLine").attr("x2", width);
|
|
|
|
brush.extent(brushExtent);
|
|
|
|
updateMiniItems();
|
|
update();
|
|
});
|
|
};
|
|
|
|
function fillArrayRight(array) {
|
|
// "fill" the array to the right, overwriting empty values with the next
|
|
// non-empty value to the left
|
|
// only false values will be overwritten (e.g. "", null, etc)
|
|
for (var i = 0; i < array.length - 1; i++) {
|
|
if (!array[i + 1]) {
|
|
array[i + 1] = array[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
function mergeNames(primary, secondary) {
|
|
// "zip" together strings in the same position in each array, and do some
|
|
// basic cleanup of results
|
|
var ret = [];
|
|
for (var i = 0; i < primary.length; i++) {
|
|
ret.push((primary[i] + '_' + secondary[i]).replace(/[ /]/g, '_'));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
function chainLoadDstat(path, yearOverride, callback) {
|
|
"use strict";
|
|
|
|
d3.text(path, function(error, data) {
|
|
if (error) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
var primaryNames = null;
|
|
var secondaryNames = null;
|
|
var names = null;
|
|
|
|
var minimums = {};
|
|
var maximums = {};
|
|
|
|
// assume UTC - may not necessarily be the case?
|
|
// dstat doesn't include the year in its logs, so we'll need to copy it
|
|
// from the subunit logs
|
|
var dateFormat = d3.time.format.utc("%d-%m %H:%M:%S");
|
|
|
|
var parsed = d3.csv.parseRows(data, function(row, i) {
|
|
if (i <= 4) { // header rows - ignore
|
|
return null;
|
|
} else if (i == 5) { // primary
|
|
primaryNames = row;
|
|
fillArrayRight(primaryNames);
|
|
return null;
|
|
} else if (i == 6) { // secondary
|
|
secondaryNames = row;
|
|
|
|
names = mergeNames(primaryNames, secondaryNames);
|
|
return null;
|
|
} else {
|
|
var ret = {};
|
|
|
|
for (var col = 0; col < row.length; col++) {
|
|
var name = names[col];
|
|
var value = row[col];
|
|
|
|
if (name == "system_time") {
|
|
value = dateFormat.parse(value);
|
|
value.setFullYear(1900 + yearOverride);
|
|
} else {
|
|
value = parseFloat(value);
|
|
}
|
|
|
|
if (!(name in minimums) || value < minimums[name]) {
|
|
minimums[name] = value;
|
|
}
|
|
|
|
if (!(name in maximums) || value > maximums[name]) {
|
|
maximums[name] = value;
|
|
}
|
|
|
|
ret[name] = value;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
});
|
|
|
|
callback(parsed, minimums, maximums);
|
|
});
|
|
}
|
|
|
|
function loadTimeline(path, options) { // eslint-disable-line no-unused-vars
|
|
"use strict";
|
|
|
|
d3.json(path, function(error, data) {
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
|
|
var minStart = null;
|
|
var maxEnd = null;
|
|
data.forEach(function(d) {
|
|
/*eslint-disable camelcase*/
|
|
d.start_date = new Date(d.timestamps[0]);
|
|
if (minStart === null || d.start_date < minStart) {
|
|
minStart = d.start_date;
|
|
}
|
|
|
|
d.end_date = new Date(d.timestamps[1]);
|
|
if (maxEnd === null || d.end_date > maxEnd) {
|
|
maxEnd = d.end_date;
|
|
}
|
|
/*eslint-enable camelcase*/
|
|
});
|
|
|
|
data = data.filter(function (d) { return d.duration > 0; });
|
|
|
|
var nested = d3.nest()
|
|
.key(function(d) { return parseWorker(d.tags); })
|
|
.sortKeys(d3.ascending)
|
|
.entries(data);
|
|
|
|
// include dstat if available
|
|
if (options.dstatPath && !options.dstatData) {
|
|
var year = data[0].start_date.getYear();
|
|
chainLoadDstat(options.dstatPath, year, function(data, mins, maxes) {
|
|
options.dstatData = data;
|
|
options.dstatMinimums = mins;
|
|
options.dstatMaximums = maxes;
|
|
|
|
initTimeline(options, nested, [ minStart, maxEnd ]);
|
|
});
|
|
} else {
|
|
initTimeline(options, nested, [ minStart, maxEnd ]);
|
|
}
|
|
});
|
|
}
|