Skip to content
ƒ FlowscriptDocs
ftw.trade docs ↗
Documentation
Build with it / Flowscript

Performance and latency

Choose native or WASM embedding, measure retained evaluation and execution latency, bound memory and queues, and account for ML training and host scheduling.

Retain a compiled program and incremental run, feed observations at their recorded availability and schedule required clock watermarks. Measure the complete path that matters to your application. Evaluator speed, data freshness, time to submit an order and time to settle it are separate measurements. A fast backtest or arithmetic kernel does not establish a live execution latency bound.

Choose the execution path#

PathWhat executesWhat the application supplies
Native Rustflowscript-core compiles typed IR; live::Run retains its evaluator stateTyped samples, ordering, persistence and an authorized action consumer
JavaScript or TypeScript embedding@flowscript/core runs the Rust evaluator in packaged WebAssemblyThe host declaration and values passed to a retained Run; the public step boundary validates and serializes input and results
Opt-in numeric foldsnumericFolds: 'wasm-v1' specializes supported numeric/boolean fold bodies while the existing run owns state, decisions and budgetsAn explicit backend choice; unsupported bodies refuse startup
Hosted TypeScript extensionsftw.trade transforms TypeScript and executes it in isolated QuickJS, itself built to WASMHost calls for IO, persistence and actions, with CPU/memory/wall limits

Rust and JavaScript use the same language core. The native Rust route avoids the npm JSON/UTF-8 boundary; it is not a promise of allocation-free evaluation. Building the evaluator or QuickJS to WASM does not compile each user's whole program into a specialized machine-code loop. Numeric fold compilation has a deliberately narrower contract; it leaves other expressions and action handling under the existing runtime. It never changes backend after an execution failure.

See Embedding for installation, host declaration and checkpoint ownership, and the repository's numeric fold contract for supported bodies, admission and recovery. No public npm release is implied by these source-checkout workflows.

Keep the live path incremental#

Compile and load model artifacts before processing observations. Warm the run once, retain it, then consume only inputs after its saved cursor. The host must also schedule clock/deadline watermarks the program requires: run.step({}, time) can advance evaluation time while holding prior source values when no new sample arrives. Retain the selected libraries, language version, host configuration and backend choice with the program. Compilation, module instantiation and warmup belong in startup measurements, not hidden inside a reported hot-step number.

run.snapshot() serializes retained state; it does not persist an input cursor or create an atomic storage commit. Save checkpoint and consumed input position together through your host's storage seam. Measure snapshot size, encoding time, commit time and recovery separately. An invalid checkpoint or runtime refusal must remain visible; do not substitute values, replay effects or silently start a fresh run.

Bound windows, tape buffers, record/array width, keys, imported libraries, live runs and output volume before admission. Profile retained bytes, temporary allocations, garbage collection, checkpoint bytes and queue high-water under realistic fan-out. A logical work quota is a deterministic resource bound, not a wall-clock deadline. WASM linear-memory capacity is a high-water allocation, not a measure of current live heap or a per-step allocation count. Native and WASM paths both retain the language's work and memory checks.

Measure a retained public step#

After building and installing the package, run this JavaScript with Node 22 or later. It uses illustrated minute-bar prices, declares no actions and sends nothing. Inputs and timing storage are prepared before measurement. Each duration covers the public Run.step call, including its validation, encoding, evaluation and result construction.

TypeScript
import { loadFlowscript } from '@flowscript/core';

const fs = await loadFlowscript();
const host = {
  sources: [{ name: 'bars.close', type: 'number', keyed: true, intervals: ['1m'] }],
  capabilities: [],
};
const source = `flowscript "0.3";
uses { bars: "^1" }
const close = bars.close("DEMO-USD", "1m");
metric("average", ema(close, 20));`;
const compileAt = performance.now();
const compiled = fs.compile(source, host);
const compileMs = performance.now() - compileAt;
if (!compiled.ok) throw new Error(JSON.stringify(compiled.problems));

