Skip to content

Commit

Permalink
Merge pull request #4635 from hashicorp/f-ui-stat-trackers
Browse files Browse the repository at this point in the history
UI: Stats trackers
  • Loading branch information
DingoEatingFuzz authored Sep 13, 2018
2 parents 95855f0 + f8c8c3c commit 1002eeb
Show file tree
Hide file tree
Showing 13 changed files with 1,038 additions and 1 deletion.
17 changes: 17 additions & 0 deletions ui/app/controllers/allocations/allocation/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
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(),

queryParams: {
sortProperty: 'sort',
sortDescending: 'desc',
Expand All @@ -15,6 +21,17 @@ 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);
Expand Down
14 changes: 14 additions & 0 deletions ui/app/controllers/clients/client.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
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: {
Expand Down Expand Up @@ -34,6 +37,17 @@ 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);
Expand Down
12 changes: 12 additions & 0 deletions ui/app/routes/allocations/allocation/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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();
},
});
11 changes: 11 additions & 0 deletions ui/app/routes/clients/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,15 @@ 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();
},
});
27 changes: 27 additions & 0 deletions ui/app/utils/classes/abstract-stats-tracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Mixin from '@ember/object/mixin';
import { assert } from '@ember/debug';

export default Mixin.create({
url: '',

fetch() {
assert('StatsTrackers need a fetch method, which should have an interface like window.fetch');
},

append(/* frame */) {
assert(
'StatsTrackers need an append method, which takes the JSON response from a request to url as an argument'
);
},

poll() {
const url = this.get('url');
assert('Url must be defined', url);

return this.get('fetch')(url)
.then(res => {
return res.json();
})
.then(frame => this.append(frame));
},
});
101 changes: 101 additions & 0 deletions ui/app/utils/classes/allocation-stats-tracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import EmberObject, { computed, get } from '@ember/object';
import { alias } from '@ember/object/computed';
import RollingArray from 'nomad-ui/utils/classes/rolling-array';
import AbstractStatsTracker from 'nomad-ui/utils/classes/abstract-stats-tracker';

const percent = (numerator, denominator) => {
if (!numerator || !denominator) {
return 0;
}
return numerator / denominator;
};

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`;
}),

append(frame) {
const cpuUsed = Math.floor(frame.ResourceUsage.CpuStats.TotalTicks) || 0;
this.get('cpu').push({
timestamp: frame.Timestamp,
used: cpuUsed,
percent: percent(cpuUsed, this.get('reservedCPU')),
});

const memoryUsed = frame.ResourceUsage.MemoryStats.RSS;
this.get('memory').push({
timestamp: frame.Timestamp,
used: memoryUsed,
percent: percent(memoryUsed / 1024 / 1024, this.get('reservedMemory')),
});

for (var taskName in frame.Tasks) {
const taskFrame = frame.Tasks[taskName];
const stats = this.get('tasks').findBy('task', taskName);

// If for whatever reason there is a task in the frame data that isn't in the
// allocation, don't attempt to append data for the task.
if (!stats) continue;

const taskCpuUsed = Math.floor(taskFrame.ResourceUsage.CpuStats.TotalTicks) || 0;
stats.cpu.push({
timestamp: taskFrame.Timestamp,
used: taskCpuUsed,
percent: percent(taskCpuUsed, stats.reservedCPU),
});

const taskMemoryUsed = taskFrame.ResourceUsage.MemoryStats.RSS;
stats.memory.push({
timestamp: taskFrame.Timestamp,
used: taskMemoryUsed,
percent: percent(taskMemoryUsed / 1024 / 1024, stats.reservedMemory),
});
}
},

// Static figures, denominators for stats
reservedCPU: alias('allocation.taskGroup.reservedCPU'),
reservedMemory: alias('allocation.taskGroup.reservedMemory'),

// Dynamic figures, collected over time
// []{ timestamp: Date, used: Number, percent: Number }
cpu: computed('allocation', function() {
return RollingArray(this.get('bufferSize'));
}),
memory: computed('allocation', function() {
return RollingArray(this.get('bufferSize'));
}),

tasks: computed('allocation', function() {
const bufferSize = this.get('bufferSize');
return this.get('allocation.taskGroup.tasks').map(task => ({
task: get(task, 'name'),

// Static figures, denominators for stats
reservedCPU: get(task, 'reservedCPU'),
reservedMemory: get(task, 'reservedMemory'),

// Dynamic figures, collected over time
// []{ timestamp: Date, used: Number, percent: Number }
cpu: RollingArray(bufferSize),
memory: RollingArray(bufferSize),
}));
}),
});

export default AllocationStatsTracker;

export function stats(allocationProp, fetch) {
return computed(allocationProp, function() {
return AllocationStatsTracker.create({
fetch: fetch.call(this),
allocation: this.get(allocationProp),
});
});
}
62 changes: 62 additions & 0 deletions ui/app/utils/classes/node-stats-tracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import EmberObject, { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import RollingArray from 'nomad-ui/utils/classes/rolling-array';
import AbstractStatsTracker from 'nomad-ui/utils/classes/abstract-stats-tracker';

const percent = (numerator, denominator) => {
if (!numerator || !denominator) {
return 0;
}
return numerator / denominator;
};

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')}`;
}),

