Skip to content

Commit

Permalink
Merge branch 'main' into disable-simulator
Browse files Browse the repository at this point in the history
  • Loading branch information
soulgalore authored Jan 8, 2024
2 parents a750385 + 6c07719 commit 941cbf2
Show file tree
Hide file tree
Showing 213 changed files with 7,327 additions and 795 deletions.
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
!browsertime
!browserscripts
!browsersupport
!index.js
!index.js
!visualmetrics
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Browsertime changelog (we do [semantic versioning](https://semver.org))

## 21.0.0 - 2024-01-07

The new version uses JSDoc to generate documentation for scripting and make it possible to use code completion/Intellisense! Documentation for that is coming soon.

### Added
* Upgraded to [Geckodriver 0.34.0](https://github.com/mozilla/geckodriver/releases/tag/v0.34.0) [#2049](https://github.com/sitespeedio/browsertime/pull/2049).
* Collect CPU consumption for Firefox. Turn that on with `--firefox.powerConsumption true` and including `power` as a geckoProfilerParams.features [#2046](https://github.com/sitespeedio/browsertime/pull/2046).
* Added more commands for mouse click on text [#2054](https://github.com/sitespeedio/browsertime/pull/2054).
* Updated AndroidCommand so you can run shell on your Android device as root [#2055](https://github.com/sitespeedio/browsertime/pull/2055).
* If you mark a test as failure, the exit code from Browsertime will be 1. If the exitCode is set in scripting, we use that and will not change that [#2057](https://github.com/sitespeedio/browsertime/pull/2057).
* Generate documentation for scripting using JSDoc [#2059](https://github.com/sitespeedio/browsertime/pull/2059).
* Make it easy to use Seleniums action API. Get access to the new command using commands.action.getActions() and chain the action. [#2061](https://github.com/sitespeedio/browsertime/pull/2061)


### Fixed
* Make sure the visual metrics files are inlcuded in the Docker file [#2053](https://github.com/sitespeedio/browsertime/pull/2053).
* Removing QVH from the npm package (used for iPhone video recording but not working) [#2051](https://github.com/sitespeedio/browsertime/pull/2051)
* Removing visual metrics test images from the npm package [#2050](https://github.com/sitespeedio/browsertime/pull/2050).
* Removed the Chromedriver fix that was needed when Chrome for testing broke testing on Chrome :D [#2045](https://github.com/sitespeedio/browsertime/pull/2045).
* Refactor of commands/context object to prepare for supporting JSDoc and a little TypeScript to add code completion/IntelliSense in editors [#2047](https://github.com/sitespeedio/browsertime/pull/2047).
* Updated documentation for scripting with better JSDoc [#204](https://github.com/sitespeedio/browsertime/pull/2048).
* The code for getting Interaction to next paint was broken. This PR fixes it, make the code cleaner and gives more attribution [#2060](https://github.com/sitespeedio/browsertime/pull/2060).

## 20.0.0 - 2023-12-22

### Breaking
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,13 @@ You can change latency by setting a Docker environment variable. Use REPLAY to t
Default browser is Chrome:

```
docker run --cap-add=NET_ADMIN --rm -v "$(pwd)":/browsertime -e REPLAY=true -e LATENCY=100 sitespeedio/browsertime:12.0.0 https://en.wikipedia.org/wiki/Barack_Obama
docker run --cap-add=NET_ADMIN --rm -v "$(pwd)":/browsertime -e REPLAY=true -e LATENCY=100 sitespeedio/browsertime:20.0.0 https://en.wikipedia.org/wiki/Barack_Obama
```

Use Firefox:

```shell
docker run --cap-add=NET_ADMIN --rm -v "$(pwd)":/browsertime -e REPLAY=true -e LATENCY=100 sitespeedio/browsertime:19.2.0 -b firefox -n 11 --firefox.acceptInsecureCerts https://en.wikipedia.org/wiki/Barack_Obama
docker run --cap-add=NET_ADMIN --rm -v "$(pwd)":/browsertime -e REPLAY=true -e LATENCY=100 sitespeedio/browsertime:20.0.0 -b firefox -n 11 --firefox.acceptInsecureCerts true https://en.wikipedia.org/wiki/Barack_Obama
```

And Chrome on your Android phone. This will only work on Linux because you need to be able to mount the usb port in Docker:
Expand Down
12 changes: 9 additions & 3 deletions bin/browsertime.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,17 @@ async function run(urls, options) {
const resultDirectory = relative(process.cwd(), storageManager.directory);

// check for errors
for (let eachResult of result) {
for (let errors of eachResult.errors) {
if (errors.length > 0) {
// If we have set the exit code in scripts, respect that
if (process.exitCode === undefined) {
for (let eachResult of result) {
if (eachResult.markedAsFailure === 1) {
process.exitCode = 1;
}
for (let errors of eachResult.errors) {
if (errors.length > 0) {
process.exitCode = 1;
}
}
}
}
log.info(`Wrote data to ${resultDirectory}`);
Expand Down
313 changes: 170 additions & 143 deletions browserscripts/pageinfo/interactionToNextPaintInfo.js
Original file line number Diff line number Diff line change
@@ -1,152 +1,179 @@
(function () {
// https://github.com/GoogleChrome/web-vitals/blob/main/src/lib/getLoadState.ts#L20

// This is an updated version of
// https://github.com/GoogleChrome/web-vitals/blob/64f133590fcac72c1bc042bf7b4ab729d7e03316/src/onINP.ts
// It was reworked the 19/5-2023
function getLoadState(timestamp) {
if (document.readyState === 'loading') {
// If the `readyState` is 'loading' there's no need to look at timestamps
// since the timestamp has to be the current time or earlier.
return 'loading';
} else {
const navigationEntry = performance.getEntriesByType('navigation')[0];
if (navigationEntry) {
if (timestamp < navigationEntry.domInteractive) {
return 'loading';
} else if (
navigationEntry.domContentLoadedEventStart === 0 ||
timestamp < navigationEntry.domContentLoadedEventStart
) {
// If the `domContentLoadedEventStart` timestamp has not yet been
// set, or if the given timestamp is less than that value.
return 'dom-interactive';
} else if (
navigationEntry.domComplete === 0 ||
timestamp < navigationEntry.domComplete
) {
// If the `domComplete` timestamp has not yet been
// set, or if the given timestamp is less than that value.
return 'dom-content-loaded';
}
}
}
// If any of the above fail, default to loaded. This could really only
// happy if the browser doesn't support the performance timeline, which
// most likely means this code would never run anyway.
return 'complete';
}

const supported = PerformanceObserver.supportedEntryTypes;
if (!supported || supported.indexOf('event') === -1 ||  supported.indexOf('first-input') === -1) {
return;
// https://github.com/GoogleChrome/web-vitals/blob/main/src/lib/getSelector.ts#L24
function getName(node) {
const name = node.nodeName;
return node.nodeType === 1
? name.toLowerCase()
: name.toUpperCase().replace(/^#/, '');
}
var observe = function observe(type, callback, opts) {
try {
if (PerformanceObserver.supportedEntryTypes.includes(type)) {
var po = new PerformanceObserver(function (list) {
Promise.resolve().then(function () {
callback(list.getEntries());
});
});
po.observe(Object.assign({type: type, buffered: true}, opts || {}));
return po;
}
} catch (e) {}
return;
};
var interactionCountEstimate = 0;
var minKnownInteractionId = Infinity;
var maxKnownInteractionId = 0;
var updateEstimate = function updateEstimate(entries) {
entries.forEach(function (e) {
if (e.interactionId) {
minKnownInteractionId = Math.min(
minKnownInteractionId,
e.interactionId
);
maxKnownInteractionId = Math.max(
maxKnownInteractionId,
e.interactionId
);
interactionCountEstimate = maxKnownInteractionId
? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1
: 0;
}
});
};
var po;
var getInteractionCount = function getInteractionCount() {
return po ? interactionCountEstimate : performance.interactionCount || 0;
};
var initInteractionCountPolyfill = function initInteractionCountPolyfill() {
if ('interactionCount' in performance || po) return;
po = observe('event', updateEstimate, {
type: 'event',
buffered: true,
durationThreshold: 0,
});
};
var prevInteractionCount = 0;
var getInteractionCountForNavigation =
function getInteractionCountForNavigation() {
return getInteractionCount() - prevInteractionCount;
};
var MAX_INTERACTIONS_TO_CONSIDER = 10;
var longestInteractionList = [];
var longestInteractionMap = {};
var processEntry = function processEntry(entry) {
var minLongestInteraction =
longestInteractionList[longestInteractionList.length - 1];
var existingInteraction = longestInteractionMap[entry.interactionId];
if (
existingInteraction ||
longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
entry.duration > minLongestInteraction.latency
) {
if (existingInteraction) {
existingInteraction.entries.push(entry);
existingInteraction.latency = Math.max(
existingInteraction.latency,
entry.duration
);
} else {
var interaction = {
id: entry.interactionId,
latency: entry.duration,
entries: [entry],
};
longestInteractionMap[interaction.id] = interaction;
longestInteractionList.push(interaction);
}
longestInteractionList.sort(function (a, b) {
return b.latency - a.latency;
});
longestInteractionList
.splice(MAX_INTERACTIONS_TO_CONSIDER)
.forEach(function (i) {
delete longestInteractionMap[i.id];
});

function getSelector(node) {
let sel = '';

try {
while (node && node.nodeType !== 9) {
const el = node;
const part = el.id
? '#' + el.id
: getName(el) +
(el.classList &&
el.classList.value &&
el.classList.value.trim() &&
el.classList.value.trim().length
? '.' + el.classList.value.trim().replace(/\s+/g, '.')
: '');
if (sel.length + part.length > 100 - 1) return sel || part;
sel = sel ? part + '>' + sel : part;
if (el.id) break;
node = el.parentNode;
}
};
var estimateP98LongestInteraction = function estimateP98LongestInteraction() {
var candidateInteractionIndex = Math.min(
longestInteractionList.length - 1,
Math.floor(getInteractionCountForNavigation() / 50)
);
return longestInteractionList[candidateInteractionIndex];
};

var opts = {};
var metric = {};
initInteractionCountPolyfill();
var handleEntries = function handleEntries(entries) {
entries.forEach(function (entry) {
if (entry.interactionId) {
processEntry(entry);
}
if (entry.entryType === 'first-input') {
var noMatchingEntry = !longestInteractionList.some(function (
interaction
) {
return interaction.entries.some(function (prevEntry) {
return (
entry.duration === prevEntry.duration &&
entry.startTime === prevEntry.startTime
);
});
});
if (noMatchingEntry) {
processEntry(entry);
} catch (err) {
// Do nothing...
}
return sel;
}

// https://gist.github.com/karlgroves/7544592
function getDomPath(el) {
const stack = [];
while (el.parentNode != null) {
let sibCount = 0;
let sibIndex = 0;
for (let i = 0; i < el.parentNode.childNodes.length; i++) {
let sib = el.parentNode.childNodes[i];
if (sib.nodeName == el.nodeName) {
if (sib === el) {
sibIndex = sibCount;
}
sibCount++;
}
});
var inp = estimateP98LongestInteraction();
if (inp && inp.latency !== metric.value) {
var cleanedEntries = [];
for (var entry of inp.entries) {
console.log(entry);
cleanedEntries.push({
id: entry.interactionId,
latency: entry.duration,
name: entry.name
});
}
return {latency: inp.latency, entries: cleanedEntries};
}
};
var po = observe('event', handleEntries, {
durationThreshold: opts.durationThreshold || 40,
});
if (po) {
po.observe({type: 'first-input', buffered: true});
if (el.hasAttribute && el.hasAttribute('id') && el.id != '') {
stack.unshift(el.nodeName.toLowerCase() + '#' + el.id);
} else if (sibCount > 1) {
stack.unshift(el.nodeName.toLowerCase() + ':eq(' + sibIndex + ')');
} else {
stack.unshift(el.nodeName.toLowerCase());
}
el = el.parentNode;
}

return stack.slice(1);
}

// https://github.com/GoogleChrome/web-vitals/blob/main/src/onINP.ts
const observer = new PerformanceObserver(list => {});
observer.observe({type: 'event', buffered: true});
observer.observe({type: 'first-input', buffered: true});
const entries = observer.takeRecords();

const MAX_INTERACTIONS_TO_CONSIDER = 10;
const longestInteractionList = [];
const longestInteractionMap = {};
for (let entry of entries) {
var minLongestInteraction =
longestInteractionList[longestInteractionList.length - 1];
var existingInteraction = longestInteractionMap[entry.interactionId];
if (
existingInteraction ||
longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
entry.duration > minLongestInteraction.latency
) {
if (existingInteraction) {
existingInteraction.entries.push(entry);
existingInteraction.latency = Math.max(
existingInteraction.latency,
entry.duration
);
} else {
var interaction = {
id: entry.interactionId,
latency: entry.duration,
entries: [entry]
};
longestInteractionMap[interaction.id] = interaction;
longestInteractionList.push(interaction);
}
longestInteractionList.sort(function (a, b) {
return b.latency - a.latency;
});
longestInteractionList
.splice(MAX_INTERACTIONS_TO_CONSIDER)
.forEach(function (i) {
delete longestInteractionMap[i.id];
});
}
}

const inp = longestInteractionList[longestInteractionList.length - 1];
if (inp) {
const cleanedEntries = [];

for (let entry of inp.entries) {
cleanedEntries.push({
id: entry.interactionId,
latency: entry.duration,
name: entry.name
});
}
})({});


const longestEntry = inp.entries.sort((a, b) => {
// Sort by: 1) duration (DESC), then 2) processing time (DESC)
return (
b.duration - a.duration ||
b.processingEnd -
b.processingStart -
(a.processingEnd - a.processingStart)
);
})[0];
let element = longestEntry.target;

return {
latency: inp.latency,
entries: cleanedEntries,
eventType: longestEntry.name,
eventTime: longestEntry.startTime,
eventTarget: getSelector(element),
loadState: getLoadState(longestEntry.startTime),
domPath: element ? getDomPath(element).join(' > ') : '',
tagName: element ? element.tagName : '',
className: element ? element.className : '',
tag: element ? element.cloneNode(false).outerHTML : ''
};
}
})({});
Loading

0 comments on commit 941cbf2

Please sign in to comment.