Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add service worker support #3419

Merged
merged 26 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fec22b7
Generate Web Worker script for every test file
kainino0x Feb 16, 2024
533afe4
Add service worker support
beaufortfrancois Feb 19, 2024
20444d0
Update wpt.ts
beaufortfrancois Feb 26, 2024
23f0a47
Run tests
beaufortfrancois Mar 4, 2024
675f6ad
Address kainino0x feedback
beaufortfrancois Mar 6, 2024
5d0e4c0
Address feedback | part 2
beaufortfrancois Mar 6, 2024
4ea5270
Address feedback | part 3
beaufortfrancois Mar 6, 2024
46683bf
Address feedback | part 4
beaufortfrancois Mar 7, 2024
b5b8538
Address feedback | part 5
beaufortfrancois Mar 7, 2024
0f173fa
Address feedback | part 6
beaufortfrancois Mar 7, 2024
1a6123a
Address feedback | part 7
beaufortfrancois Mar 7, 2024
e3b63ff
Address feedback | part 8
beaufortfrancois Mar 7, 2024
01750a3
Address feedback | part 9
beaufortfrancois Mar 7, 2024
8730e55
Apply suggestions from code review
kainino0x Mar 7, 2024
ffff9fd
use WorkerTestRunRequest in the postMessage/onmessage interface
kainino0x Mar 7, 2024
4d78304
Clean up resolvers map
kainino0x Mar 7, 2024
0571de7
Use express routing for .worker.js
kainino0x Mar 7, 2024
465dd64
Skip worker tests when run in a worker that can't support them
kainino0x Mar 7, 2024
d66ce41
Clean up all service workers on startup and shutdown
kainino0x Mar 8, 2024
01f8b22
Avoid reinitializing service workers for every single case
kainino0x Mar 8, 2024
d03d129
Merge branch 'main' into service-worker
kainino0x Mar 8, 2024
5eb75c3
lint fixes
kainino0x Mar 8, 2024
749ee9c
Merge branch 'main' into service-worker
kainino0x Mar 8, 2024
4c8178f
Catch errors in wrapTestGroupForWorker onMessage
kainino0x Mar 8, 2024
97e378e
Make sure the service worker has the correct URL
kainino0x Mar 8, 2024
53173a5
Merge branch 'main' into service-worker
kainino0x Mar 11, 2024
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
10 changes: 5 additions & 5 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ module.exports = function (grunt) {
cmd: 'node',
args: ['tools/gen_version'],
},
'generate-listings': {
'generate-listings-and-webworkers': {
cmd: 'node',
args: ['tools/gen_listings', 'gen/', ...kAllSuites.map(s => 'src/' + s)],
args: ['tools/gen_listings_and_webworkers', 'gen/', ...kAllSuites.map(s => 'src/' + s)],
},
validate: {
cmd: 'node',
Expand Down Expand Up @@ -159,14 +159,14 @@ module.exports = function (grunt) {
// Must run after generate-common and run:build-out.
files: [
{ expand: true, dest: 'out/', cwd: 'gen', src: 'common/internal/version.js' },
{ expand: true, dest: 'out/', cwd: 'gen', src: '*/listing.js' },
{ expand: true, dest: 'out/', cwd: 'gen', src: '*/**/*.js' },
],
},
'gen-to-out-wpt': {
// Must run after generate-common and run:build-out-wpt.
files: [
{ expand: true, dest: 'out-wpt/', cwd: 'gen', src: 'common/internal/version.js' },
{ expand: true, dest: 'out-wpt/', cwd: 'gen', src: 'webgpu/listing.js' },
{ expand: true, dest: 'out-wpt/', cwd: 'gen', src: 'webgpu/**/*.js' },
],
},
'htmlfiles-to-out': {
Expand Down Expand Up @@ -243,7 +243,7 @@ module.exports = function (grunt) {

grunt.registerTask('generate-common', 'Generate files into gen/ and src/', [
'run:generate-version',
'run:generate-listings',
'run:generate-listings-and-webworkers',
'run:generate-cache',
]);
grunt.registerTask('build-standalone', 'Build out/ (no checks; run after generate-common)', [
Expand Down
1 change: 1 addition & 0 deletions docs/intro/developing.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ The following url parameters change how the harness runs:
- `debug=1` enables verbose debug logging from tests.
- `worker=dedicated` runs the tests on a dedicated worker instead of the main thread.
- `worker=shared` runs the tests on a shared worker instead of the main thread.
- `worker=service` runs the tests on a service worker instead of the main thread.
- `power_preference=low-power` runs most tests passing `powerPreference: low-power` to `requestAdapter`
- `power_preference=high-performance` runs most tests passing `powerPreference: high-performance` to `requestAdapter`

Expand Down
5 changes: 4 additions & 1 deletion src/common/internal/query/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ function compareOneLevel(ordering: Ordering, aIsBig: boolean, bIsBig: boolean):
return Ordering.Unordered;
}

function comparePaths(a: readonly string[], b: readonly string[]): Ordering {
/**
* Compare two file paths, or file-local test paths, returning an Ordering between the two.
*/
export function comparePaths(a: readonly string[], b: readonly string[]): Ordering {
kainino0x marked this conversation as resolved.
Show resolved Hide resolved
const shorter = Math.min(a.length, b.length);

for (let i = 0; i < shorter; ++i) {
Expand Down
3 changes: 2 additions & 1 deletion src/common/runtime/helper/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function optionString(
* The possible options for the tests.
*/
export interface CTSOptions {
worker?: 'dedicated' | 'shared' | '';
worker?: 'dedicated' | 'shared' | 'service' | '';
debug: boolean;
compatibility: boolean;
forceFallbackAdapter: boolean;
Expand Down Expand Up @@ -68,6 +68,7 @@ export const kCTSOptionsInfo: OptionsInfos<CTSOptions> = {
{ value: '', description: 'no worker' },
{ value: 'dedicated', description: 'dedicated worker' },
{ value: 'shared', description: 'shared worker' },
{ value: 'service', description: 'service worker' },
],
},
debug: { description: 'show more info' },
Expand Down
21 changes: 3 additions & 18 deletions src/common/runtime/helper/test_worker-worker.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { setBaseResourcePath } from '../../framework/resources.js';
import { globalTestConfig } from '../../framework/test_config.js';
import { DefaultTestFileLoader } from '../../internal/file_loader.js';
import { Logger } from '../../internal/logging/logger.js';
import { parseQuery } from '../../internal/query/parseQuery.js';
import { TestQueryWithExpectation } from '../../internal/query/query.js';
import { setDefaultRequestAdapterOptions } from '../../util/navigator_gpu.js';
import { assert } from '../../util/util.js';

import { CTSOptions } from './options.js';
import { setupWorkerEnvironment } from './utils_worker.js';

// Should be WorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom".
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
Expand All @@ -22,20 +20,7 @@ async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) {
const expectations: TestQueryWithExpectation[] = ev.data.expectations;
const ctsOptions: CTSOptions = ev.data.ctsOptions;

const { debug, unrollConstEvalLoops, powerPreference, compatibility } = ctsOptions;
globalTestConfig.unrollConstEvalLoops = unrollConstEvalLoops;
globalTestConfig.compatibility = compatibility;

Logger.globalDebugMode = debug;
const log = new Logger();

if (powerPreference || compatibility) {
setDefaultRequestAdapterOptions({
...(powerPreference && { powerPreference }),
// MAINTENANCE_TODO: Change this to whatever the option ends up being
...(compatibility && { compatibilityMode: true }),
});
}
const log = setupWorkerEnvironment(ctsOptions);

const testcases = Array.from(await loader.loadCases(parseQuery(query)));
assert(testcases.length === 1, 'worker query resulted in != 1 cases');
Expand All @@ -48,7 +33,7 @@ async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) {
}

self.onmessage = (ev: MessageEvent) => {
void reportTestResults.call(self, ev);
void reportTestResults.call(ev.source || self, ev);
};

self.onconnect = (event: MessageEvent) => {
Expand Down
96 changes: 62 additions & 34 deletions src/common/runtime/helper/test_worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,39 @@ import { TestQueryWithExpectation } from '../../internal/query/query.js';

import { CTSOptions, kDefaultCTSOptions } from './options.js';

export class TestDedicatedWorker {
private readonly ctsOptions: CTSOptions;
class TestBaseWorker {
protected readonly ctsOptions: CTSOptions;
protected readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();

constructor(worker: CTSOptions['worker'], ctsOptions?: CTSOptions) {
this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker } };
}

onmessage(ev: MessageEvent) {
const query: string = ev.data.query;
const result: TransferredTestCaseResult = ev.data.result;
if (result.logs) {
for (const l of result.logs) {
Object.setPrototypeOf(l, LogMessageWithStack.prototype);
}
}
this.resolvers.get(query)!(result as LiveTestCaseResult);

// MAINTENANCE_TODO(kainino0x): update the Logger with this result (or don't have a logger and
// update the entire results JSON somehow at some point).
}
}

export class TestDedicatedWorker extends TestBaseWorker {
private readonly worker: Worker;
private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();

constructor(ctsOptions?: CTSOptions) {
this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker: 'dedicated' } };
super('dedicated', ctsOptions);
const selfPath = import.meta.url;
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
const workerPath = selfPathDir + '/test_worker-worker.js';
this.worker = new Worker(workerPath, { type: 'module' });
this.worker.onmessage = ev => {
const query: string = ev.data.query;
const result: TransferredTestCaseResult = ev.data.result;
if (result.logs) {
for (const l of result.logs) {
Object.setPrototypeOf(l, LogMessageWithStack.prototype);
}
}
this.resolvers.get(query)!(result as LiveTestCaseResult);

// MAINTENANCE_TODO(kainino0x): update the Logger with this result (or don't have a logger and
// update the entire results JSON somehow at some point).
};
this.worker.onmessage = ev => this.onmessage(ev);
}

async run(
Expand All @@ -50,32 +59,18 @@ export class TestDedicatedWorker {

export class TestWorker extends TestDedicatedWorker {}

export class TestSharedWorker {
private readonly ctsOptions: CTSOptions;
export class TestSharedWorker extends TestBaseWorker {
private readonly port: MessagePort;
private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();

constructor(ctsOptions?: CTSOptions) {
this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker: 'shared' } };
super('shared', ctsOptions);
const selfPath = import.meta.url;
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
const workerPath = selfPathDir + '/test_worker-worker.js';
const worker = new SharedWorker(workerPath, { type: 'module' });
this.port = worker.port;
this.port.start();
this.port.onmessage = ev => {
const query: string = ev.data.query;
const result: TransferredTestCaseResult = ev.data.result;
if (result.logs) {
for (const l of result.logs) {
Object.setPrototypeOf(l, LogMessageWithStack.prototype);
}
}
this.resolvers.get(query)!(result as LiveTestCaseResult);

// MAINTENANCE_TODO(kainino0x): update the Logger with this result (or don't have a logger and
// update the entire results JSON somehow at some point).
};
this.port.onmessage = ev => this.onmessage(ev);
}

async run(
Expand All @@ -94,3 +89,36 @@ export class TestSharedWorker {
rec.injectResult(workerResult);
}
}

export class TestServiceWorker extends TestBaseWorker {
constructor(ctsOptions?: CTSOptions) {
super('service', ctsOptions);
}

async run(
rec: TestCaseRecorder,
query: string,
expectations: TestQueryWithExpectation[] = []
): Promise<void> {
const [suite, name] = query.split(':', 2);
const fileName = name.split(',').join('/');
const serviceWorkerPath = `/out/${suite}/webworker/${fileName}.worker.js`;

const registration = await navigator.serviceWorker.register(serviceWorkerPath, {
type: 'module',
});
await registration.update();
kainino0x marked this conversation as resolved.
Show resolved Hide resolved
navigator.serviceWorker.onmessage = ev => this.onmessage(ev);

registration.active?.postMessage({
query,
expectations,
ctsOptions: this.ctsOptions,
});
const serviceWorkerResult = await new Promise<LiveTestCaseResult>(resolve => {
this.resolvers.set(query, resolve);
void registration.unregister();
});
rec.injectResult(serviceWorkerResult);
kainino0x marked this conversation as resolved.
Show resolved Hide resolved
}
}
24 changes: 24 additions & 0 deletions src/common/runtime/helper/utils_worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { globalTestConfig } from '../../framework/test_config.js';
import { Logger } from '../../internal/logging/logger.js';
import { setDefaultRequestAdapterOptions } from '../../util/navigator_gpu.js';

import { CTSOptions } from './options.js';

export function setupWorkerEnvironment(ctsOptions: CTSOptions): Logger {
const { debug, unrollConstEvalLoops, powerPreference, compatibility } = ctsOptions;
globalTestConfig.unrollConstEvalLoops = unrollConstEvalLoops;
globalTestConfig.compatibility = compatibility;

Logger.globalDebugMode = debug;
const log = new Logger();

if (powerPreference || compatibility) {
setDefaultRequestAdapterOptions({
...(powerPreference && { powerPreference }),
// MAINTENANCE_TODO: Change this to whatever the option ends up being
...(compatibility && { compatibilityMode: true }),
});
}

return log;
}
37 changes: 37 additions & 0 deletions src/common/runtime/helper/wrap_for_worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Fixture } from '../../framework/fixture';
import { comparePaths, comparePublicParamsPaths, Ordering } from '../../internal/query/compare.js';
import { parseQuery } from '../../internal/query/parseQuery.js';
import { TestQuerySingleCase, TestQueryWithExpectation } from '../../internal/query/query.js';
import { TestGroup } from '../../internal/test_group.js';
import { assert } from '../../util/util.js';

import { CTSOptions } from './options.js';
import { setupWorkerEnvironment } from './utils_worker.js';

export function wrapTestGroupForWorker(g: TestGroup<Fixture>) {
self.onmessage = async (ev: MessageEvent) => {
const query: string = ev.data.query;
const expectations: TestQueryWithExpectation[] = ev.data.expectations;
const ctsOptions: CTSOptions = ev.data.ctsOptions;

const log = setupWorkerEnvironment(ctsOptions);

const testQuery = parseQuery(query);
assert(testQuery instanceof TestQuerySingleCase);
let testcase = null;
for (const t of g.iterate()) {
if (comparePaths(t.testPath, testQuery.testPathParts) !== Ordering.Equal) {
continue;
}
for (const c of t.iterate(testQuery.params)) {
if (comparePublicParamsPaths(c.id.params, testQuery.params) === Ordering.Equal) {
testcase = c;
}
}
}
assert(!!testcase);
const [rec, result] = log.record(query);
await testcase.run(rec, testQuery, expectations);
ev.source?.postMessage({ query, result });
};
}
5 changes: 4 additions & 1 deletion src/common/runtime/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
OptionsInfos,
camelCaseToSnakeCase,
} from './helper/options.js';
import { TestDedicatedWorker, TestSharedWorker } from './helper/test_worker.js';
import { TestDedicatedWorker, TestSharedWorker, TestServiceWorker } from './helper/test_worker.js';

const rootQuerySpec = 'webgpu:*';
let promptBeforeReload = false;
Expand Down Expand Up @@ -66,6 +66,7 @@ setBaseResourcePath('../out/resources');
const dedicatedWorker =
options.worker === 'dedicated' ? new TestDedicatedWorker(options) : undefined;
const sharedWorker = options.worker === 'shared' ? new TestSharedWorker(options) : undefined;
const serviceWorker = options.worker === 'service' ? new TestServiceWorker(options) : undefined;

const autoCloseOnPass = document.getElementById('autoCloseOnPass') as HTMLInputElement;
const resultsVis = document.getElementById('resultsVis')!;
Expand Down Expand Up @@ -182,6 +183,8 @@ function makeCaseHTML(t: TestTreeLeaf): VisualizedSubtree {
await dedicatedWorker.run(rec, name);
} else if (sharedWorker) {
await sharedWorker.run(rec, name);
} else if (serviceWorker) {
await serviceWorker.run(rec, name);
} else {
await t.run(rec);
}
Expand Down
5 changes: 4 additions & 1 deletion src/common/runtime/wpt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { parseExpectationsForTestQuery, relativeQueryString } from '../internal/
import { assert } from '../util/util.js';

import { optionEnabled, optionString } from './helper/options.js';
import { TestDedicatedWorker, TestSharedWorker } from './helper/test_worker.js';
import { TestDedicatedWorker, TestServiceWorker, TestSharedWorker } from './helper/test_worker.js';

// testharness.js API (https://web-platform-tests.org/writing-tests/testharness-api.html)
declare interface WptTestObject {
Expand All @@ -34,6 +34,7 @@ void (async () => {
const workerString = optionString('worker');
const dedicatedWorker = workerString === 'dedicated' ? new TestDedicatedWorker() : undefined;
const sharedWorker = workerString === 'shared' ? new TestSharedWorker() : undefined;
const serviceWorker = workerString === 'service' ? new TestServiceWorker() : undefined;

globalTestConfig.unrollConstEvalLoops = optionEnabled('unroll_const_eval_loops');

Expand Down Expand Up @@ -68,6 +69,8 @@ void (async () => {
await dedicatedWorker.run(rec, name, expectations);
} else if (sharedWorker) {
await sharedWorker.run(rec, name, expectations);
} else if (serviceWorker) {
await serviceWorker.run(rec, name, expectations);
} else {
await testcase.run(rec, expectations);
}
Expand Down
6 changes: 4 additions & 2 deletions src/common/tools/dev_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,12 @@ app.get('/out/**/*.js', async (req, res, next) => {
return;
}

let absPath = path.join(srcDir, tsUrl);
// I'm not sure if this is the way I should handle it...
kainino0x marked this conversation as resolved.
Show resolved Hide resolved
const dir = jsUrl.endsWith('worker.js') ? path.resolve(srcDir, '../out') : srcDir;
let absPath = path.join(dir, tsUrl);
if (!fs.existsSync(absPath)) {
// The .ts file doesn't exist. Try .js file in case this is a .js/.d.ts pair.
absPath = path.join(srcDir, jsUrl);
absPath = path.join(dir, jsUrl);
}

try {
Expand Down
Loading