From 4cd9164d4ead4590c5761cc1465e98e92a923322 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 13 Sep 2018 12:25:35 -0700 Subject: [PATCH 01/27] Use addObject to get kvo behaviors --- ui/app/utils/classes/node-stats-tracker.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ui/app/utils/classes/node-stats-tracker.js b/ui/app/utils/classes/node-stats-tracker.js index 530a6619d75..2933553b3bc 100644 --- a/ui/app/utils/classes/node-stats-tracker.js +++ b/ui/app/utils/classes/node-stats-tracker.js @@ -21,19 +21,24 @@ const NodeStatsTracker = EmberObject.extend(AbstractStatsTracker, { }), append(frame) { + const timestamp = new Date(Math.floor(frame.Timestamp / 1000000)); + const cpuUsed = Math.floor(frame.CPUTicksConsumed) || 0; - this.get('cpu').push({ - timestamp: frame.Timestamp, + this.get('cpu').addObject({ + timestamp, used: cpuUsed, percent: percent(cpuUsed, this.get('reservedCPU')), }); const memoryUsed = frame.Memory.Used; - this.get('memory').push({ - timestamp: frame.Timestamp, + this.get('memory').addObject({ + timestamp, used: memoryUsed, percent: percent(memoryUsed / 1024 / 1024, this.get('reservedMemory')), }); + + // this.notifyPropertyChange('cpu'); + // this.notifyPropertyChange('memory'); }, // Static figures, denominators for stats From 48d02205615fa0aa2e2b7b4e4a92f1be00d49134 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 13 Sep 2018 12:26:18 -0700 Subject: [PATCH 02/27] Use percent for the y-axis binding --- ui/app/components/stats-time-series.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/stats-time-series.js b/ui/app/components/stats-time-series.js index 90a229e6390..403c6a7d7da 100644 --- a/ui/app/components/stats-time-series.js +++ b/ui/app/components/stats-time-series.js @@ -8,7 +8,7 @@ import LineChart from 'nomad-ui/components/line-chart'; export default LineChart.extend({ xProp: 'timestamp', - yProp: 'value', + yProp: 'percent', timeseries: true, xFormat() { From b80016d5a0d7b05d76205a2b978145cba7ce276c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 13 Sep 2018 12:27:27 -0700 Subject: [PATCH 03/27] Add stat charts to the client page --- ui/app/templates/clients/client.hbs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 3b84f643e04..885fd498910 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -77,6 +77,24 @@ {{/if}} +
+
+ Resource Utilization +
+
+
+
+ CPU + {{stats-time-series data=stats.cpu chartClass="is-info"}} +
+
+ Memory + {{stats-time-series data=stats.memory chartClass="is-danger"}} +
+
+
+
+
Allocations {{model.allocations.length}}
From 44b16b2b7c83cc4ef0750daa12a661d30a73fa13 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 13 Sep 2018 16:31:31 -0700 Subject: [PATCH 04/27] Full markup for time series metrics --- ui/app/templates/clients/client.hbs | 40 +++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 885fd498910..4e251ff7f26 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -85,11 +85,47 @@
CPU - {{stats-time-series data=stats.cpu chartClass="is-info"}} +
+ {{stats-time-series data=stats.cpu chartClass="is-info"}} +
+
+
+
+ + {{stats.cpu.lastObject.percent}} + +
+
+
+ {{format-percentage stats.cpu.lastObject.percent total=1}} +
+
+ {{stats.cpu.lastObject.used}} Mhz / {{stats.reservedCPU}} Mhz reserved
Memory - {{stats-time-series data=stats.memory chartClass="is-danger"}} +
+ {{stats-time-series data=stats.memory chartClass="is-danger"}} +
+
+
+
+ + {{stats.memory.lastObject.percent}} + +
+
+
+ {{format-percentage stats.memory.lastObject.percent total=1}} +
+
+ {{format-bytes stats.memory.lastObject.used}} MiB / {{stats.reservedMemory}} MiB reserved
From 48910d8334933a17cf48e3e45a7dc5bfb3d1f3c1 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 13 Sep 2018 16:32:29 -0700 Subject: [PATCH 05/27] New primary-metric component It encapsulates all the tracker, polling, and markup for this style of metric. --- ui/app/components/primary-metric.js | 103 ++++++++++++++++++ .../templates/components/primary-metric.hbs | 26 +++++ .../utils/classes/abstract-stats-tracker.js | 4 +- 3 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 ui/app/components/primary-metric.js create mode 100644 ui/app/templates/components/primary-metric.hbs diff --git a/ui/app/components/primary-metric.js b/ui/app/components/primary-metric.js new file mode 100644 index 00000000000..8423af248f0 --- /dev/null +++ b/ui/app/components/primary-metric.js @@ -0,0 +1,103 @@ +import Ember from 'ember'; +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; +import { computed } from '@ember/object'; +import NodeStatsTracker from 'nomad-ui/utils/classes/node-stats-tracker'; +import AllocationStatsTracker from 'nomad-ui/utils/classes/allocation-stats-tracker'; +import { task, timeout } from 'ember-concurrency'; + +export default Component.extend({ + token: service(), + + // One of Node, Allocation, or TaskState + resource: null, + + // cpu or memory + metric: null, + + // An instance of a StatsTracker. An alternative interface to resource + tracker: computed('trackedResource', 'type', function() { + const resource = this.get('trackedResource'); + + if (!resource) return; + + const Constructor = this.get('type') === 'node' ? NodeStatsTracker : AllocationStatsTracker; + const resourceProp = this.get('type') === 'node' ? 'node' : 'allocation'; + return Constructor.create({ + fetch: url => this.get('token').authorizedRequest(url), + [resourceProp]: resource, + }); + }), + + type: computed('resource', function() { + const resource = this.get('resource'); + return resource && resource.constructor.modelName; + }), + + trackedResource: computed('resource', 'type', function() { + // TaskStates use the allocation stats tracker + return this.get('type') === 'task-state' + ? this.get('resource.allocation') + : this.get('resource'); + }), + + metricLabel: computed('metric', function() { + const metric = this.get('metric'); + const mappings = { + cpu: 'CPU', + memory: 'Memory', + }; + return mappings[metric] || metric; + }), + + data: computed('resource', 'metric', 'type', function() { + if (!this.get('tracker')) return []; + + const metric = this.get('metric'); + if (this.get('type') === 'task-state') { + // handle getting the right task out of the tracker + const task = this.get('tracker.tasks').findBy('task', this.get('resource.name')); + return task && task[metric]; + } + + return this.get(`tracker.${metric}`); + }), + + reservedAmount: computed('resource', 'metric', 'type', function() { + const metricProperty = this.get('metric') === 'cpu' ? 'reservedCPU' : 'reservedMemory'; + + if (this.get('type') === 'task-state') { + const task = this.get('tracker.tasks').findBy('task', this.get('resource.name')); + return task[metricProperty]; + } + + return this.get(`tracker.${metricProperty}`); + }), + + chartClass: computed('metric', function() { + const metric = this.get('metric'); + const mappings = { + cpu: 'is-info', + memory: 'is-danger', + }; + + return mappings[metric] || 'is-primary'; + }), + + poller: task(function*() { + do { + yield this.get('tracker').poll(); + yield timeout(2000); + } while (!Ember.testing); + }), + + didReceiveAttrs() { + if (this.get('tracker')) { + this.get('poller').perform(); + } + }, + + willDestroy() { + this.get('poller').cancelAll(); + }, +}); diff --git a/ui/app/templates/components/primary-metric.hbs b/ui/app/templates/components/primary-metric.hbs new file mode 100644 index 00000000000..d8d4ab9acfa --- /dev/null +++ b/ui/app/templates/components/primary-metric.hbs @@ -0,0 +1,26 @@ +{{metricLabel}} +
+ {{stats-time-series data=data chartClass=chartClass}} +
+
+
+
+ + {{data.lastObject.percent}} + +
+
+
+ {{format-percentage data.lastObject.percent total=1}} +
+
+{{#if (eq metric "cpu")}} + {{data.lastObject.used}} Mhz / {{reservedAmount}} Mhz reserved +{{else if (eq metric "memory")}} + {{format-bytes data.lastObject.used}} MiB / {{reservedAmount}} MiB reserved +{{else}} + {{data.lastObject.used}} / {{reservedAmount}} reserved +{{/if}} diff --git a/ui/app/utils/classes/abstract-stats-tracker.js b/ui/app/utils/classes/abstract-stats-tracker.js index 488dec465f0..4569b29b5e3 100644 --- a/ui/app/utils/classes/abstract-stats-tracker.js +++ b/ui/app/utils/classes/abstract-stats-tracker.js @@ -19,9 +19,7 @@ export default Mixin.create({ assert('Url must be defined', url); return this.get('fetch')(url) - .then(res => { - return res.json(); - }) + .then(res => res.json()) .then(frame => this.append(frame)); }, }); From 76f9c13cb2c3937980adf697ea932bbc8a3dd371 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 13 Sep 2018 16:33:44 -0700 Subject: [PATCH 06/27] Use the new primary-metric component on the client detail page --- ui/app/templates/clients/client.hbs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 4e251ff7f26..7d83e4ae275 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -131,6 +131,22 @@
+
+
+ Resource Utilization +
+
+
+
+ {{primary-metric resource=model metric="cpu"}} +
+
+ {{primary-metric resource=model metric="memory"}} +
+
+
+
+
Allocations {{model.allocations.length}}
From 1572e8d820b3c3eae9a00edd6ddfc5b62940f12e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 13 Sep 2018 16:34:56 -0700 Subject: [PATCH 07/27] Remove old stat tracking code from the client page In favor of the new primary-metric components --- ui/app/controllers/clients/client.js | 11 ------ ui/app/routes/clients/client.js | 11 ------ ui/app/templates/clients/client.hbs | 54 ---------------------------- 3 files changed, 76 deletions(-) diff --git a/ui/app/controllers/clients/client.js b/ui/app/controllers/clients/client.js index 33873b361e8..7ded2c1be70 100644 --- a/ui/app/controllers/clients/client.js +++ b/ui/app/controllers/clients/client.js @@ -37,17 +37,6 @@ export default Controller.extend(Sortable, Searchable, { return this.get('model.drivers').sortBy('name'); }), - stats: stats('model', function statsFetch() { - return url => this.get('token').authorizedRequest(url); - }), - - pollStats: task(function*() { - do { - yield this.get('stats').poll(); - yield timeout(1000); - } while (!Ember.testing); - }), - actions: { gotoAllocation(allocation) { this.transitionToRoute('allocations.allocation', allocation); diff --git a/ui/app/routes/clients/client.js b/ui/app/routes/clients/client.js index 6cf9332ebee..9e249303003 100644 --- a/ui/app/routes/clients/client.js +++ b/ui/app/routes/clients/client.js @@ -38,15 +38,4 @@ export default Route.extend(WithWatchers, { watchAllocations: watchRelationship('allocations'), watchers: collect('watch', 'watchAllocations'), - - setupController(controller, model) { - this._super(...arguments); - if (model) { - controller.get('pollStats').perform(); - } - }, - - resetController(controller) { - controller.get('pollStats').cancelAll(); - }, }); diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 7d83e4ae275..2c286e4ba50 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -77,60 +77,6 @@
{{/if}} -
-
- Resource Utilization -
-
-
-
- CPU -
- {{stats-time-series data=stats.cpu chartClass="is-info"}} -
-
-
-
- - {{stats.cpu.lastObject.percent}} - -
-
-
- {{format-percentage stats.cpu.lastObject.percent total=1}} -
-
- {{stats.cpu.lastObject.used}} Mhz / {{stats.reservedCPU}} Mhz reserved -
-
- Memory -
- {{stats-time-series data=stats.memory chartClass="is-danger"}} -
-
-
-
- - {{stats.memory.lastObject.percent}} - -
-
-
- {{format-percentage stats.memory.lastObject.percent total=1}} -
-
- {{format-bytes stats.memory.lastObject.used}} MiB / {{stats.reservedMemory}} MiB reserved -
-
-
-
-
Resource Utilization From 9a102b73b9f1479a7280ab259cc49e8997d4b6ce Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 14 Sep 2018 08:57:26 -0700 Subject: [PATCH 08/27] Make rollingArray work with mutable array extension methods --- .../utils/classes/abstract-stats-tracker.js | 2 ++ .../utils/classes/allocation-stats-tracker.js | 2 -- ui/app/utils/classes/node-stats-tracker.js | 2 -- ui/app/utils/classes/rolling-array.js | 24 ++++++++++++------- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/ui/app/utils/classes/abstract-stats-tracker.js b/ui/app/utils/classes/abstract-stats-tracker.js index 4569b29b5e3..825851d22d7 100644 --- a/ui/app/utils/classes/abstract-stats-tracker.js +++ b/ui/app/utils/classes/abstract-stats-tracker.js @@ -4,6 +4,8 @@ import { assert } from '@ember/debug'; export default Mixin.create({ url: '', + bufferSize: 500, + fetch() { assert('StatsTrackers need a fetch method, which should have an interface like window.fetch'); }, diff --git a/ui/app/utils/classes/allocation-stats-tracker.js b/ui/app/utils/classes/allocation-stats-tracker.js index 46ca5365ba2..d0a4422df9e 100644 --- a/ui/app/utils/classes/allocation-stats-tracker.js +++ b/ui/app/utils/classes/allocation-stats-tracker.js @@ -14,8 +14,6 @@ const AllocationStatsTracker = EmberObject.extend(AbstractStatsTracker, { // Set via the stats computed property macro allocation: null, - bufferSize: 100, - url: computed('allocation', function() { return `/v1/client/allocation/${this.get('allocation.id')}/stats`; }), diff --git a/ui/app/utils/classes/node-stats-tracker.js b/ui/app/utils/classes/node-stats-tracker.js index 2933553b3bc..ca6122d2f9d 100644 --- a/ui/app/utils/classes/node-stats-tracker.js +++ b/ui/app/utils/classes/node-stats-tracker.js @@ -14,8 +14,6 @@ const NodeStatsTracker = EmberObject.extend(AbstractStatsTracker, { // Set via the stats computed property macro node: null, - bufferSize: 100, - url: computed('node', function() { return `/v1/client/stats?node_id=${this.get('node.id')}`; }), diff --git a/ui/app/utils/classes/rolling-array.js b/ui/app/utils/classes/rolling-array.js index d8d945f13d8..329229bc897 100644 --- a/ui/app/utils/classes/rolling-array.js +++ b/ui/app/utils/classes/rolling-array.js @@ -15,25 +15,33 @@ export default function RollingArray(maxLength, ...items) { array._splice = array.splice; array._unshift = array.unshift; - array.push = function(...items) { - const returnValue = this._push(...items); + // All mutable array methods build on top of insertAt + array._insertAt = array.insertAt; + // Bring the length back down to maxLength by removing from the front + array._limit = function() { const surplus = this.length - this.maxLength; if (surplus > 0) { this.splice(0, surplus); } + }; - return Math.min(returnValue, this.maxLength); + array.push = function(...items) { + this._push(...items); + this._limit(); + return this.length; }; array.splice = function(...args) { const returnValue = this._splice(...args); + this._limit(); + return returnValue; + }; - const surplus = this.length - this.maxLength; - if (surplus > 0) { - this._splice(0, surplus); - } - + array.insertAt = function(...args) { + const returnValue = this._insertAt(...args); + this._limit(); + this.arrayContentDidChange(); return returnValue; }; From adc05976c2c7f97d46c214518d448b0b13e377d6 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 14 Sep 2018 09:38:17 -0700 Subject: [PATCH 09/27] Use the prototype instead of "private" property backups --- ui/app/utils/classes/rolling-array.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/ui/app/utils/classes/rolling-array.js b/ui/app/utils/classes/rolling-array.js index 329229bc897..02790b88e12 100644 --- a/ui/app/utils/classes/rolling-array.js +++ b/ui/app/utils/classes/rolling-array.js @@ -3,21 +3,18 @@ // When max length is surpassed, items are removed from // the front of the array. +// Native array methods +let { push, splice } = Array.prototype; + +// Ember array prototype extension +let { insertAt } = Array.prototype; + // Using Classes to extend Array is unsupported in Babel so this less // ideal approach is taken: https://babeljs.io/docs/en/caveats#classes export default function RollingArray(maxLength, ...items) { const array = new Array(...items); array.maxLength = maxLength; - // Capture the originals of each array method, but - // associate them with the array to prevent closures. - array._push = array.push; - array._splice = array.splice; - array._unshift = array.unshift; - - // All mutable array methods build on top of insertAt - array._insertAt = array.insertAt; - // Bring the length back down to maxLength by removing from the front array._limit = function() { const surplus = this.length - this.maxLength; @@ -27,19 +24,20 @@ export default function RollingArray(maxLength, ...items) { }; array.push = function(...items) { - this._push(...items); + push.apply(this, items); this._limit(); return this.length; }; array.splice = function(...args) { - const returnValue = this._splice(...args); + const returnValue = splice.apply(this, args); this._limit(); return returnValue; }; + // All mutable array methods build on top of insertAt array.insertAt = function(...args) { - const returnValue = this._insertAt(...args); + const returnValue = insertAt.apply(this, args); this._limit(); this.arrayContentDidChange(); return returnValue; From 79c866707299922a00d5918cc588f6536bc04acd Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 14 Sep 2018 10:19:35 -0700 Subject: [PATCH 10/27] Handle the length = 0 and length = 1 cases for activeDatum --- ui/app/components/line-chart.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index 3ef5b1908af..707c231dcca 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -244,6 +244,8 @@ export default Component.extend(WindowResizable, { 'data' ); + if (!data || !data.length) return; + // Map the mouse coordinate to the index in the data array const bisector = d3Array.bisector(d => d[xProp]).left; const x = xScale.invert(mouseX); @@ -253,8 +255,15 @@ export default Component.extend(WindowResizable, { const dLeft = data[index - 1]; const dRight = data[index]; - // Pick the closer point - const datum = x - dLeft[xProp] > dRight[xProp] - x ? dRight : dLeft; + let datum; + + // If there is only one point, it's the activeDatum + if (dLeft && !dRight) { + datum = dLeft; + } else { + // Pick the closer point + datum = x - dLeft[xProp] > dRight[xProp] - x ? dRight : dLeft; + } this.set('activeDatum', datum); this.set('tooltipPosition', { From 58349199c6bd69a9872e242298c46fdb7e7d5263 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 14 Sep 2018 10:20:33 -0700 Subject: [PATCH 11/27] Style the primary-metric pattern --- ui/app/components/primary-metric.js | 2 ++ ui/app/styles/components.scss | 1 + ui/app/styles/components/boxed-section.scss | 1 + ui/app/styles/components/primary-metric.scss | 30 +++++++++++++++++++ .../templates/components/primary-metric.hbs | 24 ++++++++------- 5 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 ui/app/styles/components/primary-metric.scss diff --git a/ui/app/components/primary-metric.js b/ui/app/components/primary-metric.js index 8423af248f0..b7d1873a13f 100644 --- a/ui/app/components/primary-metric.js +++ b/ui/app/components/primary-metric.js @@ -9,6 +9,8 @@ import { task, timeout } from 'ember-concurrency'; export default Component.extend({ token: service(), + classNames: ['primary-metric'], + // One of Node, Allocation, or TaskState resource: null, diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index 3b99ae2aea0..d64a988efd1 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -16,6 +16,7 @@ @import './components/node-status-light'; @import './components/nomad-logo'; @import './components/page-layout'; +@import './components/primary-metric'; @import './components/simple-list'; @import './components/status-text'; @import './components/timeline'; diff --git a/ui/app/styles/components/boxed-section.scss b/ui/app/styles/components/boxed-section.scss index b8f84f0a9ad..dacc562f5f1 100644 --- a/ui/app/styles/components/boxed-section.scss +++ b/ui/app/styles/components/boxed-section.scss @@ -34,6 +34,7 @@ & + .boxed-section-body { border-top: none; + padding-top: 0.75em; } } diff --git a/ui/app/styles/components/primary-metric.scss b/ui/app/styles/components/primary-metric.scss new file mode 100644 index 00000000000..e48b52d16e3 --- /dev/null +++ b/ui/app/styles/components/primary-metric.scss @@ -0,0 +1,30 @@ +.primary-metric { + background: $white-bis; + border-radius: $radius; + padding: 0.75em; + color: $grey-dark; + + .title { + color: $grey; + font-weight: $weight-normal; + } + + .primary-graphic { + height: 150px; + } + + .secondary-graphic { + padding: 0.75em; + padding-bottom: 0; + margin-bottom: 0; + + > .column { + padding: 0.5rem 0.75rem; + } + } + + .annotation { + padding: 0 0.75em; + margin-top: -0.75rem; + } +} diff --git a/ui/app/templates/components/primary-metric.hbs b/ui/app/templates/components/primary-metric.hbs index d8d4ab9acfa..4ae759b43d0 100644 --- a/ui/app/templates/components/primary-metric.hbs +++ b/ui/app/templates/components/primary-metric.hbs @@ -1,8 +1,8 @@ -{{metricLabel}} -
+

{{metricLabel}}

+
{{stats-time-series data=data chartClass=chartClass}}
-
+
- {{format-percentage data.lastObject.percent total=1}} + {{format-percentage data.lastObject.percent total=1}}
-{{#if (eq metric "cpu")}} - {{data.lastObject.used}} Mhz / {{reservedAmount}} Mhz reserved -{{else if (eq metric "memory")}} - {{format-bytes data.lastObject.used}} MiB / {{reservedAmount}} MiB reserved -{{else}} - {{data.lastObject.used}} / {{reservedAmount}} reserved -{{/if}} +
+ {{#if (eq metric "cpu")}} + {{data.lastObject.used}} Mhz / {{reservedAmount}} Mhz reserved + {{else if (eq metric "memory")}} + {{format-bytes data.lastObject.used}} / {{reservedAmount}} MiB reserved + {{else}} + {{data.lastObject.used}} / {{reservedAmount}} reserved + {{/if}} +
From f84b14540112d0cdc668d70870dac5bdfc56122e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 14 Sep 2018 10:21:01 -0700 Subject: [PATCH 12/27] Use the appropriate methods and types in the stat trackers --- .../utils/classes/allocation-stats-tracker.js | 20 +++++++++++-------- ui/app/utils/classes/node-stats-tracker.js | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/ui/app/utils/classes/allocation-stats-tracker.js b/ui/app/utils/classes/allocation-stats-tracker.js index d0a4422df9e..5053ccea4a1 100644 --- a/ui/app/utils/classes/allocation-stats-tracker.js +++ b/ui/app/utils/classes/allocation-stats-tracker.js @@ -19,16 +19,18 @@ const AllocationStatsTracker = EmberObject.extend(AbstractStatsTracker, { }), append(frame) { + const timestamp = new Date(Math.floor(frame.Timestamp / 1000000)); + const cpuUsed = Math.floor(frame.ResourceUsage.CpuStats.TotalTicks) || 0; - this.get('cpu').push({ - timestamp: frame.Timestamp, + this.get('cpu').pushObject({ + timestamp, used: cpuUsed, percent: percent(cpuUsed, this.get('reservedCPU')), }); const memoryUsed = frame.ResourceUsage.MemoryStats.RSS; - this.get('memory').push({ - timestamp: frame.Timestamp, + this.get('memory').pushObject({ + timestamp, used: memoryUsed, percent: percent(memoryUsed / 1024 / 1024, this.get('reservedMemory')), }); @@ -41,16 +43,18 @@ const AllocationStatsTracker = EmberObject.extend(AbstractStatsTracker, { // allocation, don't attempt to append data for the task. if (!stats) continue; + const frameTimestamp = new Date(Math.floor(taskFrame.Timestamp / 1000000)); + const taskCpuUsed = Math.floor(taskFrame.ResourceUsage.CpuStats.TotalTicks) || 0; - stats.cpu.push({ - timestamp: taskFrame.Timestamp, + stats.cpu.pushObject({ + timestamp: frameTimestamp, used: taskCpuUsed, percent: percent(taskCpuUsed, stats.reservedCPU), }); const taskMemoryUsed = taskFrame.ResourceUsage.MemoryStats.RSS; - stats.memory.push({ - timestamp: taskFrame.Timestamp, + stats.memory.pushObject({ + timestamp: frameTimestamp, used: taskMemoryUsed, percent: percent(taskMemoryUsed / 1024 / 1024, stats.reservedMemory), }); diff --git a/ui/app/utils/classes/node-stats-tracker.js b/ui/app/utils/classes/node-stats-tracker.js index ca6122d2f9d..3b2702d2385 100644 --- a/ui/app/utils/classes/node-stats-tracker.js +++ b/ui/app/utils/classes/node-stats-tracker.js @@ -22,14 +22,14 @@ const NodeStatsTracker = EmberObject.extend(AbstractStatsTracker, { const timestamp = new Date(Math.floor(frame.Timestamp / 1000000)); const cpuUsed = Math.floor(frame.CPUTicksConsumed) || 0; - this.get('cpu').addObject({ + this.get('cpu').pushObject({ timestamp, used: cpuUsed, percent: percent(cpuUsed, this.get('reservedCPU')), }); const memoryUsed = frame.Memory.Used; - this.get('memory').addObject({ + this.get('memory').pushObject({ timestamp, used: memoryUsed, percent: percent(memoryUsed / 1024 / 1024, this.get('reservedMemory')), From 509f42ca0c6eac2571fd314ae2834708b4a9192f Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 14 Sep 2018 10:21:28 -0700 Subject: [PATCH 13/27] Add resource utilization graphs to the allocation index page --- .../templates/allocations/allocation/index.hbs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 590e3bfafbb..72deebf802a 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -17,6 +17,22 @@
+
+
+ Resource Utilization +
+
+
+
+ {{primary-metric resource=model metric="cpu"}} +
+
+ {{primary-metric resource=model metric="memory"}} +
+
+
+
+
Tasks From 7c2484774912a00194c6220771b721f4f84b5dec Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 14 Sep 2018 10:21:48 -0700 Subject: [PATCH 14/27] Add resource utilization graphs to the task index page --- .../allocations/allocation/task/index.hbs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ui/app/templates/allocations/allocation/task/index.hbs b/ui/app/templates/allocations/allocation/task/index.hbs index 6746fb05bad..dad7a4a71fc 100644 --- a/ui/app/templates/allocations/allocation/task/index.hbs +++ b/ui/app/templates/allocations/allocation/task/index.hbs @@ -25,6 +25,22 @@
+
+
+ Resource Utilization +
+
+
+
+ {{primary-metric resource=model metric="cpu"}} +
+
+ {{primary-metric resource=model metric="memory"}} +
+
+
+
+ {{#if ports.length}}
From cf9490ce047bac880ebbfc80693b9dd5636f9764 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 17 Sep 2018 15:53:59 -0700 Subject: [PATCH 15/27] New service to manage stats trackers This solves two problems: 1. redundant trackers making redundant requests 2. trackers being obliterated as soon as the primary metric component is destroyed It introduces a new problem where visiting more and more node and allocation pages adds to an ever-growing list of trackers that can assume lots of memory, but it solves the problem by using a least-recently-used cache to limit the number of trackers tracked. --- ui/app/services/stats-trackers-registry.js | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 ui/app/services/stats-trackers-registry.js diff --git a/ui/app/services/stats-trackers-registry.js b/ui/app/services/stats-trackers-registry.js new file mode 100644 index 00000000000..81a8540b068 --- /dev/null +++ b/ui/app/services/stats-trackers-registry.js @@ -0,0 +1,43 @@ +import Service, { inject as service } from '@ember/service'; +import { LRUMap } from 'lru_map'; +import NodeStatsTracker from 'nomad-ui/utils/classes/node-stats-tracker'; +import AllocationStatsTracker from 'nomad-ui/utils/classes/allocation-stats-tracker'; + +// An unbounded number of stat trackers is a great way to gobble up all the memory +// on a machine. This max number is unscientific, but aims to balance losing +// stat trackers a user is likely to return to with preventing gc from freeing +// memory occupied by stat trackers a user is likely to no longer care about +const MAX_STAT_TRACKERS = 10; +let registry; + +export default Service.extend({ + token: service(), + + init() { + // The LRUMap limits the number of trackers tracked by making room for + // new entries beyond the limit by removing the least recently used entry. + registry = new LRUMap(MAX_STAT_TRACKERS); + }, + + getTracker(resource) { + if (!resource) return; + + const type = resource && resource.constructor.modelName; + const key = `${type}:${resource.get('id')}`; + + const cachedTracker = registry.get(key); + if (cachedTracker) return cachedTracker; + + const Constructor = type === 'node' ? NodeStatsTracker : AllocationStatsTracker; + const resourceProp = type === 'node' ? 'node' : 'allocation'; + + const tracker = Constructor.create({ + fetch: url => this.get('token').authorizedRequest(url), + [resourceProp]: resource, + }); + + registry.set(key, tracker); + + return tracker; + }, +}); From 1a6682d23d3a4cca438ed54683a1204f7836562b Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 17 Sep 2018 15:55:48 -0700 Subject: [PATCH 16/27] New LRUMap dep --- ui/package.json | 5 ++++- ui/yarn.lock | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ui/package.json b/ui/package.json index 483a73fef3f..e47c7febf93 100644 --- a/ui/package.json +++ b/ui/package.json @@ -31,8 +31,8 @@ "d3-array": "^1.2.0", "d3-axis": "^1.0.0", "d3-format": "^1.3.0", - "d3-selection": "^1.1.0", "d3-scale": "^1.0.0", + "d3-selection": "^1.1.0", "d3-shape": "^1.2.0", "d3-time-format": "^2.1.0", "d3-transition": "^1.1.0", @@ -94,5 +94,8 @@ "lib/bulma", "lib/calendar" ] + }, + "dependencies": { + "lru_map": "^0.3.3" } } diff --git a/ui/yarn.lock b/ui/yarn.lock index 943460d2014..d29c333a7fe 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -6245,6 +6245,10 @@ lru-cache@^4.1.1: pseudomap "^1.0.2" yallist "^2.1.2" +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" + make-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" From 81788cf411e7fa20450450085b66c16a9e9c194e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 17 Sep 2018 15:56:32 -0700 Subject: [PATCH 17/27] Clean up old controller code --- ui/app/controllers/allocations/allocation/index.js | 14 -------------- ui/app/controllers/clients/client.js | 3 --- ui/app/routes/allocations/allocation/index.js | 12 ------------ 3 files changed, 29 deletions(-) delete mode 100644 ui/app/routes/allocations/allocation/index.js diff --git a/ui/app/controllers/allocations/allocation/index.js b/ui/app/controllers/allocations/allocation/index.js index 04408660eaa..4d6697c1404 100644 --- a/ui/app/controllers/allocations/allocation/index.js +++ b/ui/app/controllers/allocations/allocation/index.js @@ -1,11 +1,8 @@ -import Ember from 'ember'; import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; -import { task, timeout } from 'ember-concurrency'; import Sortable from 'nomad-ui/mixins/sortable'; import { lazyClick } from 'nomad-ui/helpers/lazy-click'; -import { stats } from 'nomad-ui/utils/classes/allocation-stats-tracker'; export default Controller.extend(Sortable, { token: service(), @@ -21,17 +18,6 @@ export default Controller.extend(Sortable, { listToSort: alias('model.states'), sortedStates: alias('listSorted'), - stats: stats('model', function statsFetch() { - return url => this.get('token').authorizedRequest(url); - }), - - pollStats: task(function*() { - do { - yield this.get('stats').poll(); - yield timeout(1000); - } while (!Ember.testing); - }), - actions: { gotoTask(allocation, task) { this.transitionToRoute('allocations.allocation.task', task); diff --git a/ui/app/controllers/clients/client.js b/ui/app/controllers/clients/client.js index 7ded2c1be70..26dfc9b64da 100644 --- a/ui/app/controllers/clients/client.js +++ b/ui/app/controllers/clients/client.js @@ -1,11 +1,8 @@ -import Ember from 'ember'; import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; import { computed } from '@ember/object'; -import { task, timeout } from 'ember-concurrency'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; -import { stats } from 'nomad-ui/utils/classes/node-stats-tracker'; export default Controller.extend(Sortable, Searchable, { queryParams: { diff --git a/ui/app/routes/allocations/allocation/index.js b/ui/app/routes/allocations/allocation/index.js deleted file mode 100644 index 6d23253776b..00000000000 --- a/ui/app/routes/allocations/allocation/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import Route from '@ember/routing/route'; - -export default Route.extend({ - setupController(controller) { - this._super(...arguments); - controller.get('pollStats').perform(); - }, - - resetController(controller) { - controller.get('pollStats').cancelAll(); - }, -}); From 670b246f4c5034178b6e54402059aa6341923ed6 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 17 Sep 2018 15:57:58 -0700 Subject: [PATCH 18/27] Use the new stats tracker service to get stats trackers in primary metric --- ui/app/components/primary-metric.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/ui/app/components/primary-metric.js b/ui/app/components/primary-metric.js index b7d1873a13f..a46a7546199 100644 --- a/ui/app/components/primary-metric.js +++ b/ui/app/components/primary-metric.js @@ -2,12 +2,11 @@ import Ember from 'ember'; import Component from '@ember/component'; import { inject as service } from '@ember/service'; import { computed } from '@ember/object'; -import NodeStatsTracker from 'nomad-ui/utils/classes/node-stats-tracker'; -import AllocationStatsTracker from 'nomad-ui/utils/classes/allocation-stats-tracker'; import { task, timeout } from 'ember-concurrency'; export default Component.extend({ token: service(), + statsTrackersRegistry: service('stats-trackers-registry'), classNames: ['primary-metric'], @@ -20,15 +19,7 @@ export default Component.extend({ // An instance of a StatsTracker. An alternative interface to resource tracker: computed('trackedResource', 'type', function() { const resource = this.get('trackedResource'); - - if (!resource) return; - - const Constructor = this.get('type') === 'node' ? NodeStatsTracker : AllocationStatsTracker; - const resourceProp = this.get('type') === 'node' ? 'node' : 'allocation'; - return Constructor.create({ - fetch: url => this.get('token').authorizedRequest(url), - [resourceProp]: resource, - }); + return this.get('statsTrackersRegistry').getTracker(resource); }), type: computed('resource', function() { From f0208c0a2407ab6ca1f50900f11cedefd64b07bc Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 17 Sep 2018 15:58:28 -0700 Subject: [PATCH 19/27] Add request throttling to the abstract stats tracker This is the best of three options 1. Users of stats trackers control polling (old method) 2. Stat tracker is stateful and has start/stop methods (like logging) 3. Stat trackers blindly throttle requests This is the best option because it means N number of concurrent users of a stats tracker can request polling without inundating the tracker with redundant frames (or the network with redundant requests), but they also don't have to coordinate amongst themselves to determine what state a tracker should be in. --- ui/app/components/primary-metric.js | 4 ++-- ui/app/utils/classes/abstract-stats-tracker.js | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ui/app/components/primary-metric.js b/ui/app/components/primary-metric.js index a46a7546199..7aae5f33d70 100644 --- a/ui/app/components/primary-metric.js +++ b/ui/app/components/primary-metric.js @@ -79,8 +79,8 @@ export default Component.extend({ poller: task(function*() { do { - yield this.get('tracker').poll(); - yield timeout(2000); + yield this.get('tracker.poll').perform(); + yield timeout(10); } while (!Ember.testing); }), diff --git a/ui/app/utils/classes/abstract-stats-tracker.js b/ui/app/utils/classes/abstract-stats-tracker.js index 825851d22d7..f730936e04f 100644 --- a/ui/app/utils/classes/abstract-stats-tracker.js +++ b/ui/app/utils/classes/abstract-stats-tracker.js @@ -1,5 +1,6 @@ import Mixin from '@ember/object/mixin'; import { assert } from '@ember/debug'; +import { task, timeout } from 'ember-concurrency'; export default Mixin.create({ url: '', @@ -16,12 +17,18 @@ export default Mixin.create({ ); }, - poll() { + // Uses EC as a form of debounce to prevent multiple + // references to the same tracker from flooding the tracker, + // but also avoiding the issue where different places where the + // same tracker is used needs to coordinate. + poll: task(function*() { const url = this.get('url'); assert('Url must be defined', url); - return this.get('fetch')(url) + yield this.get('fetch')(url) .then(res => res.json()) .then(frame => this.append(frame)); - }, + + yield timeout(2000); + }).drop(), }); From cf57ddc89e1a1bd3998be6bf2d31c36102444d44 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 17 Sep 2018 16:58:26 -0700 Subject: [PATCH 20/27] Gap support for line charts --- ui/app/components/freestyle/sg-line-chart.js | 14 ++++++++++++++ ui/app/components/line-chart.js | 2 ++ .../components/freestyle/sg-line-chart.hbs | 10 ++++++++++ 3 files changed, 26 insertions(+) diff --git a/ui/app/components/freestyle/sg-line-chart.js b/ui/app/components/freestyle/sg-line-chart.js index 145bcbabeb0..1da331dc4f3 100644 --- a/ui/app/components/freestyle/sg-line-chart.js +++ b/ui/app/components/freestyle/sg-line-chart.js @@ -52,6 +52,20 @@ export default Component.extend({ ]; }), + lineChartGapData: computed(() => { + return [ + { year: 2010, value: 10 }, + { year: 2011, value: 10 }, + { year: 2012, value: null }, + { year: 2013, value: 30 }, + { year: 2014, value: 50 }, + { year: 2015, value: 80 }, + { year: 2016, value: null }, + { year: 2017, value: 210 }, + { year: 2018, value: 340 }, + ]; + }), + lineChartLive: computed(() => { return []; }), diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index 707c231dcca..6c2eb61c4ce 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -182,6 +182,7 @@ export default Component.extend(WindowResizable, { const line = d3Shape .line() + .defined(d => d[yProp] != null) .x(d => xScale(d[xProp])) .y(d => yScale(d[yProp])); @@ -198,6 +199,7 @@ export default Component.extend(WindowResizable, { const area = d3Shape .area() + .defined(d => d[yProp] != null) .x(d => xScale(d[xProp])) .y0(yScale(0)) .y1(d => yScale(d[yProp])); diff --git a/ui/app/templates/components/freestyle/sg-line-chart.hbs b/ui/app/templates/components/freestyle/sg-line-chart.hbs index 67dd939b01d..014b061bd2a 100644 --- a/ui/app/templates/components/freestyle/sg-line-chart.hbs +++ b/ui/app/templates/components/freestyle/sg-line-chart.hbs @@ -30,3 +30,13 @@ xFormat=secondsFormat}}
{{/freestyle-usage}} + +{{#freestyle-usage "line-chart-with-gaps" title="Data with gaps"}} +
+ {{line-chart + data=lineChartGapData + xProp="year" + yProp="value" + chartClass="is-primary"}} +
+{{/freestyle-usage}} From 8de545c1b73fb4ce5b62022bda18287c2a6b1384 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 17 Sep 2018 16:59:09 -0700 Subject: [PATCH 21/27] Add cancelation support to stats trackers --- ui/app/components/primary-metric.js | 5 +++-- ui/app/utils/classes/abstract-stats-tracker.js | 16 ++++++++++++++++ ui/app/utils/classes/allocation-stats-tracker.js | 12 ++++++++++++ ui/app/utils/classes/node-stats-tracker.js | 8 ++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/ui/app/components/primary-metric.js b/ui/app/components/primary-metric.js index 7aae5f33d70..c6df40daf33 100644 --- a/ui/app/components/primary-metric.js +++ b/ui/app/components/primary-metric.js @@ -79,8 +79,8 @@ export default Component.extend({ poller: task(function*() { do { - yield this.get('tracker.poll').perform(); - yield timeout(10); + this.get('tracker.poll').perform(); + yield timeout(100); } while (!Ember.testing); }), @@ -92,5 +92,6 @@ export default Component.extend({ willDestroy() { this.get('poller').cancelAll(); + this.get('tracker.signalPause').perform(); }, }); diff --git a/ui/app/utils/classes/abstract-stats-tracker.js b/ui/app/utils/classes/abstract-stats-tracker.js index f730936e04f..6ffb5ee4d17 100644 --- a/ui/app/utils/classes/abstract-stats-tracker.js +++ b/ui/app/utils/classes/abstract-stats-tracker.js @@ -17,11 +17,20 @@ export default Mixin.create({ ); }, + pause() { + assert( + 'StatsTrackers need a pause method, which takes no arguments but adds a frame of data at the current timestamp with null as the value' + ); + }, + // Uses EC as a form of debounce to prevent multiple // references to the same tracker from flooding the tracker, // but also avoiding the issue where different places where the // same tracker is used needs to coordinate. poll: task(function*() { + // Interrupt any pause attempt + this.get('signalPause').cancelAll(); + const url = this.get('url'); assert('Url must be defined', url); @@ -31,4 +40,11 @@ export default Mixin.create({ yield timeout(2000); }).drop(), + + signalPause: task(function*() { + // wait 2 seconds + yield timeout(2000); + // if no poll called in 2 seconds, pause + this.pause(); + }).drop(), }); diff --git a/ui/app/utils/classes/allocation-stats-tracker.js b/ui/app/utils/classes/allocation-stats-tracker.js index 5053ccea4a1..ecc88fbdc34 100644 --- a/ui/app/utils/classes/allocation-stats-tracker.js +++ b/ui/app/utils/classes/allocation-stats-tracker.js @@ -10,6 +10,8 @@ const percent = (numerator, denominator) => { return numerator / denominator; }; +const empty = ts => ({ timestamp: ts, used: null, percent: null }); + const AllocationStatsTracker = EmberObject.extend(AbstractStatsTracker, { // Set via the stats computed property macro allocation: null, @@ -61,6 +63,16 @@ const AllocationStatsTracker = EmberObject.extend(AbstractStatsTracker, { } }, + pause() { + const ts = new Date(); + this.get('memory').pushObject(empty(ts)); + this.get('cpu').pushObject(empty(ts)); + this.get('tasks').forEach(task => { + task.memory.pushObject(empty(ts)); + task.cpu.pushObject(empty(ts)); + }); + }, + // Static figures, denominators for stats reservedCPU: alias('allocation.taskGroup.reservedCPU'), reservedMemory: alias('allocation.taskGroup.reservedMemory'), diff --git a/ui/app/utils/classes/node-stats-tracker.js b/ui/app/utils/classes/node-stats-tracker.js index 3b2702d2385..b4ec9603d37 100644 --- a/ui/app/utils/classes/node-stats-tracker.js +++ b/ui/app/utils/classes/node-stats-tracker.js @@ -10,6 +10,8 @@ const percent = (numerator, denominator) => { return numerator / denominator; }; +const empty = ts => ({ timestamp: ts, used: null, percent: null }); + const NodeStatsTracker = EmberObject.extend(AbstractStatsTracker, { // Set via the stats computed property macro node: null, @@ -39,6 +41,12 @@ const NodeStatsTracker = EmberObject.extend(AbstractStatsTracker, { // this.notifyPropertyChange('memory'); }, + pause() { + const ts = new Date(); + this.get('memory').pushObject(empty(ts)); + this.get('cpu').pushObject(empty(ts)); + }, + // Static figures, denominators for stats reservedCPU: alias('node.resources.cpu'), reservedMemory: alias('node.resources.memory'), From 0ede4c512ea6b7186cf5b2810a09ac35a2f9f2c2 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 19 Sep 2018 14:15:32 -0700 Subject: [PATCH 22/27] Integration tests for the primary-metric component --- ui/app/templates/components/line-chart.hbs | 2 +- .../templates/components/primary-metric.hbs | 6 +- ui/tests/integration/primary-metric-test.js | 198 ++++++++++++++++++ 3 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 ui/tests/integration/primary-metric-test.js diff --git a/ui/app/templates/components/line-chart.hbs b/ui/app/templates/components/line-chart.hbs index 33e8dc19f14..eb993918d54 100644 --- a/ui/app/templates/components/line-chart.hbs +++ b/ui/app/templates/components/line-chart.hbs @@ -1,4 +1,4 @@ - + diff --git a/ui/app/templates/components/primary-metric.hbs b/ui/app/templates/components/primary-metric.hbs index 4ae759b43d0..dd2914dcd61 100644 --- a/ui/app/templates/components/primary-metric.hbs +++ b/ui/app/templates/components/primary-metric.hbs @@ -4,7 +4,7 @@
-
+
- {{format-percentage data.lastObject.percent total=1}} + {{format-percentage data.lastObject.percent total=1}}
-
+
{{#if (eq metric "cpu")}} {{data.lastObject.used}} Mhz / {{reservedAmount}} Mhz reserved {{else if (eq metric "memory")}} diff --git a/ui/tests/integration/primary-metric-test.js b/ui/tests/integration/primary-metric-test.js new file mode 100644 index 00000000000..d11c612b932 --- /dev/null +++ b/ui/tests/integration/primary-metric-test.js @@ -0,0 +1,198 @@ +import EmberObject, { computed } from '@ember/object'; +import Service from '@ember/service'; +import { getOwner } from '@ember/application'; +import { test, moduleForComponent } from 'ember-qunit'; +import wait from 'ember-test-helpers/wait'; +import hbs from 'htmlbars-inline-precompile'; +import { find } from 'ember-native-dom-helpers'; +import { task } from 'ember-concurrency'; +import sinon from 'sinon'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; + +moduleForComponent('primary-metric', 'Integration | Component | primary metric', { + integration: true, + beforeEach() { + fragmentSerializerInitializer(getOwner(this)); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + this.server.create('node'); + + const getTrackerSpy = (this.getTrackerSpy = sinon.spy()); + const trackerPollSpy = (this.trackerPollSpy = sinon.spy()); + const trackerSignalPauseSpy = (this.trackerSignalPauseSpy = sinon.spy()); + + const MockTracker = EmberObject.extend({ + poll: task(function*() { + yield trackerPollSpy(); + }), + signalPause: task(function*() { + yield trackerSignalPauseSpy(); + }), + + cpu: computed(() => []), + memory: computed(() => []), + }); + + const mockStatsTrackersRegistry = Service.extend({ + getTracker(...args) { + getTrackerSpy(...args); + return MockTracker.create(); + }, + }); + + this.register('service:stats-trackers-registry', mockStatsTrackersRegistry); + this.statsTrackersRegistry = getOwner(this).lookup('service:stats-trackers-registry'); + }, + afterEach() { + this.server.shutdown(); + }, +}); + +const commonTemplate = hbs` + {{primary-metric + resource=resource + metric=metric}} +`; + +test('Contains a line chart, a percentage bar, a percentage figure, and an absolute usage figure', function(assert) { + let resource; + const metric = 'cpu'; + + this.store.findAll('node'); + + return wait() + .then(() => { + resource = this.store.peekAll('node').get('firstObject'); + this.setProperties({ resource, metric }); + + this.render(commonTemplate); + return wait(); + }) + .then(() => { + assert.ok(find('[data-test-line-chart]'), 'Line chart'); + assert.ok(find('[data-test-percentage-bar]'), 'Percentage bar'); + assert.ok(find('[data-test-percentage]'), 'Percentage figure'); + assert.ok(find('[data-test-absolute-value]'), 'Absolute usage figure'); + }); +}); + +test('The CPU metric maps to is-info', function(assert) { + let resource; + const metric = 'cpu'; + + this.store.findAll('node'); + + return wait() + .then(() => { + resource = this.store.peekAll('node').get('firstObject'); + this.setProperties({ resource, metric }); + + this.render(commonTemplate); + return wait(); + }) + .then(() => { + assert.ok( + find('[data-test-line-chart] .canvas').classList.contains('is-info'), + 'Info class for CPU metric' + ); + }); +}); + +test('The Memory metric maps to is-danger', function(assert) { + let resource; + const metric = 'memory'; + + this.store.findAll('node'); + + return wait() + .then(() => { + resource = this.store.peekAll('node').get('firstObject'); + this.setProperties({ resource, metric }); + + this.render(commonTemplate); + return wait(); + }) + .then(() => { + assert.ok( + find('[data-test-line-chart] .canvas').classList.contains('is-danger'), + 'Danger class for Memory metric' + ); + }); +}); + +test('Gets the tracker from the tracker registry', function(assert) { + let resource; + const metric = 'cpu'; + + this.store.findAll('node'); + + return wait() + .then(() => { + resource = this.store.peekAll('node').get('firstObject'); + this.setProperties({ resource, metric }); + + this.render(commonTemplate); + return wait(); + }) + .then(() => { + assert.ok( + this.getTrackerSpy.calledWith(resource), + 'Uses the tracker registry to get the tracker for the provided resource' + ); + }); +}); + +test('Immediately polls the tracker', function(assert) { + let resource; + const metric = 'cpu'; + + this.store.findAll('node'); + + return wait() + .then(() => { + resource = this.store.peekAll('node').get('firstObject'); + this.setProperties({ resource, metric }); + + this.render(commonTemplate); + return wait(); + }) + .then(() => { + assert.ok(this.trackerPollSpy.calledOnce, 'The tracker is polled immediately'); + }); +}); + +test('A pause signal is sent to the tracker when the component is destroyed', function(assert) { + let resource; + const metric = 'cpu'; + + // Capture a reference to the spy before the component is destroyed + const trackerSignalPauseSpy = this.trackerSignalPauseSpy; + + this.store.findAll('node'); + + return wait() + .then(() => { + resource = this.store.peekAll('node').get('firstObject'); + this.setProperties({ resource, metric, showComponent: true }); + this.render(hbs` + {{#if showComponent}} + {{primary-metric + resource=resource + metric=metric}} + }} + {{/if}} + `); + return wait(); + }) + .then(() => { + assert.notOk(trackerSignalPauseSpy.called, 'No pause signal has been sent yet'); + // This will toggle the if statement, resulting the primary-metric component being destroyed. + this.set('showComponent', false); + return wait(); + }) + .then(() => { + assert.ok(trackerSignalPauseSpy.calledOnce, 'A pause signal is sent to the tracker'); + }); +}); From 01195a810cd24cae5e0a55587aa9804e8119af87 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 19 Sep 2018 15:19:06 -0700 Subject: [PATCH 23/27] Unit tests for the stats trackers service --- ui/app/services/stats-trackers-registry.js | 6 + ui/app/utils/classes/node-stats-tracker.js | 3 - .../services/stats-trackers-registry-test.js | 152 ++++++++++++++++++ 3 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 ui/tests/unit/services/stats-trackers-registry-test.js diff --git a/ui/app/services/stats-trackers-registry.js b/ui/app/services/stats-trackers-registry.js index 81a8540b068..0233c74d367 100644 --- a/ui/app/services/stats-trackers-registry.js +++ b/ui/app/services/stats-trackers-registry.js @@ -1,3 +1,4 @@ +import { computed } from '@ember/object'; import Service, { inject as service } from '@ember/service'; import { LRUMap } from 'lru_map'; import NodeStatsTracker from 'nomad-ui/utils/classes/node-stats-tracker'; @@ -19,6 +20,11 @@ export default Service.extend({ registry = new LRUMap(MAX_STAT_TRACKERS); }, + // A read-only way of getting a reference to the registry. + // Since this could be overwritten by a bad actor, it isn't + // used in getTracker + registryRef: computed(() => registry), + getTracker(resource) { if (!resource) return; diff --git a/ui/app/utils/classes/node-stats-tracker.js b/ui/app/utils/classes/node-stats-tracker.js index b4ec9603d37..e61fbc27fcc 100644 --- a/ui/app/utils/classes/node-stats-tracker.js +++ b/ui/app/utils/classes/node-stats-tracker.js @@ -36,9 +36,6 @@ const NodeStatsTracker = EmberObject.extend(AbstractStatsTracker, { used: memoryUsed, percent: percent(memoryUsed / 1024 / 1024, this.get('reservedMemory')), }); - - // this.notifyPropertyChange('cpu'); - // this.notifyPropertyChange('memory'); }, pause() { diff --git a/ui/tests/unit/services/stats-trackers-registry-test.js b/ui/tests/unit/services/stats-trackers-registry-test.js new file mode 100644 index 00000000000..9916fafcfb7 --- /dev/null +++ b/ui/tests/unit/services/stats-trackers-registry-test.js @@ -0,0 +1,152 @@ +import EmberObject from '@ember/object'; +import { getOwner } from '@ember/application'; +import Service from '@ember/service'; +import wait from 'ember-test-helpers/wait'; +import { moduleFor, test } from 'ember-qunit'; +import Pretender from 'pretender'; +import sinon from 'sinon'; +import fetch from 'nomad-ui/utils/fetch'; +import NodeStatsTracker from 'nomad-ui/utils/classes/node-stats-tracker'; + +moduleFor('service:stats-trackers-registry', 'Unit | Service | Stats Trackers Registry', { + beforeEach() { + // Inject a mock token service + const authorizedRequestSpy = (this.tokenAuthorizedRequestSpy = sinon.spy()); + const mockToken = Service.extend({ + authorizedRequest(url) { + authorizedRequestSpy(url); + return fetch(url); + }, + }); + + this.register('service:token', mockToken); + this.token = getOwner(this).lookup('service:token'); + this.server = new Pretender(function() { + this.get('/v1/client/stats', () => [ + 200, + {}, + JSON.stringify({ + Timestamp: 1234567890, + CPUTicksConsumed: 11, + Memory: { + Used: 12, + }, + }), + ]); + }); + }, + afterEach() { + this.server.shutdown(); + }, + subject() { + return getOwner(this) + .factoryFor('service:stats-trackers-registry') + .create(); + }, +}); + +const makeModelMock = (modelName, defaults) => { + const Class = EmberObject.extend(defaults); + Class.prototype.constructor.modelName = modelName; + return Class; +}; + +const mockNode = makeModelMock('node', { id: 'test' }); + +test('Creates a tracker when one isn’t found', function(assert) { + const registry = this.subject(); + const id = 'id'; + + assert.equal(registry.get('registryRef').size, 0, 'Nothing in the registry yet'); + + const tracker = registry.getTracker(mockNode.create({ id })); + assert.ok(tracker instanceof NodeStatsTracker, 'The correct type of tracker is made'); + assert.equal(registry.get('registryRef').size, 1, 'The tracker was added to the registry'); + assert.deepEqual( + Array.from(registry.get('registryRef').keys()), + [`node:${id}`], + 'The object in the registry has the correct key' + ); +}); + +test('Returns an existing tracker when one is found', function(assert) { + const registry = this.subject(); + const node = mockNode.create(); + + const tracker1 = registry.getTracker(node); + const tracker2 = registry.getTracker(node); + + assert.equal(tracker1, tracker2, 'Returns an existing tracker for the same resource'); + assert.equal(registry.get('registryRef').size, 1, 'Only one tracker in the registry'); +}); + +test('Registry does not depend on persistent object references', function(assert) { + const registry = this.subject(); + const id = 'some-id'; + + const node1 = mockNode.create({ id }); + const node2 = mockNode.create({ id }); + + assert.notEqual(node1, node2, 'Two different resources'); + assert.equal(node1.get('id'), node2.get('id'), 'With the same IDs'); + assert.equal(node1.constructor.modelName, node2.constructor.modelName, 'And the same className'); + + assert.equal(registry.getTracker(node1), registry.getTracker(node2), 'Return the same tracker'); + assert.equal(registry.get('registryRef').size, 1, 'Only one tracker in the registry'); +}); + +test('Has a max size', function(assert) { + const registry = this.subject(); + const ref = registry.get('registryRef'); + + // Kind of a silly assertion, but the exact limit is arbitrary. Whether it's 10 or 1000 + // isn't important as long as there is one. + assert.ok(ref.limit < Infinity, `A limit (${ref.limit}) is set`); +}); + +test('Removes least recently used when something needs to be removed', function(assert) { + const registry = this.subject(); + const activeNode = mockNode.create({ id: 'active' }); + const inactiveNode = mockNode.create({ id: 'inactive' }); + const limit = registry.get('registryRef').limit; + + // First put in the two tracked nodes + registry.getTracker(activeNode); + registry.getTracker(inactiveNode); + + for (let i = 0; i < limit; i++) { + // Add a new tracker to the registry + const newNode = mockNode.create({ id: `node-${i}` }); + registry.getTracker(newNode); + + // But read the active node tracker to keep it fresh + registry.getTracker(activeNode); + } + + const ref = registry.get('registryRef'); + assert.equal(ref.size, ref.limit, 'The limit was reached'); + + assert.ok( + ref.get('node:active'), + 'The active tracker is still in the registry despite being added first' + ); + assert.notOk( + ref.get('node:inactive'), + 'The inactive tracker got pushed out due to not being accessed' + ); +}); + +test('Trackers are created using the token authorizedRequest', function(assert) { + const registry = this.subject(); + const node = mockNode.create(); + + const tracker = registry.getTracker(node); + + tracker.get('poll').perform(); + assert.ok( + this.tokenAuthorizedRequestSpy.calledWith(`/v1/client/stats?node_id=${node.get('id')}`), + 'The token service authorizedRequest function was used' + ); + + return wait(); +}); From 28d8f797e6f47b335bae8bcca5aa14226ee07dc7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 19 Sep 2018 16:32:53 -0700 Subject: [PATCH 24/27] Handle the empty data cases --- ui/app/components/line-chart.js | 9 +++++---- ui/app/components/stats-time-series.js | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index 6c2eb61c4ce..e8846026d2e 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -87,17 +87,18 @@ export default Component.extend(WindowResizable, { xScale: computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset', function() { const xProp = this.get('xProp'); const scale = this.get('timeseries') ? d3Scale.scaleTime() : d3Scale.scaleLinear(); + const data = this.get('data'); - scale - .rangeRound([10, this.get('yAxisOffset')]) - .domain(d3Array.extent(this.get('data'), d => d[xProp])); + const domain = data.length ? d3Array.extent(this.get('data'), d => d[xProp]) : [0, 1]; + + scale.rangeRound([10, this.get('yAxisOffset')]).domain(domain); return scale; }), yScale: computed('data.[]', 'yProp', 'xAxisOffset', function() { const yProp = this.get('yProp'); - let max = d3Array.max(this.get('data'), d => d[yProp]); + let max = d3Array.max(this.get('data'), d => d[yProp]) || 1; if (max > 1) { max = nice(max); } diff --git a/ui/app/components/stats-time-series.js b/ui/app/components/stats-time-series.js index 403c6a7d7da..73076217182 100644 --- a/ui/app/components/stats-time-series.js +++ b/ui/app/components/stats-time-series.js @@ -22,12 +22,15 @@ export default LineChart.extend({ xScale: computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset', function() { const xProp = this.get('xProp'); const scale = this.get('timeseries') ? d3Scale.scaleTime() : d3Scale.scaleLinear(); + const data = this.get('data'); - const [low, high] = d3Array.extent(this.get('data'), d => d[xProp]); + const [low, high] = d3Array.extent(data, d => d[xProp]); const minLow = moment(high) .subtract(5, 'minutes') .toDate(); - scale.rangeRound([10, this.get('yAxisOffset')]).domain([Math.min(low, minLow), high]); + + const extent = data.length ? [Math.min(low, minLow), high] : [minLow, new Date()]; + scale.rangeRound([10, this.get('yAxisOffset')]).domain(extent); return scale; }), From f4ae9e19a3939f0da4a20c24522d982b7881bcce Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 19 Sep 2018 16:33:18 -0700 Subject: [PATCH 25/27] Always return valid dates for timestamps --- ui/mirage/factories/client-allocation-stats.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/mirage/factories/client-allocation-stats.js b/ui/mirage/factories/client-allocation-stats.js index e4573ed08d4..369fa416313 100644 --- a/ui/mirage/factories/client-allocation-stats.js +++ b/ui/mirage/factories/client-allocation-stats.js @@ -6,13 +6,15 @@ export default Factory.extend({ _taskNames: () => [], // Set by allocation + timestamp: () => Date.now() * 1000000, + tasks() { var hash = {}; this._taskNames.forEach(task => { hash[task] = { Pids: null, ResourceUsage: generateResources(), - Timestamp: Date.now(), + Timestamp: Date.now() * 1000000, }; }); return hash; From 866f650de896c1e880246b83d9a0f7472a9f932c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 19 Sep 2018 16:33:51 -0700 Subject: [PATCH 26/27] Acceptance test coverage for all the pages with resource utilization graphs --- ui/app/components/primary-metric.js | 2 ++ ui/app/templates/components/primary-metric.hbs | 2 +- ui/app/utils/classes/allocation-stats-tracker.js | 3 ++- ui/tests/acceptance/allocation-detail-test.js | 6 ++++++ ui/tests/acceptance/client-detail-test.js | 10 ++++++++++ ui/tests/acceptance/task-detail-test.js | 6 ++++++ ui/tests/pages/allocations/detail.js | 15 ++++++++++++++- ui/tests/pages/allocations/task/detail.js | 5 +++++ ui/tests/pages/clients/detail.js | 5 +++++ 9 files changed, 51 insertions(+), 3 deletions(-) diff --git a/ui/app/components/primary-metric.js b/ui/app/components/primary-metric.js index c6df40daf33..be5f3a61cdc 100644 --- a/ui/app/components/primary-metric.js +++ b/ui/app/components/primary-metric.js @@ -16,6 +16,8 @@ export default Component.extend({ // cpu or memory metric: null, + 'data-test-primary-metric': true, + // An instance of a StatsTracker. An alternative interface to resource tracker: computed('trackedResource', 'type', function() { const resource = this.get('trackedResource'); diff --git a/ui/app/templates/components/primary-metric.hbs b/ui/app/templates/components/primary-metric.hbs index dd2914dcd61..10b9c0c114a 100644 --- a/ui/app/templates/components/primary-metric.hbs +++ b/ui/app/templates/components/primary-metric.hbs @@ -1,4 +1,4 @@ -

{{metricLabel}}

+

{{metricLabel}}

{{stats-time-series data=data chartClass=chartClass}}
diff --git a/ui/app/utils/classes/allocation-stats-tracker.js b/ui/app/utils/classes/allocation-stats-tracker.js index ecc88fbdc34..432fb4b24ec 100644 --- a/ui/app/utils/classes/allocation-stats-tracker.js +++ b/ui/app/utils/classes/allocation-stats-tracker.js @@ -88,7 +88,8 @@ const AllocationStatsTracker = EmberObject.extend(AbstractStatsTracker, { tasks: computed('allocation', function() { const bufferSize = this.get('bufferSize'); - return this.get('allocation.taskGroup.tasks').map(task => ({ + const tasks = this.get('allocation.taskGroup.tasks') || []; + return tasks.map(task => ({ task: get(task, 'name'), // Static figures, denominators for stats diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js index ddd17e91b36..14447a14631 100644 --- a/ui/tests/acceptance/allocation-detail-test.js +++ b/ui/tests/acceptance/allocation-detail-test.js @@ -64,6 +64,12 @@ test('/allocation/:id should name the allocation and link to the corresponding j }); }); +test('/allocation/:id should include resource utilization graphs', function(assert) { + assert.equal(Allocation.resourceCharts.length, 2, 'Two resource utilization graphs'); + assert.equal(Allocation.resourceCharts.objectAt(0).name, 'CPU', 'First chart is CPU'); + assert.equal(Allocation.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory'); +}); + test('/allocation/:id should list all tasks for the allocation', function(assert) { assert.equal( Allocation.tasks.length, diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js index 1c766d429c3..b13ce5ade54 100644 --- a/ui/tests/acceptance/client-detail-test.js +++ b/ui/tests/acceptance/client-detail-test.js @@ -93,6 +93,16 @@ test('/clients/:id should list additional detail for the node below the title', }); }); +test('/clients/:id should include resource utilization graphs', function(assert) { + ClientDetail.visit({ id: node.id }); + + andThen(() => { + assert.equal(ClientDetail.resourceCharts.length, 2, 'Two resource utilization graphs'); + assert.equal(ClientDetail.resourceCharts.objectAt(0).name, 'CPU', 'First chart is CPU'); + assert.equal(ClientDetail.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory'); + }); +}); + test('/clients/:id should list all allocations on the node', function(assert) { const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length; diff --git a/ui/tests/acceptance/task-detail-test.js b/ui/tests/acceptance/task-detail-test.js index cc30097f708..7d7df686df4 100644 --- a/ui/tests/acceptance/task-detail-test.js +++ b/ui/tests/acceptance/task-detail-test.js @@ -94,6 +94,12 @@ test('breadcrumbs match jobs / job / task group / allocation / task', function(a }); }); +test('/allocation/:id/:task_name should include resource utilization graphs', function(assert) { + assert.equal(Task.resourceCharts.length, 2, 'Two resource utilization graphs'); + assert.equal(Task.resourceCharts.objectAt(0).name, 'CPU', 'First chart is CPU'); + assert.equal(Task.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory'); +}); + test('the addresses table lists all reserved and dynamic ports', function(assert) { const taskResources = allocation.taskResourcesIds .map(id => server.db.taskResources.find(id)) diff --git a/ui/tests/pages/allocations/detail.js b/ui/tests/pages/allocations/detail.js index 26ec8fe734e..c824f961036 100644 --- a/ui/tests/pages/allocations/detail.js +++ b/ui/tests/pages/allocations/detail.js @@ -1,4 +1,12 @@ -import { clickable, create, collection, isPresent, text, visitable } from 'ember-cli-page-object'; +import { + attribute, + clickable, + create, + collection, + isPresent, + text, + visitable, +} from 'ember-cli-page-object'; export default create({ visit: visitable('/allocations/:id'), @@ -15,6 +23,11 @@ export default create({ visitClient: clickable('[data-test-client-link]'), }, + resourceCharts: collection('[data-test-primary-metric]', { + name: text('[data-test-primary-metric-title]'), + chartClass: attribute('class', '[data-test-percentage-chart] progress'), + }), + tasks: collection('[data-test-task-row]', { name: text('[data-test-name]'), state: text('[data-test-state]'), diff --git a/ui/tests/pages/allocations/task/detail.js b/ui/tests/pages/allocations/task/detail.js index 80ef65ee0bf..249986cfb92 100644 --- a/ui/tests/pages/allocations/task/detail.js +++ b/ui/tests/pages/allocations/task/detail.js @@ -25,6 +25,11 @@ export default create({ return this.breadcrumbs.toArray().find(crumb => crumb.id === id); }, + resourceCharts: collection('[data-test-primary-metric]', { + name: text('[data-test-primary-metric-title]'), + chartClass: attribute('class', '[data-test-percentage-chart] progress'), + }), + hasAddresses: isPresent('[data-test-task-addresses]'), addresses: collection('[data-test-task-address]', { name: text('[data-test-task-address-name]'), diff --git a/ui/tests/pages/clients/detail.js b/ui/tests/pages/clients/detail.js index 309a3f176d0..c4b98d9ded3 100644 --- a/ui/tests/pages/clients/detail.js +++ b/ui/tests/pages/clients/detail.js @@ -38,6 +38,11 @@ export default create({ eligibilityDefinition: text('[data-test-eligibility]'), datacenterDefinition: text('[data-test-datacenter-definition]'), + resourceCharts: collection('[data-test-primary-metric]', { + name: text('[data-test-primary-metric-title]'), + chartClass: attribute('class', '[data-test-percentage-chart] progress'), + }), + ...allocations(), attributesTable: isPresent('[data-test-attributes]'), From 47ec74eb3ade984109c6a6b6cbb745aa0ea903a1 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 19 Sep 2018 19:30:18 -0700 Subject: [PATCH 27/27] Update stat tracker unit tests --- .../utils/classes/abstract-stats-tracker.js | 16 ++- .../utils/allocation-stats-tracker-test.js | 111 +++++++++++------- .../unit/utils/node-stats-tracker-test.js | 27 +++-- 3 files changed, 92 insertions(+), 62 deletions(-) diff --git a/ui/app/utils/classes/abstract-stats-tracker.js b/ui/app/utils/classes/abstract-stats-tracker.js index 6ffb5ee4d17..08a8f03f556 100644 --- a/ui/app/utils/classes/abstract-stats-tracker.js +++ b/ui/app/utils/classes/abstract-stats-tracker.js @@ -31,12 +31,16 @@ export default Mixin.create({ // Interrupt any pause attempt this.get('signalPause').cancelAll(); - const url = this.get('url'); - assert('Url must be defined', url); - - yield this.get('fetch')(url) - .then(res => res.json()) - .then(frame => this.append(frame)); + try { + const url = this.get('url'); + assert('Url must be defined', url); + + yield this.get('fetch')(url) + .then(res => res.json()) + .then(frame => this.append(frame)); + } catch (error) { + throw new Error(error); + } yield timeout(2000); }).drop(), diff --git a/ui/tests/unit/utils/allocation-stats-tracker-test.js b/ui/tests/unit/utils/allocation-stats-tracker-test.js index 51a041ffe91..cbfa181fa4f 100644 --- a/ui/tests/unit/utils/allocation-stats-tracker-test.js +++ b/ui/tests/unit/utils/allocation-stats-tracker-test.js @@ -9,7 +9,8 @@ import fetch from 'nomad-ui/utils/fetch'; module('Unit | Util | AllocationStatsTracker'); -const refDate = Date.now(); +const refDate = Date.now() * 1000000; +const makeDate = ts => new Date(ts / 1000000); const MockAllocation = overrides => assign( @@ -91,7 +92,7 @@ test('the AllocationStatsTracker constructor expects a fetch definition and an a const tracker = AllocationStatsTracker.create(); assert.throws( () => { - tracker.poll(); + tracker.fetch(); }, /StatsTrackers need a fetch method/, 'Polling does not work without a fetch method provided' @@ -159,7 +160,7 @@ test('poll results in requesting the url and calling append with the resulting J this.get('/v1/client/allocation/:id/stats', () => [200, {}, JSON.stringify(mockFrame)]); }); - tracker.poll(); + tracker.get('poll').perform(); assert.equal(server.handledRequests.length, 1, 'Only one request was made'); assert.equal( @@ -199,12 +200,18 @@ test('append appropriately maps a data frame to the tracked stats for cpu and me assert.deepEqual( tracker.get('cpu'), - [{ timestamp: refDate + 1000, used: 101, percent: 101 / 200 }], + [{ timestamp: makeDate(refDate + 1000), used: 101, percent: 101 / 200 }], 'One frame of cpu' ); assert.deepEqual( tracker.get('memory'), - [{ timestamp: refDate + 1000, used: 401 * 1024 * 1024, percent: 401 / 512 }], + [ + { + timestamp: makeDate(refDate + 1000), + used: 401 * 1024 * 1024, + percent: 401 / 512, + }, + ], 'One frame of memory' ); @@ -215,22 +222,40 @@ test('append appropriately maps a data frame to the tracked stats for cpu and me task: 'service', reservedCPU: 100, reservedMemory: 256, - cpu: [{ timestamp: refDate + 1, used: 51, percent: 51 / 100 }], - memory: [{ timestamp: refDate + 1, used: 101 * 1024 * 1024, percent: 101 / 256 }], + cpu: [{ timestamp: makeDate(refDate + 1), used: 51, percent: 51 / 100 }], + memory: [ + { + timestamp: makeDate(refDate + 1), + used: 101 * 1024 * 1024, + percent: 101 / 256, + }, + ], }, { task: 'log-shipper', reservedCPU: 50, reservedMemory: 128, - cpu: [{ timestamp: refDate + 10, used: 26, percent: 26 / 50 }], - memory: [{ timestamp: refDate + 10, used: 51 * 1024 * 1024, percent: 51 / 128 }], + cpu: [{ timestamp: makeDate(refDate + 10), used: 26, percent: 26 / 50 }], + memory: [ + { + timestamp: makeDate(refDate + 10), + used: 51 * 1024 * 1024, + percent: 51 / 128, + }, + ], }, { task: 'sidecar', reservedCPU: 50, reservedMemory: 128, - cpu: [{ timestamp: refDate + 100, used: 27, percent: 27 / 50 }], - memory: [{ timestamp: refDate + 100, used: 52 * 1024 * 1024, percent: 52 / 128 }], + cpu: [{ timestamp: makeDate(refDate + 100), used: 27, percent: 27 / 50 }], + memory: [ + { + timestamp: makeDate(refDate + 100), + used: 52 * 1024 * 1024, + percent: 52 / 128, + }, + ], }, ], 'tasks represents the tasks for the allocation, each with one frame of stats' @@ -241,16 +266,16 @@ test('append appropriately maps a data frame to the tracked stats for cpu and me assert.deepEqual( tracker.get('cpu'), [ - { timestamp: refDate + 1000, used: 101, percent: 101 / 200 }, - { timestamp: refDate + 2000, used: 102, percent: 102 / 200 }, + { timestamp: makeDate(refDate + 1000), used: 101, percent: 101 / 200 }, + { timestamp: makeDate(refDate + 2000), used: 102, percent: 102 / 200 }, ], 'Two frames of cpu' ); assert.deepEqual( tracker.get('memory'), [ - { timestamp: refDate + 1000, used: 401 * 1024 * 1024, percent: 401 / 512 }, - { timestamp: refDate + 2000, used: 402 * 1024 * 1024, percent: 402 / 512 }, + { timestamp: makeDate(refDate + 1000), used: 401 * 1024 * 1024, percent: 401 / 512 }, + { timestamp: makeDate(refDate + 2000), used: 402 * 1024 * 1024, percent: 402 / 512 }, ], 'Two frames of memory' ); @@ -263,12 +288,12 @@ test('append appropriately maps a data frame to the tracked stats for cpu and me reservedCPU: 100, reservedMemory: 256, cpu: [ - { timestamp: refDate + 1, used: 51, percent: 51 / 100 }, - { timestamp: refDate + 2, used: 52, percent: 52 / 100 }, + { timestamp: makeDate(refDate + 1), used: 51, percent: 51 / 100 }, + { timestamp: makeDate(refDate + 2), used: 52, percent: 52 / 100 }, ], memory: [ - { timestamp: refDate + 1, used: 101 * 1024 * 1024, percent: 101 / 256 }, - { timestamp: refDate + 2, used: 102 * 1024 * 1024, percent: 102 / 256 }, + { timestamp: makeDate(refDate + 1), used: 101 * 1024 * 1024, percent: 101 / 256 }, + { timestamp: makeDate(refDate + 2), used: 102 * 1024 * 1024, percent: 102 / 256 }, ], }, { @@ -276,12 +301,12 @@ test('append appropriately maps a data frame to the tracked stats for cpu and me reservedCPU: 50, reservedMemory: 128, cpu: [ - { timestamp: refDate + 10, used: 26, percent: 26 / 50 }, - { timestamp: refDate + 20, used: 27, percent: 27 / 50 }, + { timestamp: makeDate(refDate + 10), used: 26, percent: 26 / 50 }, + { timestamp: makeDate(refDate + 20), used: 27, percent: 27 / 50 }, ], memory: [ - { timestamp: refDate + 10, used: 51 * 1024 * 1024, percent: 51 / 128 }, - { timestamp: refDate + 20, used: 52 * 1024 * 1024, percent: 52 / 128 }, + { timestamp: makeDate(refDate + 10), used: 51 * 1024 * 1024, percent: 51 / 128 }, + { timestamp: makeDate(refDate + 20), used: 52 * 1024 * 1024, percent: 52 / 128 }, ], }, { @@ -289,12 +314,12 @@ test('append appropriately maps a data frame to the tracked stats for cpu and me reservedCPU: 50, reservedMemory: 128, cpu: [ - { timestamp: refDate + 100, used: 27, percent: 27 / 50 }, - { timestamp: refDate + 200, used: 28, percent: 28 / 50 }, + { timestamp: makeDate(refDate + 100), used: 27, percent: 27 / 50 }, + { timestamp: makeDate(refDate + 200), used: 28, percent: 28 / 50 }, ], memory: [ - { timestamp: refDate + 100, used: 52 * 1024 * 1024, percent: 52 / 128 }, - { timestamp: refDate + 200, used: 53 * 1024 * 1024, percent: 53 / 128 }, + { timestamp: makeDate(refDate + 100), used: 52 * 1024 * 1024, percent: 52 / 128 }, + { timestamp: makeDate(refDate + 200), used: 53 * 1024 * 1024, percent: 53 / 128 }, ], }, ], @@ -323,13 +348,13 @@ test('each stat list has maxLength equal to bufferSize', function(assert) { ); assert.equal( - tracker.get('cpu')[0].timestamp, - refDate + 11000, + +tracker.get('cpu')[0].timestamp, + +makeDate(refDate + 11000), 'Old frames are removed in favor of newer ones' ); assert.equal( - tracker.get('memory')[0].timestamp, - refDate + 11000, + +tracker.get('memory')[0].timestamp, + +makeDate(refDate + 11000), 'Old frames are removed in favor of newer ones' ); @@ -347,35 +372,35 @@ test('each stat list has maxLength equal to bufferSize', function(assert) { }); assert.equal( - tracker.get('tasks').findBy('task', 'service').cpu[0].timestamp, - refDate + 11, + +tracker.get('tasks').findBy('task', 'service').cpu[0].timestamp, + +makeDate(refDate + 11), 'Old frames are removed in favor of newer ones' ); assert.equal( - tracker.get('tasks').findBy('task', 'service').memory[0].timestamp, - refDate + 11, + +tracker.get('tasks').findBy('task', 'service').memory[0].timestamp, + +makeDate(refDate + 11), 'Old frames are removed in favor of newer ones' ); assert.equal( - tracker.get('tasks').findBy('task', 'log-shipper').cpu[0].timestamp, - refDate + 110, + +tracker.get('tasks').findBy('task', 'log-shipper').cpu[0].timestamp, + +makeDate(refDate + 110), 'Old frames are removed in favor of newer ones' ); assert.equal( - tracker.get('tasks').findBy('task', 'log-shipper').memory[0].timestamp, - refDate + 110, + +tracker.get('tasks').findBy('task', 'log-shipper').memory[0].timestamp, + +makeDate(refDate + 110), 'Old frames are removed in favor of newer ones' ); assert.equal( - tracker.get('tasks').findBy('task', 'sidecar').cpu[0].timestamp, - refDate + 1100, + +tracker.get('tasks').findBy('task', 'sidecar').cpu[0].timestamp, + +makeDate(refDate + 1100), 'Old frames are removed in favor of newer ones' ); assert.equal( - tracker.get('tasks').findBy('task', 'sidecar').memory[0].timestamp, - refDate + 1100, + +tracker.get('tasks').findBy('task', 'sidecar').memory[0].timestamp, + +makeDate(refDate + 1100), 'Old frames are removed in favor of newer ones' ); }); diff --git a/ui/tests/unit/utils/node-stats-tracker-test.js b/ui/tests/unit/utils/node-stats-tracker-test.js index 55c6c070fbd..5dfbb19ff41 100644 --- a/ui/tests/unit/utils/node-stats-tracker-test.js +++ b/ui/tests/unit/utils/node-stats-tracker-test.js @@ -9,7 +9,8 @@ import fetch from 'nomad-ui/utils/fetch'; module('Unit | Util | NodeStatsTracker'); -const refDate = Date.now(); +const refDate = Date.now() * 1000000; +const makeDate = ts => new Date(ts / 1000000); const MockNode = overrides => assign( @@ -35,7 +36,7 @@ test('the NodeStatsTracker constructor expects a fetch definition and a node', f const tracker = NodeStatsTracker.create(); assert.throws( () => { - tracker.poll(); + tracker.fetch(); }, /StatsTrackers need a fetch method/, 'Polling does not work without a fetch method provided' @@ -79,7 +80,7 @@ test('poll results in requesting the url and calling append with the resulting J this.get('/v1/client/stats', () => [200, {}, JSON.stringify(mockFrame)]); }); - tracker.poll(); + tracker.get('poll').perform(); assert.equal(server.handledRequests.length, 1, 'Only one request was made'); assert.equal( @@ -109,13 +110,13 @@ test('append appropriately maps a data frame to the tracked stats for cpu and me assert.deepEqual( tracker.get('cpu'), - [{ timestamp: refDate + 1, used: 1001, percent: 1001 / 2000 }], + [{ timestamp: makeDate(refDate + 1), used: 1001, percent: 1001 / 2000 }], 'One frame of cpu' ); assert.deepEqual( tracker.get('memory'), - [{ timestamp: refDate + 1, used: 2049 * 1024 * 1024, percent: 2049 / 4096 }], + [{ timestamp: makeDate(refDate + 1), used: 2049 * 1024 * 1024, percent: 2049 / 4096 }], 'One frame of memory' ); @@ -124,8 +125,8 @@ test('append appropriately maps a data frame to the tracked stats for cpu and me assert.deepEqual( tracker.get('cpu'), [ - { timestamp: refDate + 1, used: 1001, percent: 1001 / 2000 }, - { timestamp: refDate + 2, used: 1002, percent: 1002 / 2000 }, + { timestamp: makeDate(refDate + 1), used: 1001, percent: 1001 / 2000 }, + { timestamp: makeDate(refDate + 2), used: 1002, percent: 1002 / 2000 }, ], 'Two frames of cpu' ); @@ -133,8 +134,8 @@ test('append appropriately maps a data frame to the tracked stats for cpu and me assert.deepEqual( tracker.get('memory'), [ - { timestamp: refDate + 1, used: 2049 * 1024 * 1024, percent: 2049 / 4096 }, - { timestamp: refDate + 2, used: 2050 * 1024 * 1024, percent: 2050 / 4096 }, + { timestamp: makeDate(refDate + 1), used: 2049 * 1024 * 1024, percent: 2049 / 4096 }, + { timestamp: makeDate(refDate + 2), used: 2050 * 1024 * 1024, percent: 2050 / 4096 }, ], 'Two frames of memory' ); @@ -161,13 +162,13 @@ test('each stat list has maxLength equal to bufferSize', function(assert) { ); assert.equal( - tracker.get('cpu')[0].timestamp, - refDate + 11, + +tracker.get('cpu')[0].timestamp, + +makeDate(refDate + 11), 'Old frames are removed in favor of newer ones' ); assert.equal( - tracker.get('memory')[0].timestamp, - refDate + 11, + +tracker.get('memory')[0].timestamp, + +makeDate(refDate + 11), 'Old frames are removed in favor of newer ones' ); });