Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM sitespeedio/webbrowsers:chrome-142.0-firefox-144.0-edge-141.0
FROM sitespeedio/webbrowsers:chrome-142.0-firefox-144.0-edge-142.0-b

ARG TARGETPLATFORM=linux/amd64

Expand Down
29 changes: 17 additions & 12 deletions docker/scripts/start.sh
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
#!/bin/bash
set -e

# write files owned by the user who runs the container
# if your volume is mounted at /browsertime, use it as CWD
[[ -d /browsertime && "$PWD" = "/" ]] && cd /browsertime

uid=$(stat -c '%u' . 2>/dev/null || echo 0)
gid=$(stat -c '%g' . 2>/dev/null || echo 0)

run_as_host() {
if [[ "$uid" -ne 0 && "$gid" -ne 0 ]]; then
HOME=/tmp chroot --skip-chdir --userspec="+${uid}:+${gid}" / "$@"
else
HOME=/tmp "$@"
fi
}

# See https://github.com/SeleniumHQ/docker-selenium/issues/87
export DBUS_SESSION_BUS_ADDRESS=/dev/null

# All browsers do not exist in all architectures.
if [[ `which google-chrome` ]]; then
google-chrome --version
elif [[ `which chromium-browser` ]]; then
chromium-browser --version
fi

if [[ `which firefox` ]]; then
firefox --version
firefox --version 2>/dev/null
fi

if [[ `which microsoft-edge` ]]; then
Expand All @@ -38,20 +51,12 @@ else
WPR_HTTPS_PORT=${WPR_HTTPS_PORT:-443}
fi

WORKDIR_UID=$(stat -c "%u" .)
WORKDIR_GID=$(stat -c "%g" .)

# Create user with the same UID and GID as the owner of the working directory, which will be used
# to execute node. This is partly for security and partly so output files won't be owned by root.
groupadd --non-unique --gid $WORKDIR_GID browsertime
useradd --non-unique --uid $WORKDIR_UID --gid $WORKDIR_GID --home-dir /tmp browsertime

# Need to explictly override the HOME directory to prevent dconf errors like:
# (firefox:2003): dconf-CRITICAL **: 00:31:23.379: unable to create directory '/root/.cache/dconf': Permission denied. dconf will not work properly.
export HOME=/tmp

function execNode(){
chroot --skip-chdir --userspec='browsertime:browsertime' / node "$@"
run_as_host node "$@"
}

