Skip to content
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
2747124
initial test
flashdesignory May 8, 2025
f0f3d93
temp
flashdesignory May 9, 2025
43a9646
another checkin
flashdesignory May 9, 2025
075f8ff
simplify
flashdesignory May 9, 2025
be099ff
fix e2e
flashdesignory May 9, 2025
0c73ade
fix up stuff
flashdesignory May 9, 2025
0c63bc0
fix remote runner
flashdesignory May 9, 2025
7d3145b
enable config suite by default
flashdesignory May 9, 2025
84983e0
remove console
flashdesignory May 9, 2025
17fadf7
changes
flashdesignory May 9, 2025
bdd87d4
use params
flashdesignory May 13, 2025
7c9c69e
rename configUrl to config
flashdesignory May 13, 2025
4707d5f
remove Suites and Tags
flashdesignory May 13, 2025
efb87d2
feedback
flashdesignory May 14, 2025
7c724d9
fix e2e config param
flashdesignory May 14, 2025
fba0a2c
remove console
flashdesignory May 14, 2025
4743a1b
fix it up
flashdesignory May 14, 2025
e6ea8b9
allowlist
flashdesignory May 20, 2025
4de58a8
remove comment
flashdesignory Jun 2, 2025
89ca145
run format
flashdesignory Jun 2, 2025
0f511fe
add comment
flashdesignory Jun 28, 2025
6cc6414
cleanup catch in params
flashdesignory Jun 28, 2025
2a85323
rename _containsAllowedUrl to _isAllowedUrl and pass in url directly
flashdesignory Jun 28, 2025
2a03314
add basic validation to the init function
flashdesignory Jun 28, 2025
516d684
simplify suite.disabled assignment
flashdesignory Jun 28, 2025
8c9fd21
simplify suite.disabled assignment p2
flashdesignory Jun 28, 2025
6f8ef3e
rename _suites and _tags params
flashdesignory Jun 28, 2025
a807894
cleanup _isAllowedUrl method
flashdesignory Jun 28, 2025
81925af
run format
flashdesignory Jun 28, 2025
e701c2d
check readyState
flashdesignory Jun 29, 2025
6f08e6f
run format
flashdesignory Jun 29, 2025
8cd8ccb
add default tag
flashdesignory Jul 14, 2025
0764667
Merge branch 'main' into core/config
flashdesignory Jul 14, 2025
504ccc4
fix default suite in json
flashdesignory Jul 14, 2025
b95c47e
import dataProvider in developer-mode
flashdesignory Jul 14, 2025
af9d5a4
dynamic import
flashdesignory Jul 14, 2025
7a67c8c
Merge branch 'main' into core/config
flashdesignory Jul 15, 2025
368e9b7
run format
flashdesignory Jul 15, 2025
c6075c1
use disallowed domains
flashdesignory Jul 15, 2025
8f7c95b
remove emojis
flashdesignory Jul 16, 2025
113116f
throw error if suite entry is invalid
flashdesignory Jul 16, 2025
890fcb4
validate json url
flashdesignory Jul 16, 2025
289114f
check window location instead of config location
flashdesignory Jul 16, 2025
1d8f45e
add variation
flashdesignory Jul 16, 2025
2e5f751
check variations of domains
flashdesignory Jul 18, 2025
b59adc0
run format
flashdesignory Jul 18, 2025
9a37879
rename dataProvider to benchmarkConfigurator
flashdesignory Jul 18, 2025
7e235e5
Merge branch 'main' into core/config
flashdesignory Aug 6, 2025
08af823
re-add dart workload
flashdesignory Aug 6, 2025
bcdbe1e
use private vars
flashdesignory Aug 6, 2025
eb8c8fa
rename tests.mjs file
flashdesignory Aug 6, 2025
52d10bf
remove json extension check
flashdesignory Aug 20, 2025
ad81b73
move multiple declarations into new lines
flashdesignory Aug 20, 2025
c010903
add _reportError method
flashdesignory Aug 20, 2025
530759c
remove default tag from config.json
flashdesignory Aug 20, 2025
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
13 changes: 13 additions & 0 deletions resources/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"suites": [
{
"name": "NewsSite-PostMessage",
"url": "resources/newssite/news-next/dist/index.html",
"tags": ["default", "newssite", "language"],
"type": "remote",
"config": {
"name": "default"
}
}
]
}
151 changes: 151 additions & 0 deletions resources/data-provider.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// example url for local testing:
// http://localhost:8080/?config=http://localhost:8080/resources/config.json
import { defaultSuites } from "./tests.mjs";
import { params } from "./shared/params.mjs";

