Skip to content

Portable result artifacts

Artifact schema v1 was first published with iperf3-lib 0.3.0. Schema versions are independent of package versions. Incompatible changes to canonical fields, units or interpretation require a new schema version. Historical unreleased development snapshots are separate from this published contract.

Use a versioned artifact when saving a benchmark for later analysis or sharing it with another application. Result.to_dict() remains an unversioned dataclass snapshot; it does not acquire a durable format implicitly.

Save and read a result

from pathlib import Path

from iperf3_lib.artifacts import (
    artifact_from_result,
    dumps_artifact,
    loads_artifact,
)

artifact = artifact_from_result(result)
Path("benchmark.json").write_text(dumps_artifact(artifact, indent=2), encoding="utf-8")

loaded = loads_artifact(Path("benchmark.json").read_text(encoding="utf-8"))
assert loaded.result.raw == result.raw
print(loaded.producer.version, loaded.result.execution.status)

These conversion functions do not run a benchmark, load libiperf, probe the current machine, or reparse the original native JSON. Reading an archive preserves the normalized measurements and the producer recorded by its writer. File handling belongs to the application.

The envelope contains kind="iperf3-lib.result", schema_version=1, producer, result, and extensions. The writer emits every canonical field, including explicit nulls and empty collections. Both artifact_to_dict() and artifact_from_dict() support applications that already handle JSON transport.

Execution and evidence

result.execution.status separates completed, failed, and incomplete execution. result.ok is true only for completed execution. A completed run can still lack individual measurements; diagnostics describe those gaps. Performance acceptance is a separate application decision.

execution.configuration.requested records the admitted, resolved ClientConfig. When rate intent or protocol defaults derive a native rate, the original caller config, intent, and allocation remain in the iperf3_lib.rate_intent result extension. See rate resolution. Each run validates and executes a detached snapshot. Mutating the caller's configuration during execution cannot alter that snapshot. effective contains independently returned native settings, each with a state, value, and evidence_paths pointing to native JSON. A successful setter call alone does not establish an effective value. Unavailable and unsupported settings remain explicitly unavailable; they are never copied from the request and called verified.

Timing fields distinguish:

Field in execution.timing Meaning
started_at_seconds, completed_at_seconds Wrapper-observed Unix events.
elapsed_seconds Operation duration measured with a monotonic clock.
native_started_at_seconds Timestamp reported by native JSON.
requested_duration_seconds Configured test duration, not observed elapsed time.
estimated_completed_at_seconds Explicitly inferred start-plus-duration estimate, when available.

An imported native document cannot establish actual completion from requested duration. Its estimate never becomes a Prometheus completion or freshness metric. Native version/system information and live Python/platform metadata describe the execution environment; an importer does not replace them with its own environment.

For compatibility, Result.duration_seconds retains the duration reported in native test configuration. A live wrapper's timing.requested_duration_seconds records its admitted request. If the native setting differs, both values and a configuration diagnostic are preserved; neither is observed elapsed time.

Measurements and aggregation

Flows retain separate sender and receiver observations. Stream summaries retain local socket identities; these identifiers do not identify the same connection across different runs or reporting endpoints.

Intervals explicitly declare scope as aggregate, stream, or unknown. Scope comes from the native container, even if a stream has no socket ID. Bytes, native elapsed seconds, boundaries, bitrate, packet counts, and omitted warm-up state remain separate optional fields. Never add aggregate and component-stream measurements together, or add both endpoint observations of the same transfer.

Native UDP per-stream end summaries can mix sender throughput with receiver loss/jitter evidence. Such records are retained as StreamStats.unattributed with a diagnostic, rather than falsely assigning the whole object to one endpoint. Attributed aggregate summaries and interval observations remain available for calculations.

Field units are fixed by the schema: bytes and counts are integers, duration and timing fields use seconds, bitrate uses bits/s, jitter uses milliseconds, and lost_percent uses percent. The exporter converts to its documented base units. Missing fields are null; measured zeros stay zero. availability records explicit absent, unsupported, malformed, or unknown states using canonical JSON Pointer paths. Missing native fields alone do not prove that a feature is unsupported.

On the qualified libiperf 3.19.1 and 3.21 producers, SCTP retransmission fields are unavailable: native output can contain values without retransmission measurement support, including nonnegative values. The normalized field is null with unsupported-state evidence; the original value remains in raw. For other or unidentified SCTP producers, measurement support stays unknown and the normalized field remains null until that producer is qualified.

Diagnostics carry stable code, severity, optional canonical path, and native/canonical evidence_paths. Captured native data stays in raw, including partial and failed-run evidence. Ordinary JSON capture preserves the parsed native document. Native 3.21 streaming requests full final JSON; 3.19.1 instead records an explicit reconstruction from event envelopes. In that case, extensions["iperf3_lib.native_json"] retains {"representation": "reconstructed_events", "events": [...]}, and the execution.reconstructed_json diagnostic points to /extensions/iperf3_lib.native_json/events. This extension is absent when a full native document was captured. Artifact roundtrips preserve the representation and envelopes without promoting reconstruction to a full original document.

Bounded live-callback delivery does not limit the size of retained native intervals or artifact evidence. Failed and partial streaming captures keep their actual evidence rather than manufacturing unreported native metadata.

Validation and evolution

The reader and writer validate types and cross-field consistency, including mutable manually constructed dataclasses. They reject unknown canonical fields, unsupported schema versions, invalid enum values, duplicate JSON keys, non-finite numbers, booleans substituted for numbers, invalid counts, and contradictory outcomes or compatibility projections.

ArtifactValidationError is a ValueError with a path identifying the bad data. UnsupportedArtifactVersion identifies a schema version the reader cannot interpret. Upgrade the reader or use an explicit migration; do not discard unknown fields and call the result a lossless import.

Arbitrary strict JSON remains permitted in raw and namespaced extensions. Unknown diagnostic codes are retained as advisory information. Decoding and encoding preserve producer metadata, raw data, and extensions semantically; JSON whitespace and number spelling are not preserved byte for byte.

Changing canonical fields, units, or interpretation requires a new schema version under this strict policy. Released schema readers remain supported; migrations are explicit and preserve evidence.

Migrating unversioned snapshots

Continue using to_dict() for short-lived application snapshots. To archive an existing development snapshot, use artifact_from_legacy_dict() explicitly; the normal artifact reader rejects unversioned dictionaries with a migration hint. The adapter preserves known fields and original JSON, rejects unfamiliar fields, and adds compatibility diagnostics for missing metadata.

A legacy missing socket ID does not prove aggregate scope. A legacy inferred completion timestamp does not prove an observed completion event. Those uncertainties remain explicit after migration. The Pydantic models published in 0.2.0 and arbitrary third-party dictionaries are not silently treated as this development snapshot format.

TCP/CPU fields in schema v1

The published v1 contract includes optional TCP evidence on interval and stream sender statistics, and an endpoint CPU collection on Result. The analysis guide specifies units, qualification and attribution. Strict readers validate protocol/scope/endpoint consistency and require an existing raw or namespaced receipt for every present measurement. CPU percentages may exceed 100%; windows/counts remain nonnegative integers and RTT remains finite nonnegative seconds.

Retained development fixtures gained only explicit tcp: null and cpu: [] fields. Hash regressions ensure all previous measurements, raw JSON, diagnostics and producer metadata remain unchanged. A separate fixture records newly normalized evidence. Loading earlier archives never reparses or backfills them.