blob: d3607fb7301f64d2e8e95c5a9a747f613541e950 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
/**
* Generate the stats rollup table.
*/
(function (global) {
function drawStatsTable(planController, baseUrl, cluster, environ, toponame, physicalPlan, logicalPlan) {
var NO_DATA_COLOR = "#f0f5fa";
var table = d3.selectAll('.stat-rollup-table tbody.stats');
var percentFormat = d3.format('.1%');
var d3instances = d3.selectAll('#physical-plan .instance');
var d3instancedata = d3instances.data();
// Build the status table
var rowData = [
{
name: 'Capacity Utilization',
metricName: 'capacity',
get: getAndRenderStats,
tooltip: 'Average tuple execute latency multiplied by number of tuples processed divided by the time period.',
legendDescription: 'utilization of execution capacity',
loMedHi: [0.1, 0.5, 1],
scaleTicks: [0, 0.25, 0.5, 0.75, 1]
},
{
name: 'Failures',
metricName: 'failures',
get: getAndRenderStats,
tooltip: 'Failed tuple count divided by sum of failed and executed tuples over time range.',
legendDescription: 'failure rate',
loMedHi: [0, 0.02, 0.05],
scaleTicks: [0, 0.01, 0.02, 0.03, 0.04, 0.05]
},
{
name: 'CPU',
metricName: 'cpu',
get: getAndRenderStats,
tooltip: 'CPU seconds used per second.',
legendDescription: 'CPUs used',
loMedHi: [1, 1.5, 2],
scaleTicks: [0, 0.5, 1, 1.5, 2],
format: function (d) { return d.toFixed(2); }
},
{
name: 'Memory',
metricName: 'memory',
get: getAndRenderStats,
tooltip: 'Memory used divided by total available memory per process.',
legendDescription: 'memory usage',
loMedHi: [0.7, 0.85, 1],
scaleTicks: [0, 0.25, 0.5, 0.75, 1],
},
{
name: 'GC',
metricName: 'gc',
get: getAndRenderStats,
tooltip: 'Milliseconds spent in garbage collection per minute.',
legendDescription: 'ms in GC per minute',
loMedHi: [1000, 1500, 2000],
scaleTicks: [0, 500, 1000, 1500, 2000],
format: function (d) { return d.toFixed(0); }
},
{
name: 'Back Pressure',
metricName: 'backpressure',
get: getAndRenderStats,
tooltip: 'Milliseconds spent in back pressure per minute.',
legendDescription: 'ms in back pressure per minute',
loMedHi: [3000, 12000, 30000],
scaleTicks: [0, 7500, 15000, 22500, 30000],
format: function (d) { return d.toFixed(0); }
}
];
var colData = [
{name: '3 min', seconds: 3*60},
{name: '10 min', seconds: 10*60},
{name: '1 hour', seconds: 60*60},
{name: '3 hours', seconds: 3*60*60}
];
window.pollingMetrics = rowData;
window.pollingPeriods = colData;
var colorRanges = {
// green/red
'default': ['#1a9850', '#fdae61', '#d73027'],
// blue/red
'colorblind': ['#2166ac', '#f7f7f7', '#b2182b']
};
rowData.forEach(function (d) {
d.format = d.format || d3.format('%');
d.colorScale = d3.scale.linear()
.interpolate(d3.interpolateLab)
.domain(d.loMedHi)
.range(colorRanges[localStorage.colors] || colorRanges.default)
.clamp(true);
});
var rows = table.selectAll('tr')
.data(rowData)
.enter()
.append('tr');
rows.append('td')
.attr('class', 'text-right')
.html(function (d) {
return d.name + ' <span class="glyphicon glyphicon-question-sign bs-popover text-muted" aria-hidden="true" data-toggle="popover" data-placement="top" title="' + d.name + '" data-content="' + d.tooltip + '"></span></a>';
});
$('.bs-popover').popover({
trigger: 'hover',
container: 'body'
});
var cols = rows.selectAll('td.time')
.data(function (row) {
return colData.map(function (col) {
return {
metric: row,
time: col
};
});
})
.enter()
.append('td')
.attr('class', 'time');
var links = cols.append('a')
.attr('href', '#')
.attr('class', 'strong')
.on('click', function (d) {
d.metric.get(d);
d3.event.preventDefault();
d3.event.stopPropagation();
});
var topLevelStatus = links.append('svg')
.attr('class', 'status-circle')
.attr('width', 20)
.attr('height', 15)
.append('circle')
.attr('cx', 8)
.attr('cy', 8)
.attr('r', 7);
links.append('span')
.text(function (d) { return d.time.name; });
d3.selectAll('.reset').on('click', function () {
resetColors();
});
d3.selectAll('#reset-colors').on('click', function () {
resetColors();
d3.event.stopPropagation();
d3.event.preventDefault();
});
// Start polling once a minute for all top-level stats
function fillInTopLevelStats() {
topLevelStatus.each(function (d) {
d.metric.get(d, true);
});
}
// If there are more than 20 logical nodes or 400 physical instances, don't request metrics automatically
// instead wait for the user to click on them to fill in.
setTimeout(function () {
if (d3.selectAll('#logical-plan .node').size() <= 20 && d3.selectAll('#physical-plan .instance').size() <= 400) {
fillInTopLevelStats();
setInterval(fillInTopLevelStats, 60000);
}
}, 20);
function getAndRenderStats(statTableCell, justFillInTopLevel) {
var seconds = statTableCell.time.seconds;
var durationString = statTableCell.time.name;
if (!justFillInTopLevel) {
drawColorKey(statTableCell);
clearComponentColors(statTableCell);
}
makeMetricsQuery(statTableCell.metric, seconds / 60, function (data) {
var max = d3.max(data, function (d) { return d.value; });
_.pairs(_.groupBy(data, 'name')).forEach(function (pair) {
var name = pair[0];
var values = _.pluck(pair[1], 'value');
var maxValue = d3.max(values);
pair[1].forEach(function (datapoint) {
recordInstanceMetric(datapoint.value, datapoint.id, statTableCell);
});
if (!justFillInTopLevel) {
colorGraphNodes(maxValue, name, statTableCell);
pair[1].forEach(function (datapoint) {
colorInstances(datapoint.value, datapoint.id, statTableCell);
});
}
});
colorTopLevel(max, statTableCell);
});
}
// Re-render the color key based on current metric displayed
function drawColorKey(statTableCell) {
var parentSelector = '#color-key';
var tickValues = statTableCell.metric.scaleTicks;
var gradientID = 'colorKey';
var height = 3;
var outerWidth = 400;
var outerHeight = 70;
var margin = {top: 25, left: 50, bottom: 20, right: 50};
var width = outerWidth - margin.left - margin.right;
var keyContainer = d3.selectAll(parentSelector);
var svg = keyContainer.text('').append('svg')
.attr('width', outerWidth)
.attr('height', outerHeight)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
svg.append('text')
.attr('y', -10)
.attr('x', width / 2)
.style('text-anchor', 'middle')
.text('Instances colored by ' + statTableCell.metric.legendDescription + ' over last ' + statTableCell.time.name);
var xScale = d3.scale.linear()
.domain(d3.extent(tickValues))
.range([0, width]);
// encode the color key as a gradient that stretches across the wide rectangle
d3.selectAll('svg.svgdefs').remove();
var gradient = d3.select('body')
.append('svg')
.attr('class', 'svgdefs')
.attr('width', 0)
.attr('height', 0)
.append("defs", 'defs')
.append('linearGradient', gradientID)
.attr("id", gradientID)
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "0%")
.attr("spreadMethod", "pad");
var valueToPercentScale = d3.scale.linear()
.domain(d3.extent(tickValues))
.range(["0%", "100%"]);
gradient.selectAll('stop')
.data(statTableCell.metric.colorScale.domain())
.enter()
.append("svg:stop")
.attr("offset", valueToPercentScale)
.attr("stop-color", statTableCell.metric.colorScale)
.attr("stop-opacity", 1);
svg.append('rect')
.attr('width', width)
.attr('height', height)
.attr('fill', 'url(#' + gradientID + ')');
// also label specific points on the color scale
var colorScaleAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.tickValues(tickValues)
.tickFormat(statTableCell.metric.format || d3.format('%'))
.tickSize(2);
svg
.append('g')
.attr('class', 'x axis')
.attr('transform', function() {return 'translate(' + 0 +',' + height + ')'; })
.call(colorScaleAxis);
}
// Utilities
function getLogicalComponentNames() {
var toRequest = {};
d3.selectAll('#logical-plan .node').each(function (d) {
if (d.name) {
toRequest[d.name] = true;
}
});
return _.keys(toRequest);
}
function p90(data) {
return d3.quantile(_.sortBy(data), 0.9);
}
function cleanValue(value) {
return value === 'nan' ? 0 : +value;
}
function colorGraphNodes(value, name, statTableCell) {
d3.selectAll('#logical-plan .node').filter(function (d) {
return d.name === name;
}).style('fill', function (d) {
return d.color = !_.isNumber(value) || isNaN(value) ? NO_DATA_COLOR : statTableCell.metric.colorScale(value);
});
}
function clearComponentColors(statTableCell) {
d3.selectAll('#physical-plan .instance, #logical-plan .node').style('fill', function (d) {
d.currentMetric = statTableCell;
d.currentMetricValue = null;
return d.color = NO_DATA_COLOR;
});
}
function recordInstanceMetric(value, id, statTableCell) {
var name = statTableCell.metric.name;
var seconds = statTableCell.time.seconds;
var d;
for (var i = d3instancedata.length - 1; i >= 0; i--) {
d = d3instancedata[i];
if (d.id === id) {
d.stats = d.stats || {};
d.stats[name] = d.stats[name] || {};
d.stats[name][seconds] = value;
}
}
}
function colorInstances(value, id, statTableCell, tooltipDetails) {
d3.select('html').classed('no-colors', false);
d3instances.filter(function (d) {
return id === d.id;
}).style('fill', function (d) {
d.currentMetric = statTableCell;
d.currentMetricValue = value;
d.tooltipDetails = tooltipDetails;
return d.color = !_.isNumber(value) || isNaN(value) ? NO_DATA_COLOR : statTableCell.metric.colorScale(value);
});
}
function colorTopLevel(value, statTableCell) {
topLevelStatus.filter(function (d) {
return d === statTableCell;
}).style('fill', function (d) {
return d.color = !_.isNumber(value) || isNaN(value) ? NO_DATA_COLOR : statTableCell.metric.colorScale(value);
}).style('visibility', null);
}
function resetColors() {
d3.select('html').classed('no-colors', true);
d3.select('#color-key').text('');
d3.selectAll('#physical-plan .instance, #logical-plan .node').style('fill', function (d) {
d.currentMetric = null;
d.currentMetricValue = null;
return d.color = d.defaultColor;
});
}
function createMetricsUrl(metric, component, instance, start, end) {
var url = baseUrl + '/topologies/metrics/timeline?';
return [
url + 'cluster=' + cluster,
'environ=' + environ,
'topology=' + toponame,
'metric=' + metric.metricName,
'component=' + component,
'instance=' + instance,
'starttime=' + start,
'endtime=' + end
].join('&');
}
var outstandingRequests = {};
// slight optimization, instead of requesting metrics for 3m, 10m, 60m, 1h, 3h
// always request 3 hours of data on first call and attach all simultaneous
// stats calls to result from the first one and when that responds, parse out
// the values over last N minutes.
function makeMetricsQuery(metric, lookbackMinutes, callback) {
var start = moment().startOf('minute').subtract(lookbackMinutes, 'minutes').valueOf() / 1000;
doMake3hourMetricsQuery(metric, function (results) {
var finalResult = results.map(function (d) {
var values = d.value.filter(function (datapoint) {
return datapoint.time >= start;
}).map(function (d) { return d.value; });
return {
name: d.name,
id: d.id,
value: p90(values)
};
});
callback(finalResult);
});
}
function doMake3hourMetricsQuery(metric, callback) {
var queryName = metric.name;
if (outstandingRequests[queryName]) {
outstandingRequests[queryName].push(callback);
return;
}
var callbacks = [callback];
outstandingRequests[queryName] = callbacks;
var start = moment().startOf('minute').subtract(3, 'hours').valueOf() / 1000;
var end = moment().startOf('minute').valueOf() / 1000;
fetchMetrics();
function fetchMetrics() {
var url = createMetricsUrl(metric, "*", "*", start, end);
d3.json(url, function (error, data) {
if (error) {
console.warn(error);
}
var result = [];
if (data && data.hasOwnProperty("status") && data["status"] === "success" && data.hasOwnProperty("result")) {
data.result.timeline.forEach(function (timeline) {
var split = timeline.instance.split('_');
var values = [];
for (var timestamp in timeline.data) {
if (timeline.data.hasOwnProperty(timestamp)) {
values.push({
time: timestamp,
value: timeline.data[timestamp]
});
}
}
result.push({
id: timeline.instance,
name: split.slice(2, split.length - 1).join('_'),
value: values
});
})
resolve(result);
}
});
}
function resolve(data) {
delete outstandingRequests[queryName];
callbacks.forEach(function (cb) { cb(data); });
}
}
}
global.drawStatsTable = drawStatsTable;
}(this));