Merge pull request #2046 from ross/ui-tomography

Implementation of a per-node tomography graph
This commit is contained in:
James Phillips 2016-05-18 22:06:50 -04:00
commit fa26d5f64e
5 changed files with 189 additions and 6 deletions

View File

@ -594,6 +594,15 @@
{{else}}
<p class="light small">No sessions</p>
{{/if}}
<h5>Network Tomography</h5>
{{ tomographyGraph tomography 336 }}
<p class="light small">Node: <span id="tomography-node-info"></span></p>
<p class="light small">Minimum: {{ tomography.min }}ms</p>
<p class="light small">Median: {{ tomography.median }}ms</p>
<p class="light small">Maximum: {{ tomography.max }}ms</p>
</script>
<script type="text/x-handlebars" id="acls">

View File

@ -91,3 +91,82 @@ function notify(message, ttl) {
window.notifications = [];
window.notifications.push(notification);
}
// Tomography
// TODO: not sure how to how do to this more Ember.js-y
function tomographyMouseOver(el) {
var buf = el.getAttribute('data-node') + ' - ' + el.getAttribute('data-distance') + 'ms';
document.getElementById('tomography-node-info').innerHTML = buf;
}
Ember.Handlebars.helper('tomographyGraph', function(tomography, size) {
// This is ugly, but I'm working around bugs with Handlebars and templating
// parts of svgs. Basically things render correctly the first time, but when
// stuff is updated for subsequent go arounds the templated parts don't show.
// It appears (based on google searches) that the replaced elements aren't
// being interpreted as http://www.w3.org/2000/svg. Anyway, this works and
// if/when Handlebars fixes the underlying issues all of this can be cleaned
// up drastically.
var max = -999999999;
tomography.distances.forEach(function (d, i) {
if (d.distance > max) {
max = d.distance;
}
});
var insetSize = size / 2 - 8;
var buf = '' +
' <svg width="' + size + '" height="' + size + '">' +
' <g class="tomography" transform="translate(' + (size / 2) + ', ' + (size / 2) + ')">' +
' <g>' +
' <circle class="background" r="' + insetSize + '"/>' +
' <circle class="axis" r="' + (insetSize * 0.25) + '"/>' +
' <circle class="axis" r="' + (insetSize * 0.5) + '"/>' +
' <circle class="axis" r="' + (insetSize * 0.75) + '"/>' +
' <circle class="border" r="' + insetSize + '"/>' +
' </g>' +
' <g class="lines">';
var distances = tomography.distances;
var n = distances.length;
if (tomography.n > 360) {
// We have more nodes than we want to show, take a random sampling to keep
// the number around 360.
var sampling = 360 / tomography.n;
distances = distances.filter(function (_, i) {
return i == 0 || i == n - 1 || Math.random() < sampling
});
// Re-set n to the filtered size
n = distances.length;
}
distances.forEach(function (d, i) {
buf += ' <line transform="rotate(' + (i * 360 / n) + ')" y2="' + (-insetSize * (d.distance / max)) + '" ' +
'data-node="' + d.node + '" data-distance="' + d.distance + '" onmouseover="tomographyMouseOver(this);"/>';
});
buf += '' +
' </g>' +
' <g class="labels">' +
' <circle class="point" r="5"/>' +
' <g class="tick" transform="translate(0, ' + (insetSize * -0.25 ) + ')">' +
' <line x2="70"/>' +
' <text x="75" y="0" dy=".32em">' + (max > 0 ? (parseInt(max * 25) / 100) : 0) + 'ms</text>' +
' </g>' +
' <g class="tick" transform="translate(0, ' + (insetSize * -0.5 ) + ')">' +
' <line x2="70"/>' +
' <text x="75" y="0" dy=".32em">' + (max > 0 ? (parseInt(max * 50) / 100) : 0)+ 'ms</text>' +
' </g>' +
' <g class="tick" transform="translate(0, ' + (insetSize * -0.75 ) + ')">' +
' <line x2="70"/>' +
' <text x="75" y="0" dy=".32em">' + (max > 0 ? (parseInt(max * 75) / 100) : 0) + 'ms</text>' +
' </g>' +
' <g class="tick" transform="translate(0, ' + (insetSize * -1) + ')">' +
' <line x2="70"/>' +
' <text x="75" y="0" dy=".32em">' + (max > 0 ? (parseInt(max * 100) / 100) : 0) + 'ms</text>' +
' </g>' +
' </g>' +
' </g>' +
' </svg>';
return new Handlebars.SafeString(buf);
});

