Skip to content

Instantly share code, notes, and snippets.

@martinheidegger
Last active December 22, 2017 06:08
Show Gist options
  • Save martinheidegger/8180022bd69a254f273bab76856dbb33 to your computer and use it in GitHub Desktop.
Save martinheidegger/8180022bd69a254f273bab76856dbb33 to your computer and use it in GitHub Desktop.
Stat module for Node.js to collect averages of Memory & CPU consumption over a given period of time.
time cpu[user]#avg cpu[user]#min cpu[user]#max cpu[system]#avg cpu[system]#min cpu[system]#max mem[heapUsed]#avg mem[heapUsed]#min mem[heapUsed]#max mem[heapTotal]#avg mem[heapTotal]#min mem[heapTotal]#max mem[external]#avg mem[external]#min mem[external]#max
1513914518233 78273 78273 78273 23573 23573 23573 5583596 5329040 5838152 10854400 10854400 10854400 558976 439592 678360
1513914520381.644 61037.47537077804 61037.47537077804 61037.47537077804 23573 23573 23573 7570791.859881992 7168906.021724596 7898616.114693746 10854400 10854400 10854400 1522872.917892116 1416024.60562031 1889588.4111456308
1513914522528.9185 48683.45425623419 48683.45425623419 48683.45425623419 23573 23573 23573 8585283.686068054 8419800.250697419 8931602.671535378 10854400 10854400 10854400 2458086.628234829 2394709.239230188 2883538.4351940444
1513914524676.8794 39999.763262061286 39999.763262061286 39999.763262061286 23573 23573 23573 9220846.7495376 9108108.058582326 9425541.758640196 10854400 10854400 10854400 3383111.3564198054 3331732.556550356 3839578.446441993
1513914526824.868 34028.647499221384 34028.647499221384 34028.647499221384 23573 23573 23573 9627284.211593483 9552646.626042088 9806308.933690075 10854400 10854400 10854400 4218705.417122623 4182956.819984545 4597969.619718203
1513914528972.9727 30023.722645343652 30023.72264534366 30023.72264534366 23573 23573 23573 9966954.673867144 9852138.569128916 10099403.22008614 10854400 10854400 10854400 4899566.710623558 4864151.856259296 5287625.328601734
1513914531121.2515 27412.86172752242 27412.86172752242 27412.86172752242 23572.999999999996 23573 23573 10133607.340935253 10013050.58314792 10286682.583707647 10854400 10854400 10854400 5613047.099974608 5573111.815711519 6028600.462148726
1513914533269.499 25765.708206428717 25765.708206428717 25765.708206428717 23573 23573 23573 10252514.700867983 10145575.517536875 10537604.897572512 10854400 10854400 10854400 6382103.664027426 6333377.674948864 6806861.474351015
1513914535417.4893 24765.50694164748 24765.50694164748 24765.50694164748 23573 23573 23573 10446171.598823579 10352478.31151538 10974227.344121225 10854400 10854400 10854400 7164800.033574963 7087905.812303084 7558676.7109473245
1513914537565.3906 24184.944618152877 24184.944618152877 24184.944618152877 23573 23573 23573 10773089.159788903 10702991.054153701 11654431.890443373 10854400.000000002 10854400 10854400 7782470.854957955 7681977.770495968 8096995.242429445
1513914539713.4238 23865.6912138342 23865.6912138342 23865.6912138342 23573 23573 23573 11241552.032950891 11182951.896169163 12538230.381991934 10854400 10854400 10854400 8077798.388358861 8001095.235766856 8357030.442233878
1513914541861.497 23701.334087984855 23701.334087984855 23701.334087984855 23573 23573 23573 12016819.04669139 11752839.122017872 13517437.165392563 10854399.999999998 10854400 10854400 8161962.454823924 8113302.464464047 8414956.497980166
1513914544009.5027 23623.396270666744 23623.39627066675 23623.39627066675 23573 23573 23573 12970499.62074176 12762030.389974095 13168294.408663604 11022172.160000002 10854400 15048704 8217769.3401122065 8152470.497102897 8374191.086946285
1513914546157.4685 23590.13053286272 23590.13053286272 23590.13053286272 23572.999999999996 23573 23573 12478863.755606279 11835422.360709298 12546892.711966818 15048704.000000004 15048704 15048704 8287266.995135448 8163742.139396884 8343909.085583206
1513914548272.6465 23577.780817329316 23577.780817329316 23577.780817329316 23572.999999999996 23572.999999999996 23572.999999999996 11128006.760949578 10352423.907886812 11677298.04823259 15048704 15048704 15048704 8303520.528996829 8123915.191174209 8368894.011437854
1513914550336.4277 23574.00261006238 23574.002610062384 23574.002610062384 23573 23573 23573 10766055.694924619 9424485.541928979 11037290.065413523 15048704 15048704 15048704 8302377.655632236 8145360.009263082 8347976.569264509
1513914552373.349 23573.13383228799 23573.13383228799 23573.13383228799 23573 23573 23573 9655991.164667934 8429693.043967253 10401925.920558494 15048703.999999998 15048704 15048704 8365272.890501613 8131959.935633271 8409679.957685445
1513914554401.0806 23573.007832891108 23573.007832891108 23573.007832891108 23572.999999999996 23572.999999999996 23572.999999999996 9918719.044708548 7830073.732143917 9998422.833521828 15048704.000000002 15048704 15048704 8492987.427673355 8195691.574807521 8506297.368337886
1513914556454.1975 23573.00006119446 23573.00006119446 23573.00006119446 23572.999999999996 23572.999999999996 23572.999999999996 9263270.784694213 6661059.303575788 9317187.988934096 15048704 15048704 15048704 8447687.33536095 8192903.513249209 8452070.224503778
1513914558474 78273 78273 78273 23573 23573 23573 7122185.090909092 5265976 8280840 15048704.000000006 15048704 15048704 8320424.290909092 8081720 8353216
// (MIT LICENSE)
//
// Copyright 2017 Martin Heidegger
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
/**
* Stat module for Node.js to collect averages of Memory & CPU consumption over
* a given period of time.
*/
module.exports = ({
resolution, // Maximum resolution of frames
interval // Time interval between each recording
} = {
// defaults
resolution: 20,
interval: 500
}) => {
const snapShots = []
const snapStep = 1 / (resolution - 1)
const cpuFrame = process.cpuUsage()
let frame = initFrame()
let finished = false
const nextSnap = (time) => {
if (finished) return
// To reduce the code complexity and have a better first frame
// lest start after the nextSnap
setTimeout(() => {
if (snapShots.length === resolution) {
let ratio = 1 - snapStep
for (let i = 1; i < snapShots.length - 1; i++, ratio -= snapStep) {
let next
if (i === resolution - 1) {
next = frame
} else {
next = snapShots[i + 1]
}
merge(snapShots[i], next, ratio)
}
snapShots[snapShots.length - 1] = frame
// The new resolution (frames / total) is lower: so we need
// to increase the time for each additional frame
nextSnap(time * (resolution + 1) / resolution)
} else {
// Initially the array is empty, lets fill it first
snapShots.push(frame)
nextSnap(time)
}
frame = initFrame()
count = collect(frame, cpuFrame, count)
}, time)
}
let count = 0
const int = setInterval(() => {
count = collect(frame, cpuFrame, count)
}, interval)
// Start with a frame interval that is higher than the
// recording interval in order to get multiple recordings
// in the beginning
nextSnap(interval * 3)
return {
snapShots,
stat: () => frame,
finish: () => {
finished = true
clearInterval(int)
return snapShots
},
toCSV: () => {
return 'time,' + [
'cpu[user]',
'cpu[system]',
'mem[heapUsed]',
'mem[heapTotal]',
'mem[external]'
].map(entry => `${entry}#avg,${entry}#min,${entry}#max`).join(',') + '\n' + snapShots.map(snapShot => {
if (!snapShot) {
return ''
}
return `${snapShot.time}, ` + [
snapShot.cpu.user,
snapShot.cpu.system,
snapShot.mem.heapUsed,
snapShot.mem.heapTotal,
snapShot.mem.external
]
.map(entry => `${entry.avg},${entry.min},${entry.max}`)
.join(',')
}).join('\n')
}
}
}
const initFrame = () => {
return {
time: Date.now(),
cpu: {
user: initMinMax(),
system: initMinMax()
},
mem: {
heapTotal: initMinMax(),
heapUsed: initMinMax(),
external: initMinMax()
}
}
}
const initMinMax = () => {
return {
min: Number.POSITIVE_INFINITY,
max: Number.NEGATIVE_INFINITY
}
}
function applyMinMax (count, nextCount, obj, current) {
if (obj.min > current) {
obj.min = current
}
if (obj.avg === undefined) {
obj.avg = current
} else {
obj.avg = (obj.avg / nextCount * count) + current / nextCount
}
if (obj.max < current) {
obj.max = current
}
return nextCount
}
function collect (stat, cpuFrame, count) {
const cpu = stat.cpu
const mem = stat.mem
const nextCount = count + 1
process.cpuUsage(cpuFrame)
applyMinMax(count, nextCount, cpu.user, cpuFrame.user)
applyMinMax(count, nextCount, cpu.system, cpuFrame.system)
const memShot = process.memoryUsage()
applyMinMax(count, nextCount, mem.heapTotal, memShot.heapTotal)
applyMinMax(count, nextCount, mem.heapUsed, memShot.heapUsed)
applyMinMax(count, nextCount, mem.external, memShot.external)
return nextCount
}
function mergeProperty (objA, objB, ratio, invRatio) {
objA.max = objA.max * ratio + objB.max * invRatio
objA.min = objA.min * ratio + objB.min * invRatio
objA.avg = objA.avg * ratio + objB.avg * invRatio
}
function merge (statA, statB, ratio) {
const invRatio = 1 - ratio
statA.time = statA.time * ratio + statB.time * invRatio
mergeProperty(statA.cpu.user, statB.cpu.system, ratio, invRatio)
mergeProperty(statA.cpu.system, statB.cpu.system, ratio, invRatio)
mergeProperty(statA.mem.heapUsed, statB.mem.heapTotal, ratio, invRatio)
mergeProperty(statA.mem.heapUsed, statB.mem.heapUsed, ratio, invRatio)
mergeProperty(statA.mem.external, statB.mem.external, ratio, invRatio)
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-limit=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Performance visualizer</title>
<style>
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
.area {
fill: lightsteelblue;
}
.axis line, .axis path {
stroke: grey;
}
.line {
fill: none;
stroke-width: 1.5px;
}
.line--cpuuser-min {
stroke: hsl(0, 80%, 80%)
}
.legendQuant--cpuuser g:nth-child(1) rect {
fill: hsl(0, 80%, 80%)
}
.line--cpuuser-avg {
stroke: hsl(0, 80%, 65%)
}
.legendQuant--cpuuser g:nth-child(2) rect {
fill: hsl(0, 80%, 65%)
}
.line--cpuuser-max {
stroke: hsl(0, 80%, 50%)
}
.legendQuant--cpuuser g:nth-child(3) rect {
fill: hsl(0, 80%, 50%)
}
.line--cpusystem-min {
stroke: hsl(72, 80%, 80%)
}
.legendQuant--cpusystem g:nth-child(1) rect {
fill: hsl(72, 80%, 80%)
}
.line--cpusystem-avg {
stroke: hsl(72, 80%, 65%)
}
.legendQuant--cpusystem g:nth-child(2) rect {
fill: hsl(72, 80%, 65%)
}
.line--cpusystem-max {
stroke: hsl(72, 80%, 50%)
}
.legendQuant--cpusystem g:nth-child(3) rect {
fill: hsl(72, 80%, 50%)
}
.line--memheapTotal-min {
stroke: hsl(144, 80%, 80%)
}
.legendQuant--memheapTotal g:nth-child(1) rect {
fill: hsl(144, 80%, 80%)
}
.line--memheapTotal-avg {
stroke: hsl(144, 80%, 65%)
}
.legendQuant--memheapTotal g:nth-child(2) rect {
fill: hsl(144, 80%, 65%)
}
.line--memheapTotal-max {
stroke: hsl(144, 80%, 50%)
}
.legendQuant--memheapTotal g:nth-child(3) rect {
fill: hsl(144, 80%, 50%)
}
.line--memheapUsed-min {
stroke: hsl(216, 80%, 80%)
}
.legendQuant--memheapUsed g:nth-child(1) rect {
fill: hsl(216, 80%, 80%)
}
.line--memheapUsed-avg {
stroke: hsl(216, 80%, 65%)
}
.legendQuant--memheapUsed g:nth-child(2) rect {
fill: hsl(216, 80%, 65%)
}
.line--memheapUsed-max {
stroke: hsl(216, 80%, 50%)
}
.legendQuant--memheapUsed g:nth-child(3) rect {
fill: hsl(216, 80%, 50%)
}
.line--memexternal-min {
stroke: hsl(288, 80%, 80%)
}
.legendQuant--memexternal g:nth-child(1) rect {
fill: hsl(288, 80%, 80%)
}
.line--memexternal-avg {
stroke: hsl(288, 80%, 65%)
}
.legendQuant--memexternal g:nth-child(2) rect {
fill: hsl(288, 80%, 65%)
}
.line--memexternal-max {
stroke: hsl(288, 80%, 50%)
}
.legendQuant--memexternal g:nth-child(3) rect {
fill: hsl(288, 80%, 50%)
}
.dot {
fill: white;
stroke: steelblue;
stroke-width: 1.5px;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.25.3/d3-legend.min.js"></script>
<script type="text/javascript">
// https://stackoverflow.com/questions/46595354/setting-up-d3-axis-with-kib-mib-gib-units-from-values-in-bytes
function fileSize (bytes) {
var thresh = 1024
if (Math.abs(bytes) < thresh) {
return bytes + ' B'
}
var units = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
var u = -1
do {
bytes /= thresh
++u
} while (Math.abs(bytes) >= thresh && u < units.length - 1)
return bytes.toFixed(1) + ' ' + units[u]
}
function cycles (num) {
return `${num | 0} cycles`
}
d3.csv('https://gist.githubusercontent.com/martinheidegger/8180022bd69a254f273bab76856dbb33/raw/eb0d9711a9b45f31e5207cb776f1c99210b7fe2a/example_output.csv', (data) => {
const limits = data.reduce((limits, entry) => {
for (const key in entry) {
let value = Number(entry[key])
entry[key] = value
let limit = limits[key]
if (limit === undefined) {
limit = {min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY}
limits[key] = limit
}
if (limit.min > value) {
limit.min = Math.floor(value)
}
if (limit.max < value) {
limit.max = Math.ceil(value)
}
}
return limits
}, {})
const graphWidth = 400
const graphHeight = 300
const margin = {top: 10, right: 0, bottom: 20, left: 80}
const xDomain = [0, (limits.time.max - limits.time.min) / 1000]
const xRange = [0, graphWidth]
const yRange = [0, graphHeight]
;[
{ prop: 'cpu[user]', format: cycles},
{ prop: 'cpu[system]', format: cycles},
{ prop: 'mem[heapTotal]', format: fileSize},
{ prop: 'mem[heapUsed]', format: fileSize},
{ prop: 'mem[external]', format: fileSize}
].forEach(({prop, format}, i) => {
const category = i + 3
const width = 500
const height = 300
var svg = d3.select('body').append('svg')
.datum(data)
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.attr('title', prop)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
var y = d3.scaleLinear(data.length)
.domain([limits[`${prop}#max`].max, limits[`${prop}#min`].min])
.range(yRange)
var x = d3.scaleLinear()
.domain(xDomain)
.range(xRange)
svg.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0,' + graphHeight + ')')
.call(d3.axisBottom(x).tickFormat(d => `${d}s`))
svg.append('g')
.attr('class', 'axis axis--y')
.call(d3.axisLeft(y).tickFormat(format))
const parts = [
'min',
'avg',
'max'
]
var colorLegend = d3.legendColor()
.labels(parts)
.title(prop)
.cells(parts.length)
.useClass(true);
parts.forEach((part) => {
var line = d3.line()
.defined(function (d) { return data; })
.x(d => x((d.time - limits.time.min) / 1000))
.y(d => {
// console.log(prop, part, d[`${prop}#${part}`], limits[`${prop}#min`].min, limits[`${prop}#max`].max)
return y(Math.round(d[`${prop}#${part}`]))
})
svg.append('path')
.attr('class', `line line--${prop.replace(/[\[\]]/ig, '')}-${part}`)
.attr('d', line)
})
svg.append("g")
.attr("class", `legendQuant legendQuant--${prop.replace(/[\[\]]/ig, '')}`)
.attr("transform", "translate(20,20)")
.call(colorLegend)
})
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment