browsertime
Version:
Get performance metrics from your web page using Browsertime.
380 lines (341 loc) • 10.4 kB
JavaScript
import { createRequire } from 'node:module';
import merge from 'lodash.merge';
import pick from 'lodash.pick';
import isEmpty from 'lodash.isempty';
import get from 'lodash.get';
import dayjs from 'dayjs';
import { pathToFolder } from '../pathToFolder.js';
import intel from 'intel';
const log = intel.getLogger('browsertime');
const require = createRequire(import.meta.url);
const version = require('../../../package.json').version;
function generateUniquePageId(baseId, existingIdMap) {
let newId = baseId;
while (existingIdMap.has(newId)) {
newId = newId + '-1';
}
return newId;
}
function getDocumentRequests(entries, pageId) {
let pageEntries = [...entries];
if (pageId) {
pageEntries = entries.filter(entry => entry.pageref === pageId);
}
const requests = [];
let entry;
do {
entry = pageEntries.shift();
requests.push(entry);
} while (entry.response.redirectURL);
return requests;
}
function getFinalURL(entries, pageref) {
const requests = getDocumentRequests(entries, pageref);
const finalEntry = requests.pop();
return finalEntry.request.url;
}
function addExtrasToHAR(
harPage,
visualMetricsData,
timings,
cpu,
googleWebVitals
) {
const harPageTimings = harPage.pageTimings;
const _visualMetrics = (harPage._visualMetrics = {});
harPage._cpu = cpu;
if (googleWebVitals) {
harPage._googleWebVitals = googleWebVitals;
}
// We add the timings both as a hidden field and add
// in pageTimings so we can easily show them in PerfCascade
if (visualMetricsData) {
const DO_NOT_INCLUDE_IN_HAR_TIMINGS = new Set([
'VisualReadiness',
'videoRecordingStart',
'SpeedIndex',
'PerceptualSpeedIndex',
'ContentfulSpeedIndex',
'VisualProgress',
'ContentfulSpeedIndexProgress',
'PerceptualSpeedIndexProgress'
]);
for (let key of Object.keys(visualMetricsData)) {
if (!DO_NOT_INCLUDE_IN_HAR_TIMINGS.has(key)) {
harPageTimings['_' + key.charAt(0).toLowerCase() + key.slice(1)] =
visualMetricsData[key];
_visualMetrics[key] = visualMetricsData[key];
} else if (!key.endsWith('Progress')) {
_visualMetrics[key] = visualMetricsData[key];
}
}
// Make the visual progress structure more JSON
_visualMetrics.VisualProgress = jsonifyVisualProgress(
visualMetricsData.VisualProgress
);
} else if (timings && timings.firstPaint) {
// only add first paint if we don't have visual metrics
harPageTimings._firstPaint = timings.firstPaint;
}
/*
if (cpu && cpu.longTasks && cpu.longTasks.lastLongTask) {
harPageTimings._lastCPULongTask = cpu.longTasks.lastLongTask;
}
*/
if (timings && timings.largestContentfulPaint) {
harPageTimings._largestContentfulPaint =
timings.largestContentfulPaint.renderTime;
}
if (
timings &&
timings.paintTiming &&
timings.paintTiming['first-contentful-paint']
) {
harPageTimings._firstContentfulPaint =
timings.paintTiming['first-contentful-paint'];
}
if (timings && timings.pageTimings) {
harPageTimings._domInteractiveTime = timings.pageTimings.domInteractiveTime;
harPageTimings._domContentLoadedTime =
timings.pageTimings.domContentLoadedTime;
}
}
function addMetaToHAR(index, harPage, url, browserScript, options) {
const _meta = (harPage._meta = {});
_meta.connectivity = get(options, 'connectivity.profile', 'native');
_meta.connectivity = get(options, 'connectivity.alias', _meta.connectivity);
if (options.resultURL) {
const base = options.resultURL.endsWith('/')
? options.resultURL
: options.resultURL + '/';
if (options.screenshot) {
_meta.screenshot = `${base}${pathToFolder(url, options)}screenshots/${
index + 1
}/afterPageCompleteCheck.${options.screenshotParams.type}`;
}
if (options.video) {
_meta.video = `${base}${pathToFolder(url, options)}video/${
index + 1
}.mp4`;
}
if (options.chrome && options.chrome.timeline) {
_meta.timeline = `${base}${pathToFolder(url, options)}trace-${
index + 1
}.json.gz`;
}
}
if (browserScript && browserScript.pageinfo) {
_meta.generator = browserScript.pageinfo.generator;
}
}
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%"
const visualProgressJSON = {};
for (let value of visualProgress) {
visualProgressJSON[value.timestamp] = value.percent;
}
return visualProgressJSON;
}
export function addBrowser(har, name, version, comment) {
merge(har.log, {
browser: {
name,
version,
comment
}
});
if (!comment) {
delete har.log.browser.comment;
}
return har;
}
export function addCreator(har, comment) {
merge(har.log, {
creator: {
name: 'Browsertime',
version: version,
comment: comment
}
});
if (!comment) {
delete har.log.creator.comment;
}
return har;
}
export function getMainDocumentTimings(har) {
const timings = [];
const entries = [...har.log.entries];
for (let page of har.log.pages) {
const pageId = page.id;
const url = page._url;
let pageEntries = [...entries];
const finalURL = getFinalURL(pageEntries, pageId);
pageEntries = pageEntries.filter(
entry => entry.pageref === pageId && entry.request.url === finalURL
);
timings.push({ url, timings: pageEntries[0].timings });
}
return timings;
}
export function getFullyLoaded(har) {
const fullyLoaded = [];
const entries = [...har.log.entries];
for (let page of har.log.pages) {
const pageStartDateTime = new Date(page.startedDateTime).getTime();
const pageId = page.id;
const url = page._url;
let pageEntries = [...entries];
pageEntries = pageEntries.filter(entry => entry.pageref === pageId);
let pageEnd = 0;
for (let entry of pageEntries) {
let entryEnd =
new Date(entry.startedDateTime).getTime() +
entry.time -
new Date(pageStartDateTime).getTime();
if (entryEnd > pageEnd) {
pageEnd = entryEnd;
}
}
fullyLoaded.push({ url, fullyLoaded: Number(pageEnd.toFixed(0)) });
}
return fullyLoaded;
}
export function mergeHars(hars) {
if (isEmpty(hars)) {
return;
}
if (hars.length === 1) {
return hars[0];
}
let firstLog = hars[0].log;
let combinedHar = {
log: pick(firstLog, ['version', 'creator', 'browser', 'comment'])
};
let pagesById = new Map();
let allEntries = [];
for (const har of hars) {
let pages = har.log.pages;
let entries = har.log.entries;
for (const page of pages) {
let pageId = page.id;
if (pagesById.has(pageId)) {
const oldPageId = pageId;
pageId = generateUniquePageId(oldPageId, pagesById);
page.id = pageId;
entries = entries.map(entry => {
if (entry.pageref === oldPageId) {
entry.pageref = pageId;
}
return entry;
});
}
pagesById.set(pageId, page);
}
allEntries = [...allEntries, ...entries];
}
combinedHar.log.pages = [...pagesById.values()];
combinedHar.log.entries = allEntries;
return combinedHar;
}
export function getEmptyHAR(url, browser) {
return {
log: {
version: '1.2',
creator: {
name: 'Browsertime',
version: version,
comment: ''
},
browser: {
name: browser,
version: ''
},
pages: [
{
startedDateTime: dayjs().format(),
id: 'failing_page',
title: url,
pageTimings: {},
comment: ''
}
],
entries: [],
comment: ''
}
};
}
export function addExtraFieldsToHar(totalResults, har, options) {
if (har) {
let harPageNumber = 0;
// We test one page
// Let's do a better fix for this later on
// right now this fixes https://github.com/sitespeedio/browsertime/issues/754
if (har.log.pages.length === options.iterations) {
for (let harPage of har.log.pages) {
let pageNumber = harPageNumber % totalResults.length;
const visualMetric =
totalResults[pageNumber].visualMetrics[harPageNumber];
const browserScript =
totalResults[pageNumber].browserScripts[harPageNumber];
const cpu = totalResults[pageNumber].cpu[harPageNumber];
const googleWebVitals =
totalResults[pageNumber].googleWebVitals[harPageNumber];
addMetaToHAR(
harPageNumber,
harPage,
totalResults[pageNumber].info.url,
browserScript,
options
);
if (browserScript) {
addExtrasToHAR(
harPage,
visualMetric,
browserScript.timings,
cpu,
googleWebVitals
);
}
harPageNumber++;
}
} else {
const numberOfPages = totalResults.length;
for (let page = 0; page < numberOfPages; page++) {
for (let iteration = 0; iteration < options.iterations; iteration++) {
let harIndex = iteration * totalResults.length;
harIndex += page;
const visualMetric = totalResults[page].visualMetrics[iteration];
const browserScript = totalResults[page].browserScripts[iteration];
const cpu = totalResults[page].cpu[iteration];
const googleWebVitals = totalResults[page].googleWebVitals[iteration];
const harPage = har.log.pages[harIndex];
// Only add meta if we have a HAR
if (harPage) {
addMetaToHAR(
iteration,
harPage,
totalResults[page].info.url,
browserScript,
options
);
} else {
log.error(
'Could not add meta data to the HAR, miss page ' + harIndex
);
}
// Only add the metrics if we was able to collect the metrics and have a HAR
if (browserScript && harPage) {
addExtrasToHAR(
harPage,
visualMetric,
browserScript.timings,
cpu,
googleWebVitals
);
}
}
}
}
}
}