const DEFAULT_TAGS = ["all", "default", "experimental"];
const DISALLOWED_DOMAINS = ["browserbench.org", "www.browserbench.org"];
export class DataProvider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataProvider sounds very generic. How about ConfigurationFetcher?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does a little more than just fetching a config, how about BenchmarkConfigurator or SuitesManager ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, either name is better than DataProvider.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to BenchmarkConfigurator

_tags = new Set(DEFAULT_TAGS);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just use private variables?

_suites = [];

get tags() {
return this._tags;
}

get suites() {
return this._suites;
}

/**
* Checks if a given string is a valid URL, supporting both absolute and relative paths.
*
* This function attempts to construct a URL object. For relative paths, it uses
* a dummy base URL to allow the URL constructor to parse them successfully.
*
* @param {string} url The URL string to validate.
* @returns {boolean} True if the URL is valid (absolute or relative), false otherwise.
*/
_isValidUrl(url) {
if (typeof url !== "string" || url.length === 0)
return false;

try {
new URL(url, "http://www.example.com");
return true;
} catch (error) {
return false;
}
}

_freezeSuites() {
Object.freeze(this._suites);
this._suites.forEach((suite) => {
if (!suite.tags)
suite.tags = [];
if (suite.url.startsWith("experimental/"))
suite.tags.unshift("all", "experimental");
else
suite.tags.unshift("all");
suite.enabled = suite.tags.includes("default");
Object.freeze(suite.tags);
Object.freeze(suite.steps);
});
}

_freezeTags() {
Object.freeze(this._tags);
}

async init() {
if (params.config) {
try {
const benchmarkUrl = new URL(window.location);
// Don't fetch if the URL is from DISALLOWED_DOMAINS
if (DISALLOWED_DOMAINS.includes(benchmarkUrl.hostname)) {
console.warn("Configuration fetch not allowed. Loading default suites.");
this._loadDefaultSuites();
return;
}

const response = await fetch(params.config);
// Validate that the network request was successful
if (!response.ok)
throw new Error(`Could not fetch config: ${response.status}`);

const config = await response.json();
// Validate the structure of the fetched config object
if (!config || !Array.isArray(config.suites))
throw new Error("Could not find a valid config structure!");

config.suites.flatMap((suite) => suite.tags || []).forEach((tag) => this._tags.add(tag));
config.suites.forEach((suite) => {
// Validate each suite object before processing
if (suite && suite.url && this._isValidUrl(suite.url))
this._suites.push(suite);
else
throw new Error("Invalid suite data");
});
} catch (error) {
console.warn(`Error loading custom configuration: ${error.message}. Loading default suites.`);
this._loadDefaultSuites();
}
} else {
this._loadDefaultSuites();
}

this._freezeTags();
this._freezeSuites();
}

_loadDefaultSuites() {
defaultSuites.flatMap((suite) => suite.tags).forEach((tag) => this._tags.add(tag));
defaultSuites.forEach((suite) => this._suites.push(suite));
}

