Skip to content

Commit

Permalink
Add alpine test
Browse files Browse the repository at this point in the history
  • Loading branch information
abluchet committed Mar 21, 2018
1 parent bbe4d6d commit 32fd43d
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 9 deletions.
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ cache:
- node_modules
after_success:
- npm run coverage
script:
- npm test
matrix:
include:
- node: '9'
env: ALPINE=1
services:
- docker
script:
- docker build . -t pidusage && npm run alpine
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM node:alpine

RUN mkdir -p /var/pidusage

WORKDIR /var/pidusage
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,14 @@ console.log(stats)

## Compatibility

| Property | Linux | FreeBSD | NetBSD | SunOS | macOS | Win | AIX |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `cpu` |||||| ℹ️ ||
| `memory` ||||||||
| `pid` ||||||||
| `ctime` ||||||||
| `elapsed` ||||||||
| `timestamp` ||||||||
| Property | Linux | FreeBSD | NetBSD | SunOS | macOS | Win | AIX | Alpine
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `cpu` |||||| ℹ️ |||
| `memory` |||||||||
| `pid` |||||||||
| `ctime` |||||||||
| `elapsed` |||||||||
| `timestamp` |||||||||

✅ = Working
ℹ️ = Not Accurate
Expand Down
87 changes: 87 additions & 0 deletions lib/helpers/cpu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
var os = require('os')
var fs = require('fs')
var exec = require('child_process').exec
var parallel = require('./parallel')

/**
* Gathers Clock, PageSize and system uptime through /proc/uptime
* This method is mocked in procfile tests
*/
function updateCpu (cpu, next) {
if (cpu !== null) {
getRealUptime(function (err, uptime) {
if (err) return next(err)
cpu.uptime = uptime
next(null, cpu)
})
return
}

parallel([
getClockAndPageSize,
getRealUptime
], function (err, data) {
if (err) return next(err)

cpu = {
clockTick: data[0].clockTick,
pageSize: data[0].pageSize,
uptime: data[1]
}

next(null, cpu)
})
}

module.exports = updateCpu

/**
* Fallback on os.uptime(), though /proc/uptime is more precise
*/
function getRealUptime (next) {
fs.readFile('/proc/uptime', 'utf8', function (err, uptime) {
if (err || uptime === undefined) {
console.warn("[pidusage] We couldn't find uptime from /proc/uptime, using os.uptime() value")
return next(null, os.uptime())
}

return next(null, parseFloat(uptime.split(' ')[0]))
})
}

function getClockAndPageSize (next) {
parallel([
function getClockTick (cb) {
getconf('CLK_TCK', {default: 100}, cb)
},
function getPageSize (cb) {
getconf('PAGESIZE', {default: 4096}, cb)
}
], function (err, data) {
if (err) return next(err)

next(null, {clockTick: data[0], pageSize: data[1]})
})
}

function getconf (keyword, options, next) {
if (typeof options === 'function') {
next = options
options = { default: '' }
}

exec('getconf ' + keyword, function (error, stdout, stderr) {
if (error !== null) {
console.error('Error while getting ' + keyword, error)
return next(null, options.default)
}

stdout = parseInt(stdout)

if (!isNaN(stdout)) {
return next(null, stdout)
}

return next(null, options.default)
})
}
39 changes: 39 additions & 0 deletions lib/helpers/parallel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// execute an array of asynchronous functions in parallel
// @param {Array} fns - an array of functions
// @param {Function} done - callback(err, results)
function parallel (fns, options, done) {
if (typeof options === 'function') {
done = options
options = {}
}

var keys
if (!Array.isArray(fns)) { keys = Object.keys(fns) }
var pending = keys ? keys.length : fns.length
var results = keys ? {} : []

function each (i, err, result) {
results[i] = result

if (--pending === 0 || (err && !options.graceful)) {
done && done(err, results)
done = null
}
}

if (keys) {
keys.forEach(function (key) {
fns[key](function (err, res) {
each(key, err, res)
})
})
} else {
fns.forEach(function (fn, i) {
fn(function (err, res) {
each(i, err, res)
})
})
}
}

module.exports = parallel
82 changes: 82 additions & 0 deletions lib/procfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
var fs = require('fs')
var path = require('path')
var updateCpu = require('./helpers/cpu')
var parallel = require('./helpers/parallel')
var history = require('./history')
var cpu = null

