Files
yawn/core/profile.test.js
T

123 lines
4.0 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { Worker } from "node:worker_threads";
import { SharedProfile } from "./profile.js";
import { YawnCore } from "./index.js";
test("shared profiles replace unread samples and exclude overlapping reads/writes", () => {
const writer = new SharedProfile(new SharedArrayBuffer(512));
const reader = new SharedProfile(writer.buffer);
assert.equal(reader.read(), undefined);
writer.write(JSON.stringify({ frame: 1, name: "GPU → SAB" }));
writer.write(JSON.stringify({ frame: 2, name: "light + opaque" }));
assert.deepEqual(reader.read(), { frame: 2, name: "light + opaque" });
assert.equal(reader.read(), undefined);
Atomics.store(writer.header, 0, 1);
assert.equal(writer.write('{"frame":3}'), false);
assert.equal(reader.read(), undefined);
Atomics.store(writer.header, 0, 0);
assert.equal(writer.write(" ".repeat(513)), false);
assert.equal(writer.write('{"frame":4}'), true);
writer.clear();
assert.equal(reader.read(), undefined);
});
test("concurrent shared profile publication never mixes samples", async () => {
const reader = new SharedProfile();
const done = new Int32Array(new SharedArrayBuffer(4));
const worker = new Worker(
`
const {workerData} = require('node:worker_threads');
(async () => {
const {SharedProfile} = await import(workerData.module);
const writer = new SharedProfile(workerData.buffer);
for (let frame = 1; frame <= 2000; frame++) {
const json = JSON.stringify({frame, values: Array(128).fill(frame * 3 + 7), name: 'GPU → SAB'});
while (!writer.write(json)) await new Promise(r => setImmediate(r));
}
Atomics.store(new Int32Array(workerData.done), 0, 1);
})();
`,
{
eval: true,
workerData: {
module: new URL("./profile.js", import.meta.url).href,
buffer: reader.buffer,
done: done.buffer,
},
},
);
const exit = new Promise((resolve, reject) => {
worker.once("error", reject);
worker.once("exit", (code) =>
code ? reject(new Error(`Worker exit ${code}`)) : resolve(),
);
});
try {
let last = 0;
while (!Atomics.load(done, 0) || Atomics.load(reader.header, 0) === 2) {
const sample = reader.read();
if (sample) {
assert.ok(sample.frame > last);
assert.deepEqual(sample.values, Array(128).fill(sample.frame * 3 + 7));
assert.equal(sample.name, "GPU → SAB");
last = sample.frame;
}
await new Promise((r) => setImmediate(r));
}
await exit;
assert.equal(last, 2000);
} finally {
await worker.terminate();
}
});
test("onProfile reads SAB without result messages and stops on unsubscribe/dispose", async (t) => {
t.mock.timers.enable({ apis: ["setInterval"] });
const messages = [];
let receive;
let writer;
const worker = {
addEventListener(type, listener) {
if (type === "message") receive = listener;
},
postMessage(message) {
messages.push(message.type);
writer = new SharedProfile(message.profileBuffer);
queueMicrotask(() =>
receive({
data: {
request: message.request,
result: {
buffer: new SharedArrayBuffer(64),
features: [],
rows: [],
},
},
}),
);
},
terminate() {},
};
const core = new YawnCore({}, { workerFactory: () => worker });
await core.ready;
const samples = [];
const stop = core.onProfile((sample) => samples.push(sample));
writer.write('{"frame":9,"milliseconds":1.25}');
t.mock.timers.tick(99);
assert.equal(samples.length, 0);
t.mock.timers.tick(1);
assert.deepEqual(samples, [{ frame: 9, milliseconds: 1.25 }]);
t.mock.timers.tick(100);
assert.equal(samples.length, 1);
assert.deepEqual(messages, ["init"]);
stop();
writer.write('{"frame":10}');
t.mock.timers.tick(100);
assert.equal(samples.length, 1);
core.onProfile((sample) => samples.push(sample));
core.dispose();
t.mock.timers.tick(100);
assert.equal(samples.length, 1);
});