diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/common/runtime/helper')
5 files changed, 335 insertions, 66 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts index 38974b803f..4a82c7d292 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts @@ -1,3 +1,5 @@ +import { unreachable } from '../../util/util.js'; + let windowURL: URL | undefined = undefined; function getWindowURL() { if (windowURL === undefined) { @@ -6,6 +8,7 @@ function getWindowURL() { return windowURL; } +/** Parse a runner option that is always boolean-typed. False if missing or '0'. */ export function optionEnabled( opt: string, searchParams: URLSearchParams = getWindowURL().searchParams @@ -14,30 +17,55 @@ export function optionEnabled( return val !== null && val !== '0'; } +/** Parse a runner option that is string-typed. If the option is missing, returns `null`. */ export function optionString( opt: string, searchParams: URLSearchParams = getWindowURL().searchParams -): string { - return searchParams.get(opt) || ''; +): string | null { + return searchParams.get(opt); +} + +/** Runtime modes for running tests in different types of workers. */ +export type WorkerMode = 'dedicated' | 'service' | 'shared'; +/** Parse a runner option for different worker modes (as in `?worker=shared`). Null if no worker. */ +export function optionWorkerMode( + opt: string, + searchParams: URLSearchParams = getWindowURL().searchParams +): WorkerMode | null { + const value = searchParams.get(opt); + if (value === null || value === '0') { + return null; + } else if (value === 'service') { + return 'service'; + } else if (value === 'shared') { + return 'shared'; + } else if (value === '' || value === '1' || value === 'dedicated') { + return 'dedicated'; + } + unreachable('invalid worker= option value'); } /** * The possible options for the tests. */ export interface CTSOptions { - worker: boolean; + worker: WorkerMode | null; debug: boolean; compatibility: boolean; + forceFallbackAdapter: boolean; unrollConstEvalLoops: boolean; - powerPreference?: GPUPowerPreference | ''; + powerPreference: GPUPowerPreference | null; + logToWebSocket: boolean; } export const kDefaultCTSOptions: CTSOptions = { - worker: false, + worker: null, debug: true, compatibility: false, + forceFallbackAdapter: false, unrollConstEvalLoops: false, - powerPreference: '', + powerPreference: null, + logToWebSocket: false, }; /** @@ -45,8 +73,8 @@ export const kDefaultCTSOptions: CTSOptions = { */ export interface OptionInfo { description: string; - parser?: (key: string, searchParams?: URLSearchParams) => boolean | string; - selectValueDescriptions?: { value: string; description: string }[]; + parser?: (key: string, searchParams?: URLSearchParams) => boolean | string | null; + selectValueDescriptions?: { value: string | null; description: string }[]; } /** @@ -59,19 +87,30 @@ 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: optionWorkerMode, + selectValueDescriptions: [ + { value: null, 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' }, + forceFallbackAdapter: { description: 'pass forceFallbackAdapter: true to requestAdapter' }, unrollConstEvalLoops: { description: 'unroll const eval loops in WGSL' }, powerPreference: { description: 'set default powerPreference for some tests', parser: optionString, selectValueDescriptions: [ - { value: '', description: 'default' }, + { value: null, description: 'default' }, { value: 'low-power', description: 'low-power' }, { value: 'high-performance', description: 'high-performance' }, ], }, + logToWebSocket: { description: 'send some logs to ws://localhost:59497/' }, }; /** @@ -95,7 +134,7 @@ function getOptionsInfoFromSearchString<Type extends CTSOptions>( searchString: string ): Type { const searchParams = new URLSearchParams(searchString); - const optionValues: Record<string, boolean | string> = {}; + const optionValues: Record<string, boolean | string | null> = {}; for (const [optionName, info] of Object.entries(optionsInfos)) { const parser = info.parser || optionEnabled; optionValues[optionName] = parser(camelCaseToSnakeCase(optionName), searchParams); diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts index e8d187ea7e..ebc206c3b2 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts @@ -1,15 +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, WorkerTestRunRequest } from './utils_worker.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; @@ -17,25 +13,10 @@ const loader = new DefaultTestFileLoader(); setBaseResourcePath('../../../resources'); -self.onmessage = async (ev: MessageEvent) => { - const query: string = ev.data.query; - const expectations: TestQueryWithExpectation[] = ev.data.expectations; - const ctsOptions: CTSOptions = ev.data.ctsOptions; +async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) { + 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'); @@ -44,5 +25,23 @@ 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: { + ...result, + logs: result.logs?.map(l => l.toRawData()), + }, + }); +} + +self.onmessage = (ev: MessageEvent) => { + void reportTestResults.call(ev.source || self, ev); +}; + +self.onconnect = (event: MessageEvent) => { + const port = event.ports[0]; + + port.onmessage = (ev: MessageEvent) => { + void reportTestResults.call(port, ev); + }; }; diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts index 9bbcab0946..f9a44bb7bc 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts @@ -2,48 +2,190 @@ 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 { CTSOptions, WorkerMode, kDefaultCTSOptions } from './options.js'; +import { WorkerTestRunRequest } from './utils_worker.js'; -export class TestWorker { - private readonly ctsOptions: CTSOptions; - private readonly worker: Worker; - private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => 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: true } }; - 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); +// 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(); +}); + +abstract class TestBaseWorker { + protected readonly ctsOptions: CTSOptions; + protected readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>(); + + constructor(worker: WorkerMode, ctsOptions?: CTSOptions) { + this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker } }; + } - // 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). + onmessage(ev: MessageEvent) { + const query: string = ev.data.query; + const transferredResult: TransferredTestCaseResult = ev.data.result; + + const result: LiveTestCaseResult = { + status: transferredResult.status, + timems: transferredResult.timems, + logs: transferredResult.logs?.map(l => new LogMessageWithStack(l)), }; + + this.resolvers.get(query)!(result); + 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). } - async run( - rec: TestCaseRecorder, + makeRequestAndRecordResult( + target: MessagePort | Worker | ServiceWorker, query: string, - expectations: TestQueryWithExpectation[] = [] - ): Promise<void> { - this.worker.postMessage({ + expectations: TestQueryWithExpectation[] + ): Promise<LiveTestCaseResult> { + const request: WorkerTestRunRequest = { query, expectations, ctsOptions: this.ctsOptions, - }); - const workerResult = await new Promise<LiveTestCaseResult>(resolve => { + }; + target.postMessage(request); + + return new Promise<LiveTestCaseResult>(resolve => { + assert(!this.resolvers.has(query), "can't request same query twice simultaneously"); this.resolvers.set(query, resolve); }); - rec.injectResult(workerResult); + } + + async run( + rec: TestCaseRecorder, + query: string, + expectations: TestQueryWithExpectation[] = [] + ): Promise<void> { + try { + rec.injectResult(await this.runImpl(query, expectations)); + } catch (ex) { + rec.start(); + rec.threw(ex); + rec.finish(); + } + } + + protected abstract runImpl( + query: string, + expectations: TestQueryWithExpectation[] + ): Promise<LiveTestCaseResult>; +} + +export class TestDedicatedWorker extends TestBaseWorker { + private readonly worker: Worker | Error; + + constructor(ctsOptions?: CTSOptions) { + super('dedicated', ctsOptions); + try { + if (typeof Worker === 'undefined') { + throw new Error('Dedicated Workers not available'); + } + + 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); + } catch (ex) { + assert(ex instanceof Error); + // Save the exception to re-throw in runImpl(). + this.worker = ex; + } + } + + override runImpl(query: string, expectations: TestQueryWithExpectation[] = []) { + if (this.worker instanceof Worker) { + return this.makeRequestAndRecordResult(this.worker, query, expectations); + } else { + throw this.worker; + } + } +} + +/** @deprecated Use TestDedicatedWorker instead. */ +export class TestWorker extends TestDedicatedWorker {} + +export class TestSharedWorker extends TestBaseWorker { + /** MessagePort to the SharedWorker, or an Error if it couldn't be initialized. */ + private readonly port: MessagePort | Error; + + constructor(ctsOptions?: CTSOptions) { + super('shared', ctsOptions); + try { + if (typeof SharedWorker === 'undefined') { + throw new Error('Shared Workers not available'); + } + + 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 => this.onmessage(ev); + } catch (ex) { + assert(ex instanceof Error); + // Save the exception to re-throw in runImpl(). + this.port = ex; + } + } + + override runImpl(query: string, expectations: TestQueryWithExpectation[] = []) { + if (this.port instanceof MessagePort) { + return this.makeRequestAndRecordResult(this.port, query, expectations); + } else { + throw this.port; + } + } +} + +export class TestServiceWorker extends TestBaseWorker { + constructor(ctsOptions?: CTSOptions) { + super('service', ctsOptions); + } + + override async runImpl(query: string, expectations: TestQueryWithExpectation[] = []) { + if (!('serviceWorker' in navigator)) { + throw new Error('Service Workers not available'); + } + const [suite, name] = query.split(':', 2); + const fileName = name.split(',').join('/'); + + const selfPath = import.meta.url; + const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); + // Construct the path to the worker file, then use URL to resolve the `../` components. + const serviceWorkerURL = new URL( + `${selfPathDir}/../../../${suite}/webworker/${fileName}.worker.js` + ).toString(); + + // If a registration already exists for this path, it will be ignored. + const registration = await navigator.serviceWorker.register(serviceWorkerURL, { + type: 'module', + }); + // 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); + return this.makeRequestAndRecordResult(serviceWorker, query, expectations); } } diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/utils_worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/utils_worker.ts new file mode 100644 index 0000000000..13880635bc --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/utils_worker.ts @@ -0,0 +1,35 @@ +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 { powerPreference, compatibility } = ctsOptions; + globalTestConfig.enableDebugLogs = ctsOptions.debug; + globalTestConfig.unrollConstEvalLoops = ctsOptions.unrollConstEvalLoops; + globalTestConfig.compatibility = compatibility; + globalTestConfig.logToWebSocket = ctsOptions.logToWebSocket; + + 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/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/wrap_for_worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/wrap_for_worker.ts new file mode 100644 index 0000000000..5f600fe89d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/wrap_for_worker.ts @@ -0,0 +1,54 @@ +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'; + +/** + * Sets up the currently running Web Worker to wrap the TestGroup object `g`. + * `g` is the `g` exported from a `.spec.ts` file: a TestGroupBuilder<F> interface, + * which underneath is actually a TestGroup<F> object. + * + * This is used in the generated `.worker.js` files that are generated to use as service workers. + */ +export function wrapTestGroupForWorker(g: TestGroup<Fixture>) { + 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: [LogMessageWithStack.wrapError('INTERNAL', ex)], + }, + }); + } + }; +} |