Skip to content

Commit

Permalink
Add shared worker support
Browse files Browse the repository at this point in the history
  • Loading branch information
beaufortfrancois committed Feb 5, 2024
1 parent ce757c8 commit 223ea6f
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 25 deletions.
5 changes: 3 additions & 2 deletions docs/intro/developing.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,16 @@ You can use this to preview how your test plan will appear.
You can view different suites (webgpu, unittests, stress, etc.) or different subtrees of
the test suite.

- `http://localhost:8080/standalone/` (defaults to `?runnow=0&worker=0&debug=0&q=webgpu:*`)
- `http://localhost:8080/standalone/` (defaults to `?runnow=0&debug=0&debug=0&q=webgpu:*`)
- `http://localhost:8080/standalone/?q=unittests:*`
- `http://localhost:8080/standalone/?q=unittests:basic:*`

The following url parameters change how the harness runs:

- `runnow=1` runs all matching tests on page load.
- `debug=1` enables verbose debug logging from tests.
- `worker=1` runs the tests on a Web Worker instead of the main thread.
- `worker=dedicated` runs the tests on a Web Worker instead of the main thread.
- `worker=shared` runs the tests on a Shared 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
2 changes: 1 addition & 1 deletion src/common/internal/query/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export function parseExpectationsForTestQuery(
assert(
expectationURL.pathname === wptURL.pathname,
`Invalid expectation path ${expectationURL.pathname}
Expectation should be of the form path/to/cts.https.html?worker=0&q=suite:test_path:test_name:foo=1;bar=2;...
Expectation should be of the form path/to/cts.https.html?debug=0&q=suite:test_path:test_name:foo=1;bar=2;...
`
);

Expand Down
15 changes: 12 additions & 3 deletions src/common/runtime/helper/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ export function optionString(
* The possible options for the tests.
*/
export interface CTSOptions {
worker: boolean;
worker?: 'dedicated' | 'shared' | 'service' | '';
debug: boolean;
compatibility: boolean;
unrollConstEvalLoops: boolean;
powerPreference?: GPUPowerPreference | '';
}

export const kDefaultCTSOptions: CTSOptions = {
worker: false,
worker: '',
debug: true,
compatibility: false,
unrollConstEvalLoops: false,
Expand All @@ -59,7 +59,16 @@ export type OptionsInfos<Type> = Record<keyof Type, OptionInfo>;
* Options to the CTS.
*/
export const kCTSOptionsInfo: OptionsInfos<CTSOptions> = {
worker: { description: 'run in a worker' },
worker: {
description: 'run in a worker',
parser: optionString,
selectValueDescriptions: [
{ value: '', description: 'no worker' },
{ value: 'dedicated', description: 'dedicated worker' },
{ value: 'shared', description: 'shared worker' },
{ value: 'service', description: 'service worker' },
],
},
debug: { description: 'show more info' },
compatibility: { description: 'run in compatibility mode' },
unrollConstEvalLoops: { description: 'unroll const eval loops in WGSL' },
Expand Down
18 changes: 15 additions & 3 deletions src/common/runtime/helper/test_worker-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import { assert } from '../../util/util.js';

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

// Should be DedicatedWorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom".
// Should be WorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom".
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
declare const self: any;

const loader = new DefaultTestFileLoader();

setBaseResourcePath('../../../resources');

self.onmessage = async (ev: MessageEvent) => {
async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) {
const query: string = ev.data.query;
const expectations: TestQueryWithExpectation[] = ev.data.expectations;
const ctsOptions: CTSOptions = ev.data.ctsOptions;
Expand All @@ -44,5 +44,17 @@ self.onmessage = async (ev: MessageEvent) => {
const [rec, result] = log.record(testcase.query.toString());
await testcase.run(rec, expectations);

self.postMessage({ query, result });
this.postMessage({ query, result });
}

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

self.onconnect = (event : MessageEvent) => {
const port = event.ports[0];

port.onmessage = (ev: MessageEvent) => {
reportTestResults.call(port, ev);
};
};
98 changes: 97 additions & 1 deletion src/common/runtime/helper/test_worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class TestWorker {
private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();

constructor(ctsOptions?: CTSOptions) {
this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker: true } };
this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker: 'dedicated' } };
const selfPath = import.meta.url;
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
const workerPath = selfPathDir + '/test_worker-worker.js';
Expand Down Expand Up @@ -47,3 +47,99 @@ export class TestWorker {
rec.injectResult(workerResult);
}
}

export class TestSharedWorker {
private readonly ctsOptions: CTSOptions;
private readonly sharedWorker: SharedWorker;
private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();

constructor(ctsOptions?: CTSOptions) {
this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker: 'shared' } };
const selfPath = import.meta.url;
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
const sharedWorkerPath = selfPathDir + '/test_worker-worker.js';
this.sharedWorker = new SharedWorker(sharedWorkerPath, { type: 'module' });
this.sharedWorker.port.start();
this.sharedWorker.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).
};
}

