blob: 0af764ce7f5c767b2a4c7f8046d84c8ed82cbfbc [file] [log] [blame]
Austin Eng1cdea902022-03-24 00:21:55 +00001// Copyright 2022 The Dawn Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
Austin Engb00c50e2022-08-26 22:34:27 +000015import { globalTestConfig } from '../third_party/webgpu-cts/src/common/framework/test_config.js';
Austin Eng92b32e82022-11-21 15:16:51 +000016import { dataCache } from '../third_party/webgpu-cts/src/common/framework/data_cache.js';
Austin Eng1cdea902022-03-24 00:21:55 +000017import { DefaultTestFileLoader } from '../third_party/webgpu-cts/src/common/internal/file_loader.js';
18import { prettyPrintLog } from '../third_party/webgpu-cts/src/common/internal/logging/log_message.js';
19import { Logger } from '../third_party/webgpu-cts/src/common/internal/logging/logger.js';
20import { parseQuery } from '../third_party/webgpu-cts/src/common/internal/query/parseQuery.js';
Gregg Tavarese1d87cd2023-06-29 23:36:25 +000021import { parseSearchParamLikeWithCTSOptions } from '../third_party/webgpu-cts/src/common/runtime/helper/options.js';
22import { setDefaultRequestAdapterOptions } from '../third_party/webgpu-cts/src/common/util/navigator_gpu.js';
Austin Eng1cdea902022-03-24 00:21:55 +000023
24import { TestWorker } from '../third_party/webgpu-cts/src/common/runtime/helper/test_worker.js';
25
Brian Sheedy17b1a452022-04-14 17:19:11 +000026// The Python-side websockets library has a max payload size of 72638. Set the
27// max allowable logs size in a single payload to a bit less than that.
28const LOGS_MAX_BYTES = 72000;
29
Austin Eng1cdea902022-03-24 00:21:55 +000030var socket;
31
Austin Engcaa9bae2022-08-18 22:49:10 +000032// Returns a wrapper around `fn` which gets called at most once every `intervalMs`.
33// If the wrapper is called when `fn` was called too recently, `fn` is scheduled to
34// be called later in the future after the interval passes.
35// Returns [ wrappedFn, {start, stop}] where wrappedFn is the rate-limited function,
36// and start/stop control whether or not the function is enabled. If it is stopped, calls
37// to the fn will no-op. If it is started, calls will be rate-limited, starting from
38// the time `start` is called.
39function rateLimited(fn, intervalMs) {
40 let last = undefined;
41 let timer = undefined;
42 const wrappedFn = (...args) => {
Austin Eng26ffcd12022-09-13 18:28:31 +000043 if (last === undefined) {
44 // If the function is not enabled, return.
Austin Engcaa9bae2022-08-18 22:49:10 +000045 return;
46 }
47 // Get the current time as a number.
48 const now = +new Date();
49 const diff = now - last;
50 if (diff >= intervalMs) {
51 // Clear the timer, if there was one. This could happen if a timer
52 // is scheduled, but it never runs due to long-running synchronous
53 // code.
54 if (timer) {
55 clearTimeout(timer);
56 timer = undefined;
57 }
58
59 // Call the function.
60 last = now;
61 fn(...args);
62 } else if (timer === undefined) {
63 // Otherwise, we have called `fn` too recently.
64 // Schedule a future call.
65 timer = setTimeout(() => {
66 // Clear the timer to indicate nothing is scheduled.
67 timer = undefined;
68 last = +new Date();
69 fn(...args);
70 }, intervalMs - diff + 1);
71 }
72 };
73 return [
74 wrappedFn,
75 {
76 start: () => {
77 last = +new Date();
78 },
79 stop: () => {
80 last = undefined;
81 if (timer) {
82 clearTimeout(timer);
83 timer = undefined;
84 }
85 },
86 }
87 ];
88}
89
Brian Sheedy17b1a452022-04-14 17:19:11 +000090function byteSize(s) {
91 return new Blob([s]).size;
92}
93
Austin Eng1cdea902022-03-24 00:21:55 +000094async function setupWebsocket(port) {
95 socket = new WebSocket('ws://127.0.0.1:' + port)
Austin Eng20461632023-05-03 23:36:12 +000096 socket.addEventListener('open', () => {
97 socket.send('{"type":"CONNECTION_ACK"}');
98 });
Austin Eng1cdea902022-03-24 00:21:55 +000099 socket.addEventListener('message', runCtsTestViaSocket);
100}
101
102async function runCtsTestViaSocket(event) {
103 let input = JSON.parse(event.data);
104 runCtsTest(input['q'], input['w']);
105}
106
Austin Eng92b32e82022-11-21 15:16:51 +0000107dataCache.setStore({
108 load: async (path) => {
109 return await (await fetch(`/third_party/webgpu-cts/cache/data/${path}`)).text();
110 }
111});
112
Austin Engcaa9bae2022-08-18 22:49:10 +0000113// Make a rate-limited version `sendMessageTestHeartbeat` that executes
114// at most once every 500 ms.
115const [sendHeartbeat, {
116 start: beginHeartbeatScope,
117 stop: endHeartbeatScope
118}] = rateLimited(sendMessageTestHeartbeat, 500);
119
120function wrapPromiseWithHeartbeat(prototype, key) {
121 const old = prototype[key];
122 prototype[key] = function (...args) {
123 return new Promise((resolve, reject) => {
124 // Send the heartbeat both before and after resolve/reject
125 // so that the heartbeat is sent ahead of any potentially
126 // long-running synchronous code awaiting the Promise.
127 old.call(this, ...args)
128 .then(val => { sendHeartbeat(); resolve(val) })
129 .catch(err => { sendHeartbeat(); reject(err) })
130 .finally(sendHeartbeat);
131 });
132 }
133}
134
135wrapPromiseWithHeartbeat(GPU.prototype, 'requestAdapter');
136wrapPromiseWithHeartbeat(GPUAdapter.prototype, 'requestAdapterInfo');
137wrapPromiseWithHeartbeat(GPUAdapter.prototype, 'requestDevice');
138wrapPromiseWithHeartbeat(GPUDevice.prototype, 'createRenderPipelineAsync');
139wrapPromiseWithHeartbeat(GPUDevice.prototype, 'createComputePipelineAsync');
140wrapPromiseWithHeartbeat(GPUDevice.prototype, 'popErrorScope');
141wrapPromiseWithHeartbeat(GPUQueue.prototype, 'onSubmittedWorkDone');
142wrapPromiseWithHeartbeat(GPUBuffer.prototype, 'mapAsync');
James Price55509fa2023-03-10 11:06:36 +0000143wrapPromiseWithHeartbeat(GPUShaderModule.prototype, 'getCompilationInfo');
Austin Engcaa9bae2022-08-18 22:49:10 +0000144
Austin Engb00c50e2022-08-26 22:34:27 +0000145globalTestConfig.testHeartbeatCallback = sendHeartbeat;
Austin Enge0cbb0c2022-09-13 22:16:42 +0000146globalTestConfig.noRaceWithRejectOnTimeout = true;
Austin Engcaa9bae2022-08-18 22:49:10 +0000147
Ben Clayton33bfc9882023-01-05 21:44:37 +0000148// FXC is very slow to compile unrolled const-eval loops, where the metal shader
149// compiler (Intel GPU) is very slow to compile rolled loops. Intel drivers for
150// linux may also suffer the same performance issues, so unroll const-eval loops
151// if we're not running on Windows.
152if (navigator.userAgent.indexOf("Windows") !== -1) {
153 globalTestConfig.unrollConstEvalLoops = true;
154}
155
Gregg Tavarese1d87cd2023-06-29 23:36:25 +0000156// MAINTENANCE_TODO(gman): remove use_worker since you can use worker=1 instead
157async function runCtsTest(queryString, use_worker) {
158 const { queries, options } = parseSearchParamLikeWithCTSOptions(queryString);
159 const workerEnabled = use_worker || options.worker;
160 const worker = workerEnabled ? new TestWorker(options) : undefined;
Austin Eng1cdea902022-03-24 00:21:55 +0000161
162 const loader = new DefaultTestFileLoader();
Gregg Tavarese1d87cd2023-06-29 23:36:25 +0000163 const filterQuery = parseQuery(queries[0]);
Austin Eng1cdea902022-03-24 00:21:55 +0000164 const testcases = await loader.loadCases(filterQuery);
165
Gregg Tavarese1d87cd2023-06-29 23:36:25 +0000166 const { compatibility, powerPreference } = options;
167 globalTestConfig.compatibility = compatibility;
168 if (powerPreference || compatibility) {
169 setDefaultRequestAdapterOptions({
170 ...(powerPreference && { powerPreference }),
171 // MAINTENANCE_TODO(gman): Change this to whatever the option ends up being
172 ...(compatibility && { compatibilityMode: true }),
173 });
174 }
175
Austin Eng1cdea902022-03-24 00:21:55 +0000176 const expectations = [];
177
178 const log = new Logger();
179
180 for (const testcase of testcases) {
181 const name = testcase.query.toString();
182
183 const wpt_fn = async () => {
Brian Sheedy06535012022-08-11 14:39:51 +0000184 sendMessageTestStarted();
Austin Eng1cdea902022-03-24 00:21:55 +0000185 const [rec, res] = log.record(name);
Austin Engcaa9bae2022-08-18 22:49:10 +0000186
187 beginHeartbeatScope();
Austin Eng1cdea902022-03-24 00:21:55 +0000188 if (worker) {
Austin Engb00c50e2022-08-26 22:34:27 +0000189 await worker.run(rec, name, expectations);
Austin Eng1cdea902022-03-24 00:21:55 +0000190 } else {
Austin Engb00c50e2022-08-26 22:34:27 +0000191 await testcase.run(rec, expectations);
Austin Eng1cdea902022-03-24 00:21:55 +0000192 }
Austin Engcaa9bae2022-08-18 22:49:10 +0000193 endHeartbeatScope();
Austin Eng1cdea902022-03-24 00:21:55 +0000194
Brian Sheedy06535012022-08-11 14:39:51 +0000195 sendMessageTestStatus(res.status, res.timems);
Austin Engcaa9bae2022-08-18 22:49:10 +0000196 sendMessageTestLog(res.logs);
Brian Sheedy06535012022-08-11 14:39:51 +0000197 sendMessageTestFinished();
Austin Eng1cdea902022-03-24 00:21:55 +0000198 };
199 await wpt_fn();
200 }
201}
202
Brian Sheedy06535012022-08-11 14:39:51 +0000203function splitLogsForPayload(fullLogs) {
204 let logPieces = [fullLogs]
205 // Split the log pieces until they all are guaranteed to fit into a
206 // websocket payload.
207 while (true) {
208 let tempLogPieces = []
209 for (const piece of logPieces) {
210 if (byteSize(piece) > LOGS_MAX_BYTES) {
211 let midpoint = Math.floor(piece.length / 2);
212 tempLogPieces.push(piece.substring(0, midpoint));
213 tempLogPieces.push(piece.substring(midpoint));
214 } else {
215 tempLogPieces.push(piece)
216 }
217 }
218 // Didn't make any changes - all pieces are under the size limit.
219 if (logPieces.every((value, index) => value == tempLogPieces[index])) {
220 break;
221 }
222 logPieces = tempLogPieces;
223 }
224 return logPieces
225}
226
227function sendMessageTestStarted() {
Austin Engcaa9bae2022-08-18 22:49:10 +0000228 socket.send('{"type":"TEST_STARTED"}');
229}
230
231function sendMessageTestHeartbeat() {
232 socket.send('{"type":"TEST_HEARTBEAT"}');
Brian Sheedy06535012022-08-11 14:39:51 +0000233}
234
235function sendMessageTestStatus(status, jsDurationMs) {
Ben Clayton33bfc9882023-01-05 21:44:37 +0000236 socket.send(JSON.stringify({
237 'type': 'TEST_STATUS',
238 'status': status,
239 'js_duration_ms': jsDurationMs
240 }));
Brian Sheedy06535012022-08-11 14:39:51 +0000241}
242
Austin Engcaa9bae2022-08-18 22:49:10 +0000243function sendMessageTestLog(logs) {
244 splitLogsForPayload((logs ?? []).map(prettyPrintLog).join('\n\n'))
245 .forEach((piece) => {
246 socket.send(JSON.stringify({
247 'type': 'TEST_LOG',
248 'log': piece
249 }));
250 });
Brian Sheedy06535012022-08-11 14:39:51 +0000251}
252
253function sendMessageTestFinished() {
Austin Engcaa9bae2022-08-18 22:49:10 +0000254 socket.send('{"type":"TEST_FINISHED"}');
Brian Sheedy06535012022-08-11 14:39:51 +0000255}
256
Austin Eng1cdea902022-03-24 00:21:55 +0000257window.runCtsTest = runCtsTest;
258window.setupWebsocket = setupWebsocket