# Here's a hack for fixing the problem with Chrome not starting in time
Expand Down
22 changes: 9 additions & 13 deletions lib/screenshot/loadCustomJimp.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
export async function loadCustomJimp() {
try {
const { default: configure } = await import('@jimp/custom');
const { default: pluginPng } = await import('@jimp/png');
const { default: pluginJpeg } = await import('@jimp/jpeg');
const { default: pluginScale } = await import('@jimp/plugin-scale');
// The scale plugin use resize
const { default: pluginResize } = await import('@jimp/plugin-resize');
const jimp = configure({
types: [pluginPng, pluginJpeg],
plugins: [pluginResize, pluginScale]
});
return jimp;
} catch {
return;
const { Jimp } = await import('jimp');

return Jimp;
} catch (error) {
if (error?.code === 'ERR_MODULE_NOT_FOUND') {
return;
}

throw error;
}
}
13 changes: 7 additions & 6 deletions lib/support/images/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export async function savePng(
if (jimp) {
const image = await jimp.read(data);
const buffer = await image
.deflateLevel(config.png.compressionLevel)
.scaleToFit(config.maxSize, config.maxSize, jimp.RESIZE_HERMITE)
.getBufferAsync('image/png');
.scaleToFit({ w: config.maxSize, h: config.maxSize })
.getBuffer('image/png');
console.log('3');

return storageManager.writeData(
`${name}.png`,
Expand All @@ -61,11 +61,12 @@ export async function saveJpg(

if (jimp) {
const image = await jimp.read(data);

// https://github.com/sitespeedio/sitespeed.io/issues/3922
const buffer = await image
.quality(config.jpg.quality)
.scaleToFit(config.maxSize, config.maxSize, jimp.RESIZE_HERMITE)
.getBufferAsync('image/jpeg');
.scaleToFit({ w: config.maxSize, h: config.maxSize })
.getBuffer('image/jpeg', { quality: config.jpg.quality });

return storageManager.writeData(
`${name}.jpg`,
buffer,
Expand Down
118 changes: 70 additions & 48 deletions lib/support/pathToFolder.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,90 @@
import { parse } from 'node:url';
import { createHash } from 'node:crypto';
import path from 'node:path';
import { getLogger } from '@sitespeed.io/log';
import { isEmpty } from './util.js';

const log = getLogger('browsertime');

function isHttpLikeUrl(s) {
if (typeof s !== 'string' || s.length === 0) return false;
if (s.startsWith('//')) return true;
return /^https?:\/\//iu.test(s);
}

function toSafeKey(key) {
// U+2013 : EN DASH – as used on https://en.wikipedia.org/wiki/2019–20_coronavirus_pandemic
return key.replaceAll(/[ %&()+,./:?|~–]|%7C/g, '-');
return key.replaceAll(/[ %&()+,./:?|~–]|%7C/gu, '-');
}

export function pathToFolder(url, options) {
if (options.useSameDir) {
return '';
} else {
const useHash = options.useHash;
const parsedUrl = parse(decodeURIComponent(url));
function md5Hex8(s) {
return createHash('md5').update(s).digest('hex').slice(0, 8);
}

const pathSegments = [];
const urlSegments = [];
pathSegments.push('pages', parsedUrl.hostname.split('.').join('_'));
function normalizeFsPath(input) {
let n = path.normalize(input);
if (n.startsWith(`.${path.sep}`)) n = n.slice(2);
return n;
}

if (options.urlMetaData && options.urlMetaData[url]) {
pathSegments.push(options.urlMetaData[url]);
} else {
if (!isEmpty(parsedUrl.pathname)) {
urlSegments.push(...parsedUrl.pathname.split('/').filter(Boolean));
}
export function pathToFolder(input, options) {
if (options.useSameDir) return '';

if (useHash && !isEmpty(parsedUrl.hash)) {
const md5 = createHash('md5'),
hash = md5.update(parsedUrl.hash).digest('hex').slice(0, 8);
urlSegments.push('hash-' + hash);
}
let hostname = '';
let pathname = '';
let search = '';
let hash = '';

if (!isEmpty(parsedUrl.search)) {
const md5 = createHash('md5'),
hash = md5.update(parsedUrl.search).digest('hex').slice(0, 8);
urlSegments.push('query-' + hash);
}
const isUrl = isHttpLikeUrl(input);

// This is used from sitespeed.io to match URLs on Graphite
if (options.storeURLsAsFlatPageOnDisk) {
const folder = toSafeKey(urlSegments.join('_').concat('_'));
if (folder.length > 255) {
log.info(
`The URL ${url} hit the 255 character limit used when stored on disk, you may want to give your URL an alias to make sure it will not collide with other URLs.`
);
pathSegments.push(folder.slice(0, 254));
} else {
pathSegments.push(folder);
}
} else {
pathSegments.push(...urlSegments);
}
}
if (isUrl) {
const raw = input.startsWith('//') ? `http:${input}` : input;
const u = new URL(raw);
hostname = u.hostname;
pathname = u.pathname; // '/'-separated
search = u.search; // includes '?'
hash = u.hash; // includes '#'
} else {
hostname = 'file';
const fsNormalized = normalizeFsPath(input);
pathname = `${path.sep}${fsNormalized}`;
}

const pathSegments = ['pages', hostname.split('.').join('_')];
const urlSegments = [];

if (options.urlMetaData && options.urlMetaData[input]) {
pathSegments.push(options.urlMetaData[input]);
} else {
const parts = isUrl
? pathname.split('/').filter(Boolean)
: pathname.split(/[\\/]/u).filter(Boolean);
if (!isEmpty(parts)) urlSegments.push(...parts);

pathSegments.push('data');
if (isUrl) {
if (options.useHash && !isEmpty(hash))
urlSegments.push(`hash-${md5Hex8(hash)}`);
if (!isEmpty(search)) urlSegments.push(`query-${md5Hex8(search)}`);
}

for (const [index, segment] of pathSegments.entries()) {
if (segment) {
pathSegments[index] = segment.replaceAll(/[^\w.\u0621-\u064A-]/gi, '-');
if (options.storeURLsAsFlatPageOnDisk) {
const folder = toSafeKey(`${urlSegments.join('_')}_`);
if (folder.length > 255) {
log.info(
`The URL ${input} hit the 255 character limit used when stored on disk, you may want to give your URL an alias to make sure it will not collide with other URLs.`
);
pathSegments.push(folder.slice(0, 254));
} else {
pathSegments.push(folder);
}
} else {
pathSegments.push(...urlSegments);
}
}

pathSegments.push('data');

return pathSegments.join('/').concat('/');
for (const [i, seg] of pathSegments.entries()) {
if (seg) pathSegments[i] = seg.replaceAll(/[^\w.\u0621-\u064A-]/giu, '-');
}

return `${path.join(...pathSegments)}${path.sep}`;
}
44 changes: 31 additions & 13 deletions lib/support/storageManager.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// storage-manager.js
import path from 'node:path';
import { createHash } from 'node:crypto';
import { gunzip as _gunzip, gzip as _gzip, createGzip } from 'node:zlib';
import { parse } from 'node:url';
import { URL } from 'node:url';
import { promisify } from 'node:util';
import {
writeFile as _writeFile,
Expand All @@ -25,16 +26,35 @@ const log = getLogger('browsertime');
const defaultDir = 'browsertime-results';
let timestamp = localTime().replaceAll(':', '');

function pathNameFromUrl(url) {
const parsedUrl = parse(url),
pathSegments = parsedUrl.pathname.split('/');
function pathNameFromUrl(input) {
// If it's a proper web URL → use WHATWG URL.
// Otherwise treat it as a filesystem path and return only the basename.
let asUrl;
try {
asUrl = new URL(input); // succeeds only for absolute URLs like https://..., file://..., etc.
} catch {
// Filesystem path case (e.g. "test.js", "my/path/to/test.js")
// We want exactly "test.js" (no hostname, no parent dirs).
let base = path.basename(path.normalize(input));
// If someone passed a trailing slash directory, basename returns '' → keep it stable.
if (!base)
base = input.replaceAll('\\', '/').split('/').findLast(Boolean) ?? '';
return base;
}

// URL case: mirror your previous behavior
const decodedPathname = decodeURIComponent(asUrl.pathname);
const pathSegments = decodedPathname.split('/');

pathSegments.unshift(parsedUrl.hostname);
// Only add hostname for real web URLs (file: URLs typically have empty hostname on POSIX)
if (asUrl.hostname) {
pathSegments.unshift(asUrl.hostname);
}

if (!isEmpty(parsedUrl.search)) {
const md5 = createHash('md5'),
hash = md5.update(parsedUrl.search).digest('hex').slice(0, 8);
pathSegments.push('query-' + hash);
if (!isEmpty(asUrl.search)) {
const md5 = createHash('md5');
const hash = md5.update(asUrl.search).digest('hex').slice(0, 8);
pathSegments.push(`query-${hash}`);
}

return pathSegments.filter(Boolean).join('-');
Expand Down Expand Up @@ -73,8 +93,7 @@ export class StorageManager {
}

async writeData(filename, data, subdir) {
let dirPath;
dirPath = await (subdir
const dirPath = await (subdir
? this.createSubDataDir(subdir)
: this.createDataDir());
const fullPath = path.join(dirPath, filename);
Expand All @@ -94,8 +113,7 @@ export class StorageManager {
}

async readData(filename, subdir) {
let filepath;
filepath = subdir
const filepath = subdir
? path.join(this.baseDir, subdir, filename)
: path.join(this.baseDir, filename);

Expand Down
Loading
Loading