enableSuites(names, tags) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a comment to explain what this function does regarding the 2 parameters would be good.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frankly I'd split the function in 2 functions: enableSuitesByNames and enableSuitesByTags...
(and have the default set in init)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the logic that we currently use, I just reformatted, but tried to copy / paste as much of the existing logic as possible. This function comes from the current Suites.enable (https://github.com/WebKit/Speedometer/blob/main/resources/tests.mjs#L15).

I'm trying to avoid to introduce more changes and rather would tackle that in a follow up.

I do agree though that this is an area that can get cleaned up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay; this can be made simpler to read for sure...

if (names?.length) {
const lowerCaseNames = names.map((each) => each.toLowerCase());
this._suites.forEach((suite) => {
suite.enabled = lowerCaseNames.includes(suite.name.toLowerCase());
});
} else if (tags?.length) {
tags.forEach((tag) => {
if (!this._tags.has(tag))
console.error(`Unknown Suites tag: "${tag}"`);
});
const tagsSet = new Set(tags);
this._suites.forEach((suite) => {
suite.enabled = suite.tags.some((tag) => tagsSet.has(tag));
});
} else {
console.warn("Neither names nor tags provided. Enabling all default suites.");
this._suites.forEach((suite) => {
suite.enabled = suite.tags.includes("default");
});
}
if (this._suites.some((suite) => suite.enabled))
return;
let message, debugInfo;
if (names?.length) {
message = `Suites "${names}" does not match any Suite. No tests to run.`;
debugInfo = {
providedNames: names,
validNames: this._suites.map((each) => each.name),
};
} else if (tags?.length) {
message = `Tags "${tags}" does not match any Suite. No tests to run.`;
debugInfo = {
providedTags: tags,
validTags: Array.from(this._tags),
};
}
alert(message);
console.error(message, debugInfo);
}
}

const dataProvider = new DataProvider();
await dataProvider.init();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently there's a UX problem, because we don't call prepareUI before this await ends. As a result the button start test is present but doesn't react until the config file is loaded.

What do you think of calling this init function in the constructor for MainBenchmarkClient? Or even the MainBenchmarkClient constructor could do an async import for the data-provider.mjs file instead of getting the dataProvider object as a parameter.
(frankly all these solutions would work equally for me).

But that's not all !
Indeed prepareUI currently depends on the dataProvider information being fetched. However it could be split in half:

  • part 1 doesn't depend on the dataProvider information
  • part 2 depends on the dataProvider information

Therefore, the promise you get from calling the init function or from the async import could be used.

That could look like something like that:

// Note: maybe the asynchronous stuff can be moved to an async init function?
class MainBenchmarkClient {
  constructor() {
        this._dataProviderPromise = import("./data-provider.mjs");
        this.prepareUI();
        this._showSection(window.location.hash);
        this._dataProviderPromise.then(() => { window.dispatchEvent(new Event("SpeedometerReady")); });
  }

  async _startBenchmark() {
    const dataProvider = await this._dataProviderPromise;
    ... Do everything needed with dataProvider
  }

