whats-the-damage
Version:
What's the damage? ...of running that script in real time, CPU time, and memory usage
227 lines (206 loc) • 6.32 kB
JavaScript
// @flow
import path from "path";
import { fork } from "child_process";
import stats from "stats-lite";
import os from "os";
import pidusage from "pidusage";
import type {
DamageOfResponse,
DamagesRows,
Damages,
Damage,
AverageDamage,
AverageDamages,
DamageScripts,
DamageResponse,
MemoryStat,
Snapshots,
Snapshot,
AverageDamageValue,
DamageOptions
} from "./flowtypes";
/*
* HELLO READER OF SOURCE CODE
*
* The two interesting functions in here are the default export
* at the very bottom of this file called DamageOf
*
* And forker which is just below this.
*/
const harnessPath = path.join(__dirname, "./harness.js");
const forker = (script: string | Array<string>, opts: DamageOptions) => {
if (global.gc) {
global.gc(); // attempt to free any memory before forking
}
const args = Array.isArray(script) ? script : [script];
const timeStart = process.hrtime();
const forkee = fork(harnessPath, args, { silent: true });
let closed = false;
return new Promise(
(
resolve: (result: Promise<Damage> | Damage) => void,
reject: (error: Object) => void
) => {
const messages = {};
const snapshots = [];
forkee.on("message", message => {
const keys = Object.keys(message);
keys.forEach(key => {
switch (key) {
case "memoryStart":
case "memoryEnd":
case "cpuUsageEnd":
messages[key] = message[key];
break;
default:
console.log(
"Unknown message from",
script,
`message[${key}]=`,
message[key]
);
}
});
});
const watchPid = () => {
if (closed) return;
pidusage.stat(forkee.pid, (err, stat) => {
const hrtime = process.hrtime(timeStart);
const snapshot = {
...stat,
time: parseFloat(hrtime.join("."))
};
snapshots.push(snapshot);
});
setTimeout(watchPid, opts.snapshotEveryMilliseconds);
};
watchPid();
const messageToString = data =>
data instanceof Buffer ? data.toString("utf8") : data;
forkee.stdout.on("data", data => {
console.log(
"stdout from",
script,
"(ignoring): ",
messageToString(data)
);
});
forkee.stderr.on("data", data => {
console.log("stderr from", script, "(failing)", messageToString(data));
reject({ error: data });
});
forkee.on("close", exitCode => {
const duration = process.hrtime(timeStart);
closed = true;
setTimeout(() => {
resolve({
time: parseFloat(duration.join(".")),
memory: memoryDiff(messages.memoryStart, messages.memoryEnd),
cpu: messages.cpuUsageEnd,
snapshot: {
everyMilliseconds: opts.snapshotEveryMilliseconds,
snapshots
},
exitCode
});
}, opts.snapshotEveryMilliseconds); // wait until any snapshot might finish
});
}
);
};
const memoryDiff = (start: MemoryStat, end: MemoryStat): MemoryStat => {
return Object.assign(
{},
...Object.keys(start).map(key => ({
[key]: end[key] - start[key]
}))
);
};
const runScriptsOnce = async (
scripts: DamageScripts,
opts: DamageOptions
): Damages => {
if (opts.async) {
// asynchronous ... less accurate (more competition for resources) but useful sometimes.
// Maybe useful when it's not a close race between scripts (exclude outliers)
return await Promise.all(scripts.map(script => forker(script, opts)));
} else {
// synchronous ... more accurate (less competition for resources)
const damages = [];
for (let i = 0; i < scripts.length; i++) {
damages.push(await forker(scripts[i], opts));
}
return damages;
}
};
const averageDamagesRows = (
damagesRows: DamagesRows,
options: DamageOptions
): AverageDamages =>
damagesRows[0].map((damageResponse, i) => ({
time: getAverages(damagesRows.map(d => d[i].time)),
cpu: {
user: getAverages(damagesRows.map(d => d[i].cpu.user)),
system: getAverages(damagesRows.map(d => d[i].cpu.system))
},
memory: {
rss: getAverages(damagesRows.map(d => d[i].memory.rss)),
heapTotal: getAverages(damagesRows.map(d => d[i].memory.heapTotal)),
heapUsed: getAverages(damagesRows.map(d => d[i].memory.heapUsed)),
external: getAverages(damagesRows.map(d => d[i].memory.external))
},
snapshot: {
everyMilliseconds: damageResponse.snapshot.everyMilliseconds, // constant, no need to average
snapshots: damageResponse.snapshot.snapshots.map((d, snapshotIndex) => ({
cpu: getAverages(
damagesRows.map(d => d[i].snapshot.snapshots[snapshotIndex].cpu)
),
memory: getAverages(
damagesRows.map(d => d[i].snapshot.snapshots[snapshotIndex].memory)
),
time: getAverages(
damagesRows.map(d => d[i].snapshot.snapshots[snapshotIndex].time)
)
}))
},
exitCode: damageResponse.exitCode,
repeat: options.repeat
}));
const getAverages = (values: Array<number>): AverageDamageValue => ({
mean: stats.mean(values),
median: stats.median(values),
standardDeviation: stats.stdev(values),
max: Math.max(...values),
min: Math.min(...values)
});
const defaultOptions: DamageOptions = {
async: false, // running tests in parallel will cause eratic stats. not recommended.
snapshotEveryMilliseconds: 1000,
repeat: 10,
progress: (...args) => {
console.log(...args);
}
};
export const getEnvironment = () => ({
versions: process.versions,
arch: os.arch(),
cpus: os.cpus(),
totalmem: os.totalmem(),
type: os.type()
});
const DamageOf = async (
scripts: DamageScripts,
options: DamageOptions
): Promise<DamageOfResponse> => {
const opts = {
...defaultOptions,
...options
};
const damagesRows = [];
for (let i = 0; i < opts.repeat; i++) {
damagesRows.push(await runScriptsOnce(scripts, opts));
if (opts.progress) opts.progress("Test loop", i);
}
return averageDamagesRows(damagesRows, opts);
};
export default DamageOf;