const startAt = performance.now();
const started = fs.run(compiled.program);
const startMs = performance.now() - startAt;
if (!started.ok) {
  fs.drop(compiled.program);
  throw new Error(started.error);
}
const run = started.run;
try {
  const warmup = 128;
  const count = 4096;
  const samples = Array.from({ length: warmup + count }, (_, i) => ({
    values: { 'bars.close': 100 + Math.sin(i / 17) },
    time: 1700000000 + i * 60,
  }));
  const times = new Float64Array(count);
  for (let i = 0; i < warmup; i++) {
    const step = run.step(samples[i].values, samples[i].time);
    if (!step.ok) throw new Error(step.error);
  }
  let checksum = 0;
  for (let i = 0; i < count; i++) {
    const sample = samples[warmup + i];
    const before = performance.now();
    const step = run.step(sample.values, sample.time);
    times[i] = (performance.now() - before) * 1000;
    if (!step.ok) throw new Error(step.error);
    if (!Number.isFinite(step.metrics.average)) throw new Error('average is unknown');
    checksum += step.metrics.average;
  }
  const snapshotAt = performance.now();
  const saved = run.snapshot();
  const snapshotUs = (performance.now() - snapshotAt) * 1000;
  if (!saved.ok) throw new Error(saved.error);
  times.sort();
  const percentile = q => times[Math.floor((times.length - 1) * q)];
  console.log({
    samples: count, compileMs, startMs,
    p50Us: percentile(0.5), p95Us: percentile(0.95), p99Us: percentile(0.99),
    maxUs: times[times.length - 1], snapshotUs,
    checkpointBytes: new TextEncoder().encode(saved.snapshot).byteLength,
    checksum,
  });
} finally {
  run.drop();
  fs.drop(compiled.program);
}

This is a starting diagnostic, not a service benchmark or a trading strategy. Timer overhead and host scheduling affect its samples. It excludes collection, inference, database commits, signer calls, network submission and settlement. Do not copy its local p99 into a production promise.

For comparisons, predeclare the workload and supported operations, use identical inputs, alternate lane order across repeated rounds, and check complete outputs and restored continuation before interpreting speed. Compare retained native core, native typed wrapper, raw ABI and public WASM calls separately. Raw ABI timing omits public validation and is not a recommended production API. Report code/module hashes, build profiles, machine/runtime versions, sample counts, warmup, snapshot frequency and distributions. Measure allocation counters in a separate pass because the counters change timing.

From an authorized source checkout, existing repository measurements include just flowscript-wasm-evaluate for public/raw ABI and numeric study batches, and just flowscript-numeric-fold-evaluate for complete public live steps with the explicitly selected fold backend. Their recorded scope and limitations distinguish batch throughput from single-observation latency. These commands require installed development dependencies and the Rust WASM target; they start no venue connection or signer.

Compare native core, typed native and public WASM#

The live harness builds release native latency and allocation executables plus the actual WASM module, then runs four shared finite fixtures:

Terminal
just flowscript-live-evaluate --output live-baseline.json
just flowscript-live-evaluate --output live-candidate.json --compare live-baseline.json

The harness and versioned fixtures check expanded inputs, complete outputs, checkpoints, restored suffixes and logical work across the native core, typed native facade and public WASM lanes. --compare requires matching fixture/input/output/checkpoint/work identities and nonincreasing native allocation calls and requested bytes. Timing remains descriptive unless an explicit timing gate is requested.

To enforce p99 limits, supply --timing-budget machine-budget.json with --compare. Set FLOWSCRIPT_BENCH_MACHINE to the budget's declared machine class when generating both baseline and candidate. The budget names fixture and lane p99 maxima in nanoseconds; unknown fixtures/lanes, missing comparison or mismatched machine classes refuse. Choose limits for controlled hardware, build and load conditions rather than treating a shared-machine observation as a portable guarantee.

Allocation counting runs separately and covers native System allocator requests; it does not count JavaScript/WASM allocations or establish an RSS limit. Native hot-step timing excludes destruction of the returned step; throughput and allocation passes include it, while public WASM garbage collection is runtime-controlled. Native lanes alternate within their rounds; WASM runs afterward. Cold compilation/initialization boundaries differ between lanes. These are useful retained-path diagnostics, not an isolated attribution of native-versus-WASM overhead, production arrival capacity or execution latency.

