threads

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.

totals.as
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:

CallWhat 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.

terrain.as
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.

ramps.as
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:

Some values cross cheaply:

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.