catdad/grandma

View on GitHub
lib/report-diff.js

Summary

Maintainability
B
6 hrs
Test Coverage
A
97%
var util = require('util');

var _ = require('lodash');
var async = require('async');
var chalk = require('chalk');
var Duration = require('duration-js');
var table = require('fancy-text-table');

var isStream = require('./is-stream.js');
var report = require('./report.js');

function assertStreams(streams) {
    if (!_.every(streams, isStream.readable)) {
        throw new TypeError('streams is not an array or has object of readable streams');
    }

    if (_.keys(streams).length < 2) {
        throw new Error('at least two streams are required for a diff');
    }
}

function assertEnum(enums, value, name) {
    if (!_.includes(enums, value)) {
        throw new Error(name + ' must be one of: ' + enums.join(', '));
    }
}

function assertOptions(options) {
    if (!isStream.writable(options.output)) {
        throw new TypeError('options.output is not a writable stream');
    }

    assertEnum(['fastest'], options.mode, 'options.mode');
    assertEnum(['text'], options.type, 'options.type');
}

function duration(val) {
    return (new Duration(val)).toString();
}

var stringifier = function(opts) {
    function appendTimeUnit(val) {
        return (+val).toFixed(3) + 'ms';
    }

    function statsArr(name, latencies, color) {
        return [name].concat([
            latencies.mean,
            latencies['50'],
            latencies['95'],
            latencies['99'],
            latencies.max
        ].map(appendTimeUnit)).map(function(s) {
            if (opts.color) {
                return color(s);
            }

            return s;
        });
    }

    function percentString(from, to) {
        var diff = to - from;
        var template = '(%s\%)'; // eslint-disable-line no-useless-escape

        var str;
        var color;

        if (diff < 0) {
            str = util.format(template, Math.round(Math.abs(diff) / from * 100, 10));
            color = chalk.green;
        } else {
            str = util.format(template, '-' + Math.round(diff / from * 100));
            color = chalk.red;
        }

        if (opts.color) {
            str = color(str);
        }

        return str;
    }

    function diffValue(from, to) {
        var str = '';
        str += appendTimeUnit(to);
        str += ' ' + percentString(from, to);

        return str;
    }

    function statsDiffArr(name, from, to) {
        return [name].concat([
            diffValue(from.mean, to.mean),
            diffValue(from['50'], to['50']),
            diffValue(from['95'], to['95']),
            diffValue(from['99'], to['99']),
            diffValue(from.max, to.max)
        ]);
    }

    function reportName(name, stats) {
        var str = util.format(
            '%s: %s, %s, %s, %s total',
            name,
            stats.info.name,
            duration(stats.info.duration),
            stats.info.rate ?
                stats.info.rate + ' per second' :
                stats.info.concurrent + ' concurrent',
            stats.info.count
        );

        if (opts.color) {
            str = chalk.magenta(str);
        }

        return str;
    }

    function statsColor(colorName) {
        return function() {
            return statsArr.apply(null, [].slice.call(arguments).concat(colorName));
        };
    }

    return {
        statsMainArr: statsColor(chalk.cyan),
        statsFadedArr: statsColor(chalk.gray),
        statsDiffArr: statsDiffArr,
        reportName: reportName
    };
};

module.exports = function(streams, options, callback) {
    try {
        assertStreams(streams);
        assertOptions(options);
    } catch(e) {
        setImmediate(callback, e);

        return;
    }

    var output = options.output;
    var stringify = stringifier(options);

    // async.mapValues does not preserve order. So we will
    // convert both arrays and objects to arrays, so that
    // we can use async.map, which does preserve order.
    async.map(_.map(streams, function(stream, name) {
        return {
            stream: stream,
            name: name
        };
    }), function(val, next) {
        report({
            input: val.stream,
            type: 'json'
        }, function(err, stat) {
            if (err) {
                return next(err);
            }

            stat._statTitle = val.name;

            next(null, stat);
        });
    }, function(err, stats) {
        if (err) {
            return callback(err);
        }

        var fastest = _.reduce(stats, function(a, b) {
            if (a.latencies.fullTest.mean < b.latencies.fullTest.mean) {
                return a;
            }

            return b;
        });

        var tableObj = table();

        // add latencies header
        tableObj.row([
            'Latencies:', 'mean', '50', '95', '99', 'max'
        ]);

        _.forEach(stats, function(stat) {
            // push an empty line before each summary
            tableObj.line();

            tableObj.title(stringify.reportName(stat._statTitle, stat));

            _.forEach(stat.latencies, function(val, name) {
                if (stat === fastest) {
                    tableObj.row(stringify.statsMainArr(name, val));
                } else if (fastest.latencies[name]) {
                    tableObj.row(stringify.statsDiffArr(name, fastest.latencies[name], val));
                } else {
                    tableObj.row(stringify.statsFadedArr(name, val));
                }
            });
        });

        var tableStr = tableObj.render();

        output.write(tableStr + '\n');

        if (!isStream.stdio(output)) {
            output.end();
        }

        callback();
    });
};