async run(
rec: TestCaseRecorder,
query: string,
expectations: TestQueryWithExpectation[] = []
): Promise<void> {
this.sharedWorker.port.postMessage({
query,
expectations,
ctsOptions: this.ctsOptions,
});
const sharedWorkerResult = await new Promise<LiveTestCaseResult>(resolve => {
this.resolvers.set(query, resolve);
});
rec.injectResult(sharedWorkerResult);
}
}

export class TestServiceWorker {
private readonly ctsOptions: CTSOptions;
private readonly serviceWorker: ServiceWorker;
private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();

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

async ready() {
const selfPath = import.meta.url;
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
const serviceWorkerPath = selfPathDir + '/test_worker-worker.js';
const registration = await navigator.serviceWorker.register(serviceWorkerPath,{
type: 'module',
scope: '/',
});
await navigator.serviceWorker.ready;
this.serviceWorker = registration.active;

navigator.serviceWorker.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).
};
}

async run(
rec: TestCaseRecorder,
query: string,
expectations: TestQueryWithExpectation[] = []
): Promise<void> {
this.serviceWorker.postMessage({
query,
expectations,
ctsOptions: this.ctsOptions,
});
const serviceWorkerResult = await new Promise<LiveTestCaseResult>(resolve => {
this.resolvers.set(query, resolve);
});
rec.injectResult(serviceWorkerResult);
}
}
11 changes: 9 additions & 2 deletions 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 { TestWorker } from './helper/test_worker.js';
import { TestWorker, TestServiceWorker, TestSharedWorker } from './helper/test_worker.js';

const rootQuerySpec = 'webgpu:*';
let promptBeforeReload = false;
Expand Down Expand Up @@ -56,7 +56,9 @@ const logger = new Logger();

setBaseResourcePath('../out/resources');

