UNPKG

rc-js-util

Version:

A collection of TS and C++ utilities to help writing performant and correct applications, achieved through strict typing and (removable) invariant checking.

150 lines (125 loc) 5.75 kB
import utilTestModule from "../../external/test-module.mjs"; import { Test_setDefaultFlags } from "../../test-util/test_set-default-flags.js"; import { getTestModuleOptions, TestGarbageCollector } from "../../test-util/test-utils.js"; import { type IErrorExclusions, SanitizedEmscriptenTestModule } from "../emscripten/sanitized-emscripten-test-module.js"; import { IJsUtilBindings } from "../i-js-util-bindings.js"; import { EWorkerPoolOverflowMode, type IWorkerPool, WorkerPool, WorkerPoolErrorCause } from "./worker-pool.js"; import { nullPtr } from "../emscripten/null-pointer.js"; import { promisePoll } from "../../promise/impl/promise-poll.js"; import { _Debug } from "../../debug/_debug.js"; import type { ITestOnlyBindings } from "../i-test-only-bindings.js"; import { NestedError } from "../../error-handling/nested-error.js"; describe("=> WorkerPool", () => { const testModule = new SanitizedEmscriptenTestModule<IJsUtilBindings, ITestOnlyBindings>( utilTestModule, getTestModuleOptions(), ); beforeEach(async () => { Test_setDefaultFlags(); await testModule.initialize(); }); afterEach(() => { testModule.endEmscriptenProgram(); }); it("| executes jobs on the workers", async () => { // manually verified using dev tools that this is loading the web workers evenly const pool = WorkerPool.createRoundRobin(testModule.wrapper, { workerCount: 4, queueSize: 2000 }, testModule.wrapper.rootNode); expect(pool.pointer).not.toBe(nullPtr); expect(testModule.wrapper.instance.fakeWorkerJob_getTickCount()).toBe(0); expect(await pool.start()).toBe(4); expect(pool.isRunning()).toBe(true); for (let i = 0; i < 1e4; ++i) { addTestJob(testModule, pool, false); } pool.setBatchEnd(); await promisePoll(() => pool.isBatchDone()).getPromise(); expect(testModule.wrapper.instance.fakeWorkerJob_getTickCount()).toBe(1e4); await pool.stop(); // ignore "error" emitted by emscripten around joining on the main thread testModule.runWithDisabledErrors(disabledErrors, () => testModule.wrapper.rootNode.getLinked().unlinkAll()); }); it("| allows for cancellation of jobs", async () => { const pool = WorkerPool.createRoundRobin( testModule.wrapper, { workerCount: 1, queueSize: 0xFFFF, overflowMode: EWorkerPoolOverflowMode.Throw }, testModule.wrapper.rootNode, ); expect(pool.pointer).not.toBe(nullPtr); expect(testModule.wrapper.instance.fakeWorkerJob_getTickCount()).toBe(0); expect(await pool.start()).toBe(1); for (let i = 0; i < 1e4; ++i) { addTestJob(testModule, pool, true); } pool.setBatchEnd(); pool.invalidateBatch(); await promisePoll(() => pool.areWorkersSynced()).getPromise(); await promisePoll(() => pool.isBatchDone()).getPromise(); // allow about 5 seconds (absurdly long) for the jobs to be added (each job takes > 20 ms to run, ~20 s total) expect(testModule.wrapper.instance.fakeWorkerJob_getTickCount()).toBeLessThan(2.5e3); await pool.stop(); // ignore "error" emitted by emscripten around joining on the main thread testModule.runWithDisabledErrors(disabledErrors, () => testModule.wrapper.rootNode.getLinked().unlinkAll()); }); it("| errors when not released", async () => { if (!_BUILD.DEBUG || !TestGarbageCollector.isAvailable) { return pending("Test not available in this environment"); } const spy = spyOn(_Debug, "error"); // "forget" to use the return WorkerPool.createRoundRobin(testModule.wrapper, { workerCount: 4, queueSize: 2000 }, testModule.wrapper.rootNode); expect(await TestGarbageCollector.testFriendlyGc()).toBeGreaterThan(0); expect(spy).toHaveBeenCalledWith(jasmine.stringMatching("A shared object was leaked")); testModule.wrapper.rootNode.getLinked().unlinkAll(); }); it("| throws when a job overflows the queue and the mode is to throw", async () => { const pool = WorkerPool.createRoundRobin( testModule.wrapper, { workerCount: 1, queueSize: 1, overflowMode: EWorkerPoolOverflowMode.Throw }, testModule.wrapper.rootNode, ); expect(pool.pointer).not.toBe(nullPtr); expect(testModule.wrapper.instance.fakeWorkerJob_getTickCount()).toBe(0); expect(await pool.start()).toBe(1); expect((): void => { for (let i = 0; i < 10; ++i) { addTestJob(testModule, pool, true); } }).toThrowMatching((err): boolean => { return NestedError.normalizeError(err).causedBy === WorkerPoolErrorCause.overflow; }); await pool.stop(); // ignore "error" emitted by emscripten around joining on the main thread testModule.runWithDisabledErrors(disabledErrors, () => testModule.wrapper.rootNode.getLinked().unlinkAll()); }); }); function addTestJob ( testModule: SanitizedEmscriptenTestModule<IJsUtilBindings, ITestOnlyBindings>, pool: IWorkerPool, goSlow: boolean, ) { const jobPtr = testModule.wrapper.instance.fakeWorkerJob_createJob(goSlow); expect(jobPtr).not.toEqual(nullPtr); if (jobPtr !== nullPtr) { pool.addJob(jobPtr); } } const disabledErrors: IErrorExclusions = { // while true, undefined behavior is even worse... allow shutdown to be the special case exception startsWith: ["Blocking on the main thread is very dangerous"] };