-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UI: Stats trackers #4635
UI: Stats trackers #4635
Changes from all commits
a8480fa
5c5e44d
1df44a6
0082272
d25c0d6
405cf82
c455a39
3c09777
f8c8c3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
}, | ||
}); |
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)); | ||
}, | ||
}); |
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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the difference between these and the cpu + memory that are returned on the hash from the tasks computed prop? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are aggregates for the whole allocation. In the case where there is only one task, they're in theory the same value. |
||
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), | ||
}); | ||
}); | ||
} |
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), | ||
}); | ||
}); | ||
} |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there any additional implications since ember has prototype extensions on? Seems like you would still be able to call ArrayProxy methods on the returned array, is that right? And is it a concern? (tried to think of a way to prevent that but nothing's coming to mind) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are, which I have addressed in a future branch. Mostly |
||
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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably not a use case for it, but could be cool to support push and unshift (and probably not splice? or just push out both sides with splice?) and then you remove the surplus from the opposite side so that it's a SlideArray. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yeah, that could be cool. I want to keep this as specific as possible though. The next thing I'm likely to address (probably in a different array type) is some sort of rolling aggregation. So there is still fine-grained second-by-second resolution for recent figures, but on the early end of the array, the resolution would be something like one point per 10 seconds, or one point per minute. Still a nebulous brain noodle for now, but I may end up dabbling with it after I finish this first pass at everything. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey, sorry bit late here, if you are avoiding closures, would There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normally I hesitate to do that because there is no guarantee that Good call, I'll make this change in my current branch 👍 |
||
throw new Error('Cannot unshift onto a RollingArray'); | ||
}; | ||
|
||
return array; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