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
text/typescript
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"]
};