Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku BMessageRunner: Periodic Messages Without a Polling Thread

Use Haiku BMessageRunner for looper-based timers, verify initialization, control repetition, and handle queued messages and object lifetime safely.

Haiku’s BMessageRunner asks the system roster to send a BMessage to a BMessenger at a chosen interval. It is a useful timer for UI refreshes, periodic status updates, and bounded background coordination because it delivers work through the target’s message queue instead of requiring a thread whose only job is to sleep and wake up.

That convenience does not make the callback real-time. The runner schedules message delivery; the target looper still has to dequeue and process the message. A busy or blocked looper can handle a timer late, and a burst of repeated messages can become stale work rather than useful updates. Treat the timer as a request to re-evaluate current state, not as a count of elapsed seconds.

Construction, interval, and count

The constructor takes a target messenger, a message, an interval in microseconds, and an optional count. The default count is -1, which means messages continue until the runner is reconfigured or deleted. A positive count bounds how many times the message is sent. The interval applies before the first message and between subsequent messages.

Always check InitCheck() after construction. A runner that failed to register is not a functioning timer. SetInterval() and SetCount() can reconfigure a live runner, while GetInfo() can report its interval and remaining count. If the target or operation is no longer valid, deleting the runner unregisters it; do not leave an infinite runner alive merely because the target may eventually ignore its message.

Attach the target before starting a view timer

A BMessenger identifies a BHandler and its looper. For a view-owned timer, start the runner after the view has attached to its window so the target messenger can resolve the handler in its looper. Keep the runner as an owned member and stop it when the view detaches or is destroyed.

This abbreviated C++ sketch uses a message code dedicated to one view. The clock or progress value should be calculated from current state rather than incremented by one per delivery.

class RefreshView : public BView {
public:
    RefreshView()
        : BView(BRect(0, 0, 200, 40), "refresh", B_FOLLOW_NONE,
                B_WILL_DRAW),
          fRunner(NULL)
    {
    }

    void AttachedToWindow() override
    {
        BView::AttachedToWindow();
        BMessage tick('rfrs');
        fRunner = new BMessageRunner(BMessenger(this), tick, 250000, -1);
        if (fRunner->InitCheck() != B_OK) {
            delete fRunner;
            fRunner = NULL;
        }
    }

    void DetachedFromWindow() override
    {
        delete fRunner;
        fRunner = NULL;
        BView::DetachedFromWindow();
    }

    void MessageReceived(BMessage* message) override
    {
        if (message->what == 'rfrs') {
            Invalidate();
            return;
        }
        BView::MessageReceived(message);
    }

private:
    BMessageRunner* fRunner;
};

The example’s 250,000 microsecond interval is a quarter-second. It is appropriate for a modest visual refresh, not for animation that requires frame-accurate presentation. The handler only requests a redraw; Draw() should read the authoritative model and render the current value. That keeps repeated or late timer messages from accumulating incorrect state.

Timing and backpressure

Use system_time() for elapsed durations and deadlines because it is monotonic; use wall-clock time only when the displayed value is a calendar time. On every timer delivery, compare the current monotonic time with the operation’s actual deadline. If several intervals elapsed while the looper was busy, skip obsolete refresh work and render the latest state once.

Keep MessageReceived() short. A runner does not move slow file I/O or network requests off the target looper. If the handler blocks for a long operation, input, drawing, shutdown, and other messages to that looper are delayed too. Dispatch substantial work to a worker or asynchronous API and send the result back as a separate message.

For multiple independent timers, add a distinct message code or an operation identifier. A single generic timer message can be difficult to distinguish after an operation has been canceled and restarted. Include a generation number when an old queued message must be ignored after a new task replaces the previous one.

The detached StartSending() static method has a different lifetime model from an owned BMessageRunner object. Because there is no object instance whose destructor cancels it, use it only when a deliberately detached schedule is appropriate and its finite count and target lifetime are understood.

The message runner owns a copy of the message supplied at construction; changing or deleting the caller’s original message does not rewrite the schedule. This makes it safe to build a local message and discard it after a successful InitCheck(), but it also means that changing application configuration requires deliberately creating or updating the runner’s schedule. A reply target can be supplied when replies to scheduled messages matter; otherwise the default is the application messenger. Do not add reply handling just to infer that a timer fired: the target message itself is the scheduled event, while a reply is a separate part of the protocol.

Count describes how many sends remain, not how many operations the target completed. The system roster decrements the count as it processes the scheduled send. Delivery may fail or arrive at a target whose application state has changed. Read GetInfo() only as scheduler state, and keep task completion in the operation’s own result path. When a finite runner exhausts its count it becomes unusable for reconfiguration even though InitCheck() can still report that its original initialization succeeded; create a new runner for a later lifecycle.

Deleting or reconfiguring a runner is a scheduling action, not cancellation of the application operation already started by an earlier message. A previously delivered message can already be queued in the target looper. If a refresh is no longer relevant after a view detaches or a document changes, make the handler harmless: track a generation or active-state flag and ignore obsolete message instances. Avoid storing raw pointers in the timer message to objects whose owners can disappear before dispatch.

Choose timer semantics instead of polling blindly

Use a runner for periodic opportunities to observe state, not as a substitute for a deadline engine. For a countdown or progress display, compute the visible value from an absolute monotonic deadline using system_time(); do not add one interval to a counter each time the handler runs. A busy looper can receive messages later than their nominal schedule, and callback duration does not pause or reset the schedule. Recomputing from the deadline naturally skips stale ticks after a pause.

For a timeout on a single operation, a one-shot runner can prompt the owner to check whether the operation has completed. The operation must still check its own completion state before timing out, because completion and timeout messages can race in the queue. Cancel the runner when the owner retires, and make timeout handling idempotent so a late timer cannot undo a successful result.

For an expensive poll, choose a period based on the resource being observed and back off after repeated failures. A timer that checks a network server every few milliseconds does not make the server more reliable; it can create load and pile up stale requests. Prefer event subscriptions when the subsystem supports them, and use a runner only to reconcile or refresh at a bounded cadence.

Test cancellation and overload explicitly

Test an interval shorter than normal handler execution and confirm that the application coalesces or skips stale work rather than building an unbounded queue. Test target destruction, view detach and reattach, a failed InitCheck(), count exhaustion, SetInterval(), and explicit deletion of a runner configured to repeat forever.

Also test a target that stops responding, reply delivery if configured, a late message after cancellation, a runner reconfigured while an event is pending, and a finite runner whose final send fails. Record the interval, remaining count, target validity, looper backlog, and handler duration in diagnostics. Do not infer a precise timer jitter bound from an idle machine; measure on the supported Haiku build and under realistic load if timing quality matters.

A timer is correct when it delivers a useful opportunity to inspect state and can be canceled with its owner. It is not correct merely because a callback appears at the requested average rate on an idle test machine.

Related:

Sources:

Comments