2022-03-24 00:21:55 +00:00
|
|
|
// Copyright 2022 The Dawn Authors
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
import { DefaultTestFileLoader } from '../third_party/webgpu-cts/src/common/internal/file_loader.js';
|
|
|
|
import { prettyPrintLog } from '../third_party/webgpu-cts/src/common/internal/logging/log_message.js';
|
|
|
|
import { Logger } from '../third_party/webgpu-cts/src/common/internal/logging/logger.js';
|
|
|
|
import { parseQuery } from '../third_party/webgpu-cts/src/common/internal/query/parseQuery.js';
|
|
|
|
|
|
|
|
import { TestWorker } from '../third_party/webgpu-cts/src/common/runtime/helper/test_worker.js';
|
|
|
|
|
2022-04-14 17:19:11 +00:00
|
|
|
// The Python-side websockets library has a max payload size of 72638. Set the
|
|
|
|
// max allowable logs size in a single payload to a bit less than that.
|
|
|
|
const LOGS_MAX_BYTES = 72000;
|
|
|
|
|
2022-03-24 00:21:55 +00:00
|
|
|
var socket;
|
|
|
|
|
2022-08-18 22:49:10 +00:00
|
|
|
// Returns a wrapper around `fn` which gets called at most once every `intervalMs`.
|
|
|
|
// If the wrapper is called when `fn` was called too recently, `fn` is scheduled to
|
|
|
|
// be called later in the future after the interval passes.
|
|
|
|
// Returns [ wrappedFn, {start, stop}] where wrappedFn is the rate-limited function,
|
|
|
|
// and start/stop control whether or not the function is enabled. If it is stopped, calls
|
|
|
|
// to the fn will no-op. If it is started, calls will be rate-limited, starting from
|
|
|
|
// the time `start` is called.
|
|
|
|
function rateLimited(fn, intervalMs) {
|
|
|
|
let last = undefined;
|
|
|
|
let timer = undefined;
|
|
|
|
const wrappedFn = (...args) => {
|
|
|
|
if (timer !== undefined || last === undefined) {
|
|
|
|
// If there is already a fn call scheduled, or the function is
|
|
|
|
// not enabled, return.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Get the current time as a number.
|
|
|
|
const now = +new Date();
|
|
|
|
const diff = now - last;
|
|
|
|
if (diff >= intervalMs) {
|
|
|
|
// Clear the timer, if there was one. This could happen if a timer
|
|
|
|
// is scheduled, but it never runs due to long-running synchronous
|
|
|
|
// code.
|
|
|
|
if (timer) {
|
|
|
|
clearTimeout(timer);
|
|
|
|
timer = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Call the function.
|
|
|
|
last = now;
|
|
|
|
fn(...args);
|
|
|
|
} else if (timer === undefined) {
|
|
|
|
// Otherwise, we have called `fn` too recently.
|
|
|
|
// Schedule a future call.
|
|
|
|
timer = setTimeout(() => {
|
|
|
|
// Clear the timer to indicate nothing is scheduled.
|
|
|
|
timer = undefined;
|
|
|
|
last = +new Date();
|
|
|
|
fn(...args);
|
|
|
|
}, intervalMs - diff + 1);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return [
|
|
|
|
wrappedFn,
|
|
|
|
{
|
|
|
|
start: () => {
|
|
|
|
last = +new Date();
|
|
|
|
},
|
|
|
|
stop: () => {
|
|
|
|
last = undefined;
|
|
|
|
if (timer) {
|
|
|
|
clearTimeout(timer);
|
|
|
|
timer = undefined;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2022-04-14 17:19:11 +00:00
|
|
|
function byteSize(s) {
|
|
|
|
return new Blob([s]).size;
|
|
|
|
}
|
|
|
|
|
2022-03-24 00:21:55 +00:00
|
|
|
async function setupWebsocket(port) {
|
|
|
|
socket = new WebSocket('ws://127.0.0.1:' + port)
|
|
|
|
socket.addEventListener('message', runCtsTestViaSocket);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function runCtsTestViaSocket(event) {
|
|
|
|
let input = JSON.parse(event.data);
|
|
|
|
runCtsTest(input['q'], input['w']);
|
|
|
|
}
|
|
|
|
|
2022-08-18 22:49:10 +00:00
|
|
|
// Make a rate-limited version `sendMessageTestHeartbeat` that executes
|
|
|
|
// at most once every 500 ms.
|
|
|
|
const [sendHeartbeat, {
|
|
|
|
start: beginHeartbeatScope,
|
|
|
|
stop: endHeartbeatScope
|
|
|
|
}] = rateLimited(sendMessageTestHeartbeat, 500);
|
|
|
|
|
|
|
|
function wrapPromiseWithHeartbeat(prototype, key) {
|
|
|
|
const old = prototype[key];
|
|
|
|
prototype[key] = function (...args) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
// Send the heartbeat both before and after resolve/reject
|
|
|
|
// so that the heartbeat is sent ahead of any potentially
|
|
|
|
// long-running synchronous code awaiting the Promise.
|
|
|
|
old.call(this, ...args)
|
|
|
|
.then(val => { sendHeartbeat(); resolve(val) })
|
|
|
|
.catch(err => { sendHeartbeat(); reject(err) })
|
|
|
|
.finally(sendHeartbeat);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
wrapPromiseWithHeartbeat(GPU.prototype, 'requestAdapter');
|
|
|
|
wrapPromiseWithHeartbeat(GPUAdapter.prototype, 'requestAdapterInfo');
|
|
|
|
wrapPromiseWithHeartbeat(GPUAdapter.prototype, 'requestDevice');
|
|
|
|
wrapPromiseWithHeartbeat(GPUDevice.prototype, 'createRenderPipelineAsync');
|
|
|
|
wrapPromiseWithHeartbeat(GPUDevice.prototype, 'createComputePipelineAsync');
|
|
|
|
wrapPromiseWithHeartbeat(GPUDevice.prototype, 'popErrorScope');
|
|
|
|
wrapPromiseWithHeartbeat(GPUQueue.prototype, 'onSubmittedWorkDone');
|
|
|
|
wrapPromiseWithHeartbeat(GPUBuffer.prototype, 'mapAsync');
|
|
|
|
wrapPromiseWithHeartbeat(GPUShaderModule.prototype, 'compilationInfo');
|
|
|
|
|
|
|
|
// Make a wrapper around TestCaseRecorder that sends a heartbeat before any
|
|
|
|
// recording operations.
|
|
|
|
function makeRecorderWithHeartbeat(rec) {
|
|
|
|
return new Proxy(rec, {
|
|
|
|
// Create a wrapper around all methods of the TestCaseRecorder.
|
|
|
|
get(target, prop, receiver) {
|
|
|
|
const orig = Reflect.get(target, prop, receiver);
|
|
|
|
if (typeof orig !== 'function') {
|
|
|
|
// Return the original property if it is not a function.
|
|
|
|
return orig;
|
|
|
|
}
|
|
|
|
return (...args) => {
|
|
|
|
sendHeartbeat();
|
|
|
|
return orig.call(receiver, ...args)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-03-24 00:21:55 +00:00
|
|
|
async function runCtsTest(query, use_worker) {
|
|
|
|
const workerEnabled = use_worker;
|
|
|
|
const worker = workerEnabled ? new TestWorker(false) : undefined;
|
|
|
|
|
|
|
|
const loader = new DefaultTestFileLoader();
|
|
|
|
const filterQuery = parseQuery(query);
|
|
|
|
const testcases = await loader.loadCases(filterQuery);
|
|
|
|
|
|
|
|
const expectations = [];
|
|
|
|
|
|
|
|
const log = new Logger();
|
|
|
|
|
|
|
|
for (const testcase of testcases) {
|
|
|
|
const name = testcase.query.toString();
|
|
|
|
|
|
|
|
const wpt_fn = async () => {
|
2022-08-11 14:39:51 +00:00
|
|
|
sendMessageTestStarted();
|
2022-03-24 00:21:55 +00:00
|
|
|
const [rec, res] = log.record(name);
|
2022-08-18 22:49:10 +00:00
|
|
|
const recWithHeartbeat = makeRecorderWithHeartbeat(rec);
|
|
|
|
|
|
|
|
beginHeartbeatScope();
|
2022-03-24 00:21:55 +00:00
|
|
|
if (worker) {
|
2022-08-18 22:49:10 +00:00
|
|
|
await worker.run(recWithHeartbeat, name, expectations);
|
2022-03-24 00:21:55 +00:00
|
|
|
} else {
|
2022-08-18 22:49:10 +00:00
|
|
|
await testcase.run(recWithHeartbeat, expectations);
|
2022-03-24 00:21:55 +00:00
|
|
|
}
|
2022-08-18 22:49:10 +00:00
|
|
|
endHeartbeatScope();
|
2022-03-24 00:21:55 +00:00
|
|
|
|
2022-08-11 14:39:51 +00:00
|
|
|
sendMessageTestStatus(res.status, res.timems);
|
2022-08-18 22:49:10 +00:00
|
|
|
sendMessageTestLog(res.logs);
|
2022-08-11 14:39:51 +00:00
|
|
|
sendMessageTestFinished();
|
2022-03-24 00:21:55 +00:00
|
|
|
};
|
|
|
|
await wpt_fn();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-11 14:39:51 +00:00
|
|
|
function splitLogsForPayload(fullLogs) {
|
|
|
|
let logPieces = [fullLogs]
|
|
|
|
// Split the log pieces until they all are guaranteed to fit into a
|
|
|
|
// websocket payload.
|
|
|
|
while (true) {
|
|
|
|
let tempLogPieces = []
|
|
|
|
for (const piece of logPieces) {
|
|
|
|
if (byteSize(piece) > LOGS_MAX_BYTES) {
|
|
|
|
let midpoint = Math.floor(piece.length / 2);
|
|
|
|
tempLogPieces.push(piece.substring(0, midpoint));
|
|
|
|
tempLogPieces.push(piece.substring(midpoint));
|
|
|
|
} else {
|
|
|
|
tempLogPieces.push(piece)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Didn't make any changes - all pieces are under the size limit.
|
|
|
|
if (logPieces.every((value, index) => value == tempLogPieces[index])) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
logPieces = tempLogPieces;
|
|
|
|
}
|
|
|
|
return logPieces
|
|
|
|
}
|
|
|
|
|
|
|
|
function sendMessageTestStarted() {
|
2022-08-18 22:49:10 +00:00
|
|
|
socket.send('{"type":"TEST_STARTED"}');
|
|
|
|
}
|
|
|
|
|
|
|
|
function sendMessageTestHeartbeat() {
|
|
|
|
socket.send('{"type":"TEST_HEARTBEAT"}');
|
2022-08-11 14:39:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function sendMessageTestStatus(status, jsDurationMs) {
|
|
|
|
socket.send(JSON.stringify({'type': 'TEST_STATUS',
|
|
|
|
'status': status,
|
|
|
|
'js_duration_ms': jsDurationMs}));
|
|
|
|
}
|
|
|
|
|
2022-08-18 22:49:10 +00:00
|
|
|
function sendMessageTestLog(logs) {
|
|
|
|
splitLogsForPayload((logs ?? []).map(prettyPrintLog).join('\n\n'))
|
|
|
|
.forEach((piece) => {
|
|
|
|
socket.send(JSON.stringify({
|
|
|
|
'type': 'TEST_LOG',
|
|
|
|
'log': piece
|
|
|
|
}));
|
|
|
|
});
|
2022-08-11 14:39:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function sendMessageTestFinished() {
|
2022-08-18 22:49:10 +00:00
|
|
|
socket.send('{"type":"TEST_FINISHED"}');
|
2022-08-11 14:39:51 +00:00
|
|
|
}
|
|
|
|
|
2022-03-24 00:21:55 +00:00
|
|
|
window.runCtsTest = runCtsTest;
|
|
|
|
window.setupWebsocket = setupWebsocket
|