(function () {
var holder = document.getElementById('graph-latency-heatmap');
var source = document.getElementById('graph-latency-heatmap-data');
if (!holder || !source || typeof echarts === 'undefined') {
return;
}
var data = JSON.parse(source.textContent);
if (!data || !data.series || data.series.length === 0) {
return;
}
var select = document.getElementById('graph-latency-heatmap-series');
var views = document.getElementById('graph-latency-heatmap-views');
var buttons = views.querySelectorAll('button');
var note = document.getElementById('graph-latency-heatmap-width');
var legendMin = document.getElementById('graph-latency-heatmap-min');
var legendMax = document.getElementById('graph-latency-heatmap-max');
var chart = echarts.init(holder);
var view = 'adjusted';
data.series.forEach(function (series, index) {
var option = document.createElement('option');
option.value = String(index);
option.textContent = series.name;
select.appendChild(option);
});
function color(level) {
var t = level / 1000;
var saturation = 80 + 5 * t;
var lightness = 64 - 42 * t;
return 'hsl(222, ' + saturation.toFixed(1) + '%, ' + lightness.toFixed(1) + '%)';
}
document.getElementById('graph-latency-heatmap-ramp').style.background =
'linear-gradient(to right, ' + color(0) + ', ' + color(1000) + ')';
var micros = data.buckets_us;
function span(bucket) {
var row = micros[bucket];
var end = (row[2] + 1) / 1000;
return [row[1] === 0 ? end / 2 : row[1] / 1000, end];
}
function pad(number) {
return (number < 10 ? '0' : '') + number;
}
function clock(ms) {
var date = new Date(ms);
return date.getUTCFullYear() + '-' + pad(date.getUTCMonth() + 1) + '-' +
pad(date.getUTCDate()) + ' ' + pad(date.getUTCHours()) + ':' +
pad(date.getUTCMinutes()) + ':' + pad(date.getUTCSeconds());
}
function rowMs(us) {
if (us < 1000) {
return String(Math.floor(us / 100) / 10);
}
return Math.floor(us / 1000).toLocaleString();
}
function requests(count) {
return count.toLocaleString() + (count === 1 ? ' request' : ' requests');
}
function bucketText(bucket) {
var row = micros[bucket];
if (row[2] < 1000) {
return (row[1] / 1000) + ' to ' + ((row[2] + 1) / 1000) + ' ms';
}
var lowest = Math.floor(row[1] / 1000);
if (row[2] - row[1] < 1000) {
return lowest.toLocaleString() + ' ms';
}
return lowest.toLocaleString() + ' to ' + Math.floor((row[2] + 1) / 1000).toLocaleString() + ' ms';
}
function tooltip(params) {
var value = params.value;
if (params.seriesType === 'custom') {
return clock(value[0]) + ' to ' + clock(value[1]) + '<br>' +
bucketText(value[6]) + ': ' + requests(value[5]) + '<br>' +
'p50 / p95 / p99: ' + rowMs(value[7]) + ' / ' + rowMs(value[8]) + ' / ' +
rowMs(value[9]) + ' ms';
}
return clock(value[0]) + '<br>' + params.seriesName + ': ' + rowMs(value[2]) + ' ms';
}
function draw() {
var series = data.series[Number(select.value)];
var adjustable = series.adjusted !== null;
views.hidden = !adjustable;
var adjusted = adjustable && view === 'adjusted';
for (var b = 0; b < buttons.length; b++) {
var pressed = (buttons[b].getAttribute('data-view') === 'adjusted') === adjusted;
buttons[b].setAttribute('aria-pressed', pressed ? 'true' : 'false');
buttons[b].className = pressed ? 'active' : '';
}
var shown = adjusted ? series.adjusted : series.measured;
var widthMs = series.width * 1000;
var first = data.start + data.offset + data.origin * 1000;
var cells = [];
var lines = [[], [], []];
var low = Infinity;
var high = 0;
var end = first;
var previous = null;
shown.columns.forEach(function (column) {
var from = first + column[0] * widthMs;
var to = from + widthMs;
end = Math.max(end, to);
if (previous !== null && column[0] > previous + 1) {
for (var gap = 0; gap < 3; gap++) {
lines[gap].push([from - widthMs / 2, null, null]);
}
}
previous = column[0];
for (var p = 0; p < 3; p++) {
lines[p].push([(from + to) / 2, Math.max(column[1 + p] / 1000, 0.05), column[1 + p]]);
}
for (var i = 4; i + 2 < column.length; i += 3) {
var bucketSpan = span(column[i]);
low = Math.min(low, bucketSpan[0]);
high = Math.max(high, bucketSpan[1]);
cells.push([from, to, bucketSpan[0], bucketSpan[1], column[i + 2],
column[i + 1], column[i], column[1], column[2], column[3]]);
}
});
if (cells.length === 0) {
low = 0.05;
high = 1000;
}
note.textContent = 'Each column is ' +
(series.width === 1 ? '1 second' : series.width + ' seconds') +
'. Color shows how many requests took that long; the lines are each column\'s p50, p95 and p99.';
legendMin.textContent = requests(shown.min);
legendMax.textContent = requests(shown.max);
var line = function (name, color, values) {
return {
name: name,
type: 'line',
symbol: 'none',
connectNulls: false,
lineStyle: { color: color, width: 2 },
itemStyle: { color: color },
encode: { x: 0, y: 1 },
data: values
};
};
chart.setOption({
animation: false,
useUTC: true,
tooltip: { trigger: 'item', formatter: tooltip },
legend: { data: ['p50', 'p95', 'p99'] },
grid: { left: 80, right: 40, top: 40, bottom: 50 },
xAxis: { type: 'time', min: first, max: Math.max(end, first + widthMs) },
yAxis: {
name: 'Response time [ms]',
nameLocation: 'center',
nameRotate: 90,
nameGap: 55,
type: 'log',
logBase: 10,
min: low,
max: high
},
series: [
{
type: 'custom',
progressive: 0,
renderItem: function (params, api) {
var topLeft = api.coord([api.value(0), api.value(3)]);
var bottomRight = api.coord([api.value(1), api.value(2)]);
var left = Math.round(topLeft[0]);
var top = Math.round(topLeft[1]);
var right = Math.round(bottomRight[0]);
var bottom = Math.round(bottomRight[1]);
return {
type: 'rect',
shape: {
x: left,
y: top,
width: Math.max(right - left, 1),
height: Math.max(bottom - top, 1)
},
style: { fill: color(api.value(4)) }
};
},
encode: { x: [0, 1], y: [2, 3] },
data: cells
},
line('p50', '#2c664f', lines[0]),
line('p95', '#d4730b', lines[1]),
line('p99', '#b33f3f', lines[2])
]
}, true);
}
select.addEventListener('change', draw);
for (var b = 0; b < buttons.length; b++) {
buttons[b].addEventListener('click', function (event) {
view = event.currentTarget.getAttribute('data-view');
draw();
});
}
draw();
})();