const worker = options.worker ? new TestWorker(options) : undefined;
const worker = options.worker === 'dedicated' ? new TestWorker(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 @@ -170,6 +172,11 @@ function makeCaseHTML(t: TestTreeLeaf): VisualizedSubtree {
caseResult = res;
if (worker) {
await worker.run(rec, name);
} else if (sharedWorker) {
await sharedWorker.run(rec, name);
} else if (serviceWorker) {
await serviceWorker.ready();
await serviceWorker.run(rec, name);
} else {
await t.run(rec);
}
Expand Down
2 changes: 2 additions & 0 deletions src/common/tools/dev_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ app.get('/out/**/*.js', async (req, res, next) => {
const tsUrl = jsUrl.replace(/\.js$/, '.ts');
if (compileCache.has(tsUrl)) {
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Service-Worker-Allowed', '/');
res.send(compileCache.get(tsUrl));
return;
}
Expand All @@ -166,6 +167,7 @@ app.get('/out/**/*.js', async (req, res, next) => {
compileCache.set(tsUrl, result.code);

res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Service-Worker-Allowed', '/');
res.send(result.code);
} else {
throw new Error(`Failed compile ${tsUrl}.`);
Expand Down
12 changes: 6 additions & 6 deletions src/common/tools/gen_wpt_cts_html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ where arguments.txt is a file containing a list of arguments prefixes to both ge
in the expectations. The entire variant list generation runs *once per prefix*, so this
multiplies the size of the variant list.
?worker=0&q=
?worker=1&q=
?debug=0&q=
?debug=1&q=
and myexpectations.txt is a file containing a list of WPT paths to suppress, e.g.:
path/to/cts.https.html?worker=0&q=webgpu:a/foo:bar={"x":1}
path/to/cts.https.html?worker=1&q=webgpu:a/foo:bar={"x":1}
path/to/cts.https.html?debug=0&q=webgpu:a/foo:bar={"x":1}
path/to/cts.https.html?debug=1&q=webgpu:a/foo:bar={"x":1}
path/to/cts.https.html?worker=1&q=webgpu:a/foo:bar={"x":3}
path/to/cts.https.html?debug=1&q=webgpu:a/foo:bar={"x":3}
`);
process.exit(rc);
}
Expand Down Expand Up @@ -224,7 +224,7 @@ ${queryString}`
}

lines.push({
urlQueryString: prefix + query.toString(), // "?worker=0&q=..."
urlQueryString: prefix + query.toString(), // "?debug=0&q=..."
comment: useChunking ? `estimated: ${subtreeCounts?.totalTimeMS.toFixed(3)} ms` : undefined,
});

Expand Down
46 changes: 41 additions & 5 deletions src/webgpu/web_platform/worker/worker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export const description = `
Tests WebGPU is available in a worker.
Tests WebGPU is available in a worker and a shared worker.
Note: The CTS test can be run in a worker by passing in worker=1 as
a query parameter. This test is specifically to check that WebGPU
is available in a worker.
Note: The CTS test can be run respectively in a worker and a shared worker by
passing in worker=dedicated and worker=shared as a query parameter. These tests
are specifically to check that WebGPU is available in a worker and a shared worker.
`;

import { Fixture } from '../../../common/framework/fixture.js';
Expand All @@ -17,7 +17,7 @@ function isNode(): boolean {
}

g.test('worker')
.desc(`test WebGPU is available in DedicatedWorkers and check for basic functionality`)
.desc(`test WebGPU is available in dedicated workers and check for basic functionality`)
.fn(async t => {
if (isNode()) {
t.skip('node does not support 100% compatible workers');
Expand All @@ -33,3 +33,39 @@ g.test('worker')
const result = await launchWorker();
assert(result.error === undefined, `should be no error from worker but was: ${result.error}`);
});

g.test('shared_worker')
.desc(`test WebGPU is available in shared workers and check for basic functionality`)
.fn(async t => {
if (isNode()) {
t.skip('node does not support 100% compatible shared workers');
return;
}
// Note: we load worker_launcher dynamically because ts-node support
// is using commonjs which doesn't support import.meta. Further,
// we need to put the url in a string add pass the string to import
// otherwise typescript tries to parse the file which again, fails.
// worker_launcher.js is excluded in node.tsconfig.json.
const url = './worker_launcher.js';
const { launchSharedWorker } = await import(url);
const result = await launchSharedWorker();
assert(result.error === undefined, `should be no error from shared worker but was: ${result.error}`);
});

g.test('service_worker')
.desc(`test WebGPU is available in service workers and check for basic functionality`)
.fn(async t => {
if (isNode()) {
t.skip('node does not support 100% compatible service workers');
return;
}
// Note: we load worker_launcher dynamically because ts-node support
// is using commonjs which doesn't support import.meta. Further,
// we need to put the url in a string add pass the string to import
// otherwise typescript tries to parse the file which again, fails.
// worker_launcher.js is excluded in node.tsconfig.json.
const url = './worker_launcher.js';
const { launchServiceWorker } = await import(url);
const result = await launchServiceWorker();
assert(result.error === undefined, `should be no error from service worker but was: ${result.error}`);
});
20 changes: 18 additions & 2 deletions src/webgpu/web_platform/worker/worker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { getGPU, setDefaultRequestAdapterOptions } from '../../../common/util/navigator_gpu.js';
import { assert, objectEquals, iterRange } from '../../../common/util/util.js';

// Should be WorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom".
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
declare const self: any;

async function basicTest() {
const adapter = await getGPU(null).requestAdapter();
assert(adapter !== null, 'Failed to get adapter.');
Expand Down Expand Up @@ -68,7 +72,7 @@ async function basicTest() {
device.destroy();
}

self.onmessage = async (ev: MessageEvent) => {
async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) {
const defaultRequestAdapterOptions: GPURequestAdapterOptions =
ev.data.defaultRequestAdapterOptions;
setDefaultRequestAdapterOptions(defaultRequestAdapterOptions);
Expand All @@ -79,5 +83,17 @@ self.onmessage = async (ev: MessageEvent) => {
} catch (err: unknown) {
error = (err as Error).toString();
}
self.postMessage({ error });
this.postMessage({ error });
}

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

self.onconnect = (event: MessageEvent) => {
const port = event.ports[0];

port.onmessage = async (ev: MessageEvent) => {
reportTestResults.call(port, ev);
};
};
Loading

0 comments on commit 223ea6f

Please sign in to comment.