blob: 89e146098a12f29d72d0a4ce7859c1cf9f320c7c [file]
import { Layout, Context } from "rive/host";
import { Parallel, Part, SharedArray } from "rive/parallel";
import { clock } from "rive/os";
// Not part of test.sh: the web stage benchmark, which logs what a parallel
// call costs on the page's team, as script_parallel_test.cpp's timings do
// natively, and how fast a fixed amount of work runs on all of the team.
// Run it on a page with threads and --parallel-parts 8, or the parts to
// compare.
// Each part runs passes over its own 1024 floats, a cache line apart from the
// next part's, then waits for the others.
@parallel export function stagesKernel(part: Part, data: SharedArray<f32>, stages: i32, passes: i32): void {
let start = part.index * 1040;
let end = start + 1024;
for (let s = 0; s < stages; s++) {
for (let p = 0; p < passes; p++) {
let k = <f32>p * 0.001;
for (let i = start; i < end; i++) {
data[i] = data[i] * 0.999 + k;
}
}
part.sync();
}
}
// The same work as one stage of stagesKernel, where the call's end is the
// barrier.
@parallel export function stageCall(part: Part, data: SharedArray<f32>, passes: i32): void {
let start = part.index * 1040;
let end = start + 1024;
for (let p = 0; p < passes; p++) {
let k = <f32>p * 0.001;
for (let i = start; i < end; i++) {
data[i] = data[i] * 0.999 + k;
}
}
}
@parallel export function empty(part: Part, out: SharedArray<f32>): void {}
// Stages of the same work however many parts share it, split evenly.
@parallel export function splitKernel(part: Part, data: SharedArray<f32>, stages: i32, passes: i32): void {
let start = part.start(data.length);
let end = part.end(data.length);
for (let s = 0; s < stages; s++) {
for (let p = 0; p < passes; p++) {
let k = <f32>p * 0.001;
for (let i = start; i < end; i++) {
data[i] = data[i] * 0.999 + k;
}
}
part.sync();
}
}
// The same stages, each part claiming chunks until none are left, so a
// faster core takes more of them.
@parallel export function claimKernel(part: Part, data: SharedArray<f32>, stages: i32, passes: i32): void {
let n = data.length;
for (let s = 0; s < stages; s++) {
for (let i = part.claim(n, 256); i < n; i = part.claim(n, 256)) {
let end = min(i + 256, n);
for (let p = 0; p < passes; p++) {
let k = <f32>p * 0.001;
for (let j = i; j < end; j++) {
data[j] = data[j] * 0.999 + k;
}
}
}
part.sync();
}
}
const kStages = 1000;
const kReps = 5;
function median(times: Array<f64>): f64 {
times.sort((a: f64, b: f64): i32 => a < b ? -1 : a > b ? 1 : 0);
return times[times.length >> 1];
}
// Microseconds per stage when one call syncs between stages.
function syncStageTime(parts: i32, passes: i32): f64 {
let data = new SharedArray<f32>(1040 * parts);
let times = new Array<f64>();
for (let r = 0; r < kReps; r++) {
let start = clock();
Parallel.runUpTo(parts, stagesKernel, data, kStages, passes);
times.push((clock() - start) * 1e6 / <f64>kStages);
}
return median(times);
}
// Microseconds per stage when each stage is its own call.
function callStageTime(parts: i32, passes: i32): f64 {
let data = new SharedArray<f32>(1040 * parts);
let times = new Array<f64>();
for (let r = 0; r < kReps; r++) {
let start = clock();
for (let s = 0; s < kStages; s++) {
Parallel.runParts(parts, stageCall, data, passes);
}
times.push((clock() - start) * 1e6 / <f64>kStages);
}
return median(times);
}
// Passes that make a call on one part take about target microseconds.
function callPasses(target: f64): i32 {
let passes = 1;
let one = callStageTime(1, passes);
while (one < target && passes < 100000) {
passes = <i32>(<f64>passes * target / one) + 1;
one = callStageTime(1, passes);
}
return passes;
}
function stageRows(context: Context, target: f64): void {
let passes = target > 0 ? callPasses(target) : 0;
let one = callStageTime(1, passes);
for (let parts = 2; parts <= 8; parts *= 2) {
let sync = syncStageTime(parts, passes);
context.log("stage " + target.toString() + " parts " + parts.toString() +
" sync us " + sync.toString() +
" calls us " + callStageTime(parts, passes).toString() +
" efficiency " + (one / sync).toString());
}
}
// Microseconds per stage on up to parts parts, claimed or split evenly.
function fixedStageTime(claimed: bool, parts: i32, data: SharedArray<f32>, stages: i32, passes: i32): f64 {
let times = new Array<f64>();
for (let r = 0; r < kReps; r++) {
let start = clock();
if (claimed) {
Parallel.runUpTo(parts, claimKernel, data, stages, passes);
} else {
Parallel.runUpTo(parts, splitKernel, data, stages, passes);
}
times.push((clock() - start) * 1e6 / <f64>stages);
}
return median(times);
}
// Stages of target microseconds on one part, then on every part the page
// gives a call, split evenly and claimed.
function fixedRows(context: Context, target: f64): void {
let data = new SharedArray<f32>(16384);
let stages = max(20, <i32>(20000 / target));
let passes = 1;
let one = fixedStageTime(false, 1, data, stages, passes);
while (one < target && passes < 100000) {
passes = <i32>(<f64>passes * target / one) + 1;
one = fixedStageTime(false, 1, data, stages, passes);
}
let size = Parallel.size();
let split = fixedStageTime(false, size, data, stages, passes);
let claimed = fixedStageTime(true, size, data, stages, passes);
context.log("fixed " + target.toString() + " parts " + size.toString() +
" one us " + one.toString() + " split us " + split.toString() +
" claimed us " + claimed.toString() + " speedup " + (one / split).toString() +
" " + (one / claimed).toString());
}
// Microseconds per call of target microseconds a part, each after main lane
// work long enough that the team has parked, as a frame's calls are.
function parkedRow(context: Context, target: f64): void {
let size = Parallel.size();
let data = new SharedArray<f32>(1040 * size);
let passes = callPasses(target);
let one = callStageTime(1, passes);
let times = new Array<f64>();
for (let r = 0; r < kReps; r++) {
let total = 0.0;
for (let i = 0; i < 200; i++) {
let until = clock() + 0.0005;
while (clock() < until) {}
let start = clock();
Parallel.run(stageCall, data, passes);
total += clock() - start;
}
times.push(total * 1e6 / 200);
}
context.log("parked " + target.toString() + " parts " + size.toString() +
" one us " + one.toString() + " call us " + median(times).toString());
}
export class Main extends Layout {
override init(context: Context): bool {
context.log("bench size " + Parallel.size().toString());
let out = new SharedArray<f32>(1);
for (let parts = 1; parts <= 8; parts *= 2) {
let times = new Array<f64>();
for (let r = 0; r < kReps; r++) {
let start = clock();
for (let i = 0; i < 20000; i++) {
Parallel.runParts(parts, empty, out);
}
times.push((clock() - start) * 1e6 / 20000);
}
context.log("empty parts " + parts.toString() + " us " + median(times).toString());
}
stageRows(context, 0);
stageRows(context, 1);
stageRows(context, 5);
stageRows(context, 10);
fixedRows(context, 50);
fixedRows(context, 200);
fixedRows(context, 1000);
parkedRow(context, 50);
parkedRow(context, 200);
context.log("bench done");
return true;
}
}