Work Queue
A functional FrontPanel application has several moving parts: a UI layer reacting to user input, application software coordinating behavior, gateware running on the FPGA, and the hardware itself responding in real time. A button handler that works perfectly in isolation can misbehave once it’s running alongside other handlers, background polling, or rapid user input — and the gateware on the other end has its own expectations about the order and atomicity of the operations it receives. These are the kinds of issues that don’t show up in a quick prototype but surface as soon as the application is doing real work. Understanding why is worthwhile before reaching for a solution.
When a browser-based application talks to a hardware device, three things push you toward serializing device access:
- The transport is sequential. USB transactions to a given device are inherently ordered and atomic. The FrontPanel SDK is non-reentrant per device — you cannot have two in-flight transactions racing on the same connection. Concurrent calls from JavaScript must be serialized somewhere; the only question is where.
- The UI framework is asynchronous and event-driven. React (and browser JavaScript generally) is single-threaded but heavily async. UI events — clicks, input changes, timers, WebSocket messages — fire independently and each handler can
await. Two handlers that each perform a multi-step device operation can interleave theirawaitpoints, even though no two lines of JavaScript run at the same instant. GUI applications have dealt with this for decades by funneling work through a queue; the same pattern applies here. - Gateware operations are often transactional. A read-modify-write of a control register, a configure-then-trigger sequence, or a command-then-read-status exchange only makes sense as an indivisible unit. If another operation slips in between the steps, the result is wrong — not because any individual API call failed, but because the transaction was broken.
A work queue addresses all three and provides a mechanism for serializing operations for other system-level purposes, should they arise. By posting each multi-step operation as a single ticket, you guarantee that its steps run to completion before the next ticket starts. The transport stays happy, UI events compose cleanly, and your gateware sees the transactions it expects.
Problem Example
Note that these examples aren’t technically FrontPanel API calls. They’ve been modified to use a generic dataport.
Consider a register on the FPGA where the low bit is a run/stop control and the upper bits are a configuration field. A UI handler updates the configuration like this:
const onConfigChange = async (newConfig) => {
const value = await dataport.readRegister(REG_CTRL);
const updated = (value & 0x01) | (newConfig << 1);
await dataport.writeRegister(REG_CTRL, updated);
};Code language: JavaScript (javascript)This is a read-modify-write: the handler reads the current value, modifies the configuration bits while preserving the run/stop bit, and writes it back. In isolation, it’s correct.
Now suppose a separate handler toggles the run/stop bit when the user clicks a button:
const onRunToggle = async () => {
const value = await dataport.readRegister(REG_CTRL);
const updated = value ^ 0x01;
await dataport.writeRegister(REG_CTRL, updated);
};Code language: JavaScript (javascript)Each handler is fine on its own. The problem appears when both fire close together. Because each await yields control back to the event loop, the steps can interleave on the wire:
onConfigChange: read REG_CTRL → 0x04
onRunToggle: read REG_CTRL → 0x04
onConfigChange: write REG_CTRL ← 0x06 (new config, run bit from stale read)
onRunToggle: write REG_CTRL ← 0x05 (toggled run, old config from stale read)Code language: plaintext (plaintext)Both reads see the same starting value, and the second write clobbers the first. The configuration change is lost. This is a classic lost-update race, and no amount of care inside either handler can prevent it — the handlers don’t know about each other.
Solution: Use the Work Queue
The Work Queue serializes multi-step operations so that each one runs to completion before the next begins. Wrap each transaction in a ticket and post it:
const onConfigChange = async (newConfig) => {
workQueue.post(async () => {
const value = await dataport.readRegister(REG_CTRL);
const updated = (value & 0x01) | (newConfig << 1);
await dataport.writeRegister(REG_CTRL, updated);
});
};
const onRunToggle = async () => {
workQueue.post(async () => {
const value = await dataport.readRegister(REG_CTRL);
const updated = value ^ 0x01;
await dataport.writeRegister(REG_CTRL, updated);
});
};Code language: JavaScript (javascript)Now whichever handler posts first runs its read and write to completion before the other handler’s ticket begins. The lost update can no longer occur.
Awaiting Completion
post(task) returns a Promise<void> that resolves when the task completes. Awaiting it lets the caller chain follow-up work that depends on the device transaction finishing:
const onResetClick = async () => {
setStatus("Resetting...");
await workQueue.post(async () => {
await dataport.writeRegister(REG_CTRL, 0x01);
await new Promise((resolve) => setTimeout(resolve, 50));
await dataport.writeRegister(REG_CTRL, 0x00);
});
setStatus("Reset complete.");
};Code language: JavaScript (javascript)The status messages bracket the device work: the second setStatus does not run until the queued task has fully completed.
Potential Pitfalls
Overloading the WorkQueue
Issue: Placing too many tickets into the Work Queue can overwhelm the device (meaning that work tickets are arriving faster than the device can keep up), leading to performance degradation (particularly increasing latency).
Solution: Monitor and manage the number of tasks being queued to ensure the device can keep up.
Mixed Access Methods
Issue: Mixing work-queue-managed instructions with non-work-queue-managed device accesses can lead to instruction interleaving.
Solution: Ensure all device instructions are routed through the work queue to maintain atomicity.
Use the Same Instance of Work Queue
Issue: Related to the previous issue, if you post to two separate Work Queues, the tickets may still interleave.
Solution: Use the same instance of the Work Queue to ensure serialized execution of tasks.
Customizing the WorkQueue
The WorkQueue API is intentionally minimal. Once a task is posted, it will run — there is no method to cancel a pending task, clear the queue, or query its length. If a feature you are building needs a more complex implementation that supports things like cancellation or priority scheduling, you can extend the WorkQueue provided in the Platform API or introduce your own.