Jian Li
Committed by Gerrit Code Review

[ONOS-3851] Implement default Web GUI page for CPMan

- Reduce the datapoints to 20, resolve cold start problem
- Code refactoring for CpmanViewMessageHandler
- Code refactoring for cpman.js
- Show "No Data" message when client does not receive any data
- Clean up cpman.css
- Specify default colors for charting
- Resolve ArrayIndexOutofBoundsException when the number returned
  dataset is less the number what we expected

Change-Id: I67ab3160ab66f92eaffeffc2d61c7d0e17be0512
......@@ -27,7 +27,6 @@ import org.onosproject.cpman.ControlLoadSnapshot;
import org.onosproject.cpman.ControlMetricType;
import org.onosproject.cpman.ControlPlaneMonitorService;
import org.onosproject.net.DeviceId;
import org.onosproject.net.device.DeviceService;
import org.onosproject.ui.RequestHandler;
import org.onosproject.ui.UiMessageHandler;
import org.onosproject.ui.chart.ChartModel;
......@@ -38,14 +37,16 @@ import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.LongStream;
import static org.onosproject.cpman.ControlResource.CONTROL_MESSAGE_METRICS;
import static org.onosproject.cpman.ControlResource.Type.CONTROL_MESSAGE;
/**
* CpmanViewMessageHandler class implementation.
* Message handler for control plane monitoring view related messages.
*/
public class CpmanViewMessageHandler extends UiMessageHandler {
......@@ -55,12 +56,14 @@ public class CpmanViewMessageHandler extends UiMessageHandler {
private static final String CPMAN_DATA_RESP = "cpmanDataResponse";
private static final String CPMANS = "cpmans";
// TODO: we assume that server side always returns 60 data points
// TODO: we assume that server side always returns 20 data points
// to feed 1 hour time slots, later this should make to be configurable
private static final int NUM_OF_DATA_POINTS = 60;
private static final int NUM_OF_DATA_POINTS = 20;
private static final int MILLI_CONV_UNIT = 1000;
private long timestamp = 0L;
@Override
protected Collection<RequestHandler> createRequestHandlers() {
return ImmutableSet.of(
......@@ -83,50 +86,104 @@ public class CpmanViewMessageHandler extends UiMessageHandler {
@Override
protected void populateChart(ChartModel cm, ObjectNode payload) {
String uri = string(payload, "devId");
ControlPlaneMonitorService cpms = get(ControlPlaneMonitorService.class);
ClusterService cs = get(ClusterService.class);
if (!Strings.isNullOrEmpty(uri)) {
Map<ControlMetricType, Long[]> data = Maps.newHashMap();
DeviceId deviceId = DeviceId.deviceId(uri);
ClusterService cs = get(ClusterService.class);
ControlPlaneMonitorService cpms = get(ControlPlaneMonitorService.class);
if (cpms.availableResources(CONTROL_MESSAGE).contains(deviceId.toString())) {
LocalDateTime ldt = null;
try {
for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
ControlLoadSnapshot cls = cpms.getLoad(cs.getLocalNode().id(),
cmt, NUM_OF_DATA_POINTS, TimeUnit.MINUTES,
Optional.of(deviceId)).get();
data.put(cmt, ArrayUtils.toObject(cls.recent()));
if (ldt == null) {
ldt = new LocalDateTime(cls.time() * MILLI_CONV_UNIT);
}
}
for (int i = 0; i < NUM_OF_DATA_POINTS; i++) {
Map<String, Long> local = Maps.newHashMap();
for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
local.put(StringUtils.lowerCase(cmt.name()), data.get(cmt)[i]);
}
local.put(LABEL, ldt.minusMinutes(NUM_OF_DATA_POINTS - i).toDateTime().getMillis());
populateMetric(cm.addDataPoint(ldt.minusMinutes(NUM_OF_DATA_POINTS - i)
.toDateTime().getMillis()), local);
}
} catch (InterruptedException | ExecutionException e) {
log.warn(e.getMessage());
Map<ControlMetricType, Long[]> data = generateMatrix(cpms, cs, deviceId);
LocalDateTime ldt = new LocalDateTime(timestamp * MILLI_CONV_UNIT);
populateMetrics(cm, data, ldt, NUM_OF_DATA_POINTS);
}
} else {
Set<String> deviceIds = cpms.availableResources(CONTROL_MESSAGE);
for (String deviceId : deviceIds) {
Map<ControlMetricType, Long> data =
populateDeviceMetrics(cpms, cs, DeviceId.deviceId(deviceId));
Map<String, Long> local = Maps.newHashMap();
for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
local.put(StringUtils.lowerCase(cmt.name()), data.get(cmt));
}
// TODO: need to find a way to present device id using long type
String shortId = StringUtils.substring(deviceId,
deviceId.length() - 2, deviceId.length());
local.put(LABEL, Long.valueOf(shortId));
populateMetric(cm.addDataPoint(Long.valueOf(shortId)), local);
}
}
}
private Map<ControlMetricType, Long> populateDeviceMetrics(ControlPlaneMonitorService cpms,
ClusterService cs, DeviceId deviceId) {
Map<ControlMetricType, Long> data = Maps.newHashMap();
for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
ControlLoadSnapshot cls;
try {
cls = cpms.getLoad(cs.getLocalNode().id(),
cmt, NUM_OF_DATA_POINTS, TimeUnit.MINUTES,
Optional.of(deviceId)).get();
data.put(cmt, Math.round(LongStream.of(cls.recent()).average().getAsDouble()));
timestamp = cls.time();
} catch (InterruptedException | ExecutionException e) {
log.warn(e.getMessage());
}
}
return data;
}
private Map<ControlMetricType, Long[]> generateMatrix(ControlPlaneMonitorService cpms,
ClusterService cs, DeviceId deviceId) {
Map<ControlMetricType, Long[]> data = Maps.newHashMap();
for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
ControlLoadSnapshot cls;
try {
cls = cpms.getLoad(cs.getLocalNode().id(),
cmt, NUM_OF_DATA_POINTS, TimeUnit.MINUTES,
Optional.of(deviceId)).get();
// TODO: in some cases, the number of returned dataset is
// less than what we expected (expected -1)
// As a workaround, we simply fill the slot with 0 values,
// such a bug should be fixed with updated RRD4J lib...
data.put(cmt, ArrayUtils.toObject(fillData(cls.recent(), NUM_OF_DATA_POINTS)));
timestamp = cls.time();
} catch (InterruptedException | ExecutionException e) {
log.warn(e.getMessage());
}
}
return data;
}
private long[] fillData(long[] origin, int expected) {
if (origin.length == expected) {
return origin;
} else {
DeviceService ds = get(DeviceService.class);
ds.getAvailableDevices();
long[] filled = new long[expected];
for (int i = 0; i < expected; i++) {
if (i == 0) {
filled[i] = 0;
} else {
filled[i] = origin[i - 1];
}
}
return filled;
}
}
private void populateAllDevs(ChartModel.DataPoint dataPoint, Map<String, Long> data) {
private void populateMetrics(ChartModel cm, Map<ControlMetricType,
Long[]> data, LocalDateTime time, int numOfDp) {
for (int i = 0; i < numOfDp; i++) {
Map<String, Long> local = Maps.newHashMap();
for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
local.put(StringUtils.lowerCase(cmt.name()), data.get(cmt)[i]);
}
local.put(LABEL, time.minusMinutes(numOfDp - i).toDateTime().getMillis());
populateMetric(cm.addDataPoint(time.minusMinutes(numOfDp - i)
.toDateTime().getMillis()), local);
}
}
private void populateMetric(ChartModel.DataPoint dataPoint,
......
......@@ -20,6 +20,7 @@
#ov-cpman {
padding: 20px;
position: relative;
}
.light #ov-cpman {
color: navy;
......@@ -40,22 +41,17 @@
background-color: #444;
}
#ov-cpman .my-button {
cursor: pointer;
padding: 4px;
#ov-cpman #chart-loader {
position: absolute;
width: 200px;
height: 50px;
margin-left: -100px;
margin-top: -25px;
z-index: 900;
top: 50%;
text-align: center;
}
.light #ov-cpman .my-button {
color: white;
background-color: #99d;
}
.dark #ov-cpman .my-button {
color: black;
background-color: #aaa;
}
#ov-cpman .number {
font-size: 140%;
text-align: right;
left: 50%;
font-size: 25px;
font-weight: bold;
color: #ccc;
}
\ No newline at end of file
......
<!-- partial HTML -->
<div id="ov-cpman">
<div>
<div id="chart-loader" ng-show="!devId && showLoader">
No Data
</div>
<div ng-show="!devId">
<canvas id="bar" class="chart chart-bar" chart-data="data"
chart-labels="labels" chart-legend="true" chart-click="onClick"
chart-series="series" chart-options="options" height="100%">
</canvas>
</div>
<div ng-show="devId">
<h2>
Chart for Device {{devId || "(No device selected)"}}
</h2>
<canvas id="line" class="chart chart-line" chart-data="data"
chart-labels="labels" chart-legend="true" chart-series="series">
chart-labels="labels" chart-legend="true"
chart-series="series" chart-options="options" height="100%">
</canvas>
</div>
</div>
......
......@@ -21,27 +21,48 @@
'use strict';
// injected references
var $log, $scope, $location, ks, fs, cbs;
var $log, $scope, $location, ks, fs, cbs, ns;
var labels = new Array(60);
var data = new Array(new Array(60), new Array(60), new Array(60),
new Array(60), new Array(60), new Array(60));
var hasDeviceId;
var labels = new Array(1);
var data = new Array(6);
for (var i = 0; i < 6; i++) {
data[i] = new Array(1);
}
var date, max, merged;
function ceil(num) {
if (isNaN(num)) {
return 0;
}
var pre = num.toString().length - 1
var pow = Math.pow(10, pre);
return (Math.ceil(num / pow)) * pow;
}
angular.module('ovCpman', ["chart.js"])
.controller('OvCpmanCtrl',
['$log', '$scope', '$location', 'FnService', 'ChartBuilderService',
['$log', '$scope', '$location', 'FnService', 'ChartBuilderService', 'NavService',
function (_$log_, _$scope_, _$location_, _fs_, _cbs_) {
function (_$log_, _$scope_, _$location_, _fs_, _cbs_, _ns_) {
var params;
$log = _$log_;
$scope = _$scope_;
$location = _$location_;
fs = _fs_;
cbs = _cbs_;
ns = _ns_;
params = $location.search();
if (params.hasOwnProperty('devId')) {
$scope.devId = params['devId'];
hasDeviceId = true;
} else {
$scope.type = 'StackedBar';
hasDeviceId = false;
}
cbs.buildChart({
......@@ -50,31 +71,70 @@
query: params
});
var idx = 0;
var date;
$scope.$watch('chartData', function () {
idx = 0;
if (!fs.isEmptyObject($scope.chartData)) {
$scope.chartData.forEach(function (cm) {
$scope.showLoader = false;
var length = $scope.chartData.length;
labels = new Array(length);
for (var i = 0; i < 6; i++) {
data[i] = new Array(length);
}
$scope.chartData.forEach(function (cm, idx) {
data[0][idx] = cm.inbound_packet;
data[1][idx] = cm.outbound_packet;
data[2][idx] = cm.flow_mod_packet;
data[3][idx] = cm.flow_removed_packet;
data[4][idx] = cm.request_packet;
data[5][idx] = cm.reply_packet;
date = new Date(cm.label);
labels[idx] = date.getHours() + ":" + date.getMinutes();
idx++;
if(hasDeviceId) {
date = new Date(cm.label);
labels[idx] = date.getHours() + ":" + date.getMinutes();
} else {
labels[idx] = cm.label;
}
});
}
merged = [].concat.apply([], data);
max = Math.max.apply(null, merged);
$scope.labels = labels;
$scope.data = data;
$scope.options = {
scaleOverride : true,
scaleSteps : 10,
scaleStepWidth : ceil(max) / 10,
scaleStartValue : 0
};
$scope.onClick = function (points, evt) {
if (points[0]) {
// TODO: this will be replaced with real device id
var tmpId = 'of:000000000000020' + points[0].label;
ns.navTo('cpman', { devId: tmpId });
$log.log(points[0].label);
}
};
});
$scope.series = ['INBOUND', 'OUTBOUND', 'FLOW-MOD',
'FLOW-REMOVED', 'STATS-REQUEST', 'STATS-REPLY'];
$scope.labels = labels;
$scope.data = data;
$scope.chartColors = [
'#286090',
'#F7464A',
'#46BFBD',
'#FDB45C',
'#97BBCD',
'#4D5360',
'#8c4f9f'
];
Chart.defaults.global.colours = $scope.chartColors;
$scope.showLoader = true;
$log.log('OvCpmanCtrl has been created');
}]);
......