Jobs
Run a function on a pool thread, and pick up its result when the frame needs it.
A job is an exported function marked @job.
Jobs.run runs it on a pool thread, with copies of the arguments
you pass. Each run starts from a fresh state, with every global back at its
initial value, so a job cannot keep state from one run to the next.
A job computes exactly what the main lane would from the same arguments, as
long as it reads no global the main lane has changed. So a test can call a
@job function directly.
import { Layout, Context } from "rive/host";
import { Jobs } from "rive/jobs";
@job export function sum(values: Float64Array): f64 {
let total = 0.0;
for (let i = 0; i < values.length; i++) {
total += values[i];
}
return total;
}
export class Totals extends Layout {
total: f64 = 0;
override init(context: Context): bool {
let values = new Float64Array(3);
values[0] = 1;
values[1] = 2;
values[2] = 3;
let result = Jobs.run(sum, values).join();
if (result != null && result.ok) {
this.total = result.value;
}
return true;
}
}
A @job function must be exported under its own name. It must
not be generic or a generator, and it must return a value. Name the function
directly in each Jobs.run call, so the build can find it. What a
job can reach, and what can cross into it, is on the
Threads page.
Waiting for the result
Jobs.run returns a Job handle at once. The main
lane reads the result through it in one of these ways:
| Call | What it does |
|---|---|
join() | waits for the result, but never past the budget of the call the main lane is in |
joinWithin(seconds) | as join, waiting
at most that many seconds |
Jobs.joinAny(jobs, seconds) | answers the index of a job in the array that is done, or -1 when every one is still running when the wait ends |
peek() | answers without waiting: null until the job is over |
promise() | a promise to await in an
async function, which settles on an advance after the job is over |
join, joinWithin and peek answer a
Settled, or null while the job is still running.
ok says whether the job returned, and value holds
what it returned. When the job failed or was stopped, error says
why.
A join that runs out of time answers null, and the job keeps running on the
pool. So a frame is never held past its budget. A result is decoded once:
peek, join and promise answer the same
outcome from then on, until the handle is released.
The promise fulfills with the job’s result, rejects with why it failed, and is cancelled when the job was stopped. Cancelling the promise stops the job.
Polling across frames
A join makes the frame wait for the job. To never wait, start the job in
one frame and check on it in the next ones. Test running, and once
it turns false, read the result with peek. This job also returns
its result as a Blob, which
Large results explains below.
import { Layout, Context } from "rive/host";
import { Job, Jobs } from "rive/jobs";
import { Blob } from "rive/shared";
@job export function heights(count: i32): Blob {
let values = new Float32Array(count);
for (let i = 0; i < count; i++) {
values[i] = <f32>(i % 64) / 64;
}
// One copy into shared memory; the main lane decodes nothing.
return Blob.fromArray(values);
}
export class Terrain extends Layout {
heights: Float32Array = new Float32Array(4096);
job: Job<Blob>? = null;
override init(context: Context): bool {
this.job = Jobs.run(heights, this.heights.length);
return true;
}
override advance(seconds: f64): bool {
let job = this.job;
// Never waits: checks once a frame until the job is over.
if (job != null && !job.running) {
this.job = null;
let result = job.peek();
job.release();
if (result != null && result.ok) {
let blob = result.value;
blob.readInto(0, this.heights);
blob.release();
}
}
return true;
}
}
An API that hands out results one at a time, such as a reader whose
next() says either that it has another result or that it is done,
needs a third answer once its work moves to a job: not yet. It gives that
answer while the job is still running. The caller then stops
asking until the next frame instead of calling join, so no frame
waits for the pool.
Decoding without allocating
A result that crosses is normally decoded into new objects on the main
lane. joinInto(out) and peekInto(out) decode it into
a value you already hold instead, so nothing is allocated. The value is a
typed array, a StaticArray of numbers or a class of numbers, and
its arrays must be as long as the ones the job returns.
Both answer false while the job is still running, and once it failed or was
stopped; state tells those apart. Unlike peek, each
call that answers true decodes the result again.
import { Layout } from "rive/host";
import { Job, Jobs } from "rive/jobs";
@job export function ramp(count: i32): Float32Array {
let values = new Float32Array(count);
for (let i = 0; i < count; i++) {
values[i] = <f32>i;
}
return values;
}
export class Ramps extends Layout {
values: Float32Array = new Float32Array(1024);
job: Job<Float32Array>? = null;
override advance(seconds: f64): bool {
let job = this.job;
// Decodes into values, which was allocated once.
if (job != null && job.joinInto(this.values)) {
this.job = null;
}
if (this.job == null) {
this.job = Jobs.run(ramp, this.values.length);
}
return true;
}
}
Here joinInto waits for the job within the frame’s
budget. Pass a number of seconds as its second argument to wait less, or use
peekInto to never wait.
What a job costs
A job pays for its start, for copying its arguments in and its result out, and for the wait when it finishes after the main lane needs it. On a desktop, natively, an empty job run and joined costs about a hundredth of a millisecond. A megabyte crossing in or out costs a few tenths of a millisecond. In a browser each costs a few times more.
The main lane encodes the arguments inside Jobs.run, and
decodes the result inside the call that reads it. So both copies cost its
frame. A job that runs every frame pays off when its work is clearly larger
than what crosses. Half a millisecond of work that returns two megabytes is
slower on a job than on the main lane. Several milliseconds of work that
returns the same amount gains.
Two habits make the most of a job:
- Start early and join late. Start the job as soon as its inputs are ready, do the frame’s other work, and join just before the result is used.
- Send less. Pass shared memory or a
Blobrather than large arrays, and decode into a value you keep withjoinInto.
Some values cross cheaply:
- A typed array, an
Arrayof numbers or aStaticArrayof numbers crosses as one block of bytes, never element by element. AStaticArrayalways sends its whole length, though. - A
subarrayview sends only its own bytes. That splits work across jobs without copying the whole array to each. - A
Blobcrosses by id, with no copy at all.
Every other object, string and nested array is decoded one at a time, which adds up for large results. Large data gives the numbers.
To hand every job the same large input, such as a style, publish it once to
a Snapshot large enough for it, and pass the snapshot. Its handle
crosses by id, and each job then decodes the data on its own thread.
Large results as a Blob
A Blob holds bytes in shared memory that never change once
made. A job may create blobs, and no other shared object. A blob crosses by
id, so the main lane decodes nothing when it reads one. It copies just the
range it needs with readInto, as the terrain example above does.
Or it uploads the bytes to the GPU with updateFromBlob, as the
skinning example does.
Build the numbers in a typed array, an Array or a
StaticArray, then return Blob.fromArray(values). That
copies them once, or twice on the web. The terrain example releases the
job’s handle, then the blob once its bytes are copied.
Stopping a job
stop() ends a job early, and it then settles stopped. A job
that has not started never runs. A running one is interrupted natively, and in
a browser only when it has fuel (see
Platform differences).
A long loop in a job can also check Lanes.stopping(), from
rive/lanes, and end early. A job that returns after it was asked
to stop still settles stopped. Releasing the handle, or letting it be
collected, stops the job too. So keep a Job in a field for as long
as you want its result.
For every detail of the API, run rive docs animascript/api/jobs.
The full guide is rive docs animascript/threading.