  async prepareUI() {
    // part 1 that doesn't depend on data provider
  
    const dataProvider = await this._dataProviderPromise;
    // part 2 that depends on dataProvider
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that makes sense - thanks for the suggestion.
I'll make the changes 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@julienw - implemented. Hope you still have time to review 😄


export { dataProvider };
34 changes: 18 additions & 16 deletions resources/developer-mode.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Suites, Tags } from "./tests.mjs";
import { params, LAYOUT_MODES } from "./shared/params.mjs";
import { dataProvider } from "./data-provider.mjs";

const { suites, tags } = dataProvider;

export function createDeveloperModeContainer() {
const container = document.createElement("div");
Expand Down Expand Up @@ -145,14 +147,14 @@ function createUIForSuites() {
control.className = "suites";
const checkboxes = [];
const setSuiteEnabled = (suiteIndex, enabled) => {
Suites[suiteIndex].enabled = enabled;
suites[suiteIndex].enabled = enabled;
checkboxes[suiteIndex].checked = enabled;
};

control.appendChild(createSuitesGlobalSelectButtons(setSuiteEnabled));

const ol = document.createElement("ol");
for (const suite of Suites) {
for (const suite of suites) {
const li = document.createElement("li");
const checkbox = document.createElement("input");
checkbox.id = suite.name;
Expand All @@ -169,8 +171,8 @@ function createUIForSuites() {
li.appendChild(label);
label.onclick = (event) => {
if (event?.ctrlKey || event?.metaKey) {
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++) {
if (Suites[suiteIndex] !== suite)
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
if (suites[suiteIndex] !== suite)
setSuiteEnabled(suiteIndex, false);
else
setSuiteEnabled(suiteIndex, true);
Expand All @@ -193,7 +195,7 @@ function createSuitesGlobalSelectButtons(setSuiteEnabled) {
button.className = "select-all";
button.textContent = "Select all";
button.onclick = () => {
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++)
setSuiteEnabled(suiteIndex, true);

updateURL();
Expand All @@ -204,7 +206,7 @@ function createSuitesGlobalSelectButtons(setSuiteEnabled) {
button.textContent = "Unselect all";
button.className = "unselect-all";
button.onclick = () => {
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++)
setSuiteEnabled(suiteIndex, false);

updateURL();
Expand All @@ -214,16 +216,16 @@ function createSuitesGlobalSelectButtons(setSuiteEnabled) {
}

function createSuitesTagsButton(setSuiteEnabled) {
let tags = document.createElement("div");
let buttons = tags.appendChild(document.createElement("div"));
let container = document.createElement("div");
let buttons = container.appendChild(document.createElement("div"));
buttons.className = "button-bar";
let i = 0;
const kTagsPerLine = 3;
for (const tag of Tags) {
for (const tag of tags) {
if (tag === "all")
continue;
if (!(i % kTagsPerLine)) {
buttons = tags.appendChild(document.createElement("div"));
buttons = container.appendChild(document.createElement("div"));
buttons.className = "button-bar";
}
i++;
Expand All @@ -235,8 +237,8 @@ function createSuitesTagsButton(setSuiteEnabled) {
const extendSelection = event?.shiftKey;
const invertSelection = event?.ctrlKey || event?.metaKey;
const selectedTag = event.target.dataTag;
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++) {
let enabled = Suites[suiteIndex].tags.includes(selectedTag);
for (let suiteIndex = 0; suiteIndex < suites.length; suiteIndex++) {
let enabled = suites[suiteIndex].tags.includes(selectedTag);
if (invertSelection)
enabled = !enabled;
if (extendSelection && !enabled)
Expand All @@ -247,7 +249,7 @@ function createSuitesTagsButton(setSuiteEnabled) {
};
buttons.appendChild(button);
}
return tags;
return container;
}

function createUIForRun() {
Expand Down Expand Up @@ -275,13 +277,13 @@ function updateParamsSuitesAndTags() {

// If less than all suites are selected then change the URL "Suites" GET parameter
// to comma separate only the selected
const selectedSuites = Suites.filter((suite) => suite.enabled);
const selectedSuites = suites.filter((suite) => suite.enabled);
if (!selectedSuites.length)
return;

// Try finding common tags that would result in the current suite selection.
let commonTags = new Set(selectedSuites[0].tags);
for (const suite of Suites) {
for (const suite of suites) {
if (suite.enabled)
commonTags = new Set(suite.tags.filter((tag) => commonTags.has(tag)));
else
Expand Down
45 changes: 32 additions & 13 deletions resources/main.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { BenchmarkRunner } from "./benchmark-runner.mjs";
import * as Statistics from "./statistics.mjs";
import { Suites } from "./tests.mjs";
import { renderMetricView } from "./metric-ui.mjs";
import { defaultParams, params } from "./shared/params.mjs";
import { createDeveloperModeContainer } from "./developer-mode.mjs";
Expand All @@ -19,11 +18,17 @@ class MainBenchmarkClient {
_metrics = Object.create(null);
_steppingPromise = null;
_steppingResolver = null;
_dataProviderPromise = null;

constructor() {
window.addEventListener("DOMContentLoaded", () => this.prepareUI());
this._dataProviderPromise = import("./data-provider.mjs");
this.prepareUI();
this.evaluateParams();
this._showSection(window.location.hash);
window.dispatchEvent(new Event("SpeedometerReady"));

this._dataProviderPromise.then(() => {
window.dispatchEvent(new Event("SpeedometerReady"));
});
}

start() {
Expand Down Expand Up @@ -62,20 +67,23 @@ class MainBenchmarkClient {
return this._steppingResolver !== null;
}

_startBenchmark() {
async _startBenchmark() {
if (this._isRunning)
return false;

const enabledSuites = Suites.filter((suite) => suite.enabled);
const { dataProvider } = await this._dataProviderPromise;

const enabledSuites = dataProvider.suites.filter((suite) => suite.enabled);
const totalSuitesCount = enabledSuites.length;

if (totalSuitesCount === 0) {
const message = `No suites selected - "${params.suites}" does not exist.`;
alert(message);
console.error(
message,
params.suites,
"\nValid values:",
Suites.map((each) => each.name)
dataProvider.suites.map((each) => each.name)
);
return false;
}
Expand All @@ -97,7 +105,7 @@ class MainBenchmarkClient {
this.stepCount = params.iterationCount * totalSuitesCount;
this._progressCompleted.max = this.stepCount;
this.suitesCount = enabledSuites.length;
const runner = new BenchmarkRunner(Suites, this);
const runner = new BenchmarkRunner(dataProvider.suites, this);
runner.runMultipleIterations(params.iterationCount);
return true;
}
Expand Down Expand Up @@ -336,12 +344,16 @@ class MainBenchmarkClient {
document.querySelectorAll(".start-tests-button").forEach((button) => {
button.onclick = this._startBenchmarkHandler.bind(this);
});
}

async evaluateParams() {
const { dataProvider } = await this._dataProviderPromise;

if (params.suites.length > 0 || params.tags.length > 0)
Suites.enable(params.suites, params.tags);
dataProvider.enableSuites(params.suites, params.tags);

if (params.developerMode) {
this._developerModeContainer = createDeveloperModeContainer(Suites);
this._developerModeContainer = createDeveloperModeContainer();
document.body.append(this._developerModeContainer);
}

Expand Down Expand Up @@ -464,8 +476,15 @@ class MainBenchmarkClient {
}
}

const rootStyle = document.documentElement.style;
rootStyle.setProperty("--viewport-width", `${params.viewport.width}px`);
rootStyle.setProperty("--viewport-height", `${params.viewport.height}px`);
function init() {
const rootStyle = document.documentElement.style;
rootStyle.setProperty("--viewport-width", `${params.viewport.width}px`);
rootStyle.setProperty("--viewport-height", `${params.viewport.height}px`);

globalThis.benchmarkClient = new MainBenchmarkClient();
}

globalThis.benchmarkClient = new MainBenchmarkClient();
if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", init);
else
init();
2 changes: 1 addition & 1 deletion resources/newssite/news-next/dist/404.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link rel="preload" href="./_next/static/css/a0dca1379a01e5cf.css" as="style"/><link rel="stylesheet" href="./_next/static/css/a0dca1379a01e5cf.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="./_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="./_next/static/chunks/webpack-e50e9853db18b759.js" defer=""></script><script src="./_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script><script src="./_next/static/chunks/main-2ba37e62325cc71b.js" defer=""></script><script src="./_next/static/chunks/pages/_app-77983e68be50f72a.js" defer=""></script><script src="./_next/static/chunks/pages/_error-54de1933a164a1ff.js" defer=""></script><script src="./_next/static/YM7vvwiEXAPUyTM_zGLyL/_buildManifest.js" defer=""></script><script src="./_next/static/YM7vvwiEXAPUyTM_zGLyL/_ssgManifest.js" defer=""></script></head><body><div id="__next"></div><div id="settings-container"></div><div id="notifications-container"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"YM7vvwiEXAPUyTM_zGLyL","assetPrefix":".","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link rel="preload" href="./_next/static/css/a0dca1379a01e5cf.css" as="style"/><link rel="stylesheet" href="./_next/static/css/a0dca1379a01e5cf.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="./_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="./_next/static/chunks/webpack-e50e9853db18b759.js" defer=""></script><script src="./_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script><script src="./_next/static/chunks/main-2ba37e62325cc71b.js" defer=""></script><script src="./_next/static/chunks/pages/_app-77983e68be50f72a.js" defer=""></script><script src="./_next/static/chunks/pages/_error-54de1933a164a1ff.js" defer=""></script><script src="./_next/static/tuwdCnX7HYK_fwpI0QvDm/_buildManifest.js" defer=""></script><script src="./_next/static/tuwdCnX7HYK_fwpI0QvDm/_ssgManifest.js" defer=""></script></head><body><div id="__next"></div><div id="settings-container"></div><div id="notifications-container"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"tuwdCnX7HYK_fwpI0QvDm","assetPrefix":".","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading