Written from a line-by-line source review; deterministic examples from a real run.
Introduction
The timer plugin provides time measurement and script-driven delays: two clocks (monotonic — never moves backward, and wall-clock UTC epoch — for logging), formatted local time, blocking wait, and asynchronous, callback-based one-shot or repeating schedules. It offers two distinct styles: SYNCHRONOUS operations on the main thread (MonotonicMs / EpochMs / NowFormat / SleepMs), and ASYNCHRONOUS scheduling with a background thread (Schedule / ScheduleRepeating / Cancel).
Two clocks: monotonic and epoch
MonotonicMs returns the milliseconds elapsed since program start. Guaranteed MONOTONIC: never moves backward, even if the system clock does (NTP synchronization, manual adjustment). It is the foundation for benchmarks, elapsed-time measurement, and timeouts. EpochMs returns the milliseconds elapsed since 1970-01-01 UTC — it tracks the system wall-clock, so use it for logging, timestamping, and aligning with other systems' time data, but NOT for benchmarking, because it can jump backward.
The background thread
Asynchronous scheduling is implemented by a single-threaded background worker that queues scheduled callbacks and invokes them at the right time. The background thread starts on the first Schedule/ScheduleRepeating call, and stops automatically when the script ends. The callbacks run on the DominScript runtime thread (NOT on the background thread), so they can safely use the script's own data state.
The callback signature
The scheduled callback signature is `callback i32 NAME(ref blob $X)`, where the borrowed buffer holds a TimerEvent struct in little-endian: i64 timerId (the id returned by Schedule), i32 sequence (0 = first shot, 1+ = repeats), i32 _padding (for alignment), i64 scheduledEpochMs (when it *should have* fired), i64 actualEpochMs (when it *actually* fired). The difference between scheduledEpochMs and actualEpochMs gives the real lag — on a loaded system it can be several ms.
Loading the plugin
plugin "../plugins/print/PrintPlugin"; plugin "../plugins/timer/TimerPlugin";
A typical flow — measuring elapsed time
i32 $a;
i32 $b;
$a = timer.MonotonicMs();
// ... operation under measurement ...
$b = timer.MonotonicMs();
printf("elapsed: %d ms\n", $b - $a);A typical flow — repeating callback
callback i32 OnTick(ref blob $X)
{
// ... processing every 100 ms ...
return(1);
}
i64 $tid;
$tid = timer.ScheduleRepeating("OnTick", 100, 100);
// ... later:
timer.Cancel($tid);What to know about every function (the basics)
MonotonicMs never moves backward; EpochMs can (due to NTP synchronization). Choice: benchmark vs log.
NowFormat takes a strftime format (e.g. %Y-%m-%d %H:%M:%S) and returns LOCAL time — not UTC.
SleepMs blocks: the whole script thread waits. For asynchronous waiting, use Schedule.
Schedule and ScheduleRepeating return immediately with a timerId (i64); the callback fires on the main script thread under the background thread's timing.
Cancel returns 1 if the timer was removed from the queue; 0 if it has already fired, the ID doesn't exist, or the callback is currently running.
A negative ms value (SleepMs, Schedule delay) raises a runtime error. ScheduleRepeating requires a positive intervalMs.
An error (stopping the script) is caused by a wrong argument count or type, a negative value, an invalid callback name, and a failed background-thread startup.
How to read the signatures
ms is the time in milliseconds (i32). timerId is a 64-bit identifier returned by Schedule/ScheduleRepeating and accepted by Cancel. callbackName is the textual name of a script-level callback function. The type after the -> arrow is the return type.
Timestamps
Two clocks and a formatted text time: the monotonic clock for elapsed-time measurement, the wall-clock epoch clock for logging, and NowFormat for human-readable timestamps.
timer.MonotonicMs
timer.MonotonicMs() -> int
Returns the milliseconds elapsed since program start. Guaranteed monotonic — never moves backward.
This function takes no arguments.
The elapsed ms (i64).
i32 $a;
i32 $b;
$a = timer.MonotonicMs();
timer.SleepMs(30);
$b = timer.MonotonicMs();
printf("%d\n", ($b - $a >= 30));1
For elapsed-time measurement, benchmarks, frame timing, timeouts. Never use it for time-of-day logging (its zero point is not fixed; it's only program start).
timer.EpochMs
timer.EpochMs() -> int
Returns the milliseconds elapsed since 1970-01-01 UTC (Unix epoch, wall-clock time).
This function takes no arguments.
The epoch ms (i64).
printf("%d\n", (timer.EpochMs() > 1700000000000));1
For timestamping log entries, aligning with other systems' time values, and business logic. Do NOT use it for benchmarking: NTP synchronization or manual adjustment can jump it backward.
timer.NowFormat
timer.NowFormat(format) -> string
Returns the text representation of local time according to the strftime-like format.
| Parameter | Type | Description |
|---|---|---|
| format | string | An strftime format, for example "%Y-%m-%d %H:%M:%S" (year-month-day hour:minute:second). C-standard strftime directives are available. |
The formatted time as a string (LOCAL time, not UTC).
printf("%s\n", timer.NowFormat("%Y-%m-%d %H:%M:%S"));2026-05-27 14:32:51
For human-readable timestamps (in logs, headers, user feedback). The format can be assembled at will; the time zone is the host environment's local zone.
Synchronous sleep
Blocks the whole script thread for a given time. For asynchronous waiting, use Schedule instead.
timer.SleepMs
timer.SleepMs(ms) -> int
Blocks the calling thread for the given milliseconds.
| Parameter | Type | Description |
|---|---|---|
| ms | int | The wait length in milliseconds (non-negative). 0 returns immediately. |
The given ms value. A negative value raises a runtime error.
printf("%d\n", timer.SleepMs(20));20
For frame pacing (e.g. 16 ms for ~60 fps), polling delays, or fixed-interval repetition. For longer waits or event-driven logic, the asynchronous Schedule is more appropriate.
Asynchronous scheduling
Schedules callbacks via a background thread. The callback fires on the main script thread at the scheduled time.
timer.Schedule
timer.Schedule(callbackName, delayMs) -> int
Schedules a single callback fire with the given delay.
| Parameter | Type | Description |
|---|---|---|
| callbackName | string | The name of the script-level callback to call. Signature: `callback i32 NAME(ref blob $X)`. |
| delayMs | int | The delay in milliseconds (non-negative). |
The timerId (positive i64) that you can pass to Cancel. A bad argument or callback name raises a runtime error.
callback i32 OnTick(ref blob $X) { return(1); }
i64 $tid;
$tid = timer.Schedule("OnTick", 50);
printf("%d\n", ($tid > 0));1
For a delayed action (e.g. a notification after 5 s), or a one-shot feedback. The callback fires AFTER delayMs has elapsed; the actual firing time is shown by the difference of scheduledEpochMs and actualEpochMs.
timer.ScheduleRepeating
timer.ScheduleRepeating(callbackName, firstMs, intervalMs) -> int
Schedules a repeating callback series: the first fire after firstMs, then every intervalMs.
| Parameter | Type | Description |
|---|---|---|
| callbackName | string | The name of the script-level callback to call. |
| firstMs | int | The delay of the first fire (non-negative). |
| intervalMs | int | The repeat interval in milliseconds (positive). |
The timerId (positive i64). A negative firstMs or a non-positive intervalMs raises a runtime error.
callback i32 OnTick(ref blob $X) { return(1); }
i64 $tid;
$tid = timer.ScheduleRepeating("OnTick", 30, 30);
// about 3 fires in 100 ms
printf("%d\n", ($tid > 0));1
For periodic work (heartbeat, polling, animation refresh). The `$X.sequence` field of the callback starts at 0 and increments by one per fire — so you can distinguish the first from the later fires.
timer.Cancel
timer.Cancel(timerId) -> int
Attempts to remove a scheduled timer.
| Parameter | Type | Description |
|---|---|---|
| timerId | int | The timer id returned by Schedule or ScheduleRepeating. |
1 if it was removed from the queue; 0 if it has already fired, doesn't exist, or is currently running.
i64 $tid;
$tid = timer.ScheduleRepeating("OnTick", 30, 30);
timer.SleepMs(100);
printf("%d\n", timer.Cancel($tid));1
To stop a repeating callback (worth calling before the end of the main script thread), or to cancel a delayed action. A 0 return is normal even if the timer simply expired.
Practical notes
What the timer plugin is good for
Measuring elapsed time and benchmarking (MonotonicMs).
Timestamping log entries (EpochMs, NowFormat).
Simple synchronous waits (SleepMs) for frame pacing or polling delays.
Asynchronous, callback-based scheduling (Schedule, ScheduleRepeating) — timed events, periodic tasks, heartbeats.
Monotonic vs epoch clock: which to use when?
Two clocks, two purposes. MonotonicMs is the rule for benchmarks, elapsed-time measurements, and timeouts: it never moves backward, so every delta is correct. EpochMs is the wall-clock: log entry timestamps, cross-system time alignment, business date/time operations. Never swap the two: if NTP happens to step the system clock backward, an EpochMs-based benchmark can produce an incorrect negative delta.
Callback signature in detail
The timer callback signature is `callback i32 NAME(ref blob $X)`. After `$X assign TimerEvent;`, every timing metadata is accessible from the callback. `$X.timerId` is the id given by the scheduler (same as the return of Schedule). `$X.sequence` is 0 on the first fire and increments by one per fire — so within a single callback you can distinguish the first from later fires. `$X.scheduledEpochMs` and `$X.actualEpochMs` reveal how much the actual fire lagged behind the planned one.
Sleep vs Schedule
SleepMs BLOCKS on the calling thread, so it is simple and predictable, but the script does nothing else in the meantime. Schedule/ScheduleRepeating return immediately, and the background thread schedules the callbacks — suitable for parallel tasks, periodic background work, and scripts that meanwhile do other things on the main thread. For longer (e.g. >1 s) waits, Schedule is almost always the better choice.
Cancel behavior
A success (1) from Cancel means the timer was in the background thread's queue and was removed. A 0 may mean three things: (a) the timer has already fired (a one-shot Schedule earlier), (b) such a timerId never existed, (c) the callback is firing right at this moment. A 0 return is NOT an error, just a state indicator. For repeating timers, Cancel prevents future fires — a fire already in progress can complete.
Error handling
A wrong argument count or type, a negative ms value, a 0 or negative intervalMs (ScheduleRepeating), an empty or unknown callback name, and a failed background-thread startup are reported by the runtime as a runtime error, and the script stops. On a successful call, both timestamps (MonotonicMs/EpochMs) return a meaningful positive value, SleepMs returns the given ms, Schedule* a positive timerId, and Cancel either 1 or 0.
The image image-loading plugin
Loading BMP and PNG images into an OMVF frame packet — complete function reference