Clocks, cadence and availability#

An application decides when to feed the evaluator. Run.step consumes a completed observation/bar; it does not subscribe, poll or schedule itself. Run.forming supports explicitly declared forming inputs and intrabar triggers. It has different semantics from a closed-bar replay, which reports that behavior as unmodelled. It is not an instruction to bypass bar construction or act on every raw exchange packet.

Run.step and Run.forming evaluation timestamps use whole seconds; duration helpers express seconds. They are not a precision timer or a submillisecond scheduling contract. Preserve source timestamps and observed availability separately in typed fields when your host needs finer precision. Use a monotonic host clock for local elapsed durations; do not substitute it for replayable event time or compare unsynchronized machines' monotonic clocks.

A delayed observation becomes usable when the consumer actually receives or selects it. Replay at that availability point, retaining the older source and collector receipt times for freshness checks. Backdating a queued observation or prediction to its source timestamp introduces lookahead. Declared freshness, ordering, gaps and explicit unknown observations are part of correctness, not optional latency overhead.

ML on a latency budget#

Built-in ml.* models train and predict synchronously in the evaluator. Fitting is bounded and charged before it runs; it can still make a retraining step much slower than an ordinary prediction. Measure first fit, scheduled/conditional refits and steady predictions separately, with the actual model, feature count, window and history. Smaller windows or less frequent retraining change the model's behavior and need strategy validation as well as timing evidence.

For an external Python, ONNX, GPU or remote model, load/train it in an application-owned backend. Feed recorded predictions as typed source inputs with model/feature identity, input as-of time and prediction availability time. Replay those outputs instead of rerunning inference. Declare bounds for pending requests, inference latency and stale results; an unavailable score stays explicitly unknown. The extension guide provides a runnable illustrative adapter, not a production inference or model quality claim. There is no arbitrary model-runtime import inside Flowscript.

Measure submission and settlement separately#

For a live execution SLO, correlate one admitted observation with its decision and exact order/request identity. Record queue wait, evaluation, checkpoint and intent commit, risk checks, signer RPC, submission, acknowledgement and terminal outcome. Separate local monotonic durations from venue clocks, and separate acknowledgement from a fill or confirmed transaction. Measure p50/p95/p99/max under steady traffic, bursts, many readers, model refits and slow dependencies. Include refusal rates, maximum queue age and pending bytes; a fast accepted sample alone can conceal overload.

Set capacity and tail-latency limits for the chosen deployment. Apply explicit backpressure, sampling or refusal when it cannot keep up; never silently drop events from a continuity-dependent model or trade an old signal as fresh. Single-writer fences, durable input/intent identity, risk invariants, grant checks, separate signing, in-flight exclusivity and outcome reconciliation remain required by the execution host. A timeout does not prove cancellation or release unresolved money. A compiler optimization creates no new authority.

ftw.trade host path#

The language library and ftw.trade's hosted scheduling have different contracts:

Hosted stageCurrent implementation and implication
Venue observationapps/web/lib/live-books.ts: coherent book admission and freshness provenance
Source deliverybook-delivery.ts and source-sockets.ts: bounded ordered consumers; configured delivery does not alter strategy cadence
Flowscript sourceflow-sources.ts and automations.ts: retained WASM evaluation; emitted outputs pass publication authority and Postgres commit
Hosted strategystrategies.ts: closed-bar decisions, checkpoint/log/output writes and authorized order dispatch
Placementruntime-worker.ts and project-runners.ts: existing workers schedule another strategy round a second after completion; dedicated placement is configuration-dependent
Executionperp-moves.ts, perps-live.ts and the separate signer: intent/risk checks, preparation, signing, venue submission and authoritative outcomes

The repository also contains a native platform/worker → platform/host watermark path over the same language core. Its existence does not establish that ftw.trade's hosted book consumers use it, that a dedicated placement is active, or that current live strategies have an event-by-event execution SLO. Use the platform's strategy guide for its actual supported cadence and execution workflow. Faster source delivery or numeric folds alone cannot remove closed-bar waits, persistence, signer round trips or venue settlement.