Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"automock",
"bitauth",
"bitjson",
"BNR",
"Bowser",
"cimg",
"circleci",
Expand All @@ -33,18 +34,22 @@
"libauth",
"mindmeld",
"mkdir",
"MULT",
"multistream",
"ndarray",
"Onnx",
"onnxruntime",
"preconfigured",
"prettierignore",
"retuned",
"rohit",
"sandboxed",
"SSDK",
"tfjs",
"trackingid",
"transcoding",
"transpiled",
"tunables",
"typedoc",
"Unregisters",
"untracked",
Expand Down
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
rootDir: './src',
transform: {
'^.+\\.tsx?$': 'ts-jest',
'\\.worker\\.js$': '<rootDir>/../jest.raw-transform.js',
},
};
13 changes: 13 additions & 0 deletions jest.raw-transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
/**
* Turns a *.worker.js file into a string module, matching how rollup-plugin-string
* inlines it at build time. Since tests swap in a fake Worker, the worker file's
* contents are never run — we only ever need it as a string, not as runnable code.
*
* @param sourceText - The raw worker file contents.
* @returns The transformed module source for Jest.
*/
process(sourceText) {
return { code: `module.exports = ${JSON.stringify(sourceText)};` };
},
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"rollup": "^2.63.0",
"rollup-plugin-dts": "^4.1.0",
"rollup-plugin-execute": "^1.1.1",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-typescript2": "^0.31.1",
"semantic-release": "^19.0.2",
"ts-jest": "^27.1.2",
Expand Down
12 changes: 11 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import dts from 'rollup-plugin-dts';
import execute from 'rollup-plugin-execute';
import { string } from 'rollup-plugin-string';
import typescript from 'rollup-plugin-typescript2';

// Bundle *.worker.js as a string so the probe can start it from a Blob URL
// with no separate file to load.
const workerString = string({ include: '**/*.worker.js' });

export default [
{
input: 'src/index.ts',
Expand All @@ -18,6 +23,7 @@ export default [
},
],
plugins: [
workerString,
typescript({ useTsconfigDeclarationDir: true }),
resolve({ browser: true, extensions: ['.js', '.ts'] }),
commonjs(),
Expand All @@ -32,7 +38,11 @@ export default [
format: 'es',
file: 'dist/types.d.ts',
},
plugins: [dts(), execute(['rm -f dist/types/*', 'mv dist/types.d.ts dist/types/index.d.ts'])],
plugins: [
workerString,
dts(),
execute(['rm -f dist/types/*', 'mv dist/types.d.ts dist/types/index.d.ts']),
],
watch: true,
},
];
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './wasm-runtime-probe';
export * from './browser-info';
export * from './cpu-info';
export * from './system-info';
Expand Down
176 changes: 176 additions & 0 deletions src/wasm-runtime-probe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { WasmRuntimeProbe, WasmRuntimeStatus } from './wasm-runtime-probe';
import { CapabilityState } from './web-capabilities';

interface FakeReply {
ok: boolean;
wasmMs?: number;
jsMs?: number;
}

// Shared state that controls how the mock Worker behaves in the current test.
let workerReply: FakeReply | undefined;
let workerConstructCount = 0;

/**
* Fake Worker for jsdom, which has no real one. On postMessage it replies immediately with whatever
* {@link workerReply} the test set, or stays silent so we can test the timeout path.
*/
class MockWorker {
onmessage: ((event: { data: FakeReply }) => void) | null = null;

onerror: (() => void) | null = null;

/**
* Counts how many workers were created, so the caching test can check it.
*/
constructor() {
workerConstructCount += 1;
}

/**
* Sends the configured reply back to the probe, or nothing if none is set.
*/
postMessage(): void {
if (workerReply && this.onmessage) {
this.onmessage({ data: workerReply });
}
}

/**
* Does nothing; just matches the real Worker API.
*/
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function
terminate(): void {}
}

