Files
linuxcncWebWASM/web/worker/sim-scheduler.mjs

93 lines
4.4 KiB
JavaScript

import { fnv1a64 } from "./replay.mjs";
export const SCHEDULER_SCHEMA = "sim_scheduler.v1";
export const SCHEDULER_PHASES = Object.freeze(["reconcile", "task", "tp", "kinematics", "motion", "hal_io", "callbacks", "publish"]);
export const DEFAULT_TICK_BUDGET = 1_000_000;
function call(core, name, returnType, argTypes, args) {
if (typeof core?.ccall === "function") return core.ccall(name, returnType, argTypes, args);
const direct = core?.[`_${name}`] ?? core?.[name];
if (typeof direct === "function") return direct(...args);
throw new Error(`missing core export: ${name}`);
}
function clone(value) { return structuredClone(value); }
export class SimScheduler {
constructor(core, { budget = DEFAULT_TICK_BUDGET } = {}) {
if (!Number.isSafeInteger(budget) || budget < 1 || budget > DEFAULT_TICK_BUDGET) throw new Error("scheduler_budget_invalid");
this.core = core;
this.budget = budget;
this.tick = 0;
this.last_cycles = 0;
this.phase_trace = [...SCHEDULER_PHASES];
this.coreScheduler = 0;
this.coreSize = 0;
this.ensureCore();
}
ensureCore() {
if (this.coreScheduler || typeof this.core?._sim_scheduler_size !== "function") return;
if (typeof this.core?._malloc !== "function" || typeof this.core?._free !== "function") throw new Error("scheduler_core_memory_unavailable");
this.coreSize = call(this.core, "sim_scheduler_size", "number", [], []);
this.coreScheduler = this.core._malloc(this.coreSize);
if (!this.coreScheduler) throw new Error("scheduler_core_allocation_failed");
const result = call(this.core, "sim_scheduler_init", "number", ["number", "number"], [this.coreScheduler, this.budget]);
if (result !== 0) throw new Error(`scheduler_core_init_${result}`);
}
export() {
return { schema: SCHEDULER_SCHEMA, budget: this.budget, last_cycles: this.last_cycles, phase_trace: [...this.phase_trace], tick: this.tick, state_hash: this.hash() };
}
hash() {
const unsigned = { schema: SCHEDULER_SCHEMA, budget: this.budget, phase_trace: [...this.phase_trace], tick: this.tick };
return fnv1a64(unsigned);
}
step(cycles, callbacks = {}) {
if (!Number.isSafeInteger(cycles) || cycles < 0) throw new Error("scheduler_cycles_invalid");
if (cycles > this.budget) throw new Error("scheduler_budget_exceeded");
if (this.coreScheduler) {
const result = call(this.core, "sim_scheduler_step", "number", ["number", "number"], [this.coreScheduler, cycles]);
if (result !== 0) throw new Error(`scheduler_core_step_${result}`);
}
for (let index = 0; index < cycles; index += 1) {
const nextTick = this.tick + 1;
for (const phase of SCHEDULER_PHASES) {
this.phase_trace = [...SCHEDULER_PHASES];
if (typeof callbacks[phase] === "function") callbacks[phase](nextTick);
}
this.tick = nextTick;
}
this.last_cycles = cycles;
return this.export();
}
restore(value) {
if (!value || value.schema !== SCHEDULER_SCHEMA || !Number.isSafeInteger(value.budget) || value.budget < 1 || value.budget > DEFAULT_TICK_BUDGET || !Number.isSafeInteger(value.tick) || value.tick < 0 || !Number.isSafeInteger(value.last_cycles) || value.last_cycles < 0 || !Array.isArray(value.phase_trace) || value.phase_trace.join("\0") !== SCHEDULER_PHASES.join("\0")) throw new Error("scheduler_restore_invalid");
const unsigned = { schema: SCHEDULER_SCHEMA, budget: value.budget, phase_trace: [...value.phase_trace], tick: value.tick };
if (value.state_hash !== fnv1a64(unsigned)) throw new Error("scheduler_restore_hash");
this.budget = value.budget;
this.tick = value.tick;
this.last_cycles = value.last_cycles;
this.phase_trace = [...value.phase_trace];
this.ensureCore();
if (this.coreScheduler) {
const phases = new Uint32Array(SCHEDULER_PHASES.map((_, index) => index));
const pointer = this.core._malloc(phases.byteLength);
this.core.HEAPU32.set(phases, pointer / 4);
try {
const result = call(this.core, "sim_scheduler_restore", "number", ["number", "number", "number", "number", "number", "number"], [this.coreScheduler, this.tick, this.budget, this.last_cycles, pointer, SCHEDULER_PHASES.length]);
if (result !== 0) throw new Error(`scheduler_core_restore_${result}`);
} finally { this.core._free(pointer); }
}
return this.export();
}
destroy() {
if (this.coreScheduler && typeof this.core?._free === "function") this.core._free(this.coreScheduler);
this.coreScheduler = 0;
}
}