Bug 1299271 - Service worker performance test r=asuth,sparky

These are a first-cut of service worker performance tests, just running as
vanilla mochitests for now.  Bug 1832059 tracks the integration work necessary.

Differential Revision: https://phabricator.services.mozilla.com/D177427
This commit is contained in:
Joshua Marshall 2023-11-28 16:33:38 +00:00
parent e07d89b90c
commit b831405fb6
14 changed files with 494 additions and 1 deletions

View file

@ -114,6 +114,7 @@ FINAL_LIBRARY = "xul"
MOCHITEST_MANIFESTS += [
"test/mochitest-dFPI.ini",
"test/mochitest.toml",
"test/performance/perftest.toml",
]
MOCHITEST_CHROME_MANIFESTS += [

View file

@ -0,0 +1 @@
intercepted

View file

@ -0,0 +1,14 @@
[DEFAULT]
support-files = [
"intercepted.txt",
"perfutils.js",
"sw_cacher.js",
"sw_empty.js",
"sw_intercept_target.js",
"target.txt",
"time_fetch.html",
]
["test_caching.html"]
["test_fetch.html"]
["test_registration.html"]

View file

@ -0,0 +1,46 @@
"use strict";
/**
* Given a map from test names to arrays of results, report perfherder metrics
* and log full results.
*/
function reportMetrics(journal) {
let metrics = {};
let text = "\nResults (ms)\n";
const names = Object.keys(journal);
const prefixLen = 1 + Math.max(...names.map(str => str.length));
for (const name in journal) {
const med = median(journal[name]);
text += (name + ":").padEnd(prefixLen, " ") + stringify(journal[name]);
text += " median " + med + "\n";
metrics[name] = med;
}
dump(text);
info("perfMetrics", JSON.stringify(metrics));
}
function median(arr) {
arr = [...arr].sort((a, b) => a - b);
const mid = Math.floor(arr.length / 2);
if (arr.length % 2) {
return arr[mid];
}
return (arr[mid - 1] + arr[mid]) / 2;
}
function stringify(arr) {
function pad(num) {
let s = num.toString().padStart(5, " ");
if (s[0] != " ") {
s = " " + s;
}
return s;
}
return arr.reduce((acc, elem) => acc + pad(elem), "");
}

View file

@ -0,0 +1,18 @@
"use strict";
oninstall = function (event) {
event.waitUntil(
caches.open("perftest").then(function (cache) {
return cache.put("cached.txt", new Response("cached.txt"));
})
);
};
onfetch = function (event) {
if (event.request.url.endsWith("/cached.txt")) {
var p = caches.match("cached.txt", { cacheName: "perftest" });
event.respondWith(p);
} else if (event.request.url.endsWith("/uncached.txt")) {
event.respondWith(new Response("uncached.txt"));
}
};

View file

@ -0,0 +1,7 @@
"use strict";
onfetch = function (event) {
if (event.request.url.indexOf("target.txt") != -1) {
event.respondWith(fetch("intercepted.txt"));
}
};

View file

@ -0,0 +1 @@
target

View file

@ -0,0 +1,89 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Service worker performance test: caching</title>
</head>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="../utils.js"></script>
<script src="perfutils.js"></script>
<script>
"use strict";
const NO_CACHE = "No cache";
const CACHED = "Cached";
const NO_CACHE_AGAIN = "No cache again";
var journal = {};
journal[NO_CACHE] = [];
journal[CACHED] = [];
journal[NO_CACHE_AGAIN] = [];
const ITERATIONS = 10;
var perfMetadata = {
owner: "DOM LWS",
name: "Service Worker Caching",
description: "Test service worker caching.",
options: {
default: {
perfherder: true,
perfherder_metrics: [
// Here, we can't use the constants defined above because perfherder
// grabs data from the parse tree.
{ name: "No cache", unit: "ms", shouldAlert: true },
{ name: "Cached", unit: "ms", shouldAlert: true },
{ name: "No cache again", unit: "ms", shouldAlert: true },
],
verbose: true,
manifest: "perftest.toml",
manifest_flavor: "plain",
},
},
};
add_task(async () => {
await SpecialPowers.pushPrefEnv({
set: [["dom.serviceWorkers.testing.enabled", true]]
});
});
function create_iframe(url) {
return new Promise(function(res) {
let iframe = document.createElement("iframe");
iframe.src = url;
iframe.onload = function() { res(iframe) }
document.body.appendChild(iframe);
});
}
async function time_fetch(journal, iframe, filename) {
for (let i = 0; i < ITERATIONS; i++) {
let result = await iframe.contentWindow.time_fetch(filename);
is(result.status, 200);
is(result.data, filename);
journal.push(result.elapsed_ms);
}
}
add_task(async () => {
let reg = await navigator.serviceWorker.register("sw_cacher.js");
await waitForState(reg.installing, "activated");
let iframe = await create_iframe("time_fetch.html");
await time_fetch(journal[NO_CACHE], iframe, "uncached.txt");
await time_fetch(journal[CACHED], iframe, "cached.txt");
await time_fetch(journal[NO_CACHE_AGAIN], iframe, "uncached.txt");
await reg.unregister();
});
add_task(() => {
reportMetrics(journal);
});
</script>
<body>
</body>
</html>

View file

@ -0,0 +1,168 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Service worker performance test: fetch</title>
</head>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="../utils.js"></script>
<script src="perfutils.js"></script>
<script>
"use strict";
const COLD_FETCH = "Cold fetch";
const UNDISTURBED_FETCH = "Undisturbed fetch";
const INTERCEPTED_FETCH = "Intercepted fetch";
const LIBERATED_FETCH = "Liberated fetch";
const UNDISTURBED_XHR = "Undisturbed XHR";
const INTERCEPTED_XHR = "Intercepted XHR";
const LIBERATED_XHR = "Liberated XHR";
var journal = {};
journal[COLD_FETCH] = [];
journal[UNDISTURBED_FETCH] = [];
journal[INTERCEPTED_FETCH] = [];
journal[LIBERATED_FETCH] = [];
journal[UNDISTURBED_XHR] = [];
journal[INTERCEPTED_XHR] = [];
journal[LIBERATED_XHR] = [];
const ITERATIONS = 10;
var perfMetadata = {
owner: "DOM LWS",
name: "Service Worker Fetch",
description: "Test cold and warm fetches.",
options: {
default: {
perfherder: true,
perfherder_metrics: [
// Here, we can't use the constants defined above because perfherder
// grabs data from the parse tree.
{ name: "Cold fetch", unit: "ms", shouldAlert: true },
{ name: "Undisturbed fetch", unit: "ms", shouldAlert: true },
{ name: "Intercepted fetch", unit: "ms", shouldAlert: true },
{ name: "Liberated fetch", unit: "ms", shouldAlert: true },
{ name: "Undisturbed XHR", unit: "ms", shouldAlert: true },
{ name: "Intercepted XHR", unit: "ms", shouldAlert: true },
{ name: "Liberated XHR", unit: "ms", shouldAlert: true },
],
verbose: true,
manifest: "perftest.toml",
manifest_flavor: "plain",
},
},
};
function create_iframe(url) {
return new Promise(function(res) {
let iframe = document.createElement("iframe");
iframe.src = url;
iframe.onload = function() { res(iframe) }
document.body.appendChild(iframe);
});
}
add_task(async () => {
await SpecialPowers.pushPrefEnv({
set: [["dom.serviceWorkers.testing.enabled", true]]
});
});
/**
* Time fetch from a fresh service worker.
*/
add_task(async () => {
for (let i = 0; i < ITERATIONS; i++) {
let reg = await navigator.serviceWorker.register("sw_intercept_target.js");
await waitForState(reg.installing, "activated");
let iframe = await create_iframe("time_fetch.html");
let result = await iframe.contentWindow.time_fetch("target.txt");
is(result.status, 200);
is(result.data, "intercepted\n");
journal[COLD_FETCH].push(result.elapsed_ms);
ok(document.body.removeChild(iframe), "Failed to remove child iframe");
await reg.unregister();
}
});
/**
* Time unintercepted fetch, intercepted fetch, then unintercepted
* fetch again.
*/
add_task(async () => {
let reg = await navigator.serviceWorker.register("sw_intercept_target.js");
await waitForState(reg.installing, "activated");
async function measure(journal, sw_enabled) {
await SpecialPowers.pushPrefEnv({
set: [["dom.serviceWorkers.enabled", sw_enabled]]
});
let iframe = await create_iframe("time_fetch.html");
for (let i = 0; i < ITERATIONS; i++) {
let result = await iframe.contentWindow.time_fetch("target.txt");
is(result.status, 200);
is(result.data, sw_enabled ? "intercepted\n" : "target\n");
journal.push(result.elapsed_ms);
}
ok(document.body.removeChild(iframe), "Failed to remove child iframe");
await SpecialPowers.popPrefEnv();
}
await measure(journal[UNDISTURBED_FETCH], false);
await measure(journal[INTERCEPTED_FETCH], true);
await measure(journal[LIBERATED_FETCH], false);
await reg.unregister();
});
/**
* Time unintercepted XHR, intercepted XHR, then unintercepted
* XHR again.
*/
add_task(async () => {
let reg = await navigator.serviceWorker.register("sw_intercept_target.js");
await waitForState(reg.installing, "activated");
async function measure(journal, sw_enabled) {
await SpecialPowers.pushPrefEnv({
set: [["dom.serviceWorkers.enabled", sw_enabled]]
});
let iframe = await create_iframe("time_fetch.html");
for (let i = 0; i < ITERATIONS; i++) {
let result = await iframe.contentWindow.time_xhr("target.txt");
is(result.status, 200);
is(result.data, sw_enabled ? "intercepted\n" : "target\n");
journal.push(result.elapsed_ms);
}
ok(document.body.removeChild(iframe), "Failed to remove child iframe");
await SpecialPowers.popPrefEnv();
}
await measure(journal[UNDISTURBED_XHR], false);
await measure(journal[INTERCEPTED_XHR], true);
await measure(journal[LIBERATED_XHR], false);
await reg.unregister();
});
add_task(() => {
reportMetrics(journal);
});
</script>
<body>
</body>
</html>

View file

@ -0,0 +1,89 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Service worker performance test: registration</title>
</head>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="../utils.js"></script>
<script src="perfutils.js"></script>
<script>
"use strict";
const REGISTRATION = "Registration";
const ACTIVATION = "Activation";
const UNREGISTRATION = "Unregistration";
var journal = [];
journal[REGISTRATION] = [];
journal[ACTIVATION] = [];
journal[UNREGISTRATION] = [];
const ITERATIONS = 10;
var perfMetadata = {
owner: "DOM LWS",
name: "Service Worker Registration",
description: "Test registration, activation, and unregistration.",
options: {
default: {
perfherder: true,
perfherder_metrics: [
// Here, we can't use the constants defined above because perfherder
// grabs data from the parse tree.
{ name: "Registration", unit: "ms", shouldAlert: true },
{ name: "Activation", unit: "ms", shouldAlert: true },
{ name: "Unregistration", unit: "ms", shouldAlert: true },
],
verbose: true,
manifest: "perftest.toml",
manifest_flavor: "plain",
},
},
};
function create_iframe(url) {
return new Promise(function(res) {
let iframe = document.createElement("iframe");
iframe.src = url;
iframe.onload = function() { res(iframe) }
document.body.appendChild(iframe);
});
}
add_task(async () => {
await SpecialPowers.pushPrefEnv({
set: [["dom.serviceWorkers.testing.enabled", true]]
});
async function measure() {
let begin_ts = performance.now();
let reg = await navigator.serviceWorker.register("sw_empty.js");
let reg_ts = performance.now();
await waitForState(reg.installing, "activated");
let act_ts = performance.now();
await reg.unregister();
let unreg_ts = performance.now();
journal[REGISTRATION].push(reg_ts - begin_ts);
journal[ACTIVATION].push(act_ts - reg_ts);
journal[UNREGISTRATION].push(unreg_ts - act_ts);
}
for (let i = 0; i < ITERATIONS; i++) {
await measure();
}
await SpecialPowers.popPrefEnv();
ok(true);
});
add_task(() => {
reportMetrics(journal);
});
</script>
<body>
</body>
</html>

View file

@ -0,0 +1,38 @@
<!DOCTYPE HTML>
<html>
<head>
<script>
"use strict";
async function time_fetch(url) {
let start = performance.now();
let res = await fetch(url);
let elapsed = performance.now() - start;
return {
elapsed_ms : elapsed,
status : res.status,
data : await res.text()
};
}
async function time_xhr(url) {
let xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
let start = performance.now();
xhr.send();
let elapsed = performance.now() - start;
return {
elapsed_ms : elapsed,
status : xhr.status,
data : xhr.responseText
}
}
</script>
</head>
<body>
</body>
</html>

View file

@ -151,6 +151,7 @@ class Mochitest(Layer):
# Bug 1858155 - Attempting to only use one test_path triggers a failure
# during test execution
args.test_paths = [str(test.name), str(test.name)]
args.keep_open = False
args.runByManifest = True
args.manifestFile = manifest
args.topobjdir = self.topobjdir
@ -183,7 +184,8 @@ class Mochitest(Layer):
status, log_processor = FunctionalTestRunner.test(
self.mach_cmd,
[str(test)],
self._parse_extra_args(self.get_arg("extra-args")),
self._parse_extra_args(self.get_arg("extra-args"))
+ ["--keep-open=False"],
)
if status is not None and status != 0:

View file

@ -118,6 +118,25 @@ domcount:
--browsertime-geckodriver ${MOZ_FETCHES_DIR}/geckodriver
--output $MOZ_FETCHES_DIR/../artifacts
service-worker:
description: Run service worker tests
treeherder:
symbol: perftest(linux-sw)
tier: 2
attributes:
batch: false
cron: false
run-on-projects: [autoland, mozilla-central]
run:
command: >-
mkdir -p $MOZ_FETCHES_DIR/../artifacts &&
cd $MOZ_FETCHES_DIR &&
python3 python/mozperftest/mozperftest/runner.py
dom/serviceworkers/test/performance/test_registration.html
--mochitest-binary ${MOZ_FETCHES_DIR}/firefox/firefox-bin
--flavor mochitest
--output $MOZ_FETCHES_DIR/../artifacts
http3:
description: Run HTTP/3 test
treeherder: