Latest server driver updates in 1.14 branch

These updates include several bug fixes and a new UI
for this driver.

Change-Id: I631cf322ee3724d9f1f97246d4189b5b2a008a76
Signed-off-by: Georgios Katsikas <katsikas.gp@gmail.com>
(cherry picked from commit 6dc11c1e8078a40fcb95586e9bec9336dc2507d5)
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/BasicServerDriver.java b/drivers/server/src/main/java/org/onosproject/drivers/server/BasicServerDriver.java
index 4fffe6e..d1cb020 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/BasicServerDriver.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/BasicServerDriver.java
@@ -194,6 +194,22 @@
     }
 
     /**
+     * Raise a connect event by setting the
+     * activity flag of this device.
+     *
+     * @param device a device to connect
+     */
+    protected void raiseDeviceReconnect(RestSBDevice device) {
+        // Already done!
+        if (device.isActive()) {
+            return;
+        }
+
+        log.debug("Setting device {} active", device.deviceId());
+        device.setActive(true);
+    }
+
+    /**
      * Upon a failure to contact a device, the driver
      * raises a disconnect event by resetting the
      * activity flag of this device.
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/FlowRuleProgrammableServerImpl.java b/drivers/server/src/main/java/org/onosproject/drivers/server/FlowRuleProgrammableServerImpl.java
index 9c38fe8..34947cb 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/FlowRuleProgrammableServerImpl.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/FlowRuleProgrammableServerImpl.java
@@ -22,22 +22,25 @@
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.collect.Sets;
 
-import org.slf4j.Logger;
-
 import org.onosproject.drivers.server.devices.nic.NicFlowRule;
 import org.onosproject.drivers.server.devices.nic.NicRxFilter.RxFilter;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.driver.Driver;
+import org.onosproject.net.driver.DriverService;
 import org.onosproject.net.flow.DefaultFlowEntry;
 import org.onosproject.net.flow.FlowEntry;
 import org.onosproject.net.flow.FlowRule;
 import org.onosproject.net.flow.FlowRuleProgrammable;
 import org.onosproject.net.flow.FlowRuleService;
 
+import org.slf4j.Logger;
+
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -72,6 +75,12 @@
     private static final String PARAM_CPU_RULES    = "cpuRules";
     private static final String PARAM_RULE_ID      = "ruleId";
     private static final String PARAM_RULE_CONTENT = "ruleContent";
+    private static final String PARAM_RX_FILTER_FD = "flow";
+
+    /**
+     * Driver's property to specify how many rules the controller can remove at once.
+     */
+    private static final String RULE_DELETE_BATCH_SIZE_PROPERTY = "ruleDeleteBatchSize";
 
     @Override
     public Collection<FlowEntry> getFlowEntries() {
@@ -87,7 +96,7 @@
         try {
             response = getController().get(deviceId, RULE_MANAGEMENT_URL, JSON);
         } catch (ProcessingException pEx) {
-            log.error("Failed to get flow entries from device: {}", deviceId);
+            log.error("Failed to get NIC flow entries from device: {}", deviceId);
             return Collections.EMPTY_LIST;
         }
 
@@ -99,12 +108,12 @@
             JsonNode jsonNode = mapper.convertValue(jsonMap, JsonNode.class);
             objNode = (ObjectNode) jsonNode;
         } catch (IOException ioEx) {
-            log.error("Failed to get flow entries from device: {}", deviceId);
+            log.error("Failed to get NIC flow entries from device: {}", deviceId);
             return Collections.EMPTY_LIST;
         }
 
         if (objNode == null) {
-            log.error("Failed to get flow entries from device: {}", deviceId);
+            log.error("Failed to get NIC flow entries from device: {}", deviceId);
             return Collections.EMPTY_LIST;
         }
 
@@ -148,8 +157,7 @@
                             continue;
                         // Rule trully present in the data plane => Add
                         } else {
-                            actualFlowEntries.add(
-                                new DefaultFlowEntry(
+                            actualFlowEntries.add(new DefaultFlowEntry(
                                     r, FlowEntry.FlowEntryState.ADDED, 0, 0, 0));
                         }
                     }
@@ -189,15 +197,44 @@
         DeviceId deviceId = getHandler().data().deviceId();
         checkNotNull(deviceId, DEVICE_ID_NULL);
 
+        int ruleDeleteBatchSize = getRuleDeleteBatchSizeProperty(deviceId);
+
         // Set of truly-removed rules to be reported
         Set<FlowRule> removedRules = Sets.<FlowRule>newConcurrentHashSet();
 
-        // for (FlowRule rule : rules) {
-        rules.forEach(rule -> {
-            if (removeNicFlowRule(deviceId, rule.id().value())) {
-                removedRules.add(rule);
+        List<FlowRule> ruleList = (List) rules;
+        int ruleCount = rules.size();
+        int ruleStart = 0;
+        int processed = 0;
+        int batchNb   = 1;
+        while (processed < ruleCount) {
+            String ruleIds = "";
+
+            for (int i = ruleStart; i < ruleCount; i++) {
+                // Batch completed
+                if (i >= (batchNb * ruleDeleteBatchSize)) {
+                    break;
+                }
+
+                // TODO: Turn this string into a list and modify removeNicFlowRuleBatch()
+                // Create a comma-separated sequence of rule IDs
+                ruleIds += Long.toString(ruleList.get(i).id().value()) + ",";
+
+                processed++;
             }
-        });
+
+            // Remove last comma
+            ruleIds = ruleIds.substring(0, ruleIds.length() - 1);
+
+            // Remove the entire batch of rules at once
+            if (removeNicFlowRuleBatch(deviceId, ruleIds)) {
+                removedRules.addAll(ruleList.subList(ruleStart, processed));
+            }
+
+            // Prepare for the next batch (if any)
+            batchNb++;
+            ruleStart += ruleDeleteBatchSize;
+        }
 
         return removedRules;
     }
@@ -214,16 +251,26 @@
 
         rules.forEach(rule -> {
             if (!(rule instanceof FlowEntry)) {
-                NicFlowRule nicRule = (NicFlowRule) rule;
-                String tcId = nicRule.trafficClassId();
+                NicFlowRule nicRule = null;
 
-                // Create a bucket of flow rules for this traffic class
-                if (!rulesPerTc.containsKey(tcId)) {
-                    rulesPerTc.put(tcId, Sets.<FlowRule>newConcurrentHashSet());
+                // Only NicFlowRules are accepted
+                try {
+                    nicRule = (NicFlowRule) rule;
+                } catch (ClassCastException cEx) {
+                    log.warn("Skipping rule not crafted for NIC: {}", rule);
                 }
 
-                Set<FlowRule> tcRuleSet = rulesPerTc.get(tcId);
-                tcRuleSet.add(nicRule);
+                if (nicRule != null) {
+                    String tcId = nicRule.trafficClassId();
+
+                    // Create a bucket of flow rules for this traffic class
+                    if (!rulesPerTc.containsKey(tcId)) {
+                        rulesPerTc.put(tcId, Sets.<FlowRule>newConcurrentHashSet());
+                    }
+
+                    Set<FlowRule> tcRuleSet = rulesPerTc.get(tcId);
+                    tcRuleSet.add(nicRule);
+                }
             }
         });
 
@@ -274,7 +321,7 @@
 
         // Create the object node to host the Rx filter method
         ObjectNode methodObjNode = mapper.createObjectNode();
-        methodObjNode.put(BasicServerDriver.NIC_PARAM_RX_METHOD, "flow");
+        methodObjNode.put(BasicServerDriver.NIC_PARAM_RX_METHOD, PARAM_RX_FILTER_FD);
         scsObjNode.put(BasicServerDriver.NIC_PARAM_RX_FILTER, methodObjNode);
 
         // Map each core to an array of rule IDs and rules
@@ -285,14 +332,18 @@
 
         for (FlowRule rule : rules) {
             NicFlowRule nicRule = (NicFlowRule) rule;
+            if (nicRule.isFullWildcard() && (rules.size() > 1)) {
+                log.warn("Skipping wildcard rule: {}", nicRule);
+                continue;
+            }
+
             long coreIndex = nicRule.cpuCoreIndex();
 
             // Keep the ID of the target NIC
             if (nic == null) {
                 nic = findNicInterfaceWithPort(deviceId, nicRule.interfaceNumber());
-                checkArgument(
-                    !Strings.isNullOrEmpty(nic),
-                    "Attempted to install rules on an invalid NIC");
+                checkArgument(!Strings.isNullOrEmpty(nic),
+                    "Attempted to install rules in an invalid NIC");
             }
 
             // Create a JSON array for this CPU core
@@ -341,11 +392,11 @@
 
         // Upon an error, return an empty set of rules
         if (!checkStatusCode(response)) {
-            log.error("Failed to install flow rules on device {}", deviceId);
+            log.error("Failed to install NIC flow rules in device {}", deviceId);
             return Collections.EMPTY_LIST;
         }
 
-        log.info("Successfully installed {} flow rules on device {}",
+        log.info("Successfully installed {} NIC flow rules in device {}",
             rules.size(), deviceId);
 
         // .. or all of them
@@ -353,31 +404,44 @@
     }
 
     /**
-     * Removes a FlowRule from a server device.
+     * Removes a batch of FlowRules from a server device
+     * using a single REST command.
      *
      * @param deviceId target server device ID
-     * @param ruleId NIC rule ID to be removed
+     * @param ruleIds a batch of comma-separated NIC rule IDs to be removed
      * @return boolean removal status
      */
-    private boolean removeNicFlowRule(DeviceId deviceId, long ruleId) {
+    private boolean removeNicFlowRuleBatch(DeviceId deviceId, String ruleIds) {
         int response = -1;
+        long ruleCount = ruleIds.chars().filter(ch -> ch == ',').count() + 1;
 
-        // Try to remove the rule, although server might be unreachable
+        // Try to remove the rules, although server might be unreachable
         try {
             response = getController().delete(deviceId,
-                RULE_MANAGEMENT_URL + SLASH + Long.toString(ruleId), null, JSON);
+                RULE_MANAGEMENT_URL + SLASH + ruleIds, null, JSON);
         } catch (Exception ex) {
-            log.error("Failed to remove flow rule {} from device {}", ruleId, deviceId);
+            log.error("Failed to remove NIC flow rule batch with {} rules from device {}", ruleCount, deviceId);
             return false;
         }
 
         if (!checkStatusCode(response)) {
-            log.error("Failed to remove flow rule {} from device {}", ruleId, deviceId);
+            log.error("Failed to remove NIC flow rule batch with {} rules from device {}", ruleCount, deviceId);
             return false;
         }
 
-        log.info("Successfully removed flow rule {} from device {}", ruleId, deviceId);
+        log.info("Successfully removed NIC flow rule batch with {} rules from device {}", ruleCount, deviceId);
         return true;
     }
 
+    /**
+     * Returns how many rules this driver can delete at once.
+     *
+     * @param deviceId the device's ID to delete rules from
+     * @return rule deletion batch size
+     */
+    private int getRuleDeleteBatchSizeProperty(DeviceId deviceId) {
+        Driver driver = getHandler().get(DriverService.class).getDriver(deviceId);
+        return Integer.parseInt(driver.getProperty(RULE_DELETE_BATCH_SIZE_PROPERTY));
+    }
+
 }
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 10203a3..3ff7f05 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
@@ -110,8 +110,8 @@
     private static final String PARAM_HW_VENDOR        = "hwVersion";
     private static final String PARAM_SW_VENDOR        = "swVersion";
     private static final String PARAM_SERIAL           = "serial";
-    private static final String PARAM_TIMING_STATS     = "timing_stats";
-    private static final String PARAM_TIMING_AUTOSCALE = "autoscale_timing_stats";
+    private static final String PARAM_TIMING_STATS     = "timingStats";
+    private static final String PARAM_TIMING_AUTOSCALE = "autoScaleTimingStats";
 
     private static final String NIC_PARAM_NAME             = "name";
     private static final String NIC_PARAM_PORT_INDEX       = "index";
@@ -137,20 +137,28 @@
     /**
      * CPU statistics.
      */
-    private static final String CPU_PARAM_ID        = "id";
-    private static final String CPU_PARAM_VENDOR    = "vendor";
-    private static final String CPU_PARAM_FREQUENCY = "frequency";
-    private static final String CPU_PARAM_LOAD      = "load";
-    private static final String CPU_PARAM_STATUS    = "busy";
-    private static final String CPU_STATS_BUSY_CPUS = "busyCpus";
-    private static final String CPU_STATS_FREE_CPUS = "freeCpus";
+    private static final String CPU_PARAM_ID         = "id";
+    private static final String CPU_PARAM_VENDOR     = "vendor";
+    private static final String CPU_PARAM_FREQUENCY  = "frequency";
+    private static final String CPU_PARAM_LOAD       = "load";
+    private static final String CPU_PARAM_QUEUE      = "queue";
+    private static final String CPU_PARAM_STATUS     = "busy";
+    private static final String CPU_PARAM_THROUGHPUT = "throughput";
+    private static final String CPU_PARAM_LATENCY    = "latency";
+    private static final String MON_PARAM_UNIT       = "unit";
+    private static final String MON_PARAM_BUSY_CPUS  = "busyCpus";
+    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_MAX        = "max";
 
     /**
      * Timing statistics.
      */
-    private static final String TIMING_PARAM_PARSE     = "parse";
-    private static final String TIMING_PARAM_LAUNCH    = "launch";
-    private static final String TIMING_PARAM_AUTOSCALE = "autoscale";
+    private static final String TIMING_PARAM_PARSE     = "parseTime";
+    private static final String TIMING_PARAM_LAUNCH    = "launchTime";
+    private static final String TIMING_PARAM_DEPLOY    = "deployTime";
+    private static final String TIMING_PARAM_AUTOSCALE = "autoScaleTime";
 
     /**
      * Auxiliary constants.
@@ -344,12 +352,15 @@
         RestServerSBDevice dev = new DefaultRestServerSBDevice(
             device.ip(), device.port(), device.username(),
             device.password(), device.protocol(), device.url(),
-            device.isActive(), device.testUrl().toString(),
+            device.isActive(), device.testUrl().orElse(""),
             vendor, hw, sw, AuthenticationScheme.BASIC, "",
             cpuSet, nicSet
         );
         checkNotNull(dev, DEVICE_NULL);
 
+        // Set alive
+        raiseDeviceReconnect(dev);
+
         // Updates the controller with the complete device information
         getController().removeDevice(deviceId);
         getController().addDevice((RestSBDevice) dev);
@@ -512,7 +523,7 @@
      * @param deviceId the device ID to be queried
      * @return global monitoring statistics
      */
-     private MonitoringStatistics getGlobalMonitoringStatistics(DeviceId deviceId) {
+     public MonitoringStatistics getGlobalMonitoringStatistics(DeviceId deviceId) {
         // Monitoring statistics to return
         MonitoringStatistics monStats = null;
 
@@ -562,8 +573,8 @@
         }
 
         // Get high-level CPU statistics
-        int busyCpus = objNode.path(CPU_STATS_BUSY_CPUS).asInt();
-        int freeCpus = objNode.path(CPU_STATS_FREE_CPUS).asInt();
+        int busyCpus = objNode.path(MON_PARAM_BUSY_CPUS).asInt();
+        int freeCpus = objNode.path(MON_PARAM_FREE_CPUS).asInt();
 
         // Get a list of CPU statistics per core
         Collection<CpuStatistics> cpuStats = parseCpuStatistics(deviceId, objNode);
@@ -581,11 +592,13 @@
         statsBuilder.setDeviceId(deviceId)
                 .setTimingStatistics(timinsgStats)
                 .setCpuStatistics(cpuStats)
-                .setNicStatistics(nicStats)
-                .build();
+                .setNicStatistics(nicStats);
 
         monStats = statsBuilder.build();
 
+        // When a device reports monitoring data, it means it is alive
+        raiseDeviceReconnect(device);
+
         log.debug("Global monitoring statistics: {}", monStats.toString());
 
         return monStats;
@@ -689,11 +702,13 @@
         statsBuilder.setDeviceId(deviceId)
                 .setTimingStatistics(timinsgStats)
                 .setCpuStatistics(cpuStats)
-                .setNicStatistics(nicStats)
-                .build();
+                .setNicStatistics(nicStats);
 
         monStats = statsBuilder.build();
 
+        // When a device reports monitoring data, it means it is alive
+        raiseDeviceReconnect(device);
+
         log.debug("Monitoring statistics: {}", monStats.toString());
 
         return monStats;
@@ -720,22 +735,62 @@
         for (JsonNode cn : cpuNode) {
             ObjectNode cpuObjNode = (ObjectNode) cn;
 
+            // CPU statistics builder
+            DefaultCpuStatistics.Builder cpuBuilder = DefaultCpuStatistics.builder();
+
+            // Throughput statistics are optional
+            JsonNode throughputNode = cpuObjNode.get(CPU_PARAM_THROUGHPUT);
+            if (throughputNode != null) {
+                String throughputUnit = get(throughputNode, MON_PARAM_UNIT);
+                if (!Strings.isNullOrEmpty(throughputUnit)) {
+                    cpuBuilder.setThroughputUnit(throughputUnit);
+                }
+                float averageThroughput = (float) 0;
+                if (throughputNode.get(MON_PARAM_AVERAGE) != null) {
+                    averageThroughput = throughputNode.path(MON_PARAM_AVERAGE).floatValue();
+                }
+                cpuBuilder.setAverageThroughput(averageThroughput);
+            }
+
+            // Latency statistics are optional
+            JsonNode latencyNode = cpuObjNode.get(CPU_PARAM_LATENCY);
+            if (latencyNode != null) {
+                String latencyUnit = get(latencyNode, MON_PARAM_UNIT);
+                if (!Strings.isNullOrEmpty(latencyUnit)) {
+                    cpuBuilder.setLatencyUnit(latencyUnit);
+                }
+                float minLatency = (float) 0;
+                if (latencyNode.get(MON_PARAM_MIN) != null) {
+                    minLatency = latencyNode.path(MON_PARAM_MIN).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) {
+                    maxLatency = latencyNode.path(MON_PARAM_MAX).floatValue();
+                }
+
+                cpuBuilder.setMinLatency(minLatency)
+                    .setAverageLatency(averageLatency)
+                    .setMaxLatency(maxLatency);
+            }
+
             // CPU ID with its load and status
-            int   cpuId    = cpuObjNode.path(CPU_PARAM_ID).asInt();
-            float cpuLoad  = cpuObjNode.path(CPU_PARAM_LOAD).floatValue();
-            boolean isBusy = cpuObjNode.path(CPU_PARAM_STATUS).booleanValue();
+            int cpuId = cpuObjNode.path(CPU_PARAM_ID).asInt();
+            float cpuLoad = cpuObjNode.path(CPU_PARAM_LOAD).floatValue();
+            int queueId = cpuObjNode.path(CPU_PARAM_QUEUE).asInt();
+            int busySince = cpuObjNode.path(CPU_PARAM_STATUS).asInt();
 
-            // Incorporate these statistics into an object
-            DefaultCpuStatistics.Builder cpuBuilder =
-                DefaultCpuStatistics.builder();
-
+            // This is mandatory information
             cpuBuilder.setDeviceId(deviceId)
                     .setId(cpuId)
                     .setLoad(cpuLoad)
-                    .setIsBusy(isBusy)
-                    .build();
+                    .setQueue(queueId)
+                    .setBusySince(busySince);
 
-            // We have statistics for this CPU core
+            // We have all the statistics for this CPU core
             cpuStats.add(cpuBuilder.build());
         }
 
@@ -790,8 +845,7 @@
             long txErrors  = nicObjNode.path(NIC_STATS_TX_ERRORS).asLong();
 
             // Incorporate these statistics into an object
-            DefaultPortStatistics.Builder nicBuilder =
-                DefaultPortStatistics.builder();
+            DefaultPortStatistics.Builder nicBuilder = DefaultPortStatistics.builder();
 
             nicBuilder.setDeviceId(deviceId)
                     .setPort((int) portNumber)
@@ -802,8 +856,7 @@
                     .setPacketsRxDropped(rxDropped)
                     .setPacketsRxErrors(rxErrors)
                     .setPacketsTxDropped(txDropped)
-                    .setPacketsTxErrors(txErrors)
-                    .build();
+                    .setPacketsTxErrors(txErrors);
 
             // We have statistics for this NIC
             nicStats.add(nicBuilder.build());
@@ -813,9 +866,9 @@
     }
 
     /**
-     * Parse the input JSON object, looking for timing-related
-     * statistics. Upon success, construct and return a
-     * timing statistics object.
+     * Parse the input JSON object, looking for timing-related statistics.
+     * Upon success, return a timing statistics object with the advertized values.
+     * Upon failure, return a timing statistics object with zero-initialized values.
      *
      * @param objNode input JSON node with timing statistics information
      * @return TimingStatistics object or null
@@ -827,33 +880,56 @@
             return timinsgStats;
         }
 
+        // If no timing statistics are present, then send zeros
+        if (objNode.get(PARAM_TIMING_STATS) == null) {
+            return getZeroTimingStatistics();
+        }
+
+        DefaultTimingStatistics.Builder timingBuilder = DefaultTimingStatistics.builder();
+
         // Get timing statistics
         JsonNode timingNode = objNode.path(PARAM_TIMING_STATS);
         ObjectNode timingObjNode = (ObjectNode) timingNode;
 
+        // The unit of timing statistics
+        String timingStatsUnit = get(timingNode, MON_PARAM_UNIT);
+        if (!Strings.isNullOrEmpty(timingStatsUnit)) {
+            timingBuilder.setUnit(timingStatsUnit);
+        }
+
         // Time (ns) to parse the controller's deployment instruction
-        long parsingTime = timingObjNode.path(TIMING_PARAM_PARSE).asLong();
+        long parsingTime = 0;
+        if (timingObjNode.get(TIMING_PARAM_PARSE) != null) {
+            parsingTime = timingObjNode.path(TIMING_PARAM_PARSE).asLong();
+        }
         // Time (ns) to do the deployment
-        long launchingTime = timingObjNode.path(TIMING_PARAM_LAUNCH).asLong();
-        // Total time (ns)
-        long totalTime = parsingTime + launchingTime;
+        long launchingTime = 0;
+        if (timingObjNode.get(TIMING_PARAM_LAUNCH) != null) {
+            launchingTime = timingObjNode.path(TIMING_PARAM_LAUNCH).asLong();
+        }
+        // Deployment time (ns) equals to time to parse + time to launch
+        long deployTime = 0;
+        if (timingObjNode.get(TIMING_PARAM_DEPLOY) != null) {
+            deployTime = timingObjNode.path(TIMING_PARAM_DEPLOY).asLong();
+        }
+        checkArgument(deployTime == parsingTime + launchingTime, "Inconsistent timing statistics");
+
+        timingBuilder.setParsingTime(parsingTime)
+                    .setLaunchingTime(launchingTime);
 
         // Get autoscale timing statistics
         JsonNode autoscaleTimingNode = objNode.path(PARAM_TIMING_AUTOSCALE);
-        ObjectNode autoscaleTimingObjNode = (ObjectNode) autoscaleTimingNode;
+        if (autoscaleTimingNode == null) {
+            return timingBuilder.build();
+        }
 
+        ObjectNode autoScaleTimingObjNode = (ObjectNode) autoscaleTimingNode;
         // Time (ns) to autoscale a server's load
-        long autoscaleTime = autoscaleTimingObjNode.path(
-            TIMING_PARAM_AUTOSCALE
-        ).asLong();
-
-        DefaultTimingStatistics.Builder timingBuilder =
-            DefaultTimingStatistics.builder();
-
-        timingBuilder.setParsingTime(parsingTime)
-                    .setLaunchingTime(launchingTime)
-                    .setAutoscaleTime(autoscaleTime)
-                    .build();
+        long autoScaleTime = 0;
+        if (autoScaleTimingObjNode.get(TIMING_PARAM_AUTOSCALE) != null) {
+            autoScaleTime = autoScaleTimingObjNode.path(TIMING_PARAM_AUTOSCALE).asLong();
+        }
+        timingBuilder.setAutoScaleTime(autoScaleTime);
 
         return timingBuilder.build();
     }
@@ -866,13 +942,11 @@
      * @return TimingStatistics object
      */
     private TimingStatistics getZeroTimingStatistics() {
-        DefaultTimingStatistics.Builder zeroTimingBuilder =
-            DefaultTimingStatistics.builder();
+        DefaultTimingStatistics.Builder zeroTimingBuilder = DefaultTimingStatistics.builder();
 
         zeroTimingBuilder.setParsingTime(0)
                          .setLaunchingTime(0)
-                         .setAutoscaleTime(0)
-                         .build();
+                         .setAutoScaleTime(0);
 
         return zeroTimingBuilder.build();
     }
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/DefaultDpdkNicFlowRule.java b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/DefaultDpdkNicFlowRule.java
index 223f5c3..4c2b7f0 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/DefaultDpdkNicFlowRule.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/DefaultDpdkNicFlowRule.java
@@ -18,7 +18,6 @@
 
 import org.onosproject.net.flow.FlowRule;
 
-import org.onlab.packet.Ip4Address;
 import org.onlab.packet.MacAddress;
 
 /**
@@ -76,26 +75,25 @@
             rule += "ipv4 ";
 
             if (this.ipv4Protocol() > 0) {
-                rule += "proto spec " + Integer.toString(this.ipv4Protocol()) + " ";
-                rule += "proto mask 0x0 ";
+                rule += "proto is " + Integer.toString(this.ipv4Protocol()) + " ";
             }
 
             if (this.ipv4SrcAddress() != null) {
-                rule += "src is " + this.ipv4SrcAddress().toString() + " ";
-            }
-
-            if (this.ipv4SrcMask() != null) {
-                rule += "src spec " + this.ipv4SrcMask().address().getIp4Address().toString() + " ";
-                rule += "src mask " + Ip4Address.makeMaskPrefix(this.ipv4SrcMask().prefixLength()).toString() + " ";
+                if ((this.ipv4SrcMask() != null) && (this.ipv4SrcMask().prefixLength() < 32)) {
+                    rule += "src spec " + this.ipv4SrcAddress().toString() + " ";
+                    rule += "src prefix " + this.ipv4SrcMask().prefixLength() + " ";
+                } else {
+                    rule += "src is " + this.ipv4SrcAddress().toString() + " ";
+                }
             }
 
             if (this.ipv4DstAddress() != null) {
-                rule += "dst is " + this.ipv4DstAddress().toString() + " ";
-            }
-
-            if (this.ipv4DstMask() != null) {
-                rule += "dst spec " + this.ipv4DstMask().address().getIp4Address().toString() + " ";
-                rule += "dst mask " + Ip4Address.makeMaskPrefix(this.ipv4DstMask().prefixLength()).toString() + " ";
+                if ((this.ipv4DstMask() != null) && (this.ipv4DstMask().prefixLength() < 32)) {
+                    rule += "dst spec " + this.ipv4DstAddress().toString() + " ";
+                    rule += "dst prefix " + this.ipv4DstMask().prefixLength() + " ";
+                } else {
+                    rule += "dst is " + this.ipv4DstAddress().toString() + " ";
+                }
             }
 
             rule += "/ ";
@@ -130,15 +128,16 @@
 
                 // No subsequent field
                 if (action.actionField().isEmpty()) {
+                    rule += "/ ";
                     continue;
                 }
 
                 // A subsequent field is associated with a value
                 rule += action.actionField() + " ";
-                rule += Long.toString(action.actionValue()) + " ";
+                rule += Long.toString(action.actionValue()) + " / ";
             }
 
-            rule += "/ end";
+            rule += " end";
         }
 
         return rule;
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/DefaultNicFlowRule.java b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/DefaultNicFlowRule.java
index be89997..8f50986 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/DefaultNicFlowRule.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/DefaultNicFlowRule.java
@@ -144,9 +144,8 @@
         this.ipv4ProtoCriterion = (IPProtocolCriterion) this.selector().getCriterion(IP_PROTO);
         this.ipv4SrcAddrCriterion = (IPCriterion) this.selector().getCriterion(IPV4_SRC);
         this.ipv4DstAddrCriterion = (IPCriterion) this.selector().getCriterion(IPV4_DST);
-        // Is there a criterion for IP masks?
-        this.ipv4SrcMaskCriterion = (IPCriterion) null;
-        this.ipv4DstMaskCriterion = (IPCriterion) null;
+        this.ipv4SrcMaskCriterion = (IPCriterion) this.selector().getCriterion(IPV4_SRC);
+        this.ipv4DstMaskCriterion = (IPCriterion) this.selector().getCriterion(IPV4_DST);
         this.udpSrcPortCriterion = (UdpPortCriterion) this.selector().getCriterion(UDP_SRC);
         this.udpDstPortCriterion = (UdpPortCriterion) this.selector().getCriterion(UDP_DST);
         this.tcpSrcPortCriterion = (TcpPortCriterion) this.selector().getCriterion(TCP_SRC);
@@ -168,6 +167,9 @@
                     new NicRuleAction(NicRuleAction.Action.METER, meterInstruction.meterId().id()));
             }
         }
+
+        // This action provides basic rule match counters
+        // this.actions.add(new NicRuleAction(NicRuleAction.Action.COUNT));
     }
 
     @Override
@@ -305,6 +307,16 @@
     }
 
     @Override
+    public boolean isFullWildcard() {
+        if (((ipv4SrcAddress() != null) && !ipv4SrcAddress().isZero()) ||
+            ((ipv4DstAddress() != null) && !ipv4DstAddress().isZero()) ||
+            (ipv4Protocol() > 0) || (sourcePort() > 0) || (destinationPort() > 0)) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
     public Set<NicRuleAction> actions() {
         return actions;
     }
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/FlowRxFilterValue.java b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/FlowRxFilterValue.java
index 64c792b..c2430c0 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/FlowRxFilterValue.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/FlowRxFilterValue.java
@@ -26,26 +26,29 @@
 public final class FlowRxFilterValue extends RxFilterValue
         implements Comparable {
 
-    private long cpuCoreId;
+    private long value;
     private String flowRule;
 
     /**
      * Constructs a flow-based Rx filter.
+     *
+     * @param cpuId CPU ID of the server this tag will lead to
      */
-    public FlowRxFilterValue() {
-        super();
+    public FlowRxFilterValue(int cpuId) {
+        super(cpuId);
         setValue(0);
         setRule("");
     }
 
     /**
-     * Constructs a flow-based Rx filter with CPU core ID.
+     * Constructs a flow-based Rx filter with physical CPU core ID.
      *
-     * @param cpuCoreId a CPU core ID when the flow ends up
+     * @param value Flow tag
+     * @param cpuId CPU ID of the server this tag will lead to
      */
-    public FlowRxFilterValue(long cpuCoreId) {
-        super();
-        setValue(cpuCoreId);
+    public FlowRxFilterValue(long value, int cpuId) {
+        super(cpuId);
+        setValue(value);
         setRule("");
     }
 
@@ -53,12 +56,13 @@
      * Constructs a flow-based Rx filter with CPU core ID
      * and an associated rule.
      *
-     * @param cpuCoreId a CPU core ID
+     * @param value Flow tag
+     * @param cpuId CPU ID of the server this tag will lead to
      * @param flowRule a flow rule as a string
      */
-    public FlowRxFilterValue(long cpuCoreId, String flowRule) {
-        super();
-        setValue(cpuCoreId);
+    public FlowRxFilterValue(long value, int cpuId, String flowRule) {
+        super(cpuId);
+        setValue(value);
         setRule(flowRule);
     }
 
@@ -68,7 +72,7 @@
      * @param other a source FlowRxFilterValue object
      */
     public FlowRxFilterValue(FlowRxFilterValue other) {
-        super();
+        super(other.cpuId);
         setValue(other.value());
         setRule(other.rule());
     }
@@ -79,18 +83,18 @@
      * @return Flow Rx filter value
      */
     public long value() {
-        return this.cpuCoreId;
+        return this.value;
     }
 
     /**
      * Sets the value of this Rx filter.
      *
-     * @param cpuCoreId a CPU core ID for this Rx filter
+     * @param value a CPU core ID for this Rx filter
      */
-    public void setValue(long cpuCoreId) {
-        checkArgument(cpuCoreId >= 0,
+    private void setValue(long value) {
+        checkArgument(value >= 0,
             "NIC flow Rx filter has invalid CPU core ID");
-        this.cpuCoreId = cpuCoreId;
+        this.value = value;
     }
 
     /**
@@ -115,7 +119,7 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(this.cpuCoreId, this.flowRule);
+        return Objects.hash(this.value, this.flowRule, this.cpuId);
     }
 
     @Override
@@ -130,8 +134,8 @@
 
         FlowRxFilterValue other = (FlowRxFilterValue) obj;
 
-        return (this.value() == other.value()) &&
-                this.rule().equals(other.rule());
+        return this.value() == other.value() &&
+                this.rule().equals(other.rule()) && ((RxFilterValue) this).equals(other);
     }
 
     @Override
@@ -147,15 +151,15 @@
         if (other instanceof FlowRxFilterValue) {
             FlowRxFilterValue otherRxVal = (FlowRxFilterValue) other;
 
-            long thisCoreId  = this.value();
-            long otherCoreId = otherRxVal.value();
+            long thisCpuId  = this.value();
+            long otherCpuId = otherRxVal.value();
 
-            if (thisCoreId > otherCoreId) {
+            if (thisCpuId > otherCpuId) {
                 return 1;
-            } else if (thisCoreId < otherCoreId) {
+            } else if (thisCpuId < otherCpuId) {
                 return -1;
             } else {
-                return 0;
+                return this.cpuId - otherRxVal.cpuId;
             }
         }
 
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/MacRxFilterValue.java b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/MacRxFilterValue.java
index 2676f65..25b28bb 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/MacRxFilterValue.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/MacRxFilterValue.java
@@ -30,9 +30,10 @@
 
     /**
      * Constructs a MAC-based Rx filter.
+     * @param cpuId CPU ID of the server this tag will lead to
      */
-    public MacRxFilterValue() {
-        super();
+    public MacRxFilterValue(int cpuId) {
+        super(cpuId);
         this.mac = null;
     }
 
@@ -40,9 +41,10 @@
      * Constructs a MAC-based Rx filter with specific MAC address.
      *
      * @param mac a MAC address to use as a filter
+     * @param cpuId CPU ID of the server this tag will lead to
      */
-    public MacRxFilterValue(MacAddress mac) {
-        super();
+    public MacRxFilterValue(MacAddress mac, int cpuId) {
+        super(cpuId);
         setValue(mac);
     }
 
@@ -52,7 +54,7 @@
      * @param other a source MacRxFilterValue object
      */
     public MacRxFilterValue(MacRxFilterValue other) {
-        super();
+        super(other.cpuId);
         setValue(other.value());
     }
 
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/MplsRxFilterValue.java b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/MplsRxFilterValue.java
index 2b5112f..ddc1930c 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/MplsRxFilterValue.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/MplsRxFilterValue.java
@@ -30,9 +30,11 @@
 
     /**
      * Constructs an MPLS-based Rx filter.
+     *
+     * @param cpuId CPU ID of the server this tag will lead to
      */
-    public MplsRxFilterValue() {
-        super();
+    public MplsRxFilterValue(int cpuId) {
+        super(cpuId);
         this.mplsLabel = null;
     }
 
@@ -40,9 +42,10 @@
      * Constructs an MPLS-based Rx filter with specific label.
      *
      * @param mplsLabel an MPLS label to use as a filter
+     * @param cpuId CPU ID of the server this tag will lead to
      */
-    public MplsRxFilterValue(MplsLabel mplsLabel) {
-        super();
+    public MplsRxFilterValue(MplsLabel mplsLabel, int cpuId) {
+        super(cpuId);
         setValue(mplsLabel);
     }
 
@@ -52,7 +55,7 @@
      * @param other a source MplsRxFilterValue object
      */
     public MplsRxFilterValue(MplsRxFilterValue other) {
-        super();
+        super(other.cpuId);
         setValue(other.value());
     }
 
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/NicFlowRule.java b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/NicFlowRule.java
index 1215b6f..9083395 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/NicFlowRule.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/NicFlowRule.java
@@ -206,6 +206,13 @@
     boolean hasTransport();
 
     /**
+     * Returns whether this rule is a full wildcard or not.
+     *
+     * @return boolean full wildcard status
+     */
+    boolean isFullWildcard();
+
+    /**
      * Returns the set of actions of this rule.
      *
      * @return rule's set of actions
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/NicRuleAction.java b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/NicRuleAction.java
index 3ec13c3..ff3afeb 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/NicRuleAction.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/NicRuleAction.java
@@ -196,7 +196,7 @@
         ACTION_FIELD.put(Action.JUMP, "group");
         ACTION_FIELD.put(Action.MARK, "id");
         ACTION_FIELD.put(Action.FLAG, "");
-        ACTION_FIELD.put(Action.COUNT, "id");
+        ACTION_FIELD.put(Action.COUNT, "");
         ACTION_FIELD.put(Action.QUEUE, "index");
         ACTION_FIELD.put(Action.RSS, "queue");
         ACTION_FIELD.put(Action.PF, "");
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/RssRxFilterValue.java b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/RssRxFilterValue.java
index 08f2a46..2d54a81 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/RssRxFilterValue.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/RssRxFilterValue.java
@@ -28,9 +28,11 @@
 
     /**
      * Constructs an RSS-based Rx filter.
+     *
+     * @param cpuId CPU ID of the server this tag will lead to
      */
-    public RssRxFilterValue() {
-        super();
+    public RssRxFilterValue(int cpuId) {
+        super(cpuId);
         setValue(0);
     }
 
@@ -38,9 +40,10 @@
      * Constructs an RSS-based Rx filter with specific hash.
      *
      * @param rssHash a hash value
+     * @param cpuId CPU ID of the server this tag will lead to
      */
-    public RssRxFilterValue(int rssHash) {
-        super();
+    public RssRxFilterValue(int rssHash, int cpuId) {
+        super(cpuId);
         setValue(rssHash);
     }
 
@@ -50,7 +53,7 @@
      * @param other a source RssRxFilterValue object
      */
     public RssRxFilterValue(RssRxFilterValue other) {
-        super();
+        super(other.cpuId);
         setValue(other.value());
     }
 
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/RxFilterValue.java b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/RxFilterValue.java
index 10ebe04..dceff82 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/RxFilterValue.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/RxFilterValue.java
@@ -15,12 +15,50 @@
  */
 package org.onosproject.drivers.server.devices.nic;
 
+import java.util.Objects;
+
 /**
  * The base class that holds the value of a NIC's Rx filter.
  */
 public abstract class RxFilterValue {
 
-    public RxFilterValue() {
+    /* CPU id of the server this tag will lead to */
+    protected int cpuId;
+
+    /**
+     * Constructs an Rx filter value.
+     *
+     * @param cpuId CPU ID of the server this tag will lead to
+     */
+    public RxFilterValue(int cpuId) {
+        this.cpuId = cpuId;
+    }
+
+    /**
+     * Returns the CPU ID that corresponds to this Rx filter value.
+     *
+     * @return CPU ID of the server this tag will lead to
+     */
+    public int cpuId() {
+        return this.cpuId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.cpuId);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if ((obj == null) || (!(obj instanceof RxFilterValue))) {
+            return false;
+        }
+
+        return cpuId == ((RxFilterValue) obj).cpuId;
     }
 
 }
diff --git a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/VlanRxFilterValue.java b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/VlanRxFilterValue.java
index 18a99be..8e16321 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/VlanRxFilterValue.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/devices/nic/VlanRxFilterValue.java
@@ -30,9 +30,10 @@
 
     /**
      * Constructs a VLAN-based Rx filter.
+     * @param cpuId CPU id of the server this tag will lead to
      */
-    public VlanRxFilterValue() {
-        super();
+    public VlanRxFilterValue(int cpuId) {
+        super(cpuId);
         this.vlanId = VlanId.NONE;
     }
 
@@ -40,9 +41,10 @@
      * Constructs a VLAN-based Rx filter with specific ID.
      *
      * @param vlanId a VLAN ID to use as a filter
+     * @param cpuId CPU id of the server this tag will lead to
      */
-    public VlanRxFilterValue(VlanId vlanId) {
-        super();
+    public VlanRxFilterValue(VlanId vlanId, int cpuId) {
+        super(cpuId);
         setValue(vlanId);
     }
 
@@ -52,7 +54,7 @@
      * @param other a source VlanRxFilterValue object
      */
     public VlanRxFilterValue(VlanRxFilterValue other) {
-        super();
+        super(other.cpuId);
         setValue(other.value());
     }
 
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/devices/DefaultRestServerSBDevice.java b/drivers/server/src/main/java/org/onosproject/drivers/server/impl/devices/DefaultRestServerSBDevice.java
index cc2e541..b43c2ec 100644
--- a/drivers/server/src/main/java/org/onosproject/drivers/server/impl/devices/DefaultRestServerSBDevice.java
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/impl/devices/DefaultRestServerSBDevice.java
@@ -126,11 +126,11 @@
         return MoreObjects.toStringHelper(this)
                 .omitNullValues()
                 .add("url", url())
-                .add("testUrl", testUrl())
                 .add("protocol", protocol())
                 .add("username", username())
-                .add("port", port())
                 .add("ip", ip())
+                .add("port", port())
+                .add("testUrl", testUrl().orElse(null))
                 .add("manufacturer", manufacturer().orElse(null))
                 .add("hwVersion", hwVersion().orElse(null))
                 .add("swVersion", swVersion().orElse(null))
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 f9c1989..f2b6968 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
@@ -17,10 +17,15 @@
 package org.onosproject.drivers.server.impl.stats;
 
 import org.onosproject.drivers.server.stats.CpuStatistics;
+import org.onosproject.drivers.server.stats.MonitoringUnit;
 
 import org.onosproject.net.DeviceId;
 import com.google.common.base.MoreObjects;
 
+import java.util.Optional;
+
+import static org.onosproject.drivers.server.stats.MonitoringUnit.LatencyUnit;
+import static org.onosproject.drivers.server.stats.MonitoringUnit.ThroughputUnit;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkArgument;
 
@@ -32,6 +37,9 @@
     private static final float MIN_CPU_LOAD = (float) 0.0;
     private static final float MAX_CPU_LOAD = (float) 1.0;
 
+    private static final LatencyUnit DEF_LATENCY_UNIT = LatencyUnit.NANO_SECOND;
+    private static final ThroughputUnit DEF_THROUGHPUT_UNIT = ThroughputUnit.MBPS;
+
     // Upper limit of CPU cores in one machine
     public static final int MAX_CPU_NB = 512;
 
@@ -39,35 +47,62 @@
 
     private final int id;
     private final float load;
-    private final boolean isBusy;
+    private final int queue;
+    private final int busySince;
+    private final Optional<MonitoringUnit> throughputUnit;
+    private final Optional<Float> averageThroughput;
+    private final Optional<MonitoringUnit> latencyUnit;
+    private final Optional<Float> minLatency;
+    private final Optional<Float> averageLatency;
+    private final Optional<Float> maxLatency;
 
-    private DefaultCpuStatistics(
-            DeviceId deviceId,
-            int      id,
-            float    load,
-            boolean  isBusy) {
+    private DefaultCpuStatistics(DeviceId deviceId, int id, float load, int queue, int busySince) {
+        this(deviceId, id, load, queue, busySince, null, -1, null, -1, -1, -1);
+    }
+
+    private DefaultCpuStatistics(DeviceId deviceId, int id, float load, int queue, int busySince,
+            MonitoringUnit throughputUnit, float averageThroughput, MonitoringUnit latencyUnit,
+            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) + "]"
-        );
-        checkArgument(
-            (load >= MIN_CPU_LOAD) && (load <= MAX_CPU_LOAD),
-            "Invalid CPU load " + Float.toString(load) + ", not in [" + MIN_CPU_LOAD + ", " + MAX_CPU_LOAD + "]"
-        );
+        checkArgument((id >= 0) && (id < MAX_CPU_NB),
+            "Invalid CPU core ID " + String.valueOf(id) + ", not in [0, " + String.valueOf(MAX_CPU_NB - 1) + "]");
+        checkArgument((load >= MIN_CPU_LOAD) && (load <= MAX_CPU_LOAD),
+            "Invalid CPU load " + Float.toString(load) + ", not in [" + MIN_CPU_LOAD + ", " + MAX_CPU_LOAD + "]");
 
-        this.deviceId = deviceId;
-        this.id       = id;
-        this.load     = load;
-        this.isBusy   = isBusy;
+        this.deviceId  = deviceId;
+        this.id        = id;
+        this.load      = load;
+        this.queue     = queue;
+        this.busySince = busySince;
+
+        this.throughputUnit = (throughputUnit == null) ?
+                Optional.empty() : Optional.ofNullable(throughputUnit);
+        this.averageThroughput = (averageThroughput < 0) ?
+                Optional.empty() : Optional.ofNullable(averageThroughput);
+        this.latencyUnit = (latencyUnit == null) ?
+                Optional.empty() : Optional.ofNullable(latencyUnit);
+        this.minLatency = (minLatency < 0) ?
+                Optional.empty() : Optional.ofNullable(minLatency);
+        this.averageLatency = (averageLatency < 0) ?
+                Optional.empty() : Optional.ofNullable(averageLatency);
+        this.maxLatency = (maxLatency < 0) ?
+                Optional.empty() : Optional.ofNullable(maxLatency);
     }
 
     // Constructor for serializer
     private DefaultCpuStatistics() {
-        this.deviceId = null;
-        this.id       = 0;
-        this.load     = 0;
-        this.isBusy   = false;
+        this.deviceId  = null;
+        this.id        = 0;
+        this.load      = 0;
+        this.queue     = 0;
+        this.busySince = -1;
+
+        this.throughputUnit = null;
+        this.averageThroughput = null;
+        this.latencyUnit = null;
+        this.minLatency = null;
+        this.averageLatency = null;
+        this.maxLatency = null;
     }
 
     /**
@@ -90,8 +125,48 @@
     }
 
     @Override
+    public int queue() {
+        return this.queue;
+    }
+
+    @Override
     public boolean busy() {
-        return this.isBusy;
+        return this.busySince >= 0;
+    }
+
+    @Override
+    public int busySince() {
+        return this.busySince;
+    }
+
+    @Override
+    public Optional<MonitoringUnit> throughputUnit() {
+        return this.throughputUnit;
+    }
+
+    @Override
+    public Optional<Float> averageThroughput() {
+        return this.averageThroughput;
+    }
+
+    @Override
+    public Optional<MonitoringUnit> latencyUnit() {
+        return this.latencyUnit;
+    }
+
+    @Override
+    public Optional<Float> minLatency() {
+        return this.minLatency;
+    }
+
+    @Override
+    public Optional<Float> averageLatency() {
+        return this.averageLatency;
+    }
+
+    @Override
+    public Optional<Float> maxLatency() {
+        return this.maxLatency;
     }
 
     @Override
@@ -101,7 +176,14 @@
                 .add("device", deviceId)
                 .add("id",     id())
                 .add("load",   load())
-                .add("isBusy", busy())
+                .add("queue",  queue())
+                .add("busySince", busySince())
+                .add("throughputUnit", throughputUnit.orElse(null))
+                .add("averageThroughput", averageThroughput.orElse(null))
+                .add("latencyUnit", latencyUnit.orElse(null))
+                .add("minLatency", minLatency.orElse(null))
+                .add("averageLatency", averageLatency.orElse(null))
+                .add("maxLatency", maxLatency.orElse(null))
                 .toString();
     }
 
@@ -109,8 +191,16 @@
 
         DeviceId deviceId;
         int      id;
-        float    load;
-        boolean  isBusy;
+        float    load = 0;
+        int      queue = -1;
+        int      busySince = -1;
+
+        MonitoringUnit throughputUnit = DEF_THROUGHPUT_UNIT;
+        float averageThroughput = -1;
+        MonitoringUnit latencyUnit = DEF_LATENCY_UNIT;
+        float minLatency = -1;
+        float averageLatency = -1;
+        float maxLatency = -1;
 
         private Builder() {
 
@@ -131,7 +221,7 @@
         /**
          * Sets the CPU ID.
          *
-         * @param id the CPU ID
+         * @param id CPU ID
          * @return builder object
          */
         public Builder setId(int id) {
@@ -153,13 +243,97 @@
         }
 
         /**
-         * Sets the CPU status (free or busy).
+         * Sets the hardware queue ID associated with this core.
          *
-         * @param isBusy CPU status
+         * @param queue hardware queue ID
          * @return builder object
          */
-        public Builder setIsBusy(boolean isBusy) {
-            this.isBusy = isBusy;
+        public Builder setQueue(int queue) {
+            this.queue = queue;
+
+            return this;
+        }
+
+        /**
+         * Sets the CPU status (free or busy since some ms).
+         *
+         * @param busySince CPU busy time in ms, -1 if not busy
+         * @return builder object
+         */
+        public Builder setBusySince(int busySince) {
+            this.busySince = busySince;
+
+            return this;
+        }
+
+        /**
+         * Sets the throughput unit.
+         *
+         * @param throughputUnitStr throughput unit as a string
+         * @return builder object
+         */
+        public Builder setThroughputUnit(String throughputUnitStr) {
+            this.throughputUnit = ThroughputUnit.getByName(throughputUnitStr);
+
+            return this;
+        }
+
+        /**
+         * Sets the average throughput.
+         *
+         * @param averageThroughput average throughput
+         * @return builder object
+         */
+        public Builder setAverageThroughput(float averageThroughput) {
+            this.averageThroughput = averageThroughput;
+
+            return this;
+        }
+
+        /**
+         * Sets the latency unit.
+         *
+         * @param latencyUnitStr latency unit as a string
+         * @return builder object
+         */
+        public Builder setLatencyUnit(String latencyUnitStr) {
+            this.latencyUnit = LatencyUnit.getByName(latencyUnitStr);
+
+            return this;
+        }
+
+        /**
+         * Sets the minimum latency.
+         *
+         * @param minLatency minimum latency
+         * @return builder object
+         */
+        public Builder setMinLatency(float minLatency) {
+            this.minLatency = minLatency;
+
+            return this;
+        }
+
+        /**
+         * Sets the average latency.
+         *
+         * @param averageLatency average latency
+         * @return builder object
+         */
+        public Builder setAverageLatency(float averageLatency) {
+            this.averageLatency = averageLatency;
+
+            return this;
+        }
+
+        /**
+         * Sets the maximum latency.
+         *
+         * @param maxLatency maximum latency
+         * @return builder object
+         */
+        public Builder setMaxLatency(float maxLatency) {
+            this.maxLatency = maxLatency;
 
             return this;
         }
@@ -171,11 +345,9 @@
          */
         public DefaultCpuStatistics build() {
             return new DefaultCpuStatistics(
-                deviceId,
-                id,
-                load,
-                isBusy
-            );
+                deviceId, id, load, queue, busySince,
+                throughputUnit, averageThroughput,
+                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 8256c5f..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
@@ -16,39 +16,50 @@
 
 package org.onosproject.drivers.server.impl.stats;
 
+import org.onosproject.drivers.server.stats.MonitoringUnit;
 import org.onosproject.drivers.server.stats.TimingStatistics;
 
+import com.google.common.base.Strings;
 import com.google.common.base.MoreObjects;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.drivers.server.stats.MonitoringUnit.LatencyUnit;
 
 /**
  * Default implementation for timing statistics.
  */
 public final class DefaultTimingStatistics implements TimingStatistics {
 
+    private static final LatencyUnit DEF_UNIT = LatencyUnit.NANO_SECOND;
+
+    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
     private DefaultTimingStatistics() {
+        this.unit = null;
         this.deployCommandParsingTime   = 0;
         this.deployCommandLaunchingTime = 0;
-        this.autoscaleTime = 0;
+        this.autoScaleTime = 0;
     }
 
     /**
@@ -61,6 +72,11 @@
     }
 
     @Override
+    public MonitoringUnit unit() {
+        return this.unit;
+    }
+
+    @Override
     public long deployCommandParsingTime() {
         return this.deployCommandParsingTime;
     }
@@ -76,32 +92,48 @@
     }
 
     @Override
-    public long autoscaleTime() {
-        return this.autoscaleTime;
+    public long autoScaleTime() {
+        return this.autoScaleTime;
     }
 
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this)
                 .omitNullValues()
-                .add("parsing",   this.deployCommandParsingTime())
-                .add("launching", this.deployCommandLaunchingTime())
-                .add("total",     this.totalDeploymentTime())
-                .add("autoscale", this.autoscaleTime())
+                .add("unit", this.unit().toString())
+                .add("parsingTime", this.deployCommandParsingTime())
+                .add("launchingTime", this.deployCommandLaunchingTime())
+                .add("deploymentTime", this.totalDeploymentTime())
+                .add("autoScaleTime", this.autoScaleTime())
                 .toString();
     }
 
     public static final class Builder {
 
+        MonitoringUnit unit = DEF_UNIT;
         long deployCommandParsingTime;
         long deployCommandLaunchingTime;
-        long autoscaleTime;
+        long autoScaleTime;
 
         private Builder() {
 
         }
 
         /**
+         * Sets time statistics unit.
+         *
+         * @param unitStr time statistics unit as a string
+         * @return builder object
+         */
+        public Builder setUnit(String unitStr) {
+            if (!Strings.isNullOrEmpty(unitStr)) {
+                this.unit = LatencyUnit.getByName(unitStr);
+            }
+
+            return this;
+        }
+
+        /**
          * Sets parsing time.
          *
          * @param parsingTime parsing time
@@ -128,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;
         }
@@ -144,10 +176,8 @@
          */
         public DefaultTimingStatistics build() {
             return new DefaultTimingStatistics(
-                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 9ea22b3..5296d57 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
@@ -16,6 +16,8 @@
 
 package org.onosproject.drivers.server.stats;
 
+import java.util.Optional;
+
 /**
  * CPU statistics API.
  */
@@ -38,10 +40,71 @@
     float load();
 
     /**
+     * Returns the hardware queue identifier associated with this CPU core.
+     *
+     * @return hardware queue identifier
+     */
+    int queue();
+
+    /**
      * Returns the status (true=busy, false=free) of a CPU core.
      *
      * @return boolean CPU core status
      */
     boolean busy();
 
+    /**
+     * Returns the amount of time in ms since the CPU has been busy,
+     * or a negative value if the CPU is idle.
+     *
+     * @return int time in ms since the CPU has been busy
+     */
+    int busySince();
+
+    /**
+     * Returns the unit of throughput values.
+     *
+     * @return throughput monitoring unit
+     */
+    Optional<MonitoringUnit> throughputUnit();
+
+    /**
+     * Returns the average throughput of this CPU core,
+     * expressed in throughputUnit() monitoring units.
+     *
+     * @return average throughput of a CPU core
+     */
+    Optional<Float> averageThroughput();
+
+    /**
+     * Returns the unit of latency values.
+     *
+     * @return latency monitoring unit
+     */
+    Optional<MonitoringUnit> latencyUnit();
+
+    /**
+     * Returns the minimum latency incurred by a CPU core,
+     * expressed in latencyUnit() monitoring units.
+     *
+     * @return minimum latency incurred by a CPU core
+     */
+    Optional<Float> minLatency();
+
+    /**
+     * Returns the average latency incurred by a CPU core,
+     * expressed in latencyUnit() monitoring units.
+     *
+     * @return average latency incurred by a CPU core
+     */
+    Optional<Float> averageLatency();
+
+    /**
+     * Returns the maximum latency incurred by a CPU core,
+     * expressed in latencyUnit() monitoring units.
+     *
+     * @return maximum latency incurred by a CPU core
+     */
+    Optional<Float> maxLatency();
+
 }
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
new file mode 100644
index 0000000..33d8763
--- /dev/null
+++ b/drivers/server/src/main/java/org/onosproject/drivers/server/stats/MonitoringUnit.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2017-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.stats;
+
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * Representation of a monitoring unit.
+ */
+public interface MonitoringUnit {
+
+    /**
+     * Throughput-related monitoring units.
+     */
+    public enum ThroughputUnit implements MonitoringUnit {
+
+        BPS("bps"),
+        KBPS("kbps"),
+        MBPS("mbps"),
+        GBPS("gbps");
+
+        private String throughputUnit;
+
+        // Statically maps throughput monitoring units to enum types
+        private static final Map<String, MonitoringUnit> MAP =
+            new HashMap<String, MonitoringUnit>();
+        static {
+            for (ThroughputUnit tu : ThroughputUnit.values()) {
+                MAP.put(tu.toString().toLowerCase(), (MonitoringUnit) tu);
+            }
+        }
+
+        private ThroughputUnit(String throughputUnit) {
+            this.throughputUnit = throughputUnit;
+        }
+
+        public static MonitoringUnit getByName(String tu) {
+            tu = tu.toLowerCase();
+            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;
+        }
+
+    };
+
+    /**
+     * Latency-related monitoring units.
+     */
+    public enum LatencyUnit implements MonitoringUnit {
+
+        NANO_SECOND("ns"),
+        MICRO_SECOND("us"),
+        MILLI_SECOND("ms"),
+        SECOND("s");
+
+        private String latencyUnit;
+
+        // Statically maps latency monitoring units to enum types
+        private static final Map<String, MonitoringUnit> MAP =
+            new HashMap<String, MonitoringUnit>();
+        static {
+            for (LatencyUnit lu : LatencyUnit.values()) {
+                MAP.put(lu.toString().toLowerCase(), (MonitoringUnit) lu);
+            }
+        }
+
+        private LatencyUnit(String latencyUnit) {
+            this.latencyUnit = latencyUnit;
+        }
+
+        public static MonitoringUnit getByName(String lu) {
+            lu = lu.toLowerCase();
+            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;
+        }
+
+    };
+
+    String toString();
+
+}
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 102b186..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
@@ -22,6 +22,13 @@
 public interface TimingStatistics {
 
     /**
+     * Returns the unit of timing statistics.
+     *
+     * @return timing statistics' unit
+     */
+    MonitoringUnit unit();
+
+    /**
      * Time (ns) to parse the controller's deployment instruction.
      *
      * @return time in nanoseconds to parse a 'deploy' command
@@ -45,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>
diff --git a/drivers/server/src/main/resources/server-drivers.xml b/drivers/server/src/main/resources/server-drivers.xml
index 9058fa1..11c75d1 100644
--- a/drivers/server/src/main/resources/server-drivers.xml
+++ b/drivers/server/src/main/resources/server-drivers.xml
@@ -36,6 +36,7 @@
 
         <behaviour api="org.onosproject.net.flow.FlowRuleProgrammable"
                    impl="org.onosproject.drivers.server.FlowRuleProgrammableServerImpl"/>
+        <property name="ruleDeleteBatchSize">500</property>
     </driver>
 </drivers>