tzientist
Version:
Scientist-like library for Node.js in TypeScript
154 lines (153 loc) • 5.88 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.experiment = experiment;
exports.experimentAsync = experimentAsync;
function hrtimeToMs(hrtime) {
const MS_PER_SEC = 1000;
const NS_PER_MS = 1e6;
const [seconds, nanoseconds] = hrtime;
return seconds * MS_PER_SEC + nanoseconds / NS_PER_MS;
}
function defaultPublish(results) {
if (results.candidateResult !== results.controlResult ||
(results.candidateError && !results.controlError) ||
(!results.candidateError && results.controlError)) {
console.warn(`Experiment ${results.experimentName}: difference found`);
}
}
const defaultOptionsSync = {
publish: defaultPublish
};
/**
* A factory that creates an experiment function.
*
* @param name - The name of the experiment, typically for use in publish.
* @param control - The legacy function you are trying to replace.
* @param candidate - The new function intended to replace the control.
* @param [options] - Options for the experiment. You will usually want to specify a publish function.
* @returns A function that acts like the control while also running the candidate and publishing results.
*/
function experiment({ name, control, candidate, options = defaultOptionsSync }) {
const publish = options.publish || defaultPublish;
return (...args) => {
let controlResult;
let candidateResult;
let controlError;
let candidateError;
let controlTimeMs;
let candidateTimeMs;
const isEnabled = !options.enabled || options.enabled(...args);
function publishResults() {
if (isEnabled) {
publish({
experimentName: name,
experimentArguments: args,
controlResult,
candidateResult,
controlError,
candidateError,
controlTimeMs,
candidateTimeMs
});
}
}
if (isEnabled) {
try {
// Not using bigint version of hrtime for Node 8 compatibility
const candidateStartTime = process.hrtime();
candidateResult = candidate(...args);
candidateTimeMs = hrtimeToMs(process.hrtime(candidateStartTime));
}
catch (e) {
candidateError = e;
}
}
try {
const controlStartTime = process.hrtime();
controlResult = control(...args);
controlTimeMs = hrtimeToMs(process.hrtime(controlStartTime));
}
catch (e) {
controlError = e;
publishResults();
throw e;
}
publishResults();
return controlResult;
};
}
async function executeAndTime(controlOrCandidate, args) {
// Not using bigint version of hrtime for Node 8 compatibility
const startTime = process.hrtime();
const result = await controlOrCandidate(...args);
const timeMs = hrtimeToMs(process.hrtime(startTime));
return [result, timeMs];
}
const defaultOptionsAsync = Object.assign(Object.assign({}, defaultOptionsSync), { inParallel: true });
/**
* A factory that creates an asynchronous experiment function.
*
* @param name - The name of the experiment, typically for use in publish.
* @param control - The legacy async function you are trying to replace.
* @param candidate - The new async function intended to replace the control.
* @param [options] - Options for the experiment. You will usually want to specify a publish function.
* @returns An async function that acts like the control while also running the candidate and publishing results.
*/
function experimentAsync({ name, control, candidate, options = defaultOptionsAsync }) {
var _a, _b;
const publish = (_a = options.publish) !== null && _a !== void 0 ? _a : defaultOptionsAsync.publish;
const inParallel = (_b = options.inParallel) !== null && _b !== void 0 ? _b : defaultOptionsAsync.inParallel;
return async (...args) => {
let controlResult;
let candidateResult;
let controlError;
let candidateError;
let controlTimeMs;
let candidateTimeMs;
const isEnabled = !options.enabled || options.enabled(...args);
function publishResults() {
if (isEnabled) {
publish({
experimentName: name,
experimentArguments: args,
controlResult,
candidateResult,
controlError,
candidateError,
controlTimeMs,
candidateTimeMs
});
}
}
if (isEnabled) {
const runFunctions = getRunFunctions(inParallel);
[[candidateResult, candidateTimeMs], [controlResult, controlTimeMs]] =
await runFunctions(() => executeAndTime(candidate, args).catch((e) => {
candidateError = e;
return [undefined, undefined];
}), () => executeAndTime(control, args).catch((e) => {
controlError = e;
return [undefined, undefined];
}));
}
else {
controlResult = await control(...args).catch((e) => {
controlError = e;
return undefined;
});
}
publishResults();
if (controlError) {
throw controlError;
}
return controlResult;
};
}
function getRunFunctions(inParallel) {
return async (function1, function2) => {
if (inParallel) {
return Promise.all([function1(), function2()]);
}
return [await function1(), await function2()];
};
}