describe('WasmRuntimeProbe', () => {
const originalWebAssembly = globalThis.WebAssembly;

beforeEach(() => {
// Clear the per-page cache so each test starts fresh (private, reached via a cast).
(WasmRuntimeProbe as unknown as { cachedResult?: unknown }).cachedResult = undefined;
workerReply = undefined;
workerConstructCount = 0;
});

afterEach(() => {
(globalThis as { WebAssembly?: typeof WebAssembly }).WebAssembly = originalWebAssembly;
});

it('should return DISABLED when WebAssembly is hard-disabled', async () => {
expect.assertions(5);
delete (globalThis as { WebAssembly?: typeof WebAssembly }).WebAssembly;

const result = await WasmRuntimeProbe.check();

expect(result.status).toBe(WasmRuntimeStatus.DISABLED);
expect(result.capability).toBe(CapabilityState.NOT_CAPABLE);
expect(result.ratio).toBeNull();
expect(result.wasmMs).toBeNull();
expect(result.jsMs).toBeNull();
});

it('should return UNKNOWN when Web Workers are not available', async () => {
expect.assertions(2);

const result = await WasmRuntimeProbe.check();

expect(result.status).toBe(WasmRuntimeStatus.UNKNOWN);
expect(result.capability).toBe(CapabilityState.UNKNOWN);
});

describe('worker benchmark', () => {
beforeEach(() => {
Object.defineProperty(globalThis, 'Worker', {
writable: true,
configurable: true,
value: MockWorker,
});
Object.defineProperty(URL, 'createObjectURL', {
writable: true,
configurable: true,
value: jest.fn(() => 'blob:mock'),
});
Object.defineProperty(URL, 'revokeObjectURL', {
writable: true,
configurable: true,
value: jest.fn(),
});
});

afterEach(() => {
delete (globalThis as { Worker?: unknown }).Worker;
delete (URL as { createObjectURL?: unknown }).createObjectURL;
delete (URL as { revokeObjectURL?: unknown }).revokeObjectURL;
});

it('should return SLOW when the wasm/js ratio is below the threshold', async () => {
expect.assertions(5);
workerReply = { ok: true, wasmMs: 25, jsMs: 100 };

const result = await WasmRuntimeProbe.check();

expect(result.status).toBe(WasmRuntimeStatus.SLOW);
expect(result.capability).toBe(CapabilityState.NOT_CAPABLE);
expect(result.ratio).toBe(0.25);
expect(result.wasmMs).toBe(25);
expect(result.jsMs).toBe(100);
});

it('should return OK when the wasm/js ratio is at or above the threshold', async () => {
expect.assertions(5);
workerReply = { ok: true, wasmMs: 110, jsMs: 100 };

const result = await WasmRuntimeProbe.check();

expect(result.status).toBe(WasmRuntimeStatus.OK);
expect(result.capability).toBe(CapabilityState.CAPABLE);
expect(result.ratio).toBe(1.1);
expect(result.wasmMs).toBe(110);
expect(result.jsMs).toBe(100);
});

it('should return UNKNOWN when the worker reports a failure', async () => {
expect.assertions(1);
workerReply = { ok: false };

const result = await WasmRuntimeProbe.check();

expect(result.status).toBe(WasmRuntimeStatus.UNKNOWN);
});

it('should return UNKNOWN when jsMs is not a positive number', async () => {
expect.assertions(1);
workerReply = { ok: true, wasmMs: 10, jsMs: 0 };

const result = await WasmRuntimeProbe.check();

expect(result.status).toBe(WasmRuntimeStatus.UNKNOWN);
});

it('should return UNKNOWN when the worker does not reply before the timeout', async () => {
expect.assertions(1);
jest.useFakeTimers();
workerReply = undefined; // never replies

const promise = WasmRuntimeProbe.check();
jest.advanceTimersByTime(3000);
const result = await promise;

expect(result.status).toBe(WasmRuntimeStatus.UNKNOWN);
jest.useRealTimers();
});

it('should cache the result so repeated calls run the benchmark only once', async () => {
expect.assertions(2);
workerReply = { ok: true, wasmMs: 110, jsMs: 100 };

const first = WasmRuntimeProbe.check();
const second = WasmRuntimeProbe.check();

expect(first).toBe(second);
await first;
expect(workerConstructCount).toBe(1);
});
});
});
Loading
Loading