append(frame) {
const cpuUsed = Math.floor(frame.CPUTicksConsumed) || 0;
this.get('cpu').push({
timestamp: frame.Timestamp,
used: cpuUsed,
percent: percent(cpuUsed, this.get('reservedCPU')),
});

const memoryUsed = frame.Memory.Used;
this.get('memory').push({
timestamp: frame.Timestamp,
used: memoryUsed,
percent: percent(memoryUsed / 1024 / 1024, this.get('reservedMemory')),
});
},

// Static figures, denominators for stats
reservedCPU: alias('node.resources.cpu'),
reservedMemory: alias('node.resources.memory'),

// Dynamic figures, collected over time
// []{ timestamp: Date, used: Number, percent: Number }
cpu: computed('node', function() {
return RollingArray(this.get('bufferSize'));
}),
memory: computed('node', function() {
return RollingArray(this.get('bufferSize'));
}),
});

export default NodeStatsTracker;

export function stats(nodeProp, fetch) {
return computed(nodeProp, function() {
return NodeStatsTracker.create({
fetch: fetch.call(this),
node: this.get(nodeProp),
});
});
}
45 changes: 45 additions & 0 deletions ui/app/utils/classes/rolling-array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// An array with a max length.
//
// When max length is surpassed, items are removed from
// the front of the array.

// 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;

array.push = function(...items) {
const returnValue = this._push(...items);

const surplus = this.length - this.maxLength;
if (surplus > 0) {
this.splice(0, surplus);
}

return Math.min(returnValue, this.maxLength);
};

array.splice = function(...args) {
const returnValue = this._splice(...args);

const surplus = this.length - this.maxLength;
if (surplus > 0) {
this._splice(0, surplus);
}

return returnValue;
};

array.unshift = function() {
throw new Error('Cannot unshift onto a RollingArray');
};

return array;
}
2 changes: 1 addition & 1 deletion ui/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export default function() {
this.get('/client/allocation/:id/stats', clientAllocationStatsHandler);
this.get('/client/fs/logs/:allocation_id', clientAllocationLog);

this.get('/client/v1/client/stats', function({ clientStats }, { queryParams }) {
this.get('/client/stats', function({ clientStats }, { queryParams }) {
return this.serialize(clientStats.find(queryParams.node_id));
});

Expand Down
4 changes: 4 additions & 0 deletions ui/mirage/factories/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ export default Factory.extend({
node.update({
eventIds: events.mapBy('id'),
});

server.create('client-stats', {
id: node.id,
});
},
});

Expand Down
Loading

0 comments on commit 1002eeb

Please sign in to comment.