Skip to content

Instantly share code, notes, and snippets.

@na--

na--/summary.js Secret

Created January 15, 2021 07:45
Show Gist options
  • Save na--/eaf6540fae765499cbfddce1a052583f to your computer and use it in GitHub Desktop.
Save na--/eaf6540fae765499cbfddce1a052583f to your computer and use it in GitHub Desktop.
k6 text summary
var forEach = function (obj, callback) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (callback(key, obj[key])) {
break;
}
}
}
}
function noop(text) {
return text;
}
function green(text) {
return "\x1b[32m" + text + "\x1b[0m";
}
function red(text) {
return "\x1b[31m" + text + "\x1b[0m";
}
function cyan(text) {
return "\x1b[36m" + text + "\x1b[0m";
}
function faint(text) {
return "\x1b[2m" + text + "\x1b[0m";
}
function faintCyan(text) {
return "\x1b[36;2m" + text + "\x1b[0m";
}
var groupPrefix = '█';
var detailsPrefix = '↳';
var succMark = '✓';
var failMark = '✗';
var defaultOptions = {
indent: ' ',
//enableColors: true, // TODO: implement
summaryTimeUnit: undefined,
summaryTrendStats: undefined,
};
// strWidth tries to return the actual width the string will take up on the
// screen, without any terminal formatting, unicode ligatures, etc.
function strWidth(s) {
// TODO: determine if NFC or NFKD are not more appropriate? or just give up? https://hsivonen.fi/string-length/
var data = s.normalize('NFKC'); // This used to be NFKD in Go, but this should be better
var inEscSeq = false;
var inLongEscSeq = false;
var width = 0;
for (var char of data) {
if (char.done) {
break;
}
// Skip over ANSI escape codes.
if (char == '\x1b') {
inEscSeq = true;
continue;
}
if (inEscSeq && char == '[') {
inLongEscSeq = true;
continue;
}
if (inEscSeq && inLongEscSeq && char.charCodeAt(0) >= 0x40 && char.charCodeAt(0) <= 0x7E) {
inEscSeq = false;
inLongEscSeq = false;
continue;
}
if (inEscSeq && !inLongEscSeq && char.charCodeAt(0) >= 0x40 && char.charCodeAt(0) <= 0x5F) {
inEscSeq = false;
continue;
}
if (!inEscSeq && !inLongEscSeq) {
width++;
}
}
return width;
}
function summarizeCheck(indent, check) {
if (check.fails == 0) {
return green(indent + succMark + ' ' + check.name)
}
var succPercent = Math.floor(100 * check.passes / (check.passes + check.fails))
return red(
indent + failMark + ' ' + check.name + '\n' +
indent + ' ' + detailsPrefix + ' ' + succPercent + '% — ' +
succMark + ' ' + check.passes + ' / ' + failMark + ' ' + check.fails
);
}
function summarizeGroup(indent, group) {
var result = [];
if (group.name != '') {
result.push(indent + groupPrefix + ' ' + group.name);
indent = indent + ' ';
}
for (var i = 0; i < group.checks.length; i++) {
result.push(summarizeCheck(indent, group.checks[i]));
}
if (group.checks.length > 0) {
result.push('');
}
for (var i = 0; i < group.groups.length; i++) {
Array.prototype.push.apply(result, summarizeGroup(indent, group.groups[i]));
}
return result;
}
function displayNameForMetric(name) {
var subMetricPos = name.indexOf('{');
if (subMetricPos >= 0) {
return "{ " + name.substring(subMetricPos + 1, name.length - 1) + " }";
}
return name;
}
function indentForMetric(name) {
if (name.indexOf('{') >= 0) {
return ' ';
}
return '';
}
function humanizeBytes(bytes) {
var units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var base = 1000;
if (bytes < 10) {
return bytes + ' B';
}
var e = Math.floor(Math.log(bytes) / Math.log(base));
var suffix = units[e | 0];
var val = Math.floor(bytes / Math.pow(base, e) * 10 + 0.5) / 10
return val.toFixed(val < 10 ? 1 : 0) + ' ' + suffix;
}
var unitMap = {
's': { unit: 's', coef: 0.001 },
'ms': { unit: 'ms', coef: 1 },
'us': { unit: 'µs', coef: 1000 },
}
function humanizeDuration(dur, timeUnit) {
if (timeUnit != '' && unitMap.hasOwnProperty(timeUnit)) {
return (dur * unitMap[timeUnit].coef).toFixed(2) + unitMap[timeUnit].unit;
}
// TODO: properly truncate and format time values like Go!!
return dur.toFixed(2) + 'ms';
/*
case d > time.Minute:
d -= d % (1 * time.Second)
case d > time.Second:
d -= d % (10 * time.Millisecond)
case d > time.Millisecond:
d -= d % (10 * time.Microsecond)
case d > time.Microsecond:
d -= d % (10 * time.Nanosecond)
}
return d.String()
}
*/
}
function humanizeValue(val, metric, timeUnit) {
if (metric.type == 'rate') {
// Truncate instead of round when decreasing precision to 2 decimal places
return (Math.trunc(val * 100 * 100) / 100).toFixed(2) + '%';
}
switch (metric.contains) {
case 'data':
return humanizeBytes(val);
case 'time':
return humanizeDuration(val, timeUnit);
default:
return parseFloat(val.toFixed(6)).toString();
}
}
function nonTrendMetricValueForSum(metric, timeUnit) {
switch (metric.type) {
case 'counter':
return [
humanizeValue(metric.values.count, metric, timeUnit),
humanizeValue(metric.values.rate, metric, timeUnit) + "/s",
];
case 'gauge':
return [
humanizeValue(metric.values.value, metric, timeUnit),
"min=" + humanizeValue(metric.values.min, metric, timeUnit),
"max=" + humanizeValue(metric.values.max, metric, timeUnit),
];
case 'rate':
return [
humanizeValue(metric.values.rate, metric, timeUnit),
"✓ " + metric.values.passes,
"✗ " + metric.values.fails,
];
default:
return ['[no data]'];
}
}
function summarizeMetrics(options, data) {
var indent = options.indent + ' ';
var result = [];
var names = [];
var nameLenMax = 0;
var nonTrendValues = {};
var nonTrendValueMaxLen = 0;
var nonTrendExtras = {};
var nonTrendExtraMaxLens = [0, 0];
var trendCols = {};
var numTrendColumns = options.summaryTrendStats.length;
var trendColMaxLens = new Array(numTrendColumns).fill(0);
forEach(data.metrics, function (name, metric) {
names.push(name);
// When calculating widths for metrics, account for the indentation on submetrics.
var displayName = indentForMetric(name) + displayNameForMetric(name);
var displayNameWidth = strWidth(displayName);
if (displayNameWidth > nameLenMax) {
nameLenMax = displayNameWidth;
}
if (metric.type == 'trend') {
var cols = [];
for (var i = 0; i < numTrendColumns; i++) {
var tc = options.summaryTrendStats[i];
var value = metric.values[tc];
if (tc === "count") {
value = value.toString();
} else {
value = humanizeValue(value, metric, options.summaryTimeUnit);
}
var valLen = strWidth(value);
if (valLen > trendColMaxLens[i]) {
trendColMaxLens[i] = valLen;
}
cols[i] = value;
}
trendCols[name] = cols;
return;
}
var values = nonTrendMetricValueForSum(metric, options.summaryTimeUnit);
nonTrendValues[name] = values[0];
var valueLen = strWidth(values[0]);
if (valueLen > nonTrendValueMaxLen) {
nonTrendValueMaxLen = valueLen;
}
nonTrendExtras[name] = values.slice(1);
for (var i = 1; i < values.length; i++) {
var extraLen = strWidth(values[i]);
if (extraLen > nonTrendExtraMaxLens[i - 1]) {
nonTrendExtraMaxLens[i - 1] = extraLen;
}
}
});
names.sort();
var getData = function (name) {
if (trendCols.hasOwnProperty(name)) {
var cols = trendCols[name];
var tmpCols = new Array(numTrendColumns);
for (var i = 0; i < cols.length; i++) {
tmpCols[i] = options.summaryTrendStats[i] + '=' + cyan(cols[i]) +
' '.repeat(trendColMaxLens[i] - strWidth(cols[i]));
}
return tmpCols.join(' ');
}
var value = nonTrendValues[name];
var fmtData = cyan(value) + ' '.repeat(nonTrendValueMaxLen - strWidth(value));
var extras = nonTrendExtras[name];
if (extras.length == 1) {
fmtData = fmtData + ' ' + faintCyan(extras[0]);
} else if (extras.length > 1) {
var parts = new Array(extras.length);
for (var i = 0; i < extras.length; i++) {
parts[i] = faintCyan(extras[i]) + ' '.repeat(nonTrendExtraMaxLens[i] - strWidth(extras[i]));
}
fmtData = fmtData + ' ' + parts.join(' ');
}
return fmtData;
}
for (var name of names) {
var metric = data.metrics[name];
var mark = ' ';
var markColor = noop;
if (metric.thresholds) {
mark = succMark;
markColor = green;
forEach(metric.thresholds, function (name, threshold) {
if (!threshold.ok) {
mark = failMark;
markColor = red;
return true; // break
}
});
}
var fmtIndent = indentForMetric(name);
var fmtName = displayNameForMetric(name);
fmtName = fmtName + faint(
'.'.repeat(nameLenMax - strWidth(fmtName) - strWidth(fmtIndent) + 3) + ':'
);
result.push(indent + fmtIndent + markColor(mark) + ' ' + fmtName + ' ' + getData(name));
}
return result;
}
function textSummary(data, options) {
var mergedOpts = Object.assign({}, defaultOptions, data.options, options);
var lines = [];
Array.prototype.push.apply(lines, summarizeGroup(mergedOpts.indent + ' ', data.root_group));
Array.prototype.push.apply(lines, summarizeMetrics(mergedOpts, data));
return lines.join('\n');
}
exports.textSummary = textSummary;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment