blob: d02ae3fa35c1bac125067a92459926f519b9a115 [file] [log] [blame]
Austin Engcc2516a2023-10-17 20:57:54 +00001// Copyright 2022 The Dawn & Tint Authors
Austin Eng1cdea902022-03-24 00:21:55 +00002//
Austin Engcc2516a2023-10-17 20:57:54 +00003// Redistribution and use in source and binary forms, with or without
4// modification, are permitted provided that the following conditions are met:
Austin Eng1cdea902022-03-24 00:21:55 +00005//
Austin Engcc2516a2023-10-17 20:57:54 +00006// 1. Redistributions of source code must retain the above copyright notice, this
7// list of conditions and the following disclaimer.
Austin Eng1cdea902022-03-24 00:21:55 +00008//
Austin Engcc2516a2023-10-17 20:57:54 +00009// 2. Redistributions in binary form must reproduce the above copyright notice,
10// this list of conditions and the following disclaimer in the documentation
11// and/or other materials provided with the distribution.
12//
13// 3. Neither the name of the copyright holder nor the names of its
14// contributors may be used to endorse or promote products derived from
15// this software without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Austin Eng1cdea902022-03-24 00:21:55 +000027
Austin Engb00c50e2022-08-26 22:34:27 +000028import { globalTestConfig } from '../third_party/webgpu-cts/src/common/framework/test_config.js';
Austin Eng92b32e82022-11-21 15:16:51 +000029import { dataCache } from '../third_party/webgpu-cts/src/common/framework/data_cache.js';
Ben Clayton2880e5d2023-11-16 17:16:57 +000030import { getResourcePath } from '../third_party/webgpu-cts/src/common/framework/resources.js';
Austin Eng1cdea902022-03-24 00:21:55 +000031import { DefaultTestFileLoader } from '../third_party/webgpu-cts/src/common/internal/file_loader.js';
32import { prettyPrintLog } from '../third_party/webgpu-cts/src/common/internal/logging/log_message.js';
33import { Logger } from '../third_party/webgpu-cts/src/common/internal/logging/logger.js';
34import { parseQuery } from '../third_party/webgpu-cts/src/common/internal/query/parseQuery.js';
Gregg Tavarese1d87cd2023-06-29 23:36:25 +000035import { parseSearchParamLikeWithCTSOptions } from '../third_party/webgpu-cts/src/common/runtime/helper/options.js';
36import { setDefaultRequestAdapterOptions } from '../third_party/webgpu-cts/src/common/util/navigator_gpu.js';
Kai Ninomiyaca296e12024-04-08 21:20:13 +000037import { unreachable } from '../third_party/webgpu-cts/src/common/util/util.js';
Austin Eng1cdea902022-03-24 00:21:55 +000038
Kai Ninomiyaca296e12024-04-08 21:20:13 +000039import { TestDedicatedWorker, TestSharedWorker, TestServiceWorker } from '../third_party/webgpu-cts/src/common/runtime/helper/test_worker.js';
Austin Eng1cdea902022-03-24 00:21:55 +000040
Brian Sheedy17b1a452022-04-14 17:19:11 +000041// The Python-side websockets library has a max payload size of 72638. Set the
42// max allowable logs size in a single payload to a bit less than that.
43const LOGS_MAX_BYTES = 72000;
44
Austin Eng1cdea902022-03-24 00:21:55 +000045var socket;
46
Austin Engcaa9bae2022-08-18 22:49:10 +000047// Returns a wrapper around `fn` which gets called at most once every `intervalMs`.
48// If the wrapper is called when `fn` was called too recently, `fn` is scheduled to
49// be called later in the future after the interval passes.
50// Returns [ wrappedFn, {start, stop}] where wrappedFn is the rate-limited function,
51// and start/stop control whether or not the function is enabled. If it is stopped, calls
52// to the fn will no-op. If it is started, calls will be rate-limited, starting from
53// the time `start` is called.
54function rateLimited(fn, intervalMs) {
55 let last = undefined;
56 let timer = undefined;
57 const wrappedFn = (...args) => {
Austin Eng26ffcd12022-09-13 18:28:31 +000058 if (last === undefined) {
59 // If the function is not enabled, return.
Austin Engcaa9bae2022-08-18 22:49:10 +000060 return;
61 }
62 // Get the current time as a number.
63 const now = +new Date();
64 const diff = now - last;
65 if (diff >= intervalMs) {
66 // Clear the timer, if there was one. This could happen if a timer
67 // is scheduled, but it never runs due to long-running synchronous
68 // code.
69 if (timer) {
70 clearTimeout(timer);
71 timer = undefined;
72 }
73
74 // Call the function.
75 last = now;
76 fn(...args);
77 } else if (timer === undefined) {
78 // Otherwise, we have called `fn` too recently.
79 // Schedule a future call.
80 timer = setTimeout(() => {
81 // Clear the timer to indicate nothing is scheduled.
82 timer = undefined;
83 last = +new Date();
84 fn(...args);
85 }, intervalMs - diff + 1);
86 }
87 };
88 return [
89 wrappedFn,
90 {
91 start: () => {
92 last = +new Date();
93 },
94 stop: () => {
95 last = undefined;
96 if (timer) {
97 clearTimeout(timer);
98 timer = undefined;
99 }
100 },
101 }
102 ];
103}
104
Brian Sheedy17b1a452022-04-14 17:19:11 +0000105function byteSize(s) {
106 return new Blob([s]).size;
107}
108
Austin Eng1cdea902022-03-24 00:21:55 +0000109async function setupWebsocket(port) {
110 socket = new WebSocket('ws://127.0.0.1:' + port)
Austin Eng20461632023-05-03 23:36:12 +0000111 socket.addEventListener('open', () => {
112 socket.send('{"type":"CONNECTION_ACK"}');
113 });
Austin Eng1cdea902022-03-24 00:21:55 +0000114 socket.addEventListener('message', runCtsTestViaSocket);
115}
116
117async function runCtsTestViaSocket(event) {
118 let input = JSON.parse(event.data);
Kai Ninomiyaca296e12024-04-08 21:20:13 +0000119 runCtsTest(input['q']);
Austin Eng1cdea902022-03-24 00:21:55 +0000120}
121
Austin Eng92b32e82022-11-21 15:16:51 +0000122dataCache.setStore({
123 load: async (path) => {
Ben Clayton2880e5d2023-11-16 17:16:57 +0000124 const fullPath = getResourcePath(`cache/${path}`);
125 const response = await fetch(fullPath);
126 if (!response.ok) {
Ben Clayton248bad42023-11-20 22:40:34 +0000127 return Promise.reject(`failed to load cache file: '${fullPath}'
128reason: ${response.statusText}`);
Ben Claytonb8b17ce2023-10-25 23:00:07 +0000129 }
Ben Clayton2880e5d2023-11-16 17:16:57 +0000130 return new Uint8Array(await response.arrayBuffer());
Austin Eng92b32e82022-11-21 15:16:51 +0000131 }
132});
133
Austin Engcaa9bae2022-08-18 22:49:10 +0000134// Make a rate-limited version `sendMessageTestHeartbeat` that executes
135// at most once every 500 ms.
136const [sendHeartbeat, {
137 start: beginHeartbeatScope,
138 stop: endHeartbeatScope
139}] = rateLimited(sendMessageTestHeartbeat, 500);
140
141function wrapPromiseWithHeartbeat(prototype, key) {
142 const old = prototype[key];
143 prototype[key] = function (...args) {
144 return new Promise((resolve, reject) => {
145 // Send the heartbeat both before and after resolve/reject
146 // so that the heartbeat is sent ahead of any potentially
147 // long-running synchronous code awaiting the Promise.
148 old.call(this, ...args)
149 .then(val => { sendHeartbeat(); resolve(val) })
150 .catch(err => { sendHeartbeat(); reject(err) })
151 .finally(sendHeartbeat);
152 });
153 }
154}
155
156wrapPromiseWithHeartbeat(GPU.prototype, 'requestAdapter');
157wrapPromiseWithHeartbeat(GPUAdapter.prototype, 'requestAdapterInfo');
158wrapPromiseWithHeartbeat(GPUAdapter.prototype, 'requestDevice');
159wrapPromiseWithHeartbeat(GPUDevice.prototype, 'createRenderPipelineAsync');
160wrapPromiseWithHeartbeat(GPUDevice.prototype, 'createComputePipelineAsync');
161wrapPromiseWithHeartbeat(GPUDevice.prototype, 'popErrorScope');
162wrapPromiseWithHeartbeat(GPUQueue.prototype, 'onSubmittedWorkDone');
163wrapPromiseWithHeartbeat(GPUBuffer.prototype, 'mapAsync');
James Price55509fa2023-03-10 11:06:36 +0000164wrapPromiseWithHeartbeat(GPUShaderModule.prototype, 'getCompilationInfo');
Austin Engcaa9bae2022-08-18 22:49:10 +0000165
Austin Engb00c50e2022-08-26 22:34:27 +0000166globalTestConfig.testHeartbeatCallback = sendHeartbeat;
Austin Enge0cbb0c2022-09-13 22:16:42 +0000167globalTestConfig.noRaceWithRejectOnTimeout = true;
Austin Engcaa9bae2022-08-18 22:49:10 +0000168
Ben Clayton33bfc9882023-01-05 21:44:37 +0000169// FXC is very slow to compile unrolled const-eval loops, where the metal shader
170// compiler (Intel GPU) is very slow to compile rolled loops. Intel drivers for
171// linux may also suffer the same performance issues, so unroll const-eval loops
172// if we're not running on Windows.
Antonio Maiorano8a15bb12023-09-05 21:47:13 +0000173const isWindows = navigator.userAgent.includes("Windows");
174if (!isWindows) {
Ben Clayton33bfc9882023-01-05 21:44:37 +0000175 globalTestConfig.unrollConstEvalLoops = true;
176}
177
Kai Ninomiya9392c042024-04-09 00:06:52 +0000178let lastOptionsKey, testWorker;
179
Kai Ninomiyaca296e12024-04-08 21:20:13 +0000180async function runCtsTest(queryString) {
Gregg Tavarese1d87cd2023-06-29 23:36:25 +0000181 const { queries, options } = parseSearchParamLikeWithCTSOptions(queryString);
Kai Ninomiya9392c042024-04-09 00:06:52 +0000182
183 // Set up a worker with the options passed into the test, avoiding creating
184 // a new worker if one was already set up for the last test.
185 // In practice, the options probably won't change between tests in a single
186 // invocation of run_gpu_integration_test.py, but this handles if they do.
187 const currentOptionsKey = JSON.stringify(options);
188 if (currentOptionsKey !== lastOptionsKey) {
189 lastOptionsKey = currentOptionsKey;
190 testWorker =
191 options.worker === null ? null :
192 options.worker === 'dedicated' ? new TestDedicatedWorker(options) :
193 options.worker === 'shared' ? new TestSharedWorker(options) :
194 options.worker === 'service' ? new TestServiceWorker(options) :
195 unreachable();
196 }
Austin Eng1cdea902022-03-24 00:21:55 +0000197
198 const loader = new DefaultTestFileLoader();
Gregg Tavarese1d87cd2023-06-29 23:36:25 +0000199 const filterQuery = parseQuery(queries[0]);
Brian Sheedybbcdd3e2023-09-29 23:00:21 +0000200 const testcases = Array.from(await loader.loadCases(filterQuery));
201
202 if (testcases.length === 0) {
203 sendMessageInfraFailure('Did not find matching test');
204 return;
205 }
206 if (testcases.length !== 1) {
207 sendMessageInfraFailure('Found more than 1 test for given query');
208 return;
209 }
210 const testcase = testcases[0];
Austin Eng1cdea902022-03-24 00:21:55 +0000211
Gregg Tavarese1d87cd2023-06-29 23:36:25 +0000212 const { compatibility, powerPreference } = options;
213 globalTestConfig.compatibility = compatibility;
214 if (powerPreference || compatibility) {
215 setDefaultRequestAdapterOptions({
216 ...(powerPreference && { powerPreference }),
217 // MAINTENANCE_TODO(gman): Change this to whatever the option ends up being
218 ...(compatibility && { compatibilityMode: true }),
219 });
220 }
221
Austin Eng1cdea902022-03-24 00:21:55 +0000222 const expectations = [];
223
224 const log = new Logger();
225
Brian Sheedybbcdd3e2023-09-29 23:00:21 +0000226 const name = testcase.query.toString();
Austin Eng1cdea902022-03-24 00:21:55 +0000227
Brian Sheedybbcdd3e2023-09-29 23:00:21 +0000228 const wpt_fn = async () => {
229 sendMessageTestStarted();
230 const [rec, res] = log.record(name);
Austin Engcaa9bae2022-08-18 22:49:10 +0000231
Brian Sheedybbcdd3e2023-09-29 23:00:21 +0000232 beginHeartbeatScope();
Kai Ninomiyaca296e12024-04-08 21:20:13 +0000233 if (testWorker) {
234 await testWorker.run(rec, name, expectations);
Brian Sheedybbcdd3e2023-09-29 23:00:21 +0000235 } else {
236 await testcase.run(rec, expectations);
237 }
238 endHeartbeatScope();
Austin Eng1cdea902022-03-24 00:21:55 +0000239
Brian Sheedybbcdd3e2023-09-29 23:00:21 +0000240 sendMessageTestStatus(res.status, res.timems);
Austin Eng3c0186a2024-06-19 04:26:46 +0000241 if (res.status === 'pass') {
242 // Send an "OK" log. Passing tests don't report logs to Telemetry.
243 sendMessageTestLogOK();
244 } else {
Kai Ninomiyadf5c89a2024-06-29 01:14:29 +0000245 sendMessageTestLog(res.logs ?? []);
Austin Eng3c0186a2024-06-19 04:26:46 +0000246 }
Brian Sheedybbcdd3e2023-09-29 23:00:21 +0000247 sendMessageTestFinished();
248 };
249 await wpt_fn();
Austin Eng1cdea902022-03-24 00:21:55 +0000250}
251
Brian Sheedy06535012022-08-11 14:39:51 +0000252function splitLogsForPayload(fullLogs) {
253 let logPieces = [fullLogs]
254 // Split the log pieces until they all are guaranteed to fit into a
255 // websocket payload.
256 while (true) {
257 let tempLogPieces = []
258 for (const piece of logPieces) {
259 if (byteSize(piece) > LOGS_MAX_BYTES) {
260 let midpoint = Math.floor(piece.length / 2);
261 tempLogPieces.push(piece.substring(0, midpoint));
262 tempLogPieces.push(piece.substring(midpoint));
263 } else {
264 tempLogPieces.push(piece)
265 }
266 }
267 // Didn't make any changes - all pieces are under the size limit.
268 if (logPieces.every((value, index) => value == tempLogPieces[index])) {
269 break;
270 }
271 logPieces = tempLogPieces;
272 }
273 return logPieces
274}
275
276function sendMessageTestStarted() {
Austin Engcaa9bae2022-08-18 22:49:10 +0000277 socket.send('{"type":"TEST_STARTED"}');
278}
279
280function sendMessageTestHeartbeat() {
281 socket.send('{"type":"TEST_HEARTBEAT"}');
Brian Sheedy06535012022-08-11 14:39:51 +0000282}
283
284function sendMessageTestStatus(status, jsDurationMs) {
Ben Clayton33bfc9882023-01-05 21:44:37 +0000285 socket.send(JSON.stringify({
286 'type': 'TEST_STATUS',
287 'status': status,
288 'js_duration_ms': jsDurationMs
289 }));
Brian Sheedy06535012022-08-11 14:39:51 +0000290}
291
Austin Eng3c0186a2024-06-19 04:26:46 +0000292function sendMessageTestLogOK() {
293 socket.send('{"type":"TEST_LOG","log":"OK"}');
294}
295
Kai Ninomiyadf5c89a2024-06-29 01:14:29 +0000296// Logs look like: " - EXPECTATION FAILED: subcase: foobar=2;foo=a;bar=2\n...."
297// or "EXCEPTION: Name!: Message!\nsubcase: fail = true\n..."
298const kSubcaseLogPrefixRegex = /\bsubcase: .*$\n/m;
299
300/** Send logs with the most important log line on the first line as a "summary". */
Austin Engcaa9bae2022-08-18 22:49:10 +0000301function sendMessageTestLog(logs) {
Kai Ninomiyadf5c89a2024-06-29 01:14:29 +0000302 // Find the first log that is max-severity (doesn't have its stack hidden)
303 let summary = '';
304 let details = [];
305 for (const log of logs) {
306 let detail = prettyPrintLog(log);
307 if (summary === '' && log.stackHiddenMessage === undefined) {
308 // First top-severity message (because its stack is not hidden).
309 // Clean up this message and pick it as the summary:
310 summary = ' ' + log.toJSON()
311 .replace(kSubcaseLogPrefixRegex, '')
312 .split('\n')[0] + '\n';
313 // Replace ' - ' with '--> ':
314 detail = '--> ' + detail.slice(4);
315 }
316 details.push(detail);
317 }
318
319 const outLogsString = summary + details.join('\n');
320 splitLogsForPayload(outLogsString)
Austin Engcaa9bae2022-08-18 22:49:10 +0000321 .forEach((piece) => {
322 socket.send(JSON.stringify({
323 'type': 'TEST_LOG',
324 'log': piece
325 }));
326 });
Brian Sheedy06535012022-08-11 14:39:51 +0000327}
328
329function sendMessageTestFinished() {
Austin Engcaa9bae2022-08-18 22:49:10 +0000330 socket.send('{"type":"TEST_FINISHED"}');
Brian Sheedy06535012022-08-11 14:39:51 +0000331}
332
Brian Sheedybbcdd3e2023-09-29 23:00:21 +0000333function sendMessageInfraFailure(message) {
334 socket.send(JSON.stringify({
Ben Claytonb8b17ce2023-10-25 23:00:07 +0000335 'type': 'INFRA_FAILURE',
336 'message': message,
Brian Sheedybbcdd3e2023-09-29 23:00:21 +0000337 }));
338}
339
Austin Eng1cdea902022-03-24 00:21:55 +0000340window.setupWebsocket = setupWebsocket