browsertime
Version:
Get performance metrics from your web page using Browsertime.
220 lines (195 loc) • 6.02 kB
JavaScript
import get from 'lodash.get';
import set from 'lodash.set';
import { Stats } from 'fast-stats';
import intel from 'intel';
const log = intel.getLogger('browsertime');
function validateType(value, type, message) {
if (typeof value !== type) {
throw new TypeError(message || 'expected variable to be of type ' + type);
}
}
function hasDecimals(num) {
return num !== Math.floor(num);
}
function percentileName(percentile) {
if (percentile === 0) {
return 'min';
} else if (percentile === 100) {
return 'max';
} else {
return 'p' + String(percentile).replace('.', '_');
}
}
function isNumeric(n) {
return !Number.isNaN(Number.parseFloat(n)) && Number.isFinite(n);
}
function addNumber(target, key, value) {
let stats = get(target, key, new Stats());
stats.push(value);
set(target, key, stats);
}
export class Statistics {
constructor() {
this.data = {};
}
add(key, value) {
validateType(key, 'string', 'key (' + key + ') must be a string');
validateType(value, 'number', 'value (' + value + ') must be a number');
if (!Number.isFinite(value)) {
throw new RangeError("value can't be infinite");
}
let stats = this.data[key] || new Stats();
this.data[key] = stats.push(value);
}
addAll(data) {
let samples = Array.isArray(data) ? data : [data];
for (const sample of samples) {
for (const key of Object.keys(sample)) {
let value = sample[key];
// TODO check type of value, to see if it should be included. Possibly via optional transform function
this.add(key, value);
}
}
return this;
}
addDeep(data, tranformFunction) {
function addRecursive(target, keyPrefix, value) {
if (tranformFunction && keyPrefix.length > 0) {
value = tranformFunction(keyPrefix, value);
}
const valueType = typeof value;
switch (valueType) {
case 'number': {
{
addNumber(target, keyPrefix, value);
}
break;
}
case 'object': {
{
if (value === null) {
break;
}
for (const key of Object.keys(value)) {
const path = [...keyPrefix, key];
addRecursive(target, path, value[key]);
}
}
break;
}
case 'string': {
{
if (isNumeric(value)) {
addNumber(target, keyPrefix, Number.parseFloat(value));
}
}
break;
}
case 'boolean': {
{
// skip
}
break;
}
default: {
log.error(
'Unhandled value type %s found when adding data for %s ',
valueType,
keyPrefix
);
}
}
}
addRecursive(this.data, [], data);
}
// TODO this isn't used so we should just remove it
summarize(options) {
options = options || {};
let percentiles = options.percentiles || [0, 10, 90, 99, 100];
// eslint-disable-next-line unicorn/no-array-reduce
return Object.keys(this.data).reduce((results, key) => {
let decimals = options.decimals || 0;
let stats = get(this.data, [key]);
if (options.iqr && stats.median() !== 0) {
// https://en.wikipedia.org/wiki/Interquartile_range
stats = stats.iqr();
}
if (stats.median() < 10 && stats.median() > 0) {
decimals = 4;
}
const node = {
median: Number.parseFloat(stats.median().toFixed(decimals)),
mean: Number.parseFloat(stats.amean().toFixed(decimals)),
mdev: Number.parseFloat(
stats.stddev() / Math.sqrt(stats.length).toFixed(decimals)
) // "standard deviation of the mean"
};
for (const p of percentiles) {
let name = percentileName(p);
node[name] = Number.parseFloat(stats.percentile(p).toFixed(decimals));
}
set(results, [key], node);
return results;
}, {});
}
summarizeDeep(options) {
options = options || {};
let percentiles = options.percentiles || [0, 10, 90, 99, 100];
function summarize(stats) {
let decimals = options.decimals || 0;
if (
options.iqr &&
stats.median() !== 0 &&
stats.range()[0] !== stats.range()[1]
) {
// https://en.wikipedia.org/wiki/Interquartile_range
stats = stats.iqr();
}
if (stats.median() < 1 && stats.median() > 0) {
decimals = 4;
} else if (
hasDecimals(stats.median()) &&
stats.median() < 100 &&
stats.median() > 1
) {
decimals = 2;
}
const results = {
median: Number.parseFloat(stats.median().toFixed(decimals)),
mean: Number.parseFloat(stats.amean().toFixed(decimals)),
mdev: Number.parseFloat(
(stats.stddev() / Math.sqrt(stats.length)).toFixed(4)
), // "standard deviation of the mean"
rsd:
stats.stddev() > 0
? Number.parseFloat((100 * stats.stddev()) / stats.amean())
: 0, // Relative standard deviation
stddev: Number.parseFloat(stats.stddev().toFixed(decimals))
};
for (const p of percentiles) {
let name = percentileName(p);
results[name] = Number.parseFloat(
stats.percentile(p).toFixed(decimals)
);
}
return results;
}
function summarizeRecursive(target, keyPrefix, data) {
if (data instanceof Stats) {
set(target, keyPrefix, summarize(data));
} else if (typeof data === 'object') {
for (const key of Object.keys(data)) {
const path = [...keyPrefix, key];
summarizeRecursive(target, path, data[key]);
}
} else {
throw new TypeError(
'Unhandled data type ' + typeof data + ' found when summarizing data'
);
}
}
const result = {};
summarizeRecursive(result, [], this.data);
return result;
}
}