-
-
Save na--/eaf6540fae765499cbfddce1a052583f to your computer and use it in GitHub Desktop.
k6 text summary
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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