Skip to content

Consume live events

Live events let an application show progress while a benchmark runs. Use an owned async context to consume them and retrieve the completed Result from the same operation. This API is available starting in 0.4.0.

import asyncio

from iperf3_lib import Client, ClientConfig


async def main():
    client = Client(ClientConfig(
        server="127.0.0.1", duration=2, interval_seconds=0.25,
    ))
    async with client.events(timeout=10) as stream:
        async for event in stream:
            print(event.kind, event.payload)
        result = await stream.result()
    print(result.ok, result.error)


asyncio.run(main())

The destination needs a listening iperf3 server. For a Python server, use async with server.events_once(timeout=10) as stream to observe one accepted test and obtain its server-side result. The timeout includes worker startup and the wait for a client. Worker readiness alone does not establish that the server socket is listening.

Own the operation

Creating a stream starts no worker. Entering its async context admits the operation and snapshots configuration. A stream has one consumer and cannot be entered again. Keep consumption inside async with; its exit path handles an early break, an application exception, and cancellation, and awaits owned worker cleanup. aclose() provides explicit, idempotent closure. Closure retains the bounded buffered observations and terminal event for subsequent reads; an already completed result remains available through result(). Closing ends native ownership without erasing retained evidence.

The iterator supplies progress; await stream.result() supplies the completed result, including an unsuccessful native result. Native end, error, and complete-document messages are observations. Only the wrapper's terminal event describes the settled operation, after cleanup. An operation exception does not become a successful result merely because native end arrived.

Iteration yields one terminal event and then stops. result() raises an operation exception; context exit also raises an operation error that the consumer has not observed. An early close cancels an unfinished operation without treating that intentional cancellation as an unexpected error. If the context body raises, its exception takes precedence over an ordinary operation error; a cleanup failure takes precedence and retains the earlier cause. Cancelling an iteration or result wait stops the owned operation and waits for cleanup before propagating cancellation. Repeated cancellation cannot abandon that cleanup.

The async execution contract still applies: cancellation requests process termination and waits for cleanup; unconfirmed cleanup raises IperfCleanupError and retains ownership for later reaping. Forced termination cannot prove that native C finalizers ran. A server instance remains unavailable while its owned worker is unreaped.

These methods always use an isolated Python/CFFI worker. Basic synchronous Client.run() calls remain non-reentrant in one process. Wrapping a direct call in an application executor does not add cancellation support.

Interpret progress and final evidence

Typed events distinguish worker state, native start, intervals, native errors, native end, complete documents, server output, unknown or malformed input, delivery gaps, and wrapper terminal status. A native complete document is not an additional interval. Unknown future native kinds retain advisory raw evidence without inventing measurements.

Run identity associates events with the eventual result's worker receipt. Native callback order, transport order, and consumer delivery order are different counters. Callback arrival uses a monotonic offset; native interval start/end values describe measurement time. Arrival time must not be used to derive throughput or packet chronology.

LiveEvent exposes request_id, worker_id, run_index, delivery_sequence, optional capture_sequence, arrival_offset_seconds, and wall-clock received_at_seconds, alongside kind and its typed payload. Wrapper-generated events do not invent native capture or arrival values.

Kind Payload type Meaning
worker_state WorkerStatePayload Starting or ready; no measured traffic claim.
native_start NativeStartPayload Native protocol, method, role, and copied setup evidence.
interval IntervalPayload Raw interval and typed Measurement observations.
native_error NativeErrorPayload Native error evidence; the owner may still be cleaning up.
native_end NativeEndPayload Native summaries; not wrapper completion.
native_document NativeDocumentPayload Complete native document and its projected measurements.
server_output ServerOutputPayload Advisory server output.
unknown UnknownPayload Preserved future native kind and copied evidence.
malformed MalformedPayload Bounded parse or projection diagnostic.
delivery_gap DeliveryGapPayload Dropped count and the stage where loss occurred.
terminal TerminalPayload Settled operation outcome, result availability, native status, and cleanup/capture/delivery evidence.

Import payload types and Measurement from iperf3_lib.events; LiveEvent and EventStream are also available from the package root. A terminal outcome="completed" means the operation returned a result. Check native_status and result.ok to determine whether the native benchmark succeeded.

Interval observations follow the canonical result semantics: flow direction, sender/receiver observation, aggregate/stream scope, and local stream identity remain separate. Missing values and ambiguous association stay unknown. A native socket identifier alone cannot identify a flow across both reporting endpoints. TCP provenance pointers in an interval payload refer to its local normalized fragment; interval indices can differ from those in the final multi-interval result. Compare measured values and associations, not pointer strings alone.

Envelopes and payload records have frozen attributes. Copied JSON evidence is detached from native memory, other deliveries, and the final result; nested JSON containers are ordinary Python values. Persist final evidence with the existing result artifact API. Live events are runtime progress objects, not a new persisted report schema.

Slow consumers and capture quality

The native callback copies a bounded payload and records arrival metadata. JSON parsing, result assembly, framing, and application consumption occur outside that callback. Queues admit bounded item counts and byte totals; excess progress is dropped and counted rather than blocking native execution until an application catches up. Terminal bookkeeping has reserved capacity.

Stage Limit
One copied native payload or full-result getter 8 MiB
Pending native copies 256 items and 16 MiB
Combined serialized retained native evidence 12 MiB
Worker and parent progress queues, each 256 events and 8 MiB
One transported progress event 1 MiB
Async consumer queue 128 events and 2 MiB

The async bridge coalesces loop wakeups, so a slow event loop does not accumulate one scheduled callback per native event. Capture diagnostics retain at most eight samples of 256 characters. These are separate limits: an 8 MiB complete document can supply the final result while being too large for live delivery.

Native JSON parsing rejects nonfinite values and duplicate fields, with one compatibility exception in libiperf 3.19.1, 3.21 and 3.22: a native start record can repeat target_bitrate exactly twice with the same nonnegative integer value. Capture retains that value once. Conflicting values, other duplicate fields, and further repetitions remain invalid. Saved artifact and worker transport decoders keep their strict duplicate-field rules.

Loss at native capture is different from loss while delivering already captured progress. A slow consumer can miss intervals while the independent final result remains complete. Check capture and delivery diagnostics before treating a progress sequence as a complete history.

Native 3.21 and 3.22 supply an independent complete document when full streaming output is available. A retained valid document can recover final evidence despite earlier progress loss. Native 3.19.1 requires reconstruction from retained event envelopes. Missing or malformed reconstruction input produces an unsuccessful, incomplete result even if a native end fragment survived. An original native failure remains a failure.

The established iperf3_lib.native_json extension still identifies reconstructed_events and preserves their original envelopes. Separate capture metadata records limits and data quality. Reconstruction never claims fields absent from the original envelopes. Byte limits account for copied and serialized evidence; they are not a bound on Python or native allocator RSS.

Keep existing callbacks

run(on_event=...), arun(on_event=...), and the server callback methods continue to deliver the four-field NativeEvent. Its received_at_seconds retains its wall-clock meaning, and sequence gaps still expose delivery loss. Typed wrapper state, gap, and terminal events do not enter that legacy callback or alter archived plan-event fields. See callback delivery for callback-error and active-callback cleanup behavior.