View File

@ -104,6 +104,9 @@ App.DcRoute = App.BaseRoute.extend({
});
return objs;
}),
coordinates: Ember.$.getJSON(formatUrl(consulHost + '/v1/coordinate/nodes', params.dc, token)).then(function(data) {
return data;
})
});
},
@ -112,6 +115,7 @@ App.DcRoute = App.BaseRoute.extend({
controller.set('content', models.dc);
controller.set('nodes', models.nodes);
controller.set('dcs', models.dcs);
controller.set('coordinates', models.coordinates);
controller.set('isDropdownVisible', false);
},
});
@ -257,19 +261,78 @@ App.ServicesShowRoute = App.BaseRoute.extend({
}
});
function distance(a, b) {
a = a.Coord;
b = b.Coord;
var sum = 0;
for (var i = 0; i < a.Vec.length; i++) {
var diff = a.Vec[i] - b.Vec[i];
sum += diff * diff;
}
var rtt = Math.sqrt(sum) + a.Height + b.Height;
var adjusted = rtt + a.Adjustment + b.Adjustment;
if (adjusted > 0.0) {
rtt = adjusted;
}
return Math.round(rtt * 100000.0) / 100.0;
}
App.NodesShowRoute = App.BaseRoute.extend({
model: function(params) {
var dc = this.modelFor('dc').dc;
var dc = this.modelFor('dc');
var token = App.get('settings.token');
var min = 999999999;
var max = -999999999;
var sum = 0;
var distances = [];
dc.coordinates.forEach(function (node) {
if (params.name == node.Node) {
dc.coordinates.forEach(function (other) {
if (node.Node != other.Node) {
var dist = distance(node, other);
distances.push({ node: other.Node, distance: dist });
sum += dist;
if (dist < min) {
min = dist;
}
if (dist > max) {
max = dist;
}
}
});
distances.sort(function (a, b) {
return a.distance - b.distance;
});
}
});
var n = distances.length;
var halfN = Math.floor(n / 2);
var median;
if (n % 2) {
// odd
median = distances[halfN].distance;
} else {
median = (distances[halfN - 1].distance + distances[halfN].distance) / 2;
}
// Return a promise hash of the node and nodes
return Ember.RSVP.hash({
dc: dc,
dc: dc.dc,
token: token,
node: Ember.$.getJSON(formatUrl(consulHost + '/v1/internal/ui/node/' + params.name, dc, token)).then(function(data) {
tomography: {
distances: distances,
n: distances.length,
min: parseInt(min * 100) / 100,
median: parseInt(median * 100) / 100,
max: parseInt(max * 100) / 100
},
node: Ember.$.getJSON(formatUrl(consulHost + '/v1/internal/ui/node/' + params.name, dc.dc, token)).then(function(data) {
return App.Node.create(data);
}),
nodes: Ember.$.getJSON(formatUrl(consulHost + '/v1/internal/ui/node/' + params.name, dc, token)).then(function(data) {
nodes: Ember.$.getJSON(formatUrl(consulHost + '/v1/internal/ui/node/' + params.name, dc.dc, token)).then(function(data) {
return App.Node.create(data);
})
});
@ -286,6 +349,7 @@ App.NodesShowRoute = App.BaseRoute.extend({
setupController: function(controller, models) {
controller.set('content', models.node);
controller.set('sessions', models.sessions);
controller.set('tomography', models.tomography);
//
// Since we have 2 column layout, we need to also display the
// list of nodes on the left. Hence setting the attribute

File diff suppressed because one or more lines are too long

View File

@ -148,3 +148,34 @@ a {
opacity: 0.6;
margin-top: -3px;
}
.tomography .background {
fill: $gray-background;
}
.tomography .axis {
fill: none;
stroke: $gray-light;
stroke-dasharray: 4 4;
}
.tomography .border {
fill: none;
stroke: $gray-darker;
}
.tomography .point {
stroke: $gray-darker;
fill: $green-faded;
}
.tomography .lines line {
stroke: $red;
}
.tomography .lines line:hover {
stroke: $gray-darker;
stroke-width: 2px;
}
.tomography .tick line {
stroke: $gray-light;
}
.tomography .tick text {
font-size: 14px;
text-anchor: start;
}