threads icon indicating copy to clipboard operation
threads copied to clipboard

Shouldn't this also be paired with a monotonic time opcode?

Open dead-claudia opened this issue 8 months ago • 9 comments

Sleep timers are normally batched for performance, with durations turned into deadlines, and effective batching requires tracking a monotonic clock to know precisely how long to wait for. In fact, processors normally mandate such batching at the operating system level. And this primitive is implemented on every platform, from embedded CPUs to high-performance operating systems, as it's a strict necessity.

Here's the API on various relevant platforms:

  • Operating systems:
    • Most Unix-like: clock_gettime(CLOCK_MONOTONIC, &tv);
    • Linux: clock_gettime(CLOCK_BOOTTIME, &tv);
    • Windows: QueryInterruptTime(&ticks);
  • The ACPI timer can be used as a clock reference even on older desktops.
  • Processors:
    • x86, x86-64: HPET, rdtsc * base frequency on modern processors
    • ARM: Generic Timer system register
    • RISC-V: memory-mapped mtime
    • AVR doesn't natively, but real-time clock peripherals are pervasive, and Arduinos have them built-in.
    • Others also have their various ways - just look at the Linux VDSO clock_gettime implementations for various architectures.

I propose something like an atomic.clock.get that returns an i64 nanosecond count, to align with the existing wait opcodes.

dead-claudia avatar May 04 '25 04:05 dead-claudia

I think in a virtual system like Wasm, monotonic time sounds more like an API than an instruction. Indeed in JS we have Performance.now() which is monotonic, and it looks like wasi-clocks is what you'd want elsewhere.

dschuff avatar May 05 '25 16:05 dschuff

@dschuff Similar could be said about 80% of https://github.com/WebAssembly/shared-everything-threads and 100% of https://github.com/WebAssembly/stringref, by the way.

Not sure where the line should be drawn with that, if it shouldn't be drawn before either of those.

dead-claudia avatar May 05 '25 17:05 dead-claudia

Another potential angle here is that threads with shared memory can already be used to build timers: spawn a thread and have it spin incrementing an integer value in a shared memory location. Defining an instruction doesn't add anything you can't already do; it'd just be much more efficient, less noisy (spectre mitigations aside), and more portable (if we chose to specify the clock resolution).

sunfishcode avatar May 05 '25 18:05 sunfishcode

@sunfishcode Is that supposed to be a point for the idea or a point against the idea? Or is it just a general remark? 😕

dead-claudia avatar May 05 '25 20:05 dead-claudia

Notably we have (so far) drawn the line before stringref by shipping https://github.com/WebAssembly/js-string-builtins instead :) Shared-everything threads is modeled after this initial threads proposal in what it provides in the language and what it requires to be imported APIs.

tlively avatar May 05 '25 21:05 tlively

I'll also point out that timed conditional waits and notifies for those waiters have traditionally not been implemented at the bytecode level, in language VMs or in hardware ISAs. Language VMs almost always rely on library calls, while hardware normally exposes either a tick counter + value to send an interrupt (like RISC-V) or a number of ticks before interrupt (like ARM). The BEAM VM is a rare exception, but even then, it's a timeout to receive a value, not a timeout to detect a change.

Language VMs also typically implement monotonic timestamp access as functions, while hardware ISAs implement them natively as either internal registers or memory-mapped registers.

dead-claudia avatar May 05 '25 21:05 dead-claudia

@dead-claudia My comment above sketches a possible argument for adding a new instruction: by arguing that it's useful while perhaps not adding a new capability. I hypothesize that "does it add a capability?" is ultimately the main concrete question underneath the question "does it sound like a library API?".

That said, an argument against adding a new instruction is that if it defines the result to represent milliseconds or any other fixed quantity, it would introduce the first physical unit to the core spec shared by all Wasm users. That's not something to do lightly. For example, I expect 1 millisecond is too coarse-grained for some use cases, while not all devices support nanoseconds.

As another argument against, consider a Wasm engine that can take a snapshot of a running Wasm instance and save it to a Wasm module that can later be instantiated to resume execution. Such an engine wouldn't be able to ensure monotonicity across a suspend/resume in the presence of a monotonic clock instruction. Perhaps that could be addressed by adding a second instruction that increments the monotonic timer to a specified value, though that would add more complexity.

sunfishcode avatar May 06 '25 01:05 sunfishcode

That said, an argument against adding a new instruction is that if it defines the result to represent milliseconds or any other fixed quantity, it would introduce the first physical unit to the core spec shared by all Wasm users. That's not something to do lightly. For example, I expect 1 millisecond is too coarse-grained for some use cases, while not all devices support nanoseconds.

@sunfishcode Sorry, I meant nanoseconds. Intent was first to align with the wait opcodes, and any further justification was intended to just build on top of that. Fixed the proposal in this issue to reflect that.

dead-claudia avatar May 06 '25 03:05 dead-claudia

Ah, I had missed that the wait opcode already has a timeout in nanoseconds. That's indeed a precedent for using nanoseconds here.

sunfishcode avatar May 15 '25 16:05 sunfishcode