diff --git a/Gruntfile.js b/Gruntfile.js index c354249f8b69..eba38704f3ee 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -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', @@ -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': { @@ -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)', [ diff --git a/docs/intro/developing.md b/docs/intro/developing.md index d8869f985b31..23da946b1e0c 100644 --- a/docs/intro/developing.md +++ b/docs/intro/developing.md @@ -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` diff --git a/docs/terms.md b/docs/terms.md index 032639be577a..0dc6f0ca1701 100644 --- a/docs/terms.md +++ b/docs/terms.md @@ -111,7 +111,7 @@ Each Suite has one Listing File (`suite/listing.[tj]s`), containing a list of th in the suite. In `src/suite/listing.ts`, this is computed dynamically. -In `out/suite/listing.js`, the listing has been pre-baked (by `tools/gen_listings`). +In `out/suite/listing.js`, the listing has been pre-baked (by `tools/gen_listings_and_webworkers`). **Type:** Once `import`ed, `ListingFile` diff --git a/src/common/internal/query/compare.ts b/src/common/internal/query/compare.ts index a9419b87c196..f49833f5a2e0 100644 --- a/src/common/internal/query/compare.ts +++ b/src/common/internal/query/compare.ts @@ -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 { const shorter = Math.min(a.length, b.length); for (let i = 0; i < shorter; ++i) { diff --git a/src/common/internal/test_suite_listing.ts b/src/common/internal/test_suite_listing.ts index 2d2b555366e4..c5a0e1144839 100644 --- a/src/common/internal/test_suite_listing.ts +++ b/src/common/internal/test_suite_listing.ts @@ -1,6 +1,6 @@ // A listing of all specs within a single suite. This is the (awaited) type of // `groups` in '{cts,unittests}/listing.ts' and `listing` in the auto-generated -// 'out/{cts,unittests}/listing.js' files (see tools/gen_listings). +// 'out/{cts,unittests}/listing.js' files (see tools/gen_listings_and_webworkers). export type TestSuiteListing = TestSuiteListingEntry[]; export type TestSuiteListingEntry = TestSuiteListingEntrySpec | TestSuiteListingEntryReadme; diff --git a/src/common/runtime/helper/options.ts b/src/common/runtime/helper/options.ts index 60c2d1069181..67fd00372d39 100644 --- a/src/common/runtime/helper/options.ts +++ b/src/common/runtime/helper/options.ts @@ -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; @@ -68,6 +68,7 @@ export const kCTSOptionsInfo: OptionsInfos = { { value: '', description: 'no worker' }, { value: 'dedicated', description: 'dedicated worker' }, { value: 'shared', description: 'shared worker' }, + { value: 'service', description: 'service worker' }, ], }, debug: { description: 'show more info' }, diff --git a/src/common/runtime/helper/test_worker-worker.ts b/src/common/runtime/helper/test_worker-worker.ts index caf6e7a1bc17..cb31f4d26388 100644 --- a/src/common/runtime/helper/test_worker-worker.ts +++ b/src/common/runtime/helper/test_worker-worker.ts @@ -1,13 +1,9 @@ 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, WorkerTestRunRequest } from './utils_worker.js'; // Should be WorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom". /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -18,24 +14,9 @@ const loader = new DefaultTestFileLoader(); setBaseResourcePath('../../../resources'); 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; + const { query, expectations, ctsOptions } = ev.data as WorkerTestRunRequest; - 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'); @@ -48,7 +29,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) => { diff --git a/src/common/runtime/helper/test_worker.ts b/src/common/runtime/helper/test_worker.ts index 9e2ec17efa95..42848106b68e 100644 --- a/src/common/runtime/helper/test_worker.ts +++ b/src/common/runtime/helper/test_worker.ts @@ -2,80 +2,121 @@ import { LogMessageWithStack } from '../../internal/logging/log_message.js'; import { TransferredTestCaseResult, LiveTestCaseResult } from '../../internal/logging/result.js'; import { TestCaseRecorder } from '../../internal/logging/test_case_recorder.js'; import { TestQueryWithExpectation } from '../../internal/query/query.js'; +import { timeout } from '../../util/timeout.js'; +import { assert } from '../../util/util.js'; import { CTSOptions, kDefaultCTSOptions } from './options.js'; +import { WorkerTestRunRequest } from './utils_worker.js'; -export class TestDedicatedWorker { - private readonly ctsOptions: CTSOptions; - private readonly worker: Worker; - private readonly resolvers = new Map void>(); +/** Query all currently-registered service workers, and unregister them. */ +function unregisterAllServiceWorkers() { + void navigator.serviceWorker.getRegistrations().then(registrations => { + for (const registration of registrations) { + void registration.unregister(); + } + }); +} - constructor(ctsOptions?: CTSOptions) { - 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'; - 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); - } +// NOTE: This code runs on startup for any runtime with worker support. Here, we use that chance to +// delete any leaked service workers, and register to clean up after ourselves at shutdown. +unregisterAllServiceWorkers(); +window.addEventListener('beforeunload', () => { + unregisterAllServiceWorkers(); +}); + +class TestBaseWorker { + protected readonly ctsOptions: CTSOptions; + protected readonly resolvers = new Map 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); + } + this.resolvers.get(query)!(result as LiveTestCaseResult); + this.resolvers.delete(query); - // 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). - }; + // 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( + async makeRequestAndRecordResult( + target: MessagePort | Worker | ServiceWorker, rec: TestCaseRecorder, query: string, - expectations: TestQueryWithExpectation[] = [] - ): Promise { - this.worker.postMessage({ + expectations: TestQueryWithExpectation[] + ) { + const request: WorkerTestRunRequest = { query, expectations, ctsOptions: this.ctsOptions, - }); + }; + target.postMessage(request); + const workerResult = await new Promise(resolve => { + assert(!this.resolvers.has(query), "can't request same query twice simultaneously"); this.resolvers.set(query, resolve); }); rec.injectResult(workerResult); } } +export class TestDedicatedWorker extends TestBaseWorker { + private readonly worker: Worker; + + constructor(ctsOptions?: CTSOptions) { + 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 => this.onmessage(ev); + } + + async run( + rec: TestCaseRecorder, + query: string, + expectations: TestQueryWithExpectation[] = [] + ): Promise { + await this.makeRequestAndRecordResult(this.worker, rec, query, expectations); + } +} + 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 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); + this.port.onmessage = ev => this.onmessage(ev); + } - // 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 { + await this.makeRequestAndRecordResult(this.port, rec, query, expectations); + } +} + +export class TestServiceWorker extends TestBaseWorker { + constructor(ctsOptions?: CTSOptions) { + super('service', ctsOptions); } async run( @@ -83,14 +124,25 @@ export class TestSharedWorker { query: string, expectations: TestQueryWithExpectation[] = [] ): Promise { - this.port.postMessage({ - query, - expectations, - ctsOptions: this.ctsOptions, - }); - const workerResult = await new Promise(resolve => { - this.resolvers.set(query, resolve); + const [suite, name] = query.split(':', 2); + const fileName = name.split(',').join('/'); + const serviceWorkerURL = new URL( + `/out/${suite}/webworker/${fileName}.worker.js`, + window.location.href + ).toString(); + + // If a registration already exists for this path, it will be ignored. + const registration = await navigator.serviceWorker.register(serviceWorkerURL, { + type: 'module', }); - rec.injectResult(workerResult); + // Make sure the registration we just requested is active. (We don't worry about it being + // outdated from a previous page load, because we wipe all service workers on shutdown/startup.) + while (!registration.active || registration.active.scriptURL !== serviceWorkerURL) { + await new Promise(resolve => timeout(resolve, 0)); + } + const serviceWorker = registration.active; + + navigator.serviceWorker.onmessage = ev => this.onmessage(ev); + await this.makeRequestAndRecordResult(serviceWorker, rec, query, expectations); } } diff --git a/src/common/runtime/helper/utils_worker.ts b/src/common/runtime/helper/utils_worker.ts new file mode 100644 index 000000000000..c8ca6a2a3a0a --- /dev/null +++ b/src/common/runtime/helper/utils_worker.ts @@ -0,0 +1,34 @@ +import { globalTestConfig } from '../../framework/test_config.js'; +import { Logger } from '../../internal/logging/logger.js'; +import { TestQueryWithExpectation } from '../../internal/query/query.js'; +import { setDefaultRequestAdapterOptions } from '../../util/navigator_gpu.js'; + +import { CTSOptions } from './options.js'; + +export interface WorkerTestRunRequest { + query: string; + expectations: TestQueryWithExpectation[]; + ctsOptions: CTSOptions; +} + +/** + * Set config environment for workers with ctsOptions and return a Logger. + */ +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; +} diff --git a/src/common/runtime/helper/wrap_for_worker.ts b/src/common/runtime/helper/wrap_for_worker.ts new file mode 100644 index 000000000000..8e206f8399ab --- /dev/null +++ b/src/common/runtime/helper/wrap_for_worker.ts @@ -0,0 +1,43 @@ +import { Fixture } from '../../framework/fixture'; +import { LogMessageWithStack } from '../../internal/logging/log_message.js'; +import { comparePaths, comparePublicParamsPaths, Ordering } from '../../internal/query/compare.js'; +import { parseQuery } from '../../internal/query/parseQuery.js'; +import { TestQuerySingleCase } from '../../internal/query/query.js'; +import { TestGroup } from '../../internal/test_group.js'; +import { assert } from '../../util/util.js'; + +import { setupWorkerEnvironment, WorkerTestRunRequest } from './utils_worker.js'; + +export function wrapTestGroupForWorker(g: TestGroup) { + self.onmessage = async (ev: MessageEvent) => { + const { query, expectations, ctsOptions } = ev.data as WorkerTestRunRequest; + try { + 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, 'testcase not found'); + const [rec, result] = log.record(query); + await testcase.run(rec, testQuery, expectations); + + ev.source?.postMessage({ query, result }); + } catch (thrown) { + const ex = thrown instanceof Error ? thrown : new Error(`${thrown}`); + ev.source?.postMessage({ + query, + result: { status: 'fail', timems: 0, logs: [new LogMessageWithStack('INTERNAL', ex)] }, + }); + } + }; +} diff --git a/src/common/runtime/standalone.ts b/src/common/runtime/standalone.ts index fa938c55a276..62090ee5dc92 100644 --- a/src/common/runtime/standalone.ts +++ b/src/common/runtime/standalone.ts @@ -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; @@ -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')!; @@ -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); } diff --git a/src/common/runtime/wpt.ts b/src/common/runtime/wpt.ts index aacd34b13e51..56ff9649e045 100644 --- a/src/common/runtime/wpt.ts +++ b/src/common/runtime/wpt.ts @@ -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 { @@ -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'); @@ -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); } diff --git a/src/common/tools/dev_server.ts b/src/common/tools/dev_server.ts index 57cb6a7ea4f6..8e0e3bdbe656 100644 --- a/src/common/tools/dev_server.ts +++ b/src/common/tools/dev_server.ts @@ -144,6 +144,19 @@ app.get('/out/:suite([a-zA-Z0-9_-]+)/listing.js', async (req, res, next) => { } }); +// Serve .worker.js files by generating the necessary wrapper. +app.get('/out/:suite([a-zA-Z0-9_-]+)/webworker/:filepath(*).worker.js', (req, res, next) => { + const { suite, filepath } = req.params; + const result = `\ +import { g } from '/out/${suite}/${filepath}.spec.js'; +import { wrapTestGroupForWorker } from '/out/common/runtime/helper/wrap_for_worker.js'; + +wrapTestGroupForWorker(g); +`; + res.setHeader('Content-Type', 'application/javascript'); + res.send(result); +}); + // Serve all other .js files by fetching the source .ts file and compiling it. app.get('/out/**/*.js', async (req, res, next) => { const jsUrl = path.relative('/out', req.url); diff --git a/src/common/tools/gen_listings.ts b/src/common/tools/gen_listings.ts deleted file mode 100644 index 032ebaa890f0..000000000000 --- a/src/common/tools/gen_listings.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as process from 'process'; - -import { crawl } from './crawl.js'; - -function usage(rc: number): void { - console.error(`Usage: tools/gen_listings [options] [OUT_DIR] [SUITE_DIRS...] - -For each suite in SUITE_DIRS, generate listings and write each listing.js -into OUT_DIR/{suite}/listing.js. Example: - tools/gen_listings gen/ src/unittests/ src/webgpu/ - -Options: - --help Print this message and exit. -`); - process.exit(rc); -} - -const argv = process.argv; -if (argv.indexOf('--help') !== -1) { - usage(0); -} - -{ - // Ignore old argument that is now the default - const i = argv.indexOf('--no-validate'); - if (i !== -1) { - argv.splice(i, 1); - } -} - -if (argv.length < 4) { - usage(0); -} - -const myself = 'src/common/tools/gen_listings.ts'; - -const outDir = argv[2]; - -for (const suiteDir of argv.slice(3)) { - // Run concurrently for each suite (might be a tiny bit more efficient) - void crawl(suiteDir, false).then(listing => { - const suite = path.basename(suiteDir); - const outFile = path.normalize(path.join(outDir, `${suite}/listing.js`)); - fs.mkdirSync(path.join(outDir, suite), { recursive: true }); - fs.writeFileSync( - outFile, - `\ -// AUTO-GENERATED - DO NOT EDIT. See ${myself}. - -export const listing = ${JSON.stringify(listing, undefined, 2)}; -` - ); - }); -} diff --git a/src/common/tools/gen_listings_and_webworkers.ts b/src/common/tools/gen_listings_and_webworkers.ts new file mode 100644 index 000000000000..e446936b66cf --- /dev/null +++ b/src/common/tools/gen_listings_and_webworkers.ts @@ -0,0 +1,90 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as process from 'process'; + +import { crawl } from './crawl.js'; + +function usage(rc: number): void { + console.error(`Usage: tools/gen_listings_and_webworkers [options] [OUT_DIR] [SUITE_DIRS...] + +For each suite in SUITE_DIRS, generate listings into OUT_DIR/{suite}/listing.js, +and generate Web Worker proxies in OUT_DIR/{suite}/webworker/**/*.worker.js for +every .spec.js file. (Note {suite}/webworker/ is reserved for this purpose.) + +Example: + tools/gen_listings_and_webworkers gen/ src/unittests/ src/webgpu/ + +Options: + --help Print this message and exit. +`); + process.exit(rc); +} + +const argv = process.argv; +if (argv.indexOf('--help') !== -1) { + usage(0); +} + +{ + // Ignore old argument that is now the default + const i = argv.indexOf('--no-validate'); + if (i !== -1) { + argv.splice(i, 1); + } +} + +if (argv.length < 4) { + usage(0); +} + +const myself = 'src/common/tools/gen_listings_and_webworkers.ts'; + +const outDir = argv[2]; + +for (const suiteDir of argv.slice(3)) { + // Run concurrently for each suite (might be a tiny bit more efficient) + void crawl(suiteDir, false).then(listing => { + const suite = path.basename(suiteDir); + + // Write listing.js + const outFile = path.normalize(path.join(outDir, `${suite}/listing.js`)); + fs.mkdirSync(path.join(outDir, suite), { recursive: true }); + fs.writeFileSync( + outFile, + `\ +// AUTO-GENERATED - DO NOT EDIT. See ${myself}. + +export const listing = ${JSON.stringify(listing, undefined, 2)}; +` + ); + + // Write suite/webworker/**/*.worker.js + for (const entry of listing) { + if ('readme' in entry) continue; + + const outFileDir = path.join( + outDir, + suite, + 'webworker', + ...entry.file.slice(0, entry.file.length - 1) + ); + const outFile = path.join(outDir, suite, 'webworker', ...entry.file) + '.worker.js'; + + const relPathToSuiteRoot = Array(entry.file.length).fill('..').join('/'); + + fs.mkdirSync(outFileDir, { recursive: true }); + fs.writeFileSync( + outFile, + `\ +// AUTO-GENERATED - DO NOT EDIT. See ${myself}. + +// g is a TestGroup object (defined in common/internal/test_group.ts). +import { g } from '${relPathToSuiteRoot}/${entry.file.join('/')}.spec.js'; +import { wrapTestGroupForWorker } from '${relPathToSuiteRoot}/../common/runtime/helper/wrap_for_worker.js'; + +wrapTestGroupForWorker(g); +` + ); + } + }); +} diff --git a/src/demo/webworker/.gitignore b/src/demo/webworker/.gitignore new file mode 100644 index 000000000000..8496277ad72e --- /dev/null +++ b/src/demo/webworker/.gitignore @@ -0,0 +1,2 @@ +# DIRECTORY RESERVED FOR GENERATED FILES. See gen_listings_and_webworkers. +* diff --git a/src/manual/webworker/.gitignore b/src/manual/webworker/.gitignore new file mode 100644 index 000000000000..8496277ad72e --- /dev/null +++ b/src/manual/webworker/.gitignore @@ -0,0 +1,2 @@ +# DIRECTORY RESERVED FOR GENERATED FILES. See gen_listings_and_webworkers. +* diff --git a/src/resources/cache/hashes.json b/src/resources/cache/hashes.json index 3e6a9f6e58d3..a92aa14b76db 100644 --- a/src/resources/cache/hashes.json +++ b/src/resources/cache/hashes.json @@ -1,110 +1,110 @@ { - "webgpu/shader/execution/binary/af_addition.bin": "b6797474", - "webgpu/shader/execution/binary/af_logical.bin": "ff27f50f", - "webgpu/shader/execution/binary/af_division.bin": "97ba2e6d", - "webgpu/shader/execution/binary/af_matrix_addition.bin": "842a508f", - "webgpu/shader/execution/binary/af_matrix_subtraction.bin": "f086bb00", - "webgpu/shader/execution/binary/af_multiplication.bin": "fb6d6475", - "webgpu/shader/execution/binary/af_remainder.bin": "aa462ca7", - "webgpu/shader/execution/binary/af_subtraction.bin": "f04fbb32", - "webgpu/shader/execution/binary/f16_addition.bin": "3f136f5a", - "webgpu/shader/execution/binary/f16_logical.bin": "8556deb1", - "webgpu/shader/execution/binary/f16_division.bin": "693df765", - "webgpu/shader/execution/binary/f16_matrix_addition.bin": "296a11b6", - "webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin": "cf1ca531", - "webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin": "d3a317bd", - "webgpu/shader/execution/binary/f16_matrix_subtraction.bin": "a7a1b148", - "webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin": "8f540eee", - "webgpu/shader/execution/binary/f16_multiplication.bin": "1a7bca1c", - "webgpu/shader/execution/binary/f16_remainder.bin": "62b7bff8", - "webgpu/shader/execution/binary/f16_subtraction.bin": "c921e09d", - "webgpu/shader/execution/binary/f32_addition.bin": "7d7629bd", - "webgpu/shader/execution/binary/f32_logical.bin": "3e903bfb", - "webgpu/shader/execution/binary/f32_division.bin": "35b49326", - "webgpu/shader/execution/binary/f32_matrix_addition.bin": "1c2aff7c", - "webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin": "966b264e", - "webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin": "4ab99310", - "webgpu/shader/execution/binary/f32_matrix_subtraction.bin": "77f37d5a", - "webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin": "50dfea9a", - "webgpu/shader/execution/binary/f32_multiplication.bin": "2524db25", - "webgpu/shader/execution/binary/f32_remainder.bin": "b47ad113", - "webgpu/shader/execution/binary/f32_subtraction.bin": "e98584ae", - "webgpu/shader/execution/binary/i32_arithmetic.bin": "88d15602", - "webgpu/shader/execution/binary/i32_comparison.bin": "ddd815a3", - "webgpu/shader/execution/binary/u32_arithmetic.bin": "d882b7f6", - "webgpu/shader/execution/binary/u32_comparison.bin": "e3dfeec7", - "webgpu/shader/execution/abs.bin": "b6f6d4ec", - "webgpu/shader/execution/acos.bin": "d681fe45", - "webgpu/shader/execution/acosh.bin": "a58482ca", - "webgpu/shader/execution/asin.bin": "3856670", - "webgpu/shader/execution/asinh.bin": "7567d4b7", - "webgpu/shader/execution/atan.bin": "80fc2c53", - "webgpu/shader/execution/atan2.bin": "2fc91f76", - "webgpu/shader/execution/atanh.bin": "b68b4e98", - "webgpu/shader/execution/bitcast.bin": "142a34e", - "webgpu/shader/execution/ceil.bin": "65c551a0", - "webgpu/shader/execution/clamp.bin": "369a3396", - "webgpu/shader/execution/cos.bin": "894e3d79", - "webgpu/shader/execution/cosh.bin": "3bf810f1", - "webgpu/shader/execution/cross.bin": "582e87e8", - "webgpu/shader/execution/degrees.bin": "762053b5", - "webgpu/shader/execution/determinant.bin": "a60ac4f9", - "webgpu/shader/execution/distance.bin": "602fc718", - "webgpu/shader/execution/dot.bin": "c33454b1", - "webgpu/shader/execution/exp.bin": "ad6670cf", - "webgpu/shader/execution/exp2.bin": "6f8a0b39", - "webgpu/shader/execution/faceForward.bin": "759ef028", - "webgpu/shader/execution/floor.bin": "d8f91dbe", - "webgpu/shader/execution/fma.bin": "5019731d", - "webgpu/shader/execution/fract.bin": "3a9c858d", - "webgpu/shader/execution/frexp.bin": "33ffb585", - "webgpu/shader/execution/inverseSqrt.bin": "3684c8a", - "webgpu/shader/execution/ldexp.bin": "30881102", - "webgpu/shader/execution/length.bin": "fed952", - "webgpu/shader/execution/log.bin": "b55f7cd0", - "webgpu/shader/execution/log2.bin": "e4fddf41", - "webgpu/shader/execution/max.bin": "ca77aba5", - "webgpu/shader/execution/min.bin": "6b57ac82", - "webgpu/shader/execution/mix.bin": "89f42f5a", - "webgpu/shader/execution/modf.bin": "77eb4ff5", - "webgpu/shader/execution/normalize.bin": "36214034", - "webgpu/shader/execution/pack2x16float.bin": "8bee7573", - "webgpu/shader/execution/pow.bin": "8bcaf788", - "webgpu/shader/execution/quantizeToF16.bin": "8c1bc956", - "webgpu/shader/execution/radians.bin": "c35eecd0", - "webgpu/shader/execution/reflect.bin": "3591dbf7", - "webgpu/shader/execution/refract.bin": "8b2a061d", - "webgpu/shader/execution/round.bin": "58787d04", - "webgpu/shader/execution/saturate.bin": "b5c39be6", - "webgpu/shader/execution/sign.bin": "3b6e8c88", - "webgpu/shader/execution/sin.bin": "1e39b6f9", - "webgpu/shader/execution/sinh.bin": "296767d4", - "webgpu/shader/execution/smoothstep.bin": "2771eb6a", - "webgpu/shader/execution/sqrt.bin": "78c4534c", - "webgpu/shader/execution/step.bin": "b8817ec0", - "webgpu/shader/execution/tan.bin": "f2537ac4", - "webgpu/shader/execution/tanh.bin": "c611a910", - "webgpu/shader/execution/transpose.bin": "7fdac41d", - "webgpu/shader/execution/trunc.bin": "da4434eb", - "webgpu/shader/execution/unpack2x16float.bin": "a5db67bb", - "webgpu/shader/execution/unpack2x16snorm.bin": "b0e6f0f4", - "webgpu/shader/execution/unpack2x16unorm.bin": "ddf30c4f", - "webgpu/shader/execution/unpack4x8snorm.bin": "599b3fb9", - "webgpu/shader/execution/unpack4x8unorm.bin": "d47c58be", - "webgpu/shader/execution/unary/af_arithmetic.bin": "790d7c95", - "webgpu/shader/execution/unary/af_assignment.bin": "679cfa9a", - "webgpu/shader/execution/unary/bool_conversion.bin": "d10f1dbb", - "webgpu/shader/execution/unary/f16_arithmetic.bin": "1b44c7f", - "webgpu/shader/execution/unary/f16_conversion.bin": "436a5636", - "webgpu/shader/execution/unary/f32_arithmetic.bin": "8f8fcd42", - "webgpu/shader/execution/unary/f32_conversion.bin": "97d0e21a", - "webgpu/shader/execution/unary/i32_arithmetic.bin": "41460aa", - "webgpu/shader/execution/unary/i32_conversion.bin": "d5a46ee6", - "webgpu/shader/execution/unary/u32_conversion.bin": "f03edff7", - "webgpu/shader/execution/unary/ai_assignment.bin": "5d612a40", - "webgpu/shader/execution/binary/ai_arithmetic.bin": "c0dd04f9", - "webgpu/shader/execution/unary/ai_arithmetic.bin": "3ea222b8", - "webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin": "3d9cf9b7", - "webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin": "a84ecc73", - "webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin": "6fe3e741" + "webgpu/shader/execution/binary/af_addition.bin": "1043cbb2", + "webgpu/shader/execution/binary/af_logical.bin": "9ab41597", + "webgpu/shader/execution/binary/af_division.bin": "9ec22a4e", + "webgpu/shader/execution/binary/af_matrix_addition.bin": "1da2ca22", + "webgpu/shader/execution/binary/af_matrix_subtraction.bin": "25303b8e", + "webgpu/shader/execution/binary/af_multiplication.bin": "70d8f01a", + "webgpu/shader/execution/binary/af_remainder.bin": "101c7835", + "webgpu/shader/execution/binary/af_subtraction.bin": "26791651", + "webgpu/shader/execution/binary/f16_addition.bin": "2d67db8f", + "webgpu/shader/execution/binary/f16_logical.bin": "80f9e754", + "webgpu/shader/execution/binary/f16_division.bin": "53e3bdaa", + "webgpu/shader/execution/binary/f16_matrix_addition.bin": "911d18f7", + "webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin": "a306da0a", + "webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin": "be77024a", + "webgpu/shader/execution/binary/f16_matrix_subtraction.bin": "ea813df5", + "webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin": "1692bec1", + "webgpu/shader/execution/binary/f16_multiplication.bin": "7c618e51", + "webgpu/shader/execution/binary/f16_remainder.bin": "9de2be15", + "webgpu/shader/execution/binary/f16_subtraction.bin": "c77bff8a", + "webgpu/shader/execution/binary/f32_addition.bin": "d4282b93", + "webgpu/shader/execution/binary/f32_logical.bin": "47467130", + "webgpu/shader/execution/binary/f32_division.bin": "4d79a5f6", + "webgpu/shader/execution/binary/f32_matrix_addition.bin": "5972ccf0", + "webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin": "8bc3f04a", + "webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin": "7376da03", + "webgpu/shader/execution/binary/f32_matrix_subtraction.bin": "c4785f3a", + "webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin": "20d4b1", + "webgpu/shader/execution/binary/f32_multiplication.bin": "7586ed6d", + "webgpu/shader/execution/binary/f32_remainder.bin": "59f46306", + "webgpu/shader/execution/binary/f32_subtraction.bin": "4311ba10", + "webgpu/shader/execution/binary/i32_arithmetic.bin": "d345f9c7", + "webgpu/shader/execution/binary/i32_comparison.bin": "6d4ae7e0", + "webgpu/shader/execution/binary/u32_arithmetic.bin": "163f8a38", + "webgpu/shader/execution/binary/u32_comparison.bin": "f6c1497b", + "webgpu/shader/execution/abs.bin": "63fa988e", + "webgpu/shader/execution/acos.bin": "47521b3e", + "webgpu/shader/execution/acosh.bin": "59762b6f", + "webgpu/shader/execution/asin.bin": "db0c6641", + "webgpu/shader/execution/asinh.bin": "f0329d7f", + "webgpu/shader/execution/atan.bin": "f6fea79e", + "webgpu/shader/execution/atan2.bin": "1c390d0f", + "webgpu/shader/execution/atanh.bin": "4c430905", + "webgpu/shader/execution/bitcast.bin": "2dfc8f9", + "webgpu/shader/execution/ceil.bin": "3319384e", + "webgpu/shader/execution/clamp.bin": "4873dd79", + "webgpu/shader/execution/cos.bin": "c16d181a", + "webgpu/shader/execution/cosh.bin": "137f4997", + "webgpu/shader/execution/cross.bin": "aebda306", + "webgpu/shader/execution/degrees.bin": "5001b340", + "webgpu/shader/execution/determinant.bin": "c461c715", + "webgpu/shader/execution/distance.bin": "d05e2f8d", + "webgpu/shader/execution/dot.bin": "7f8b7e13", + "webgpu/shader/execution/exp.bin": "d448dc91", + "webgpu/shader/execution/exp2.bin": "1d232c0e", + "webgpu/shader/execution/faceForward.bin": "87c0576c", + "webgpu/shader/execution/floor.bin": "c3a60f36", + "webgpu/shader/execution/fma.bin": "6b94384d", + "webgpu/shader/execution/fract.bin": "d49c3d50", + "webgpu/shader/execution/frexp.bin": "252e0615", + "webgpu/shader/execution/inverseSqrt.bin": "1deb1185", + "webgpu/shader/execution/ldexp.bin": "617e4cc4", + "webgpu/shader/execution/length.bin": "f7b51c96", + "webgpu/shader/execution/log.bin": "ae124706", + "webgpu/shader/execution/log2.bin": "c428e9f2", + "webgpu/shader/execution/max.bin": "a45914cd", + "webgpu/shader/execution/min.bin": "1017e86d", + "webgpu/shader/execution/mix.bin": "3f5ebb6e", + "webgpu/shader/execution/modf.bin": "fedc0c50", + "webgpu/shader/execution/normalize.bin": "60173a1e", + "webgpu/shader/execution/pack2x16float.bin": "e472a927", + "webgpu/shader/execution/pow.bin": "c5a2a9c1", + "webgpu/shader/execution/quantizeToF16.bin": "8d1cb7a6", + "webgpu/shader/execution/radians.bin": "94cbdf6c", + "webgpu/shader/execution/reflect.bin": "39a479a0", + "webgpu/shader/execution/refract.bin": "63e5b11a", + "webgpu/shader/execution/round.bin": "13bf324", + "webgpu/shader/execution/saturate.bin": "c368b1a7", + "webgpu/shader/execution/sign.bin": "5cd05b5c", + "webgpu/shader/execution/sin.bin": "85679e", + "webgpu/shader/execution/sinh.bin": "eaa61750", + "webgpu/shader/execution/smoothstep.bin": "bd06d7cc", + "webgpu/shader/execution/sqrt.bin": "997f5572", + "webgpu/shader/execution/step.bin": "87505049", + "webgpu/shader/execution/tan.bin": "9d7ca121", + "webgpu/shader/execution/tanh.bin": "8bfa913c", + "webgpu/shader/execution/transpose.bin": "7f8fa4c6", + "webgpu/shader/execution/trunc.bin": "9a054523", + "webgpu/shader/execution/unpack2x16float.bin": "ed71c71d", + "webgpu/shader/execution/unpack2x16snorm.bin": "89b4cd2d", + "webgpu/shader/execution/unpack2x16unorm.bin": "df3b3851", + "webgpu/shader/execution/unpack4x8snorm.bin": "aa10073", + "webgpu/shader/execution/unpack4x8unorm.bin": "2fe14b98", + "webgpu/shader/execution/unary/af_arithmetic.bin": "accfc1bd", + "webgpu/shader/execution/unary/af_assignment.bin": "cb3e120e", + "webgpu/shader/execution/unary/bool_conversion.bin": "d0c1e5a3", + "webgpu/shader/execution/unary/f16_arithmetic.bin": "310a05dd", + "webgpu/shader/execution/unary/f16_conversion.bin": "9d94aa72", + "webgpu/shader/execution/unary/f32_arithmetic.bin": "fe955d67", + "webgpu/shader/execution/unary/f32_conversion.bin": "918f6cb3", + "webgpu/shader/execution/unary/i32_arithmetic.bin": "a8649cbb", + "webgpu/shader/execution/unary/i32_conversion.bin": "e5157a69", + "webgpu/shader/execution/unary/u32_conversion.bin": "d07d0c20", + "webgpu/shader/execution/unary/ai_assignment.bin": "f62c765c", + "webgpu/shader/execution/binary/ai_arithmetic.bin": "43501242", + "webgpu/shader/execution/unary/ai_arithmetic.bin": "8e448c53", + "webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin": "7ea0df71", + "webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin": "4919c460", + "webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin": "33c10dad" } \ No newline at end of file diff --git a/src/stress/webworker/.gitignore b/src/stress/webworker/.gitignore new file mode 100644 index 000000000000..8496277ad72e --- /dev/null +++ b/src/stress/webworker/.gitignore @@ -0,0 +1,2 @@ +# DIRECTORY RESERVED FOR GENERATED FILES. See gen_listings_and_webworkers. +* diff --git a/src/unittests/webworker/.gitignore b/src/unittests/webworker/.gitignore new file mode 100644 index 000000000000..8496277ad72e --- /dev/null +++ b/src/unittests/webworker/.gitignore @@ -0,0 +1,2 @@ +# DIRECTORY RESERVED FOR GENERATED FILES. See gen_listings_and_webworkers. +* diff --git a/src/webgpu/listing_meta.json b/src/webgpu/listing_meta.json index 887bbeffe2bf..94126a1f711b 100644 --- a/src/webgpu/listing_meta.json +++ b/src/webgpu/listing_meta.json @@ -2219,6 +2219,7 @@ "webgpu:web_platform,external_texture,video:importExternalTexture,sampleWithVideoFrameWithVisibleRectParam:*": { "subcaseMS": 29.160 }, "webgpu:web_platform,external_texture,video:importExternalTexture,sample_non_YUV_video_frame:*": { "subcaseMS": 36.270 }, "webgpu:web_platform,worker,worker:dedicated_worker:*": { "subcaseMS": 245.901 }, + "webgpu:web_platform,worker,worker:service_worker:*": { "subcaseMS": 102.800 }, "webgpu:web_platform,worker,worker:shared_worker:*": { "subcaseMS": 26.801 }, "_end": "" } diff --git a/src/webgpu/web_platform/worker/worker.spec.ts b/src/webgpu/web_platform/worker/worker.spec.ts index 0f34a58ddaa5..deed5d75e232 100644 --- a/src/webgpu/web_platform/worker/worker.spec.ts +++ b/src/webgpu/web_platform/worker/worker.spec.ts @@ -4,6 +4,9 @@ Tests WebGPU is available in a dedicated worker and a shared worker. Note: The CTS test can be run respectively in a dedicated 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 dedicated worker and a shared worker. + +TODO[2]: Figure out how to make these tests run in service workers (not actually +important unless service workers gain the ability to launch other workers). `; import { Fixture } from '../../../common/framework/fixture.js'; @@ -12,24 +15,26 @@ import { assert } from '../../../common/util/util.js'; export const g = makeTestGroup(Fixture); -function isNode(): boolean { - return typeof process !== 'undefined' && process?.versions?.node !== undefined; -} +const isNode = typeof process !== 'undefined' && process?.versions?.node !== undefined; + +// [1]: 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 and pass the string to import +// otherwise typescript tries to parse the file which again, fails. +// worker_launcher.js is excluded in node.tsconfig.json. + +// [2]: That hack does not work in Service Workers. +const isServiceWorker = globalThis.constructor.name === 'ServiceWorkerGlobalScope'; g.test('dedicated_worker') .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'); - 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. + t.skipIf(isNode, 'node does not support 100% compatible workers'); + + t.skipIf(isServiceWorker, 'Service workers do not support this import() hack'); // [2] const url = './worker_launcher.js'; - const { launchDedicatedWorker } = await import(url); + const { launchDedicatedWorker } = await import(url); // [1] + const result = await launchDedicatedWorker(); assert(result.error === undefined, `should be no error from worker but was: ${result.error}`); }); @@ -37,17 +42,25 @@ g.test('dedicated_worker') 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 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. + t.skipIf(isNode, 'node does not support 100% compatible workers'); + + t.skipIf(isServiceWorker, 'Service workers do not support this import() hack'); // [2] const url = './worker_launcher.js'; - const { launchSharedWorker } = await import(url); + const { launchSharedWorker } = await import(url); // [1] + const result = await launchSharedWorker(); assert(result.error === undefined, `should be no error from 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 => { + t.skipIf(isNode, 'node does not support 100% compatible workers'); + + t.skipIf(isServiceWorker, 'Service workers do not support this import() hack'); // [2] + const url = './worker_launcher.js'; + const { launchServiceWorker } = await import(url); // [1] + + const result = await launchServiceWorker(); + assert(result.error === undefined, `should be no error from worker but was: ${result.error}`); + }); diff --git a/src/webgpu/web_platform/worker/worker.ts b/src/webgpu/web_platform/worker/worker.ts index f3c907a4118a..033473d63a97 100644 --- a/src/webgpu/web_platform/worker/worker.ts +++ b/src/webgpu/web_platform/worker/worker.ts @@ -87,7 +87,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) => { diff --git a/src/webgpu/web_platform/worker/worker_launcher.ts b/src/webgpu/web_platform/worker/worker_launcher.ts index 0487e4ad38b4..4b1d31ae4921 100644 --- a/src/webgpu/web_platform/worker/worker_launcher.ts +++ b/src/webgpu/web_platform/worker/worker_launcher.ts @@ -1,3 +1,4 @@ +import { SkipTestCase } from '../../../common/framework/fixture.js'; import { getDefaultRequestAdapterOptions } from '../../../common/util/navigator_gpu.js'; export type TestResult = { @@ -5,6 +6,10 @@ export type TestResult = { }; export async function launchDedicatedWorker() { + if (typeof Worker === 'undefined') { + throw new SkipTestCase(`Worker undefined in context ${globalThis.constructor.name}`); + } + const selfPath = import.meta.url; const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); const workerPath = selfPathDir + '/worker.js'; @@ -18,6 +23,10 @@ export async function launchDedicatedWorker() { } export async function launchSharedWorker() { + if (typeof SharedWorker === 'undefined') { + throw new SkipTestCase(`SharedWorker undefined in context ${globalThis.constructor.name}`); + } + const selfPath = import.meta.url; const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); const workerPath = selfPathDir + '/worker.js'; @@ -33,3 +42,34 @@ export async function launchSharedWorker() { }); return await promise; } + +export async function launchServiceWorker() { + if (typeof navigator === 'undefined' || typeof navigator.serviceWorker === 'undefined') { + throw new SkipTestCase( + `navigator.serviceWorker undefined in context ${globalThis.constructor.name}` + ); + } + + const selfPath = import.meta.url; + const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); + const serviceWorkerPath = selfPathDir + '/worker.js'; + const registration = await navigator.serviceWorker.register(serviceWorkerPath, { + type: 'module', + }); + await registration.update(); + + const promise = new Promise(resolve => { + navigator.serviceWorker.addEventListener( + 'message', + ev => { + resolve(ev.data as TestResult); + void registration.unregister(); + }, + { once: true } + ); + }); + registration.active?.postMessage({ + defaultRequestAdapterOptions: getDefaultRequestAdapterOptions(), + }); + return await promise; +} diff --git a/src/webgpu/webworker/.gitignore b/src/webgpu/webworker/.gitignore new file mode 100644 index 000000000000..8496277ad72e --- /dev/null +++ b/src/webgpu/webworker/.gitignore @@ -0,0 +1,2 @@ +# DIRECTORY RESERVED FOR GENERATED FILES. See gen_listings_and_webworkers. +* diff --git a/tools/gen_listings b/tools/gen_listings deleted file mode 100755 index 6c25622423a1..000000000000 --- a/tools/gen_listings +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node - -// Crawl a suite directory (e.g. src/webgpu/) to generate a listing.js containing -// the listing of test files in the suite. - -require('../src/common/tools/setup-ts-in-node.js'); -require('../src/common/tools/gen_listings.ts'); diff --git a/tools/gen_listings_and_webworkers b/tools/gen_listings_and_webworkers new file mode 100755 index 000000000000..285df3657d2a --- /dev/null +++ b/tools/gen_listings_and_webworkers @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +// Crawl a suite directory (e.g. src/webgpu/) to generate a listing.js containing +// the listing of test files in the suite, and some generated test files +// (like suite/serviceworker/**/*.spec.js). + +require('../src/common/tools/setup-ts-in-node.js'); +require('../src/common/tools/gen_listings_and_webworkers.ts');