changeset 238:e905e459ea48

Add jvm-cpu component Reviewed-by: jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/025145.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-October/025348.html
author Andrew Azores <aazores@redhat.com>
date Wed, 11 Oct 2017 09:39:05 -0400
parents 68d7308271aa
children 3757ec9994fa
files src/app/components/jvm-info/en.locale.yaml src/app/components/jvm-info/jvm-cpu/en.locale.yaml src/app/components/jvm-info/jvm-cpu/jvm-cpu.component.js src/app/components/jvm-info/jvm-cpu/jvm-cpu.controller.js src/app/components/jvm-info/jvm-cpu/jvm-cpu.controller.spec.js src/app/components/jvm-info/jvm-cpu/jvm-cpu.html src/app/components/jvm-info/jvm-cpu/jvm-cpu.routing.js src/app/components/jvm-info/jvm-cpu/jvm-cpu.routing.spec.js src/app/components/jvm-info/jvm-cpu/jvm-cpu.service.js src/app/components/jvm-info/jvm-cpu/jvm-cpu.service.spec.js src/app/components/jvm-info/jvm-info.html
diffstat 11 files changed, 826 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/src/app/components/jvm-info/en.locale.yaml	Wed Oct 04 10:23:50 2017 -0400
+++ b/src/app/components/jvm-info/en.locale.yaml	Wed Oct 11 09:39:05 2017 -0400
@@ -34,5 +34,6 @@
     NONE: None
     MEMORY: Memory Usage
     GC: Garbage Collection
+    CPU: CPU Usage
     IO: File I/O
     BYTEMAN: Byteman
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-cpu/en.locale.yaml	Wed Oct 11 09:39:05 2017 -0400
@@ -0,0 +1,17 @@
+jvmCpu:
+  CARD_TITLE: JVM CPU Usage
+  REFRESH_RATE_LABEL: Refresh Rate
+  MAX_DATA_AGE_LABEL: Max Data Age
+
+  chart:
+    UNITS: '%'
+    X_AXIS_LABEL: timestamp
+    Y_AXIS_LABEL: utilization
+
+  refresh:
+    DISABLED: Disabled
+    SECONDS: '{SECONDS, plural, =0{0 Seconds} one{1 Second} other{# Seconds}}{DEFAULT, select, true{ (Default)} other{}}'
+
+  dataAge:
+    SECONDS: '{SECONDS, plural, =0{0 Seconds} one{1 Second} other{# Seconds}}{DEFAULT, select, true{ (Default)} other{}}'
+    MINUTES: '{MINUTES, plural, =0{0 Minutes} one{1 Minute} other{# Minutes}}{DEFAULT, select, true{ (Default)} other{}}'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-cpu/jvm-cpu.component.js	Wed Oct 11 09:39:05 2017 -0400
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import controller from './jvm-cpu.controller.js';
+import service from './jvm-cpu.service.js';
+
+export default angular
+  .module('jvmCpuComponent', [
+    controller,
+    service
+  ])
+  .component('jvmCpu', {
+    controller: 'JvmCpuController',
+    template: require('./jvm-cpu.html')
+  })
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-cpu/jvm-cpu.controller.js	Wed Oct 11 09:39:05 2017 -0400
@@ -0,0 +1,176 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import service from './jvm-cpu.service.js';
+import filters from 'shared/filters/filters.module.js';
+
+class JvmCpuController {
+  constructor ($stateParams, jvmCpuService, $interval, $translate, dateFilter, DATE_FORMAT, metricToNumberFilter) {
+    'ngInject';
+    this.jvmId = $stateParams.jvmId;
+    this._svc = jvmCpuService;
+    this._interval = $interval;
+    this._translate = $translate;
+    this._dateFilter = dateFilter;
+    this._dateFormat = DATE_FORMAT;
+    this._metricToNumber = metricToNumberFilter;
+
+    this._refreshRate = 2000;
+    this._dataAgeLimit = 30000;
+
+    this._xData = ['timestamp'];
+    this._yData = ['CPU Load'];
+    this.data = {
+      xData: this._xData,
+      yData: this._yData
+    };
+  }
+
+  $onInit () {
+    this._makeConfig().then(() => this._start());
+  }
+
+  $onDestroy() {
+    this._stop();
+  }
+
+  _makeConfig () {
+    return this._translate([
+      'jvmCpu.chart.UNITS',
+      'jvmCpu.chart.X_AXIS_LABEL',
+      'jvmCpu.chart.Y_AXIS_LABEL'
+    ]).then(translations => {
+      this.config = {
+        chartId: 'jvm-cpu-chart',
+        units: translations['jvmCpu.chart.UNITS'],
+        axis: {
+          x: {
+            label: translations['jvmCpu.chart.X_AXIS_LABEL'],
+            show: true,
+            type: 'timeseries',
+            localtime: false,
+            tick: {
+              format: timestamp => this._dateFilter(timestamp, this._dateFormat.time.medium),
+              count: 5
+            }
+          },
+          y: {
+            label: translations['jvmCpu.chart.Y_AXIS_LABEL'],
+            show: true,
+            tick: {
+              format: d => d
+            }
+          }
+        },
+        tooltip: {
+          format: {
+            title: x => x,
+            value: y => y
+          }
+        },
+        onmouseover: () => this._stop(),
+        onmouseout: () => this._start()
+      };
+    });
+  }
+
+  _start () {
+    this._stop();
+    this._update();
+    this._refresh = this._interval(() => this._update(), this.refreshRate);
+  }
+
+  _stop () {
+    if (angular.isDefined(this._refresh)) {
+      this._interval.cancel(this._refresh);
+      delete this._refresh;
+    }
+  }
+
+  _update () {
+    this._svc.getJvmCpuData(this.jvmId).then(resp => {
+      let data = resp.data.response[0];
+      let timestamp = this._metricToNumber(data.timeStamp);
+      this._xData.push(timestamp);
+      let load = data.cpuLoad;
+      this._yData.push(load);
+
+      this._trimData();
+    });
+  }
+
+  _trimData () {
+    let now = Date.now();
+    let expiry = now - this._dataAgeLimit;
+    while (true) {
+      let oldest = this._xData[1];
+      if (oldest < expiry) {
+        this._xData.splice(1, 1);
+        this._yData.splice(1, 1);
+      } else {
+        break;
+      }
+    }
+  }
+
+  set refreshRate (val) {
+    this._stop();
+    this._refreshRate = parseInt(val);
+    if (this._refreshRate > 0) {
+      this._start();
+    }
+  }
+
+  get refreshRate () {
+    return this._refreshRate.toString();
+  }
+
+  set dataAgeLimit (val) {
+    this._dataAgeLimit = val;
+    this._trimData();
+  }
+
+  get dataAgeLimit () {
+    return this._dataAgeLimit.toString();
+  }
+
+  multichartFn () {
+    return new Promise(resolve => {
+      this._svc.getJvmCpuData(this.jvmId).then(resp => {
+        resolve(resp.data.response[0].cpuLoad);
+      });
+    });
+  }
+}
+
+export default angular
+  .module('jvmCpu.controller', [
+    service,
+    filters
+  ])
+  .controller('JvmCpuController', JvmCpuController)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-cpu/jvm-cpu.controller.spec.js	Wed Oct 11 09:39:05 2017 -0400
@@ -0,0 +1,285 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import filtersModule from 'shared/filters/filters.module.js';
+import controllerModule from './jvm-cpu.controller.js';
+
+describe('JvmCpuController', () => {
+
+  beforeEach(angular.mock.module(filtersModule));
+  beforeEach(angular.mock.module(controllerModule));
+
+  let interval, dateFilterStub, metricToNumberFilterStub, dateFormatSpy, svc, promise, ctrl, translate;
+  beforeEach(inject(($controller) => {
+    'ngInject';
+
+    dateFilterStub = sinon.stub().returns('mockDate');
+    dateFormatSpy = {
+      time: {
+        medium: sinon.spy()
+      }
+    };
+
+    metricToNumberFilterStub = sinon.stub().returns(Date.now());
+
+    interval = sinon.stub().returns('interval-sentinel');
+    interval.cancel = sinon.spy();
+
+    promise = { then: sinon.spy() };
+    svc = { getJvmCpuData: sinon.stub().returns(promise) };
+
+    translate = sinon.stub().returns({
+      then: sinon.stub().yields({
+        'jvmCpu.chart.UNITS': 'microseconds',
+        'jvmCpu.chart.X_AXIS_LABEL': 'timestamp',
+        'jvmCpu.chart.Y_AXIS_LABEL': 'elapsed'
+      }).returns({
+        then: sinon.stub().yields()
+      })
+    });
+
+    ctrl = $controller('JvmCpuController', {
+      $stateParams: { jvmId: 'foo-jvmId' },
+      jvmCpuService: svc,
+      $interval: interval,
+      $translate: translate,
+      dateFilter: dateFilterStub,
+      DATE_FORMAT: dateFormatSpy,
+      metricToNumberFilter: metricToNumberFilterStub
+    });
+    ctrl.$onInit();
+  }));
+
+  it('should exist', () => {
+    should.exist(ctrl);
+  });
+
+  it('should update on init', () => {
+    svc.getJvmCpuData.should.be.calledOnce();
+    svc.getJvmCpuData.should.be.calledWith('foo-jvmId');
+  });
+
+  it('should call to service on update', () => {
+    svc.getJvmCpuData.should.be.calledOnce();
+    promise.then.should.be.calledOnce();
+    ctrl._update();
+    svc.getJvmCpuData.should.be.calledTwice();
+    promise.then.should.be.calledTwice();
+
+    promise.then.secondCall.should.be.calledWith(sinon.match.func);
+    ctrl.data.should.deepEqual({
+      xData: ['timestamp'],
+      yData: ['CPU Load']
+    });
+    let successHandler = promise.then.secondCall.args[0];
+    successHandler({
+      data: {
+        response: [
+          {
+            cpuLoad: 0.25,
+            programTicks: { $numberLong: '2' },
+            timeStamp: { $numberLong: '100' }
+          }
+        ]
+      }
+    });
+  });
+
+  it('should reset interval on refreshRate change', () => {
+    ctrl.should.have.ownProperty('_refresh');
+    ctrl.refreshRate = '1';
+    interval.should.be.calledWith(sinon.match.func, sinon.match(1));
+    ctrl.should.have.ownProperty('_refresh');
+    ctrl._refresh.should.equal('interval-sentinel');
+    ctrl.refreshRate.should.equal('1');
+  });
+
+  it('should trim data on dataAgeLimit change', () => {
+    sinon.spy(ctrl, '_trimData');
+    ctrl._trimData.should.not.be.called();
+    ctrl.dataAgeLimit = 10000;
+    ctrl._trimData.should.be.calledOnce();
+    ctrl._trimData.restore();
+    ctrl.dataAgeLimit.should.equal('10000');
+  });
+
+
+  it('should trim old data', () => {
+    let oldSample = {
+      cpuLoad: 0.125,
+      timestamp: 1
+    };
+
+    let futureSample = {
+      cpuLoad: 0.25,
+      timestamp: Date.now() + 600000
+    };
+
+    ctrl._xData = ['timestamp', oldSample.timestamp, futureSample.timestamp];
+    ctrl._yData = ['utilization', oldSample.cpuLoad, futureSample.cpuLoad];
+    ctrl.data = {
+      xData: ctrl._xData,
+      yData: ctrl._yData
+    };
+    ctrl._trimData();
+
+    ctrl.data.should.deepEqual({
+      xData: ['timestamp', futureSample.timestamp],
+      yData: ['utilization', futureSample.cpuLoad]
+    });
+  });
+
+  it('should set interval on start', () => {
+    interval.should.be.calledOnce();
+    interval.should.be.calledWith(sinon.match.func, '2000');
+    interval.cancel.should.not.be.called();
+  });
+
+  it('should disable when set refreshRate is called with a non-positive value', () => {
+    interval.cancel.should.not.be.called();
+
+    ctrl.refreshRate = '1';
+
+    interval.cancel.should.be.calledOnce();
+    ctrl.should.have.ownProperty('_refresh');
+
+    ctrl.refreshRate = '-1';
+
+    interval.cancel.should.be.calledTwice();
+    ctrl.should.not.have.ownProperty('_refresh');
+  });
+
+  it('should call controller#update() on refresh', () => {
+    ctrl.refreshRate = 1;
+    let func = interval.args[0][0];
+    let callCount = svc.getJvmCpuData.callCount;
+    func();
+    svc.getJvmCpuData.callCount.should.equal(callCount + 1);
+  });
+
+  describe('_makeConfig', () => {
+    let cfg;
+    beforeEach(() => {
+      ctrl._makeConfig('fooCollector');
+      cfg = ctrl.config;
+    });
+
+    it('should return a promise', () => {
+      ctrl._makeConfig().should.be.a.Promise();
+    });
+
+    it('should set chartId', () => {
+      cfg.chartId.should.equal('jvm-cpu-chart');
+    });
+
+    it('should use dateFilter with DATE_FORMAT.time.medium to format x ticks', () => {
+      let fn = cfg.axis.x.tick.format;
+      fn.should.be.a.Function();
+      fn('fooTimestamp').should.equal('mockDate');
+      dateFilterStub.should.be.calledWith('fooTimestamp', dateFormatSpy.time.medium);
+    });
+
+    it('should format y ticks directly', () => {
+      let fn = cfg.axis.y.tick.format;
+      fn.should.be.a.Function();
+      fn(100).should.equal(100);
+    });
+
+    it('should set tooltip', () => {
+      let fmt = cfg.tooltip.format;
+      fmt.should.have.ownProperty('title');
+      fmt.title.should.be.a.Function();
+      fmt.should.have.ownProperty('value');
+      fmt.value.should.be.a.Function();
+
+      fmt.title(100).should.equal(100);
+      fmt.value(200).should.equal(200);
+    });
+
+    it('should stop on mouseover', () => {
+      ctrl._start();
+      let fn = cfg.onmouseover;
+      fn.should.be.a.Function();
+      interval.cancel.should.be.calledOnce();
+      fn();
+      interval.cancel.should.be.calledTwice();
+    });
+
+    it('should start on mouseout', () => {
+      ctrl._stop();
+      let fn = cfg.onmouseout;
+      fn.should.be.a.Function();
+      interval.should.be.calledOnce();
+      fn();
+      interval.should.be.calledTwice();
+    });
+  });
+
+  describe('ondestroy handler', () => {
+    it('should cancel refresh', () => {
+      ctrl._refresh = 'interval-sentinel';
+      ctrl.$onDestroy();
+      interval.cancel.should.be.calledWith('interval-sentinel');
+    });
+
+    it('should do nothing if refresh is undefined', () => {
+      delete ctrl._refresh;
+      ctrl.$onDestroy();
+      interval.cancel.should.not.be.called();
+    });
+  });
+
+  describe('multichartFn', () => {
+    it('should return a promise', () => {
+      let res = ctrl.multichartFn();
+      res.should.be.a.Promise();
+    });
+
+    it('should resolve jvm-cpu stat', done => {
+      promise.then.should.be.calledOnce();
+      let res = ctrl.multichartFn();
+      res.then(v => {
+        v.should.equal(0.4);
+        done();
+      });
+      promise.then.should.be.calledTwice();
+      let prom = promise.then.secondCall.args[0];
+      prom({
+        data: {
+          response: [
+            {
+              cpuLoad: 0.4,
+              programTicks: 200,
+              timeStamp: { $numberLong: '1000' }
+            }
+          ]
+        }
+      });
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-cpu/jvm-cpu.html	Wed Oct 11 09:39:05 2017 -0400
@@ -0,0 +1,46 @@
+<div class="container-fluid">
+
+  <div class="row" style="margin-top:2vh">
+    <div class="col-xs-12 col-md-3">
+      <label for="refreshCombo" class="label label-info" translate>jvmCpu.REFRESH_RATE_LABEL</label>
+      <select name="refreshCombo" class="combobox form-control" ng-model="$ctrl.refreshRate">
+        <option value="-1" translate>jvmCpu.refresh.DISABLED</option>
+        <option value="1000" translate="jvmCpu.refresh.SECONDS" translate-values="{ SECONDS: 1 }" translate-interpolation="messageformat"></option>
+        <option value="2000" selected translate="jvmCpu.refresh.SECONDS" translate-values="{ SECONDS: 2, DEFAULT: true }" translate-interpolation="messageformat"></option>
+        <option value="5000" translate="jvmCpu.refresh.SECONDS" translate-values="{ SECONDS: 5 }" translate-interpolation="messageformat"></option>
+        <option value="10000" translate="jvmCpu.refresh.SECONDS" translate-values="{ SECONDS: 10 }" translate-interpolation="messageformat"></option>
+        <option value="30000" translate="jvmCpu.refresh.SECONDS" translate-values="{ SECONDS: 30 }" translate-interpolation="messageformat"></option>
+      </select>
+    </div>
+
+    <div class="col-xs-12 col-md-3">
+      <label for="dataAgeCombo" class="label label-info" translate>jvmCpu.MAX_DATA_AGE_LABEL</label>
+      <select name="dataAgeCombo" class="combobox form-control" ng-model="$ctrl.dataAgeLimit">
+        <option value="10000" translate="jvmCpu.dataAge.SECONDS" translate-values="{ SECONDS: 10 }" translate-interpolation="messageformat"></option>
+        <option value="30000" selected translate="jvmCpu.dataAge.SECONDS" translate-values="{ SECONDS: 30, DEFAULT: true }" translate-interpolation="messageformat"></option>
+        <option value="60000" translate="jvmCpu.dataAge.MINUTES" translate-values="{ MINUTES: 1 }" translate-interpolation="messageformat"></option>
+        <option value="300000" translate="jvmCpu.dataAge.MINUTES" translate-values="{ MINUTES: 5 }" translate-interpolation="messageformat"></option>
+        <option value="900000" translate="jvmCpu.dataAge.MINUTES" translate-values="{ MINUTES: 15 }" translate-interpolation="messageformat"></option>
+      </select>
+    </div>
+  </div>
+
+  <div class="row row-cards-pf">
+    <div class="container-fluid container-cards-pf">
+
+      <div class="col-md-12">
+        <div class="card-pf card-pf-view">
+          <div class="card-pf-heading">
+            <label class="card-pf-title" translate>jvmCpu.CARD_TITLE</label>
+          </div>
+          <div class="card-pf-body text-center">
+            <mc-add class="pull-right" svc-name="{{$ctrl.jvmId}}-cpu" get-fn="$ctrl.multichartFn()"></mc-add>
+            <pf-line-chart id="jvm-cpu-chart" config="$ctrl.config" chart-data="$ctrl.data"></pf-line-chart>
+          </div>
+        </div>
+      </div>
+
+    </div>
+
+  </div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-cpu/jvm-cpu.routing.js	Wed Oct 11 09:39:05 2017 -0400
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+function config ($stateProvider) {
+  'ngInject';
+
+  $stateProvider.state('jvmInfo.jvmCpu', {
+    url: '/jvm-cpu',
+    component: 'jvmCpu',
+    resolve: {
+      lazyLoad: ($q, $ocLazyLoad) => {
+        'ngInject';
+        return $q(resolve => {
+          require.ensure(['./jvm-cpu.component.js'], () => {
+            let module = require('./jvm-cpu.component.js');
+            $ocLazyLoad.load({ name: module.default });
+            resolve(module);
+          });
+        });
+      }
+    }
+  });
+}
+
+export { config };
+
+export default angular
+  .module('jvmCpu.routing', [
+    'ui.router',
+    'oc.lazyLoad'
+  ])
+  .config(config)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-cpu/jvm-cpu.routing.spec.js	Wed Oct 11 09:39:05 2017 -0400
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+describe('JvmCpuRouting', () => {
+
+  let module = require('./jvm-cpu.routing.js');
+  let stateProvider, args, q, ocLazyLoad;
+  beforeEach(() => {
+    angular.mock.module(module.default);
+    stateProvider = {
+      state: sinon.spy()
+    };
+    module.config(stateProvider);
+    args = stateProvider.state.args[0];
+    q = sinon.spy();
+    ocLazyLoad = {
+      load: sinon.spy()
+    };
+  });
+
+  describe('stateProvider', () => {
+    it('should call $stateProvider.state', () => {
+      stateProvider.state.should.be.calledOnce();
+    });
+
+    it('should define a \'jvmInfo.jvmCpu\' state', () => {
+      args[0].should.equal('jvmInfo.jvmCpu');
+    });
+
+    it('should map to /jvm-cpu', () => {
+      args[1].url.should.equal('/jvm-cpu');
+    });
+
+    it('resolve should load jvm-cpu component', done => {
+      let resolveFn = args[1].resolve.lazyLoad[2];
+      resolveFn.should.be.a.Function();
+      resolveFn(q, ocLazyLoad);
+      q.should.be.calledOnce();
+
+      let deferred = q.args[0][0];
+      deferred.should.be.a.Function();
+
+      let resolve = sinon.stub().callsFake(val => {
+        let mod = require('./jvm-cpu.component.js');
+        ocLazyLoad.load.should.be.calledWith({ name: mod.default });
+        val.should.equal(mod);
+        done();
+      });
+      deferred(resolve);
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-cpu/jvm-cpu.service.js	Wed Oct 11 09:39:05 2017 -0400
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import configModule from 'shared/config/config.module.js';
+import urlJoin from 'url-join';
+
+class JvmCpuService {
+  constructor ($http, gatewayUrl) {
+    'ngInject';
+    this.http = $http;
+    this.gatewayUrl = gatewayUrl;
+  }
+
+  getJvmCpuData (jvmId) {
+    let params = { sort: '-timeStamp' };
+    return this.http.get(urlJoin(this.gatewayUrl, 'jvm-cpu', '0.0.1', 'jvms', jvmId), { params: params });
+  }
+}
+
+export default angular
+  .module('jvmCpu.service', [ configModule ])
+  .service('jvmCpuService', JvmCpuService)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/jvm-cpu/jvm-cpu.service.spec.js	Wed Oct 11 09:39:05 2017 -0400
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import configModule from 'shared/config/config.module.js';
+import serviceModule from './jvm-cpu.service.js';
+
+describe('JvmCpuService', () => {
+
+  beforeEach(() => {
+    angular.mock.module(configModule, $provide => {
+      'ngInject';
+      $provide.constant('gatewayUrl', 'http://example.com:1234');
+    });
+
+    angular.mock.module(serviceModule);
+  });
+
+  let httpBackend, scope, svc;
+  beforeEach(inject(($httpBackend, $rootScope, jvmCpuService) => {
+    'ngInject';
+    httpBackend = $httpBackend;
+
+    scope = $rootScope;
+    svc = jvmCpuService;
+  }));
+
+  afterEach(() => {
+    httpBackend.verifyNoOutstandingExpectation();
+    httpBackend.verifyNoOutstandingRequest();
+  });
+
+  it('should exist', () => {
+    should.exist(svc);
+  });
+
+  describe('getJvmCpuData(jvmId)', () => {
+    it('should resolve mock data', done => {
+      let expected = {
+        cpuLoad: 0.125,
+        programTicks: { $numberLong: '1000' },
+        timeStamp: { $numberLong: '2000' }
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvm-cpu/0.0.1/jvms/foo-jvmId?sort=-timeStamp')
+        .respond(expected);
+      svc.getJvmCpuData('foo-jvmId').then(res => {
+        res.data.should.deepEqual(expected);
+        done();
+      });
+      httpBackend.expectGET('http://example.com:1234/jvm-cpu/0.0.1/jvms/foo-jvmId?sort=-timeStamp');
+      httpBackend.flush();
+      scope.$apply();
+    });
+  });
+
+});
--- a/src/app/components/jvm-info/jvm-info.html	Wed Oct 04 10:23:50 2017 -0400
+++ b/src/app/components/jvm-info/jvm-info.html	Wed Oct 11 09:39:05 2017 -0400
@@ -137,6 +137,7 @@
         <option value="jvmMemory" translate>jvmInfo.subview.MEMORY</option>
         <option value="jvmGc" translate>jvmInfo.subview.GC</option>
         <option value="jvmIo" translate>jvmInfo.subview.IO</option>
+        <option value="jvmCpu" translate>jvmInfo.subview.CPU</option>
         <option value="byteman" translate>jvmInfo.subview.BYTEMAN</option>
       </select>
     </div>