UI for server device driver

This patch introduces a graphical user interface
that visualizes the CPU utilization of server devices
using bar charts.

Code optimizations and a bug fix is applied after first
code review.

Additional bar plots are implemented, visualizing
average throughput and latency per core,
when these statistics are present.

Added external library in Bazel's BUILD file.

Fixed scaling and font issues in the UIs after getting
feedback from ONOS reviewers.

Change-Id: I92972ef871e6a91dd70cdffd8cd650f498ffca26
Signed-off-by: Georgios Katsikas <katsikas.gp@gmail.com>
diff --git a/drivers/server/BUCK b/drivers/server/BUCK
index 9be0e49..cfb6acd 100644
--- a/drivers/server/BUCK
+++ b/drivers/server/BUCK
@@ -2,6 +2,7 @@
     '//lib:CORE_DEPS',
     '//lib:JACKSON',
     '//lib:javax.ws.rs-api',
+    '//lib:joda-time',
     '//incubator/api:onos-incubator-api',
     '//utils/rest:onlab-rest',
     '//protocols/rest/api:onos-protocols-rest-api',
diff --git a/drivers/server/BUILD b/drivers/server/BUILD
index c2ab312..1dd2700 100644
--- a/drivers/server/BUILD
+++ b/drivers/server/BUILD
@@ -1,5 +1,6 @@
 COMPILE_DEPS = CORE_DEPS + JACKSON + [
     "@javax_ws_rs_api//jar",
+    "@joda_time//jar",
     "//incubator/api:onos-incubator-api",
     "//utils/rest:onlab-rest",
     "//protocols/rest/api:onos-protocols-rest-api",
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/ServerDevicesDiscovery.java b/drivers/server/src/main/java/org/onosproject/drivers/server/ServerDevicesDiscovery.java
index e82411f..ca4e809 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/ServerDevicesDiscovery.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/ServerDevicesDiscovery.java
@@ -150,7 +150,6 @@
     private static final String MON_PARAM_FREE_CPUS  = "freeCpus";
     private static final String MON_PARAM_MIN        = "min";
     private static final String MON_PARAM_AVERAGE    = "average";
-    private static final String MON_PARAM_MEDIAN     = "median";
     private static final String MON_PARAM_MAX        = "max";
 
     /**
@@ -764,9 +763,9 @@
                 if (latencyNode.get(MON_PARAM_MIN) != null) {
                     minLatency = latencyNode.path(MON_PARAM_MIN).floatValue();
                 }
-                float medianLatency = (float) 0;
-                if (latencyNode.get(MON_PARAM_MEDIAN) != null) {
-                    medianLatency = latencyNode.path(MON_PARAM_MEDIAN).floatValue();
+                float averageLatency = (float) 0;
+                if (latencyNode.get(MON_PARAM_AVERAGE) != null) {
+                    averageLatency = latencyNode.path(MON_PARAM_AVERAGE).floatValue();
                 }
                 float maxLatency = (float) 0;
                 if (latencyNode.get(MON_PARAM_MAX) != null) {
@@ -774,7 +773,7 @@
                 }
 
                 cpuBuilder.setMinLatency(minLatency)
-                    .setMedianLatency(medianLatency)
+                    .setAverageLatency(averageLatency)
                     .setMaxLatency(maxLatency);
             }
 
@@ -924,13 +923,13 @@
             return timingBuilder.build();
         }
 
-        ObjectNode autoscaleTimingObjNode = (ObjectNode) autoscaleTimingNode;
+        ObjectNode autoScaleTimingObjNode = (ObjectNode) autoscaleTimingNode;
         // Time (ns) to autoscale a server's load
-        long autoscaleTime = 0;
-        if (autoscaleTimingObjNode.get(TIMING_PARAM_AUTOSCALE) != null) {
-            autoscaleTime = autoscaleTimingObjNode.path(TIMING_PARAM_AUTOSCALE).asLong();
+        long autoScaleTime = 0;
+        if (autoScaleTimingObjNode.get(TIMING_PARAM_AUTOSCALE) != null) {
+            autoScaleTime = autoScaleTimingObjNode.path(TIMING_PARAM_AUTOSCALE).asLong();
         }
-        timingBuilder.setAutoscaleTime(autoscaleTime);
+        timingBuilder.setAutoScaleTime(autoScaleTime);
 
         return timingBuilder.build();
     }
@@ -947,7 +946,7 @@
 
         zeroTimingBuilder.setParsingTime(0)
                          .setLaunchingTime(0)
-                         .setAutoscaleTime(0);
+                         .setAutoScaleTime(0);
 
         return zeroTimingBuilder.build();
     }
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/gui/BaseViewMessageHandler.java b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/BaseViewMessageHandler.java
new file mode 100644
index 0000000..a2a151c
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/BaseViewMessageHandler.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.server.gui;
+
+import org.onosproject.drivers.server.BasicServerDriver;
+import org.onosproject.drivers.server.ServerDevicesDiscovery;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.ui.UiMessageHandler;
+import org.onosproject.ui.chart.ChartModel;
+import org.onosproject.ui.chart.ChartRequestHandler;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.joda.time.LocalDateTime;
+import org.slf4j.Logger;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.IntStream;
+
+import static org.slf4j.LoggerFactory.getLogger;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Base message handler for passing server data to the Web UI.
+ */
+public abstract class BaseViewMessageHandler extends UiMessageHandler {
+
+    private static final Logger log = getLogger(BaseViewMessageHandler.class);
+
+    // Time axis
+    protected long timestamp = 0L;
+
+    // Instance of the basic server driver
+    protected static BasicServerDriver basicDriver = new BasicServerDriver();
+
+    // Instance of the server driver
+    protected static ServerDevicesDiscovery serverDriver = new ServerDevicesDiscovery();
+
+    // A local memory to store monitoring data
+    protected static Map<DeviceId, Map<Integer, LruCache<Float>>> devDataMap =
+        new HashMap<DeviceId, Map<Integer, LruCache<Float>>>();
+
+    // Data series length
+    public static final int NUM_OF_DATA_POINTS = 30;
+
+    // The maximum number of columns that can be projected
+    public static final int MAX_COLUMNS_NB = 16;
+
+    // Minimum CPU load
+    public static final float MIN_CPU_LOAD = (float) 0.01;
+
+    // Time axis
+    public static final String TIME_FORMAT = "HH:mm:ss";
+
+    // Device IDs
+    public static final String DEVICE_IDS = "deviceIds";
+
+    // Chart designer
+    protected abstract class ControlMessageRequest extends ChartRequestHandler {
+
+        protected ControlMessageRequest(String req, String res, String label) {
+            super(req, res, label);
+        }
+
+        @Override
+        protected abstract String[] getSeries();
+
+        @Override
+        protected abstract void populateChart(ChartModel cm, ObjectNode payload);
+
+        /**
+         * Returns a x-axis label for a monitoring value.
+         *
+         * @param metric label metric
+         * @param index label index
+         * @return a data label
+         */
+        protected String getLabel(MetricType metric, int index) {
+            return StringUtils.lowerCase(metric.name()) + "_" + Integer.toString(index);
+        }
+
+        /**
+         * Fills an array of strings acting as x-axis.
+         *
+         * @param metric x-axis metric
+         * @param length the length of the array
+         * @return an array of strings
+         */
+        protected String[] createSeries(MetricType metric, int length) {
+            if (length <= 0) {
+                return null;
+            }
+
+            if (length > MAX_COLUMNS_NB) {
+                length = MAX_COLUMNS_NB;
+            }
+
+            String[] series = IntStream.range(0, length)
+                .mapToObj(i -> getLabel(metric, i))
+                .toArray(String[]::new);
+
+            return series;
+        }
+
+        /**
+         * Returns a map of monitoring parameters to their load history buffers.
+         *
+         * @param deviceId the device being monitored
+         * @param length the length of the array
+         * @return a map monitoring parameters to their load history buffers
+         */
+        protected Map<Integer, LruCache<Float>> fetchCacheForDevice(DeviceId deviceId, int length) {
+            if (!isValid(deviceId, length - 1)) {
+                log.error("Invalid access to data history by device {} with {} cores", deviceId, length);
+                return null;
+            }
+
+            if (devDataMap.containsKey(deviceId)) {
+                return devDataMap.get(deviceId);
+            }
+
+            Map<Integer, LruCache<Float>> dataMap = new HashMap<Integer, LruCache<Float>>();
+            for (int i = 0; i < length; i++) {
+                dataMap.put(i, new LruCache<Float>(NUM_OF_DATA_POINTS));
+            }
+
+            devDataMap.put(deviceId, dataMap);
+
+            return dataMap;
+        }
+
+        /**
+         * Adds a value into a buffer with the latest data entries.
+         *
+         * @param deviceId the device being monitored
+         * @param length the length of the array
+         * @param index the data index
+         * @param value the data value
+         */
+        protected void addToCache(
+                DeviceId deviceId, int length, int index, float value) {
+            if (!isValid(deviceId, length - 1) ||
+                !isValid(deviceId, index)) {
+                log.error("Invalid access to data {} history by device {} with {} cores",
+                    index, deviceId, length);
+                return;
+            }
+
+            Map<Integer, LruCache<Float>> dataMap = devDataMap.get(deviceId);
+            if (dataMap == null) {
+                dataMap = fetchCacheForDevice(deviceId, length);
+                checkNotNull(dataMap, "Failed to add measurement in the cache");
+            }
+
+            dataMap.get(index).add(value);
+        }
+
+        /**
+         * Returns a buffer with the latest
+         * entries of a device's monitoring parameter.
+         *
+         * @param deviceId the device being monitored
+         * @param index a data index
+         * @return a history of values
+         */
+        protected LruCache<Float> getDataHistory(DeviceId deviceId, int index) {
+            if (!isValid(deviceId, index)) {
+                log.error("Invalid access to CPU {} load history by device {}", index, deviceId);
+                return null;
+            }
+
+            Map<Integer, LruCache<Float>> dataMap = devDataMap.get(deviceId);
+            if (dataMap == null) {
+                return null;
+            }
+
+            return dataMap.get(index);
+        }
+
+        /**
+         * Fill the UI memory's current values with zeros.
+         *
+         * @param deviceId the device ID being monitored
+         * @param length the length of the array
+         * @return a map of monitoring parameters to their initial values
+         */
+        protected Map<Integer, Float> populateZeroData(DeviceId deviceId, int length) {
+            Map<Integer, Float> data = initializeData(length);
+
+            for (int i = 0; i < length; i++) {
+                // Store it locally
+                addToCache(deviceId, length, i, 0);
+            }
+
+            return data;
+        }
+
+        /**
+         * Fill the UI memory's history with zeros.
+         *
+         * @param deviceId the device ID being monitored
+         * @param length the length of the array
+         * @return a map of monitoring parameters to their initial arrays of values
+         */
+        protected Map<Integer, Float[]> populateZeroDataHistory(DeviceId deviceId, int length) {
+            Map<Integer, Float[]> data = initializeDataHistory(length);
+
+            for (int i = 0; i < length; i++) {
+                addToCache(deviceId, length, i, 0);
+            }
+
+            // Keep a timestamp
+            timestamp = System.currentTimeMillis();
+
+            return data;
+        }
+
+        /**
+         * Populate a specific metric with data.
+         *
+         * @param dataPoint the particular part of the chart to be fed
+         * @param data the data to feed the metric of the chart
+         */
+        protected void populateMetric(ChartModel.DataPoint dataPoint, Map<String, Object> data) {
+            data.forEach(dataPoint::data);
+        }
+
+        /**
+         * Populate the metrics to the Web UI.
+         *
+         * @param cm the chart to be fed with data
+         * @param data the data to feed the chart
+         * @param time a timestamp
+         * @param metric a metric
+         * @param numberOfPoints the number of data points
+         */
+        protected void populateMetrics(
+                ChartModel            cm,
+                Map<Integer, Float[]> data,
+                LocalDateTime         time,
+                MetricType            metric,
+                int                   numberOfPoints) {
+            for (int i = 0; i < numberOfPoints; i++) {
+                Map<String, Object> local = Maps.newHashMap();
+                for (int j = 0; j < data.size(); j++) {
+                    if (data.containsKey(j)) {
+                        local.put(getLabel(metric, j), data.get(j)[i]);
+                    }
+                }
+
+                String calculated = time.minusSeconds(numberOfPoints - i).toString(TIME_FORMAT);
+                local.put(LABEL, calculated);
+
+                populateMetric(cm.addDataPoint(calculated), local);
+            }
+        }
+
+        /**
+         * Checks the validity of a device's information.
+         *
+         * @param deviceId the device being monitored
+         * @param length the length of the array
+         * @return boolean data validity status
+         */
+        protected boolean isValid(DeviceId deviceId, int length) {
+            return ((deviceId != null) && (length >= 0) &&
+                    (length < MAX_COLUMNS_NB));
+        }
+
+        /**
+         * Create a data structure with zero-initialized data.
+         *
+         * @param length the length of the array
+         * @return a map of metrics to their initial values
+         */
+        protected Map<Integer, Float> initializeData(int length) {
+            Map<Integer, Float> data = Maps.newHashMap();
+
+            for (int i = 0; i < length; i++) {
+                data.put(i, (float) 0);
+            }
+
+            return data;
+        }
+
+        /**
+         * Create a data structure with zero-initialized arrays of data.
+         *
+         * @param length the length of the array
+         * @return a map of metrics to their initial arrays of values
+         */
+        protected Map<Integer, Float[]> initializeDataHistory(int length) {
+            Map<Integer, Float[]> data = Maps.newHashMap();
+
+            for (int i = 0; i < length; i++) {
+                data.put(i, ArrayUtils.toObject(new float[NUM_OF_DATA_POINTS]));
+            }
+
+            return data;
+        }
+
+        /**
+         * Fill the contents of an input array until a desired point.
+         *
+         * @param origin the original array with the data
+         * @param expectedLength the desired length of the array
+         * @return an array of a certain length
+         */
+        protected float[] fillData(float[] origin, int expectedLength) {
+            if (origin.length == expectedLength) {
+                return origin;
+            } else {
+                int desiredLength = origin.length;
+                if (origin.length > expectedLength) {
+                    desiredLength = expectedLength;
+                }
+
+                float[] filled = new float[expectedLength];
+                for (int i = 0; i < desiredLength; i++) {
+                    filled[i] = origin[i];
+                }
+
+                for (int i = desiredLength - 1; i < expectedLength; i++) {
+                    filled[i] = origin[origin.length - 1];
+                }
+
+                return filled;
+            }
+        }
+
+        /**
+         * Attach the list of all devices to the top of the chart.
+         *
+         * @param cm the chart to be fed with data
+         * @param deviceIds the set of Device IDs to show up
+         */
+        protected void attachDeviceList(ChartModel cm, Set<DeviceId> deviceIds) {
+            checkNotNull(deviceIds, "No device IDs provided to chart");
+            ArrayNode array = arrayNode();
+            deviceIds.forEach(id -> array.add(id.toString()));
+            cm.addAnnotation(DEVICE_IDS, array);
+        }
+
+        /**
+         * Returns zero-initialized data for a metric when no devices are present.
+         *
+         * @param cm the chart to be fed with data
+         * @param metric a metric to reset
+         * @param length the length of the data array
+         */
+        protected void fillDataWhenNoDevicePresent(
+                ChartModel cm, MetricType metric, int length) {
+            Map<String, Object> local = Maps.newHashMap();
+            for (int i = 0; i < length; i++) {
+                local.put(getLabel(metric, i), new Float(0));
+            }
+
+            local.put(LABEL, "No Servers");
+            populateMetric(cm.addDataPoint(""), local);
+        }
+
+    }
+
+}
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/gui/CpuUI.java b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/CpuUI.java
new file mode 100644
index 0000000..31fc57a
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/CpuUI.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.server.gui;
+
+import org.onosproject.ui.UiExtension;
+import org.onosproject.ui.UiExtensionService;
+import org.onosproject.ui.UiMessageHandlerFactory;
+import org.onosproject.ui.UiView;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+import static org.onosproject.ui.UiView.Category.NETWORK;
+import static org.onosproject.ui.GlyphConstants.ENDSTATION;
+
+/**
+ * Mechanism to stream CPU data to the GUI.
+ */
+@Component(immediate = true, enabled = true)
+@Service(value = CpuUI.class)
+public class CpuUI {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    /**
+     * GUI Information.
+     */
+    private static final String CPU_ID = "cpu";
+    private static final String CPU_TEXT = "Servers-CPU";
+    private static final String RES_PATH = "gui";
+    private static final ClassLoader CL = CpuUI.class.getClassLoader();
+
+    // Factory for UI message handlers
+    private final UiMessageHandlerFactory messageHandlerFactory =
+            () -> ImmutableList.of(new CpuViewMessageHandler());
+
+    // List of application views
+    private final List<UiView> views = ImmutableList.of(
+            new UiView(NETWORK, CPU_ID, CPU_TEXT, ENDSTATION)
+    );
+
+    // Application UI extension
+    private final UiExtension uiExtension =
+            new UiExtension.Builder(CL, views)
+                    .messageHandlerFactory(messageHandlerFactory)
+                    .resourcePath(RES_PATH)
+                    .build();
+
+    /**
+     * Interact with ONOS.
+     */
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected UiExtensionService uiExtensionService;
+
+    @Activate
+    protected void activate() {
+        uiExtensionService.register(uiExtension);
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        uiExtensionService.unregister(uiExtension);
+        log.info("Stopped");
+    }
+
+}
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/gui/CpuViewMessageHandler.java b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/CpuViewMessageHandler.java
new file mode 100644
index 0000000..87be684
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/CpuViewMessageHandler.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.server.gui;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.primitives.Floats;
+
+import org.onosproject.drivers.server.behavior.CpuStatisticsDiscovery;
+import org.onosproject.drivers.server.devices.RestServerSBDevice;
+import org.onosproject.drivers.server.stats.CpuStatistics;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.ui.RequestHandler;
+import org.onosproject.ui.chart.ChartModel;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.joda.time.LocalDateTime;
+import org.slf4j.Logger;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+import static org.onosproject.drivers.server.gui.MetricType.CPU;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Message handler for passing CPU load data to the Web UI.
+ */
+public class CpuViewMessageHandler extends BaseViewMessageHandler {
+
+    private static final Logger log = getLogger(CpuViewMessageHandler.class);
+
+    private static final String CPU_DATA_REQ = "cpuDataRequest";
+    private static final String CPU_DATA_RESP = "cpuDataResponse";
+    private static final String CPUS_LABEL = "cpus";
+
+    @Override
+    protected Collection<RequestHandler> createRequestHandlers() {
+        return ImmutableSet.of(new CpuMessageRequest());
+    }
+
+    private final class CpuMessageRequest extends BaseViewMessageHandler.ControlMessageRequest {
+
+        private CpuMessageRequest() {
+            super(CPU_DATA_REQ, CPU_DATA_RESP, CPUS_LABEL);
+        }
+
+        @Override
+        protected String[] getSeries() {
+            return createSeries(CPU, MAX_COLUMNS_NB);
+        }
+
+        @Override
+        protected void populateChart(ChartModel cm, ObjectNode payload) {
+            DeviceService ds = get(DeviceService.class);
+            if ((ds == null) || (ds.getAvailableDeviceCount() == 0)) {
+                fillDataWhenNoDevicePresent(cm, CPU, MAX_COLUMNS_NB);
+                return;
+            }
+
+            String uri = string(payload, "devId");
+
+            // Project only one device over time
+            if (!Strings.isNullOrEmpty(uri)) {
+                DeviceId deviceId = DeviceId.deviceId(uri);
+                RestServerSBDevice serverDev =
+                    (RestServerSBDevice) basicDriver.getController().getDevice(deviceId);
+
+                List<CpuStatistics> cpuStats = null;
+                Map<Integer, Float[]> data = null;
+                try {
+                    cpuStats = new ArrayList(serverDriver.getCpuStatistics(deviceId));
+                    data = populateCpuDataHistory(deviceId, serverDev.numberOfCpus(), cpuStats);
+                } catch (Exception ex) {
+                    data = populateZeroDataHistory(deviceId, MAX_COLUMNS_NB);
+                }
+                checkNotNull(data, "No CPU data history to visualize");
+
+                // Generate a timestamp
+                LocalDateTime ldt = new LocalDateTime(timestamp);
+
+                // Project the data
+                populateMetrics(cm, data, ldt, CPU, NUM_OF_DATA_POINTS);
+
+                Set<DeviceId> deviceIds = Sets.newHashSet();
+                for (Device device : ds.getAvailableDevices()) {
+                    // Only devices that support CPU monitoring are considered
+                    if (device.is(CpuStatisticsDiscovery.class) && serverDev.isActive()) {
+                        deviceIds.add(device.id());
+                    }
+                }
+
+                // Drop down list to select devices
+                attachDeviceList(cm, deviceIds);
+            } else {
+                for (Device device : ds.getAvailableDevices()) {
+                    // Only devices that support CPU monitoring are considered
+                    if (!device.is(CpuStatisticsDiscovery.class)) {
+                        continue;
+                    }
+
+                    DeviceId deviceId = device.id();
+                    RestServerSBDevice serverDev =
+                        (RestServerSBDevice) basicDriver.getController().getDevice(deviceId);
+
+                    List<CpuStatistics> cpuStats = null;
+                    Map<Integer, Float> data = null;
+                    try {
+                        cpuStats = new ArrayList(serverDriver.getCpuStatistics(deviceId));
+                        data = populateCpuData(deviceId, serverDev.numberOfCpus(), cpuStats);
+                    } catch (Exception ex) {
+                        data = populateZeroData(deviceId, MAX_COLUMNS_NB);
+                    }
+                    checkNotNull(data, "No CPU data to visualize");
+
+                    // Map them to the CPU cores
+                    Map<String, Object> local = Maps.newHashMap();
+                    for (int i = 0; i < data.size(); i++) {
+                        local.put(getLabel(CPU, i), data.get(i));
+                    }
+
+                    // Last piece of data is the device ID
+                    if (serverDev.isActive()) {
+                        local.put(LABEL, deviceId);
+                        populateMetric(cm.addDataPoint(deviceId), local);
+                    } else {
+                        local.put(LABEL, "");
+                        populateMetric(cm.addDataPoint(""), local);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Turn the current monitoring data into a data
+         * structure that can feed the CPU UI memory.
+         *
+         * @param deviceId the device ID being monitored
+         * @param length the length of the array
+         * @param cpuStats the CPU load per core
+         * @return a map of CPU metrics to their values
+         */
+        private Map<Integer, Float> populateCpuData(
+                DeviceId deviceId, int length, List<CpuStatistics> cpuStats) {
+            Map<Integer, Float> data = initializeData(MAX_COLUMNS_NB);
+
+            for (CpuStatistics stats : cpuStats) {
+                int index = stats.id();
+
+                // Store it locally
+                addToCache(deviceId, length, index, stats.load());
+
+                // Project the floating point load value in [0, 1] to [0, 100]
+                Float projectedVal = new Float(stats.load() * (float) 100);
+
+                // Now the data is in the right form
+                data.put(index, projectedVal);
+            }
+
+            return data;
+        }
+
+        /**
+         * Turn the monitoring data history into a
+         * data structure that can feed the CPU UI memory.
+         *
+         * @param deviceId the device ID being monitored
+         * @param length the length of the array
+         * @param cpuStats the CPU load per core
+         * @return a map of CPU metrics to their arrays of values
+         */
+        private Map<Integer, Float[]> populateCpuDataHistory(
+                DeviceId deviceId, int length, List<CpuStatistics> cpuStats) {
+            Map<Integer, Float[]> data = initializeDataHistory(MAX_COLUMNS_NB);
+
+            for (CpuStatistics stats : cpuStats) {
+                int index = stats.id();
+
+                // Store it locally
+                addToCache(deviceId, length, index, stats.load());
+
+                LruCache<Float> loadCache = getDataHistory(deviceId, index);
+                if (loadCache == null) {
+                    continue;
+                }
+                float[] floatArray = Floats.toArray(Arrays.asList(loadCache.values().toArray(new Float[0])));
+
+                // Project the load array to the range of [0, 100]
+                for (int j = 0; j < floatArray.length; j++) {
+                    floatArray[j] = floatArray[j] * (float) 100;
+                }
+
+                // Fill the missing points
+                float[] filledLoadArray = fillData(floatArray, NUM_OF_DATA_POINTS);
+
+                // Set the data
+                data.put(index, ArrayUtils.toObject(filledLoadArray));
+            }
+
+            // Keep a timestamp
+            timestamp = System.currentTimeMillis();
+
+            return data;
+        }
+
+    }
+
+}
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/gui/LatencyUI.java b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/LatencyUI.java
new file mode 100644
index 0000000..9b8c864
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/LatencyUI.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.server.gui;
+
+import org.onosproject.ui.UiExtension;
+import org.onosproject.ui.UiExtensionService;
+import org.onosproject.ui.UiMessageHandlerFactory;
+import org.onosproject.ui.UiView;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+import static org.onosproject.ui.UiView.Category.NETWORK;
+import static org.onosproject.ui.GlyphConstants.ENDSTATION;
+
+/**
+ * Mechanism to stream latency data to the GUI.
+ */
+@Component(immediate = true, enabled = true)
+@Service(value = LatencyUI.class)
+public class LatencyUI {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    /**
+     * GUI Information.
+     */
+    private static final String LATENCY_ID = "latency";
+    private static final String LATENCY_TEXT = "Servers-Latency";
+    private static final String RES_PATH = "gui";
+    private static final ClassLoader CL = LatencyUI.class.getClassLoader();
+
+    // Factory for UI message handlers
+    private final UiMessageHandlerFactory messageHandlerFactory =
+            () -> ImmutableList.of(new LatencyViewMessageHandler());
+
+    // List of application views
+    private final List<UiView> views = ImmutableList.of(
+            new UiView(NETWORK, LATENCY_ID, LATENCY_TEXT, ENDSTATION)
+    );
+
+    // Application UI extension
+    private final UiExtension uiExtension =
+            new UiExtension.Builder(CL, views)
+                    .messageHandlerFactory(messageHandlerFactory)
+                    .resourcePath(RES_PATH)
+                    .build();
+
+    /**
+     * Interact with ONOS.
+     */
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected UiExtensionService uiExtensionService;
+
+    @Activate
+    protected void activate() {
+        uiExtensionService.register(uiExtension);
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        uiExtensionService.unregister(uiExtension);
+        log.info("Stopped");
+    }
+
+}
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/gui/LatencyViewMessageHandler.java b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/LatencyViewMessageHandler.java
new file mode 100644
index 0000000..c76c97d
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/LatencyViewMessageHandler.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.server.gui;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.primitives.Floats;
+
+import org.onosproject.drivers.server.behavior.MonitoringStatisticsDiscovery;
+import org.onosproject.drivers.server.devices.RestServerSBDevice;
+import org.onosproject.drivers.server.stats.CpuStatistics;
+import org.onosproject.drivers.server.stats.MonitoringStatistics;
+import org.onosproject.drivers.server.stats.MonitoringUnit.LatencyUnit;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.ui.RequestHandler;
+import org.onosproject.ui.chart.ChartModel;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.joda.time.LocalDateTime;
+import org.slf4j.Logger;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+import static org.onosproject.drivers.server.gui.MetricType.LATENCY;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Message handler for passing latency data to the Web UI.
+ */
+public class LatencyViewMessageHandler extends BaseViewMessageHandler {
+
+    private static final Logger log = getLogger(LatencyViewMessageHandler.class);
+
+    private static final String LATENCY_DATA_REQ = "latencyDataRequest";
+    private static final String LATENCY_DATA_RESP = "latencyDataResponse";
+    private static final String LATENCY_LABEL = "latencys";
+
+    @Override
+    protected Collection<RequestHandler> createRequestHandlers() {
+        return ImmutableSet.of(new LatencyMessageRequest());
+    }
+
+    private final class LatencyMessageRequest extends BaseViewMessageHandler.ControlMessageRequest {
+
+        private LatencyMessageRequest() {
+            super(LATENCY_DATA_REQ, LATENCY_DATA_RESP, LATENCY_LABEL);
+        }
+
+        @Override
+        protected String[] getSeries() {
+            return createSeries(LATENCY, MAX_COLUMNS_NB);
+        }
+
+        @Override
+        protected void populateChart(ChartModel cm, ObjectNode payload) {
+            DeviceService ds = get(DeviceService.class);
+            if ((ds == null) || (ds.getAvailableDeviceCount() == 0)) {
+                fillDataWhenNoDevicePresent(cm, LATENCY, MAX_COLUMNS_NB);
+                return;
+            }
+
+            String uri = string(payload, "devId");
+
+            // Project only one device over time
+            if (!Strings.isNullOrEmpty(uri)) {
+                DeviceId deviceId = DeviceId.deviceId(uri);
+                RestServerSBDevice serverDev =
+                    (RestServerSBDevice) basicDriver.getController().getDevice(deviceId);
+
+                Map<Integer, Float[]> data = null;
+                MonitoringStatistics monStats = serverDriver.getGlobalMonitoringStatistics(deviceId);
+                if (monStats == null) {
+                    data = populateZeroDataHistory(deviceId, MAX_COLUMNS_NB);
+                } else {
+                    data = populateLatencyDataHistory(deviceId, serverDev.numberOfCpus(), monStats);
+                }
+                checkNotNull(data, "No latency data history to visualize");
+
+                // Generate a timestamp
+                LocalDateTime ldt = new LocalDateTime(timestamp);
+
+                // Project the data
+                populateMetrics(cm, data, ldt, LATENCY, NUM_OF_DATA_POINTS);
+
+                Set<DeviceId> deviceIds = Sets.newHashSet();
+                for (Device device : ds.getAvailableDevices()) {
+                    // Only devices that support this type of monitoring behaviour are considered
+                    if (device.is(MonitoringStatisticsDiscovery.class) && serverDev.isActive()) {
+                        deviceIds.add(device.id());
+                    }
+                }
+
+                // Drop down list to select devices
+                attachDeviceList(cm, deviceIds);
+            } else {
+                for (Device device : ds.getAvailableDevices()) {
+                    // Only devices that support this type of monitoring behaviour are considered
+                    if (!device.is(MonitoringStatisticsDiscovery.class)) {
+                        continue;
+                    }
+
+                    DeviceId deviceId = device.id();
+                    RestServerSBDevice serverDev =
+                        (RestServerSBDevice) basicDriver.getController().getDevice(deviceId);
+
+                    Map<Integer, Float> data = null;
+                    MonitoringStatistics monStats = serverDriver.getGlobalMonitoringStatistics(deviceId);
+                    if (monStats == null) {
+                        data = populateZeroData(deviceId, MAX_COLUMNS_NB);
+                    } else {
+                        data = populateLatencyData(deviceId, serverDev.numberOfCpus(), monStats);
+                    }
+                    checkNotNull(data, "No latency data to visualize");
+
+                    // Map them to the CPU cores
+                    Map<String, Object> local = Maps.newHashMap();
+                    for (int i = 0; i < data.size(); i++) {
+                        local.put(getLabel(LATENCY, i), data.get(i));
+                    }
+
+                    // Last piece of data is the device ID
+                    if (serverDev.isActive()) {
+                        local.put(LABEL, deviceId);
+                        populateMetric(cm.addDataPoint(deviceId), local);
+                    } else {
+                        local.put(LABEL, "");
+                        populateMetric(cm.addDataPoint(""), local);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Turn the current monitoring data into a data
+         * structure that can feed the Latency UI memory.
+         *
+         * @param deviceId the device ID being monitored
+         * @param length the length of the array
+         * @param monStats a MonitoringStatistics object
+         * @return a map of latency metrics to their values
+         */
+        private Map<Integer, Float> populateLatencyData(
+                DeviceId deviceId, int length, MonitoringStatistics monStats) {
+            Map<Integer, Float> data = initializeData(MAX_COLUMNS_NB);
+
+            for (CpuStatistics stats : monStats.cpuStatisticsAll()) {
+                int index = stats.id();
+
+                // TODO: Use min and max latency to plot bars plots with error bars
+                Float value = null;
+                if ((stats.averageLatency().isPresent()) && (stats.load() > MIN_CPU_LOAD)) {
+                    value = stats.averageLatency().get();
+                } else {
+                    value = new Float(0);
+                }
+
+                // Unit conversion
+                LatencyUnit latencyUnit = null;
+                if (stats.latencyUnit().isPresent()) {
+                    latencyUnit = (LatencyUnit) stats.latencyUnit().get();
+                } else {
+                    latencyUnit = LatencyUnit.NANO_SECOND;
+                }
+                value = LatencyUnit.toNano(value, latencyUnit);
+
+                // Store it locally
+                addToCache(deviceId, length, index, value);
+
+                // And into the map
+                data.put(index, value);
+            }
+
+            return data;
+        }
+
+        /**
+         * Turn the monitoring data history into a
+         * data structure that can feed the Latency UI memory.
+         *
+         * @param deviceId the device ID being monitored
+         * @param length the length of the array
+         * @param monStats a MonitoringStatistics object
+         * @return a map of latency metrics to their arrays of values
+         */
+        private Map<Integer, Float[]> populateLatencyDataHistory(
+                DeviceId deviceId, int length, MonitoringStatistics monStats) {
+            Map<Integer, Float[]> data = initializeDataHistory(MAX_COLUMNS_NB);
+
+            for (CpuStatistics stats : monStats.cpuStatisticsAll()) {
+                int index = stats.id();
+
+                // TODO: Use min and max latency to plot bars plots with error bars
+                Float value = null;
+                if ((stats.averageLatency().isPresent()) && (stats.load() > MIN_CPU_LOAD)) {
+                    value = stats.averageLatency().get();
+                } else {
+                    value = new Float(0);
+                }
+
+                // Unit conversion
+                LatencyUnit latencyUnit = null;
+                if (stats.latencyUnit().isPresent()) {
+                    latencyUnit = (LatencyUnit) stats.latencyUnit().get();
+                } else {
+                    latencyUnit = LatencyUnit.NANO_SECOND;
+                }
+                value = LatencyUnit.toNano(value, latencyUnit);
+
+                // Store it locally
+                addToCache(deviceId, length, index, value);
+
+                LruCache<Float> loadCache = getDataHistory(deviceId, index);
+                if (loadCache == null) {
+                    continue;
+                }
+                float[] floatArray = Floats.toArray(Arrays.asList(loadCache.values().toArray(new Float[0])));
+
+                // Fill the missing points
+                float[] filledLoadArray = fillData(floatArray, NUM_OF_DATA_POINTS);
+
+                // Set the data
+                data.put(index, ArrayUtils.toObject(filledLoadArray));
+            }
+
+            // Keep a timestamp
+            timestamp = System.currentTimeMillis();
+
+            return data;
+        }
+
+    }
+
+}
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/gui/LruCache.java b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/LruCache.java
new file mode 100644
index 0000000..c38af84
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/LruCache.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.server.gui;
+
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.NavigableSet;
+import java.util.SortedSet;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Data structure that implements Least Recently Used (LRU) policy.
+ */
+public class LruCache<T> extends LinkedHashMap<Integer, T> {
+    private static final Logger log = getLogger(LruCache.class);
+
+    // After this size, LRU is applied
+    private final int maxEntries;
+    private static final int DEFAULT_INITIAL_CAPACITY = 5;
+    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
+
+    public LruCache(int initialCapacity,
+                    float loadFactor,
+                    int maxEntries) {
+        super(initialCapacity, loadFactor, true);
+        this.maxEntries = maxEntries;
+    }
+
+    public LruCache(int initialCapacity, int maxEntries) {
+        this(initialCapacity, DEFAULT_LOAD_FACTOR, maxEntries);
+    }
+
+    public LruCache(int maxEntries) {
+        this(DEFAULT_INITIAL_CAPACITY, maxEntries);
+    }
+
+    @Override
+    protected synchronized boolean removeEldestEntry(
+            Map.Entry<Integer, T> eldest) {
+        // Remove the oldest element when size limit is reached
+        return size() > maxEntries;
+    }
+
+    /**
+     * Adds a new entry to the LRU.
+     *
+     * @param newValue the value to be added
+     */
+    public synchronized void add(T newValue) {
+        this.put(this.getNextKey(), newValue);
+    }
+
+    /**
+     * Returns the first (eldest) key of this LRU cache.
+     *
+     * @return first (eldest) key of this LRU cache
+     */
+    public synchronized Integer getFirstKey() {
+        return this.keySet().iterator().next();
+    }
+
+    /**
+     * Returns the last (newest) key of this LRU cache.
+     *
+     * @return last (newest) key of this LRU cache
+     */
+    public synchronized Integer getLastKey() {
+        Integer out = null;
+        for (Integer key : this.keySet()) {
+            out = key;
+        }
+
+        return out;
+    }
+
+    /**
+     * Returns the first (eldest) value of this LRU cache.
+     *
+     * @return first (eldest) value of this LRU cache
+     */
+    public synchronized T getFirstValue() {
+        // Get all keys sorted
+        SortedSet<Integer> keys =
+            new ConcurrentSkipListSet<Integer>(this.keySet());
+
+        // Return the value that corresponds to the first key
+        return this.get(keys.first());
+    }
+
+    /**
+     * Returns the last (newest) value of this LRU cache.
+     *
+     * @return last (newest) value of this LRU cache
+     */
+    public synchronized T getLastValue() {
+        // Get all keys sorted
+        SortedSet<Integer> keys =
+            new ConcurrentSkipListSet<Integer>(this.keySet());
+
+        // Return the value that corresponds to the last key
+        return this.get(keys.last());
+    }
+
+    /**
+     * Returns the first (oldest) values of this LRU cache.
+     * The number is denoted by the argument.
+     *
+     * @param numberOfEntries the number of entries to include in the list
+     * @return list of first (oldest) values of this LRU cache
+     */
+    public synchronized List<T> getFirstValues(int numberOfEntries) {
+        List<T> outList = new ArrayList<T>();
+
+        if (numberOfEntries <= 0) {
+            return outList;
+        }
+
+        // Get all keys sorted
+        SortedSet<Integer> keys =
+            new ConcurrentSkipListSet<Integer>(this.keySet());
+
+        int i = 0;
+
+        // Iterate the sorted keys
+        for (Integer k : keys) {
+            // Pick up the first 'numberOfEntries' entries
+            if (i >= numberOfEntries) {
+                break;
+            }
+
+            outList.add(this.get(k));
+            i++;
+        }
+
+        return outList;
+    }
+
+    /**
+     * Returns the last (newest) values of this LRU cache.
+     * The number is denoted by the argument.
+     *
+     * @param numberOfEntries the number of entries to include in the list
+     * @return list of last (newest) values of this LRU cache
+     */
+    public synchronized List<T> getLastValues(int numberOfEntries) {
+        List<T> outList = new ArrayList<T>();
+
+        if (numberOfEntries <= 0) {
+            return outList;
+        }
+
+        // Get all keys sorted
+        NavigableSet<Integer> keys =
+            new ConcurrentSkipListSet<Integer>(this.keySet());
+
+        int i = 0;
+
+        // Iterate the sorted keys backwards
+        for (Integer k : keys.descendingSet()) {
+            // Pick up the last 'numberOfEntries' entries
+            if (i >= numberOfEntries) {
+                break;
+            }
+
+            outList.add(this.get(k));
+            i++;
+        }
+
+        return outList;
+    }
+
+    /**
+     * Returns the next position to store data.
+     *
+     * @return next key to store data
+     */
+    private synchronized Integer getNextKey() {
+        // The oldest will be the next..
+        if (this.size() == maxEntries) {
+            return this.getFirstKey();
+        }
+
+        Integer lastKey = this.getLastKey();
+        // First insertion
+        if (lastKey == null) {
+            return new Integer(0);
+        }
+
+        // Regular next key insertion
+        return new Integer(lastKey.intValue() + 1);
+    }
+
+}
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/gui/MetricType.java b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/MetricType.java
new file mode 100644
index 0000000..841b83e
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/MetricType.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.server.gui;
+
+/**
+ * A set of metrics to be projected.
+ */
+public enum MetricType {
+
+    /**
+     * CPU cores of a commodity server.
+     */
+    CPU,
+
+    /**
+     * Per core latency.
+     */
+    LATENCY,
+
+    /**
+     * Per core throughput.
+     */
+    THROUGHPUT;
+
+}
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/gui/ThroughputUI.java b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/ThroughputUI.java
new file mode 100644
index 0000000..9820051
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/ThroughputUI.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.server.gui;
+
+import org.onosproject.ui.UiExtension;
+import org.onosproject.ui.UiExtensionService;
+import org.onosproject.ui.UiMessageHandlerFactory;
+import org.onosproject.ui.UiView;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+import static org.onosproject.ui.UiView.Category.NETWORK;
+import static org.onosproject.ui.GlyphConstants.ENDSTATION;
+
+/**
+ * Mechanism to stream throughput data to the GUI.
+ */
+@Component(immediate = true, enabled = true)
+@Service(value = ThroughputUI.class)
+public class ThroughputUI {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    /**
+     * GUI Information.
+     */
+    private static final String THROUGHPUT_ID = "throughput";
+    private static final String THROUGHPUT_TEXT = "Servers-Throughput";
+    private static final String RES_PATH = "gui";
+    private static final ClassLoader CL = ThroughputUI.class.getClassLoader();
+
+    // Factory for UI message handlers
+    private final UiMessageHandlerFactory messageHandlerFactory =
+            () -> ImmutableList.of(new ThroughputViewMessageHandler());
+
+    // List of application views
+    private final List<UiView> views = ImmutableList.of(
+            new UiView(NETWORK, THROUGHPUT_ID, THROUGHPUT_TEXT, ENDSTATION)
+    );
+
+    // Application UI extension
+    private final UiExtension uiExtension =
+            new UiExtension.Builder(CL, views)
+                    .messageHandlerFactory(messageHandlerFactory)
+                    .resourcePath(RES_PATH)
+                    .build();
+
+    /**
+     * Interact with ONOS.
+     */
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected UiExtensionService uiExtensionService;
+
+    @Activate
+    protected void activate() {
+        uiExtensionService.register(uiExtension);
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        uiExtensionService.unregister(uiExtension);
+        log.info("Stopped");
+    }
+
+}
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/gui/ThroughputViewMessageHandler.java b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/ThroughputViewMessageHandler.java
new file mode 100644
index 0000000..91105e9
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/ThroughputViewMessageHandler.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.server.gui;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.primitives.Floats;
+
+import org.onosproject.drivers.server.behavior.MonitoringStatisticsDiscovery;
+import org.onosproject.drivers.server.devices.RestServerSBDevice;
+import org.onosproject.drivers.server.stats.CpuStatistics;
+import org.onosproject.drivers.server.stats.MonitoringStatistics;
+import org.onosproject.drivers.server.stats.MonitoringUnit.ThroughputUnit;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.ui.RequestHandler;
+import org.onosproject.ui.chart.ChartModel;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.joda.time.LocalDateTime;
+import org.slf4j.Logger;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+import static org.onosproject.drivers.server.gui.MetricType.THROUGHPUT;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Message handler for passing throughput data to the Web UI.
+ */
+public class ThroughputViewMessageHandler extends BaseViewMessageHandler {
+
+    private static final Logger log = getLogger(ThroughputViewMessageHandler.class);
+
+    private static final String THROUGHPUT_DATA_REQ = "throughputDataRequest";
+    private static final String THROUGHPUT_DATA_RESP = "throughputDataResponse";
+    private static final String THROUGHPUT_LABEL = "throughputs";
+
+    @Override
+    protected Collection<RequestHandler> createRequestHandlers() {
+        return ImmutableSet.of(new ThroughputMessageRequest());
+    }
+
+    private final class ThroughputMessageRequest extends BaseViewMessageHandler.ControlMessageRequest {
+
+        private ThroughputMessageRequest() {
+            super(THROUGHPUT_DATA_REQ, THROUGHPUT_DATA_RESP, THROUGHPUT_LABEL);
+        }
+
+        @Override
+        protected String[] getSeries() {
+            return createSeries(THROUGHPUT, MAX_COLUMNS_NB);
+        }
+
+        @Override
+        protected void populateChart(ChartModel cm, ObjectNode payload) {
+            DeviceService ds = get(DeviceService.class);
+            if ((ds == null) || (ds.getAvailableDeviceCount() == 0)) {
+                fillDataWhenNoDevicePresent(cm, THROUGHPUT, MAX_COLUMNS_NB);
+                return;
+            }
+
+            String uri = string(payload, "devId");
+
+            // Project only one device over time
+            if (!Strings.isNullOrEmpty(uri)) {
+                DeviceId deviceId = DeviceId.deviceId(uri);
+                RestServerSBDevice serverDev =
+                    (RestServerSBDevice) basicDriver.getController().getDevice(deviceId);
+
+                Map<Integer, Float[]> data = null;
+                MonitoringStatistics monStats = serverDriver.getGlobalMonitoringStatistics(deviceId);
+                if (monStats == null) {
+                    data = populateZeroDataHistory(deviceId, MAX_COLUMNS_NB);
+                } else {
+                    data = populateThroughputDataHistory(deviceId, serverDev.numberOfCpus(), monStats);
+                }
+                checkNotNull(data, "No throughput data history to visualize");
+
+                // Generate a timestamp
+                LocalDateTime ldt = new LocalDateTime(timestamp);
+
+                // Project the data
+                populateMetrics(cm, data, ldt, THROUGHPUT, NUM_OF_DATA_POINTS);
+
+                Set<DeviceId> deviceIds = Sets.newHashSet();
+                for (Device device : ds.getAvailableDevices()) {
+                    // Only devices that support this type of monitoring behaviour are considered
+                    if (device.is(MonitoringStatisticsDiscovery.class) && serverDev.isActive()) {
+                        deviceIds.add(device.id());
+                    }
+                }
+
+                // Drop down list to select devices
+                attachDeviceList(cm, deviceIds);
+            } else {
+                for (Device device : ds.getAvailableDevices()) {
+                    // Only devices that support this type of monitoring behaviour are considered
+                    if (!device.is(MonitoringStatisticsDiscovery.class)) {
+                        continue;
+                    }
+
+                    DeviceId deviceId = device.id();
+                    RestServerSBDevice serverDev =
+                        (RestServerSBDevice) basicDriver.getController().getDevice(deviceId);
+
+                    Map<Integer, Float> data = null;
+                    MonitoringStatistics monStats = serverDriver.getGlobalMonitoringStatistics(deviceId);
+                    if (monStats == null) {
+                        data = populateZeroData(deviceId, MAX_COLUMNS_NB);
+                    } else {
+                        data = populateThroughputData(deviceId, serverDev.numberOfCpus(), monStats);
+                    }
+                    checkNotNull(data, "No throughput data to visualize");
+
+                    // Map them to the CPU cores
+                    Map<String, Object> local = Maps.newHashMap();
+                    for (int i = 0; i < data.size(); i++) {
+                        local.put(getLabel(THROUGHPUT, i), data.get(i));
+                    }
+
+                    // Last piece of data is the device ID
+                    if (serverDev.isActive()) {
+                        local.put(LABEL, deviceId);
+                        populateMetric(cm.addDataPoint(deviceId), local);
+                    } else {
+                        local.put(LABEL, "");
+                        populateMetric(cm.addDataPoint(""), local);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Turn the current monitoring data into a data
+         * structure that can feed the Throughput UI memory.
+         *
+         * @param deviceId the device ID being monitored
+         * @param length the length of the array
+         * @param monStats a MonitoringStatistics object
+         * @return a map of throughput metrics to their values
+         */
+        private Map<Integer, Float> populateThroughputData(
+                DeviceId deviceId, int length, MonitoringStatistics monStats) {
+            Map<Integer, Float> data = initializeData(MAX_COLUMNS_NB);
+
+            for (CpuStatistics stats : monStats.cpuStatisticsAll()) {
+                int index = stats.id();
+
+                Float value = null;
+                if ((stats.averageThroughput().isPresent()) && (stats.load() > MIN_CPU_LOAD)) {
+                    value = stats.averageThroughput().get();
+                } else {
+                    value = new Float(0);
+                }
+
+                // Unit conversion
+                ThroughputUnit throughputUnit = null;
+                if (stats.throughputUnit().isPresent()) {
+                    throughputUnit = (ThroughputUnit) stats.throughputUnit().get();
+                } else {
+                    throughputUnit = ThroughputUnit.BPS;
+                }
+                value = ThroughputUnit.toGbps(value, throughputUnit);
+
+                // Store it locally
+                addToCache(deviceId, length, index, value);
+
+                // And into the map
+                data.put(index, value);
+            }
+
+            return data;
+        }
+
+        /**
+         * Turn the monitoring data history into a
+         * data structure that can feed the Throughput UI memory.
+         *
+         * @param deviceId the device ID being monitored
+         * @param length the length of the array
+         * @param monStats a MonitoringStatistics object
+         * @return a map of throughput metrics to their arrays of values
+         */
+        private Map<Integer, Float[]> populateThroughputDataHistory(
+                DeviceId deviceId, int length, MonitoringStatistics monStats) {
+            Map<Integer, Float[]> data = initializeDataHistory(MAX_COLUMNS_NB);
+
+            for (CpuStatistics stats : monStats.cpuStatisticsAll()) {
+                int index = stats.id();
+
+                Float value = null;
+                if ((stats.averageThroughput().isPresent()) && (stats.load() > MIN_CPU_LOAD)) {
+                    value = stats.averageThroughput().get();
+                } else {
+                    value = new Float(0);
+                }
+
+                // Unit conversion
+                ThroughputUnit throughputUnit = null;
+                if (stats.throughputUnit().isPresent()) {
+                    throughputUnit = (ThroughputUnit) stats.throughputUnit().get();
+                } else {
+                    throughputUnit = ThroughputUnit.BPS;
+                }
+                value = ThroughputUnit.toGbps(value, throughputUnit);
+
+                // Store it locally
+                addToCache(deviceId, length, index, value);
+
+                LruCache<Float> loadCache = getDataHistory(deviceId, index);
+                if (loadCache == null) {
+                    continue;
+                }
+                float[] floatArray = Floats.toArray(Arrays.asList(loadCache.values().toArray(new Float[0])));
+
+                // Fill the missing points
+                float[] filledLoadArray = fillData(floatArray, NUM_OF_DATA_POINTS);
+
+                // Set the data
+                data.put(index, ArrayUtils.toObject(filledLoadArray));
+            }
+
+            // Keep a timestamp
+            timestamp = System.currentTimeMillis();
+
+            return data;
+        }
+
+    }
+
+}
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/gui/package-info.java b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/package-info.java
new file mode 100644
index 0000000..0d5cbf8
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/gui/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Web GUI for the server device driver.
+ */
+package org.onosproject.drivers.server.gui;
\ No newline at end of file
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/impl/stats/DefaultCpuStatistics.java b/drivers/server/src/main/java/org/onosproject/drivers/server/impl/stats/DefaultCpuStatistics.java
index 2f5305d..99e6c84 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/impl/stats/DefaultCpuStatistics.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/impl/stats/DefaultCpuStatistics.java
@@ -53,7 +53,7 @@
     private final Optional<Float> averageThroughput;
     private final Optional<MonitoringUnit> latencyUnit;
     private final Optional<Float> minLatency;
-    private final Optional<Float> medianLatency;
+    private final Optional<Float> averageLatency;
     private final Optional<Float> maxLatency;
 
     private DefaultCpuStatistics(DeviceId deviceId, int id, float load, int queue, boolean isBusy) {
@@ -62,7 +62,7 @@
 
     private DefaultCpuStatistics(DeviceId deviceId, int id, float load, int queue, boolean isBusy,
             MonitoringUnit throughputUnit, float averageThroughput, MonitoringUnit latencyUnit,
-            float minLatency, float medianLatency, float maxLatency) {
+            float minLatency, float averageLatency, float maxLatency) {
         checkNotNull(deviceId, "Device ID is NULL");
         checkArgument((id >= 0) && (id < MAX_CPU_NB),
             "Invalid CPU core ID " + String.valueOf(id) + ", not in [0, " + String.valueOf(MAX_CPU_NB - 1) + "]");
@@ -83,8 +83,8 @@
                 Optional.empty() : Optional.ofNullable(latencyUnit);
         this.minLatency = (minLatency < 0) ?
                 Optional.empty() : Optional.ofNullable(minLatency);
-        this.medianLatency = (medianLatency < 0) ?
-                Optional.empty() : Optional.ofNullable(medianLatency);
+        this.averageLatency = (averageLatency < 0) ?
+                Optional.empty() : Optional.ofNullable(averageLatency);
         this.maxLatency = (maxLatency < 0) ?
                 Optional.empty() : Optional.ofNullable(maxLatency);
     }
@@ -101,7 +101,7 @@
         this.averageThroughput = null;
         this.latencyUnit = null;
         this.minLatency = null;
-        this.medianLatency = null;
+        this.averageLatency = null;
         this.maxLatency = null;
     }
 
@@ -155,8 +155,8 @@
     }
 
     @Override
-    public Optional<Float> medianLatency() {
-        return this.medianLatency;
+    public Optional<Float> averageLatency() {
+        return this.averageLatency;
     }
 
     @Override
@@ -177,7 +177,7 @@
                 .add("averageThroughput", averageThroughput.orElse(null))
                 .add("latencyUnit", latencyUnit.orElse(null))
                 .add("minLatency", minLatency.orElse(null))
-                .add("medianLatency", medianLatency.orElse(null))
+                .add("averageLatency", averageLatency.orElse(null))
                 .add("maxLatency", maxLatency.orElse(null))
                 .toString();
     }
@@ -194,7 +194,7 @@
         float averageThroughput = -1;
         MonitoringUnit latencyUnit = DEF_LATENCY_UNIT;
         float minLatency = -1;
-        float medianLatency = -1;
+        float averageLatency = -1;
         float maxLatency = -1;
 
         private Builder() {
@@ -310,13 +310,13 @@
         }
 
         /**
-         * Sets the median latency.
+         * Sets the average latency.
          *
-         * @param medianLatency median latency
+         * @param averageLatency average latency
          * @return builder object
          */
-        public Builder setMedianLatency(float medianLatency) {
-            this.medianLatency = medianLatency;
+        public Builder setAverageLatency(float averageLatency) {
+            this.averageLatency = averageLatency;
 
             return this;
         }
@@ -342,7 +342,7 @@
             return new DefaultCpuStatistics(
                 deviceId, id, load, queue, isBusy,
                 throughputUnit, averageThroughput,
-                latencyUnit, minLatency, medianLatency, maxLatency);
+                latencyUnit, minLatency, averageLatency, maxLatency);
         }
     }
 
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/impl/stats/DefaultTimingStatistics.java b/drivers/server/src/main/java/org/onosproject/drivers/server/impl/stats/DefaultTimingStatistics.java
index 0fe253d..6c4f86f 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/impl/stats/DefaultTimingStatistics.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/impl/stats/DefaultTimingStatistics.java
@@ -36,22 +36,22 @@
     private final MonitoringUnit unit;
     private final long deployCommandParsingTime;
     private final long deployCommandLaunchingTime;
-    private long autoscaleTime;
+    private long autoScaleTime;
 
     private DefaultTimingStatistics(
             MonitoringUnit unit,
             long parsingTime,
             long launchingTime,
-            long autoscaleTime) {
+            long autoScaleTime) {
         checkNotNull(unit, "Time statistics unit is null");
         checkArgument(parsingTime   >= 0, "Parsing time is negative");
         checkArgument(launchingTime >= 0, "Launching time is negative");
-        checkArgument(autoscaleTime >= 0, "Autoscale time is negative");
+        checkArgument(autoScaleTime >= 0, "Auto-scale time is negative");
 
         this.unit = unit;
         this.deployCommandParsingTime   = parsingTime;
         this.deployCommandLaunchingTime = launchingTime;
-        this.autoscaleTime = autoscaleTime;
+        this.autoScaleTime = autoScaleTime;
     }
 
     // Constructor for serializer
@@ -59,7 +59,7 @@
         this.unit = null;
         this.deployCommandParsingTime   = 0;
         this.deployCommandLaunchingTime = 0;
-        this.autoscaleTime = 0;
+        this.autoScaleTime = 0;
     }
 
     /**
@@ -92,8 +92,8 @@
     }
 
     @Override
-    public long autoscaleTime() {
-        return this.autoscaleTime;
+    public long autoScaleTime() {
+        return this.autoScaleTime;
     }
 
     @Override
@@ -104,7 +104,7 @@
                 .add("parsingTime", this.deployCommandParsingTime())
                 .add("launchingTime", this.deployCommandLaunchingTime())
                 .add("deploymentTime", this.totalDeploymentTime())
-                .add("autoScaleTime", this.autoscaleTime())
+                .add("autoScaleTime", this.autoScaleTime())
                 .toString();
     }
 
@@ -113,7 +113,7 @@
         MonitoringUnit unit = DEF_UNIT;
         long deployCommandParsingTime;
         long deployCommandLaunchingTime;
-        long autoscaleTime;
+        long autoScaleTime;
 
         private Builder() {
 
@@ -160,11 +160,11 @@
         /**
          * Sets autoscale time.
          *
-         * @param autoscaleTime time required to autoscale
+         * @param autoScaleTime time required to autoscale
          * @return builder object
          */
-        public Builder setAutoscaleTime(long autoscaleTime) {
-            this.autoscaleTime = autoscaleTime;
+        public Builder setAutoScaleTime(long autoScaleTime) {
+            this.autoScaleTime = autoScaleTime;
 
             return this;
         }
@@ -176,11 +176,8 @@
          */
         public DefaultTimingStatistics build() {
             return new DefaultTimingStatistics(
-                unit,
-                deployCommandParsingTime,
-                deployCommandLaunchingTime,
-                autoscaleTime
-            );
+                unit, deployCommandParsingTime,
+                deployCommandLaunchingTime, autoScaleTime);
         }
     }
 
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/stats/CpuStatistics.java b/drivers/server/src/main/java/org/onosproject/drivers/server/stats/CpuStatistics.java
index f30134d..c43b826 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/stats/CpuStatistics.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/stats/CpuStatistics.java
@@ -84,12 +84,12 @@
     Optional<Float> minLatency();
 
     /**
-     * Returns the median latency incurred by a CPU core,
+     * Returns the average latency incurred by a CPU core,
      * expressed in latencyUnit() monitoring units.
      *
-     * @return median latency incurred by a CPU core
+     * @return average latency incurred by a CPU core
      */
-    Optional<Float> medianLatency();
+    Optional<Float> averageLatency();
 
     /**
      * Returns the maximum latency incurred by a CPU core,
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/stats/MonitoringUnit.java b/drivers/server/src/main/java/org/onosproject/drivers/server/stats/MonitoringUnit.java
index 0528a69..33d8763 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/stats/MonitoringUnit.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/stats/MonitoringUnit.java
@@ -53,6 +53,22 @@
             return MAP.get(tu);
         }
 
+        public static float toGbps(float value, ThroughputUnit fromUnit) {
+            if (value == 0) {
+                return value;
+            }
+
+            if (fromUnit == BPS) {
+                return value / 1000000000;
+            } else if (fromUnit == KBPS) {
+                return value / 1000000;
+            } else if (fromUnit == MBPS) {
+                return value / 1000;
+            }
+
+            return value;
+        }
+
         @Override
         public String toString() {
             return this.throughputUnit;
@@ -90,6 +106,22 @@
             return MAP.get(lu);
         }
 
+        public static float toNano(float value, LatencyUnit fromUnit) {
+            if (value == 0) {
+                return value;
+            }
+
+            if (fromUnit == MICRO_SECOND) {
+                return value * 1000;
+            } else if (fromUnit == MILLI_SECOND) {
+                return value * 1000000;
+            } else if (fromUnit == SECOND) {
+                return value * 1000000000;
+            }
+
+            return value;
+        }
+
         @Override
         public String toString() {
             return this.latencyUnit;
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/stats/TimingStatistics.java b/drivers/server/src/main/java/org/onosproject/drivers/server/stats/TimingStatistics.java
index 9cf46dd..23215ce 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/stats/TimingStatistics.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/stats/TimingStatistics.java
@@ -52,10 +52,10 @@
 
     /**
      * Time (ns) to perform a local reconfiguration.
-     * (i.e., the agent autoscales the number of CPUs).
+     * (i.e., the agent auto-scales the number of CPUs).
      *
-     * @return time in nanoseconds to autoscale
+     * @return time in nanoseconds to auto scale
      */
-    long autoscaleTime();
+    long autoScaleTime();
 
 }
diff --git a/drivers/server/src/main/resources/app/view/cpu/cpu.css b/drivers/server/src/main/resources/app/view/cpu/cpu.css
new file mode 100644
index 0000000..3f6f72e
--- /dev/null
+++ b/drivers/server/src/main/resources/app/view/cpu/cpu.css
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- CPU Manager -- CSS file
+ */
+
+#ov-cpu {
+    padding: 20px;
+    position: relative;
+}
+.light #ov-cpu {
+    color: navy;
+}
+.dark #ov-cpu {
+    color: #88f;
+}
+
+#ov-cpu .button-panel {
+    margin: 10px;
+    width: 200px;
+}
+
+.light #ov-cpu .button-panel {
+    background-color: #ccf;
+}
+.dark #ov-cpu .button-panel {
+    background-color: #444;
+}
+
+#ov-cpu #chart-loader {
+    position: absolute;
+    width: 200px;
+    height: 50px;
+    margin-left: -100px;
+    margin-top: -25px;
+    z-index: 900;
+    top: 50%;
+    text-align: center;
+    left: 50%;
+    font-size: 25px;
+    font-weight: bold;
+    color: #ccc;
+}
\ No newline at end of file
diff --git a/drivers/server/src/main/resources/app/view/cpu/cpu.html b/drivers/server/src/main/resources/app/view/cpu/cpu.html
new file mode 100644
index 0000000..7fd758f
--- /dev/null
+++ b/drivers/server/src/main/resources/app/view/cpu/cpu.html
@@ -0,0 +1,27 @@
+<!-- partial HTML -->
+<div id="ov-cpu">
+    <div id="chart-loader" ng-show="!devId && showLoader">
+        No Servers
+    </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>
+        <div class="ctrl-btns">
+            <select ng-options="deviceId as deviceId for deviceId in deviceIds"
+                    ng-model="selectedItem" ng-change="onChange(selectedItem)">
+                <option value="">-- select a device --</option>
+            </select>
+        </div>
+        <canvas id="line" class="chart chart-line" chart-data="data"
+                chart-labels="labels" chart-legend="true"
+                chart-series="series" chart-options="options" height="100%">
+        </canvas>
+    </div>
+</div>
diff --git a/drivers/server/src/main/resources/app/view/cpu/cpu.js b/drivers/server/src/main/resources/app/view/cpu/cpu.js
new file mode 100644
index 0000000..93f3ac8
--- /dev/null
+++ b/drivers/server/src/main/resources/app/view/cpu/cpu.js
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- CPU Manager View Module
+ */
+(function () {
+    'use strict';
+
+    // injected references
+    var $log, $scope, $location, ks, fs, cbs, ns;
+
+    var hasDeviceId;
+    // TODO: Pass this dynamically
+    var coresNb = 16;
+
+    var labels = new Array(1);
+    var data = new Array(coresNb);
+    for (var i = 0; i < coresNb; i++) {
+        data[i] = new Array(1);
+        data[i][0] = 0;
+    }
+
+    angular.module('ovCpu', ["chart.js"])
+        .controller('OvCpuCtrl',
+        ['$log', '$scope', '$location', 'FnService', 'ChartBuilderService', 'NavService',
+
+        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 {
+                hasDeviceId = false;
+            }
+
+            cbs.buildChart({
+                scope: $scope,
+                tag: 'cpu',
+                query: params
+            });
+
+            $scope.$watch('chartData', function () {
+                if (!fs.isEmptyObject($scope.chartData)) {
+                    $scope.showLoader = false;
+                    var length = $scope.chartData.length;
+                    labels = new Array(length);
+                    for (var i = 0; i < coresNb; i++) {
+                        data[i] = new Array(length);
+                    }
+
+                    $scope.chartData.forEach(
+                        function (cm, idx) {
+                            // TODO: Squeeze using a working loop?
+                            data[0][idx]  = cm.cpu_0;
+                            data[1][idx]  = cm.cpu_1;
+                            data[2][idx]  = cm.cpu_2;
+                            data[3][idx]  = cm.cpu_3;
+                            data[4][idx]  = cm.cpu_4;
+                            data[5][idx]  = cm.cpu_5;
+                            data[6][idx]  = cm.cpu_6;
+                            data[7][idx]  = cm.cpu_7;
+                            data[8][idx]  = cm.cpu_8;
+                            data[9][idx]  = cm.cpu_9;
+                            data[10][idx] = cm.cpu_10;
+                            data[11][idx] = cm.cpu_11;
+                            data[12][idx] = cm.cpu_12;
+                            data[13][idx] = cm.cpu_13;
+                            data[14][idx] = cm.cpu_14;
+                            data[15][idx] = cm.cpu_15;
+
+                            labels[idx] = cm.label;
+                        }
+                    );
+                }
+
+                $scope.labels = labels;
+                $scope.data = data;
+
+                $scope.options = {
+                    scales: {
+                        yAxes: [{
+                            type: 'linear',
+                            position: 'left',
+                            id: 'y-axis-cpu',
+                            ticks: {
+                                min: 0,
+                                max: 100,
+                                fontSize: 28,
+                            },
+                            scaleLabel: {
+                                display: true,
+                                labelString: 'Utilization/CPU Core (%)',
+                                fontSize: 28,
+                            }
+                        }],
+                        xAxes: [{
+                            id: 'x-axis-servers-cores',
+                            ticks: {
+                                fontSize: 28,
+                            },
+                            scaleLabel: {
+                                display: true,
+                                fontSize: 28,
+                            }
+                        }]
+                    }
+                };
+
+                $scope.onClick = function (points, evt) {
+                    var label = labels[points[0]._index];
+                    if (label) {
+                        ns.navTo('cpu', { devId: label });
+                        $log.log(label);
+                    }
+                };
+
+                if (!fs.isEmptyObject($scope.annots)) {
+                    $scope.deviceIds = JSON.parse($scope.annots.deviceIds);
+                }
+
+                $scope.onChange = function (deviceId) {
+                    ns.navTo('cpu', { devId: deviceId });
+                };
+            });
+
+            $scope.series = new Array(coresNb);
+            for (var i = 0; i < coresNb; i++) {
+                $scope.series[i] = 'CPU ' + i;
+            }
+
+            $scope.labels = labels;
+            $scope.data = data;
+
+            // TODO: For some reason, this assignment does not work
+            $scope.chartColors = [
+                '#e6194b',       // Red
+                '#3cb44b',       // Green
+                '#ffe119',       // Yellow
+                '#0082c8',       // Blue
+                '#f58231',       // Orange
+                '#808080',       // Grey
+                '#fffac8',       // Beige
+                '#aaffc3',       // Mint
+                '#911eb4',       // Purple
+                '#46f0f0',       // Cyan
+                '#d2f53c',       // Lime
+                '#800000',       // Maroon
+                '#000000',       // Black
+                '#f032e6',       // Magenta
+                '#008080',       // Teal
+                '#808000',       // Olive
+                '#aa6e28'        // Brown
+            ];
+            Chart.defaults.global.colours = $scope.chartColors;
+
+            $scope.showLoader = true;
+
+            $log.log('OvCpuCtrl has been created');
+        }]);
+
+}());
diff --git a/drivers/server/src/main/resources/app/view/latency/latency.css b/drivers/server/src/main/resources/app/view/latency/latency.css
new file mode 100644
index 0000000..61f9716
--- /dev/null
+++ b/drivers/server/src/main/resources/app/view/latency/latency.css
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Latency UI -- CSS file
+ */
+
+#ov-latency {
+    padding: 20px;
+    position: relative;
+}
+.light #ov-latency {
+    color: navy;
+}
+.dark #ov-latency {
+    color: #88f;
+}
+
+#ov-latency .button-panel {
+    margin: 10px;
+    width: 200px;
+}
+
+.light #ov-latency .button-panel {
+    background-color: #ccf;
+}
+.dark #ov-latency .button-panel {
+    background-color: #444;
+}
+
+#ov-latency #chart-loader {
+    position: absolute;
+    width: 200px;
+    height: 50px;
+    margin-left: -100px;
+    margin-top: -25px;
+    z-index: 900;
+    top: 50%;
+    text-align: center;
+    left: 50%;
+    font-size: 25px;
+    font-weight: bold;
+    color: #ccc;
+}
\ No newline at end of file
diff --git a/drivers/server/src/main/resources/app/view/latency/latency.html b/drivers/server/src/main/resources/app/view/latency/latency.html
new file mode 100644
index 0000000..1e5e6b2
--- /dev/null
+++ b/drivers/server/src/main/resources/app/view/latency/latency.html
@@ -0,0 +1,27 @@
+<!-- partial HTML -->
+<div id="ov-latency">
+    <div id="chart-loader" ng-show="!devId && showLoader">
+        No Servers
+    </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>
+        <div class="ctrl-btns">
+            <select ng-options="deviceId as deviceId for deviceId in deviceIds"
+                    ng-model="selectedItem" ng-change="onChange(selectedItem)">
+                <option value="">-- select a device --</option>
+            </select>
+        </div>
+        <canvas id="line" class="chart chart-line" chart-data="data"
+                chart-labels="labels" chart-legend="true"
+                chart-series="series" chart-options="options" height="100%">
+        </canvas>
+    </div>
+</div>
diff --git a/drivers/server/src/main/resources/app/view/latency/latency.js b/drivers/server/src/main/resources/app/view/latency/latency.js
new file mode 100644
index 0000000..4277506
--- /dev/null
+++ b/drivers/server/src/main/resources/app/view/latency/latency.js
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Latency View Module
+ */
+(function () {
+    'use strict';
+
+    // injected references
+    var $log, $scope, $location, ks, fs, cbs, ns;
+
+    var hasDeviceId;
+    // TODO: Pass this dynamically
+    var coresNb = 16;
+
+    var labels = new Array(1);
+    var data = new Array(coresNb);
+    for (var i = 0; i < coresNb; i++) {
+        data[i] = new Array(1);
+        data[i][0] = 0;
+    }
+
+    angular.module('ovLatency', ["chart.js"])
+        .controller('OvLatencyCtrl',
+        ['$log', '$scope', '$location', 'FnService', 'ChartBuilderService', 'NavService',
+
+        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 {
+                hasDeviceId = false;
+            }
+
+            cbs.buildChart({
+                scope: $scope,
+                tag: 'latency',
+                query: params
+            });
+
+            $scope.$watch('chartData', function () {
+                if (!fs.isEmptyObject($scope.chartData)) {
+                    $scope.showLoader = false;
+                    var length = $scope.chartData.length;
+                    labels = new Array(length);
+                    for (var i = 0; i < coresNb; i++) {
+                        data[i] = new Array(length);
+                    }
+
+                    $scope.chartData.forEach(
+                        function (cm, idx) {
+                            // TODO: Squeeze using a working loop?
+                            data[0][idx]  = cm.latency_0;
+                            data[1][idx]  = cm.latency_1;
+                            data[2][idx]  = cm.latency_2;
+                            data[3][idx]  = cm.latency_3;
+                            data[4][idx]  = cm.latency_4;
+                            data[5][idx]  = cm.latency_5;
+                            data[6][idx]  = cm.latency_6;
+                            data[7][idx]  = cm.latency_7;
+                            data[8][idx]  = cm.latency_8;
+                            data[9][idx]  = cm.latency_9;
+                            data[10][idx] = cm.latency_10;
+                            data[11][idx] = cm.latency_11;
+                            data[12][idx] = cm.latency_12;
+                            data[13][idx] = cm.latency_13;
+                            data[14][idx] = cm.latency_14;
+                            data[15][idx] = cm.latency_15;
+
+                            labels[idx] = cm.label;
+                        }
+                    );
+                }
+
+                $scope.labels = labels;
+                $scope.data = data;
+
+                $scope.options = {
+                    scales: {
+                        yAxes: [{
+                            type: 'linear',
+                            position: 'left',
+                            id: 'y-axis-latency',
+                            ticks: {
+                                beginAtZero: true,
+                                fontSize: 28,
+                            },
+                            scaleLabel: {
+                                display: true,
+                                labelString: 'Latency/CPU Core (ns)',
+                                fontSize: 28,
+                            }
+                        }],
+                        xAxes: [{
+                            id: 'x-axis-servers-cores',
+                            ticks: {
+                                fontSize: 28,
+                            },
+                            scaleLabel: {
+                                display: true,
+                                fontSize: 28,
+                            }
+                        }]
+                    }
+                };
+
+                $scope.onClick = function (points, evt) {
+                    var label = labels[points[0]._index];
+                    if (label) {
+                        ns.navTo('latency', { devId: label });
+                        $log.log(label);
+                    }
+                };
+
+                if (!fs.isEmptyObject($scope.annots)) {
+                    $scope.deviceIds = JSON.parse($scope.annots.deviceIds);
+                }
+
+                $scope.onChange = function (deviceId) {
+                    ns.navTo('latency', { devId: deviceId });
+                };
+            });
+
+            $scope.series = new Array(coresNb);
+            for (var i = 0; i < coresNb; i++) {
+                $scope.series[i] = 'Latency-CPU ' + i;
+            }
+
+            $scope.labels = labels;
+            $scope.data = data;
+
+            // TODO: For some reason, this assignment does not work
+            $scope.chartColors = [
+                '#e6194b',       // Red
+                '#3cb44b',       // Green
+                '#ffe119',       // Yellow
+                '#0082c8',       // Blue
+                '#f58231',       // Orange
+                '#808080',       // Grey
+                '#fffac8',       // Beige
+                '#aaffc3',       // Mint
+                '#911eb4',       // Purple
+                '#46f0f0',       // Cyan
+                '#d2f53c',       // Lime
+                '#800000',       // Maroon
+                '#000000',       // Black
+                '#f032e6',       // Magenta
+                '#008080',       // Teal
+                '#808000',       // Olive
+                '#aa6e28'        // Brown
+            ];
+            Chart.defaults.global.colours = $scope.chartColors;
+
+            $scope.showLoader = true;
+
+            $log.log('OvLatencyCtrl has been created');
+        }]);
+
+}());
diff --git a/drivers/server/src/main/resources/app/view/throughput/throughput.css b/drivers/server/src/main/resources/app/view/throughput/throughput.css
new file mode 100644
index 0000000..2914588
--- /dev/null
+++ b/drivers/server/src/main/resources/app/view/throughput/throughput.css
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Throughput UI -- CSS file
+ */
+
+#ov-throughput {
+    padding: 20px;
+    position: relative;
+}
+.light #ov-throughput {
+    color: navy;
+}
+.dark #ov-throughput {
+    color: #88f;
+}
+
+#ov-throughput .button-panel {
+    margin: 10px;
+    width: 200px;
+}
+
+.light #ov-throughput .button-panel {
+    background-color: #ccf;
+}
+.dark #ov-throughput .button-panel {
+    background-color: #444;
+}
+
+#ov-throughput #chart-loader {
+    position: absolute;
+    width: 200px;
+    height: 50px;
+    margin-left: -100px;
+    margin-top: -25px;
+    z-index: 900;
+    top: 50%;
+    text-align: center;
+    left: 50%;
+    font-size: 25px;
+    font-weight: bold;
+    color: #ccc;
+}
\ No newline at end of file
diff --git a/drivers/server/src/main/resources/app/view/throughput/throughput.html b/drivers/server/src/main/resources/app/view/throughput/throughput.html
new file mode 100644
index 0000000..2535794
--- /dev/null
+++ b/drivers/server/src/main/resources/app/view/throughput/throughput.html
@@ -0,0 +1,27 @@
+<!-- partial HTML -->
+<div id="ov-throughput">
+    <div id="chart-loader" ng-show="!devId && showLoader">
+        No Servers
+    </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>
+        <div class="ctrl-btns">
+            <select ng-options="deviceId as deviceId for deviceId in deviceIds"
+                    ng-model="selectedItem" ng-change="onChange(selectedItem)">
+                <option value="">-- select a device --</option>
+            </select>
+        </div>
+        <canvas id="line" class="chart chart-line" chart-data="data"
+                chart-labels="labels" chart-legend="true"
+                chart-series="series" chart-options="options" height="100%">
+        </canvas>
+    </div>
+</div>
diff --git a/drivers/server/src/main/resources/app/view/throughput/throughput.js b/drivers/server/src/main/resources/app/view/throughput/throughput.js
new file mode 100644
index 0000000..c16e2c0
--- /dev/null
+++ b/drivers/server/src/main/resources/app/view/throughput/throughput.js
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Throughput View Module
+ */
+(function () {
+    'use strict';
+
+    // injected references
+    var $log, $scope, $location, ks, fs, cbs, ns;
+
+    var hasDeviceId;
+    // TODO: Pass this dynamically
+    var coresNb = 16;
+
+    var labels = new Array(1);
+    var data = new Array(coresNb);
+    for (var i = 0; i < coresNb; i++) {
+        data[i] = new Array(1);
+        data[i][0] = 0;
+    }
+
+    angular.module('ovThroughput', ["chart.js"])
+        .controller('OvThroughputCtrl',
+        ['$log', '$scope', '$location', 'FnService', 'ChartBuilderService', 'NavService',
+
+        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 {
+                hasDeviceId = false;
+            }
+
+            cbs.buildChart({
+                scope: $scope,
+                tag: 'throughput',
+                query: params
+            });
+
+            $scope.$watch('chartData', function () {
+                if (!fs.isEmptyObject($scope.chartData)) {
+                    $scope.showLoader = false;
+                    var length = $scope.chartData.length;
+                    labels = new Array(length);
+                    for (var i = 0; i < coresNb; i++) {
+                        data[i] = new Array(length);
+                    }
+
+                    $scope.chartData.forEach(
+                        function (cm, idx) {
+                            // TODO: Squeeze using a working loop?
+                            data[0][idx]  = cm.throughput_0;
+                            data[1][idx]  = cm.throughput_1;
+                            data[2][idx]  = cm.throughput_2;
+                            data[3][idx]  = cm.throughput_3;
+                            data[4][idx]  = cm.throughput_4;
+                            data[5][idx]  = cm.throughput_5;
+                            data[6][idx]  = cm.throughput_6;
+                            data[7][idx]  = cm.throughput_7;
+                            data[8][idx]  = cm.throughput_8;
+                            data[9][idx]  = cm.throughput_9;
+                            data[10][idx] = cm.throughput_10;
+                            data[11][idx] = cm.throughput_11;
+                            data[12][idx] = cm.throughput_12;
+                            data[13][idx] = cm.throughput_13;
+                            data[14][idx] = cm.throughput_14;
+                            data[15][idx] = cm.throughput_15;
+
+                            labels[idx] = cm.label;
+                        }
+                    );
+                }
+
+                $scope.labels = labels;
+                $scope.data = data;
+
+                $scope.options = {
+                    scales: {
+                        yAxes: [{
+                            type: 'linear',
+                            position: 'left',
+                            id: 'y-axis-throughput',
+                            ticks: {
+                                min: 0,
+                                max: 100,
+                                fontSize: 28,
+                            },
+                            scaleLabel: {
+                                display: true,
+                                labelString: 'Throughput/CPU Core (Gbps)',
+                                fontSize: 28,
+                            }
+                        }],
+                        xAxes: [{
+                            id: 'x-axis-servers-cores',
+                            ticks: {
+                                fontSize: 28,
+                            },
+                            scaleLabel: {
+                                display: true,
+                                fontSize: 28,
+                            }
+                        }]
+                    }
+                };
+
+                $scope.onClick = function (points, evt) {
+                    var label = labels[points[0]._index];
+                    if (label) {
+                        ns.navTo('throughput', { devId: label });
+                        $log.log(label);
+                    }
+                };
+
+                if (!fs.isEmptyObject($scope.annots)) {
+                    $scope.deviceIds = JSON.parse($scope.annots.deviceIds);
+                }
+
+                $scope.onChange = function (deviceId) {
+                    ns.navTo('throughput', { devId: deviceId });
+                };
+            });
+
+            $scope.series = new Array(coresNb);
+            for (var i = 0; i < coresNb; i++) {
+                $scope.series[i] = 'Throughput-CPU ' + i;
+            }
+
+            $scope.labels = labels;
+            $scope.data = data;
+
+            // TODO: For some reason, this assignment does not work
+            $scope.chartColors = [
+                '#e6194b',       // Red
+                '#3cb44b',       // Green
+                '#ffe119',       // Yellow
+                '#0082c8',       // Blue
+                '#f58231',       // Orange
+                '#808080',       // Grey
+                '#fffac8',       // Beige
+                '#aaffc3',       // Mint
+                '#911eb4',       // Purple
+                '#46f0f0',       // Cyan
+                '#d2f53c',       // Lime
+                '#800000',       // Maroon
+                '#000000',       // Black
+                '#f032e6',       // Magenta
+                '#008080',       // Teal
+                '#808000',       // Olive
+                '#aa6e28'        // Brown
+            ];
+            Chart.defaults.global.colours = $scope.chartColors;
+
+            $scope.showLoader = true;
+
+            $log.log('OvThroughputCtrl has been created');
+        }]);
+
+}());
diff --git a/drivers/server/src/main/resources/gui/css.html b/drivers/server/src/main/resources/gui/css.html
new file mode 100644
index 0000000..8eca7d4
--- /dev/null
+++ b/drivers/server/src/main/resources/gui/css.html
@@ -0,0 +1,3 @@
+<link rel="stylesheet" href="app/view/cpu/cpu.css">
+<link rel="stylesheet" href="app/view/latency/latency.css">
+<link rel="stylesheet" href="app/view/throughput/throughput.css">
diff --git a/drivers/server/src/main/resources/gui/js.html b/drivers/server/src/main/resources/gui/js.html
new file mode 100644
index 0000000..99a84e8
--- /dev/null
+++ b/drivers/server/src/main/resources/gui/js.html
@@ -0,0 +1,3 @@
+<script src="app/view/cpu/cpu.js"></script>
+<script src="app/view/latency/latency.js"></script>
+<script src="app/view/throughput/throughput.js"></script>