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

[Canonical documentation](https://flowscript.ftw.trade/docs/performance)

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

| Path | What executes | What the application supplies |
|---|---|---|
| Native Rust | `flowscript-core` compiles typed IR; `live::Run` retains its evaluator state | Typed samples, ordering, persistence and an authorized action consumer |
| JavaScript or TypeScript embedding | `@flowscript/core` runs the Rust evaluator in packaged WebAssembly | The host declaration and values passed to a retained `Run`; the public step boundary validates and serializes input and results |
| Opt-in numeric folds | `numericFolds: 'wasm-v1'` specializes supported numeric/boolean fold bodies while the existing run owns state, decisions and budgets | An explicit backend choice; unsupported bodies refuse startup |
| Hosted TypeScript extensions | ftw.trade transforms TypeScript and executes it in isolated QuickJS, itself built to WASM | Host 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](https://flowscript.ftw.trade/docs/embedding) for installation, host declaration and
checkpoint ownership, and the repository's
[numeric fold contract](https://github.com/johnny-emp/flywheel/blob/main/flowscript/docs/numeric-folds.md)
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](https://flowscript.ftw.trade/docs/embedding#install-and-load),
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.

```js
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](https://github.com/johnny-emp/flywheel/blob/main/flowscript/docs/wasm-backend-evaluation.md)
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:

```sh
just flowscript-live-evaluate --output live-baseline.json
just flowscript-live-evaluate --output live-candidate.json --compare live-baseline.json
```

The [harness](https://github.com/johnny-emp/flywheel/blob/main/scripts/flowscript-live-evaluation.ts)
and [versioned fixtures](https://github.com/johnny-emp/flywheel/blob/main/flowscript/fixtures/live-latency.json)
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](https://flowscript.ftw.trade/docs/extending#built-in-learning-and-external-inference)
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 stage | Current implementation and implication |
|---|---|
| Venue observation | `apps/web/lib/live-books.ts`: coherent book admission and freshness provenance |
| Source delivery | `book-delivery.ts` and `source-sockets.ts`: bounded ordered consumers; configured delivery does not alter strategy cadence |
| Flowscript source | `flow-sources.ts` and `automations.ts`: retained WASM evaluation; emitted outputs pass publication authority and Postgres commit |
| Hosted strategy | `strategies.ts`: closed-bar decisions, checkpoint/log/output writes and authorized order dispatch |
| Placement | `runtime-worker.ts` and `project-runners.ts`: existing workers schedule another strategy round a second after completion; dedicated placement is configuration-dependent |
| Execution | `perp-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](https://docs.ftw.trade/strategies/overview)
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.
