UNPKG

browsertime

Version:

Get performance metrics from your web page using Browsertime.

220 lines (195 loc) 6.02 kB
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; } }