UNPKG

browsertime

Version:

Get performance metrics from your web page using Browsertime.

437 lines (391 loc) 11.5 kB
import { getLogger } from '@sitespeed.io/log'; const log = getLogger('browsertime'); export function formatMetric(name, metric, multiple, inMs, extras) { if (metric === undefined) return; function fmt(value) { if (inMs) { return value < 1000 ? value + 'ms' : (value / 1000).toFixed(2) + 's'; } else return value; } let formatted = `${name}: ${fmt(multiple ? metric.median : metric)}`; if (extras) { formatted += ` (σ${fmt(metric.stddev.toFixed(2))} ${ metric.stddev > 0 ? metric.rsd.toFixed(1) : '0' }%)`; } return formatted; } export function logResultLogLine(results) { let index = 0; for (let result of results) { let totalSize = 0; let requests = ''; let firstPaint = '', domContent = '', pageLoad = '', speedIndex = '', perceptualSpeedIndex = '', contentfulSpeedIndex = '', startRender = '', visualComplete85 = '', backEndTime = '', lastVisualChange = '', memory = '', lcp = '', fcp = '', cls = '', tbt = '', cpuBenchmark = '', nRuns = result.browserScripts.length, m = nRuns > 1; if (results.har && results.har.log.pages[index]) { // get the id let pageId = results.har.log.pages[index].id; let entriesByPage = groupBy(results.har.log.entries, 'pageref'); requests = entriesByPage[pageId] ? entriesByPage[pageId].length + ' requests' : ''; if (entriesByPage[pageId]) { for (const request of entriesByPage[pageId]) { // transfer size totalSize += request.response.bodySize; } } totalSize = totalSize > 1024 ? (totalSize / 1024).toFixed(2) + ' kb' : totalSize + ' bytes'; } if (result.statistics.browser && result.statistics.browser.cpuBenchmark) { cpuBenchmark = formatMetric( 'CPUBenchmark', result.statistics.browser.cpuBenchmark, true, true, m ); } if (result.statistics.timings && result.statistics.timings.pageTimings) { let pt = result.statistics.timings.pageTimings, t = result.statistics.timings, vm = result.statistics.visualMetrics, pi = result.statistics.pageinfo, cpu = result.statistics.cpu; firstPaint = formatMetric('firstPaint', t.firstPaint, true, true, m); if (t.largestContentfulPaint) { lcp = formatMetric( 'LCP', t.largestContentfulPaint.renderTime, true, true, m ); } if (pi && pi.cumulativeLayoutShift) { cls = formatMetric('CLS', pi.cumulativeLayoutShift, true, false, m); } if (cpu && cpu.longTasks && cpu.longTasks.totalBlockingTime) { tbt = formatMetric( 'TBT', cpu.longTasks.totalBlockingTime, true, true, m ); } if ( result.statistics.timings.paintTiming && result.statistics.timings.paintTiming['first-contentful-paint'] ) { fcp = formatMetric( 'FCP', result.statistics.timings.paintTiming['first-contentful-paint'], true, true, m ); } domContent = formatMetric( 'DOMContentLoaded', pt.domContentLoadedTime, true, true, m ); speedIndex = formatMetric( 'speedIndex', vm ? vm.SpeedIndex : vm, true, true, m ); perceptualSpeedIndex = formatMetric( 'perceptualSpeedIndex', vm ? vm.PerceptualSpeedIndex : vm, true, true, m ); contentfulSpeedIndex = formatMetric( 'contentfulSpeedIndex', vm ? vm.ContentfulSpeedIndex : vm, true, true, m ); startRender = formatMetric( 'firstVisualChange', vm ? vm.FirstVisualChange : vm, true, true, m ); lastVisualChange = formatMetric( 'lastVisualChange', vm ? vm.LastVisualChange : vm, true, true, m ); visualComplete85 = formatMetric( 'visualComplete85', vm ? vm.VisualComplete85 : vm, true, true, m ); pageLoad = formatMetric('Load', pt.pageLoadTime, true, true, m); backEndTime = formatMetric('TTFB', pt.backEndTime, true, true, m); } if (result.statistics.memory) { let mem = result.statistics.memory; for (let value in mem) { mem[value] = Math.round(mem[value] / 1024 / 1024); } memory = formatMetric('Memory', mem, true, false, m); memory = memory + 'mb'; } let lines = [ `${requests}`, `${totalSize > 0 ? totalSize : ''}`, backEndTime, firstPaint, startRender, fcp, domContent, lcp, cls, tbt, cpuBenchmark, pageLoad, memory, speedIndex, perceptualSpeedIndex, contentfulSpeedIndex, visualComplete85, lastVisualChange ], note = m ? ` (${nRuns} runs)` : ''; lines = lines.filter(Boolean).join(', '); log.info(`${result.info.url} ${lines}${note}`); index++; } } export function toArray(arrayLike) { if (arrayLike === undefined || arrayLike === null) { return []; } if (Array.isArray(arrayLike)) { return arrayLike; } return [arrayLike]; } export function jsonifyVisualProgress(visualProgress) { // Original data looks like // "0=0, 1500=81, 1516=81, 1533=84, 1550=84, 1566=84, 1600=95, 1683=95, 1833=100" if (typeof visualProgress === 'string') { const visualProgressArray = []; for (const value of visualProgress.split(', ')) { const [timestamp, percent] = value.split('='); visualProgressArray.push({ timestamp: Number.parseInt(timestamp, 10), percent: Number.parseInt(percent, 10) }); } return visualProgressArray; } return visualProgress; } export function jsonifyKeyColorFrames(keyColorFrames) { // Original data looks like // "FrameName1=[0-133 255-300], FrameName2=[133-255] FrameName3=[]" if (typeof keyColorFrames === 'string') { const keyColorFramesObject = {}; for (const keyColorPair of keyColorFrames.split(', ')) { const [name, values] = keyColorPair.split('='); keyColorFramesObject[name] = []; const rangePairs = values.replace('[', '').replace(']', ''); if (rangePairs) { for (const rangePair of rangePairs.split(' ')) { const [start, end] = rangePair.split('-'); keyColorFramesObject[name].push({ startTimestamp: Number.parseInt(start, 10), endTimestamp: Number.parseInt(end, 10) }); } } } return keyColorFramesObject; } return keyColorFrames; } export function adjustVisualProgressTimestamps( visualProgress, profilerStartTime, recordingStartTime ) { // calculate offset between unix timestamps which represents the profiler start // time and the time of the first frame after the orange frame after recording start for (const value of visualProgress) { value.timestamp += recordingStartTime - profilerStartTime; } return visualProgress; } export function localTime() { const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); const offset = -date.getTimezoneOffset(); const sign = offset >= 0 ? '+' : '-'; const offsetHours = String(Math.floor(Math.abs(offset) / 60)).padStart( 2, '0' ); const offsetMinutes = String(Math.abs(offset) % 60).padStart(2, '0'); return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${sign}${offsetHours}:${offsetMinutes}`; } export function pick(obj, keys) { const result = {}; if (!obj || typeof obj !== 'object') { return result; } for (const key of keys) { if (key in obj) { result[key] = obj[key]; } } return result; } export function isEmpty(value) { if (value === null) return true; if (value === undefined) return true; if (typeof value === 'boolean') return false; if (typeof value === 'number') return false; if (typeof value === 'string') return value.length === 0; if (typeof value === 'function') return false; if (Array.isArray(value)) return value.length === 0; if (value instanceof Map || value instanceof Set) return value.size === 0; if (typeof value === 'object') { return Object.keys(value).length === 0; } return false; } function groupBy(array, property) { const grouped = {}; for (const item of array) { const key = item[property]; if (!grouped[key]) { grouped[key] = []; } grouped[key].push(item); } return grouped; } export function setProperty(object, path, value) { if (typeof path === 'string') { path = path.split('.'); } if (!Array.isArray(path) || path.length === 0) { return; } let current = object; for (let index = 0; index < path.length - 1; index++) { const key = path[index]; if (current[key] === undefined) { current[key] = {}; } current = current[key]; } current[path.at(-1)] = value; } /** * A replacement for lodash.get(object, path, [defaultValue]). * */ export function getProperty(object, path, defaultValue) { // eslint-disable-next-line unicorn/no-null if (object == null) { return defaultValue; } if (!Array.isArray(path)) { path = path .replaceAll(/\[(.*?)\]/g, '.$1') .split('.') .filter(Boolean); } let result = object; for (const key of path) { // eslint-disable-next-line unicorn/no-null if (result == null) { return defaultValue; } result = result[key]; } return result === undefined ? defaultValue : result; } function isPlainObject(value) { if (value === null || typeof value !== 'object') return false; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } function canMergeInto(value) { return value !== null && typeof value === 'object'; } function mergeInto(target, source) { for (const key of Object.keys(source)) { if (key === '__proto__' || key === 'constructor' || key === 'prototype') { continue; } const sourceValue = source[key]; const targetValue = target[key]; if (Array.isArray(sourceValue)) { const into = Array.isArray(targetValue) ? targetValue : []; mergeInto(into, sourceValue); target[key] = into; } else if (isPlainObject(sourceValue)) { const into = canMergeInto(targetValue) ? targetValue : {}; mergeInto(into, sourceValue); target[key] = into; } else if (sourceValue === undefined) { if (!(key in target)) target[key] = undefined; } else { target[key] = sourceValue; } } } /** * A replacement for lodash.merge(target, ...sources). * */ export function merge(target, ...sources) { if (target === undefined || target === null) return target; for (const source of sources) { if (source === undefined || source === null) continue; mergeInto(target, source); } return target; }