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 <eal...@redhat.com> Authored: Tue May 29 05:36:30 2018 -0400 Committer: Ernest Allen <eal...@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-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org