function readProcFile (pid, options, done) {
var hst = history.get(pid, options.maxage)
if (hst === undefined) hst = {}

// Arguments to path.join must be strings
fs.readFile(path.join('/proc', '' + pid, 'stat'), 'utf8', function (err, infos) {
if (err) {
if (err.code === 'ENOENT') {
err.message = 'No maching pid found'
}

return done(err, null)
}

var date = Date.now()
// https://github.com/arunoda/node-usage/commit/a6ca74ecb8dd452c3c00ed2bde93294d7bb75aa8
// preventing process space in name by removing values before last ) (pid (name) ...)
var index = infos.lastIndexOf(')')
infos = infos.substr(index + 2).split(' ')

// according to http://man7.org/linux/man-pages/man5/proc.5.html (index 0 based - 2)
// In kernels before Linux 2.6, start was expressed in jiffies. Since Linux 2.6, the value is expressed in clock ticks
var stat = {
ppid: parseInt(infos[1]),
utime: parseFloat(infos[11]),
stime: parseFloat(infos[12]),
cutime: parseFloat(infos[13]),
cstime: parseFloat(infos[14]),
start: parseFloat(infos[19]) / cpu.clockTick,
rss: parseFloat(infos[21]),
uptime: cpu.uptime
}

// http://stackoverflow.com/questions/16726779/total-cpu-usage-of-an-application-from-proc-pid-stat/16736599#16736599
var childrens = options.childrens ? stat.cutime + stat.cstime : 0
var total = (stat.stime - (hst.stime || 0) + stat.utime - (hst.utime || 0) + childrens) / cpu.clockTick
// time elapsed between calls in seconds
var seconds = Math.abs(hst.uptime !== undefined ? stat.uptime - hst.uptime : stat.start - stat.uptime)
if (seconds === 0) seconds = 1 // we sure can't divide through 0

history.set(stat, options.maxage)

var cpuPercent = Math.min(Math.round((total / seconds) * 100000) / 1000, 100.0)
var memory = stat.rss * cpu.pageSize

return done(null, {
cpu: cpuPercent,
memory: memory,
ctime: (stat.utime + stat.stime) / cpu.clockTick,
elapsed: date - (stat.start * 1000),
timestamp: stat.start * 1000, // start date
pid: pid,
ppid: stat.ppid
})
})
}

function procfile (pid, options, done) {
updateCpu(cpu, function (err, result) {
if (err) return done(err)

cpu = result
var fns = {}

pid.forEach(function (id, i) {
fns[id] = function (cb) {
readProcFile(id, options, cb)
}
})

parallel(fns, {graceful: true}, done)
})
}

module.exports = procfile
4 changes: 3 additions & 1 deletion lib/stats.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

var fs = require('fs')
var os = require('os')

var platformToMethod = {
Expand All @@ -12,14 +13,15 @@ var platformToMethod = {
aix: 'ps'
}

var useProcfile = fs.existsSync('/etc/alpine-release')
var platform = os.platform()
if (platform.match(/^win/)) {
platform = 'win'
}

var stat
try {
stat = require('./' + platformToMethod[platform])
stat = useProcfile ? require('./procfile') : require('./' + platformToMethod[platform])
} catch (err) {}

/**
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"scripts": {
"lint": "standard index.js lib/**/*.js test/**/*.js",
"test": "npm run lint && nyc ava -m \"!*benchmark*\"",
"alpine": "docker run -v $(pwd):/var/pidusage pidusage:latest npm test",
"coverage": "codecov",
"bench": "ava -m \"*benchmark*\""
},
Expand Down
68 changes: 68 additions & 0 deletions test/procfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import mockery from 'mockery'
import test from 'ava'

test.before(() => {
mockery.enable({
warnOnReplace: false,
warnOnUnregistered: false,
useCleanCache: true
})
})

test.beforeEach(() => {
mockery.resetCache()
})

test.after(() => {
mockery.disable()
})

test('procfile stat', async t => {
var fs = require('fs')
fs.readFile = function (path, encoding, callback) {
if (path === '/proc/uptime') {
callback(null, '100 0')
return
}

// proc/<pid>/stat
var infos = '0 (test)'
for (var i = 0; i < 22; i++) {
if (i === 12) {
infos += ' ' + 10000 // currentStime 10000 * clockTick
} else {
infos += ' 0'
}
}
callback(null, infos)
}

fs.existsSync = function (path) {
if (path === '/etc/alpine-release') { return true }
return false
}

var os = require('os')
os.platform = function () { return 'linux' }

mockery.registerMock('os', os)
mockery.registerMock('fs', fs)
mockery.registerMock('./cpu.js', function (cpu, next) {
next({
clockTick: 100,
uptime: 100,
pagesize: 4096
})
})

var m = require('..')
var stat = await m(10)
t.is(stat.cpu, 100)
t.is(stat.memory, 0)
t.is(stat.ppid, 0)
t.is(stat.pid, 10)
t.is(typeof stat.elapsed, 'number', 'elapsed')
t.false(isNaN(stat.elapsed), 'elapsed')
t.is(typeof stat.timestamp, 'number', 'timestamp')
t.false(isNaN(stat.timestamp), 'timestamp')
})

0 comments on commit 32fd43d

Please sign in to comment.