You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ea...@apache.org on 2018/05/29 09:36:54 UTC
qpid-dispatch git commit: DISPATCH-1014 Add link utilization
visualization to console's topology page
Repository: qpid-dispatch
Updated Branches:
refs/heads/master 58967e47f -> e54c632bc
DISPATCH-1014 Add link utilization visualization to console's topology page
Project: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/repo
Commit: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/commit/e54c632b
Tree: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/tree/e54c632b
Diff: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/diff/e54c632b
Branch: refs/heads/master
Commit: e54c632bca5f4cf0f54728f2f3bf1851e664caf1
Parents: 58967e4
Author: Ernest Allen <ea...@redhat.com>
Authored: Tue May 29 05:36:30 2018 -0400
Committer: Ernest Allen <ea...@redhat.com>
Committed: Tue May 29 05:36:30 2018 -0400
----------------------------------------------------------------------
console/stand-alone/plugin/css/dispatch.css | 19 +-
console/stand-alone/plugin/css/dispatchpf.css | 5 -
.../stand-alone/plugin/html/qdrTopology.html | 69 ++-
.../plugin/js/topology/qdrTopology.js | 139 ++---
.../stand-alone/plugin/js/topology/traffic.js | 531 ++++++++++++++-----
5 files changed, 541 insertions(+), 222 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/e54c632b/console/stand-alone/plugin/css/dispatch.css
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/css/dispatch.css b/console/stand-alone/plugin/css/dispatch.css
index a815ca1..3f0c9db 100644
--- a/console/stand-alone/plugin/css/dispatch.css
+++ b/console/stand-alone/plugin/css/dispatch.css
@@ -34,27 +34,32 @@ svg:not(.active):not(.ctrl) {
stroke: #33F;
fill: #33F;
}
-path.link.selected {
+path.link.selected:not(.traffic) {
/* stroke-dasharray: 10,2; */
stroke: #33F !important;
}
path.link {
fill: #000;
- stroke: #000;
+ /* stroke: #000; */
stroke-width: 4px;
cursor: default;
}
+path:not(.traffic) {
+ stroke: #000;
+}
svg:not(.active):not(.ctrl) path.link {
cursor: pointer;
}
path.link.small {
stroke-width: 2.5;
- stroke: darkgray;
}
-path.link.highlighted {
+path.link.small:not(.traffic) {
+ stroke: darkgray;
+}
+path.link.highlighted:not(.traffic) {
stroke: #6F6 !important;
}
marker#start-arrow-highlighted,
@@ -65,7 +70,9 @@ marker#start-arrow-small,
marker#end-arrow-small {
fill: darkgray;
}
-
+marker {
+ stroke-width: 0;
+}
path.link.dragline {
pointer-events: none;
}
@@ -700,4 +707,4 @@ select.required, input.required {
.error {
color: red;
font-weight: bold;
-}
\ No newline at end of file
+}
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/e54c632b/console/stand-alone/plugin/css/dispatchpf.css
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/css/dispatchpf.css b/console/stand-alone/plugin/css/dispatchpf.css
index 49e7d4b..09b8eb7 100644
--- a/console/stand-alone/plugin/css/dispatchpf.css
+++ b/console/stand-alone/plugin/css/dispatchpf.css
@@ -118,14 +118,11 @@ ul.fancytree-container a {
[class^="icon-"],
[class*=" icon-"] {
display: inline;
- width: auto;
- height: auto;
line-height: normal;
vertical-align: baseline;
background-image: none;
background-position: 0% 0%;
background-repeat: repeat;
- margin-top: 0;
}
[class^="icon-"],
[class*=" icon-"] {
@@ -141,7 +138,6 @@ ul.fancytree-container a {
[class*=" icon-"]:before {
text-decoration: inherit;
display: inline-block;
- speak: none;
}
.icon-bar-chart:before {
@@ -166,7 +162,6 @@ ul.fancytree-container a {
[class^="icon-"]:before, [class*=" icon-"]:before {
text-decoration: inherit;
display: inline-block;
- speak: none;
}
#svg_legend {
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/e54c632b/console/stand-alone/plugin/html/qdrTopology.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrTopology.html b/console/stand-alone/plugin/html/qdrTopology.html
index e355413..91c58a1 100644
--- a/console/stand-alone/plugin/html/qdrTopology.html
+++ b/console/stand-alone/plugin/html/qdrTopology.html
@@ -128,7 +128,7 @@ button.page-menu-button {
opacity: 0.1;
}
- #topo_legend ul.addresses li, #topo_legend ul.options li {
+ #topo_legend ul.addresses li {
margin-top: 0.5em;
margin-bottom: 1em;
}
@@ -137,6 +137,10 @@ button.page-menu-button {
margin-bottom: 1.5em;
}
+#topo_logend ul.congestion, #topo_logend ul.congestion li {
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
/* the checkboxes for the addresses */
#topo_legend ul li input[type=checkbox]:checked + label::before {
content:'\2713';
@@ -168,6 +172,15 @@ button.page-menu-button {
max-height: 11.6em; /* show up to 4 addresses */
overflow-y: auto;
}
+
+li.legend-sublist > ul ul {
+ margin-left: 1em;
+}
+
+#popover-div h4 {
+ margin-top: 0;
+ margin-bottom: 0;
+}
</style>
<div class="qdrTopology" ng-controller="QDR.TopologyController">
<div ng-controller="QDR.TopologyFormController">
@@ -185,22 +198,52 @@ button.page-menu-button {
<div class="legend-container hidden-xs">
<uib-accordion id="topo_legend" close-others="false">
- <div uib-accordion-group class="panel-default" is-open="legend.status.optionsOpen" heading="Options">
+ <div uib-accordion-group class="panel-default" is-open="legend.status.optionsOpen" heading="Show Traffic">
<ul class="options">
- <li class="legend-line">
- <checkbox title="Show message traffic" ng-model="legendOptions.showTraffic"></checkbox>
- <span class="legend-text" title="Select to show message traffic">Show traffic</span>
- </li>
<li class="legend-sublist" ng-hide="!legendOptions.showTraffic">
- <ul class="addresses">
- <li ng-repeat="(address, color) in addresses" class="legend-line">
- <checkbox style="background-color: {{addressColors[address]}};"
- title="{{address}}" ng-change="addressFilterChanged()"
- ng-model="addresses[address]"></checkbox>
- <span class="legend-text" ng-mouseenter="enterLegend(address)" ng-mouseleave="leaveLegend()" ng-click="addressClick(address)" title="{{address}}">{{address | limitTo : 15}}{{address.length>15 ? '…' : ''}}</span>
+ <ul>
+ <li><label>
+ <input type='radio' ng-model="legendOptions.trafficType" value="dots" />
+ Message path by address
+ </label></li>
+ <li>
+ <ul class="addresses" ng-show="legendOptions.trafficType === 'dots'">
+ <li ng-repeat="(address, color) in addresses" class="legend-line">
+ <checkbox style="background-color: {{addressColors[address]}};"
+ title="{{address}}" ng-change="addressFilterChanged()"
+ ng-model="addresses[address]"></checkbox>
+ <span class="legend-text" ng-mouseenter="enterLegend(address)" ng-mouseleave="leaveLegend()" ng-click="addressClick(address)" title="{{address}}">{{address | limitTo : 15}}{{address.length>15 ? '…' : ''}}</span>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <ul>
+ <li><label>
+ <input type='radio' ng-model="legendOptions.trafficType" value="congestion" />
+ Link utilization
+ </label></li>
+ <li>
+ <ul class="congestion" ng-show="legendOptions.trafficType === 'congestion'">
+ <li>
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" preserveAspectRatio="xMidYMid meet" width="140" height="40">
+ <defs>
+ <linearGradient xmlns="http://www.w3.org/2000/svg" id="gradienta1bEihLEHL" gradientUnits="userSpaceOnUse" x1="0%" y1="0%" x2="100%" y2="0%">
+ <stop style="stop-color: #cccccc;stop-opacity: 1" offset="0"/>
+ <stop style="stop-color: #cccccc;stop-opacity: 1" offset="0.06"/>
+ <stop style="stop-color: #00FF00;stop-opacity: 1" offset="0.333"/>
+ <stop style="stop-color: #FFA500;stop-opacity: 1" offset="0.666"/>
+ <stop style="stop-color: #FF0000;stop-opacity: 1" offset="1"/></linearGradient>
+ </defs>
+ <g>
+ <rect width="140" height="20" x="0" y="0" fill="url(#gradienta1bEihLEHL)"></rect>
+ <text x="1" y="30" class="label">Idle</text>
+ <text x="110" y="30" class="label">Busy</text>
+ </g>
+ </svg></li>
+ </ul>
</li>
</ul>
- </li>
+ </li>
</ul>
</div>
<div uib-accordion-group class="panel-default" is-open="legend.status.legendOpen" heading="Legend">
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/e54c632b/console/stand-alone/plugin/js/topology/qdrTopology.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/qdrTopology.js b/console/stand-alone/plugin/js/topology/qdrTopology.js
index 82af88a..1844937 100644
--- a/console/stand-alone/plugin/js/topology/qdrTopology.js
+++ b/console/stand-alone/plugin/js/topology/qdrTopology.js
@@ -98,44 +98,42 @@ var QDR = (function(QDR) {
let nodes = [];
let links = [];
let forceData = {nodes: nodes, links: links};
+ let urlPrefix = $location.absUrl();
+ urlPrefix = urlPrefix.split('#')[0];
+ QDR.log.debug('started QDR.TopologyController with urlPrefix: ' + urlPrefix);
$scope.multiData = [];
$scope.quiesceState = {};
let dontHide = false;
$scope.crosshtml = $sce.trustAsHtml('');
- $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false};
+ $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false, trafficType: 'dots'};
+ if (!$scope.legendOptions.trafficType)
+ $scope.legendOptions.trafficType = 'dots';
$scope.legend = {status: {legendOpen: true, optionsOpen: true}};
+ $scope.legend.status.optionsOpen = $scope.legendOptions.showTraffic;
let traffic = new Traffic($scope, $timeout, QDRService, separateAddresses,
- radius, forceData, nextHop);
+ radius, forceData, nextHop, $scope.legendOptions.trafficType, urlPrefix);
// the showTraaffic checkbox was just toggled (or initialized)
- $scope.$watch('legendOptions.showTraffic', function () {
+ $scope.$watch('legend.status.optionsOpen', function () {
+ $scope.legendOptions.showTraffic = $scope.legend.status.optionsOpen;
localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
- if ($scope.legendOptions.showTraffic) {
+ if ($scope.legend.status.optionsOpen) {
traffic.start();
} else {
traffic.stop();
traffic.remove();
+ restart();
+ }
+ });
+ $scope.$watch('legendOptions.trafficType', function () {
+ localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
+ if ($scope.legendOptions.showTraffic) {
+ restart();
+ traffic.setAnimationType($scope.legendOptions.trafficType, separateAddresses, radius);
+ traffic.start();
}
});
- // event notification that an address checkbox has changed
- $scope.addressFilterChanged = function () {
- traffic.updateAddresses();
- };
-
- // called by angular when mouse enters one of the address legends
- $scope.enterLegend = function (address) {
- // fade all flows that aren't for this address
- traffic.fadeOtherAddresses(address);
- };
- // called when the mouse leaves one of the address legends
- $scope.leaveLegend = function () {
- traffic.unFadeAll();
- };
- // clicked on the address name. toggle the address checkbox
- $scope.addressClick = function (address) {
- traffic.toggleAddress(address);
- };
$scope.quiesceConnection = function(row) {
let entity = row.entity;
@@ -342,10 +340,6 @@ var QDR = (function(QDR) {
]
};
- let urlPrefix = $location.absUrl();
- urlPrefix = urlPrefix.split('#')[0];
- QDR.log.debug('started QDR.TopologyController with urlPrefix: ' + urlPrefix);
-
// mouse event vars
let selected_node = null,
selected_link = null,
@@ -759,31 +753,28 @@ var QDR = (function(QDR) {
.on('end', function () {savePositions();})
.start();
- svg.append('svg:defs').selectAll('marker')
- .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 'end-arrow-highlighted']) // Different link/path types can be defined here
- .enter().append('svg:marker') // This section adds in the arrows
- .attr('id', String)
+ // This section adds in the arrows
+ svg.append('svg:defs').attr('class', 'marker-defs').selectAll('marker')
+ .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 'end-arrow-highlighted',
+ 'start-arrow', 'start-arrow-selected', 'start-arrow-small', 'start-arrow-highlighted'])
+ .enter().append('svg:marker')
+ .attr('id', function (d) { return d; })
.attr('viewBox', '0 -5 10 10')
- .attr('refX', 24)
+ .attr('refX', function (d) {
+ if (d.substr(0, 3) === 'end') {
+ return 24;
+ }
+ return d !== 'start-arrow-small' ? -14 : -24;})
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('orient', 'auto')
.classed('small', function (d) {return d.indexOf('small') > -1;})
.append('svg:path')
- .attr('d', 'M 0 -5 L 10 0 L 0 5 z');
-
- svg.append('svg:defs').selectAll('marker')
- .data(['start-arrow', 'start-arrow-selected', 'start-arrow-small', 'start-arrow-highlighted']) // Different link/path types can be defined here
- .enter().append('svg:marker') // This section adds in the arrows
- .attr('id', String)
- .attr('viewBox', '0 -5 10 10')
- .attr('refX', function (d) { return d !== 'start-arrow-small' ? -14 : -24;})
- .attr('markerWidth', 4)
- .attr('markerHeight', 4)
- .attr('orient', 'auto')
- .append('svg:path')
- .attr('d', 'M 10 -5 L 0 0 L 10 5 z');
+ .attr('d', function (d) {
+ return d.substr(0, 3) === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 -5 L 0 0 L 10 5 z';
+ });
+ // gradient for sender/receiver client
let grad = svg.append('svg:defs').append('linearGradient')
.attr('id', 'half-circle')
.attr('x1', '0%')
@@ -1127,6 +1118,8 @@ var QDR = (function(QDR) {
// takes the nodes and links array of objects and adds svg elements for everything that hasn't already
// been added
function restart(start) {
+ if (!circle)
+ return;
circle.call(force.drag);
// path (link) group
@@ -1138,19 +1131,22 @@ var QDR = (function(QDR) {
})
.classed('highlighted', function(d) {
return d.highlighted;
- })
- .attr('marker-start', function(d) {
- let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
- if (d.highlighted)
- sel = '-highlighted';
- return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : '';
- })
- .attr('marker-end', function(d) {
- let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
- if (d.highlighted)
- sel = '-highlighted';
- return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : '';
});
+ if (!$scope.legend.status.optionsOpen || $scope.legendOptions.trafficType === 'dots') {
+ path
+ .attr('marker-start', function(d) {
+ let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
+ if (d.highlighted)
+ sel = '-highlighted';
+ return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : '';
+ })
+ .attr('marker-end', function(d) {
+ let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
+ if (d.highlighted)
+ sel = '-highlighted';
+ return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : '';
+ });
+ }
// add new links. if a link with a new uid is found in the data, add a new path
path.enter().append('svg:path')
.attr('class', 'link')
@@ -1198,16 +1194,27 @@ var QDR = (function(QDR) {
restart();
})
.on('mousemove', function (d) {
+ let updateTooltip = function () {
+ $timeout(function () {
+ $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d));
+ });
+ };
+ // update the contents of the popup tooltip each time the data is polled
+ QDRService.management.topology.addUpdatedAction('connectionPopupHTML', updateTooltip);
+
+ // show the tooltip
let top = $('#topology').offset().top - 5;
- $timeout(function () {
- $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d));
- });
d3.select('#popover-div')
.style('display', 'block')
.style('left', (d3.event.pageX+5)+'px')
.style('top', (d3.event.pageY-top)+'px');
+
+ // update the tooltip right now
+ updateTooltip();
+
})
.on('mouseout', function() { // mouse out of a path
+ QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
d3.select('#popover-div')
.style('display', 'none');
selected_link = null;
@@ -1993,6 +2000,7 @@ var QDR = (function(QDR) {
QDRService.management.topology.stopUpdating();
QDRService.management.topology.delUpdatedAction('normalsStats');
QDRService.management.topology.delUpdatedAction('topology');
+ QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
d3.select('#SVG_ID').remove();
window.removeEventListener('resize', resize);
@@ -2094,16 +2102,23 @@ var QDR = (function(QDR) {
});
if (rightIndex < 0) {
// we have a connection to a client/service
- rightIndex = +left.connectionId;
+ rightIndex = +left.resultIndex;
}
if (isNaN(rightIndex)) {
// we have a connection to a console
- rightIndex = +right.connectionId;
+ rightIndex = +right.resultIndex;
}
let HTML = '';
if (rightIndex >= 0) {
let conn = onode['connection'].results[rightIndex];
conn = QDRService.utilities.flatten(onode['connection'].attributeNames, conn);
+ if ($scope.legend.status.optionsOpen && traffic) {
+ HTML = traffic.connectionPopupHTML(onode, conn, d);
+ if (HTML)
+ return HTML;
+ else
+ HTML = '';
+ }
HTML += '<table class="popupTable">';
HTML += ('<tr><td>Security</td><td>' + connSecurity(conn) + '</td></tr>');
HTML += ('<tr><td>Authentication</td><td>' + connAuth(conn) + '</td></tr>');
@@ -2122,4 +2137,4 @@ var QDR = (function(QDR) {
return QDR;
-}(QDR || {}));
\ No newline at end of file
+}(QDR || {}));
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/e54c632b/console/stand-alone/plugin/js/topology/traffic.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/traffic.js b/console/stand-alone/plugin/js/topology/traffic.js
index 883cb2f..e061000 100644
--- a/console/stand-alone/plugin/js/topology/traffic.js
+++ b/console/stand-alone/plugin/js/topology/traffic.js
@@ -18,42 +18,25 @@ under the License.
*/
'use strict';
-/* global d3 ChordData MIN_CHORD_THRESHOLD */
+/* global d3 ChordData MIN_CHORD_THRESHOLD Promise */
-/* Create animated dots moving along the links between routers
- to show that there is message flow between routers.
- */
const transitionDuration = 1000;
-const CHORDFILTERKEY = 'chordFilter';
+const CHORDFILTERKEY = 'chordFilter';
-function Traffic ($scope, $timeout, QDRService, converter, radius, topology, nextHop, excluded) {
+function Traffic ($scope, $timeout, QDRService, converter, radius, topology, nextHop, type, prefix) {
+ $scope.addressColors = {};
this.QDRService = QDRService;
- this.radius = radius; // the radius of a router circle
- this.topology = topology; // contains the list of router nodes
- this.nextHop = nextHop; // fn that returns the route through the network between two routers
+ this.type = type; // moving dots or colored path
+ this.prefix = prefix; // url prefix used in svg url()s
+ this.topology = topology; // contains the list of router nodes
+ this.nextHop = nextHop; // fn that returns the route through the network between two routers
this.$scope = $scope;
this.$timeout = $timeout;
- $scope.addressColors = {};
- this.excludedAddresses = JSON.parse(localStorage[CHORDFILTERKEY]) || [];
-
// internal variables
- this.interval = null; // setInterval handle
- this.lastFlows = {}; // the number of dots animated between routers
- this.chordData = new ChordData(this.QDRService, true, converter); // gets ingressHistogram data
- this.chordData.setFilter(this.excludedAddresses);
- this.$scope.addresses = {};
- this.chordData.getMatrix().then( function () {
- $timeout( function () {
- this.$scope.addresses = this.chordData.getAddresses();
- for (let address in this.$scope.addresses) {
- this.fillColor(address);
- }
- }.bind(this));
- }.bind(this));
+ this.interval = null; // setInterval handle
+ this.setAnimationType(type, converter, radius);
}
-/* Public methods on Traffic object */
-
// stop updating the traffic data
Traffic.prototype.stop = function () {
if (this.interval) {
@@ -63,124 +46,384 @@ Traffic.prototype.stop = function () {
};
// start updating the traffic data
Traffic.prototype.start = function () {
- this.doUpdate(this);
+ this.doUpdate();
this.interval = setInterval(this.doUpdate.bind(this), transitionDuration);
};
-// remove any animationions that are in progress
-Traffic.prototype.remove = function () {
+// remove any animations that are in progress
+Traffic.prototype.remove = function() {
+ if (this.vis)
+ this.vis.remove();
+};
+// called when one of the address checkboxes is toggled
+Traffic.prototype.setAnimationType = function (type, converter, radius) {
+ this.stop();
+ this.remove();
+ this.type = type;
+ this.vis = type === 'dots' ? new Dots(this, converter, radius) :
+ new Congestion(this);
+};
+// called periodically to refresh the traffic flow
+Traffic.prototype.doUpdate = function () {
+ this.vis.doUpdate();
+};
+Traffic.prototype.connectionPopupHTML = function (onode, conn, d) {
+ return this.vis.connectionPopupHTML(onode, conn, d);
+};
+
+/* Base class for congestion and dots visualizations */
+function TrafficAnimation (traffic) {
+ this.traffic = traffic;
+}
+TrafficAnimation.prototype.nodeIndexFor = function (nodes, name) {
+ for (let i=0; i<nodes.length; i++) {
+ let node = nodes[i];
+ if (node.container === name)
+ return i;
+ }
+ return -1;
+};
+TrafficAnimation.prototype.connectionPopupHTML = function () {
+ return null;
+};
+
+/* Color the links between router to show how heavily used the links are. */
+function Congestion (traffic) {
+ TrafficAnimation.call(this, traffic);
+ this.init_markerDef();
+}
+Congestion.prototype = Object.create(TrafficAnimation.prototype);
+Congestion.prototype.constructor = Congestion;
+
+Congestion.prototype.init_markerDef = function () {
+ this.custom_markers_def = d3.select('#SVG_ID').select('defs.custom-markers');
+ if (this.custom_markers_def.empty()) {
+ this.custom_markers_def = d3.select('#SVG_ID').append('svg:defs').attr('class', 'custom-markers');
+ }
+};
+Congestion.prototype.findResult = function (node, entity, attribute, value) {
+ let attrIndex = node[entity].attributeNames.indexOf(attribute);
+ if (attrIndex >= 0) {
+ for (let i=0; i<node[entity].results.length; i++) {
+ if (node[entity].results[i][attrIndex] === value) {
+ return this.traffic.QDRService.utilities.flatten(node[entity].attributeNames, node[entity].results[i]);
+ }
+ }
+ }
+ return null;
+};
+Congestion.prototype.doUpdate = function () {
+ let self = this;
+ this.traffic.QDRService.management.topology.ensureAllEntities(
+ [{ entity: 'router.link', force: true},{entity: 'connection'}], function () {
+ let links = {};
+ let nodeInfo = self.traffic.QDRService.management.topology.nodeInfo();
+ // accumulate all the inter-router links in an object
+ // keyed by the svgs path id
+ for (let nodeId in nodeInfo) {
+ let node = nodeInfo[nodeId];
+ let nodeLinks = node['router.link'];
+ for (let n=0; n<nodeLinks.results.length; n++) {
+ let link = self.traffic.QDRService.utilities.flatten(nodeLinks.attributeNames, nodeLinks.results[n]);
+ if (link.linkType !== 'router-control') {
+ let f = self.nodeIndexFor(self.traffic.topology.nodes,
+ self.traffic.QDRService.management.topology.nameFromId(nodeId));
+ let connection = self.findResult(node, 'connection', 'identity', link.connectionId);
+ if (connection) {
+ let t = self.nodeIndexFor(self.traffic.topology.nodes, connection.container);
+ let little = Math.min(f, t);
+ let big = Math.max(f, t);
+ let key = ['#path', little, big].join('-');
+ if (!links[key])
+ links[key] = [];
+ links[key].push(link);
+ }
+ }
+ }
+ }
+ // accumulate the colors/directions to be used
+ let colors = {};
+ for (let key in links) {
+ let congestion = self.congestion(links[key]);
+ let path = d3.select(key);
+ if (path && !path.empty()) {
+ let dir = path.attr('marker-end') === '' ? 'start' : 'end';
+ let small = path.attr('class').indexOf('small') > -1;
+ let id = dir + '-' + congestion.substr(1) + (small ? '-s' : '');
+ colors[id] = {dir: dir, color: congestion, small: small};
+ path
+ .attr('stroke', congestion)
+ .classed('traffic', true)
+ .attr('marker-start', function(d) {
+ return d.left ? 'url(' + self.traffic.prefix + '#' + id + ')' : '';
+ })
+ .attr('marker-end', function(d) {
+ return d.right ? 'url(' + self.traffic.prefix + '#' + id + ')' : '';
+ });
+ }
+ }
+
+ // create the svg:def that holds the custom markers
+ self.init_markerDef();
+
+ let colorKeys = Object.keys(colors);
+ let custom_markers = self.custom_markers_def.selectAll('marker')
+ .data(colorKeys, function (d) {return d;});
+
+ custom_markers.enter().append('svg:marker')
+ .attr('id', function (d) { return d; })
+ .attr('viewBox', '0 -5 10 10')
+ .attr('refX', function (d) {
+ return colors[d].dir === 'end' ? 24 : (colors[d].small) ? -24 : -14;
+ })
+ .attr('markerWidth', 4)
+ .attr('markerHeight', 4)
+ .attr('orient', 'auto')
+ .style('fill', function (d) {return colors[d].color;})
+ .append('svg:path')
+ .attr('d', function (d) {
+ return colors[d].dir === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 -5 L 0 0 L 10 5 z';
+ });
+ custom_markers.exit().remove();
+
+ });
+};
+Congestion.prototype.congestion = function (links) {
+ let v = 0;
+ for (let l=0; l<links.length; l++) {
+ let link = links[l];
+ v = Math.max(v, (link.undeliveredCount+link.unsettledCount)/link.capacity);
+ }
+ return this.fillColor(v);
+};
+Congestion.prototype.fillColor = function (v) {
+ let color = d3.scale.linear().domain([0, 1, 2, 3])
+ .interpolate(d3.interpolateHcl)
+ .range([d3.rgb('#CCCCCC'), d3.rgb('#00FF00'), d3.rgb('#FFA500'), d3.rgb('#FF0000')]);
+ return color(v);
+};
+Congestion.prototype.remove = function () {
+ d3.select('#SVG_ID').selectAll('path.traffic')
+ .classed('traffic', false);
+ d3.select('#SVG_ID').select('defs.custom-markers')
+ .selectAll('marker').remove();
+};
+
+// construct HTML to be used in a popup when the mouse is moved over a link.
+// The HTML is sanitized elsewhere before it is displayed
+Congestion.prototype.connectionPopupHTML = function (onode, conn, d) {
+ const max_links = 10;
+ const fields = ['undelivered', 'unsettled', 'rejected', 'released', 'modified'];
+ // local function to determine if a link's connectionId is in any of the connections
+ let isLinkFor = function (connectionId, conns) {
+ for (let c=0; c<conns.length; c++) {
+ if (conns[c].identity === connectionId)
+ return true;
+ }
+ return false;
+ };
+ let fnJoin = function (ar, sepfn) {
+ let out = '';
+ out = ar[0];
+ for (let i=1; i<ar.length; i++) {
+ let sep = sepfn(ar[i]);
+ out += (sep[0] + sep[1]);
+ }
+ return out;
+ };
+ let conns = [conn];
+ // if the data for the line is from a client (small circle), we may have multiple connections
+ if (d.cls === 'small') {
+ conns = [];
+ let normals = d.target.normals ? d.target.normals : d.source.normals;
+ for (let n=0; n<normals.length; n++) {
+ if (normals[n].resultIndex !== undefined) {
+ conns.push(this.traffic.QDRService.utilities.flatten(onode['connection'].attributeNames,
+ onode['connection'].results[normals[n].resultIndex]));
+ }
+ }
+ }
+ // loop through all links for this router and accumulate those belonging to the connection(s)
+ let nodeLinks = onode['router.link'];
+ let links = [];
+ let hasAddress = false;
+ for (let n=0; n<nodeLinks.results.length; n++) {
+ let link = this.traffic.QDRService.utilities.flatten(nodeLinks.attributeNames, nodeLinks.results[n]);
+ if (link.linkType !== 'router-control') {
+ if (isLinkFor(link.connectionId, conns)) {
+ if (link.owningAddr)
+ hasAddress = true;
+ links.push(link);
+ }
+ }
+ }
+ // we may need to limit the number of links displayed, so sort descending by the sum of the field values
+ links.sort( function (a, b) {
+ let asum = a.undeliveredCount + a.unsettledCount + a.rejectedCount + a.releasedCount + a.modifiedCount;
+ let bsum = b.undeliveredCount + b.unsettledCount + b.rejectedCount + b.releasedCount + b.modifiedCount;
+ return asum < bsum ? 1 : asum > bsum ? -1 : 0;
+ });
+ let HTMLHeading = '<h4>Links</h4>';
+ let HTML = '<table class="popupTable">';
+ // copy of fields since we may be prepending an address
+ let th = fields.slice();
+ // convert to actual attribute names
+ let td = fields.map( function (f) {return f + 'Count';});
+ th.unshift('dir');
+ td.unshift('linkDir');
+ // add an address field if any of the links had an owningAddress
+ if (hasAddress) {
+ th.unshift('address');
+ td.unshift('owningAddr');
+ }
+ // add rows to the table for each link
+ HTML += ('<tr><td>' + th.join('</td><td>') + '</tr></td>');
+ for (let l=0; l<links.length; l++) {
+ if (l>=max_links) {
+ HTMLHeading = '<h4>Top ' + max_links + ' Links</h4>';
+ break;
+ }
+ let link = links[l];
+ let vals = td.map( function (f) {
+ if (f === 'owningAddr') {
+ let identity = this.traffic.QDRService.utilities.identity_clean(link.owningAddr);
+ return this.traffic.QDRService.utilities.addr_text(identity);
+ }
+ return link[f];
+ }.bind(this));
+ let joinedVals = fnJoin(vals, function (v1) {
+ return ['</td><td' + (isNaN(+v1) ? '': ' align="right"') + '>', this.traffic.QDRService.utilities.pretty(v1 || '0')];
+ }.bind(this));
+ HTML += ('<tr><td>' + joinedVals + '</td></tr>');
+ }
+ HTML += '</table>';
+ return HTMLHeading + HTML;
+};
+
+/* Create animated dots moving along the links between routers
+ to show message flow */
+function Dots (traffic, converter, radius) {
+ TrafficAnimation.call(this, traffic);
+ this.excludedAddresses = localStorage[CHORDFILTERKEY] ? JSON.parse(localStorage[CHORDFILTERKEY]) : [];
+ this.radius = radius; // the radius of a router circle
+ this.lastFlows = {}; // the number of dots animated between routers
+ this.chordData = new ChordData(this.traffic.QDRService, true, converter); // gets ingressHistogram data
+ this.chordData.setFilter(this.excludedAddresses);
+ traffic.$scope.addresses = {};
+ this.chordData.getMatrix().then(function () {
+ this.traffic.$timeout(function () {
+ this.traffic.$scope.addresses = this.chordData.getAddresses();
+ for (let address in this.traffic.$scope.addresses) {
+ this.fillColor(address);
+ }
+ }.bind(this));
+ }.bind(this));
+ // colors
+ this.colorGen = d3.scale.category10();
+ let self = this;
+ // event notification that an address checkbox has changed
+ traffic.$scope.addressFilterChanged = function () {
+ self.updateAddresses()
+ .then(function () {
+ // don't wait for the next polling cycle. update now
+ self.traffic.stop();
+ self.traffic.start();
+ });
+ };
+ // called by angular when mouse enters one of the address legends
+ traffic.$scope.enterLegend = function (address) {
+ // fade all flows that aren't for this address
+ self.fadeOtherAddresses(address);
+ };
+ // called when the mouse leaves one of the address legends
+ traffic.$scope.leaveLegend = function () {
+ self.unFadeAll();
+ };
+ // clicked on the address name. toggle the address checkbox
+ traffic.$scope.addressClick = function (address) {
+ self.toggleAddress(address)
+ .then(function () {
+ self.updateAddresses();
+ });
+ };
+}
+Dots.prototype = Object.create(TrafficAnimation.prototype);
+Dots.prototype.constructor = Dots;
+
+Dots.prototype.remove = function () {
for (let id in this.lastFlows) {
d3.select('#SVG_ID').selectAll('circle.flow' + id).remove();
}
this.lastFlows = {};
};
-// called when one of the address checkboxes is toggled
-Traffic.prototype.updateAddresses = function () {
+Dots.prototype.updateAddresses = function () {
this.excludedAddresses = [];
- for (let address in this.$scope.addresses) {
- if (!this.$scope.addresses[address])
+ for (let address in this.traffic.$scope.addresses) {
+ if (!this.traffic.$scope.addresses[address])
this.excludedAddresses.push(address);
}
localStorage[CHORDFILTERKEY] = JSON.stringify(this.excludedAddresses);
- if (this.chordData)
+ if (this.chordData)
this.chordData.setFilter(this.excludedAddresses);
- // don't wait for the next polling cycle. update now
- this.stop();
- this.start();
+ return new Promise(function (resolve) {
+ return resolve();
+ });
};
-Traffic.prototype.toggleAddress = function (address) {
- this.$scope.addresses[address] = !this.$scope.addresses[address];
- this.updateAddresses();
+Dots.prototype.toggleAddress = function (address) {
+ this.traffic.$scope.addresses[address] = !this.traffic.$scope.addresses[address];
+ return new Promise(function (resolve) {
+ return resolve();
+ });
};
-Traffic.prototype.fadeOtherAddresses = function (address) {
- d3.selectAll('circle.flow').classed('fade', function(d) {
+Dots.prototype.fadeOtherAddresses = function (address) {
+ d3.selectAll('circle.flow').classed('fade', function (d) {
return d.address !== address;
});
};
-Traffic.prototype.unFadeAll = function () {
+Dots.prototype.unFadeAll = function () {
d3.selectAll('circle.flow').classed('fade', false);
};
-
-/* The following don't need to be public, but they are for simplicity sake */
-
-// called periodically to refresh the traffic flow
-Traffic.prototype.doUpdate = function () {
+Dots.prototype.doUpdate = function () {
let self = this;
// we need the nextHop data to show traffic between routers that are connected by intermediaries
- this.QDRService.management.topology.ensureAllEntities([{entity: 'router.node', attrs: ['id','nextHop']}],
- function () {
- // get the ingressHistogram data for all routers
- self.chordData.getMatrix().then(self.render.bind(self), function (e) {
- console.log('Could not get message histogram' + e);
- });
+ this.traffic.QDRService.management.topology.ensureAllEntities([{ entity: 'router.node', attrs: ['id', 'nextHop'] }], function () {
+ // get the ingressHistogram data for all routers
+ self.chordData.getMatrix().then(self.render.bind(self), function (e) {
+ console.log('Could not get message histogram' + e);
});
+ });
};
-
-// calculate the translation for each dot along the path
-let translateDots = function (radius, path, count, back) {
- let pnode = path.node();
- // will be called for each element in the flow selection (for each dot)
- return function(d) {
- // will be called with t going from 0 to 1 for each dot
- return function(t) {
- // start the points at different positions depending on their value (d)
- let tt = t * 1000;
- let f = ((tt + (d.i*1000/count)) % 1000)/1000;
- if (back)
- f = 1 - f;
- // l needs to be calculated each tick because the path's length might be changed during the animation
- let l = pnode.getTotalLength();
- let p = pnode.getPointAtLength(f * l);
- return 'translate(' + p.x + ',' + p.y + ')';
- };
- };
-};
-// animate the d3 selection (flow) along the given path
-Traffic.prototype.animateFlow = function (flow, path, count, back, rate) {
- let self = this;
- let l = path.node().getTotalLength();
- flow.transition()
- .ease('easeLinear')
- .duration(l*10/rate)
- .attrTween('transform', translateDots(self.radius, path, count, back))
- .each('end', function () {self.animateFlow(flow, path, count, back, rate);});
-};
-
-Traffic.prototype.render = function (matrix) {
- this.$timeout(
- function () {
- this.$scope.addresses = this.chordData.getAddresses();
- }.bind(this)
- );
+Dots.prototype.render = function (matrix) {
+ this.traffic.$timeout(function () {
+ this.traffic.$scope.addresses = this.chordData.getAddresses();
+ }.bind(this));
// get the rate of message flow between routers
- let hops = {}; // every hop between routers that is involved in message flow
+ let hops = {}; // every hop between routers that is involved in message flow
let matrixMessages = matrix.matrixMessages();
// the fastest traffic rate gets 3 times as many dots as the slowest
let minmax = matrix.getMinMax();
- let flowScale = d3.scale.linear().domain(minmax).range([1,1.75]);
-
+ let flowScale = d3.scale.linear().domain(minmax).range([1, 1.75]);
// row is ingress router, col is egress router. Value at [row][col] is the rate
- matrixMessages.forEach( function (row, r) {
+ matrixMessages.forEach(function (row, r) {
row.forEach(function (val, c) {
if (val > MIN_CHORD_THRESHOLD) {
// translate between matrix row/col and node index
- let f = nodeIndexFor(this.topology.nodes, matrix.rows[r].egress);
- let t = nodeIndexFor(this.topology.nodes, matrix.rows[r].ingress);
+ let f = this.nodeIndexFor(this.traffic.topology.nodes, matrix.rows[r].egress);
+ let t = this.nodeIndexFor(this.traffic.topology.nodes, matrix.rows[r].ingress);
let address = matrix.getAddress(r, c);
-
if (r !== c) {
- // move the dots along the links between the routers
- this.nextHop(this.topology.nodes[f], this.topology.nodes[t], function (link, fnode, tnode) {
+ // accumulate the hops between the ingress and egress routers
+ this.traffic.nextHop(this.traffic.topology.nodes[f], this.traffic.topology.nodes[t], function (link, fnode, tnode) {
let key = '-' + link.uid;
let back = fnode.index < tnode.index;
if (!hops[key])
hops[key] = [];
- hops[key].push({val: val, back: back, address: address});
+ hops[key].push({ val: val, back: back, address: address });
});
}
// Find the senders connected to nodes[f] and the receivers connected to nodes[t]
// and add their links to the animation
- addClients(hops, this.topology.nodes, f, val, true, address);
- addClients(hops, this.topology.nodes, t, val, false, address);
+ this.addClients(hops, this.traffic.topology.nodes, f, val, true, address);
+ this.addClients(hops, this.traffic.topology.nodes, t, val, false, address);
}
}.bind(this));
}.bind(this));
@@ -188,12 +431,12 @@ Traffic.prototype.render = function (matrix) {
let keep = {};
for (let id in hops) {
let hop = hops[id];
- for (let h=0; h<hop.length; h++) {
+ for (let h = 0; h < hop.length; h++) {
let ahop = hop[h];
- let flowId = id + '-' + addressIndex(this, ahop.address) + (ahop.back ? 'b' : '');
+ let flowId = id + '-' + this.addressIndex(this, ahop.address) + (ahop.back ? 'b' : '');
let path = d3.select('#path' + id);
// start the animation. If the animation is already running, this will have no effect
- this.startDots(path, flowId, ahop, flowScale(ahop.val));
+ this.startAnimation(path, flowId, ahop, flowScale(ahop.val));
keep[flowId] = true;
}
}
@@ -205,17 +448,29 @@ Traffic.prototype.render = function (matrix) {
}
}
};
-
+// animate the d3 selection (flow) along the given path
+Dots.prototype.animateFlow = function (flow, path, count, back, rate) {
+ let self = this;
+ let l = path.node().getTotalLength();
+ flow.transition()
+ .ease('easeLinear')
+ .duration(l * 10 / rate)
+ .attrTween('transform', self.translateDots(self.radius, path, count, back))
+ .each('end', function () { self.animateFlow(flow, path, count, back, rate); });
+};
// create dots along the path between routers
-Traffic.prototype.startDots = function (path, id, hop, rate) {
- let back = hop.back, address = hop.address;
+Dots.prototype.startAnimation = function (path, id, hop, rate) {
if (!path.node())
return;
+ this.animateDots(path, id, hop, rate);
+};
+Dots.prototype.animateDots = function (path, id, hop, rate) {
+ let back = hop.back, address = hop.address;
// the density of dots is determined by the rate of this traffic relative to the other traffic
let len = Math.max(Math.floor(path.node().getTotalLength() / 50), 1);
let dots = [];
- for (let i=0, offset=addressIndex(this, address); i<len; ++i) {
- dots[i] = {i: i + 10 * offset, address: address};
+ for (let i = 0, offset = this.addressIndex(this, address); i < len; ++i) {
+ dots[i] = { i: i + 10 * offset, address: address };
}
// keep track of the number of dots for each link. If the length of the link is changed,
// re-create the animation
@@ -229,38 +484,23 @@ Traffic.prototype.startDots = function (path, id, hop, rate) {
}
let flow = d3.select('#SVG_ID').selectAll('circle.flow' + id)
.data(dots, function (d) { return d.i + d.address; });
-
let circles = flow
.enter().append('circle')
.attr('class', 'flow flow' + id)
.attr('fill', this.fillColor(address))
.attr('r', 5);
-
this.animateFlow(circles, path, dots.length, back, rate);
-
flow.exit()
.remove();
};
-
-// colors
-let colorGen = d3.scale.category10();
-Traffic.prototype.fillColor = function (n) {
- if (!(n in this.$scope.addressColors)) {
- let ci = Object.keys(this.$scope.addressColors).length;
- this.$scope.addressColors[n] = colorGen(ci);
+Dots.prototype.fillColor = function (n) {
+ if (!(n in this.traffic.$scope.addressColors)) {
+ let ci = Object.keys(this.traffic.$scope.addressColors).length;
+ this.traffic.$scope.addressColors[n] = this.colorGen(ci);
}
- return this.$scope.addressColors[n];
+ return this.traffic.$scope.addressColors[n];
};
-// return the node index for a router name
-let nodeIndexFor = function (nodes, name) {
- for (let i=0; i<nodes.length; i++) {
- let node = nodes[i];
- if (node.routerId === name)
- return i;
- }
- return -1;
-};
-let addClients = function (hops, nodes, f, val, sender, address) {
+Dots.prototype.addClients = function (hops, nodes, f, val, sender, address) {
let cdir = sender ? 'out' : 'in';
for (let n=0; n<nodes.length; n++) {
let node = nodes[n];
@@ -273,6 +513,25 @@ let addClients = function (hops, nodes, f, val, sender, address) {
}
}
};
-let addressIndex = function (traffic, address) {
- return Object.keys(traffic.$scope.addresses).indexOf(address);
-};
\ No newline at end of file
+Dots.prototype.addressIndex = function (vis, address) {
+ return Object.keys(vis.traffic.$scope.addresses).indexOf(address);
+};
+// calculate the translation for each dot along the path
+Dots.prototype.translateDots = function (radius, path, count, back) {
+ let pnode = path.node();
+ // will be called for each element in the flow selection (for each dot)
+ return function(d) {
+ // will be called with t going from 0 to 1 for each dot
+ return function(t) {
+ // start the points at different positions depending on their value (d)
+ let tt = t * 1000;
+ let f = ((tt + (d.i*1000/count)) % 1000)/1000;
+ if (back)
+ f = 1 - f;
+ // l needs to be calculated each tick because the path's length might be changed during the animation
+ let l = pnode.getTotalLength();
+ let p = pnode.getPointAtLength(f * l);
+ return 'translate(' + p.x + ',' + p.y + ')';
+ };
+ };
+};
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org