Skip to content

caliper.jobs.v1

Service id caliper.jobs.v1 — background compute with progress + cancel (PLATFORM.md §7.5). This page embeds the header verbatim; the docs build fails if the file moves.

#pragma once
/* caliper.jobs.v1 — background compute with progress + cancel (PLATFORM.md
 * §7.5). IMMUTABLE once published: new capability = jobs_v2, alongside.
 *
 * THREADING HONESTY (§15): job functions run on HOST WORKER THREADS as
 * trusted code. They are NOT crash-guarded — the signal guard is
 * UI-thread-only by documented precondition — so a fault in a job takes the
 * process down. Cancellation is cooperative: poll cancelled() in your inner
 * loop and return promptly. */
#include <stdint.h>
#include <stdbool.h>

#define CALIPER_JOBS_V1 "caliper.jobs.v1"

#ifdef __cplusplus
extern "C" {
#endif

typedef struct CaliperJobControl CaliperJobControl;
struct CaliperJobControl {
    uint32_t struct_size;
    /* Poll in loops; return promptly when true. */
    bool (*cancelled)(const CaliperJobControl* ctl);
    /* frac in [0,1]; msg_utf8 may be NULL. Surfaced in the host jobs tray. */
    void (*progress)(const CaliperJobControl* ctl, float frac,
                     const char* msg_utf8);
};

/* Runs on a host worker thread. user must outlive the job. */
typedef void (*CaliperJobFn)(void* user, const CaliperJobControl* ctl);

typedef struct CaliperJobsV1 {
    uint32_t struct_size;
    /* Returns a job id; 0 = error (never a valid id). */
    uint64_t (*submit)(const char* label_utf8, CaliperJobFn fn, void* user);
    void     (*request_cancel)(uint64_t job);
    bool     (*is_running)(uint64_t job);
    float    (*progress_of)(uint64_t job);  /* last reported frac; 0 if none */
} CaliperJobsV1;

#ifdef __cplusplus
}
#endif

Semantics

Threading. Job functions do not run on the frame thread — that is the whole point of the service. You call submit(label, fn, user) from on_frame (or anywhere), and the host runs fn on a host worker thread. Your frame stays fluid while the work grinds. The corollary, stated in the header and not softened: job functions run on host worker threads as trusted code, and they are not crash-guarded — the signal guard is UI-thread-only by documented precondition (see the trust model) — so a fault inside a job takes the whole process down. Keep job code as disciplined as you would keep any code that runs without a net.

Cancellation is cooperative. request_cancel(job) sets a flag; it does not interrupt your function. Your job must poll ctl->cancelled(ctl) in its inner loop and return promptly when it reads true. The framework contract (PLATFORM.md §16) is that a well-behaved job honours cancel within ≤ 100 ms — this is a tested guarantee, not advice, and the exemplar's teardown relies on it (below). A job that ignores cancelled() and runs for a minute is a bug in the applet, not the host.

user must outlive the job. submit takes a raw void* user and the worker dereferences it for the job's entire lifetime. If you pass this (the common case — see the exemplar), then this must not be destroyed while the job is still running. Because the host destroys your applet object right after on_cleanup() returns, an applet with a live job must, in on_cleanup, request cancel and then bounded-wait on is_running(job) before returning — so the worker has exited before destroy() frees the object out from under it. The ≤ 100 ms cancel contract is what makes that wait bounded: a short poll loop (e.g. up to 300 ms) cannot hang teardown.

Ids. submit returns a job id; 0 is never a valid id — it means the submission failed (for example, a headless host that does not vend the service). is_running/progress_of on an unknown or finished id return false/0.

The canonical consumer is the MLScope exemplar (examples/ml_scope/): it submits an MLP training loop, polls cancelled() each epoch, publishes loss under a mutex, and does the bounded-wait teardown described above.